a73x

a493bcfe

test: make web routing checks comment safe

a73x   2026-08-18 12:27

Commit message
test: make web routing checks comment safe

web/verify.js
Old New
@@ -31,6 +31,155 @@ function check(name, got, want) {
31 else { failed++; console.error(`FAIL ${name}: got ${got}, want ${want}`); } 31 else { failed++; console.error(`FAIL ${name}: got ${got}, want ${want}`); }
32 } 32 }
33 33
34 // Replace comments with spaces, preserving newlines and every live token.
35 // This is a deliberately small lexer rather than a comment-shaped regex:
36 // mux.js uses strings and nested template expressions, and regex literals
37 // can themselves contain quotes and escaped comment delimiters. Regex-vs-
38 // division is decided from the preceding token, covering every expression
39 // form used by mux.js and the mutation fixture below.
40 function maskJsComments(source) {
41 const out = source.split('');
42 let i = 0;
43
44 const mask = (start, end) => {
45 for (let j = start; j < end; j++) {
46 if (out[j] !== '\n' && out[j] !== '\r') out[j] = ' ';
47 }
48 };
49
50 const skipQuoted = (quote) => {
51 i++;
52 while (i < source.length) {
53 if (source[i] === '\\') { i += 2; continue; }
54 const c = source[i++];
55 if (c === quote || c === '\n' || c === '\r') return;
56 }
57 };
58
59 const skipRegex = () => {
60 i++; // opening slash
61 let inClass = false;
62 while (i < source.length) {
63 if (source[i] === '\\') { i += 2; continue; }
64 const c = source[i++];
65 if (c === '[') inClass = true;
66 else if (c === ']') inClass = false;
67 else if (c === '/' && !inClass) {
68 while (i < source.length && /[a-z]/i.test(source[i])) i++;
69 return;
70 } else if (c === '\n' || c === '\r') return;
71 }
72 };
73
74 const expressionKeywords = new Set([
75 'await', 'case', 'delete', 'do', 'else', 'in', 'instanceof', 'new',
76 'of', 'return', 'throw', 'typeof', 'void', 'yield',
77 ]);
78
79 let scanCode;
80 const skipTemplate = () => {
81 i++; // opening backtick
82 while (i < source.length) {
83 if (source[i] === '\\') { i += 2; continue; }
84 if (source[i] === '`') { i++; return; }
85 if (source[i] === '$' && source[i + 1] === '{') {
86 i += 2;
87 scanCode(true);
88 continue;
89 }
90 i++;
91 }
92 };
93
94 scanCode = (templateExpression) => {
95 let expressionAllowed = true;
96 let braceDepth = 0;
97 while (i < source.length) {
98 const c = source[i];
99 const next = source[i + 1];
100
101 if (/\s/.test(c)) { i++; continue; }
102 if (templateExpression && c === '}' && braceDepth === 0) { i++; return; }
103
104 if (c === '/' && next === '/') {
105 const start = i;
106 i += 2;
107 while (i < source.length && source[i] !== '\n' && source[i] !== '\r') i++;
108 mask(start, i);
109 continue;
110 }
111 if (c === '/' && next === '*') {
112 const start = i;
113 i += 2;
114 while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) i++;
115 if (i < source.length) i += 2;
116 mask(start, i);
117 continue;
118 }
119 if (c === '"' || c === "'") {
120 skipQuoted(c);
121 expressionAllowed = false;
122 continue;
123 }
124 if (c === '`') {
125 skipTemplate();
126 expressionAllowed = false;
127 continue;
128 }
129 if (c === '/' && expressionAllowed) {
130 skipRegex();
131 expressionAllowed = false;
132 continue;
133 }
134
135 if (/[A-Za-z_$]/.test(c)) {
136 const start = i++;
137 while (i < source.length && /[A-Za-z0-9_$]/.test(source[i])) i++;
138 expressionAllowed = expressionKeywords.has(source.slice(start, i));
139 continue;
140 }
141 if (/[0-9]/.test(c)) {
142 i++;
143 while (i < source.length && /[A-Za-z0-9_.]/.test(source[i])) i++;
144 expressionAllowed = false;
145 continue;
146 }
147
148 if (c === '{') {
149 if (templateExpression) braceDepth++;
150 i++;
151 expressionAllowed = true;
152 } else if (c === '}') {
153 if (templateExpression) braceDepth--;
154 i++;
155 expressionAllowed = false;
156 } else if (c === ')' || c === ']') {
157 i++;
158 expressionAllowed = false;
159 } else if (c === '.' || ((c === '+' || c === '-') && next === c)) {
160 i += next === c ? 2 : 1;
161 expressionAllowed = false;
162 } else if (c === '/') {
163 i += next === '=' ? 2 : 1;
164 expressionAllowed = true;
165 } else {
166 i++;
167 expressionAllowed = true;
168 }
169 }
170 };
171
172 scanCode(false);
173 return out.join('');
174 }
175
176 function wasmCalls(source) {
177 const executable = maskJsComments(source);
178 return [...new Set(
179 [...executable.matchAll(/\bcore\.(mux_[a-z_0-9]+)/g)].map((m) => m[1]),
180 )].sort();
181 }
182
34 // --- wire builders (layouts golden-pinned in protocol.zig) --- 183 // --- wire builders (layouts golden-pinned in protocol.zig) ---
35 function snapshotPayload({ seq, history, cols, rows, epoch }, state) { 184 function snapshotPayload({ seq, history, cols, rows, epoch }, state) {
36 const stateBytes = Buffer.from(state, 'utf8'); 185 const stateBytes = Buffer.from(state, 'utf8');
@@ -294,14 +443,57 @@ async function main() {
294 // nowhere else, and the wasm builds fine without it. 443 // nowhere else, and the wasm builds fine without it.
295 const shell = fs.readFileSync(path.join(__dirname, 'mux.js'), 'utf8'); 444 const shell = fs.readFileSync(path.join(__dirname, 'mux.js'), 'utf8');
296 445
446 // Mutation fixture for the source checks below. A correct-looking route
447 // in either kind of comment must be invisible, while comment delimiters
448 // inside live literals must survive unchanged.
449 const lexicalFixture = [
450 '// const MSG = { term_modes: 0x8d, term_event: 0x8f };',
451 '/* const CLIENT_ACTION = { ignored: 0, terminalModes: 1, clipboard: 2, bell: 3 }; */',
452 '/* case MSG.term_modes:',
453 'case MSG.term_event: { const action = this.core.mux_client_frame(type, payload.length); }',
454 'case MSG.pty_mode: */',
455 '// sendPaste(text) { try { this.sendText(text); } finally {} }',
456 '/* onClipboardEffect() {} */',
457 '// this.core.mux_comment_decoy(1);',
458 'const stringLiteral = "https://host/*literal*/";',
459 'const templateLiteral = `// literal /* template */`;',
460 "const regexLiteral = /[\"']https?:\\/\\/host/; // remove this tail",
461 ].join('\n');
462 const executableFixture = maskJsComments(lexicalFixture);
463 check('source masker rejects line-comment declaration decoy', /\bconst MSG\b/.test(executableFixture), false);
464 check('source masker rejects block-comment declaration decoy', /\bconst CLIENT_ACTION\b/.test(executableFixture), false);
465 check('source masker rejects commented semantic-route decoy', /\bcase MSG\.term_modes\b/.test(executableFixture), false);
466 check('source masker rejects commented sendPaste decoy', /\bsendPaste\s*\(/.test(executableFixture), false);
467 check('source masker rejects commented placeholder decoy', /\bonClipboardEffect\s*\(/.test(executableFixture), false);
468 check('WASM-call scan rejects commented decoy', wasmCalls(lexicalFixture).includes('mux_comment_decoy'), false);
469 check('source masker removes a regex trailing comment', /remove this tail/.test(executableFixture), false);
470 check('source masker preserves string comment text', executableFixture.includes('"https://host/*literal*/"'), true);
471 check('source masker preserves template comment text', executableFixture.includes('`// literal /* template */`'), true);
472 check('source masker preserves regex comment text', executableFixture.includes('/[\"\']https?:\\/\\/host/'), true);
473
474 const executableShell = maskJsComments(shell);
475
297 // The browser must route sampled terminal state and host effects through 476 // The browser must route sampled terminal state and host effects through
298 // the same semantic core as the CLI. Pin the real page source here: this 477 // the same semantic core as the CLI. Pin the real page source here: this
299 // verifier otherwise exercises only the WASM half of that handshake. 478 // verifier otherwise exercises only the WASM half of that handshake.
300 const msgDecl = shell.match(/const MSG\s*=\s*\{([\s\S]*?)\};/)?.[1] ?? ''; 479 const msgBindings = executableShell.match(/\b(?:const|let|var)\s+MSG\b/g) ?? [];
480 const msgObjects = [...executableShell.matchAll(/\bconst MSG\s*=\s*\{([\s\S]*?)\};/g)];
481 check('shell has exactly one live MSG declaration', msgBindings.length, 1);
482 check('shell MSG declaration has the expected object shape', msgObjects.length, 1);
483 const msgDecl = msgObjects.length === 1 ? msgObjects[0][1] : '';
484 check('shell has one term_modes property', (msgDecl.match(/\bterm_modes\s*:/g) ?? []).length, 1);
485 check('shell has one term_event property', (msgDecl.match(/\bterm_event\s*:/g) ?? []).length, 1);
301 check('shell declares term_modes wire code', /\bterm_modes\s*:\s*0x8d\b/.test(msgDecl), true); 486 check('shell declares term_modes wire code', /\bterm_modes\s*:\s*0x8d\b/.test(msgDecl), true);
302 check('shell declares term_event wire code', /\bterm_event\s*:\s*0x8f\b/.test(msgDecl), true); 487 check('shell declares term_event wire code', /\bterm_event\s*:\s*0x8f\b/.test(msgDecl), true);
303 488
304 const actionDecl = shell.match(/const CLIENT_ACTION\s*=\s*\{([\s\S]*?)\};/)?.[1] ?? ''; 489 const actionBindings = executableShell.match(/\b(?:const|let|var)\s+CLIENT_ACTION\b/g) ?? [];
490 const actionObjects = [...executableShell.matchAll(/\bconst CLIENT_ACTION\s*=\s*\{([\s\S]*?)\};/g)];
491 check('shell has exactly one live CLIENT_ACTION declaration', actionBindings.length, 1);
492 check('shell CLIENT_ACTION declaration has the expected object shape', actionObjects.length, 1);
493 const actionDecl = actionObjects.length === 1 ? actionObjects[0][1] : '';
494 for (const name of ['ignored', 'terminalModes', 'clipboard', 'bell']) {
495 check(`shell has one ${name} client action`, (actionDecl.match(new RegExp(`\\b${name}\\s*:`, 'g')) ?? []).length, 1);
496 }
305 check( 497 check(
306 'shell pins shared client action ABI', 498 'shell pins shared client action ABI',
307 /\bignored\s*:\s*0\b/.test(actionDecl) 499 /\bignored\s*:\s*0\b/.test(actionDecl)
@@ -311,10 +503,14 @@ async function main() {
311 true, 503 true,
312 ); 504 );
313 505
314 const semanticCases = shell.match( 506 const semanticRoutes = [...executableShell.matchAll(
315 /case MSG\.term_modes:\s*case MSG\.term_event:\s*\{([\s\S]*?)\n\s*\}\s*case MSG\.pty_mode:/, 507 /case\s+MSG\.term_modes\s*:\s*case\s+MSG\.term_event\s*:\s*\{([\s\S]*?)\}\s*case\s+MSG\.pty_mode\s*:/g,
316 )?.[1] ?? ''; 508 )];
317 check('shell shares one semantic frame route', semanticCases.length > 0, true); 509 check('shell has one live term_modes case', (executableShell.match(/case\s+MSG\.term_modes\s*:/g) ?? []).length, 1);
510 check('shell has one live term_event case', (executableShell.match(/case\s+MSG\.term_event\s*:/g) ?? []).length, 1);
511 check('shell shares exactly one semantic frame route', semanticRoutes.length, 1);
512 const semanticCases = semanticRoutes.length === 1 ? semanticRoutes[0][1] : '';
513 check('shell stages a semantic payload exactly once', (semanticCases.match(/this\.stage\(payload\)/g) ?? []).length, 1);
318 check( 514 check(
319 'shell stages semantic payload before WASM', 515 'shell stages semantic payload before WASM',
320 /if\s*\(\s*!this\.stage\(payload\)\s*\)\s*return\s*;/.test(semanticCases), 516 /if\s*\(\s*!this\.stage\(payload\)\s*\)\s*return\s*;/.test(semanticCases),
@@ -325,6 +521,7 @@ async function main() {
325 /(?:const|let)\s+action\s*=\s*this\.core\.mux_client_frame\(type,\s*payload\.length\)\s*;/.test(semanticCases), 521 /(?:const|let)\s+action\s*=\s*this\.core\.mux_client_frame\(type,\s*payload\.length\)\s*;/.test(semanticCases),
326 true, 522 true,
327 ); 523 );
524 check('shell calls the semantic WASM entry exactly once', (semanticCases.match(/\.mux_client_frame\s*\(/g) ?? []).length, 1);
328 check( 525 check(
329 'shell dispatches only the clipboard action', 526 'shell dispatches only the clipboard action',
330 /if\s*\(\s*action\s*===\s*CLIENT_ACTION\.clipboard\s*\)\s*this\.onClipboardEffect\(\)\s*;/.test(semanticCases) 527 /if\s*\(\s*action\s*===\s*CLIENT_ACTION\.clipboard\s*\)\s*this\.onClipboardEffect\(\)\s*;/.test(semanticCases)
@@ -334,12 +531,21 @@ async function main() {
334 check('shell leaves semantic payload parsing to WASM', /DataView|TextDecoder|payload\s*\[/.test(semanticCases), false); 531 check('shell leaves semantic payload parsing to WASM', /DataView|TextDecoder|payload\s*\[/.test(semanticCases), false);
335 check( 532 check(
336 'shell has no duplicate bracketed-paste state', 533 'shell has no duplicate bracketed-paste state',
337 /this\.(?:bracketedPaste|bracketed_paste)\s*=/.test(shell), 534 /this\.(?:bracketedPaste|bracketed_paste)\s*=/.test(executableShell),
338 false, 535 false,
339 ); 536 );
340 check('shell clipboard effect is an empty placeholder', /onClipboardEffect\s*\(\s*\)\s*\{\s*\}/.test(shell), true); 537 const clipboardMethods = executableShell.match(/\bonClipboardEffect\s*\([^)]*\)\s*\{/g) ?? [];
341 538 const emptyClipboardMethods = executableShell.match(/\bonClipboardEffect\s*\(\s*\)\s*\{\s*\}/g) ?? [];
342 const sendPaste = shell.match(/sendPaste\(text\)\s*\{([\s\S]*?)\n\s*\}\n\s*sendResizeIfDiffers/)?.[1] ?? ''; 539 check('shell has exactly one live clipboard effect method', clipboardMethods.length, 1);
540 check('shell clipboard effect is the one empty placeholder', emptyClipboardMethods.length, 1);
541
542 const sendPasteMethods = executableShell.match(/\bsendPaste\s*\([^)]*\)\s*\{/g) ?? [];
543 const sendPasteBodies = [...executableShell.matchAll(
544 /\bsendPaste\s*\(\s*text\s*\)\s*\{([\s\S]*?)\}\s*sendResizeIfDiffers/g,
545 )];
546 check('shell has exactly one live sendPaste method', sendPasteMethods.length, 1);
547 check('shell sendPaste has the expected text contract', sendPasteBodies.length, 1);
548 const sendPaste = sendPasteBodies.length === 1 ? sendPasteBodies[0][1] : '';
343 check('shell paste has one begin call', (sendPaste.match(/mux_paste_begin\(\)/g) ?? []).length, 1); 549 check('shell paste has one begin call', (sendPaste.match(/mux_paste_begin\(\)/g) ?? []).length, 1);
344 check('shell paste has one end call', (sendPaste.match(/mux_paste_end\(\)/g) ?? []).length, 1); 550 check('shell paste has one end call', (sendPaste.match(/mux_paste_end\(\)/g) ?? []).length, 1);
345 check( 551 check(
@@ -348,9 +554,7 @@ async function main() {
348 true, 554 true,
349 ); 555 );
350 556
351 const called = [...new Set( 557 const called = wasmCalls(shell);
352 [...shell.matchAll(/\bcore\.(mux_[a-z_0-9]+)/g)].map((m) => m[1]),
353 )].sort();
354 check('shell calls something', called.length > 0, true); 558 check('shell calls something', called.length > 0, true);
355 const absent = called.filter((n) => typeof e[n] !== 'function'); 559 const absent = called.filter((n) => typeof e[n] !== 'function');
356 check(`shell calls only real exports (${called.length} of them)`, absent.join(','), ''); 560 check(`shell calls only real exports (${called.length} of them)`, absent.join(','), '');