diff --git a/browse/src/server.ts b/browse/src/server.ts index 042616e75b..e63a87e949 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -324,6 +324,7 @@ const DIALOG_LOG_PATH = config.dialogLog; // terminal-agent.ts; chat queue + per-tab agent multiplexing are no // longer needed. +let lastConsoleFlushed = 0; let lastNetworkFlushed = 0; let lastDialogFlushed = 0; let flushInProgress = false; diff --git a/browse/test/server-flush-trackers.test.ts b/browse/test/server-flush-trackers.test.ts new file mode 100644 index 0000000000..306729af4e --- /dev/null +++ b/browse/test/server-flush-trackers.test.ts @@ -0,0 +1,73 @@ +/** + * Regression: flushBuffers state-tracker declaration audit. + * + * `flushBuffers()` (server.ts) maintains per-buffer cursors so it only + * appends *new* entries to each on-disk log on every interval tick: + * + * const newConsoleCount = consoleBuffer.totalAdded - lastConsoleFlushed; + * const newNetworkCount = networkBuffer.totalAdded - lastNetworkFlushed; + * const newDialogCount = dialogBuffer.totalAdded - lastDialogFlushed; + * + * The trackers must be declared with `let X = 0;` at module scope so the + * subtraction returns a real number on the first tick. If a tracker is + * referenced inside flushBuffers but never declared at module scope, the + * interval throws `ReferenceError: X is not defined` every second — the + * throw is swallowed by the catch at the bottom of flushBuffers (logged + * as `[browse] Buffer flush failed: is not defined`), the + * corresponding on-disk log file is *never written*, and the regression + * is silent in production. + * + * This source-level guard catches that exact class of regression — a + * future flush-perf refactor that adds a fourth buffer cursor (or a + * future contributor that copy-pastes the `last*Flushed` pattern without + * the matching declaration) will fail this test before it ships. + * + * Pattern matches `terminal-agent.test.ts` and `dual-listener.test.ts`: + * read source as text, assert an invariant, no daemon required. + */ + +import { describe, test, expect } from 'bun:test'; +import { readFileSync } from 'fs'; +import * as path from 'path'; + +const SERVER_TS = readFileSync( + path.resolve(import.meta.dir, '../src/server.ts'), + 'utf-8', +); + +describe('server.ts — flushBuffers tracker declarations', () => { + test('every `last*Flushed` tracker referenced inside flushBuffers is declared at module scope', () => { + // Locate the flushBuffers function body. The function is `async function + // flushBuffers() { ... }` — match through the closing brace at the start + // of a line (one-level-deep function in the file). + const fnMatch = SERVER_TS.match( + /async function flushBuffers\([^)]*\)[^{]*\{([\s\S]*?)\n\}/, + ); + expect(fnMatch, 'flushBuffers function not found in server.ts').not.toBeNull(); + const body = fnMatch![1]!; + + // Pull every identifier matching the `lastXxxFlushed` cursor pattern. + const trackerMatches = [...body.matchAll(/\blast([A-Z]\w+)Flushed\b/g)]; + const trackers = Array.from(new Set(trackerMatches.map((m) => `last${m[1]}Flushed`))); + + expect( + trackers.length, + 'flushBuffers should reference at least one last*Flushed tracker', + ).toBeGreaterThan(0); + + for (const tracker of trackers) { + // Module-level `let X = 0;` declaration (not inside a function body). + // Anchored start-of-line to avoid matching nested re-declarations or + // string literals. + const declared = new RegExp( + `(?:^|\\n)let\\s+${tracker}\\s*=\\s*0\\s*;`, + ).test(SERVER_TS); + expect( + declared, + `\`${tracker}\` is referenced inside flushBuffers but never declared at module scope ` + + `with \`let ${tracker} = 0;\` — the interval will throw ReferenceError every tick ` + + `and the corresponding on-disk log will never be written`, + ).toBe(true); + } + }); +});