hub-section-source-ui-runtime.test.mjs file-level

at sha256:f · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:9 feat(calendar): hosted bridge/gateway route parity and timeline noteRec… · aaronrene · Jun 19, 2026
1 /**
2 * Hub UI SectionSource runtime tests.
3 *
4 * Phase 1P exposes a minimal body-free SectionSource UI surface in the note
5 * detail drawer. It must call only the accepted REST route, render escaped
6 * allowlist fields, and avoid canister, search, persistence, Scooling,
7 * provider, resource, body, snippet, and write-back surfaces.
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
19 const __dirname = path.dirname(fileURLToPath(import.meta.url));
20 const repoRoot = path.dirname(__dirname);
21
22 function readRepoFile(relativePath) {
23 return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
24 }
25
26 function sourceSlice(source, startNeedle, endNeedle) {
27 const start = source.indexOf(startNeedle);
28 const end = source.indexOf(endNeedle, start);
29 assert.notEqual(start, -1, `missing ${startNeedle}`);
30 assert.notEqual(end, -1, `missing ${endNeedle}`);
31 return source.slice(start, end);
32 }
33
34 function hubSectionSourceRuntime() {
35 return sourceSlice(
36 readRepoFile('web/hub/hub.js'),
37 'const SECTION_SOURCE_SCHEMA',
38 'function switchNoteToReadMode',
39 );
40 }
41
42 function hubUiSource() {
43 return [
44 readRepoFile('web/hub/index.html'),
45 readRepoFile('web/hub/hub.js'),
46 readRepoFile('web/hub/hub.css'),
47 ].join('\n');
48 }
49
50 function makeCtx(overrides = {}) {
51 return {
52 userId: 'google:actor',
53 canisterUserId: 'google:owner',
54 vaultId: 'vault-section-source-ui-runtime',
55 role: 'viewer',
56 token: 'tok-section-source-ui-runtime',
57 canisterUrl: 'http://canister.test:4322',
58 bridgeUrl: 'http://bridge.test:4321',
59 canisterAuthSecret: 'gw-secret-section-source-ui-runtime',
60 ...overrides,
61 };
62 }
63
64 function installFetchMock(handler) {
65 const calls = [];
66 const origFetch = globalThis.fetch;
67 globalThis.fetch = async (url, init) => {
68 calls.push({ url: String(url), init });
69 return handler(String(url), init, calls);
70 };
71 return {
72 calls,
73 restore() {
74 globalThis.fetch = origFetch;
75 },
76 };
77 }
78
79 async function connectPair(ctx = makeCtx()) {
80 const mcpServer = createHostedMcpServer(ctx);
81 const client = new Client({ name: 'section-source-hub-ui-runtime', version: '0.0.1' });
82 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
83 await mcpServer.connect(serverTransport);
84 await client.connect(clientTransport);
85 return { client };
86 }
87
88 describe('Hub UI SectionSource runtime', () => {
89 let mock;
90 let client;
91
92 afterEach(async () => {
93 try {
94 await client?.close();
95 } catch (_) {}
96 mock?.restore?.();
97 });
98
99 it('unit: defines the intended detail-drawer SectionSource UI surface', () => {
100 const js = readRepoFile('web/hub/hub.js');
101 const html = readRepoFile('web/hub/index.html');
102 const css = readRepoFile('web/hub/hub.css');
103
104 assert.match(js, /function createSectionSourceButton\(actionsEl\)/);
105 assert.match(js, /sectionBtn\.textContent = 'Sections'/);
106 assert.match(js, /sectionBtn\.setAttribute\('aria-expanded', 'false'\)/);
107 assert.match(js, /data-section-source-panel/);
108 assert.match(html, /hub\.js\?v=20260603b/);
109 assert.match(html, /hub\.css\?v=20260526a/);
110 assert.match(css, /\.section-source-panel\b/);
111 assert.match(css, /\.section-source-list\b/);
112 });
113
114 it('unit: renders readable section rows without inline raw ID dumps', () => {
115 const runtime = hubSectionSourceRuntime();
116 const renderer = sourceSlice(runtime, 'function renderSectionSourceData', 'async function loadSectionSourceForCurrentNote');
117 const visibleRows = sourceSlice(renderer, 'const heading = document.createElement', 'const debugDetails = document.createElement');
118 const debugRows = sourceSlice(renderer, 'const debugDetails = document.createElement', 'list.appendChild(item)');
119
120 assert.match(renderer, /levelBadge\.textContent = 'H' \+ section\.level/);
121 assert.match(renderer, /detail\.textContent = 'Heading level: H' \+ section\.level/);
122 assert.match(renderer, /pathLine\.textContent =\s*'Heading path: '/);
123 assert.match(renderer, /childLine\.textContent = 'Child sections: ' \+ section\.child_section_ids\.length/);
124 assert.doesNotMatch(visibleRows, /section\.section_id|section\.heading_id|child_section_ids\.join/);
125 assert.match(debugRows, /const debugDetails = document\.createElement\('details'\)/);
126 assert.match(debugRows, /debugSummary\.textContent = 'IDs'/);
127 assert.match(debugRows, /appendSectionSourceDebugRow\(\s*debugList,\s*'Section ID',\s*section\.section_id/s);
128 assert.match(debugRows, /appendSectionSourceDebugRow\(\s*debugList,\s*'Heading ID',\s*section\.heading_id/s);
129 assert.match(debugRows, /section\.child_section_ids\.join\(', '\)/);
130 assert.doesNotMatch(renderer, /body returned|snippet returned/i);
131 });
132
133 it('integration: calls only the accepted REST endpoint shape from the Hub UI runtime', () => {
134 const runtime = hubSectionSourceRuntime();
135 const gateway = readRepoFile('hub/gateway/server.mjs');
136 const canister = readRepoFile('hub/icp/src/hub/main.mo');
137
138 assert.match(runtime, /return '\/api\/v1\/section-source\?path=' \+ encodeURIComponent\(path\)/);
139 assert.match(runtime, /api\(sectionSourceEndpointForPath\(path\), \{ method: 'GET' \}\)/);
140 assert.doesNotMatch(runtime, /body:\s*JSON\.stringify/);
141 assert.equal(gateway.includes('/api/v1/section-source'), true);
142 assert.equal(canister.includes('section-source'), false);
143 assert.equal(canister.includes('SectionSource'), false);
144 });
145
146 it('end-to-end: hosted MCP remains available while Hub UI runtime is present', async () => {
147 mock = installFetchMock(() => ({
148 ok: true,
149 status: 200,
150 json: async () => ({
151 path: 'ignored.md',
152 frontmatter: '{"title":"Hosted MCP Still Available"}',
153 body: '# A\n\nPrivate body must not leak.',
154 }),
155 text: async () => '{}',
156 }));
157 ({ client } = await connectPair());
158
159 const result = await client.callTool({
160 name: 'get_section_source',
161 arguments: { path: 'inbox/one.md' },
162 });
163 const data = JSON.parse(result.content[0].text);
164
165 assert.equal(data.schema, 'knowtation.section_source/v0');
166 assert.equal(JSON.stringify(data).includes('Private body must not leak'), false);
167 assert.equal(hubUiSource().includes('/api/v1/section-source'), true);
168 });
169
170 it('stress: runtime checks stay bounded to Hub UI, REST, OpenAPI, and contract files', () => {
171 const started = Date.now();
172 const files = [
173 'web/hub/index.html',
174 'web/hub/hub.js',
175 'web/hub/hub.css',
176 'hub/gateway/server.mjs',
177 'docs/openapi.yaml',
178 'docs/SECTION-SOURCE-HUB-UI-SPEC.md',
179 'docs/SECTION-SOURCE-V0-SPEC.md',
180 'test/hub-section-source-ui-runtime.test.mjs',
181 ].map((relativePath) => readRepoFile(relativePath));
182 const elapsedMs = Date.now() - started;
183
184 assert.equal(files.length, 8);
185 assert.ok(elapsedMs < 300, `expected bounded Hub UI runtime check under 300ms, got ${elapsedMs}ms`);
186 });
187
188 it('data-integrity: adds no SectionSource UI storage, writes, sidecars, indexes, vectors, summaries, or memory', () => {
189 const runtime = hubSectionSourceRuntime();
190
191 assert.doesNotMatch(runtime, /\blocalStorage\b/);
192 assert.doesNotMatch(runtime, /\bsessionStorage\b/);
193 assert.doesNotMatch(runtime, /\bwrite(File|Text)?\b/i);
194 assert.doesNotMatch(runtime, /\b(method:\s*['"](POST|PUT|PATCH|DELETE)['"]|deleteOpenNote|switchNoteToEditMode)\b/);
195 assert.doesNotMatch(runtime, /\b(index|vector|memory|sidecar|Scooling)\b/i);
196 assert.doesNotMatch(runtime, /\b(summarize|summarization|summaries)\b/i);
197 });
198
199 it('performance: keeps SectionSource UI reads one-note, no-store, scan-free, and provider-free', () => {
200 const runtime = hubSectionSourceRuntime();
201 const apiHelper = sourceSlice(readRepoFile('web/hub/hub.js'), 'async function api', '/** Busy state');
202
203 assert.match(runtime, /sectionSourceEndpointForPath\(path\)/);
204 assert.match(runtime, /method: 'GET'/);
205 assert.doesNotMatch(runtime, /\/api\/v1\/notes\?/);
206 assert.doesNotMatch(runtime, /\/api\/v1\/search|\/api\/v1\/index|PageIndex|OCR|LLM/i);
207 assert.match(apiHelper, /cache: fetchOpts\.cache != null \? fetchOpts\.cache : 'no-store'/);
208 });
209
210 it('security: renders allowlisted fields as text and uses sanitized error states', () => {
211 const runtime = hubSectionSourceRuntime();
212
213 assert.match(runtime, /const SECTION_SOURCE_SCHEMA = 'knowtation\.section_source\/v0'/);
214 assert.match(runtime, /SECTION_SOURCE_FORBIDDEN_KEYS/);
215 assert.match(runtime, /body_returned: item\.body_returned === true/);
216 assert.match(runtime, /snippet_returned: item\.snippet_returned === true/);
217 assert.match(runtime, /headingText\.textContent = section\.heading_text/);
218 assert.match(runtime, /pathLine\.textContent =\s*'Heading path: '/);
219 assert.match(runtime, /const debugDetails = document\.createElement\('details'\)/);
220 assert.match(runtime, /return 'Sections are unavailable right now\.'/);
221 assert.match(runtime, /return 'Sections are unavailable for this session\.'/);
222 assert.doesNotMatch(runtime, /\.innerHTML\s*=/);
223 assert.doesNotMatch(runtime, /renderNoteMarkdownHtml|marked|DOMPurify|console\.(log|error|warn)/);
224 });
225 });