diff --git a/evals/capability/lib.test.ts b/evals/capability/lib.test.ts index 93c15feaf..4c9784903 100644 --- a/evals/capability/lib.test.ts +++ b/evals/capability/lib.test.ts @@ -667,8 +667,8 @@ describe("resolveRequestedProviderModel", () => { expect(requested).toEqual({ provider: raw.provider, model: raw.model }); const fallback = detectProviderFallback({ - requestedProvider: requested.provider, - requestedModel: requested.model, + ...(requested.provider !== undefined ? { requestedProvider: requested.provider } : {}), + ...(requested.model !== undefined ? { requestedModel: requested.model } : {}), resolvedProvider: cell!.provider, resolvedModel: cell!.model, }); @@ -695,7 +695,7 @@ describe("resolveRequestedProviderModel", () => { {}, { provider: "(default)", model: "(default)" }, ); - expect(requested).toEqual({ provider: undefined, model: undefined }); + expect(requested).toEqual({}); }); }); diff --git a/evals/capability/lib.ts b/evals/capability/lib.ts index 6ead036f4..153a2dd96 100644 --- a/evals/capability/lib.ts +++ b/evals/capability/lib.ts @@ -477,9 +477,11 @@ export function resolveRequestedProviderModel( ): { provider?: string; model?: string } { const requested = (v?: string): string | undefined => v === undefined || v === "(default)" ? undefined : v; + const provider = variant.provider ?? requested(labels.provider); + const model = variant.model ?? requested(labels.model); return { - provider: variant.provider ?? requested(labels.provider), - model: variant.model ?? requested(labels.model), + ...(provider !== undefined ? { provider } : {}), + ...(model !== undefined ? { model } : {}), }; } diff --git a/scripts/eval-capability.test.ts b/scripts/eval-capability.test.ts index bb403eec9..946cca158 100644 --- a/scripts/eval-capability.test.ts +++ b/scripts/eval-capability.test.ts @@ -5,8 +5,8 @@ import { join } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; -import { initEvalGitRepo, mapPool, parseArgs, buildEvalDiagnostics } from "./eval-capability.ts"; -import type { Config } from "../src/config/index.ts"; +import { initEvalGitRepo, mapPool, parseArgs, buildEvalDiagnostics } from "./eval-capability.js"; +import type { Config } from "../src/config/index.js"; const execFileAsync = promisify(execFile); diff --git a/scripts/eval-capability.ts b/scripts/eval-capability.ts index 64d729bde..6bc00dde0 100755 --- a/scripts/eval-capability.ts +++ b/scripts/eval-capability.ts @@ -257,13 +257,20 @@ export function parseArgs(argv: readonly string[]): CliOptions { return opts; } +// exactOptionalPropertyTypes forbids passing an explicit `undefined` for an +// optional field, so build the fallback object with the key present only +// when the CLI option was actually given. +function providerModelFallback(opts: CliOptions): { provider?: string; model?: string } { + return { + ...(opts.provider !== undefined ? { provider: opts.provider } : {}), + ...(opts.model !== undefined ? { model: opts.model } : {}), + }; +} + function requireExplicitModelPair(opts: CliOptions): void { const matrix = opts.matrix?.trim(); if (matrix !== undefined && matrix.length > 0) { - parseMatrix(matrix, { - provider: opts.provider, - model: opts.model, - }); + parseMatrix(matrix, providerModelFallback(opts)); return; } if (!opts.provider && !opts.model) { @@ -705,8 +712,8 @@ async function runCase( const resolvedProvider = execResult.provider ?? config.providerName ?? labels.provider; const resolvedModel = execResult.model ?? config.model ?? labels.model; providerFallback = detectProviderFallback({ - requestedProvider: requested.provider, - requestedModel: requested.model, + ...(requested.provider !== undefined ? { requestedProvider: requested.provider } : {}), + ...(requested.model !== undefined ? { requestedModel: requested.model } : {}), resolvedProvider, resolvedModel, }); @@ -852,10 +859,7 @@ async function main(): Promise { } const all = await loadEvalCases(CASES_ROOT); const selected = filterCases(all, opts.caseSelector); - const variants = parseMatrix(opts.matrix, { - provider: opts.provider, - model: opts.model, - }); + const variants = parseMatrix(opts.matrix, providerModelFallback(opts)); const plan = expandMatrix(selected, variants); console.log( diff --git a/scripts/eval-public-swe-one.test.ts b/scripts/eval-public-swe-one.test.ts index 967b573eb..5335e5f3b 100644 --- a/scripts/eval-public-swe-one.test.ts +++ b/scripts/eval-public-swe-one.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { parseArgs } from "./eval-public-swe-one.ts"; +import { parseArgs } from "./eval-public-swe-one.js"; describe("parseArgs", () => { test("--help does not require provider or model", () => { diff --git a/src/cost/pricing-fetcher.ts b/src/cost/pricing-fetcher.ts index 7ce85ca2b..811d3d115 100644 --- a/src/cost/pricing-fetcher.ts +++ b/src/cost/pricing-fetcher.ts @@ -27,7 +27,9 @@ export interface PricingCache { export interface PricingFetcherOptions { cachePath?: string; endpoint?: string; - fetchImpl?: typeof fetch; + // The plain call signature, not `typeof fetch` — `typeof fetch` also carries + // static members (e.g. `preconnect`) that a test double has no reason to implement. + fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise; fetchTimeoutMs?: number; now?: () => number; refreshIntervalMs?: number; diff --git a/tests/fixtures/plugins/implement-feature/src/index.ts b/tests/fixtures/plugins/implement-feature/src/index.ts index b9a3ca1cd..42853628f 100644 --- a/tests/fixtures/plugins/implement-feature/src/index.ts +++ b/tests/fixtures/plugins/implement-feature/src/index.ts @@ -1,6 +1,6 @@ import { implementFeature } from "./workflows/implement-feature.js"; -import type { CommandPlugin } from "../../../src/tui/commands/registry.js"; -import type { WorkflowPlugin } from "../../../src/workflows/definition.js"; +import type { CommandPlugin } from "../../../../../src/tui/commands/registry.js"; +import type { WorkflowPlugin } from "../../../../../src/workflows/definition.js"; export const workflowPlugin: WorkflowPlugin = { workflows: [implementFeature], diff --git a/tests/fixtures/plugins/implement-feature/src/workflows/implement-feature.ts b/tests/fixtures/plugins/implement-feature/src/workflows/implement-feature.ts index 376ffbe1d..7c45842af 100644 --- a/tests/fixtures/plugins/implement-feature/src/workflows/implement-feature.ts +++ b/tests/fixtures/plugins/implement-feature/src/workflows/implement-feature.ts @@ -1,4 +1,4 @@ -import type { Workflow } from "../../../../src/workflows/definition.js"; +import type { Workflow } from "../../../../../../src/workflows/definition.js"; export const implementFeature: Workflow = { name: "implement-feature", diff --git a/tests/integration/vendored-carry.test.ts b/tests/integration/vendored-carry.test.ts index a0d185a29..d1b874278 100644 --- a/tests/integration/vendored-carry.test.ts +++ b/tests/integration/vendored-carry.test.ts @@ -75,7 +75,12 @@ describe("integration — vendored feature carry", () => { const requests = session.harness.scenario.matchedRequests(); expect(requests.length).toBeGreaterThan(0); - const bodies = await Promise.all(requests.map((r) => r.clone().text())); + // HarnessRequest resolves to a body-less fallback shape under this project's + // DOM-less lib config, even though it carries a real body at runtime; cast + // through the Fetch Request shape to read it. + const bodies = await Promise.all( + requests.map((r) => (r.clone() as unknown as Request).text()), + ); expect(bodies.some((b) => b.includes(TRANSFORM_MARKER))).toBe(true); } finally { await closeIntegrationSession(session); @@ -148,7 +153,12 @@ describe("integration — vendored feature carry", () => { const requests = harness.scenario.matchedRequests(); expect(requests.length).toBeGreaterThan(0); - const bodies = await Promise.all(requests.map((r) => r.clone().text())); + // HarnessRequest resolves to a body-less fallback shape under this project's + // DOM-less lib config, even though it carries a real body at runtime; cast + // through the Fetch Request shape to read it. + const bodies = await Promise.all( + requests.map((r) => (r.clone() as unknown as Request).text()), + ); expect(bodies.some((b) => b.includes(NUDGE_MARKER))).toBe(true); // Prompt-only: the nudge must not be persisted. diff --git a/tests/unit/codex-session.test.ts b/tests/unit/codex-session.test.ts index 145ccca47..0ad79b74d 100644 --- a/tests/unit/codex-session.test.ts +++ b/tests/unit/codex-session.test.ts @@ -52,7 +52,7 @@ describe("getValidCodexToken", () => { await withHome(async (home) => { globalThis.fetch = (() => { throw new Error("should not be called"); - }) as typeof fetch; + }) as unknown as typeof fetch; await saveCodexProfile( { name: "p", @@ -69,7 +69,7 @@ describe("getValidCodexToken", () => { await withHome(async (home) => { globalThis.fetch = (() => { throw new Error("should not be called"); - }) as typeof fetch; + }) as unknown as typeof fetch; await saveCodexProfile( { name: "p", @@ -97,7 +97,7 @@ describe("getValidCodexToken", () => { status: 200, headers: { "content-type": "application/json" }, }, - )) as typeof fetch; + )) as unknown as typeof fetch; const token = await getValidCodexToken("p", 5_000, home); expect(token.access).toBe("fresh"); const stored = await loadCodexProfile("p", home); @@ -122,7 +122,8 @@ describe("getValidCodexToken", () => { { name: "p", createdAt: 0, tokens: { access: "old", refresh: "bad", expiresAt: 1_000 } }, home, ); - globalThis.fetch = (async () => new Response("revoked", { status: 400 })) as typeof fetch; + globalThis.fetch = (async () => + new Response("revoked", { status: 400 })) as unknown as typeof fetch; const err = await getValidCodexToken("p", 5_000, home).catch((e: unknown) => e); expect(err).toBeInstanceOf(CodexAuthError); expect((err as CodexAuthError).reason).toBe("refresh-failed"); diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index 8e9a0b401..e198a03d6 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -154,7 +154,7 @@ test("loadSettings cannot silently drop a known optional key", async () => { expect(loaded).not.toBeNull(); for (const key of GLOBAL_SETTINGS_OPTIONAL_KEYS) { expect(loaded).toHaveProperty(key); - expect((loaded as Record)[key]).not.toBeUndefined(); + expect((loaded as unknown as Record)[key]).not.toBeUndefined(); } // Undefined optionals stay absent (not { foo: undefined }). const minimalPath = join(cwd, "minimal.json"); diff --git a/tests/unit/corbits-skills-catalog.test.ts b/tests/unit/corbits-skills-catalog.test.ts index ef1c58e73..ac0a83361 100644 --- a/tests/unit/corbits-skills-catalog.test.ts +++ b/tests/unit/corbits-skills-catalog.test.ts @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"; import { readdir } from "node:fs/promises"; import { join } from "node:path"; import { expect, test } from "bun:test"; -import { loadSkillCommands } from "../../src/plugins/skill-commands.ts"; +import { loadSkillCommands } from "../../src/plugins/skill-commands.js"; const pluginRoot = join(import.meta.dirname, "../../plugins/corbits-skills"); diff --git a/tests/unit/director.test.ts b/tests/unit/director.test.ts index 0c001d567..fe48c83f0 100644 --- a/tests/unit/director.test.ts +++ b/tests/unit/director.test.ts @@ -22,7 +22,7 @@ const usage: TokenUsage = { }; const source: LastCycleSource = { - id: "test", + sourceId: "test", provider: "openai", model: "test-model", }; diff --git a/tests/unit/hooks.test.ts b/tests/unit/hooks.test.ts index 31c9799f1..8accbeb82 100644 --- a/tests/unit/hooks.test.ts +++ b/tests/unit/hooks.test.ts @@ -25,7 +25,7 @@ const usage: TokenUsage = { }; const source: LastCycleSource = { - id: "test-source", + sourceId: "test-source", provider: "openai", model: "test-model", }; diff --git a/tests/unit/mcp.test.ts b/tests/unit/mcp.test.ts index 66c10c77e..dfa3f1cc3 100644 --- a/tests/unit/mcp.test.ts +++ b/tests/unit/mcp.test.ts @@ -166,6 +166,7 @@ describe("mcpClientToAgentTools (production gated path)", () => { expect(capturedName).toBe("do_thing"); expect(capturedArgs).toEqual({ x: 1 }); + if (typeof result === "string") throw new Error("expected structured ToolResult"); expect(result.content).toBe("done"); expect(result.isError).toBeUndefined(); }); @@ -187,6 +188,7 @@ describe("mcpClientToAgentTools (production gated path)", () => { new AbortController().signal, ); + if (typeof result === "string") throw new Error("expected structured ToolResult"); expect(result.isError).toBe(true); expect(result.content).toBe("server error"); }); @@ -210,6 +212,7 @@ describe("mcpClientToAgentTools (production gated path)", () => { new AbortController().signal, ); expect(asked).toBe(1); + if (typeof result === "string") throw new Error("expected structured ToolResult"); expect(result.isError).toBe(true); expect(result.content).toContain("Blocked by permission policy"); }); diff --git a/tests/unit/permission/cross-commit-composition.test.ts b/tests/unit/permission/cross-commit-composition.test.ts index f31e39a42..7e7ce67c8 100644 --- a/tests/unit/permission/cross-commit-composition.test.ts +++ b/tests/unit/permission/cross-commit-composition.test.ts @@ -27,7 +27,10 @@ describe("comment normalization x exact full-command grants", () => { const gate = gateWith(async (req) => { prompts++; const exact = req.scopes.find((s) => s.id === "exact"); - return { allow: true, persist: { pattern: exact?.pattern ?? null, grant: "session" } }; + return { + allow: true, + ...(exact !== undefined ? { persist: { ...exact, grant: "session" as const } } : {}), + }; }); const withComment = "# build then test\ngit fetch origin && git rebase origin/main"; expect((await gate.evaluate(call(withComment))).allowed).toBe(true); diff --git a/tests/unit/plugin-register.test.ts b/tests/unit/plugin-register.test.ts index dae8058bc..461914dcd 100644 --- a/tests/unit/plugin-register.test.ts +++ b/tests/unit/plugin-register.test.ts @@ -119,7 +119,7 @@ test("isPluginModuleEnabled: missing settings + repo without flag is off", () => test("isPluginModuleEnabled: marketplace/path/user defaultEnabled is ignored", () => { const flagged = { - manifest: { id: "mkt", name: "mkt", kind: "command", defaultEnabled: true }, + manifest: { id: "mkt", name: "mkt", kind: "command" as const, defaultEnabled: true }, }; expect(isPluginModuleEnabled({ ...flagged, origin: "user" }, {})).toBe(false); expect(isPluginModuleEnabled({ ...flagged, origin: "path" }, {})).toBe(false); diff --git a/tests/unit/resolve-inference-spec.test.ts b/tests/unit/resolve-inference-spec.test.ts index d8a60c499..0742381ee 100644 --- a/tests/unit/resolve-inference-spec.test.ts +++ b/tests/unit/resolve-inference-spec.test.ts @@ -8,9 +8,12 @@ import type { InferenceSpec } from "../../src/agent/profile-types.js"; const baseSettings: Settings = { providers: { - anthropic: { models: ["claude-sonnet-4", "claude-haiku-4"] }, - xai: { models: ["grok-4"] }, - local: { models: [] }, // empty models list = unrestricted + anthropic: { + baseURL: "https://api.anthropic.com", + models: ["claude-sonnet-4", "claude-haiku-4"], + }, + xai: { baseURL: "https://api.x.ai", models: ["grok-4"] }, + local: { baseURL: "http://localhost:11434", models: [] }, // empty models list = unrestricted }, }; diff --git a/tests/unit/run-agent.test.ts b/tests/unit/run-agent.test.ts index 45380aafe..82846da5e 100644 --- a/tests/unit/run-agent.test.ts +++ b/tests/unit/run-agent.test.ts @@ -9,29 +9,32 @@ async function* makeStream(events: ReactorEmittedEvent[]): AsyncIterable { + // Cast each event as a whole rather than just `data` — casting `data` alone still + // leaves it typed as the union across all event kinds, which does not line up with + // the `type` discriminant on the surrounding object. const events: ReactorEmittedEvent[] = [ - { type: "reactor.start", seq: 1, data: {} as unknown as ReactorEmittedEvent["data"] }, + { type: "reactor.start", seq: 1, data: {} } as unknown as ReactorEmittedEvent, { type: "inference.tool_call.start", seq: 2, - data: { name: "read_file" } as unknown as ReactorEmittedEvent["data"], - }, + data: { name: "read_file" }, + } as unknown as ReactorEmittedEvent, { type: "tool.done", seq: 3, data: { result: { callId: "c1", content: "ok", isError: false }, - } as unknown as ReactorEmittedEvent["data"], - }, + }, + } as unknown as ReactorEmittedEvent, ]; const received: ReactorEmittedEvent[] = []; await consumeStream(makeStream(events), (event) => received.push(event)); expect(received.length).toBe(3); - expect(received[0].type).toBe("reactor.start"); - expect(received[1].type).toBe("inference.tool_call.start"); - expect(received[2].type).toBe("tool.done"); + expect(received[0]?.type).toBe("reactor.start"); + expect(received[1]?.type).toBe("inference.tool_call.start"); + expect(received[2]?.type).toBe("tool.done"); }); test("consumeStream handles empty stream", async () => { diff --git a/tests/unit/subagent-session-store.test.ts b/tests/unit/subagent-session-store.test.ts index 80d5e1400..ad9a9cf45 100644 --- a/tests/unit/subagent-session-store.test.ts +++ b/tests/unit/subagent-session-store.test.ts @@ -290,7 +290,8 @@ describe("createTaskTool session recording", () => { }, signal ?? new AbortController().signal, ); - return result.content; + if (typeof result === "string") return result; + return typeof result.content === "string" ? result.content : JSON.stringify(result.content); } test("records a session on spawn and completes it with the report", async () => { diff --git a/tests/unit/subagent.test.ts b/tests/unit/subagent.test.ts index 0904c5a3b..f499b261d 100644 --- a/tests/unit/subagent.test.ts +++ b/tests/unit/subagent.test.ts @@ -699,7 +699,7 @@ test("a profile-resolved provider carries the bifrost virtual-key marker", async cwd: "/repo", getWorkdirBase: () => "/repo/.ctx", provider, - settings: settings as unknown as Parameters[0]["settings"], + settings: settings as unknown as NonNullable[0]["settings"]>, profiles: [ { id: "p", @@ -742,7 +742,9 @@ describe("createTaskTool profile resolution", () => { cwd: "/repo", getWorkdirBase: () => "/repo/.ctx", provider, - settings: baseSettings as unknown as Parameters[0]["settings"], + settings: baseSettings as unknown as NonNullable< + Parameters[0]["settings"] + >, profiles: [ { id: "p", @@ -779,7 +781,9 @@ describe("createTaskTool profile resolution", () => { cwd: "/repo", getWorkdirBase: () => "/repo/.ctx", provider, - settings: baseSettings as unknown as Parameters[0]["settings"], + settings: baseSettings as unknown as NonNullable< + Parameters[0]["settings"] + >, profiles: [ { id: "p", @@ -811,7 +815,9 @@ describe("createTaskTool profile resolution", () => { cwd: "/repo", getWorkdirBase: () => "/repo/.ctx", provider, - settings: baseSettings as unknown as Parameters[0]["settings"], + settings: baseSettings as unknown as NonNullable< + Parameters[0]["settings"] + >, profiles: [ { id: "p", @@ -852,7 +858,9 @@ describe("createTaskTool profile resolution", () => { cwd: "/repo", getWorkdirBase: () => "/repo/.ctx", provider: { ...provider, reasoningEffort: "high" }, - settings: baseSettings as unknown as Parameters[0]["settings"], + settings: baseSettings as unknown as NonNullable< + Parameters[0]["settings"] + >, profiles: [ { id: "p", @@ -883,7 +891,9 @@ describe("createTaskTool profile resolution", () => { cwd: "/repo", getWorkdirBase: () => "/repo/.ctx", provider: { ...provider, reasoningEffort: "high" }, - settings: baseSettings as unknown as Parameters[0]["settings"], + settings: baseSettings as unknown as NonNullable< + Parameters[0]["settings"] + >, profiles: [ { id: "p", @@ -912,7 +922,9 @@ describe("createTaskTool profile resolution", () => { cwd: "/repo", getWorkdirBase: () => "/repo/.ctx", provider: { ...provider, reasoningEffort: "low" }, - settings: baseSettings as unknown as Parameters[0]["settings"], + settings: baseSettings as unknown as NonNullable< + Parameters[0]["settings"] + >, profiles: [ { id: "orch", @@ -947,7 +959,9 @@ describe("createTaskTool profile resolution", () => { cwd: "/repo", getWorkdirBase: () => "/repo/.ctx", provider, - settings: baseSettings as unknown as Parameters[0]["settings"], + settings: baseSettings as unknown as NonNullable< + Parameters[0]["settings"] + >, profiles: [{ id: "karen", systemPromptRole: "You are karen.", orchestrator: true }], run: async (params) => { received = params; diff --git a/tests/unit/telemetry-product-events.test.ts b/tests/unit/telemetry-product-events.test.ts index 6570447e2..0709d1cdf 100644 --- a/tests/unit/telemetry-product-events.test.ts +++ b/tests/unit/telemetry-product-events.test.ts @@ -137,7 +137,7 @@ test("skill_used carries no skill name, so an employer-named skill cannot leak", const tool = createUseSkillTool(cwd, [], telemetry); if (tool.kind !== "string") throw new Error(`expected string tool, got ${tool.kind}`); - const result = await tool.handler({ name: "acme-internal-deploy" }); + const result = await tool.handler({ name: "acme-internal-deploy" }, new AbortController().signal); // Guard against the test passing because resolution failed: the event only // fires on a resolved skill, so a silent miss would trivially "not leak". @@ -194,7 +194,9 @@ test('subagent events bucket a project-defined profile id to "custom"', async () getWorkdirBase: () => cwd, permissionGate: gate, provider: { providerName: "test-provider", baseURL: "http://localhost", model: "test-model" }, - profiles: [{ id: "acmecorp-release-captain", description: "release", prompt: "release" }], + profiles: [ + { id: "acmecorp-release-captain", description: "release", systemPromptRole: "release" }, + ], run: async () => "done", telemetry, }); diff --git a/tests/unit/telemetry-toggle.test.ts b/tests/unit/telemetry-toggle.test.ts index 76f78b1f7..8acb53838 100644 --- a/tests/unit/telemetry-toggle.test.ts +++ b/tests/unit/telemetry-toggle.test.ts @@ -262,9 +262,9 @@ test("session_id on captured payloads stays constant across an enable/disable/en await getInstance().flush(); expect(capturedBodies.length).toBe(2); - const sessionId = capturedBodies[0].batch[0].properties.session_id; + const sessionId = capturedBodies[0]!.batch[0]!.properties.session_id; expect(typeof sessionId).toBe("string"); expect((sessionId as string).length).toBeGreaterThan(0); - expect(capturedBodies[1].batch[0].properties.session_id).toBe(sessionId); + expect(capturedBodies[1]!.batch[0]!.properties.session_id).toBe(sessionId); expect(sessionId).toBe(getSessionId()); }); diff --git a/tests/unit/telemetry.test.ts b/tests/unit/telemetry.test.ts index ca499bfc5..61c50bc2b 100644 --- a/tests/unit/telemetry.test.ts +++ b/tests/unit/telemetry.test.ts @@ -135,7 +135,7 @@ test("capture strips properties not in the event's allowlist", async () => { }); await telemetry.flush(); expect(events().length).toBe(1); - const body = events()[0]; + const body = events()[0]!; expect(body.properties.status).toBe("ok"); expect(body.properties.turn_count).toBe(3); expect(body.properties.duration_ms).toBe(100); @@ -256,8 +256,8 @@ test("capture payload shape includes distinct_id and common props, with no clien telemetry.capture("cli_start"); await telemetry.flush(); expect(bodies.length).toBe(1); - expect(bodies[0].api_key).toBe("test-key"); - const body = events()[0]; + expect(bodies[0]!.api_key).toBe("test-key"); + const body = events()[0]!; expect(body.event).toBe("cli_start"); expect(body.properties.distinct_id).toBe("my-install-id"); expect(typeof body.timestamp).toBe("string"); @@ -342,10 +342,10 @@ test("capture attaches the same session_id across multiple events in one process await telemetry.flush(); const captured = events(); expect(captured.length).toBe(2); - const sessionId = captured[0].properties.session_id; + const sessionId = captured[0]!.properties.session_id; expect(typeof sessionId).toBe("string"); expect((sessionId as string).length).toBeGreaterThan(0); - expect(captured[1].properties.session_id).toBe(sessionId); + expect(captured[1]!.properties.session_id).toBe(sessionId); expect(sessionId).toBe(getSessionId()); }); @@ -355,7 +355,7 @@ test("ensureTelemetrySettings called twice keeps installationId and enabled flag try { const first = await ensureTelemetrySettings(path); expect(typeof first.telemetry?.installationId).toBe("string"); - expect(first.telemetry?.installationId.length).toBeGreaterThan(0); + expect(first.telemetry?.installationId!.length).toBeGreaterThan(0); const second = await ensureTelemetrySettings(path); expect(second.telemetry?.installationId).toBe(first.telemetry?.installationId); @@ -431,7 +431,7 @@ test("reaching the batch size sends one request holding every queued event", asy telemetry.capture("session_end", { turn_count: 3 }); await new Promise((resolve) => setTimeout(resolve, 5)); expect(bodies.length).toBe(1); - expect(turnCounts(bodies[0])).toEqual([1, 2, 3]); + expect(turnCounts(bodies[0]!)).toEqual([1, 2, 3]); }); test("a partial batch is sent once the batch interval elapses", async () => { @@ -448,7 +448,7 @@ test("a partial batch is sent once the batch interval elapses", async () => { await new Promise((resolve) => setTimeout(resolve, 60)); expect(bodies.length).toBe(1); - expect(turnCounts(bodies[0])).toEqual([1]); + expect(turnCounts(bodies[0]!)).toEqual([1]); }); test("overflowing the queue drops the oldest events", async () => { @@ -463,7 +463,7 @@ test("overflowing the queue drops the oldest events", async () => { for (let turn = 1; turn <= 5; turn++) telemetry.capture("session_end", { turn_count: turn }); await telemetry.flush(); expect(bodies.length).toBe(1); - expect(turnCounts(bodies[0])).toEqual([3, 4, 5]); + expect(turnCounts(bodies[0]!)).toEqual([3, 4, 5]); }); test("captures during a request queue behind it instead of opening a second one", async () => { @@ -502,7 +502,7 @@ test("flush drains a partially full queue within its deadline", async () => { await telemetry.flush(); expect(Date.now() - start).toBeLessThan(500); expect(bodies.length).toBe(1); - expect(turnCounts(bodies[0])).toEqual([1, 2]); + expect(turnCounts(bodies[0]!)).toEqual([1, 2]); }); test("a hung endpoint caps the queue and never opens a second request", async () => { @@ -520,7 +520,7 @@ test("a hung endpoint caps the queue and never opens a second request", async () } expect(gate.bodies.length).toBe(1); expect(gate.peak()).toBe(1); - expect(turnCounts(gate.bodies[0])).toEqual([1, 2]); + expect(turnCounts(gate.bodies[0]!)).toEqual([1, 2]); gate.openGate(); await telemetry.flush(); diff --git a/tests/unit/tui/agent-tools.test.ts b/tests/unit/tui/agent-tools.test.ts index 79f8ad4b8..798f3fec9 100644 --- a/tests/unit/tui/agent-tools.test.ts +++ b/tests/unit/tui/agent-tools.test.ts @@ -1,6 +1,7 @@ import { afterAll, test, expect, mock } from "bun:test"; import type { ToolDefinition, ToolCall } from "@intx/types/runtime"; import { TOOL_NAMES } from "@intx/tools-posix"; +import type { PermissionGate } from "../../../src/permission/gate.js"; const mockDispose = mock(async () => {}); @@ -68,11 +69,16 @@ mock.module("../../../src/agent/posix-tool-plugins.js", () => ({ buildCorePosixToolPlugins: () => [], })); -const mockConnectMCPServer = mock(async (config: { name: string }) => ({ - ok: false as const, - serverName: config.name, - error: "not connected", -})); +const mockConnectMCPServer = mock( + async ( + config: { name: string }, + _options?: import("../../../src/mcp/client.js").MCPConnectOptions, + ) => ({ + ok: false as const, + serverName: config.name, + error: "not connected", + }), +); mock.module("../../../src/mcp/client.js", () => ({ ...realMcpClient, @@ -159,12 +165,22 @@ afterAll(() => { const { createAgentToolset } = await import("../../../src/agent/tools.js"); -const fakePermissionGate = { +const preApproveMock = mock((_tool: string, _pattern: string) => {}); + +const fakePermissionGate: PermissionGate = { evaluate: mock(async () => ({ allowed: true as const })), - preApprove: mock(() => {}), + getApprovals: () => [], + reset: () => {}, + getSessionApprovals: () => [], + removeSessionApproval: () => {}, + setSeededApprovals: () => {}, + getAuto: () => false, + setAuto: () => {}, + getSkipPermissions: () => false, + setSkipPermissions: () => {}, + preApprove: preApproveMock, registerMcpClient: mock(() => {}), unregisterMcpServer: mock(() => {}), - getSkipPermissions: () => false, }; const callOperator = async ( @@ -243,7 +259,7 @@ test("onOperatorGate callback is invoked when the operator tool handler is calle }); test("operator tool pre-approves the declared command for run_shell when an option is chosen", async () => { - fakePermissionGate.preApprove.mockClear(); + preApproveMock.mockClear(); const toolset = await createAgentToolset({ cwd: "/fake", @@ -257,12 +273,12 @@ test("operator tool pre-approves the declared command for run_shell when an opti command: "bun install", }); - expect(fakePermissionGate.preApprove).toHaveBeenCalledWith("run_shell", "bun install"); - expect(fakePermissionGate.preApprove).toHaveBeenCalledTimes(1); + expect(preApproveMock).toHaveBeenCalledWith("run_shell", "bun install"); + expect(preApproveMock).toHaveBeenCalledTimes(1); }); test("operator tool does not pre-approve anything when no command is declared", async () => { - fakePermissionGate.preApprove.mockClear(); + preApproveMock.mockClear(); const toolset = await createAgentToolset({ cwd: "/fake", @@ -272,7 +288,7 @@ test("operator tool does not pre-approve anything when no command is declared", await callOperator(toolset, { question: "Which approach?", options: ["A", "B"] }); - expect(fakePermissionGate.preApprove).not.toHaveBeenCalled(); + expect(preApproveMock).not.toHaveBeenCalled(); }); test("operator tool returns the operator's free-form answer", async () => { diff --git a/tests/unit/tui/run-sink.test.ts b/tests/unit/tui/run-sink.test.ts index 08533a3a0..e88fb9334 100644 --- a/tests/unit/tui/run-sink.test.ts +++ b/tests/unit/tui/run-sink.test.ts @@ -73,7 +73,8 @@ test("sink forwards events via the emitter", () => { test("getTurnCollector is available and has expected shape", () => { const args = makeArgs(); const runSink = createRunSink(args); - const collector = runSink.getTurnCollector(); + // hooks are configured in makeArgs(), so the collector is non-null here + const collector = runSink.getTurnCollector()!; expect(typeof collector.observe).toBe("function"); expect(typeof collector.getTurns).toBe("function"); expect(typeof collector.getTokenUsage).toBe("function"); @@ -100,7 +101,8 @@ test("reset clears status, error, and turn collector between sessions", () => { expect(runSink.getRunError()).toBeUndefined(); // The turn collector returned after reset is fresh. - const collector = runSink.getTurnCollector(); + // hooks are configured in makeArgs(), so the collector is non-null here + const collector = runSink.getTurnCollector()!; expect(collector.getTurns()).toHaveLength(0); expect(collector.getToolCallCount()).toBe(0); diff --git a/tests/unit/tui/runner.test.ts b/tests/unit/tui/runner.test.ts index 9d1ee3cb2..d663d1e6a 100644 --- a/tests/unit/tui/runner.test.ts +++ b/tests/unit/tui/runner.test.ts @@ -40,8 +40,10 @@ test("loadLocalSettingsWriteBase distinguishes absent from unreadable", async () expect(await loadLocalSettingsWriteBase("/nope", async () => null)).toEqual({}); // Readable → merge base. - expect(await loadLocalSettingsWriteBase("/ok", async () => ({ sessionMode: "single" }))).toEqual({ - sessionMode: "single", + expect( + await loadLocalSettingsWriteBase("/ok", async () => ({ sessionMode: "orchestrator" })), + ).toEqual({ + sessionMode: "orchestrator", }); // Unreadable/invalid → null so the caller skips the write instead of @@ -83,7 +85,8 @@ test("rotation resets run-sink so a new session starts from a clean state", () = runSink.reset(); // The new collector is a fresh instance — not the same object as before. - const collectorAfterReset = runSink.getTurnCollector(); + // hooks are configured above, so the collector is non-null here + const collectorAfterReset = runSink.getTurnCollector()!; expect(collectorAfterReset).not.toBe(collectorBeforeReset); // Status is cancelled (no events received in new session yet). diff --git a/tests/unit/tui/view-spec.test.ts b/tests/unit/tui/view-spec.test.ts index 75e33bf96..4937824bf 100644 --- a/tests/unit/tui/view-spec.test.ts +++ b/tests/unit/tui/view-spec.test.ts @@ -109,7 +109,9 @@ describe("view line count", () => { ], }), ).toBe(3); - const many = Array.from({ length: 250 }, (_, i) => [{ type: "text", text: String(i) }]); + const many = Array.from({ length: 250 }, (_, i) => [ + { type: "text" as const, text: String(i) }, + ]); // 200 visible + 1 "+more" footer line expect(at({ type: "grid", columns: cols, rows: many })).toBe(201); }); diff --git a/tests/unit/workflows-director.test.ts b/tests/unit/workflows-director.test.ts index 40cf5bd77..f76494284 100644 --- a/tests/unit/workflows-director.test.ts +++ b/tests/unit/workflows-director.test.ts @@ -28,7 +28,7 @@ const state: ReactorState = { function makeCapabilities(): ReactorCapabilities { return { - infer: (options) => ({ type: "infer", options }), + infer: (options) => (options === undefined ? { type: "infer" } : { type: "infer", options }), executeTools: () => ({ type: "execute_tools", calls: [] }), suspend: (gate) => ({ type: "suspend", gate }), fork: (mode, forkId) => ({ type: "fork", mode, forkId }), @@ -99,7 +99,18 @@ test("the active step directive is injected into the inferred system prompt", as const event: ReactorInboundEvent = { type: "message.received", - message: { role: "user", content: "go" }, + message: { + ref: { uid: 1, mailbox: "INBOX" }, + headers: { + from: "user@test", + to: ["agent@test"], + date: "1970-01-01T00:00:00Z", + messageId: "m1", + }, + flags: [], + signatureStatus: "missing", + content: "go", + }, }; const result = await director.decide(event, state, makeCapabilities()); @@ -131,7 +142,7 @@ test("an advance_workflow tool call advances the runtime through the director", timestamp: 0, }, usage, - source: { id: "t", provider: "openai", model: "test-model" }, + source: { sourceId: "t", provider: "openai", model: "test-model" }, }; await director.decide(turn, state, caps); @@ -155,7 +166,7 @@ function textTurn(text: string): ReactorInboundEvent { timestamp: 0, }, usage, - source: { id: "t", provider: "openai", model: "test-model" }, + source: { sourceId: "t", provider: "openai", model: "test-model" }, }; } @@ -204,7 +215,7 @@ function manageTasksTurn(status: "todo" | "doing" | "done"): ReactorInboundEvent timestamp: 0, }, usage, - source: { id: "t", provider: "openai", model: "test-model" }, + source: { sourceId: "t", provider: "openai", model: "test-model" }, }; } @@ -213,7 +224,7 @@ function emptyTurn(): ReactorInboundEvent { type: "inference.done", turn: { role: "assistant", content: [], model: "test-model", timestamp: 0 }, usage, - source: { id: "t", provider: "openai", model: "test-model" }, + source: { sourceId: "t", provider: "openai", model: "test-model" }, }; } diff --git a/tsconfig.json b/tsconfig.json index 0ddf66dde..afc76ca80 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,48 @@ "@intx/types/*": ["./vendor/intx-types/src/*"] } }, - "include": ["src/**/*.ts", "packages/**/*.ts"] + "include": [ + "src/**/*.ts", + "packages/**/*.ts", + "tests/**/*.ts", + "evals/**/*.ts", + "scripts/**/*.ts" + ], + // Excluded: hermetic sandbox repos for the capability eval suite, not real + // application code. They own their own package.json and are copied into + // per-run eval workdirs rather than imported, and tests/fixtures/buggy-service + // deliberately ships a bug as fixture content for an eval case. + // + // tests/fixtures/crash-run and tests/fixtures/plugins/implement-feature are + // NOT listed here on purpose: they import real production modules by + // relative path (src/index.ts, src/session/active-run.ts, src/session/state.ts, + // src/tui/runner.ts, src/workflows/definition.ts, src/tui/commands/registry.ts) + // and tests/integration/crash-finalize.test.ts spawns them as live exercises + // of that code. Excluding them would recreate exactly the silent-drift gap + // this config change exists to close. + "exclude": [ + "tests/fixtures/broken-toolchain/**", + "tests/fixtures/buggy-service/**", + "tests/fixtures/cart-service/**", + "tests/fixtures/codex-sse/**", + "tests/fixtures/demo-comparison/**", + "tests/fixtures/env-config-build/**", + "tests/fixtures/flaky-baseline/**", + "tests/fixtures/flaky-cache/**", + "tests/fixtures/inventory-service/**", + "tests/fixtures/large-read/**", + "tests/fixtures/marketplace/**", + "tests/fixtures/multi-file-service/**", + "tests/fixtures/multiline-edit/**", + "tests/fixtures/plugins/exa/**", + "tests/fixtures/plugins/example-agent/**", + "tests/fixtures/plugins/example-commands/**", + "tests/fixtures/plugins/example-tool/**", + "tests/fixtures/rawmode-sigint/**", + "tests/fixtures/report-pipeline/**", + "tests/fixtures/skill-workspace/**", + "tests/fixtures/slow-command/**", + "tests/fixtures/web-note/**", + "evals/capability/cases/**" + ] }