mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
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:
1 parent
03e5911dba
commit
42e53413da
38 files changed
+1254
-62
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user