verify-hosted-task-loop-read-smoke.mjs
224 lines 7.4 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 10 days ago
1 #!/usr/bin/env node
2 /**
3 * Phase 2G-c — hosted Hub task loop read + loop-pass-audit mirror smoke.
4 *
5 * Prerequisites:
6 * - Hub or gateway with JWT auth (self-hosted hub/server.mjs or api.knowtation.store)
7 * - Vault with viewer+ role
8 *
9 * Usage:
10 * KNOWTATION_HUB_TOKEN='<jwt>' KNOWTATION_HUB_VAULT_ID=default \
11 * node scripts/verify-hosted-task-loop-read-smoke.mjs
12 *
13 * Optional mirror append (requires LOOP_PASS_AUDIT_MIRROR_ENABLED=1 on Hub/bridge):
14 * LOOP_PASS_AUDIT_MIRROR_ENABLED=1 node scripts/verify-hosted-task-loop-read-smoke.mjs --mirror-append
15 */
16
17 import fs from 'node:fs';
18 import path from 'node:path';
19 import { fileURLToPath } from 'node:url';
20 import dotenv from 'dotenv';
21
22 const __dirname = path.dirname(fileURLToPath(import.meta.url));
23 const repoRoot = path.resolve(__dirname, '..');
24 dotenv.config({ path: path.join(repoRoot, '.env') });
25
26 function resolveToken() {
27 let t = process.env.KNOWTATION_HUB_TOKEN || process.env.HUB_JWT || '';
28 const fp = (process.env.KNOWTATION_HUB_TOKEN_FILE || '').trim();
29 if (!t && fp) {
30 const expanded = fp.startsWith('~') ? path.join(process.env.HOME || '', fp.slice(1)) : fp;
31 t = fs.readFileSync(expanded, 'utf8').trim();
32 }
33 return t;
34 }
35
36 const token = resolveToken();
37 const apiBase = (process.env.KNOWTATION_HUB_API || 'http://localhost:3000').replace(/\/$/, '');
38 const vaultId = process.env.KNOWTATION_HUB_VAULT_ID || 'default';
39 const mirrorAppend = process.argv.includes('--mirror-append');
40
41 /** @type {{ step: string, ok: boolean, detail: string }[]} */
42 const results = [];
43
44 function record(step, ok, detail) {
45 results.push({ step, ok, detail });
46 const mark = ok ? 'PASS' : 'FAIL';
47 console.log(`[${mark}] ${step}: ${detail}`);
48 }
49
50 function headers(extra = {}) {
51 const h = {
52 Accept: 'application/json',
53 'Content-Type': 'application/json',
54 'X-Vault-Id': vaultId,
55 ...extra,
56 };
57 if (token) h.Authorization = 'Bearer ' + token;
58 return h;
59 }
60
61 /**
62 * @param {string} method
63 * @param {string} pathSuffix
64 * @param {object | undefined} body
65 */
66 async function api(method, pathSuffix, body) {
67 const res = await fetch(apiBase + pathSuffix, {
68 method,
69 headers: headers(),
70 body: body ? JSON.stringify(body) : undefined,
71 });
72 const text = await res.text();
73 let json = null;
74 try {
75 json = text ? JSON.parse(text) : null;
76 } catch {
77 json = { raw: text.slice(0, 200) };
78 }
79 return { status: res.status, json };
80 }
81
82 async function main() {
83 console.log(`2G-c task loop read smoke — ${apiBase} vault=${vaultId}`);
84
85 if (!token) {
86 record('auth', false, 'KNOWTATION_HUB_TOKEN (or _FILE) required');
87 process.exit(1);
88 }
89
90 const session = await api('GET', '/api/v1/auth/session');
91 if (session.status !== 200) {
92 record('auth', false, `session HTTP ${session.status} — refresh Hub JWT`);
93 process.exit(1);
94 }
95 record('auth', true, `session OK role=${session.json?.role ?? 'unknown'}`);
96
97 const list = await api('GET', '/api/v1/task-loops');
98 const listOk =
99 list.status === 200 &&
100 list.json?.schema === 'knowtation.task_loop_list/v0' &&
101 Array.isArray(list.json?.loops) &&
102 list.json.loops.length >= 1;
103 record(
104 'GET /api/v1/task-loops (lazy seed)',
105 listOk,
106 listOk ? `${list.json.loops.length} loops` : `HTTP ${list.status} ${list.json?.code ?? ''}`,
107 );
108
109 const getSchoolTrip = await api('GET', '/api/v1/task-loops/loop_school_trip');
110 const handoffRefs = getSchoolTrip.json?.loop?.handoff_refs;
111 const getOk =
112 getSchoolTrip.status === 200 &&
113 getSchoolTrip.json?.schema === 'knowtation.task_loop_get/v0' &&
114 getSchoolTrip.json?.loop?.loop_id === 'loop_school_trip' &&
115 Array.isArray(handoffRefs) &&
116 handoffRefs.includes('graph_school_trip');
117 record(
118 'GET /api/v1/task-loops/loop_school_trip',
119 getOk,
120 getOk
121 ? `handoff_refs=${handoffRefs.join(',')}`
122 : `HTTP ${getSchoolTrip.status} code=${getSchoolTrip.json?.code ?? ''}`,
123 );
124
125 const personalList = await api('GET', '/api/v1/task-loops?scope=personal');
126 const personalOk =
127 personalList.status === 200 &&
128 Array.isArray(personalList.json?.loops) &&
129 personalList.json.loops.every((l) => l.scope === 'personal');
130 record(
131 'GET /api/v1/task-loops?scope=personal',
132 personalOk,
133 personalOk
134 ? `${personalList.json.loops.length} personal loops`
135 : `HTTP ${personalList.status}`,
136 );
137
138 const unknown = await api('GET', '/api/v1/task-loops/loop_nonexistent_fixture');
139 const unknownOk = unknown.status === 404 && unknown.json?.code === 'unknown_task_loop';
140 record(
141 'GET unknown loop id',
142 unknownOk || session.json?.role === 'admin',
143 unknownOk
144 ? '404 unknown_task_loop'
145 : `HTTP ${unknown.status} code=${unknown.json?.code ?? 'none'}`,
146 );
147
148 const mirrorDisabled = await api('POST', '/api/v1/loop-pass-audit', {
149 pass_id: 'pass_smoke_mirror_gated',
150 loop_id: 'loop_school_trip',
151 outcome: 'idle',
152 boundary_policy: 'observe_only',
153 context_refs: [{ kind: 'loop', ref: 'loop_school_trip' }],
154 occurred_at: new Date().toISOString(),
155 scope: 'personal',
156 });
157 const mirrorGateOff =
158 process.env.LOOP_PASS_AUDIT_MIRROR_ENABLED !== '1' &&
159 process.env.LOOP_PASS_AUDIT_MIRROR_ENABLED !== 'true';
160 const mirrorDisabledOk =
161 mirrorGateOff && mirrorDisabled.status === 403 && mirrorDisabled.json?.code === 'LOOP_PASS_AUDIT_MIRROR_DISABLED';
162 record(
163 'POST /api/v1/loop-pass-audit gated off',
164 mirrorDisabledOk || !mirrorGateOff,
165 mirrorDisabledOk
166 ? '403 LOOP_PASS_AUDIT_MIRROR_DISABLED'
167 : mirrorGateOff
168 ? `HTTP ${mirrorDisabled.status} code=${mirrorDisabled.json?.code ?? ''}`
169 : 'skipped — mirror env on',
170 );
171
172 if (mirrorAppend && !mirrorGateOff) {
173 const passId = `pass_smoke_${Date.now()}`;
174 const append = await api('POST', '/api/v1/loop-pass-audit', {
175 pass_id: passId,
176 loop_id: 'loop_school_trip',
177 instance_task_id: null,
178 graph_id: 'graph_school_trip',
179 outcome: 'scheduled',
180 boundary_policy: 'observe_only',
181 context_refs: [{ kind: 'task', ref: 'task_school_trip_2026_w25' }],
182 scooling_pass_audit_ref: passId,
183 occurred_at: new Date().toISOString(),
184 scope: 'personal',
185 });
186 const appendOk = append.status === 201 && append.json?.schema === 'knowtation.loop_pass_audit/v0';
187 record(
188 'POST /api/v1/loop-pass-audit append',
189 appendOk,
190 appendOk ? `audit_id=${append.json.audit_id}` : `HTTP ${append.status}`,
191 );
192
193 const dup = await api('POST', '/api/v1/loop-pass-audit', {
194 pass_id: passId,
195 loop_id: 'loop_school_trip',
196 outcome: 'scheduled',
197 boundary_policy: 'observe_only',
198 context_refs: [{ kind: 'task', ref: 'task_school_trip_2026_w25' }],
199 scooling_pass_audit_ref: passId,
200 occurred_at: new Date().toISOString(),
201 scope: 'personal',
202 });
203 const dupOk = dup.status === 200 && dup.json?.pass_id === passId;
204 record(
205 'POST loop-pass-audit idempotent on pass_id',
206 dupOk,
207 dupOk ? '200 duplicate pass_id' : `HTTP ${dup.status}`,
208 );
209 } else if (mirrorAppend) {
210 record('--mirror-append', false, 'Set LOOP_PASS_AUDIT_MIRROR_ENABLED=1 for live mirror append');
211 }
212
213 const failed = results.filter((r) => !r.ok);
214 console.log(`\nSummary: ${results.length - failed.length}/${results.length} PASS`);
215 if (failed.length) {
216 process.exit(1);
217 }
218 console.log('2G-c task loop read smoke PASS');
219 }
220
221 main().catch((err) => {
222 console.error(err);
223 process.exit(1);
224 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago