diff --git a/src/browser/CdpPageCaptureScripts.test.ts b/src/browser/CdpPageCaptureScripts.test.ts index 4494cf70f2..19b7bfccc0 100644 --- a/src/browser/CdpPageCaptureScripts.test.ts +++ b/src/browser/CdpPageCaptureScripts.test.ts @@ -4,10 +4,7 @@ import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { inspectWebPageInputSchema } from "../domain/browserObservation.js"; import { CdpCaptureEvents } from "./CdpCaptureEvents.js"; import type { CdpConnection } from "./CdpConnection.js"; -import { - CDP_SCRIPT_SOURCE_BYTES_OMISSION_REASON, - CDP_SCRIPT_SOURCE_DEADLINE_OMISSION_REASON, -} from "./CdpScriptSourceBudget.js"; +import { CDP_SCRIPT_SOURCE_BYTES_OMISSION_REASON } from "./CdpScriptSourceBudget.js"; import { captureScripts } from "./CdpPageCaptureScripts.js"; import type { CaptureContext } from "./CdpPageCapture.js"; import type { CapturedScript } from "./CdpCaptureEventTypes.js"; @@ -126,73 +123,98 @@ it("preserves the caller cancellation reason when a source read is pending", asy ).rejects.toMatchObject({ _tag: "AnalysisCancelledError", cause: reason }); }); -it("stops source reads at the shared collection deadline and retains metadata", async () => { - vi.useFakeTimers(); - try { - const input = inspectWebPageInputSchema.parse({ - cdp_endpoint: "http://127.0.0.1:9222", - target_id: "page-1", - allowed_origins: ["https://example.test"], - observation_ms: 0, - include_script_sources: true, - }); - const events = new CdpCaptureEvents(input, new Set(input.allowed_origins)); - events.scripts.set("first", script("first")); - events.scripts.set("second", script("second")); - const send = vi.fn( - ( - _method: string, - _params: unknown, - _sessionId: string | undefined, - signal?: AbortSignal, - ) => - new Promise((_resolve, reject) => { - signal?.addEventListener( - "abort", - () => reject(new AnalysisCancelledError("inspect_web_page")), - { once: true }, - ); - }), - ); - const context = { - connection: { send } as unknown as CdpConnection, - sessionId: undefined, - operation: "inspect_web_page", - discovery: undefined, - target: undefined, - input, - } as unknown as CaptureContext; - const pending = captureScripts({ - context, - events, - rawResources: [], - resources: [], - frameIds: new Set(), - }); +it.each([ + { selection: "default", commandTimeoutMs: undefined, deadlineMs: 20_000 }, + { selection: "caller-selected", commandTimeoutMs: 1_234, deadlineMs: 1_234 }, +])( + "stops source reads at the $selection collection deadline and retains metadata", + async ({ commandTimeoutMs, deadlineMs }) => { + vi.useFakeTimers(); + try { + const input = inspectWebPageInputSchema.parse({ + cdp_endpoint: "http://127.0.0.1:9222", + target_id: "page-1", + allowed_origins: ["https://example.test"], + observation_ms: 0, + include_script_sources: true, + ...(commandTimeoutMs === undefined + ? {} + : { command_timeout_ms: commandTimeoutMs }), + }); + const events = new CdpCaptureEvents( + input, + new Set(input.allowed_origins), + ); + events.scripts.set("first", script("first")); + events.scripts.set("second", script("second")); + const send = vi.fn( + ( + _method: string, + _params: unknown, + _sessionId: string | undefined, + signal?: AbortSignal, + ) => + new Promise((_resolve, reject) => { + signal?.addEventListener( + "abort", + () => reject(new AnalysisCancelledError("inspect_web_page")), + { once: true }, + ); + }), + ); + const context = { + connection: { send } as unknown as CdpConnection, + sessionId: undefined, + operation: "inspect_web_page", + discovery: undefined, + target: undefined, + input, + } as unknown as CaptureContext; + const pending = captureScripts({ + context, + events, + rawResources: [], + resources: [], + frameIds: new Set(), + }); - await vi.advanceTimersByTimeAsync(20_000); - const result = await pending; + await vi.advanceTimersByTimeAsync(deadlineMs); + const result = await pending; - expect(send).toHaveBeenCalledTimes(1); - expect(result.inventory.items.map((item) => item.source)).toEqual([ - { - included: false, - reason: CDP_SCRIPT_SOURCE_DEADLINE_OMISSION_REASON, - }, - { - included: false, - reason: CDP_SCRIPT_SOURCE_DEADLINE_OMISSION_REASON, - }, - ]); - expect(events.completeness.snapshot().excluded).toContainEqual({ - section: "script_sources", - reason: "resource_budget_exhausted", - count: 2, - }); - } finally { - vi.useRealTimers(); - } -}); + expect(send).toHaveBeenCalledTimes(1); + expect(result.inventory.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + cdp_hash: "first", + url: "https://example.test/first.js", + }), + expect.objectContaining({ + cdp_hash: "second", + url: "https://example.test/second.js", + }), + ]), + ); + const expectedReason = `script source was omitted because the ${deadlineMs} ms command_timeout_ms source collection deadline elapsed (deadlineExpired). This is not the caller observation window. The 33554432-byte source budget stays fixed. This release has no runtime override. command_timeout_ms does not raise it.`; + expect(result.inventory.items.map((item) => item.source)).toEqual([ + { + included: false, + reason: expectedReason, + }, + { + included: false, + reason: expectedReason, + }, + ]); + expect(events.completeness.snapshot().excluded).toContainEqual({ + section: "script_sources", + reason: "resource_budget_exhausted", + count: 2, + }); + } finally { + vi.useRealTimers(); + } + }, +); it("leaves source-off capture unchanged and does not issue source reads", async () => { const { result, events, send } = await capture({ first: "unused" }, false); diff --git a/src/browser/CdpScriptSourceBudget.ts b/src/browser/CdpScriptSourceBudget.ts index f6b1a89f40..09bd007344 100644 --- a/src/browser/CdpScriptSourceBudget.ts +++ b/src/browser/CdpScriptSourceBudget.ts @@ -15,9 +15,6 @@ export const cdpScriptSourceDeadlineOmissionReason = ( ): string => `script source was omitted because the ${String(timeoutMs)} ms command_timeout_ms source collection deadline elapsed (deadlineExpired). This is not the caller observation window. ${noRuntimeOverrideSentence(`The ${String(WEB_RUNTIME_LIMITS.sourceBytes)}-byte source budget stays fixed.`, "command_timeout_ms")}`; -export const CDP_SCRIPT_SOURCE_DEADLINE_OMISSION_REASON = - cdpScriptSourceDeadlineOmissionReason(selectedCdpCommandTimeoutMs(undefined)); - /** Shared admission for source text retained from passive CDP script reads. */ export class CdpScriptSourceBudget { readonly #deadline: ProviderStartupDeadline; diff --git a/src/config/mcpStartupBudgets.test.ts b/src/config/mcpStartupBudgets.test.ts index e1a77ed9b9..cf5fbddb09 100644 --- a/src/config/mcpStartupBudgets.test.ts +++ b/src/config/mcpStartupBudgets.test.ts @@ -4,7 +4,6 @@ import { ConfigurationError } from "../domain/configurationErrors.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { parseEnvironment } from "./environment.js"; import { - DEFAULT_MCP_STARTUP_BUDGETS, MCP_PHASE_BUDGET_FLOOR_MS, MCP_PHASE_BUDGET_MAXIMUM_MS, parseMcpStartupBudgets, @@ -12,12 +11,51 @@ import { } from "./mcpStartupBudgets.js"; import { parseConfig } from "./parseConfig.js"; -it("keeps today's phase budgets when the settings are unset", () => { +const expectedDefaultBudgets = { + initialize: { + setting: "REA_MCP_INITIALIZE_BUDGET_MS", + phase: "MCP initialize phase budget", + milliseconds: 15_000, + source: "default", + defaultMilliseconds: 15_000, + floorMilliseconds: 1, + maximumMilliseconds: 2_147_483_647, + }, + toolsList: { + setting: "REA_MCP_TOOLS_LIST_BUDGET_MS", + phase: "MCP tools/list phase budget", + milliseconds: 5_000, + source: "default", + defaultMilliseconds: 5_000, + floorMilliseconds: 1, + maximumMilliseconds: 2_147_483_647, + }, + firstCatalog: { + setting: "REA_MCP_FIRST_CATALOG_BUDGET_MS", + phase: "MCP first-catalog phase budget", + milliseconds: 20_000, + source: "default", + defaultMilliseconds: 20_000, + floorMilliseconds: 1, + maximumMilliseconds: 2_147_483_647, + }, + doctor: { + setting: "REA_MCP_DOCTOR_DEADLINE_MS", + phase: "MCP doctor startup deadline", + milliseconds: 30_000, + source: "default", + defaultMilliseconds: 30_000, + floorMilliseconds: 1, + maximumMilliseconds: 2_147_483_647, + }, +}; + +it("resolves the default phase budgets and their configuration metadata", () => { expect(parseMcpStartupBudgets({})).toEqual({ ok: true, - value: DEFAULT_MCP_STARTUP_BUDGETS, + value: expectedDefaultBudgets, }); - expect(resolveMcpStartupBudgets({})).toEqual(DEFAULT_MCP_STARTUP_BUDGETS); + expect(resolveMcpStartupBudgets({})).toEqual(expectedDefaultBudgets); const parsed = parseConfig({}); if (!parsed.ok) throw new Error("unset phase budgets were rejected"); expect(parsed.value.analysisProvider).toBe("auto"); @@ -35,23 +73,23 @@ it("uses valid phase budget overrides", () => { ok: true, value: { initialize: { - ...DEFAULT_MCP_STARTUP_BUDGETS.initialize, + ...expectedDefaultBudgets.initialize, milliseconds: 16_000, source: "setting", }, toolsList: { - ...DEFAULT_MCP_STARTUP_BUDGETS.toolsList, + ...expectedDefaultBudgets.toolsList, milliseconds: 6_000, source: "setting", }, firstCatalog: { - ...DEFAULT_MCP_STARTUP_BUDGETS.firstCatalog, + ...expectedDefaultBudgets.firstCatalog, milliseconds: 21_000, source: "setting", }, doctor: { - ...DEFAULT_MCP_STARTUP_BUDGETS.doctor, - milliseconds: MCP_PHASE_BUDGET_MAXIMUM_MS, + ...expectedDefaultBudgets.doctor, + milliseconds: 2_147_483_647, source: "setting", }, }, diff --git a/src/config/mcpStartupBudgets.ts b/src/config/mcpStartupBudgets.ts index 85a07b96be..5753661dbc 100644 --- a/src/config/mcpStartupBudgets.ts +++ b/src/config/mcpStartupBudgets.ts @@ -112,14 +112,6 @@ const defaultBudget = (phase: McpPhaseBudgetDefinition): McpPhaseBudget => ({ maximumMilliseconds: MCP_PHASE_BUDGET_MAXIMUM_MS, }); -/** Budgets used when every phase setting is unset. */ -export const DEFAULT_MCP_STARTUP_BUDGETS: McpStartupBudgets = { - initialize: defaultBudget(PHASES[0]), - toolsList: defaultBudget(PHASES[1]), - firstCatalog: defaultBudget(PHASES[2]), - doctor: defaultBudget(PHASES[3]), -}; - const parsePhaseBudget = ( phase: McpPhaseBudgetDefinition, value: string | undefined, diff --git a/src/ghidra/GhidraDoctor.test.ts b/src/ghidra/GhidraDoctor.test.ts index ae7dcb458c..02ccaafbf3 100644 --- a/src/ghidra/GhidraDoctor.test.ts +++ b/src/ghidra/GhidraDoctor.test.ts @@ -8,7 +8,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { runDoctor, type DoctorHost } from "../application/Doctor.js"; import { createDoctorHostFixture } from "../application/Doctor.fixture.js"; import { providerRegistrationEnvironment } from "../application/SetupRegistrationEnvironment.js"; -import { WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON } from "../process/WindowsAuthority.js"; import * as nativeLoader from "../windows/WindowsNativeLoader.js"; import { inspectGhidraInstallation, @@ -199,7 +198,7 @@ describe("Windows Ghidra doctor authority", () => { vi.spyOn(nativeLoader, "systemWindowsNativeAuthority").mockReturnValue({ available: false, failureStage: "artifact", - reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + reason: "Authored native artifact unavailable diagnostic", }); }); afterEach(() => vi.restoreAllMocks()); @@ -243,7 +242,7 @@ describe("Windows Ghidra doctor authority", () => { ).toMatchObject({ ok: false, classification: "missing_dependency", - detail: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + detail: "Authored native artifact unavailable diagnostic", remediation: expect.stringContaining("matching Windows x64 REA package"), }); }); @@ -267,7 +266,7 @@ describe("Windows Ghidra doctor authority", () => { expect.objectContaining({ name: "ghidra-native_authority", classification: "missing_dependency", - detail: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + detail: "Authored native artifact unavailable diagnostic", }), ); }); @@ -278,7 +277,7 @@ describe("Windows native doctor failure diagnostics", () => { vi.spyOn(nativeLoader, "systemWindowsNativeAuthority").mockReturnValue({ available: false, failureStage: "artifact", - reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + reason: "Authored native artifact unavailable diagnostic", }); }); afterEach(() => vi.restoreAllMocks()); @@ -371,7 +370,7 @@ describe("Windows native doctor failure diagnostics", () => { expect.objectContaining({ name: "native_authority", code: "runtime_missing", - detail: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + detail: "Authored native artifact unavailable diagnostic", }), ); }); diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index 5d51e4069f..9241b086b5 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -19,7 +19,6 @@ import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest"; import * as nativeLoader from "../windows/WindowsNativeLoader.js"; -import { WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON } from "../process/WindowsAuthority.js"; import { fixtureDosLoadImage, compareDosClientFixture, @@ -703,12 +702,13 @@ describe("Ghidra platform support", () => { describe("Ghidra Windows platform support", () => { it("keeps Windows P0 unavailable until native isolation authority exists", async () => { + const reason = "Authored native artifact unavailable diagnostic"; const native = vi .spyOn(nativeLoader, "systemWindowsNativeAuthority") .mockReturnValue({ available: false, failureStage: "artifact", - reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + reason, }); onTestFinished(() => native.mockRestore()); const ghidra = provider(installationHost("win32")); @@ -728,9 +728,7 @@ describe("Ghidra Windows platform support", () => { expect(await ghidra.inspectAvailability()).toMatchObject({ status: "unavailable", code: "runtime_missing", - reason: expect.stringContaining( - "Windows native controls are unavailable", - ), + reason, diagnostics: { windows_native_failure: { stage: "artifact", diff --git a/src/main/messages.test.ts b/src/main/messages.test.ts index 376a918ad9..9e4be50c0e 100644 --- a/src/main/messages.test.ts +++ b/src/main/messages.test.ts @@ -10,8 +10,8 @@ import { DEFAULT_CAPTURE_DECODER_HEAP, } from "../config/captureDecoderHeap.js"; import { - DEFAULT_MCP_STARTUP_BUDGETS, mcpPhaseBudgetRecovery, + resolveMcpStartupBudgets, } from "../config/mcpStartupBudgets.js"; import { AnalysisTimeoutError } from "../domain/analysisErrorCore.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; @@ -77,7 +77,7 @@ it("keeps ceiling dial recoveries and scrubs non-adjustable timeout teaching", a expect(heap).toContain("source "); expect(heap).toContain("then restart REA"); expect(heap).not.toContain("tell the user"); - const phase = mcpPhaseBudgetRecovery(DEFAULT_MCP_STARTUP_BUDGETS.initialize); + const phase = mcpPhaseBudgetRecovery(resolveMcpStartupBudgets({}).initialize); expect(phase).toContain("Set "); expect(phase).toContain("then restart REA"); expect(phase).not.toContain("tell the user"); diff --git a/src/process/WindowsAuthority.ts b/src/process/WindowsAuthority.ts index bef4f0c544..7fa4ed7366 100644 --- a/src/process/WindowsAuthority.ts +++ b/src/process/WindowsAuthority.ts @@ -34,10 +34,6 @@ export interface WindowsNativeCapabilitySet { readonly reparse_safe_path_admission: WindowsNativeCapability; } -/** Stable explanation used whenever the native Windows authority is absent. */ -export const WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON = - "Windows native controls are unavailable in this runtime; use the Windows x64 REA package with its matching bundled native artifact."; - /** One readiness observation, shared by diagnostics and security controls. */ export type WindowsNativeReadiness = | { readonly available: true } diff --git a/tests/boundary/mcp/mcpStartupPolicy.test.ts b/tests/boundary/mcp/mcpStartupPolicy.test.ts index e2522c5961..a23555512b 100644 --- a/tests/boundary/mcp/mcpStartupPolicy.test.ts +++ b/tests/boundary/mcp/mcpStartupPolicy.test.ts @@ -13,7 +13,6 @@ import { profileMcpModuleLoading, } from "../../../scripts/lib/mcp-startup-probe.mjs"; import { - DEFAULT_MCP_STARTUP_BUDGETS, MCP_PHASE_BUDGET_FLOOR_MS, MCP_PHASE_BUDGET_MAXIMUM_MS, mcpPhaseBudgetRecovery, @@ -60,7 +59,6 @@ const expectThrownBudget = async ( describe("MCP startup policy", () => { it("names phase budget settings without turning Codex's 30s into a REA knob", () => { const budgets = resolveMcpStartupBudgets({}); - expect(budgets).toEqual(DEFAULT_MCP_STARTUP_BUDGETS); expect(MCP_STARTUP_POLICY.codexStartupTimeoutSeconds).toBe(30); expect(budgets.initialize.milliseconds).toBe(15_000); expect(budgets.toolsList.milliseconds).toBe(5_000); @@ -123,15 +121,6 @@ describe("MCP startup policy", () => { const probeChild = resolve("tests/fixtures/mcpStartupProbeChild.mjs"); -const selectedBudget = ( - budget: McpPhaseBudget, - milliseconds: number, -): McpPhaseBudget => ({ - ...budget, - milliseconds, - source: "setting", -}); - const probePolicy = ( overrides: Partial<{ initializeBudgetMs: number; @@ -141,24 +130,18 @@ const probePolicy = ( }> = {}, ) => { const generous = 30_000; - return { - initialize: selectedBudget( - DEFAULT_MCP_STARTUP_BUDGETS.initialize, + return resolveMcpStartupBudgets({ + REA_MCP_INITIALIZE_BUDGET_MS: String( overrides.initializeBudgetMs ?? generous, ), - toolsList: selectedBudget( - DEFAULT_MCP_STARTUP_BUDGETS.toolsList, + REA_MCP_TOOLS_LIST_BUDGET_MS: String( overrides.toolsListBudgetMs ?? generous, ), - firstCatalog: selectedBudget( - DEFAULT_MCP_STARTUP_BUDGETS.firstCatalog, + REA_MCP_FIRST_CATALOG_BUDGET_MS: String( overrides.firstCatalogBudgetMs ?? generous, ), - doctor: selectedBudget( - DEFAULT_MCP_STARTUP_BUDGETS.doctor, - overrides.doctorDeadlineMs ?? generous, - ), - }; + REA_MCP_DOCTOR_DEADLINE_MS: String(overrides.doctorDeadlineMs ?? generous), + }); }; describe("packaged MCP startup probe failures", () => { diff --git a/tests/conformance/providers/ghidra/windowsCapabilities.test.ts b/tests/conformance/providers/ghidra/windowsCapabilities.test.ts index abc96587d8..77d2bca8bd 100644 --- a/tests/conformance/providers/ghidra/windowsCapabilities.test.ts +++ b/tests/conformance/providers/ghidra/windowsCapabilities.test.ts @@ -5,7 +5,6 @@ import { systemNoFollowOpenCapability, type WindowsCapabilityDependencies, } from "../../../../src/application/WindowsCapabilities.js"; -import { WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON } from "../../../../src/process/WindowsAuthority.js"; const dependencies = ( overrides: Partial = {}, @@ -15,7 +14,7 @@ const dependencies = ( nativeCapabilities: () => ({ job_object_process_ownership: { available: false, - reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + reason: "Authored Job Object authority unavailable diagnostic", proof: "not-proven", code: "runtime_missing", failureStage: "artifact", @@ -23,7 +22,7 @@ const dependencies = ( }, private_runtime_dacl: { available: false, - reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + reason: "Authored private DACL authority unavailable diagnostic", proof: "not-proven", code: "runtime_missing", failureStage: "artifact", @@ -31,7 +30,7 @@ const dependencies = ( }, reparse_safe_path_admission: { available: false, - reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + reason: "Authored reparse admission authority unavailable diagnostic", proof: "not-proven", code: "runtime_missing", failureStage: "artifact", @@ -98,7 +97,7 @@ describe("Windows host capability report", () => { security: { job_object_process_ownership: { available: false, - reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + reason: "Authored Job Object authority unavailable diagnostic", proof: "not-proven", code: "runtime_missing", failureStage: "artifact", @@ -106,7 +105,7 @@ describe("Windows host capability report", () => { }, private_runtime_dacl: { available: false, - reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + reason: "Authored private DACL authority unavailable diagnostic", proof: "not-proven", code: "runtime_missing", failureStage: "artifact", @@ -114,7 +113,7 @@ describe("Windows host capability report", () => { }, reparse_safe_path_admission: { available: false, - reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON, + reason: "Authored reparse admission authority unavailable diagnostic", proof: "not-proven", code: "runtime_missing", failureStage: "artifact",