section-source-hub-rest-openapi-spec.test.mjs
244 lines 9.7 KB
Raw
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2 feat(auth): persistent login system + C7 session introspection Human minor ⚠ breaking 16 days ago
1 /**
2 * SectionSource Hub REST/OpenAPI implementation spec tests.
3 *
4 * Phase 1M accepts Hub REST/OpenAPI planning only. It must not add Hub REST
5 * routes, OpenAPI paths or schemas, Hub UI, canister routes, search,
6 * persistence, Scooling runtime behavior, section bodies, snippets, providers,
7 * resource URIs, PageIndex, OCR, or LLM calls.
8 */
9 import { describe, it, afterEach } from 'node:test';
10 import assert from 'node:assert/strict';
11 import fs from 'fs';
12 import path from 'path';
13 import { fileURLToPath } from 'url';
14 import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15 import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
16
17 import { createHostedMcpServer } from '../hub/gateway/mcp-hosted-server.mjs';
18 import { readSectionSource } from '../lib/section-source-note.mjs';
19
20 const __dirname = path.dirname(fileURLToPath(import.meta.url));
21 const repoRoot = path.dirname(__dirname);
22 const fixtureVault = path.join(__dirname, 'fixtures', 'vault-fs');
23 const specPath = path.join(repoRoot, 'docs', 'SECTION-SOURCE-HUB-REST-OPENAPI-SPEC.md');
24
25 function readRepoFile(relativePath) {
26 return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
27 }
28
29 function makeCtx(overrides = {}) {
30 return {
31 userId: 'google:actor',
32 canisterUserId: 'google:owner',
33 vaultId: 'vault-section-source-rest-spec',
34 role: 'viewer',
35 token: 'tok-section-source-rest-spec',
36 canisterUrl: 'http://canister.test:4322',
37 bridgeUrl: 'http://bridge.test:4321',
38 canisterAuthSecret: 'gw-secret-section-source-rest-spec',
39 ...overrides,
40 };
41 }
42
43 function installFetchMock(handler) {
44 const calls = [];
45 const origFetch = globalThis.fetch;
46 globalThis.fetch = async (url, init) => {
47 calls.push({ url: String(url), init });
48 return handler(String(url), init, calls);
49 };
50 return {
51 calls,
52 restore() {
53 globalThis.fetch = origFetch;
54 },
55 };
56 }
57
58 async function connectPair(ctx = makeCtx()) {
59 const mcpServer = createHostedMcpServer(ctx);
60 const client = new Client({ name: 'section-source-rest-openapi-spec', version: '0.0.1' });
61 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
62 await mcpServer.connect(serverTransport);
63 await client.connect(clientTransport);
64 return { client };
65 }
66
67 function hubRuntimeSource() {
68 return [
69 readRepoFile('hub/gateway/server.mjs'),
70 readRepoFile('hub/icp/src/hub/main.mo'),
71 ].join('\n');
72 }
73
74 function openApiSource() {
75 return [
76 readRepoFile('docs/HUB-API.md'),
77 readRepoFile('docs/openapi.yaml'),
78 ].join('\n');
79 }
80
81 describe('SectionSource Hub REST/OpenAPI implementation spec', () => {
82 let mock;
83 let client;
84
85 afterEach(async () => {
86 try {
87 await client?.close();
88 } catch (_) {}
89 mock?.restore?.();
90 });
91
92 it('unit: spec covers required Hub REST/OpenAPI decision areas', () => {
93 const spec = fs.readFileSync(specPath, 'utf8');
94 const requiredSections = [
95 '## Planning Decision',
96 '## Future REST Endpoint',
97 '## REST Auth Requirements',
98 '## Active Vault Boundary',
99 '## Effective Canister User Boundary',
100 '## Canister Auth And Header Behavior',
101 '## One-Note Read Behavior',
102 '## Path Normalization And Unsafe Path Rejection',
103 '## Output Allowlist',
104 '## Explicitly Excluded Output',
105 '## Error Sanitization',
106 '## Logging Exclusions',
107 '## Deletion, Export, And Staleness',
108 '## Prompt-Injection Handling',
109 '## Hosted MCP Parity Boundary',
110 '## Scooling Consumption Boundary',
111 '## Seven-Tier Test Requirements',
112 '## Contract Guards',
113 '## Stop Conditions',
114 '## Acceptance Criteria',
115 ];
116 const requiredPhrases = [
117 'Phase 1M accepts the Hub REST/OpenAPI implementation specification only.',
118 'GET /api/v1/section-source?path=<vault-relative-note-path>',
119 'Authorization: Bearer <access_token>',
120 'X-Vault-Id: <active vault id>',
121 'X-User-Id: <effective canister user id>',
122 'GET {canisterUrl}/api/v1/notes/{encodeURIComponent(normalizedPath)}',
123 '"schema": "knowtation.section_source/v0"',
124 'Hosted MCP `get_section_source` remains available in this planning phase.',
125 'Scooling remains a downstream consumer behind its adapter boundary.',
126 ];
127
128 for (const section of requiredSections) {
129 assert.equal(spec.includes(section), true, `${section} is documented`);
130 }
131 for (const phrase of requiredPhrases) {
132 assert.equal(spec.includes(phrase), true, `${phrase} is documented`);
133 }
134 });
135
136 it('integration: Hub REST route and OpenAPI SectionSource surface are registered without canister routes', () => {
137 const gateway = readRepoFile('hub/gateway/server.mjs');
138 const canister = readRepoFile('hub/icp/src/hub/main.mo');
139 const api = openApiSource();
140
141 assert.equal(gateway.includes('/api/v1/section-source'), true);
142 assert.equal(gateway.includes('buildSectionSource'), true);
143 assert.equal(canister.includes('/api/v1/section-source'), false);
144 assert.equal(canister.includes('section_source/v0'), false);
145 assert.equal(api.includes('/section-source'), true);
146 assert.equal(api.includes('SectionSource'), true);
147 assert.equal(api.includes('knowtation.section_source/v0'), true);
148 });
149
150 it('end-to-end: hosted MCP get_section_source remains available after REST registration', async () => {
151 mock = installFetchMock(() => ({
152 ok: true,
153 status: 200,
154 json: async () => ({
155 path: 'ignored.md',
156 frontmatter: '{"title":"Hosted MCP Still Available"}',
157 body: '# A\n\nPrivate body must not leak.',
158 }),
159 text: async () => '{}',
160 }));
161 ({ client } = await connectPair());
162
163 const { tools } = await client.listTools();
164 const result = await client.callTool({
165 name: 'get_section_source',
166 arguments: { path: 'inbox/one.md' },
167 });
168 const data = JSON.parse(result.content[0].text);
169
170 assert.equal(tools.some((tool) => tool.name === 'get_section_source'), true);
171 assert.equal(result.isError, undefined);
172 assert.equal(data.schema, 'knowtation.section_source/v0');
173 assert.equal(JSON.stringify(data).includes('Private body must not leak'), false);
174 assert.equal(readRepoFile('hub/gateway/server.mjs').includes('/api/v1/section-source'), true);
175 });
176
177 it('stress: planning checks stay bounded to contract and route files', () => {
178 const started = Date.now();
179 const files = [
180 'docs/SECTION-SOURCE-HUB-REST-OPENAPI-SPEC.md',
181 'docs/SECTION-SOURCE-HOSTED-IMPLEMENTATION-SPEC.md',
182 'docs/SECTION-SOURCE-V0-SPEC.md',
183 'docs/HUB-API.md',
184 'docs/openapi.yaml',
185 'hub/gateway/server.mjs',
186 'hub/icp/src/hub/main.mo',
187 ].map((relativePath) => readRepoFile(relativePath));
188 const elapsedMs = Date.now() - started;
189
190 assert.equal(files.length, 7);
191 assert.ok(elapsedMs < 300, `expected bounded Hub REST/OpenAPI spec check under 300ms, got ${elapsedMs}ms`);
192 });
193
194 it('data-integrity: runtime adds no writes, sidecars, indexes, vectors, summaries, or persistence', () => {
195 const source = readSectionSource(fixtureVault, 'inbox/one.md');
196 const serialized = JSON.stringify(source);
197 const runtime = hubRuntimeSource();
198
199 assert.equal(source.schema, 'knowtation.section_source/v0');
200 assert.equal(source.sections.every((section) => section.body_returned === false), true);
201 assert.equal(source.sections.every((section) => section.snippet_returned === false), true);
202 assert.equal(Object.hasOwn(source, 'body'), false);
203 assert.equal(Object.hasOwn(source, 'frontmatter'), false);
204 assert.equal(Object.hasOwn(source, 'snippet'), false);
205 assert.equal(serialized.includes('Body of inbox one'), false);
206 assert.doesNotMatch(runtime, /section[-_]source[\s\S]{0,120}\b(write|post|put|delete|index|vector|summary|memory|sidecar)\b/i);
207 });
208
209 it('performance: spec requires one-note reads and no scans or providers', () => {
210 const spec = fs.readFileSync(specPath, 'utf8');
211
212 assert.match(spec, /The future endpoint must read one note only/);
213 assert.match(spec, /The future endpoint must not scan the whole vault/);
214 assert.match(spec, /The future endpoint must not call bridge search/);
215 assert.match(spec, /The future endpoint must not call external providers/);
216 assert.match(spec, /Output size must remain bounded by accepted SectionSource caps/);
217 });
218
219 it('security: runtime blocks body, snippet, provider, resource, canister, and Scooling exposure', () => {
220 const spec = fs.readFileSync(specPath, 'utf8');
221 const gateway = readRepoFile('hub/gateway/server.mjs');
222 const canister = readRepoFile('hub/icp/src/hub/main.mo');
223 const api = openApiSource();
224 const blockedPhrases = [
225 'No note body text appears in future SectionSource REST output.',
226 'No section body text appears in future SectionSource REST output.',
227 'No snippets appear in future SectionSource REST output.',
228 'No full frontmatter appears in future SectionSource REST output.',
229 'No absolute filesystem paths appear in future SectionSource REST output or errors.',
230 'No raw canister payload appears in future SectionSource REST output or errors.',
231 'No provider payload appears in future SectionSource REST output or errors.',
232 'No MCP resource URI appears for SectionSource REST content.',
233 'Search, persistence, Scooling, PageIndex, OCR, LLM, and provider exposure remain blocked.',
234 ];
235
236 for (const phrase of blockedPhrases) {
237 assert.equal(spec.includes(phrase), true, `${phrase} is documented`);
238 }
239 assert.equal(gateway.includes('/api/v1/section-source'), true);
240 assert.equal(canister.includes('/api/v1/section-source'), false);
241 assert.equal(api.includes('/section-source'), true);
242 assert.equal(api.includes('SectionSource'), true);
243 });
244 });
File History 1 commit
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2 feat(auth): persistent login system + C7 session introspection Human minor 16 days ago