section-source-hosted-implementation-spec.test.mjs
245 lines 10.3 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * SectionSource hosted implementation spec tests.
3 *
4 * Phase 1K accepts the hosted implementation specification only. It must not
5 * register hosted get_section_source, add hosted ACLs, add Hub routes, add
6 * search, persistence, Scooling runtime behavior, section bodies, snippets,
7 * provider routing, 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 { allowedToolsForRole, isToolAllowed } from '../hub/gateway/mcp-tool-acl.mjs';
19 import { readSectionSource } from '../lib/section-source-note.mjs';
20
21 const __dirname = path.dirname(fileURLToPath(import.meta.url));
22 const repoRoot = path.dirname(__dirname);
23 const fixtureVault = path.join(__dirname, 'fixtures', 'vault-fs');
24 const specPath = path.join(repoRoot, 'docs', 'SECTION-SOURCE-HOSTED-IMPLEMENTATION-SPEC.md');
25
26 function readRepoFile(relativePath) {
27 return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
28 }
29
30 function makeCtx(overrides = {}) {
31 return {
32 userId: 'google:actor',
33 canisterUserId: 'google:owner',
34 vaultId: 'vault-section-source-hosted-spec',
35 role: 'viewer',
36 token: 'tok-section-source-hosted-spec',
37 canisterUrl: 'http://canister.test:4322',
38 bridgeUrl: 'http://bridge.test:4321',
39 canisterAuthSecret: 'gw-secret-section-source-hosted-spec',
40 ...overrides,
41 };
42 }
43
44 function installFetchMock(handler) {
45 const calls = [];
46 const origFetch = globalThis.fetch;
47 globalThis.fetch = async (url, init) => {
48 calls.push({ url: String(url), init });
49 return handler(String(url), init, calls);
50 };
51 return {
52 calls,
53 restore() {
54 globalThis.fetch = origFetch;
55 },
56 };
57 }
58
59 async function connectPair(ctx = makeCtx()) {
60 const mcpServer = createHostedMcpServer(ctx);
61 const client = new Client({ name: 'hosted-section-source-implementation-spec', version: '0.0.1' });
62 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
63 await mcpServer.connect(serverTransport);
64 await client.connect(clientTransport);
65 return { client };
66 }
67
68 function hostedRuntimeSource() {
69 return [
70 readRepoFile('hub/gateway/mcp-hosted-server.mjs'),
71 readRepoFile('hub/gateway/mcp-tool-acl.mjs'),
72 readRepoFile('hub/gateway/server.mjs'),
73 ].join('\n');
74 }
75
76 function assertHostedSectionSourcePresentAndScoped(source) {
77 assert.equal(source.includes('get_section_source'), true);
78 assert.equal(source.includes('buildSectionSource'), true);
79 assert.equal(source.includes('readSectionSource'), false);
80 }
81
82 describe('SectionSource hosted implementation spec', () => {
83 let mock;
84 let client;
85
86 afterEach(async () => {
87 try {
88 await client?.close();
89 } catch (_) {}
90 mock?.restore?.();
91 });
92
93 it('unit: spec covers the required hosted implementation decision areas', () => {
94 const spec = fs.readFileSync(specPath, 'utf8');
95 const requiredSections = [
96 '## Planning Decision',
97 '## Future Hosted Tool',
98 '## Input Schema',
99 '## Hosted Role ACL Requirements',
100 '## Active Vault Boundary',
101 '## Effective Canister User Boundary',
102 '## Canister Auth And Header Behavior',
103 '## One-Note Read Behavior',
104 '## Path Normalization And Unsafe Path Rejection',
105 '## Output Allowlist',
106 '## Explicitly Excluded Output',
107 '## Error Sanitization',
108 '## Logging Exclusions',
109 '## Deletion, Export, And Staleness',
110 '## Prompt-Injection Handling',
111 '## Scooling Consumption Boundary',
112 '## Seven-Tier Test Requirements',
113 '## Contract Guards',
114 '## Stop Conditions',
115 '## Acceptance Criteria',
116 ];
117 const requiredPhrases = [
118 'Phase 1K accepts the hosted implementation specification only.',
119 "isToolAllowed('get_section_source', role)",
120 'ctx.vaultId',
121 'ctx.canisterUserId || ctx.userId',
122 'GET {canisterUrl}/api/v1/notes/{encodeURIComponent(normalizedPath)}',
123 'Unsafe path errors must not echo the raw unsafe path.',
124 '"schema": "knowtation.section_source/v0"',
125 'Missing notes return a generic upstream status class such as `Upstream 404`.',
126 'Unauthorized notes return a generic upstream status class such as `Upstream 401` or',
127 'The future hosted tool must read one note only.',
128 'Scooling remains a downstream consumer behind its adapter boundary.',
129 ];
130
131 for (const section of requiredSections) {
132 assert.equal(spec.includes(section), true, `${section} is documented`);
133 }
134 for (const phrase of requiredPhrases) {
135 assert.equal(spec.includes(phrase), true, `${phrase} is documented`);
136 }
137 assert.match(readRepoFile('docs/SECTION-SOURCE-V0-SPEC.md'), /### Phase 1K: Hosted Implementation Spec/);
138 });
139
140 it('integration: hosted ACL and runtime expose get_section_source after the runtime phase', () => {
141 const runtime = hostedRuntimeSource();
142
143 for (const role of ['viewer', 'editor', 'evaluator', 'admin']) {
144 assert.equal(isToolAllowed('get_section_source', role), true, `${role} ACL allows get_section_source`);
145 assert.equal(allowedToolsForRole(role).has('get_section_source'), true, `${role} ACL includes get_section_source`);
146 }
147 assertHostedSectionSourcePresentAndScoped(runtime);
148 assert.equal(readRepoFile('hub/gateway/server.mjs').includes('get_section_source'), false);
149 });
150
151 it('end-to-end: hosted MCP can list and call get_section_source after the runtime phase', async () => {
152 mock = installFetchMock(() => ({
153 ok: true,
154 status: 200,
155 json: async () => ({
156 path: '/Users/owner/private/ignored.md',
157 frontmatter: '{"title":"Hosted Runtime"}',
158 body: '# A\n\nPrivate body must not leak.',
159 }),
160 text: async () => '{}',
161 }));
162
163 for (const role of ['viewer', 'editor', 'evaluator', 'admin']) {
164 ({ client } = await connectPair(makeCtx({ role })));
165 const { tools } = await client.listTools();
166
167 assert.equal(tools.some((tool) => tool.name === 'get_section_source'), true, `${role} can list get_section_source`);
168 const result = await client.callTool({ name: 'get_section_source', arguments: { path: 'inbox/one.md' } });
169 const data = JSON.parse(result.content[0].text);
170 assert.equal(result.isError, undefined, `${role} can call get_section_source`);
171 assert.equal(data.schema, 'knowtation.section_source/v0');
172 assert.equal(JSON.stringify(data).includes('Private body must not leak'), false);
173 await client.close();
174 client = undefined;
175 }
176 assert.equal(mock.calls.length, 4);
177 });
178
179 it('stress: hosted implementation spec checks remain bounded to contract files', () => {
180 const started = Date.now();
181 const files = [
182 'docs/SECTION-SOURCE-HOSTED-IMPLEMENTATION-SPEC.md',
183 'docs/SECTION-SOURCE-HOSTED-AUTHORIZATION-REVIEW-SPEC.md',
184 'docs/SECTION-SOURCE-SCOOLING-ADAPTER-PLANNING-SPEC.md',
185 'docs/SECTION-SOURCE-V0-SPEC.md',
186 'hub/gateway/mcp-hosted-server.mjs',
187 'hub/gateway/mcp-tool-acl.mjs',
188 ].map((relativePath) => readRepoFile(relativePath));
189 const elapsedMs = Date.now() - started;
190
191 assert.equal(files.length, 6);
192 assert.ok(elapsedMs < 250, `expected bounded hosted implementation spec check under 250ms, got ${elapsedMs}ms`);
193 });
194
195 it('data-integrity: no Hub, search, persistence, Scooling, provider, or resource surface is added', () => {
196 const source = readSectionSource(fixtureVault, 'inbox/one.md');
197 const serialized = JSON.stringify(source);
198 const runtime = hostedRuntimeSource();
199
200 assert.equal(source.schema, 'knowtation.section_source/v0');
201 assert.equal(source.sections.every((section) => section.body_returned === false), true);
202 assert.equal(source.sections.every((section) => section.snippet_returned === false), true);
203 assert.equal(Object.hasOwn(source, 'body'), false);
204 assert.equal(Object.hasOwn(source, 'frontmatter'), false);
205 assert.equal(Object.hasOwn(source, 'snippet'), false);
206 assert.equal(serialized.includes('Body of inbox one'), false);
207 assertHostedSectionSourcePresentAndScoped(runtime);
208 assert.equal(readRepoFile('hub/gateway/server.mjs').includes('get_section_source'), false);
209 assert.equal(readRepoFile('hub/gateway/server.mjs').includes('section_source/v0'), false);
210 });
211
212 it('performance: spec requires one-note reads and no scans or providers', () => {
213 const spec = fs.readFileSync(specPath, 'utf8');
214
215 assert.match(spec, /The future hosted tool must read one note only/);
216 assert.match(spec, /The future hosted tool must not scan the whole vault/);
217 assert.match(spec, /The future hosted tool must not call bridge search/);
218 assert.match(spec, /The future hosted tool must not call external providers/);
219 assert.match(spec, /Output size must remain bounded by accepted SectionSource caps/);
220 });
221
222 it('security: planning blocks hosted ACL, runtime, body, snippet, provider, and resource exposure', () => {
223 const spec = fs.readFileSync(specPath, 'utf8');
224 const runtime = hostedRuntimeSource();
225 const blockedPhrases = [
226 'Hosted runtime exposure remains blocked in this phase.',
227 'Hosted ACL exposure remains blocked in this phase.',
228 'No note body text appears in hosted SectionSource output.',
229 'No section body text appears in hosted SectionSource output.',
230 'No snippets appear in hosted SectionSource output.',
231 'No full frontmatter appears in hosted SectionSource output.',
232 'No absolute filesystem paths appear in hosted SectionSource output or errors.',
233 'No raw canister payload appears in hosted SectionSource output or errors.',
234 'No provider payload appears in hosted SectionSource output or errors.',
235 'No MCP resource URI appears for hosted SectionSource content.',
236 'Hub, search, persistence, Scooling, PageIndex, OCR, LLM, and provider exposure remain',
237 ];
238
239 for (const phrase of blockedPhrases) {
240 assert.equal(spec.includes(phrase), true, `${phrase} is documented`);
241 }
242 assertHostedSectionSourcePresentAndScoped(runtime);
243 assert.equal(readRepoFile('hub/gateway/server.mjs').includes('get_section_source'), false);
244 });
245 });
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