From ac6dff19a156159a21a4e06fd3eaac09420c4cca Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 26 Oct 2026 12:40:09 +0000 Subject: [PATCH 1/5] tests: lint scripts/**/*.test.ts and vscode/src with ESLint and oxlint; fix their findings --- .oxlintrc.json | 44 ++++++++++++++ eslint.config.mjs | 60 +++++++++++++++++++ scripts/check-startup-imports-runtime.test.ts | 1 + scripts/checkOxlintConfig.test.ts | 2 +- scripts/dry/check.test.ts | 23 ++++--- scripts/generate-npm-shrinkwrap.test.ts | 1 + scripts/lib/lint_targets.js | 4 +- scripts/lint.sh | 2 + scripts/mutate/check.test.ts | 5 +- scripts/notarize-mac.test.ts | 2 +- scripts/orpcSchemasBarrelLint.test.ts | 14 +++-- scripts/perf/analyzeProfilesCore.test.ts | 18 ++++-- scripts/perf/bench.test.ts | 1 + scripts/perf/desktopColdStartStats.test.ts | 10 ++-- scripts/perf/loopTestBaseline.test.ts | 7 ++- scripts/perf/loopTestStats.test.ts | 2 +- scripts/perf/loopTestTiming.test.ts | 7 ++- scripts/perf/pageTimingCore.test.ts | 9 ++- scripts/perf/pageTimingStats.test.ts | 2 +- scripts/perf/perfReport.test.ts | 10 ++-- scripts/perf/perfTrend.test.ts | 3 +- scripts/perf/perfTrendCore.test.ts | 3 +- vscode/src/extension.test.ts | 5 +- vscode/src/extension.ts | 20 ++++--- vscode/src/orpcAllowlist.ts | 2 +- vscode/src/parseWebviewToExtensionMessage.ts | 2 +- vscode/src/webview/App.test.tsx | 13 ++-- vscode/src/webview/App.tsx | 6 +- vscode/src/webview/WorkspacePicker.tsx | 2 +- .../src/webview/createVscodeOrpcLink.test.ts | 40 +++++++------ vscode/src/webview/createVscodeOrpcLink.ts | 8 ++- vscode/src/webview/protocol.ts | 24 ++++---- vscode/src/webview/vscodeBridge.ts | 11 ++-- vscode/src/workspaceActivityPump.test.ts | 4 +- 34 files changed, 255 insertions(+), 112 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 91701485e74..c1e529573e4 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1108,6 +1108,50 @@ "document": "readonly" } }, + { + "files": ["scripts/**/*.test.ts", "vscode/src/**/*.{ts,tsx}"], + "rules": { + "no-unused-vars": [ + "error", + { + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": true, + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrors": "all" + } + ] + }, + "globals": { + "console": "readonly", + "process": "readonly", + "Buffer": "readonly", + "__dirname": "readonly", + "__filename": "readonly", + "require": "readonly", + "setTimeout": "readonly", + "clearTimeout": "readonly", + "setInterval": "readonly", + "clearInterval": "readonly", + "window": "readonly", + "document": "readonly" + } + }, + { + "files": ["vscode/src/**/*.tsx"], + "plugins": ["react"], + "rules": { + "react/rules-of-hooks": "error", + "react/exhaustive-deps": "warn" + } + }, + { + "files": ["scripts/**/*.test.ts", "vscode/src/**/*.test.{ts,tsx}"], + "rules": { + "no-empty-function": "off" + } + }, { "files": ["**/*.test.ts", "**/*.test.tsx"], "globals": { diff --git a/eslint.config.mjs b/eslint.config.mjs index 49e7b1f7892..77cba8ead0e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -922,6 +922,66 @@ export default defineConfig([ ], }, }, + { + // Tooling tests (scripts/**/*.test.ts) and the VS Code extension (vscode/src) are type-checked + // by tsconfig.tooling.json (make typecheck), not by the tsconfig.json the project service + // would pick, so lint them against that program with the same base rules as tests/. + files: ["scripts/**/*.test.ts", "vscode/src/**/*.{ts,tsx}"], + plugins: { + local: localPlugin, + "react-hooks": reactHooks, + }, + languageOptions: { + parserOptions: { + project: "./tsconfig.tooling.json", + tsconfigRootDir: import.meta.dirname, + }, + globals: { + console: "readonly", + process: "readonly", + Buffer: "readonly", + __dirname: "readonly", + __filename: "readonly", + require: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + window: "readonly", + document: "readonly", + }, + }, + rules: { + // Same options as src/ and tests/: a leading underscore marks an intentionally unused binding. + "@typescript-eslint/no-unused-vars": [ + "error", + { + vars: "all", + args: "after-used", + ignoreRestSiblings: true, + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrors: "all", + }, + ], + }, + }, + { + // The webview is React: run the two hook rules src/ runs (src/ also runs the compiler rules). + files: ["vscode/src/**/*.tsx"], + rules: { + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + }, + }, + { + // Like tests/: test doubles implement async interfaces with synchronous or empty bodies. + files: ["scripts/**/*.test.ts", "vscode/src/**/*.test.{ts,tsx}"], + rules: { + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-empty-function": "off", + }, + }, { // Test file configuration files: ["**/*.test.ts", "**/*.test.tsx"], diff --git a/scripts/check-startup-imports-runtime.test.ts b/scripts/check-startup-imports-runtime.test.ts index 56ed4cd1431..fb7ac85a8be 100644 --- a/scripts/check-startup-imports-runtime.test.ts +++ b/scripts/check-startup-imports-runtime.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/await-thenable -- bun:test types `await expect(...).rejects.toThrow()` as void */ // Fixture tests for scripts/check-startup-imports-runtime.ts. Each test writes a tiny // project to a temp dir and loads its main.js through the real Node harness. import { afterEach, beforeEach, expect, test } from "bun:test"; diff --git a/scripts/checkOxlintConfig.test.ts b/scripts/checkOxlintConfig.test.ts index 621cb1cb33a..703d77a2e37 100644 --- a/scripts/checkOxlintConfig.test.ts +++ b/scripts/checkOxlintConfig.test.ts @@ -22,7 +22,7 @@ async function mismatches( files, oxlintConfig, eslintOptions: { overrideConfigFile: true, overrideConfig: eslintConfig }, - })) as Map; + })); return Object.fromEntries(found); } finally { fs.rmSync(cwd, { recursive: true, force: true }); diff --git a/scripts/dry/check.test.ts b/scripts/dry/check.test.ts index 6b79ed91302..255366ff8d2 100644 --- a/scripts/dry/check.test.ts +++ b/scripts/dry/check.test.ts @@ -7,6 +7,7 @@ import { spawnSync } from "node:child_process"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; +import type { DryReport } from "./check"; const CHECK = join(import.meta.dir, "check.ts"); // Hermetic git: CI has no identity, and host config (hooks, signing) must not leak in. @@ -27,13 +28,15 @@ function write(path: string, text: string): void { writeFileSync(join(repo, path), text); } -function check() { +function check(): { status: number | null; report: DryReport } { const result = spawnSync(process.execPath, [CHECK, "--base", "HEAD", "--json"], { cwd: repo, env: GIT_ENV, encoding: "utf8", }); - return { status: result.status, report: result.status === 2 ? null : JSON.parse(result.stdout) }; + // Exit 2 is a usage or tool error with no JSON report; no test expects it. + if (result.status === 2) throw new Error(`check.ts exited 2: ${result.stderr}`); + return { status: result.status, report: JSON.parse(result.stdout) as DryReport }; } const bucket = (name: string) => `export function ${name}(n: number): string { @@ -63,9 +66,7 @@ test("a new copy of existing code is a finding", () => { const { status, report } = check(); expect(status).toBe(1); - const pairs = (report.pairs as Array<{ left: { name: string }; right: { name: string } }>).map( - (pair) => `${pair.left.name}~${pair.right.name}` - ); + const pairs = report.pairs.map((pair) => `${pair.left.name}~${pair.right.name}`); expect(pairs.sort()).toEqual(["fee~cents", "fee~count"]); }); @@ -113,8 +114,7 @@ test("a new copy that shares a name with an old match is a finding", () => { const { status, report } = check(); expect(status).toBe(1); - type Side = { path: string; name: string; startLine: number }; - const pairs = (report.pairs as Array<{ left: Side; right: Side }>) + const pairs = report.pairs .filter((p) => p.left.path === "src/r.ts" && p.left.startLine === 2) .filter((p) => p.right.path === "src/r.ts") .map((p) => p.right.name); @@ -134,7 +134,7 @@ test("a dry-duplicate-ok marker in the changed function accepts the pair", () => const { status, report } = check(); expect(status).toBe(0); - const reasons = (report.pairs as Array<{ accepted?: string }>).map((p) => p.accepted); + const reasons = report.pairs.map((p) => p.accepted); expect(reasons).toEqual(["fees and cents must change apart", "fees and cents must change apart"]); }); @@ -148,7 +148,7 @@ test("a marker on the old side does not excuse a new copy", () => { const { status, report } = check(); expect(status).toBe(1); - expect((report.pairs as Array<{ accepted?: string }>).some((p) => p.accepted)).toBe(false); + expect(report.pairs.some((p) => p.accepted)).toBe(false); }); test("a function moved to another file is reported again (moves are not tracked)", () => { @@ -172,9 +172,6 @@ test("a function moved to another file is reported again (moves are not tracked) // cents~count matched before the move. See new_pairs in dryer_report.py. expect(status).toBe(1); - type Side = { name: string }; - const pairs = (report.pairs as Array<{ left: Side; right: Side }>).map( - (pair) => `${pair.left.name}~${pair.right.name}` - ); + const pairs = report.pairs.map((pair) => `${pair.left.name}~${pair.right.name}`); expect(pairs).toEqual(["cents~count"]); }); diff --git a/scripts/generate-npm-shrinkwrap.test.ts b/scripts/generate-npm-shrinkwrap.test.ts index bafc51fae69..812222809a2 100644 --- a/scripts/generate-npm-shrinkwrap.test.ts +++ b/scripts/generate-npm-shrinkwrap.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/await-thenable -- bun:test types `await expect(...).rejects.toThrow()` as void */ import { describe, expect, test } from "bun:test"; import { buildShrinkwrap, diff --git a/scripts/lib/lint_targets.js b/scripts/lib/lint_targets.js index f6d70438a98..9201c6ad89f 100644 --- a/scripts/lib/lint_targets.js +++ b/scripts/lib/lint_targets.js @@ -23,6 +23,8 @@ const LINT_FILE_PATTERNS = [ /^src\/node\/builtinSkills\/.+\.js$/, /^src\/node\/workflowRuntime\/[^/]+\.js$/, /^scripts\/lib\/[^/]+\.js$/, + /^scripts\/.+\.test\.ts$/, + /^vscode\/src\/.+\.tsx?$/, ]; // Changes that can alter findings in files the branch did not edit, so only a full run is @@ -109,7 +111,7 @@ function listChangedFiles(cwd, base) { * oxlint walks these directories itself: a list of every file would exceed the Windows * command-line limit (~32K characters). Every file that LINT_FILE_PATTERNS matches is inside one. */ -const OXLINT_ROOTS = ["src", "tests", "scripts/lib"]; +const OXLINT_ROOTS = ["src", "tests", "scripts", "vscode/src"]; // The extensions oxlint lints when it walks a directory. const OXLINT_FILE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/; diff --git a/scripts/lint.sh b/scripts/lint.sh index 63e965efa4c..6059760ee78 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -65,6 +65,8 @@ ESLINT_PATTERNS=( 'src/node/builtinSkills/**/*.js' 'src/node/workflowRuntime/*.js' 'scripts/lib/*.js' + 'scripts/**/*.test.ts' + 'vscode/src/**/*.{ts,tsx}' ) get_default_eslint_concurrency() { diff --git a/scripts/mutate/check.test.ts b/scripts/mutate/check.test.ts index 5f07fef84cf..41d0163b380 100644 --- a/scripts/mutate/check.test.ts +++ b/scripts/mutate/check.test.ts @@ -34,7 +34,10 @@ function check(): { status: number | null; report: MutateReport | null } { env: GIT_ENV, encoding: "utf8", }); - return { status: result.status, report: result.status === 2 ? null : JSON.parse(result.stdout) }; + return { + status: result.status, + report: result.status === 2 ? null : (JSON.parse(result.stdout) as MutateReport), + }; } function outcomes(report: MutateReport | null): string[] { diff --git a/scripts/notarize-mac.test.ts b/scripts/notarize-mac.test.ts index 9d2b26a45f8..f41e90c23fe 100644 --- a/scripts/notarize-mac.test.ts +++ b/scripts/notarize-mac.test.ts @@ -83,7 +83,7 @@ exit 99 process.env.PATH = `${binDir}${path.delimiter}${savedEnv.PATH ?? ""}`; } -function calls(): Array<{ command: string; cwd: string }> { +function calls(): { command: string; cwd: string }[] { if (!fs.existsSync(callLog)) return []; return fs .readFileSync(callLog, "utf8") diff --git a/scripts/orpcSchemasBarrelLint.test.ts b/scripts/orpcSchemasBarrelLint.test.ts index cccca21f9dd..599b757ff70 100644 --- a/scripts/orpcSchemasBarrelLint.test.ts +++ b/scripts/orpcSchemasBarrelLint.test.ts @@ -25,11 +25,13 @@ async function restrictedImports(filePath: string, code: string): Promise message.message); } -const barrelMessage = expect.stringContaining("not the @/common/orpc/schemas barrel"); -const nodeMessage = expect.stringContaining("must not value-import src/node"); -const backendOnlyMessage = expect.stringContaining("module is backend-only"); -const shikiMessage = expect.stringContaining("Shiki must only be imported in highlightWorker.ts"); -const tokenizerMessage = expect.stringContaining("ai-tokenizer/encoding barrel"); +const barrelMessage = expect.stringContaining("not the @/common/orpc/schemas barrel") as string; +const nodeMessage = expect.stringContaining("must not value-import src/node") as string; +const backendOnlyMessage = expect.stringContaining("module is backend-only") as string; +const shikiMessage = expect.stringContaining( + "Shiki must only be imported in highlightWorker.ts" +) as string; +const tokenizerMessage = expect.stringContaining("ai-tokenizer/encoding barrel") as string; describe("orpc schemas barrel guard", () => { test.each([ @@ -166,7 +168,7 @@ describe("src/node guard for browser and common code", () => { [ "src/common/config/schemas/example.test.ts", 'import { X } from "@/common/orpc/schemas/runtime";', - expect.stringContaining("Config schemas must not import from ORPC"), + expect.stringContaining("Config schemas must not import from ORPC") as string, ], ])("keeps the folder rules for tests in %s: %s", async (filePath, code, message) => { expect(await restrictedImports(filePath, code)).toEqual([message]); diff --git a/scripts/perf/analyzeProfilesCore.test.ts b/scripts/perf/analyzeProfilesCore.test.ts index 93b65c96698..43a40ecb997 100644 --- a/scripts/perf/analyzeProfilesCore.test.ts +++ b/scripts/perf/analyzeProfilesCore.test.ts @@ -119,7 +119,7 @@ describe("leaderboard", () => { }); // A and B run in an unmapped .js bundle; a leaderboard warns too, not only a diff. expect(report.warnings).toEqual([ - expect.stringContaining("1.1 ms of self time in unmapped bundle frames"), + expect.stringContaining("1.1 ms of self time in unmapped bundle frames") as string, ]); }); @@ -157,7 +157,11 @@ describe("readCpuProfile", () => { ) ); expect(result.profile.stacks).toEqual([ - { frames: [expect.objectContaining({ functionName: "A" })], weightUs: 500, count: 3 }, + { + frames: [expect.objectContaining({ functionName: "A" }) as Frame], + weightUs: 500, + count: 3, + }, ]); expect(result.warnings).toEqual(["1 negative timeDeltas clamped to 0 (sample reordering)"]); }); @@ -171,7 +175,7 @@ describe("readCpuProfile", () => { [2], [10] ); - (json.nodes as Array>)[1].parent = 1; + (json.nodes as Record[])[1].parent = 1; expect(read(json).profile.stacks[0].frames.map((f) => f.functionName)).toEqual(["A"]); }); @@ -243,7 +247,7 @@ describe("frame category", () => { describe("diff", () => { /** One profile with a flat list of functions and one sample each; wall time in µs. */ - function flat(wallUs: number, functions: Array<[string, number]>): Record { + function flat(wallUs: number, functions: [string, number][]): Record { return cpuProfile( [ { id: 1, name: "(root)", children: functions.map((_, i) => i + 2) }, @@ -784,7 +788,7 @@ test("CLI maps each diff side with its own maps when bundle names are stable", ( ["cand.ts"], ]); expect((JSON.parse(unmatched.stdout) as Report).warnings).toContainEqual( - expect.stringContaining("--baseline-map-dir matched no profiled script") + expect.stringContaining("--baseline-map-dir matched no profiled script") as string ); // Without --baseline there is no baseline side for the option to map. const noBaseline = runCli([ @@ -813,7 +817,9 @@ test("folded stacks keep scripts that share a file name apart", () => { .trimEnd() .split("\n"); expect(lines).toHaveLength(2); - expect(lines.map((l) => Number(l.split(" ").pop()))).toEqual(expect.arrayContaining([1, 2])); + expect(lines.map((l) => Number(l.split(" ").pop()))).toEqual( + expect.arrayContaining([1, 2]) as number[] + ); }); test("mapped frames with one name on one original line stay apart by original column", () => { diff --git a/scripts/perf/bench.test.ts b/scripts/perf/bench.test.ts index 40f045c77e3..f35bd27d3f2 100644 --- a/scripts/perf/bench.test.ts +++ b/scripts/perf/bench.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/await-thenable -- bun:test types `await expect(...).rejects.toThrow()` as void */ import { afterEach, describe, expect, test } from "bun:test"; import * as fs from "node:fs"; import * as path from "node:path"; diff --git a/scripts/perf/desktopColdStartStats.test.ts b/scripts/perf/desktopColdStartStats.test.ts index 26e0aecb981..3e4957eca8f 100644 --- a/scripts/perf/desktopColdStartStats.test.ts +++ b/scripts/perf/desktopColdStartStats.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { armPairMedians, launchPlan, pairedRelativeDelta } from "./desktopColdStartStats"; -const near = (value: number) => expect.closeTo(value, 12); +const near = (value: number) => expect.closeTo(value, 12) as number; describe("launchPlan", () => { test("alternates warm-ups, runs pairs ABBA, and measures every pair once per arm", () => { @@ -53,19 +53,21 @@ describe("pairedRelativeDelta", () => { test("20% trim: drops 4 of 20 pairs per side, winsorized sd, t(0.95, df 11) = 1.796", () => { // d = -10%, then 18 pairs alternating -1% / +1%, then +60%: the two strays are trimmed away. const head = [90, ...[...Array(18).keys()].map((i) => (i % 2 ? 101 : 99)), 160]; - const stats = pairedRelativeDelta(Array(20).fill(100), head, 0.2); + const stats = pairedRelativeDelta(Array(20).fill(100), head, 0.2); // Winsorized at the 5th and 16th sorted d: 10 values of -1% and 10 of +1%, sd = 0.01 * sqrt(20 / 19). const halfWidth = 1.796 * Math.sqrt((19 * (0.01 ** 2 * 20)) / 19 / (12 * 11)); expect([stats.n, stats.trim, stats.meanDelta]).toEqual([20, 0.2, near(0)]); expect([stats.halfWidth95, stats.upperBound95]).toEqual([near(halfWidth), near(halfWidth)]); // Untrimmed, the two strays decide the result. - expect(pairedRelativeDelta(Array(20).fill(100), head).upperBound95).toBeGreaterThan(0.05); + expect(pairedRelativeDelta(Array(20).fill(100), head).upperBound95).toBeGreaterThan( + 0.05 + ); }); test("uses t = 1.729 at 20 pairs (df 19) and keeps 1.697 above df 30", () => { const alternating = (n: number) => pairedRelativeDelta( - Array(n).fill(100), + Array(n).fill(100), [...Array(n).keys()].map((i) => (i % 2 ? 99 : 101)) ); // d alternates +1% / -1%, so halfWidth = t * sd / sqrt(n) = t * 0.01 / sqrt(n - 1). diff --git a/scripts/perf/loopTestBaseline.test.ts b/scripts/perf/loopTestBaseline.test.ts index 0f16736b6e9..99061d6fc7e 100644 --- a/scripts/perf/loopTestBaseline.test.ts +++ b/scripts/perf/loopTestBaseline.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/await-thenable -- bun:test types `await expect(...).rejects.toThrow()` as void */ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; @@ -355,8 +356,10 @@ describe("the script on a fixture repo", () => { ), "utf8" ) - ) as { files: Array<{ file: string; failedRuns: number }> }; - expect(record.files).toEqual([expect.objectContaining({ file: loopTest, failedRuns: 0 })]); + ) as { files: { file: string; failedRuns: number }[] }; + expect(record.files).toEqual([ + expect.objectContaining({ file: loopTest, failedRuns: 0 }) as (typeof record.files)[number], + ]); }); test("without src/version.ts it exits 2 and asks for make version", async () => { diff --git a/scripts/perf/loopTestStats.test.ts b/scripts/perf/loopTestStats.test.ts index 13ef1059dad..8378a1915ef 100644 --- a/scripts/perf/loopTestStats.test.ts +++ b/scripts/perf/loopTestStats.test.ts @@ -44,7 +44,7 @@ describe("summarizeFile", () => { }); describe("trips", () => { - const record = (files: Array<[string, number[], number]>): LoopTestRecord => ({ + const record = (files: [string, number[], number][]): LoopTestRecord => ({ commit: "c", dirty: false, bun: "1.4.3", diff --git a/scripts/perf/loopTestTiming.test.ts b/scripts/perf/loopTestTiming.test.ts index 0c906b1ffd3..d638add9daa 100644 --- a/scripts/perf/loopTestTiming.test.ts +++ b/scripts/perf/loopTestTiming.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/await-thenable -- bun:test types `await expect(...).rejects.toThrow()` as void */ import { describe, expect, test } from "bun:test"; import { promisify } from "node:util"; import { @@ -117,7 +118,7 @@ describe("timedTimeout", () => { test("records a timer of 1 ms or more from the call to the fire, with its arguments", () => { const seen: Interval[] = []; const fired: unknown[][] = []; - const queued: Array<() => void> = []; + const queued: (() => void)[] = []; const fakeSetTimeout = ( handler: (...args: unknown[]) => void, _ms?: number, @@ -135,7 +136,7 @@ describe("timedTimeout", () => { const seen: Interval[] = []; const custom = (_ms: number, value?: unknown) => Promise.resolve(value); const fake = Object.assign(() => 0, { [promisify.custom]: custom }); - const sleep = promisify(timedTimeout(fake, (i) => seen.push(i), clock(5, 9))) as typeof custom; + const sleep = promisify(timedTimeout(fake, (i) => seen.push(i), clock(5, 9))); expect(await sleep(0, "yield")).toBe("yield"); expect(await sleep(4, "wait")).toBe("wait"); expect(seen).toEqual([[5, 9]]); @@ -196,7 +197,7 @@ describe("timedInterval", () => { test("each fire of an interval of 1 ms or more records since the call or the fire before", () => { const seen: Interval[] = []; const fired: unknown[][] = []; - const handlers: Array<(...args: unknown[]) => void> = []; + const handlers: ((...args: unknown[]) => void)[] = []; const fakeSetInterval = (handler: (...args: unknown[]) => void, _ms?: number) => handlers.push(handler); const wrapped = timedInterval(fakeSetInterval, (i) => seen.push(i), clock(0, 10, 25)); diff --git a/scripts/perf/pageTimingCore.test.ts b/scripts/perf/pageTimingCore.test.ts index 84eb6b6e6ff..db4da0b3cd8 100644 --- a/scripts/perf/pageTimingCore.test.ts +++ b/scripts/perf/pageTimingCore.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/await-thenable -- bun:test types `await expect(...).rejects.toThrow()` as void */ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { GlobalWindow } from "happy-dom"; import { @@ -60,7 +61,8 @@ test("each run records the host's 1-minute load and PSI cpu some avg10, or null }); test("processes of a run that outlive the server by 10 s are killed before it returns, and the run is invalid", async () => { - let [alive, killed, sleeps] = [[41, 42], [] as number[], 0]; + let [alive, sleeps] = [[41, 42], 0]; + const killed: number[] = []; // A real macrotask per poll: a reaper that never ends fails on the test timeout instead of hanging. const sleep = () => new Promise((resolve) => setImmediate(() => resolve(void sleeps++))); const kill = (pid: number) => { @@ -192,7 +194,10 @@ describe("the probe's IR check", () => { } for (const name of names) { const value = (dom as unknown as Record)[name]; - const bound = typeof value === "function" && /^[a-z]/.test(name) ? value.bind(dom) : value; + const bound = + typeof value === "function" && /^[a-z]/.test(name) + ? (value as (...args: unknown[]) => unknown).bind(dom) + : value; Object.defineProperty(globalThis, name, { value: bound, configurable: true, writable: true }); } // Element Timing is UCS, not IR: a no-op observer keeps this test on the IR path. diff --git a/scripts/perf/pageTimingStats.test.ts b/scripts/perf/pageTimingStats.test.ts index b87d41b174e..07343ffb8e1 100644 --- a/scripts/perf/pageTimingStats.test.ts +++ b/scripts/perf/pageTimingStats.test.ts @@ -85,7 +85,7 @@ describe("gate", () => { // The 14 A/A pairs per page and signal from two blocks on the dev host (main built twice), in ms. Expected // rates and MDIs come from the prototype bootstrap (scratch an/qualsim3.py, 200,000 blocks). const ms = (s: string) => s.split(" ").map((x) => Number(x) / 1000); -const fixture: Array<[string, stats.Signal, number[], number, number, number]> = [ +const fixture: [string, stats.Signal, number[], number, number, number][] = [ // name, signal, pairs, MDI (s), qualification false-fail (%), no-regression false-fail (%) ["P1 UCS", "ucs", ms("68 12 -56 -28 -204 92 -16 680 -52 128 -44 -152 -204 -124"), 1.36, 8.1, 0.1], [ diff --git a/scripts/perf/perfReport.test.ts b/scripts/perf/perfReport.test.ts index 36a96f0dd24..afd082076d9 100644 --- a/scripts/perf/perfReport.test.ts +++ b/scripts/perf/perfReport.test.ts @@ -560,11 +560,9 @@ describe("workspace-open milestones", () => { test.each( MILESTONES.flatMap((milestone, index) => - [null, undefined, "12", -1, 1e999, Number.NaN].map((value): [string, unknown, number] => [ - milestone.id, - value, - index, - ]) + [null, undefined, "12", -1, Number.POSITIVE_INFINITY, Number.NaN].map( + (value): [string, unknown, number] => [milestone.id, value, index] + ) ) )("milestone %s = %p is a problem; the other values stay", (id, value, index) => { const milestones: Record = { @@ -626,6 +624,7 @@ describe("sink safety", () => { ]; const HOSTILE = `${CRAFTED.join(" ")} |\`@org/team\``; const LONG_TITLE = `${HOSTILE} ${"x".repeat(5000)}`; + // eslint-disable-next-line no-control-regex -- the test asserts that no control characters reach the output const CONTROL = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/; function hostileReport(): Report { @@ -718,6 +717,7 @@ describe("sink safety", () => { test("the summary has no escapes, HTML or broken tables, and long titles are truncated in their cell", () => { const markdown = render(hostileReport()); + // eslint-disable-next-line no-control-regex -- the test asserts that no control characters reach the output expect(markdown).not.toMatch(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u2028\u2029]/); expect(markdown).not.toMatch(/[<>]/); for (const line of markdown.split("\n").filter((l) => l.startsWith("- "))) { diff --git a/scripts/perf/perfTrend.test.ts b/scripts/perf/perfTrend.test.ts index ae7de17cb0a..2d8e451367f 100644 --- a/scripts/perf/perfTrend.test.ts +++ b/scripts/perf/perfTrend.test.ts @@ -120,7 +120,7 @@ describe("perfTrend CLI", () => { const broken = (overrides: Record) => ({ out: { total_count: 2, workflow_runs: [apiRun(1), apiRun(2, overrides)] }, }); - const failures: Array<[string, string, Replies[string]]> = [ + const failures: [string, string, Replies[string]][] = [ ["a runs listing that exits 1", RUNS, { err: HOSTILE, code: 1 }], ["a failed current-run fetch", "repos/o/r/actions/runs/100", { err: HOSTILE, code: 1 }], ["a failed artifact listing", "repos/o/r/actions/runs/99/artifacts", { err: HOSTILE, code: 1 }], @@ -157,6 +157,7 @@ describe("perfTrend CLI", () => { } const markdown = readFileSync(summary, "utf8"); expect(markdown).toContain("- history fetch failed: `HTTP 401: ::error::bad tickb`\n"); + // eslint-disable-next-line no-control-regex -- the test asserts that no ESC (ANSI escape) reaches the summary expect(markdown).not.toMatch(/[\u001b<>]|second line/); }); diff --git a/scripts/perf/perfTrendCore.test.ts b/scripts/perf/perfTrendCore.test.ts index 0389bbec409..4f4b04319f3 100644 --- a/scripts/perf/perfTrendCore.test.ts +++ b/scripts/perf/perfTrendCore.test.ts @@ -23,7 +23,7 @@ function run(slot: number, overrides: Partial = {}): RunInfo { } /** One night per slot (0 = current); `series[metric][slot]` is scenario `label`'s value. */ -function nights(series: Partial>>, label = "s") { +function nights(series: Partial>, label = "s") { const length = Math.max(...Object.values(series).map((values) => values?.length ?? 0)); return Array.from({ length }, (_, slot): Night => { const values: Values = {}; @@ -290,6 +290,7 @@ describe("renderTrendSummary", () => { ); for (const historyError of [undefined, evil]) { const lines = render(all, historyError).split("\n"); + // eslint-disable-next-line no-control-regex -- the test asserts that no control characters reach the output expect(lines.join("")).not.toMatch(/[\u0000-\u001f\u007f-\u009f\u2028\u2029<>]/); const table = lines.filter((line) => line.startsWith("|")); expect(table.length).toBe(historyError === undefined ? 3 : 0); diff --git a/vscode/src/extension.test.ts b/vscode/src/extension.test.ts index cd36fdacc8b..f7438cdeaf8 100644 --- a/vscode/src/extension.test.ts +++ b/vscode/src/extension.test.ts @@ -35,6 +35,7 @@ let registeredProvider: unknown = null; const fakeUri = (value: string) => ({ toString: () => value }); // The real "vscode" module exists only inside the extension host, so there is nothing to restore. +// eslint-disable-next-line local/require-module-mock-restore -- no real "vscode" exports exist under bun to restore (see above) void mock.module("vscode", () => ({ Uri: { joinPath: (base: { toString(): string }, ...parts: string[]) => @@ -254,7 +255,7 @@ afterAll(async () => { afterEach(() => { for (const subscription of subscriptions.splice(0)) subscription.dispose(); - for (const server of servers.splice(0)) server.stop(); + for (const server of servers.splice(0)) void server.stop(); settings.clear(); }); @@ -505,7 +506,7 @@ describe("chat view live workspace list and dock activity (#5109)", () => { parentWorkspaceId: WORKSPACE.id, ...extra, }); - type Posted = PostedMessage & { workspaces?: Array<{ id: string }>; activity?: object }; + type Posted = PostedMessage & { workspaces?: { id: string }[]; activity?: object }; const lists = (posted: Posted[]) => posted.filter((message) => message.type === "workspaces"); test("a burst of metadata events re-posts the list only when the projection changes", async () => { diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index fd2784799e5..82d03a23bf6 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -54,9 +54,7 @@ interface PendingAutoSelectState { let xumLogChannel: vscode.LogOutputChannel | undefined; function getXumLogChannel(): vscode.LogOutputChannel { - if (!xumLogChannel) { - xumLogChannel = vscode.window.createOutputChannel("Xum", { log: true }); - } + xumLogChannel ??= vscode.window.createOutputChannel("Xum", { log: true }); return xumLogChannel; } @@ -69,6 +67,7 @@ function formatLogData(data: unknown): string { try { return JSON.stringify(data); } catch { + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- last-resort fallback for values JSON.stringify rejects (cycles, BigInt) return String(data); } } @@ -952,12 +951,12 @@ function renderChatViewHtml( - - + +
- + `; @@ -986,7 +985,7 @@ class XumChatViewProvider implements vscode.WebviewViewProvider, vscode.Disposab private traceId: string | null = null; private readyProbeInterval: ReturnType | null = null; - private readyProbeTimeouts: Array> = []; + private readyProbeTimeouts: ReturnType[] = []; private isWebviewReady = false; private connectionStatus: UiConnectionStatus = { mode: "file" }; @@ -1094,7 +1093,8 @@ class XumChatViewProvider implements vscode.WebviewViewProvider, vscode.Disposab this.selectedWorkspaceId = workspaceId; await this.context.workspaceState.update( SELECTED_WORKSPACE_STATE_KEY, - workspaceId ? workspaceId : undefined + // An empty id clears the stored selection too, like null. + workspaceId !== null && workspaceId !== "" ? workspaceId : undefined ); this.postMessage({ type: "setSelectedWorkspace", workspaceId }); @@ -2070,4 +2070,6 @@ export async function activate(context: vscode.ExtensionContext): Promise /** * Deactivate the extension */ -export function deactivate() {} +export function deactivate() { + // Intentionally empty: VS Code disposes everything activate() put on context.subscriptions. +} diff --git a/vscode/src/orpcAllowlist.ts b/vscode/src/orpcAllowlist.ts index 95e62680a1d..2e0a40c1c9a 100644 --- a/vscode/src/orpcAllowlist.ts +++ b/vscode/src/orpcAllowlist.ts @@ -398,7 +398,7 @@ function redactBackgroundBashState(value: unknown): unknown { if (typeof record.monitor !== "object" || record.monitor === null) { return process; } - return { ...record, monitor: { ...(record.monitor as object), lastLines: [] } }; + return { ...record, monitor: { ...(record.monitor), lastLines: [] } }; }), }; } diff --git a/vscode/src/parseWebviewToExtensionMessage.ts b/vscode/src/parseWebviewToExtensionMessage.ts index a58b9423c38..6f893e916e0 100644 --- a/vscode/src/parseWebviewToExtensionMessage.ts +++ b/vscode/src/parseWebviewToExtensionMessage.ts @@ -71,7 +71,7 @@ export function parseWebviewToExtensionMessage(raw: unknown): WebviewToExtension return { type, requestId: record.requestId, - path: path as string[], + path: path, input: record.input, lastEventId, }; diff --git a/vscode/src/webview/App.test.tsx b/vscode/src/webview/App.test.tsx index 3056c9e889d..141ada236d1 100644 --- a/vscode/src/webview/App.test.tsx +++ b/vscode/src/webview/App.test.tsx @@ -88,7 +88,7 @@ class TestBridge implements VscodeBridge { await this.emit({ type: "orpcStreamData", streamId, value }); } - orpcCalls(path: string): Array> { + orpcCalls(path: string): Extract[] { return this.sent.filter( (message): message is Extract => message.type === "orpcCall" && message.path.join(".") === path @@ -676,7 +676,7 @@ describe("vscode webview turn status and jump to bottom (#4971)", () => { }); // Stop and Esc share one interrupt path; both must reach the host and mark the stream. - const interruptTriggers: Array<[string, (view: ReturnType) => void]> = [ + const interruptTriggers: [string, (view: ReturnType) => void][] = [ ["Stop", (view) => fireEvent.click(view.getByRole("button", { name: "Stop streaming" }))], ["Esc", () => fireEvent.keyDown(window, { key: "Escape" })], [ @@ -800,7 +800,7 @@ describe("vscode webview turn status and jump to bottom (#4971)", () => { await selectWorkspace(bridge); const sentinel = view.getByTestId("transcript-bottom-sentinel"); - const scrollport = sentinel.parentElement as HTMLElement; + const scrollport = sentinel.parentElement!; const content = scrollport.firstElementChild as HTMLElement; const scrollUp = () => act(async () => { @@ -2213,6 +2213,7 @@ describe("vscode webview message rows", () => { // ChatPane-level derivations the webview shares with desktop (#5002). test("prompt arrows skip machine wakes and scroll the neighboring prompt into view", async () => { const scrolled: Element[] = []; + // eslint-disable-next-line @typescript-eslint/unbound-method -- saved only to restore the prototype method afterwards const originalScrollIntoView = Element.prototype.scrollIntoView; Element.prototype.scrollIntoView = function (this: Element) { scrolled.push(this); @@ -3172,7 +3173,7 @@ describe("vscode webview background processes strip (#5092)", () => { }; const bridge = new TestBridge(); // Snapshot the document after every commit, before passive effects run. - const commits: Array<{ rows: boolean; error: boolean }> = []; + const commits: { rows: boolean; error: boolean }[] = []; const onRender = () => { const text = document.body.textContent ?? ""; commits.push({ @@ -3296,12 +3297,12 @@ describe("vscode webview first reveal (#5202)", () => { workspaceName: `reveal-${label}`, }; const bridge = new TestBridge(); - const commits: Array<{ + const commits: { transcript: boolean; held: boolean; strip: boolean; status: boolean; - }> = []; + }[] = []; const onRender = () => { const text = document.body.textContent ?? ""; commits.push({ diff --git a/vscode/src/webview/App.tsx b/vscode/src/webview/App.tsx index cebd3daa491..5ca53800116 100644 --- a/vscode/src/webview/App.tsx +++ b/vscode/src/webview/App.tsx @@ -243,13 +243,13 @@ function createChatReplayState(workspaceId: string): ChatReplayState { function shouldBufferUntilCaughtUp(event: WorkspaceChatMessage): boolean { switch (event.type) { + // The backend replays a turn's terminal error before caught-up; applied early, it would be + // dropped when history loads (as WorkspaceStore buffers it too). + case "stream-error": case "stream-start": case "stream-delta": case "stream-end": case "stream-abort": - // The backend replays a turn's terminal error before caught-up; applied early, it would be - // dropped when history loads (as WorkspaceStore buffers it too). - case "stream-error": case "tool-call-start": case "tool-call-delta": case "tool-call-end": diff --git a/vscode/src/webview/WorkspacePicker.tsx b/vscode/src/webview/WorkspacePicker.tsx index f16d3d03210..cd8e8d167f1 100644 --- a/vscode/src/webview/WorkspacePicker.tsx +++ b/vscode/src/webview/WorkspacePicker.tsx @@ -104,7 +104,7 @@ export function WorkspacePicker(props: { : props.workspaces; const projectGroups = (() => { - const groups: Array<{ projectName: string; projectPath: string; workspaces: UiWorkspace[] }> = + const groups: { projectName: string; projectPath: string; workspaces: UiWorkspace[] }[] = []; const byProjectPath = new Map(); diff --git a/vscode/src/webview/createVscodeOrpcLink.test.ts b/vscode/src/webview/createVscodeOrpcLink.test.ts index 70dd2a85925..eb44f0f66e8 100644 --- a/vscode/src/webview/createVscodeOrpcLink.test.ts +++ b/vscode/src/webview/createVscodeOrpcLink.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/await-thenable -- bun:test types `await expect(...).rejects.toThrow()` as void */ import { describe, expect, test } from "bun:test"; import { createVscodeOrpcLink } from "./createVscodeOrpcLink"; @@ -39,11 +40,13 @@ class TestBridge implements VscodeBridge { } } -function getLastSent( +function getLastSent( bridge: TestBridge, - type: WebviewToExtensionMessage["type"] -): WebviewToExtensionMessage { - const msg = [...bridge.sent].reverse().find((m) => m.type === type); + type: Type +): Extract { + const msg = [...bridge.sent] + .reverse() + .find((m): m is Extract => m.type === type); if (!msg) { throw new Error(`Expected a message of type ${type}`); } @@ -58,10 +61,10 @@ describe("createVscodeOrpcLink", () => { const promise = link.call( ["workspace", "sendMessage"], { workspaceId: "w", message: "hi" }, - {} as any + { context: {} } ); - const callMsg = getLastSent(bridge, "orpcCall") as any; + const callMsg = getLastSent(bridge, "orpcCall"); bridge.emit({ type: "orpcResponse", requestId: callMsg.requestId, @@ -78,14 +81,17 @@ describe("createVscodeOrpcLink", () => { const link = createVscodeOrpcLink(bridge, { callTimeoutMs: 1_000 }); const controller = new AbortController(); - const promise = link.call(["general", "ping"], "hi", { signal: controller.signal } as any); + const promise = link.call(["general", "ping"], "hi", { + context: {}, + signal: controller.signal, + }); - const callMsg = getLastSent(bridge, "orpcCall") as any; + const callMsg = getLastSent(bridge, "orpcCall"); controller.abort(); await expect(promise).rejects.toThrow("aborted"); - const cancelMsg = getLastSent(bridge, "orpcCancel") as any; + const cancelMsg = getLastSent(bridge, "orpcCancel"); expect(cancelMsg.requestId).toBe(callMsg.requestId); }); @@ -93,13 +99,13 @@ describe("createVscodeOrpcLink", () => { const bridge = new TestBridge(); const link = createVscodeOrpcLink(bridge, { callTimeoutMs: 10 }); - const promise = link.call(["general", "ping"], "hi", {} as any); + const promise = link.call(["general", "ping"], "hi", { context: {} }); - const callMsg = getLastSent(bridge, "orpcCall") as any; + const callMsg = getLastSent(bridge, "orpcCall"); await expect(promise).rejects.toThrow("timed out"); - const cancelMsg = getLastSent(bridge, "orpcCancel") as any; + const cancelMsg = getLastSent(bridge, "orpcCancel"); expect(cancelMsg.requestId).toBe(callMsg.requestId); }); @@ -107,9 +113,9 @@ describe("createVscodeOrpcLink", () => { const bridge = new TestBridge(); const link = createVscodeOrpcLink(bridge, { callTimeoutMs: 1_000 }); - const promise = link.call(["providers", "onConfigChanged"], undefined, {} as any); + const promise = link.call(["providers", "onConfigChanged"], undefined, { context: {} }); - const callMsg = getLastSent(bridge, "orpcCall") as any; + const callMsg = getLastSent(bridge, "orpcCall"); bridge.emit({ type: "orpcResponse", requestId: callMsg.requestId, @@ -136,9 +142,9 @@ describe("createVscodeOrpcLink", () => { const bridge = new TestBridge(); const link = createVscodeOrpcLink(bridge, { callTimeoutMs: 1_000, maxBufferedStreamEvents: 1 }); - const promise = link.call(["providers", "onConfigChanged"], undefined, {} as any); + const promise = link.call(["providers", "onConfigChanged"], undefined, { context: {} }); - const callMsg = getLastSent(bridge, "orpcCall") as any; + const callMsg = getLastSent(bridge, "orpcCall"); bridge.emit({ type: "orpcResponse", requestId: callMsg.requestId, @@ -155,7 +161,7 @@ describe("createVscodeOrpcLink", () => { await expect(iterator.next()).rejects.toThrow("buffer overflow"); - const cancelMsg = getLastSent(bridge, "orpcStreamCancel") as any; + const cancelMsg = getLastSent(bridge, "orpcStreamCancel"); expect(cancelMsg.streamId).toBe("s-overflow"); }); }); diff --git a/vscode/src/webview/createVscodeOrpcLink.ts b/vscode/src/webview/createVscodeOrpcLink.ts index 27e8a3047c2..478ec4104cf 100644 --- a/vscode/src/webview/createVscodeOrpcLink.ts +++ b/vscode/src/webview/createVscodeOrpcLink.ts @@ -93,10 +93,10 @@ function isOrpcStreamErrorMessage(raw: unknown): raw is OrpcStreamError { } class VscodeOrpcAsyncIterator implements AsyncIterator, AsyncIterable { - private readonly pending: Array<{ + private readonly pending: { resolve: (value: IteratorResult) => void; reject: (error: unknown) => void; - }> = []; + }[] = []; private readonly buffered: T[] = []; private done = false; @@ -219,6 +219,7 @@ class VscodeOrpcAsyncIterator implements AsyncIterator, AsyncIterable { }); } + // eslint-disable-next-line @typescript-eslint/require-await -- AsyncIterator.return() must return a Promise, and async turns a throw from cancel() into a rejection async return(): Promise> { this.cancel("Iterator return() called"); return { value: undefined as unknown as T, done: true }; @@ -307,7 +308,8 @@ export function createVscodeOrpcLink( }); const call = async (path: readonly string[], input: unknown, options: ClientOptions) => { - assert(Array.isArray(path), "ORPC call requires path array"); + // Check `path as unknown`: Array.isArray narrows a readonly string[] to any[]. + assert(Array.isArray(path as unknown), "ORPC call requires path array"); const requestId = createRequestId(); diff --git a/vscode/src/webview/protocol.ts b/vscode/src/webview/protocol.ts index 7592dd5ef95..813c2f340f5 100644 --- a/vscode/src/webview/protocol.ts +++ b/vscode/src/webview/protocol.ts @@ -59,23 +59,23 @@ export interface UiConnectionStatus { error?: string; } -export type OrpcCall = { +export interface OrpcCall { type: "orpcCall"; requestId: string; path: string[]; input: unknown; lastEventId?: string | undefined; -}; +} -export type OrpcCancel = { +export interface OrpcCancel { type: "orpcCancel"; requestId: string; -}; +} -export type OrpcStreamCancel = { +export interface OrpcStreamCancel { type: "orpcStreamCancel"; streamId: string; -}; +} export type OrpcResponse = | { @@ -99,22 +99,22 @@ export type OrpcResponse = error: string; }; -export type OrpcStreamData = { +export interface OrpcStreamData { type: "orpcStreamData"; streamId: string; value: unknown; -}; +} -export type OrpcStreamEnd = { +export interface OrpcStreamEnd { type: "orpcStreamEnd"; streamId: string; -}; +} -export type OrpcStreamError = { +export interface OrpcStreamError { type: "orpcStreamError"; streamId: string; error: string; -}; +} export type WebviewToExtensionMessage = | { type: "ready" } diff --git a/vscode/src/webview/vscodeBridge.ts b/vscode/src/webview/vscodeBridge.ts index 0ff3da31b30..0d0bd5f44ce 100644 --- a/vscode/src/webview/vscodeBridge.ts +++ b/vscode/src/webview/vscodeBridge.ts @@ -18,6 +18,9 @@ function safeStringify(value: unknown): string { } } +// VS Code injects this global into the webview. +declare function acquireVsCodeApi(): { postMessage: (data: unknown) => void }; + let cachedBridge: VscodeBridge | null = null; export function getVscodeBridge(): VscodeBridge { @@ -25,13 +28,11 @@ export function getVscodeBridge(): VscodeBridge { return cachedBridge; } - const traceId = - (document.body && document.body.dataset && document.body.dataset.muxTraceId) || "unknown"; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- an empty trace id also falls back to "unknown" + const traceId = document.body?.dataset?.muxTraceId || "unknown"; let vscode: { postMessage: (data: unknown) => void }; try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore VS Code injects this in the webview vscode = acquireVsCodeApi(); } catch (error) { throw new Error(`mux webview: acquireVsCodeApi failed: ${String(error)}`); @@ -54,7 +55,6 @@ export function getVscodeBridge(): VscodeBridge { try { handler(event.data); } catch (error) { - // eslint-disable-next-line no-console console.error("mux webview: message handler crashed", error); } } @@ -76,7 +76,6 @@ export function getVscodeBridge(): VscodeBridge { const debugLog = (message: string, data?: unknown): void => { // Mirror to DevTools console. - // eslint-disable-next-line no-console console.log(`[mux-webview ${traceId}] ${message}`, data); try { diff --git a/vscode/src/workspaceActivityPump.test.ts b/vscode/src/workspaceActivityPump.test.ts index b058c64c968..eefce6ca86e 100644 --- a/vscode/src/workspaceActivityPump.test.ts +++ b/vscode/src/workspaceActivityPump.test.ts @@ -16,7 +16,7 @@ type ActivityEvent = /** A subscription the test drives: events are delivered in order, then end or fail. */ function createFakeActivityClient(snapshot: Record | null) { const calls: string[] = []; - const queue: Array<{ event: ActivityEvent } | { end: true } | { error: unknown }> = []; + const queue: ({ event: ActivityEvent } | { end: true } | { error: unknown })[] = []; let wake: (() => void) | null = null; const enqueue = (item: (typeof queue)[number]) => { queue.push(item); @@ -77,7 +77,7 @@ function startPump( ) { // The selected workspace's monitor count per post, and every posted map. const posted: number[] = []; - const maps: Array> = []; + const maps: Record[] = []; const errors: unknown[] = []; const controller = new AbortController(); const done = pumpWorkspaceActivity({ From c1bc94c3d58b4e38335a0b6b6dd553d02fadbcbc Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 26 Oct 2026 12:49:23 +0000 Subject: [PATCH 2/5] tests: lint_targets excludes node_modules in the git query (ENOBUFS with scripts/mintlify/node_modules) --- scripts/lib/lint_targets.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/lib/lint_targets.js b/scripts/lib/lint_targets.js index 9201c6ad89f..3a45d713587 100644 --- a/scripts/lib/lint_targets.js +++ b/scripts/lib/lint_targets.js @@ -117,7 +117,12 @@ const OXLINT_FILE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/; function listFiles(cwd, args) { return ( - git(["ls-files", "-z", ...args, "--", ...OXLINT_ROOTS], cwd) + // Exclude node_modules in the query itself: an installed scripts/mintlify/node_modules + // lists megabytes of ignored paths, more than execFileSync's output buffer holds. + git( + ["ls-files", "-z", ...args, "--", ...OXLINT_ROOTS, ":(exclude,glob)**/node_modules/**"], + cwd + ) .split("\0") .filter((file) => file !== "") // ESLint never lints node_modules. From a56b8350a061fbe790e19c56ae0f59d7fef681b5 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 26 Oct 2026 12:51:06 +0000 Subject: [PATCH 3/5] tests: lintTargets covers the scripts/ and vscode/src roots --- tests/lintTargets.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/lintTargets.test.ts b/tests/lintTargets.test.ts index 472c326af03..95f3f3d1fe0 100644 --- a/tests/lintTargets.test.ts +++ b/tests/lintTargets.test.ts @@ -193,22 +193,32 @@ describe("lint target git discovery", () => { git("add", "-f", "src/committed.generated.ts"); write("src/untracked.generated.ts"); write("docs/page.md", "# page\n"); + // Only tests are linted under scripts/; vscode/src is linted whole. + write("scripts/tool.ts"); + write("scripts/tool.test.ts"); + write("vscode/src/extension.ts"); + write("scripts/mintlify/node_modules/pkg/index.ts"); expect(lintTargets.listLintFiles(repo)).toEqual([ + "scripts/tool.test.ts", "src/committed.generated.ts", "src/edited.ts", "src/kept.ts", "src/node/workflowRuntime/runtime.js", "src/renamed.ts", "src/untracked.generated.ts", + "vscode/src/extension.ts", ]); // oxlint walks the directories: skip what ESLint does not lint, add the gitignored lint files. expect(lintTargets.oxlintArgs(repo)).toEqual([ + "--ignore-pattern", + "scripts/tool.ts", "--ignore-pattern", "tests/__mocks__/styleMock.js", "src", "tests", - "scripts/lib", + "scripts", + "vscode/src", "src/committed.generated.ts", "src/untracked.generated.ts", ]); From fccb6408a66322dd4458ad5f9e7d28cf8ceba57b Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 26 Oct 2026 13:05:52 +0000 Subject: [PATCH 4/5] tests: ESLint targets install the vscode extension's deps first (CI raced typecheck's install) --- Makefile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 63377a1f5dc..0504937c39c 100644 --- a/Makefile +++ b/Makefile @@ -626,17 +626,21 @@ check-bench-agent: node_modules/.installed src/version.ts $(BUILTIN_SKILLS_GENER @./scripts/check-bench-agent.sh LINT_PREREQS := node_modules/.installed src/version.ts $(BUILTIN_SKILLS_GENERATED) $(BUILTIN_WORKFLOWS_GENERATED) $(WORKFLOW_RUNTIME_SOURCES_GENERATED) +# Type-aware ESLint lints vscode/src, whose types (@types/vscode) come from the extension's own +# install. Without it, `make -j static-check-full` races typecheck's install and every vscode +# type reads as `error`. +ESLINT_PREREQS := $(LINT_PREREQS) vscode/node_modules/.installed lint: $(LINT_PREREQS) ## Run oxlint on all files (the eslint.config.mjs rules it supports; no type-aware rules) @./scripts/lint.sh -lint-eslint: $(LINT_PREREQS) ## Run oxlint, then type-aware ESLint on files changed since the merge base with origin/main [XUM_LINT_BASE=] +lint-eslint: $(ESLINT_PREREQS) ## Run oxlint, then type-aware ESLint on files changed since the merge base with origin/main [XUM_LINT_BASE=] @./scripts/lint.sh --eslint -lint-full: $(LINT_PREREQS) ## Run oxlint, then ESLint on all files, one ESLint run per machine at a time (as CI does) +lint-full: $(ESLINT_PREREQS) ## Run oxlint, then ESLint on all files, one ESLint run per machine at a time (as CI does) @./scripts/lint.sh --full -lint-fix: $(LINT_PREREQS) ## Run oxlint --fix, then ESLint --fix on changed files (scripts/lint.sh --full --fix for all files) +lint-fix: $(ESLINT_PREREQS) ## Run oxlint --fix, then ESLint --fix on changed files (scripts/lint.sh --full --fix for all files) @./scripts/lint.sh --eslint --fix lint-actions: lint-actionlint lint-zizmor ## Lint GitHub Actions workflows From 917f80e37084894e6cc7395afa6c69d4b769d052 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 26 Oct 2026 13:20:23 +0000 Subject: [PATCH 5/5] tests: lint-eslint lints everything when the vscode extension's dependencies change --- scripts/lib/lint_targets.js | 4 ++++ tests/lintTargets.test.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/lib/lint_targets.js b/scripts/lib/lint_targets.js index 3a45d713587..ee39106498d 100644 --- a/scripts/lib/lint_targets.js +++ b/scripts/lib/lint_targets.js @@ -34,6 +34,10 @@ const FULL_LINT_TRIGGERS = [ /^eslint-local-rules\.mjs$/, /^package\.json$/, /^bun\.lock$/, + // vscode/src is linted with the extension's own types (@types/vscode), so an update to its + // dependencies can surface findings in unchanged files. + /^vscode\/package\.json$/, + /^vscode\/bun\.lock$/, /^tsconfig[^/]*\.json$/, /^scripts\/lint\.sh$/, /^scripts\/lib\/lint_targets\.js$/, diff --git a/tests/lintTargets.test.ts b/tests/lintTargets.test.ts index 95f3f3d1fe0..52f42b0c911 100644 --- a/tests/lintTargets.test.ts +++ b/tests/lintTargets.test.ts @@ -48,6 +48,8 @@ describe("lint target selection", () => { "eslint-local-rules.mjs", "package.json", "bun.lock", + "vscode/package.json", + "vscode/bun.lock", "tsconfig.json", "tsconfig.main.json", "scripts/lint.sh", @@ -60,7 +62,7 @@ describe("lint target selection", () => { }); it("does not treat nested lookalikes as config changes", () => { - expect(lintTargets.selectLintTargets(["vscode/package.json", "src/tsconfig.json"])).toEqual({ + expect(lintTargets.selectLintTargets(["src/package.json", "src/tsconfig.json"])).toEqual({ mode: "files", files: [], });