gateway-scooling-note-outline-smoke.test.mjs
224 lines 8.1 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 2 days ago
1 import { describe, it } from 'node:test';
2 import assert from 'node:assert/strict';
3 import http from 'node:http';
4 import express from 'express';
5
6 import {
7 createScoolingNoteOutlineSmokeRouter,
8 normalizeVaultRelativePath,
9 sanitizeNoteOutline,
10 } from '../hub/gateway/scooling-note-outline-smoke.mjs';
11
12 const BRIDGE_TOKEN = 'bridge-owned-note-outline-token';
13
14 function validOutline(path = 'inbox/a.md') {
15 return {
16 schema: 'knowtation.note_outline/v1',
17 path,
18 title: 'Bridge Outline',
19 headings: [
20 { level: 1, text: 'Intro', id: 'h1-intro-0001' },
21 { level: 2, text: 'Next', id: 'h2-next-0002' },
22 ],
23 truncated: false,
24 };
25 }
26
27 async function startServer(app) {
28 const server = http.createServer(app);
29 await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
30 const address = server.address();
31 assert.ok(address && typeof address === 'object');
32 return {
33 base: `http://127.0.0.1:${address.port}`,
34 close: () => new Promise((resolve) => server.close(resolve)),
35 };
36 }
37
38 async function withBridgeServer({ upstreamHandler, routerOptions = {}, fn }) {
39 const upstreamApp = express();
40 upstreamApp.get('/api/v1/note-outline', upstreamHandler);
41 const upstream = await startServer(upstreamApp);
42
43 const bridgeApp = express();
44 bridgeApp.use(
45 createScoolingNoteOutlineSmokeRouter({
46 upstreamEndpoint: `${upstream.base}/api/v1/note-outline`,
47 authorizationHeader: () => `Bearer ${BRIDGE_TOKEN}`,
48 isEnabled: () => true,
49 environment: () => 'local',
50 ...routerOptions,
51 }),
52 );
53 const bridge = await startServer(bridgeApp);
54
55 try {
56 await fn({ bridgeBase: bridge.base, upstreamBase: upstream.base });
57 } finally {
58 await bridge.close();
59 await upstream.close();
60 }
61 }
62
63 describe('Scooling NoteOutline smoke bridge helpers', () => {
64 it('unit: validates vault-relative paths and body-free NoteOutline payloads', () => {
65 assert.equal(normalizeVaultRelativePath('inbox\\a.md'), 'inbox/a.md');
66 assert.throws(() => normalizeVaultRelativePath('../secret.md'));
67 assert.throws(() => normalizeVaultRelativePath('/Users/private/a.md'));
68 assert.deepEqual(sanitizeNoteOutline({ ...validOutline(), body: 'must not leak' }, 'inbox/a.md'), validOutline());
69 assert.throws(() =>
70 sanitizeNoteOutline(
71 {
72 ...validOutline(),
73 headings: [{ level: 1, text: 'Intro', id: 'h1-intro-0001', body: 'must not leak' }],
74 },
75 'inbox/a.md',
76 ),
77 );
78 });
79
80 it('integration: owns the upstream bearer token and rejects Scooling-supplied credentials', async () => {
81 const upstreamCalls = [];
82 await withBridgeServer({
83 upstreamHandler: (req, res) => {
84 upstreamCalls.push({
85 path: req.query.path,
86 authorization: req.headers.authorization,
87 });
88 res.json(validOutline(String(req.query.path)));
89 },
90 fn: async ({ bridgeBase }) => {
91 const ok = await fetch(`${bridgeBase}/scooling/note-outline/smoke?path=inbox/a.md`);
92 assert.equal(ok.status, 200);
93 assert.equal(upstreamCalls.length, 1);
94 assert.equal(upstreamCalls[0].authorization, `Bearer ${BRIDGE_TOKEN}`);
95
96 const rejected = await fetch(`${bridgeBase}/scooling/note-outline/smoke?path=inbox/a.md`, {
97 headers: { Authorization: 'Bearer scooling-must-not-send' },
98 });
99 const body = await rejected.json();
100 assert.equal(rejected.status, 400);
101 assert.equal(body.containsRawCredentials, false);
102 assert.equal(upstreamCalls.length, 1);
103 },
104 });
105 });
106
107 it('end-to-end: returns only raw body-free NoteOutline JSON for Scooling transport', async () => {
108 await withBridgeServer({
109 upstreamHandler: (req, res) => {
110 res.json({
111 ...validOutline(String(req.query.path)),
112 body: 'private body must not cross bridge',
113 frontmatter: { api_key: 'must-not-leak' },
114 mcp_resource_uri: 'knowtation://private',
115 });
116 },
117 fn: async ({ bridgeBase }) => {
118 const res = await fetch(`${bridgeBase}/scooling/note-outline/smoke?path=inbox/a.md`);
119 const body = await res.json();
120 const serialized = JSON.stringify(body);
121
122 assert.equal(res.status, 200);
123 assert.deepEqual(body, validOutline());
124 assert.equal(serialized.includes('private body'), false);
125 assert.equal(serialized.includes('must-not-leak'), false);
126 assert.equal(serialized.includes('knowtation://'), false);
127 },
128 });
129 });
130
131 it('stress: repeated bridge calls stay deterministic', async () => {
132 const upstreamCalls = [];
133 await withBridgeServer({
134 upstreamHandler: (req, res) => {
135 upstreamCalls.push(req.originalUrl);
136 res.json(validOutline(String(req.query.path)));
137 },
138 fn: async ({ bridgeBase }) => {
139 const outputs = [];
140 for (let index = 0; index < 5; index += 1) {
141 const res = await fetch(`${bridgeBase}/scooling/note-outline/smoke?path=inbox/a.md`);
142 outputs.push(await res.text());
143 }
144 assert.equal(new Set(outputs).size, 1);
145 assert.equal(upstreamCalls.length, 5);
146 },
147 });
148 });
149
150 it('data-integrity: rejects unsafe paths before upstream fetch', async () => {
151 const upstreamCalls = [];
152 await withBridgeServer({
153 upstreamHandler: (req, res) => {
154 upstreamCalls.push(req.originalUrl);
155 res.status(500).json({ error: 'must not be called' });
156 },
157 fn: async ({ bridgeBase }) => {
158 for (const unsafePath of ['../secret.md', '/Users/private/a.md', 'C:\\Users\\private\\a.md']) {
159 const res = await fetch(
160 `${bridgeBase}/scooling/note-outline/smoke?path=${encodeURIComponent(unsafePath)}`,
161 );
162 const body = await res.json();
163 const serialized = JSON.stringify(body);
164 assert.equal(res.status, 400);
165 assert.equal(body.containsRawCredentials, false);
166 assert.equal(serialized.includes('secret.md'), false);
167 assert.equal(serialized.includes('/Users'), false);
168 }
169 assert.equal(upstreamCalls.length, 0);
170 },
171 });
172 });
173
174 it('performance: performs exactly one upstream NoteOutline GET per bridge request', async () => {
175 const upstreamCalls = [];
176 await withBridgeServer({
177 upstreamHandler: (req, res) => {
178 upstreamCalls.push({ method: req.method, url: req.originalUrl });
179 res.json(validOutline(String(req.query.path)));
180 },
181 fn: async ({ bridgeBase }) => {
182 const res = await fetch(`${bridgeBase}/scooling/note-outline/smoke?path=inbox/a.md`);
183 assert.equal(res.status, 200);
184 assert.deepEqual(upstreamCalls, [{ method: 'GET', url: '/api/v1/note-outline?path=inbox%2Fa.md' }]);
185 },
186 });
187 });
188
189 it('security: hides bridge unless enabled and sanitizes upstream failures', async () => {
190 await withBridgeServer({
191 routerOptions: { isEnabled: () => false },
192 upstreamHandler: (_req, res) => res.json(validOutline()),
193 fn: async ({ bridgeBase }) => {
194 const res = await fetch(`${bridgeBase}/scooling/note-outline/smoke?path=inbox/a.md`);
195 assert.equal(res.status, 404);
196 },
197 });
198
199 await withBridgeServer({
200 upstreamHandler: (_req, res) =>
201 res.status(403).json({ error: 'forbidden', body: 'private body', token: 'must-not-leak' }),
202 fn: async ({ bridgeBase }) => {
203 const res = await fetch(`${bridgeBase}/scooling/note-outline/smoke?path=inbox/a.md`);
204 const body = await res.json();
205 const serialized = JSON.stringify(body);
206 assert.equal(res.status, 403);
207 assert.equal(body.code, 'FORBIDDEN');
208 assert.equal(serialized.includes('private body'), false);
209 assert.equal(serialized.includes('must-not-leak'), false);
210 },
211 });
212
213 await withBridgeServer({
214 upstreamHandler: (_req, res) => res.json({ ...validOutline(), path: 'other.md' }),
215 fn: async ({ bridgeBase }) => {
216 const res = await fetch(`${bridgeBase}/scooling/note-outline/smoke?path=inbox/a.md`);
217 const body = await res.json();
218 assert.equal(res.status, 502);
219 assert.equal(body.code, 'BAD_GATEWAY');
220 assert.equal(body.returnedBodyText, false);
221 },
222 });
223 });
224 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 2 days ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 2 days ago