gateway-hosted-notes-frontmatter.test.mjs
257 lines 9.2 KB
Raw
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor ⚠ breaking 7 hours ago
1 /**
2 * Hosted gateway note frontmatter normalization.
3 *
4 * These tests exercise the real gateway proxy with mocked canister and bridge
5 * responses so direct reads, list rows, facets, and hosted search stay aligned.
6 */
7 import { describe, it, before, after } from 'node:test';
8 import assert from 'node:assert/strict';
9 import http from 'node:http';
10 import crypto from 'node:crypto';
11 import path from 'node:path';
12 import { fileURLToPath, pathToFileURL } from 'node:url';
13
14 const __dirname = path.dirname(fileURLToPath(import.meta.url));
15 const projectRoot = path.resolve(__dirname, '..');
16 const SECRET = 'gateway-hosted-notes-frontmatter-secret-32';
17 const GATEWAY_AUTH_SECRET = 'gateway-hosted-notes-frontmatter-gw-secret';
18 const PROPOSAL_PATH =
19 'projects/born-free/scripts/proposals/2026-06-06-quantum-computing-online-test-script.md';
20 const PROPOSAL_TAGS = [
21 'script-proposal',
22 'born-free',
23 'quantum-ai',
24 'human-review',
25 'freshness-gate',
26 'ready-for-videofactory-handoff',
27 ];
28 const PROPOSAL_FRONTMATTER = {
29 title: 'Quantum Computing Online Test Script',
30 project: 'born-free',
31 status: 'approved',
32 tags: PROPOSAL_TAGS,
33 handoff_target: 'videofactory',
34 handoff_status: 'ready',
35 };
36
37 function bearer(role = 'editor') {
38 const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
39 const payload = Buffer.from(JSON.stringify({ sub: 'google:actor', role })).toString('base64url');
40 const data = `${header}.${payload}`;
41 const sig = crypto.createHmac('sha256', SECRET).update(data).digest('base64url');
42 return `${data}.${sig}`;
43 }
44
45 function responseJson(status, payload) {
46 const text = JSON.stringify(payload);
47 return {
48 ok: status >= 200 && status < 300,
49 status,
50 headers: new Headers({ 'content-type': 'application/json', etag: 'mock-etag' }),
51 text: async () => text,
52 json: async () => payload,
53 };
54 }
55
56 function installFetchMock(calls) {
57 const notes = new Map([
58 [
59 'inbox/object-frontmatter.md',
60 {
61 path: 'inbox/object-frontmatter.md',
62 frontmatter: {
63 title: 'Object Frontmatter',
64 project: 'object-project',
65 status: 'draft',
66 tags: ['object-tag'],
67 },
68 body: 'Object-shaped hosted note body.',
69 },
70 ],
71 [
72 'inbox/string-frontmatter.md',
73 {
74 path: 'inbox/string-frontmatter.md',
75 frontmatter: JSON.stringify({
76 title: 'String Frontmatter',
77 project: 'string-project',
78 status: 'draft',
79 tags: ['string-tag'],
80 }),
81 body: 'String-shaped hosted note body.',
82 },
83 ],
84 [
85 PROPOSAL_PATH,
86 {
87 path: PROPOSAL_PATH,
88 frontmatter: JSON.stringify(PROPOSAL_FRONTMATTER),
89 body: '# Quantum Computing Online Test Script\n\nCanonical approved script proposal.',
90 },
91 ],
92 ]);
93
94 globalThis.fetch = async (url, opts = {}) => {
95 const method = opts.method || 'GET';
96 const u = new URL(String(url));
97 calls.push({ url: String(url), method, headers: opts.headers });
98
99 if (u.origin === 'https://mock-bridge.test' && u.pathname === '/api/v1/hosted-context') {
100 return responseJson(200, {
101 actor_sub: 'google:actor',
102 effective_canister_user_id: 'google:owner',
103 allowed_vault_ids: ['default'],
104 role: 'editor',
105 scope: null,
106 });
107 }
108
109 if (u.origin === 'https://mock-bridge.test' && u.pathname === '/api/v1/search') {
110 return responseJson(200, {
111 results: [
112 {
113 path: PROPOSAL_PATH,
114 project: PROPOSAL_FRONTMATTER.project,
115 tags: PROPOSAL_TAGS,
116 score: 1,
117 },
118 ],
119 query: 'ready-for-videofactory-handoff',
120 mode: 'keyword',
121 });
122 }
123
124 if (u.origin === 'https://mock-canister.test' && u.pathname === '/api/v1/notes' && method === 'GET') {
125 return responseJson(200, { notes: [...notes.values()], total: notes.size });
126 }
127
128 if (u.origin === 'https://mock-canister.test' && u.pathname.startsWith('/api/v1/notes/') && method === 'GET') {
129 const encoded = u.pathname.slice('/api/v1/notes/'.length);
130 const notePath = decodeURIComponent(encoded);
131 const note = notes.get(notePath);
132 if (!note) return responseJson(404, { error: 'Not found', code: 'NOT_FOUND' });
133 return responseJson(200, note);
134 }
135
136 return responseJson(500, { error: `unexpected ${method} ${u.href}` });
137 };
138 }
139
140 describe('hosted gateway note frontmatter normalization', () => {
141 /** @type {import('http').Server} */
142 let server;
143 /** @type {string} */
144 let base;
145 /** @type {typeof fetch} */
146 let origFetch;
147 /** @type {Array<{ url: string, method: string, headers?: HeadersInit }>} */
148 let calls;
149
150 before(async () => {
151 origFetch = globalThis.fetch.bind(globalThis);
152 process.env.NETLIFY = '1';
153 process.env.CANISTER_URL = 'https://mock-canister.test';
154 process.env.BRIDGE_URL = 'https://mock-bridge.test';
155 process.env.SESSION_SECRET = SECRET;
156 process.env.CANISTER_AUTH_SECRET = GATEWAY_AUTH_SECRET;
157 process.env.BILLING_ENFORCE = 'false';
158 delete process.env.KNOWTATION_AIR_ENDPOINT;
159
160 calls = [];
161 installFetchMock(calls);
162
163 const gwEntry = pathToFileURL(path.join(projectRoot, 'hub', 'gateway', 'server.mjs')).href;
164 const { app } = await import(`${gwEntry}?hostednotesfrontmatter=${Date.now()}-${Math.random()}`);
165 server = http.createServer(app);
166 await new Promise((resolve, reject) => {
167 server.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
168 });
169 const addr = server.address();
170 assert.ok(addr && typeof addr === 'object');
171 base = `http://127.0.0.1:${addr.port}`;
172 });
173
174 after(async () => {
175 if (server) await new Promise((resolve) => server.close(() => resolve()));
176 globalThis.fetch = origFetch;
177 });
178
179 it('direct GET returns frontmatter object when upstream stores an object', async () => {
180 const res = await origFetch(`${base}/api/v1/notes/${encodeURIComponent('inbox/object-frontmatter.md')}`, {
181 headers: { Authorization: `Bearer ${bearer()}` },
182 });
183 const body = await res.json();
184
185 assert.equal(res.status, 200);
186 assert.deepEqual(body.frontmatter, {
187 title: 'Object Frontmatter',
188 project: 'object-project',
189 status: 'draft',
190 tags: ['object-tag'],
191 });
192 });
193
194 it('direct GET parses hosted JSON-string frontmatter into an object', async () => {
195 const res = await origFetch(`${base}/api/v1/notes/${encodeURIComponent('inbox/string-frontmatter.md')}`, {
196 headers: { Authorization: `Bearer ${bearer()}` },
197 });
198 const body = await res.json();
199
200 assert.equal(res.status, 200);
201 assert.equal(typeof body.frontmatter, 'object');
202 assert.equal(body.frontmatter.title, 'String Frontmatter');
203 assert.equal(body.frontmatter.project, 'string-project');
204 assert.deepEqual(body.frontmatter.tags, ['string-tag']);
205 });
206
207 it('direct GET exposes approved proposal tags and workflow fields', async () => {
208 const res = await origFetch(`${base}/api/v1/notes/${encodeURIComponent(PROPOSAL_PATH)}`, {
209 headers: { Authorization: `Bearer ${bearer()}` },
210 });
211 const body = await res.json();
212 const serialized = JSON.stringify(body);
213
214 assert.equal(res.status, 200);
215 assert.equal(body.path, PROPOSAL_PATH);
216 assert.equal(body.body.includes('Canonical approved script proposal.'), true);
217 assert.equal(body.frontmatter.title, PROPOSAL_FRONTMATTER.title);
218 assert.equal(body.frontmatter.project, 'born-free');
219 assert.equal(body.frontmatter.status, 'approved');
220 assert.equal(body.frontmatter.handoff_target, 'videofactory');
221 assert.equal(body.frontmatter.handoff_status, 'ready');
222 assert.deepEqual(body.frontmatter.tags, PROPOSAL_TAGS);
223 assert.equal(serialized.includes(GATEWAY_AUTH_SECRET), false);
224 });
225
226 it('list, facets, search, and direct GET agree on proposal note tags', async () => {
227 const auth = { Authorization: `Bearer ${bearer()}` };
228 const directRes = await origFetch(`${base}/api/v1/notes/${encodeURIComponent(PROPOSAL_PATH)}`, {
229 headers: auth,
230 });
231 const listRes = await origFetch(`${base}/api/v1/notes?limit=10&offset=0`, { headers: auth });
232 const facetsRes = await origFetch(`${base}/api/v1/notes/facets`, { headers: auth });
233 const searchRes = await origFetch(`${base}/api/v1/search`, {
234 method: 'POST',
235 headers: { ...auth, 'Content-Type': 'application/json' },
236 body: JSON.stringify({ mode: 'keyword', query: 'ready-for-videofactory-handoff' }),
237 });
238
239 const direct = await directRes.json();
240 const list = await listRes.json();
241 const facets = await facetsRes.json();
242 const search = await searchRes.json();
243 const listed = list.notes.find((n) => n.path === PROPOSAL_PATH);
244 const searched = search.results.find((n) => n.path === PROPOSAL_PATH);
245
246 assert.equal(directRes.status, 200);
247 assert.equal(listRes.status, 200);
248 assert.equal(facetsRes.status, 200);
249 assert.equal(searchRes.status, 200);
250 assert.ok(listed);
251 assert.ok(searched);
252 assert.deepEqual(direct.frontmatter.tags, PROPOSAL_TAGS);
253 assert.deepEqual(listed.frontmatter.tags, PROPOSAL_TAGS);
254 assert.deepEqual(searched.tags, PROPOSAL_TAGS);
255 assert.ok(facets.tags.includes('ready-for-videofactory-handoff'));
256 });
257 });
File History 1 commit
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor 7 hours ago