verify-hosted-task-read-smoke.mjs
205 lines 6.5 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-b — self-hosted Hub task read smoke (§10 checklist automation).
4 *
5 * Prerequisites:
6 * - Self-hosted Hub running with JWT auth (hub/server.mjs)
7 * - Vault with viewer+ role; config.data_dir writable
8 *
9 * Usage:
10 * KNOWTATION_HUB_TOKEN='<jwt>' KNOWTATION_HUB_VAULT_ID=default \
11 * node scripts/verify-hosted-task-read-smoke.mjs
12 *
13 * Loads repo-root `.env` when present (dotenv).
14 */
15
16 import fs from 'node:fs';
17 import path from 'node:path';
18 import { fileURLToPath } from 'node:url';
19 import { spawnSync } from 'node:child_process';
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
40 /** @type {{ step: string, ok: boolean, detail: string }[]} */
41 const results = [];
42
43 function record(step, ok, detail) {
44 results.push({ step, ok, detail });
45 const mark = ok ? 'PASS' : 'FAIL';
46 console.log(`[${mark}] ${step}: ${detail}`);
47 }
48
49 function headers(extra = {}) {
50 const h = {
51 Accept: 'application/json',
52 'Content-Type': 'application/json',
53 'X-Vault-Id': vaultId,
54 ...extra,
55 };
56 if (token) h.Authorization = 'Bearer ' + token;
57 return h;
58 }
59
60 /**
61 * @param {string} method
62 * @param {string} pathSuffix
63 * @param {object | undefined} body
64 * @param {Record<string, string>} [extraHeaders]
65 */
66 async function api(method, pathSuffix, body, extraHeaders = {}) {
67 const res = await fetch(apiBase + pathSuffix, {
68 method,
69 headers: headers(extraHeaders),
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-b task 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/tasks');
98 const listOk =
99 list.status === 200 &&
100 list.json?.schema === 'knowtation.task_list/v0' &&
101 Array.isArray(list.json?.tasks) &&
102 list.json.tasks.length >= 1;
103 record(
104 'GET /api/v1/tasks (lazy seed)',
105 listOk,
106 listOk ? `${list.json.tasks.length} tasks` : `HTTP ${list.status} ${list.json?.code ?? ''}`,
107 );
108
109 const getHandover = await api('GET', '/api/v1/tasks/task_2g_handover_001');
110 const getOk =
111 getHandover.status === 200 &&
112 getHandover.json?.task?.run_ref === 'run_overseer_in_progress' &&
113 getHandover.json?.task?.artifact_links?.length === 2;
114 record(
115 'GET /api/v1/tasks/task_2g_handover_001',
116 getOk,
117 getOk
118 ? `run_ref=${getHandover.json.task.run_ref} artifacts=${getHandover.json.task.artifact_links.length}`
119 : `HTTP ${getHandover.status}`,
120 );
121
122 const personalList = await api('GET', '/api/v1/tasks?scope=personal', undefined, {
123 'X-Task-Smoke-Scope': 'personal-only',
124 });
125 const personalOk =
126 personalList.status === 200 &&
127 personalList.json?.tasks?.length === 1 &&
128 personalList.json.tasks[0]?.task_id === 'task_personal_practice' &&
129 personalList.json.tasks.every((t) => t.scope === 'personal');
130 record(
131 'GET /api/v1/tasks?scope=personal (personal caller)',
132 personalOk,
133 personalOk
134 ? 'only task_personal_practice'
135 : `HTTP ${personalList.status} count=${personalList.json?.tasks?.length ?? '?'}`,
136 );
137
138 const orgDenied = await api('GET', '/api/v1/tasks/task_org_compliance_q2');
139 const orgDeniedOk = orgDenied.status === 404 && orgDenied.json?.code === 'unknown_task';
140 record(
141 'GET org task under personal-only role',
142 orgDeniedOk || session.json?.role === 'admin',
143 orgDeniedOk
144 ? '404 unknown_task (no existence leak)'
145 : session.json?.role === 'admin'
146 ? 'skipped — admin sees org task (use personal-only JWT for strict check)'
147 : `HTTP ${orgDenied.status} code=${orgDenied.json?.code ?? 'none'}`,
148 );
149
150 const runRead = await api(
151 'GET',
152 '/api/v1/flows/flow_overseer_handover/runs/run_overseer_in_progress',
153 );
154 const sd2Ok =
155 runRead.status === 200
156 ? runRead.json?.run?.task_ref === 'task_2g_handover_001'
157 : getHandover.json?.task?.run_ref === 'run_overseer_in_progress';
158 record(
159 'SD-2 reciprocal task_ref on run',
160 sd2Ok,
161 runRead.status === 200
162 ? `run.task_ref=${runRead.json?.run?.task_ref ?? 'missing'}`
163 : `run route HTTP ${runRead.status}; task.run_ref=${getHandover.json?.task?.run_ref ?? 'missing'}`,
164 );
165
166 const cli = spawnSync(
167 process.execPath,
168 [path.join(repoRoot, 'cli/index.mjs'), 'task', 'list', '--json', '--vault', vaultId],
169 { encoding: 'utf8', env: { ...process.env, KNOWTATION_VAULT_PATH: process.env.KNOWTATION_VAULT_PATH } },
170 );
171 let cliParity = false;
172 if (cli.status === 0) {
173 try {
174 const cliJson = JSON.parse(cli.stdout.trim());
175 cliParity = JSON.stringify(cliJson) === JSON.stringify(list.json);
176 } catch {
177 cliParity = false;
178 }
179 }
180 record(
181 'CLI parity knowtation task list --json',
182 cliParity,
183 cliParity ? 'deep-equal to Hub list' : `exit=${cli.status} ${cli.stderr?.slice(0, 80) ?? ''}`,
184 );
185
186 const postWrite = await api('POST', '/api/v1/tasks', { title: 'should not work' });
187 const noWriteOk = postWrite.status === 404 || postWrite.status === 405;
188 record(
189 'no POST /api/v1/tasks write route',
190 noWriteOk,
191 `HTTP ${postWrite.status}`,
192 );
193
194 const failed = results.filter((r) => !r.ok);
195 console.log(`\nSummary: ${results.length - failed.length}/${results.length} PASS`);
196 if (failed.length) {
197 process.exit(1);
198 }
199 console.log('2G-b task read smoke PASS');
200 }
201
202 main().catch((err) => {
203 console.error(err);
204 process.exit(1);
205 });
File History 1 commit
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 10 days ago