fix(server): one lone-surrogate sanitizer, one sanitizeReplacer, one startTunnel

Three copies of the surrogate sanitizer existed with two algorithms
(sanitize.ts regex vs a hand-rolled charCodeAt walk in server.ts — verified
byte-identical across 11 edge cases before converging) plus two identical
sanitizeReplacer definitions each wrapping a different copy. sanitize.ts is
now the single source of truth; the runs-INSIDE-JSON.stringify egress
invariant is unchanged at every call site and its pin tests were adapted to
the new import shape without losing intent.

The ngrok tunnel-start sequence existed three times in server.ts — the
/tunnel/start route and the BROWSE_TUNNEL=1 autostart were line-for-line
equivalent (a comment admitted 'Same cleanup as /tunnel/start's error path').
One startTunnel() now owns the ephemeral loopback bind, the pre-send egress
receipt, the state-file RMW via tmpStatePath(), and the ordered error-path
cleanup; callers keep their distinct response surfaces. The
BROWSE_TUNNEL_LOCAL_ONLY test path shares nothing (no ngrok, different state
field) and deliberately stays separate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-14 21:12:21 -07:00
co-authored by Claude Fable 5
parent 60134c4320
commit e426293017
5 changed files with 231 additions and 195 deletions
+31 -24
View File
@@ -2,23 +2,15 @@ import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
// The sanitizer is module-private in server.ts. Rather than refactor it to a
// separate module just for testing, we extract its source via a regex slice and
// eval it in a fresh function scope. Keeps the production layout untouched.
// The sanitizer used to be module-private in server.ts (extracted here via a
// regex slice + eval). It now lives in sanitize.ts as the single source of
// truth for server.ts, sse-helpers.ts, and the read/snapshot pipeline — so
// this suite imports the canonical export and pins the server.ts wiring.
import { stripLoneSurrogates as sanitizeLoneSurrogates } from '../src/sanitize';
const SERVER_PATH = path.resolve(import.meta.dir, '..', 'src', 'server.ts');
const SERVER_SRC = fs.readFileSync(SERVER_PATH, 'utf-8');
const fnMatch = SERVER_SRC.match(
/function sanitizeLoneSurrogates\(str: string\): string \{[\s\S]*?\n\}/
);
if (!fnMatch) throw new Error('Could not locate sanitizeLoneSurrogates in server.ts');
// Strip TS annotations so eval works under plain JS.
const jsSrc = fnMatch[0].replace('(str: string): string', '(str)');
const sanitizeLoneSurrogates = new Function(`${jsSrc}\nreturn sanitizeLoneSurrogates;`)() as (
s: string,
) => string;
describe('sanitizeLoneSurrogates — unit cases', () => {
test('passthrough ASCII', () => {
expect(sanitizeLoneSurrogates('hello')).toBe('hello');
@@ -110,7 +102,7 @@ describe('sanitizeLoneSurrogates — wiring invariants', () => {
// refactor moves sanitization back to handleCommand only, this test
// fails by detecting the missing wrapper.
expect(SERVER_SRC).toContain('async function handleCommandInternalImpl(');
expect(SERVER_SRC).toContain('result: sanitizeLoneSurrogates(cr.result)');
expect(SERVER_SRC).toContain('result: stripLoneSurrogates(cr.result)');
});
test('SSE activity feed routes outbound frames through createSseEndpoint', () => {
@@ -142,16 +134,31 @@ describe('sanitizeLoneSurrogates — wiring invariants', () => {
const helperSrc = fs.readFileSync(helperPath, 'utf-8');
expect(helperSrc).toContain('JSON.stringify(');
expect(helperSrc).toContain('sanitizeReplacer');
// The sanitizer itself uses stripLoneSurrogates (the shared utility in
// sanitize.ts) — not a private copy. Re-confirms the helper is wired
// to the canonical sanitizer, not a drift'd duplicate.
expect(helperSrc).toContain("import { stripLoneSurrogates } from './sanitize'");
// The replacer is the canonical export from sanitize.ts — not a private
// copy. Re-confirms the helper is wired to the canonical sanitizer, not
// a drift'd duplicate.
expect(helperSrc).toContain("import { sanitizeReplacer } from './sanitize'");
});
test('sanitizeReplacer is a function defined in server.ts (for non-SSE egress)', () => {
// server.ts keeps its own sanitizeReplacer for the non-SSE JSON egress
// paths (handleCommandInternal etc.). The SSE path uses sse-helpers.ts's
// own sanitizeReplacer; both must exist independently.
expect(SERVER_SRC).toContain('function sanitizeReplacer(');
test('sanitizeReplacer is the canonical export wrapping stripLoneSurrogates', () => {
// Single source of truth: sanitize.ts defines the one replacer, and it
// must wrap the shared stripLoneSurrogates (a fast-path rewrite that
// stops sanitizing string values would regress every JSON egress at once).
const sanitizePath = path.resolve(import.meta.dir, '..', 'src', 'sanitize.ts');
const sanitizeSrc = fs.readFileSync(sanitizePath, 'utf-8');
expect(sanitizeSrc).toContain('export function sanitizeReplacer(');
expect(sanitizeSrc).toContain(
"typeof value === 'string' ? stripLoneSurrogates(value) : value",
);
});
test('server.ts imports sanitizeReplacer for non-SSE JSON egress and still uses it', () => {
// server.ts used to define its own private sanitizeReplacer for the
// non-SSE JSON egress paths (/pty-inject-scan, /memory snapshot, etc.).
// It now imports the canonical one — and must still pass it at those
// JSON.stringify egress sites.
expect(SERVER_SRC).toMatch(/import \{[^}]*sanitizeReplacer[^}]*\} from '\.\/sanitize'/);
expect(SERVER_SRC).not.toContain('function sanitizeReplacer(');
expect(SERVER_SRC).toContain(', sanitizeReplacer)');
});
});