hosted-import-integration.test.mjs
577 lines 22.3 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Hosted import: bridge runs importers and POSTs notes to canister; gateway streams multipart to bridge.
3 */
4 import { test } from 'node:test';
5 import assert from 'node:assert/strict';
6 import http from 'http';
7 import fs from 'fs';
8 import os from 'os';
9 import path from 'path';
10 import { fileURLToPath, pathToFileURL } from 'url';
11 import crypto from 'crypto';
12 import express from 'express';
13 import multer from 'multer';
14 import AdmZip from 'adm-zip';
15
16 const __dirname = path.dirname(fileURLToPath(import.meta.url));
17 const projectRoot = path.resolve(__dirname, '..');
18
19 const SECRET = 'hosted-import-integration-test-secret-32';
20
21 /** HS256 JWT compatible with `jsonwebtoken` (bridge/gateway). */
22 function signTestJwt(payload) {
23 const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
24 const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
25 const data = `${header}.${body}`;
26 const sig = crypto.createHmac('sha256', SECRET).update(data).digest('base64url');
27 return `${data}.${sig}`;
28 }
29
30 function headerGet(headers, name) {
31 if (!headers) return null;
32 if (typeof headers.get === 'function') return headers.get(name) ?? headers.get(name.toLowerCase());
33 const lower = name.toLowerCase();
34 for (const k of Object.keys(headers)) {
35 if (k.toLowerCase() === lower) return headers[k];
36 }
37 return null;
38 }
39
40 test('bridge POST /api/v1/import: markdown upload → mock canister receives note + import provenance', async (t) => {
41 const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kn-bridge-import-int-'));
42 t.after(() => {
43 try {
44 fs.rmSync(dataDir, { recursive: true, force: true });
45 } catch (_) {}
46 });
47
48 process.env.NETLIFY = '1';
49 process.env.CANISTER_URL = 'http://mock-canister.test';
50 process.env.SESSION_SECRET = SECRET;
51 process.env.DATA_DIR = dataDir;
52
53 const noteWrites = [];
54 const origFetch = globalThis.fetch;
55 globalThis.fetch = async (url, init = {}) => {
56 const u = String(url);
57 if (u.includes('/api/v1/vaults') && (init.method === undefined || init.method === 'GET')) {
58 return new Response(JSON.stringify({ vaults: [{ id: 'default' }] }), {
59 status: 200,
60 headers: { 'Content-Type': 'application/json' },
61 });
62 }
63 if (String(init.method || 'GET').toUpperCase() === 'POST' && u.includes('/api/v1/notes')) {
64 let bodyText = '';
65 if (typeof init.body === 'string') bodyText = init.body;
66 else if (init.body != null && typeof init.body === 'object' && Symbol.asyncIterator in init.body) {
67 const chunks = [];
68 for await (const c of init.body) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
69 bodyText = Buffer.concat(chunks).toString('utf8');
70 }
71 noteWrites.push({
72 url: u,
73 'x-user-id': headerGet(init.headers, 'x-user-id'),
74 'x-actor-id': headerGet(init.headers, 'x-actor-id'),
75 'x-vault-id': headerGet(init.headers, 'x-vault-id'),
76 body: bodyText,
77 });
78 const batch = u.includes('/notes/batch');
79 return new Response(
80 batch
81 ? JSON.stringify({ imported: JSON.parse(bodyText).notes?.length ?? 0, written: true })
82 : JSON.stringify({ ok: true }),
83 {
84 status: 200,
85 headers: { 'Content-Type': 'application/json' },
86 },
87 );
88 }
89 return origFetch(url, init);
90 };
91 t.after(() => {
92 globalThis.fetch = origFetch;
93 });
94
95 const bridgeEntry = pathToFileURL(path.join(projectRoot, 'hub', 'bridge', 'server.mjs')).href;
96 const { app } = await import(`${bridgeEntry}?t=${Date.now()}`);
97
98 const token = signTestJwt({ sub: 'github:integration-tester' });
99 const server = http.createServer(app);
100 await new Promise((resolve, reject) => {
101 server.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
102 });
103 t.after(() => new Promise((r) => server.close(() => r())));
104
105 const { port } = /** @type {import('net').AddressInfo} */ (server.address());
106 const fd = new FormData();
107 fd.set('source_type', 'markdown');
108 fd.set('file', new Blob(['# Hello\n\nIntegration body.\n'], { type: 'text/markdown' }), 'note.md');
109
110 const res = await fetch(`http://127.0.0.1:${port}/api/v1/import`, {
111 method: 'POST',
112 headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'default' },
113 body: fd,
114 });
115
116 const resText = await res.text();
117 assert.equal(res.status, 200, resText);
118 const json = JSON.parse(resText);
119 assert.equal(json.count, 1);
120 assert.ok(Array.isArray(json.imported));
121 assert.equal(json.imported.length, 1);
122
123 assert.equal(noteWrites.length, 1);
124 const nw = noteWrites[0];
125 assert.match(nw.url, /\/api\/v1\/notes\/batch/);
126 assert.equal(nw['x-user-id'], 'github:integration-tester');
127 assert.equal(nw['x-actor-id'], 'github:integration-tester');
128 assert.equal(nw['x-vault-id'], 'default');
129 const posted = JSON.parse(nw.body);
130 assert.ok(Array.isArray(posted.notes));
131 assert.equal(posted.notes.length, 1);
132 assert.equal(posted.notes[0].path, 'inbox/note.md');
133 assert.match(posted.notes[0].body, /Integration body/);
134 assert.equal(typeof posted.notes[0].frontmatter, 'object');
135 assert.match(JSON.stringify(posted.notes[0].frontmatter), /import/);
136 });
137
138 test('bridge POST /api/v1/import: pdf upload → note body contains extracted text', async (t) => {
139 const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kn-bridge-import-pdf-'));
140 t.after(() => {
141 try {
142 fs.rmSync(dataDir, { recursive: true, force: true });
143 } catch (_) {}
144 });
145
146 process.env.NETLIFY = '1';
147 process.env.CANISTER_URL = 'http://mock-canister.test';
148 process.env.SESSION_SECRET = SECRET;
149 process.env.DATA_DIR = dataDir;
150
151 const noteWrites = [];
152 const origFetch = globalThis.fetch;
153 globalThis.fetch = async (url, init = {}) => {
154 const u = String(url);
155 if (u.includes('/api/v1/vaults') && (init.method === undefined || init.method === 'GET')) {
156 return new Response(JSON.stringify({ vaults: [{ id: 'default' }] }), {
157 status: 200,
158 headers: { 'Content-Type': 'application/json' },
159 });
160 }
161 if (String(init.method || 'GET').toUpperCase() === 'POST' && u.includes('/api/v1/notes')) {
162 let bodyText = '';
163 if (typeof init.body === 'string') bodyText = init.body;
164 else if (init.body != null && typeof init.body === 'object' && Symbol.asyncIterator in init.body) {
165 const chunks = [];
166 for await (const c of init.body) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
167 bodyText = Buffer.concat(chunks).toString('utf8');
168 }
169 noteWrites.push({ url: u, body: bodyText });
170 return new Response(JSON.stringify({ imported: JSON.parse(bodyText).notes?.length ?? 0, written: true }), {
171 status: 200,
172 headers: { 'Content-Type': 'application/json' },
173 });
174 }
175 return origFetch(url, init);
176 };
177 t.after(() => {
178 globalThis.fetch = origFetch;
179 });
180
181 const bridgeEntry = pathToFileURL(path.join(projectRoot, 'hub', 'bridge', 'server.mjs')).href;
182 const { app } = await import(`${bridgeEntry}?t=${Date.now()}-pdf`);
183
184 const server = http.createServer(app);
185 await new Promise((resolve, reject) => {
186 server.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
187 });
188 t.after(() => new Promise((r) => server.close(() => r())));
189
190 const { port } = /** @type {import('net').AddressInfo} */ (server.address());
191 const pdfPath = path.join(projectRoot, 'test', 'fixtures', 'pdf-import', 'hello.pdf');
192 const pdfBuf = fs.readFileSync(pdfPath);
193 const token = signTestJwt({ sub: 'github:integration-pdf' });
194 const fd = new FormData();
195 fd.set('source_type', 'pdf');
196 fd.set('file', new Blob([pdfBuf], { type: 'application/pdf' }), 'hello.pdf');
197
198 const res = await fetch(`http://127.0.0.1:${port}/api/v1/import`, {
199 method: 'POST',
200 headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'default' },
201 body: fd,
202 });
203
204 const resText = await res.text();
205 assert.equal(res.status, 200, resText);
206 const json = JSON.parse(resText);
207 assert.equal(json.count, 1);
208 assert.equal(noteWrites.length, 1);
209 const posted = JSON.parse(noteWrites[0].body);
210 assert.ok(posted.notes[0].body.includes('Knowtation PDF fixture'));
211 assert.equal(posted.notes[0].frontmatter.source, 'pdf-import');
212 });
213
214 test('bridge POST /api/v1/import: docx upload → note body contains converted markdown', async (t) => {
215 const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kn-bridge-import-docx-'));
216 t.after(() => {
217 try {
218 fs.rmSync(dataDir, { recursive: true, force: true });
219 } catch (_) {}
220 });
221
222 process.env.NETLIFY = '1';
223 process.env.CANISTER_URL = 'http://mock-canister.test';
224 process.env.SESSION_SECRET = SECRET;
225 process.env.DATA_DIR = dataDir;
226
227 const noteWrites = [];
228 const origFetch = globalThis.fetch;
229 globalThis.fetch = async (url, init = {}) => {
230 const u = String(url);
231 if (u.includes('/api/v1/vaults') && (init.method === undefined || init.method === 'GET')) {
232 return new Response(JSON.stringify({ vaults: [{ id: 'default' }] }), {
233 status: 200,
234 headers: { 'Content-Type': 'application/json' },
235 });
236 }
237 if (String(init.method || 'GET').toUpperCase() === 'POST' && u.includes('/api/v1/notes')) {
238 let bodyText = '';
239 if (typeof init.body === 'string') bodyText = init.body;
240 else if (init.body != null && typeof init.body === 'object' && Symbol.asyncIterator in init.body) {
241 const chunks = [];
242 for await (const c of init.body) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
243 bodyText = Buffer.concat(chunks).toString('utf8');
244 }
245 noteWrites.push({ url: u, body: bodyText });
246 return new Response(JSON.stringify({ imported: JSON.parse(bodyText).notes?.length ?? 0, written: true }), {
247 status: 200,
248 headers: { 'Content-Type': 'application/json' },
249 });
250 }
251 return origFetch(url, init);
252 };
253 t.after(() => {
254 globalThis.fetch = origFetch;
255 });
256
257 const bridgeEntry = pathToFileURL(path.join(projectRoot, 'hub', 'bridge', 'server.mjs')).href;
258 const { app } = await import(`${bridgeEntry}?t=${Date.now()}-docx`);
259
260 const server = http.createServer(app);
261 await new Promise((resolve, reject) => {
262 server.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
263 });
264 t.after(() => new Promise((r) => server.close(() => r())));
265
266 const { port } = /** @type {import('net').AddressInfo} */ (server.address());
267 const docxPath = path.join(projectRoot, 'test', 'fixtures', 'docx-import', 'hello.docx');
268 const docxBuf = fs.readFileSync(docxPath);
269 const token = signTestJwt({ sub: 'github:integration-docx' });
270 const fd = new FormData();
271 fd.set('source_type', 'docx');
272 fd.set('file', new Blob([docxBuf], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }), 'hello.docx');
273
274 const res = await fetch(`http://127.0.0.1:${port}/api/v1/import`, {
275 method: 'POST',
276 headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'default' },
277 body: fd,
278 });
279
280 const resText = await res.text();
281 assert.equal(res.status, 200, resText);
282 const json = JSON.parse(resText);
283 assert.equal(json.count, 1);
284 assert.equal(noteWrites.length, 1);
285 const posted = JSON.parse(noteWrites[0].body);
286 assert.ok(posted.notes[0].body.includes('Knowtation DOCX fixture'));
287 assert.equal(posted.notes[0].frontmatter.source, 'docx-import');
288 });
289
290 test('bridge POST /api/v1/import: generic-csv upload → one canister note per row', async (t) => {
291 const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kn-bridge-import-csv-'));
292 t.after(() => {
293 try {
294 fs.rmSync(dataDir, { recursive: true, force: true });
295 } catch (_) {}
296 });
297
298 process.env.NETLIFY = '1';
299 process.env.CANISTER_URL = 'http://mock-canister.test';
300 process.env.SESSION_SECRET = SECRET;
301 process.env.DATA_DIR = dataDir;
302
303 const noteWrites = [];
304 const origFetch = globalThis.fetch;
305 globalThis.fetch = async (url, init = {}) => {
306 const u = String(url);
307 if (u.includes('/api/v1/vaults') && (init.method === undefined || init.method === 'GET')) {
308 return new Response(JSON.stringify({ vaults: [{ id: 'default' }] }), {
309 status: 200,
310 headers: { 'Content-Type': 'application/json' },
311 });
312 }
313 if (String(init.method || 'GET').toUpperCase() === 'POST' && u.includes('/api/v1/notes')) {
314 let bodyText = '';
315 if (typeof init.body === 'string') bodyText = init.body;
316 else if (init.body != null && typeof init.body === 'object' && Symbol.asyncIterator in init.body) {
317 const chunks = [];
318 for await (const c of init.body) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
319 bodyText = Buffer.concat(chunks).toString('utf8');
320 }
321 noteWrites.push({ url: u, body: bodyText });
322 return new Response(JSON.stringify({ imported: JSON.parse(bodyText).notes?.length ?? 0, written: true }), {
323 status: 200,
324 headers: { 'Content-Type': 'application/json' },
325 });
326 }
327 return origFetch(url, init);
328 };
329 t.after(() => {
330 globalThis.fetch = origFetch;
331 });
332
333 const bridgeEntry = pathToFileURL(path.join(projectRoot, 'hub', 'bridge', 'server.mjs')).href;
334 const { app } = await import(`${bridgeEntry}?t=${Date.now()}-gcsv`);
335
336 const server = http.createServer(app);
337 await new Promise((resolve, reject) => {
338 server.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
339 });
340 t.after(() => new Promise((r) => server.close(() => r())));
341
342 const { port } = /** @type {import('net').AddressInfo} */ (server.address());
343 const csvPath = path.join(projectRoot, 'test', 'fixtures', 'generic-csv-import', 'sample.csv');
344 const csvBuf = fs.readFileSync(csvPath);
345 const token = signTestJwt({ sub: 'github:integration-gcsv' });
346 const fd = new FormData();
347 fd.set('source_type', 'generic-csv');
348 fd.set('file', new Blob([csvBuf], { type: 'text/csv' }), 'sample.csv');
349
350 const res = await fetch(`http://127.0.0.1:${port}/api/v1/import`, {
351 method: 'POST',
352 headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'default' },
353 body: fd,
354 });
355
356 const resText = await res.text();
357 assert.equal(res.status, 200, resText);
358 const json = JSON.parse(resText);
359 assert.equal(json.count, 2);
360 assert.equal(noteWrites.length, 1);
361 const posted = JSON.parse(noteWrites[0].body);
362 assert.equal(posted.notes.length, 2);
363 assert.equal(posted.notes[0].frontmatter.source, 'csv-import');
364 assert.equal(posted.notes[0].frontmatter.title, 'sample.csv · Alice');
365 assert.equal(posted.notes[1].frontmatter.title, 'sample.csv · Bob');
366 assert.ok(posted.notes[0].body.includes('Alice'));
367 assert.ok(posted.notes[1].body.includes('Bob'));
368 });
369
370 test('bridge POST /api/v1/import: ZIP with multiple .md files → one canister batch (≤100 notes)', async (t) => {
371 const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kn-bridge-import-multi-'));
372 t.after(() => {
373 try {
374 fs.rmSync(dataDir, { recursive: true, force: true });
375 } catch (_) {}
376 });
377
378 process.env.NETLIFY = '1';
379 process.env.CANISTER_URL = 'http://mock-canister.test';
380 process.env.SESSION_SECRET = SECRET;
381 process.env.DATA_DIR = dataDir;
382
383 const noteWrites = [];
384 const origFetch = globalThis.fetch;
385 globalThis.fetch = async (url, init = {}) => {
386 const u = String(url);
387 if (u.includes('/api/v1/vaults') && (init.method === undefined || init.method === 'GET')) {
388 return new Response(JSON.stringify({ vaults: [{ id: 'default' }] }), {
389 status: 200,
390 headers: { 'Content-Type': 'application/json' },
391 });
392 }
393 if (String(init.method || 'GET').toUpperCase() === 'POST' && u.includes('/api/v1/notes')) {
394 let bodyText = '';
395 if (typeof init.body === 'string') bodyText = init.body;
396 noteWrites.push({ url: u, body: bodyText });
397 const batch = u.includes('/notes/batch');
398 return new Response(
399 batch
400 ? JSON.stringify({ imported: JSON.parse(bodyText).notes?.length ?? 0, written: true })
401 : JSON.stringify({ ok: true }),
402 { status: 200, headers: { 'Content-Type': 'application/json' } },
403 );
404 }
405 return origFetch(url, init);
406 };
407 t.after(() => {
408 globalThis.fetch = origFetch;
409 });
410
411 const bridgeEntry = pathToFileURL(path.join(projectRoot, 'hub', 'bridge', 'server.mjs')).href;
412 const { app } = await import(`${bridgeEntry}?t=${Date.now()}-multi`);
413
414 const server = http.createServer(app);
415 await new Promise((resolve, reject) => {
416 server.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
417 });
418 t.after(() => new Promise((r) => server.close(() => r())));
419
420 const { port } = /** @type {import('net').AddressInfo} */ (server.address());
421 const zip = new AdmZip();
422 zip.addFile('one.md', Buffer.from('# One\n\nfirst'));
423 zip.addFile('two.md', Buffer.from('# Two\n\nsecond'));
424 const zipBuf = zip.toBuffer();
425
426 const token = signTestJwt({ sub: 'github:integration-multi' });
427 const fd = new FormData();
428 fd.set('source_type', 'markdown');
429 fd.set('file', new Blob([zipBuf], { type: 'application/zip' }), 'pair.zip');
430
431 const res = await fetch(`http://127.0.0.1:${port}/api/v1/import`, {
432 method: 'POST',
433 headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'default' },
434 body: fd,
435 });
436
437 const resText = await res.text();
438 assert.equal(res.status, 200, resText);
439 const json = JSON.parse(resText);
440 assert.equal(json.count, 2);
441 assert.equal(noteWrites.length, 1);
442 assert.match(noteWrites[0].url, /\/notes\/batch/);
443 const batchBody = JSON.parse(noteWrites[0].body);
444 assert.equal(batchBody.notes.length, 2);
445 });
446
447 test('bridge import chunks canister batch at 100 notes (101 files → 2 POSTs)', async (t) => {
448 const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kn-bridge-import-chunk-'));
449 t.after(() => {
450 try {
451 fs.rmSync(dataDir, { recursive: true, force: true });
452 } catch (_) {}
453 });
454
455 process.env.NETLIFY = '1';
456 process.env.CANISTER_URL = 'http://mock-canister.test';
457 process.env.SESSION_SECRET = SECRET;
458 process.env.DATA_DIR = dataDir;
459
460 const noteWrites = [];
461 const origFetch = globalThis.fetch;
462 globalThis.fetch = async (url, init = {}) => {
463 const u = String(url);
464 if (u.includes('/api/v1/vaults') && (init.method === undefined || init.method === 'GET')) {
465 return new Response(JSON.stringify({ vaults: [{ id: 'default' }] }), {
466 status: 200,
467 headers: { 'Content-Type': 'application/json' },
468 });
469 }
470 if (String(init.method || 'GET').toUpperCase() === 'POST' && u.includes('/api/v1/notes')) {
471 let bodyText = '';
472 if (typeof init.body === 'string') bodyText = init.body;
473 noteWrites.push({ url: u, body: bodyText });
474 return new Response(
475 JSON.stringify({ imported: JSON.parse(bodyText).notes?.length ?? 0, written: true }),
476 { status: 200, headers: { 'Content-Type': 'application/json' } },
477 );
478 }
479 return origFetch(url, init);
480 };
481 t.after(() => {
482 globalThis.fetch = origFetch;
483 });
484
485 const bridgeEntry = pathToFileURL(path.join(projectRoot, 'hub', 'bridge', 'server.mjs')).href;
486 const { app } = await import(`${bridgeEntry}?t=${Date.now()}-chunk`);
487
488 const server = http.createServer(app);
489 await new Promise((resolve, reject) => {
490 server.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
491 });
492 t.after(() => new Promise((r) => server.close(() => r())));
493
494 const { port } = /** @type {import('net').AddressInfo} */ (server.address());
495 const zip = new AdmZip();
496 for (let i = 0; i < 101; i++) {
497 zip.addFile(`n${i}.md`, Buffer.from(`# Note ${i}\n\nbody ${i}.\n`));
498 }
499 const zipBuf = zip.toBuffer();
500
501 const token = signTestJwt({ sub: 'github:chunk-test' });
502 const fd = new FormData();
503 fd.set('source_type', 'markdown');
504 fd.set('file', new Blob([zipBuf], { type: 'application/zip' }), 'many.zip');
505
506 const res = await fetch(`http://127.0.0.1:${port}/api/v1/import`, {
507 method: 'POST',
508 headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'default' },
509 body: fd,
510 });
511
512 const resText = await res.text();
513 assert.equal(res.status, 200, resText);
514 const out = JSON.parse(resText);
515 assert.equal(out.count, 101);
516 assert.equal(noteWrites.length, 2);
517 assert.match(noteWrites[0].url, /\/notes\/batch/);
518 assert.match(noteWrites[1].url, /\/notes\/batch/);
519 assert.equal(JSON.parse(noteWrites[0].body).notes.length, 100);
520 assert.equal(JSON.parse(noteWrites[1].body).notes.length, 1);
521 });
522
523 test('gateway POST /api/v1/import streams multipart to BRIDGE_URL (mock bridge)', async (t) => {
524 const mockBridge = express();
525 const upload = multer({ storage: multer.memoryStorage() }).single('file');
526 let received = /** @type {{ source_type?: string, fileLen: number, name?: string } | null} */ (null);
527 mockBridge.post('/api/v1/import', upload, (req, res) => {
528 received = {
529 source_type: req.body?.source_type,
530 fileLen: req.file?.buffer?.length ?? 0,
531 name: req.file?.originalname,
532 };
533 res.json({ imported: [{ path: 'inbox/x.md' }], count: 1 });
534 });
535 const bridgeSrv = http.createServer(mockBridge);
536 await new Promise((resolve, reject) => {
537 bridgeSrv.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
538 });
539 t.after(() => new Promise((r) => bridgeSrv.close(() => r())));
540 const bridgePort = /** @type {import('net').AddressInfo} */ (bridgeSrv.address()).port;
541 const bridgeUrl = `http://127.0.0.1:${bridgePort}`;
542
543 process.env.NETLIFY = '1';
544 process.env.CANISTER_URL = 'http://canister.placeholder.test';
545 process.env.SESSION_SECRET = SECRET;
546 process.env.BRIDGE_URL = bridgeUrl;
547
548 const gwEntry = pathToFileURL(path.join(projectRoot, 'hub', 'gateway', 'server.mjs')).href;
549 const { app: gwApp } = await import(`${gwEntry}?gw=${Date.now()}`);
550
551 const gwSrv = http.createServer(gwApp);
552 await new Promise((resolve, reject) => {
553 gwSrv.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
554 });
555 t.after(() => new Promise((r) => gwSrv.close(() => r())));
556 const gwPort = /** @type {import('net').AddressInfo} */ (gwSrv.address()).port;
557
558 const token = signTestJwt({ sub: 'google:gw-import-test' });
559 const fd = new FormData();
560 fd.set('source_type', 'markdown');
561 fd.set('file', new Blob(['# Z\n'], { type: 'text/markdown' }), 'z.md');
562
563 const res = await fetch(`http://127.0.0.1:${gwPort}/api/v1/import`, {
564 method: 'POST',
565 headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'default' },
566 body: fd,
567 });
568
569 const text = await res.text();
570 assert.equal(res.status, 200, text);
571 const json = JSON.parse(text);
572 assert.equal(json.count, 1);
573 assert.ok(received);
574 assert.equal(received.source_type, 'markdown');
575 assert.ok(received.fileLen > 0);
576 assert.equal(received.name, 'z.md');
577 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago