feat: require a zero-error product typecheck and a test type-debt ratchet

Adds tsconfig.json (strict) over product code, fixes its remaining 90
diagnostics (type-only, interface corrections, and explicit narrowing),
and adds a typecheck job to the required free-tests aggregate running
bun run typecheck, the test-code ratchet (identity -> count baseline, fails
on new, repeated, or unlocked fixed diagnostics), and the lib/cso format
check. Reuses fixes from #2447 where they still applied.
This commit is contained in:
garrytan committed 2026-09-29 14:34:02 +00:00
1 parent 03e5911dba
commit 42e53413da
38 files changed
+1254 -62

No files matched your search

+18 -9
View File
@@ -15,6 +15,7 @@
* restores state. Falls back to clean slate on any failure.
*/
import type { ChildProcess } from 'node:child_process';
import { chromium, type Browser, type BrowserContext, type BrowserContextOptions, type Page, type Locator, type Cookie } from 'playwright';
import { writeSecureFile, mkdirSecure } from './file-permissions';
import { addConsoleEntry, addNetworkEntry, addDialogEntry, networkBuffer, type DialogEntry } from './buffers';
@@ -174,6 +175,12 @@ export function probePoisonedChromiumBundle(chromiumExecutablePath: string): voi
);
}
/** Playwright's public Browser type omits `process()`, which only browsers we launched provide. */
function launchedProcess(browser: Browser | null | undefined): ChildProcess | null {
const withProcess = browser as (Browser & { process?: () => ChildProcess | null }) | null | undefined;
return typeof withProcess?.process === 'function' ? withProcess.process() : null;
}
/**
* Resolve why the underlying Chromium ChildProcess is going away.
*
@@ -196,7 +203,7 @@ export async function resolveDisconnectCause(browser: Browser | null): Promise<'
// obtained via connectOverCDP() (or a stub in tests) has no such method —
// calling it blind throws inside the disconnect handler, which killed the
// whole daemon with "browser?.process is not a function".
const proc = typeof browser?.process === 'function' ? browser.process() : null;
const proc = launchedProcess(browser);
if (proc && proc.exitCode === null && proc.signalCode === null) {
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 1000);
@@ -599,7 +606,7 @@ export class BrowserManager {
// #2709: record the child's identity so the CLI can reap a survivor after
// daemon shutdown. `.process()` exists here — we launched this browser.
{
const proc = typeof this.browser.process === 'function' ? this.browser.process() : null;
const proc = launchedProcess(this.browser);
this.chromiumProcInfo = proc?.pid
? { pid: proc.pid, startTime: readPidStartTime(proc.pid) }
: null;
@@ -955,7 +962,7 @@ export class BrowserManager {
this.context ? this.context.close() : Promise.resolve(),
raceTimeout(this.closeRaceMs),
]).catch(() => {});
} else {
} else if (this.browser) {
// Launched mode: close the browser we spawned.
this.browser.removeAllListeners('disconnected');
// Grab the child handle BEFORE the race: nulling this.browser after a
@@ -963,7 +970,7 @@ export class BrowserManager {
// caller's event loop (and keep-alive connections into test servers)
// open forever — the intermittent whole-suite wedge. If graceful close
// doesn't finish in time, the child gets SIGKILL, not freedom.
const child = this.browser.process?.();
const child = launchedProcess(this.browser);
const closed = await Promise.race([
this.browser.close().then(() => true as const),
raceTimeout(this.closeRaceMs),
@@ -976,7 +983,7 @@ export class BrowserManager {
}
if (previousBrowser && previousBrowser !== currentBrowser) {
previousBrowser.removeAllListeners('disconnected');
const child = previousBrowser.process?.();
const child = launchedProcess(previousBrowser);
const closed = await Promise.race([
previousBrowser.close().then(() => true), raceTimeout(this.closeRaceMs),
]).catch(() => false);
@@ -2029,10 +2036,12 @@ export class BrowserManager {
tabSessions.delete(id);
console.log(`[browse] Tab closed (id=${id}, remaining=${pages.size})`);
// If the closed tab was active, switch to another
const state = pages === this.pages ? this : this.handoffPrevious?.pages === pages ? this.handoffPrevious : null;
if (state?.activeTabId === id) {
const remaining = [...pages.keys()];
state.activeTabId = remaining.length > 0 ? remaining[remaining.length - 1] : 0;
const remaining = [...pages.keys()];
const fallback = remaining.length > 0 ? remaining[remaining.length - 1]! : 0;
if (pages === this.pages) {
if (this.activeTabId === id) this.activeTabId = fallback;
} else if (this.handoffPrevious?.pages === pages && this.handoffPrevious.activeTabId === id) {
this.handoffPrevious.activeTabId = fallback;
}
break;
}
+2 -1
View File
@@ -289,7 +289,8 @@ export function appendSecureFile(
data: string | NodeJS.ArrayBufferView,
): void {
const existed = fs.existsSync(filePath);
fs.appendFileSync(filePath, data, { mode: 0o600 });
const payload = typeof data === 'string' ? data : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
fs.appendFileSync(filePath, payload, { mode: 0o600 });
if (!existed) restrictFilePermissions(filePath);
}
+1 -1
View File
@@ -174,7 +174,7 @@ export function combineVerdict(signals: LayerSignal[], opts: CombineVerdictOpts
for (const s of transcriptSignals) {
const v = classifyTranscript(s);
if (v === 'block') { transcriptVote = 'block'; break; }
if (v === 'warn' && transcriptVote !== 'block') transcriptVote = 'warn';
if (v === 'warn') transcriptVote = 'warn';
}
// Scalar-layer votes.
+2 -2
View File
@@ -2102,7 +2102,7 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
let body: any;
try { body = await req.json(); } catch { body = null; }
const sessionId = typeof body?.sessionId === 'string' ? body.sessionId : null;
const v = sessionId ? validateLease(sessionId) : { ok: false };
const v = sessionId ? validateLease(sessionId) : { ok: false as const };
if (!v.ok) {
// 410 Gone — session window has closed (lease expired or never
// existed). Client must fall back to /pty-session for a brand-new
@@ -2226,7 +2226,7 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
let body: any;
try { body = await req.json(); } catch { body = null; }
const sessionId = typeof body?.sessionId === 'string' ? body.sessionId : null;
const r = sessionId ? refreshLease(sessionId) : { ok: false };
const r = sessionId ? refreshLease(sessionId) : { ok: false as const };
if (!r.ok) {
return new Response(JSON.stringify({ error: 'lease expired or unknown' }), {
status: 410, headers: { 'Content-Type': 'application/json' },
+1 -1
View File
@@ -268,7 +268,7 @@ export async function handleSnapshot(
const parts: string[] = [];
let current: Element | null = el;
while (current && current !== document.documentElement) {
const parent = current.parentElement;
const parent: Element | null = current.parentElement;
if (!parent) break;
const siblings = [...parent.children];
const index = siblings.indexOf(current) + 1;
+1 -1
View File
@@ -132,7 +132,7 @@ export async function startSocksBridge(opts: {
clientSocket.once('close', () => inFlight.delete(clientSocket));
let state: State = 'greeting';
let buf = Buffer.alloc(0);
let buf: Buffer = Buffer.alloc(0);
let upstreamSocket: net.Socket | null = null;
const killBoth = (reason?: string) => {
+11 -6
View File
@@ -516,8 +516,13 @@ function maybeSpawnPty(ws: any, session: PtySession): boolean {
return true;
}
interface TerminalAgentWsData {
cookie: string;
sessionId: string | null;
}
function buildServer(port: number) {
return Bun.serve({
return Bun.serve<TerminalAgentWsData>({
hostname: '127.0.0.1',
// #2314: allocated from the SAME fixed 10000-60000 scan range the main
// server uses (port-allocator.ts, decision 8) — never `port: 0`. Binding
@@ -695,8 +700,8 @@ function buildServer(port: number) {
* after `spawned: true` is a no-op.
*/
open(ws) {
const sessionId = (ws.data as any)?.sessionId ?? null;
const cookie = (ws.data as any)?.cookie || '';
const sessionId = ws.data?.sessionId ?? null;
const cookie = ws.data?.cookie || '';
// Commit 3 re-attach: if this sessionId already has a detached
// PtySession in sessionsById, REPLACE its liveWs ref and replay
@@ -770,9 +775,9 @@ function buildServer(port: number) {
proc: null,
cols: 80,
rows: 24,
cookie: (ws.data as any)?.cookie || '',
cookie: ws.data?.cookie || '',
liveWs: ws,
sessionId: (ws.data as any)?.sessionId ?? null,
sessionId: ws.data?.sessionId ?? null,
spawned: false,
pingInterval: null,
ringBuffer: [],
@@ -850,7 +855,7 @@ function buildServer(port: number) {
// Always drop the WS-keyed map entry and the per-attach
// attachToken — the attach grant was single-use.
sessions.delete(ws);
const cookie = (ws.data as any)?.cookie;
const cookie = ws.data?.cookie;
if (cookie) validTokens.delete(cookie);
// A reattach can replace liveWs before the old socket's close arrives.
// That stale callback must not retire the new socket, grant or child.