diff --git a/src/domain/browserObservation.ts b/src/domain/browserObservation.ts index 94f6ee2931..dbddf6de1e 100644 --- a/src/domain/browserObservation.ts +++ b/src/domain/browserObservation.ts @@ -207,7 +207,7 @@ export const listBrowserTargetsInputSchema = z.strictObject({ const inspectWebPageInputFacts = { ...browserInput, - target_id: z.string().trim().min(1), + target_id: z.string().min(1), command_timeout_ms: webRuntimeCommandTimeoutSchema, observation_ms: z.int().min(0).default(500), include_accessibility_text: z.boolean().default(false), diff --git a/src/domain/browserScenarioValues.ts b/src/domain/browserScenarioValues.ts index 8c748dfcc8..72d86241ce 100644 --- a/src/domain/browserScenarioValues.ts +++ b/src/domain/browserScenarioValues.ts @@ -182,7 +182,7 @@ export const browserScenarioBrowserSchema = z.discriminatedUnion("mode", [ z.strictObject({ mode: z.literal("connect"), cdp_endpoint: browserEndpointSchema, - target_id: z.string().trim().min(1), + target_id: z.string().min(1), }), ]); diff --git a/src/domain/browserSession.ts b/src/domain/browserSession.ts index e6ac49f5da..c1bcbe6bd8 100644 --- a/src/domain/browserSession.ts +++ b/src/domain/browserSession.ts @@ -12,7 +12,7 @@ import { browserVersionSchema } from "./browserObservationSchemas.js"; export const observeWebSessionInputSchema = z.strictObject({ cdp_endpoint: browserObservationEndpointSchema, allowed_origins: browserAllowedOriginsSchema, - target_id: z.string().trim().min(1), + target_id: z.string().min(1), observation_ms: z.int().min(1).default(10_000), command_timeout_ms: webRuntimeCommandTimeoutSchema, }); diff --git a/src/domain/javascript/electronObservation.ts b/src/domain/javascript/electronObservation.ts index 9f2e9f3bfe..a49a456ac4 100644 --- a/src/domain/javascript/electronObservation.ts +++ b/src/domain/javascript/electronObservation.ts @@ -26,7 +26,7 @@ export type ListElectronTargetsInput = z.infer< /** Input for passive structural inspection of one Electron file page. */ const inspectElectronPageFacts = { ...approvedElectronInput, - target_id: z.string().trim().min(1), + target_id: z.string().min(1), observation_ms: z.int().min(0).default(100), command_timeout_ms: webRuntimeCommandTimeoutSchema, } as const; diff --git a/src/domain/javascript/javascriptRuntimeObservation.ts b/src/domain/javascript/javascriptRuntimeObservation.ts index 76e1bd6dd7..749d158495 100644 --- a/src/domain/javascript/javascriptRuntimeObservation.ts +++ b/src/domain/javascript/javascriptRuntimeObservation.ts @@ -30,7 +30,7 @@ const observedJavaScriptRuntimeKindSchema = javascriptRuntimeKindSchema.or( /** Input for one passive attach-only V8 Inspector observation. */ export const observeJavaScriptRuntimeInputSchema = z.strictObject({ ...runtimeEndpoint, - target_id: z.string().trim().min(1), + target_id: z.string().min(1), runtime_kind: javascriptRuntimeKindSchema.optional(), observation_ms: z.int().min(0).default(100), }); diff --git a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts index 864a7e5c55..1d05d20986 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts @@ -293,7 +293,7 @@ const assertV8RuntimeParameters = ( z .object({ inspector_endpoint: browserEndpointSchema, - target_id: z.string().trim().min(1), + target_id: z.string().min(1), runtime_kind: javascriptRuntimeKindSchema.optional(), }) .passthrough(), @@ -456,7 +456,7 @@ const assertRuntimeParameters = ( path: readonly (string | number)[], ): void => { const common = { - target_id: z.string().trim().min(1), + target_id: z.string().min(1), include_script_sources: z.boolean(), }; if (expected.kind === "browser") { diff --git a/src/domain/webMcpDiscovery.ts b/src/domain/webMcpDiscovery.ts index 5c2566a4ad..a44dae347f 100644 --- a/src/domain/webMcpDiscovery.ts +++ b/src/domain/webMcpDiscovery.ts @@ -14,7 +14,7 @@ import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js"; export const discoverWebMcpToolsInputSchema = z.strictObject({ cdp_endpoint: browserObservationEndpointSchema, allowed_origins: browserAllowedOriginsSchema, - target_id: z.string().trim().min(1), + target_id: z.string().min(1), observation_ms: z.int().min(0).default(100), command_timeout_ms: webRuntimeCommandTimeoutSchema, }); diff --git a/src/domain/webScreenshot.ts b/src/domain/webScreenshot.ts index 5cb6b6d801..a0246b0764 100644 --- a/src/domain/webScreenshot.ts +++ b/src/domain/webScreenshot.ts @@ -37,7 +37,7 @@ export type WebScreenshotArtifact = z.infer; export const captureWebScreenshotInputSchema = z.strictObject({ cdp_endpoint: browserObservationEndpointSchema, allowed_origins: browserAllowedOriginsSchema, - target_id: z.string().trim().min(1), + target_id: z.string().min(1), command_timeout_ms: webRuntimeCommandTimeoutSchema, }); export type CaptureWebScreenshotInput = z.infer< diff --git a/tests/boundary/browser/browserToolContractExamples.test.ts b/tests/boundary/browser/browserToolContractExamples.test.ts index 094a8d6d5f..4e507d8f69 100644 --- a/tests/boundary/browser/browserToolContractExamples.test.ts +++ b/tests/boundary/browser/browserToolContractExamples.test.ts @@ -1,5 +1,7 @@ import { expect, it } from "vitest"; +import { browserScenarioBrowserSchema } from "../../../src/domain/browserScenarioValues.js"; + import { BROWSER_TOOL_CONTRACTS } from "../../../src/contracts/browserToolContracts.js"; import { comparePngScreenshots } from "../../../src/browser/PngVisualDiff.js"; import { compareWebScreenshotsInputSchema } from "../../../src/domain/webScreenshot.js"; @@ -17,3 +19,26 @@ it("executes the advertised screenshot comparison example through PNG checksum v changed_pixels: 0, }); }); + +it.each([" target ", "\t", "target"])( + "preserves the connect scenario target identity %j", + (targetId) => { + expect( + browserScenarioBrowserSchema.parse({ + mode: "connect", + cdp_endpoint: "http://127.0.0.1:9222", + target_id: targetId, + }), + ).toMatchObject({ target_id: targetId }); + }, +); + +it("rejects an empty connect scenario target identity", () => { + expect( + browserScenarioBrowserSchema.safeParse({ + mode: "connect", + cdp_endpoint: "http://127.0.0.1:9222", + target_id: "", + }).success, + ).toBe(false); +}); diff --git a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts index 79256b4748..cb0a393695 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts @@ -42,43 +42,52 @@ const readInput = (input: unknown) => { const reconcileInput = (input: unknown) => reconcileJavaScriptRuntime(readInput(input)); -it("preserves source-capture input paths for runtime Evidence", async () => { - const root = await applicationFixture(); - const application = await analyzeFixture(root); - const runtime = electronRuntimeEvidence(root, SOURCE); - const invalidRuntime = createEvidence(undefined, runtime.provider, { - predicateType: runtime.predicate_type, - operation: runtime.operation, - parameters: { ...runtime.parameters, include_script_sources: false }, - result: runtime.normalized_result, - confidence: runtime.confidence, - authority: runtime.authority, - }); +it.each([ + { + parameter: "include_script_sources", + value: false, + message: + "Runtime Evidence contains source without source-capture selection", + }, + { + parameter: "target_id", + value: " target-main ", + message: "Runtime Evidence target disagrees with its captured result", + }, +])( + "preserves $parameter input paths for runtime Evidence", + async ({ parameter, value, message }) => { + const root = await applicationFixture(); + const application = await analyzeFixture(root); + const runtime = electronRuntimeEvidence(root, SOURCE); + const invalidRuntime = createEvidence(undefined, runtime.provider, { + predicateType: runtime.predicate_type, + operation: runtime.operation, + parameters: { ...runtime.parameters, [parameter]: value }, + result: runtime.normalized_result, + confidence: runtime.confidence, + authority: runtime.authority, + }); - const result = reconcileJavaScriptRuntimeEvidence({ - static_layers: [{ role: "application", analysis: application }], - runtime_observations: [invalidRuntime], - }); - expect(result).toMatchObject({ - ok: false, - error: { - _tag: "AnalysisInputError", - issues: [ - { - path: [ - "runtime_observations", - 0, - "parameters", - "include_script_sources", - ], - reason: "invalid_value", - message: - "Runtime Evidence contains source without source-capture selection", - }, - ], - }, - }); -}); + const result = reconcileJavaScriptRuntimeEvidence({ + static_layers: [{ role: "application", analysis: application }], + runtime_observations: [invalidRuntime], + }); + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + issues: [ + { + path: ["runtime_observations", 0, "parameters", parameter], + reason: "invalid_value", + message, + }, + ], + }, + }); + }, +); it("reconciles runtime scripts inside dot-prefixed child directories", async () => { const root = await applicationFixture(); diff --git a/tests/boundary/mcp/browserMcp.test.ts b/tests/boundary/mcp/browserMcp.test.ts index f1c0247f09..2c426ba8ac 100644 --- a/tests/boundary/mcp/browserMcp.test.ts +++ b/tests/boundary/mcp/browserMcp.test.ts @@ -746,6 +746,40 @@ it.each(["full", "compact"] as const)( }, ); +it.each([ + ["inspect_web_page", 0], + ["analyze_web_bundle", 0], + ["observe_web_session", 1], + ["discover_webmcp_tools", 0], + ["capture_web_screenshot", undefined], +] as const)( + "selects the exact opaque target ID through %s", + async (name, observationMs) => { + const browser = await startFakeCdpBrowser(); + browsers.push(browser); + const { client } = await connectBrowser(); + const args = { + cdp_endpoint: browser.endpoint, + allowed_origins: [browser.allowedOrigin], + ...(observationMs === undefined ? {} : { observation_ms: observationMs }), + }; + const rejected = await client.callTool({ + name, + arguments: { ...args, target_id: " allowed-page " }, + }); + expect(parseMcpToolError(rejected)).toMatchObject({ + error: { details: { reason: "target_not_found" } }, + }); + expect(browser.commands).toHaveLength(0); + const accepted = await client.callTool({ + name, + arguments: { ...args, target_id: "allowed-page" }, + }); + expect(accepted.isError).not.toBe(true); + expect(browser.commands.length).toBeGreaterThan(0); + }, +); + it("does not attach to a target outside the request's allowed origin scope", async () => { const browser = await startFakeCdpBrowser(); browsers.push(browser); diff --git a/tests/boundary/mcp/electronMcp.test.ts b/tests/boundary/mcp/electronMcp.test.ts index cf62073f0a..84717af1cc 100644 --- a/tests/boundary/mcp/electronMcp.test.ts +++ b/tests/boundary/mcp/electronMcp.test.ts @@ -344,6 +344,7 @@ it("exposes endpoint-scoped Electron discovery and inspection as Evidence", asyn targets: [{ target_id: "electron-page" }], }, }); + await expectPaddedElectronTargetRejected(client, browser); const inspected = await client.callTool({ name: "inspect_electron_page", arguments: { @@ -703,3 +704,22 @@ it("reports every unresolved Electron path without starting a provider", async ( expect(capture).not.toHaveBeenCalled(); await client.ping(); }); + +const expectPaddedElectronTargetRejected = async ( + client: Client, + browser: FakeCdpBrowser, +): Promise => { + const commandsBeforeRejectedInspection = browser.commands.length; + const rejectedInspection = await client.callTool({ + name: "inspect_electron_page", + arguments: { + cdp_endpoint: browser.endpoint, + target_id: " electron-page ", + observation_ms: 0, + }, + }); + expect(parseMcpToolError(rejectedInspection)).toMatchObject({ + error: { details: { reason: "target_not_found" } }, + }); + expect(browser.commands).toHaveLength(commandsBeforeRejectedInspection); +}; diff --git a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts index c01768fa0e..82edcee870 100644 --- a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts +++ b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts @@ -92,6 +92,7 @@ function registerRuntimeParameterContract( for (const [target_id, runtime_kind, expectedPath] of [ ["example-v8-target", "node", "runtime_kind"], ["different-target", "electron-main", "target_id"], + [" example-v8-target ", "electron-main", "target_id"], ["example-v8-target", "electron-main", null], ] as const) { const evidence = createJavaScriptRuntimeObservationEvidence( @@ -226,6 +227,18 @@ function registerRetainedRuntimeEvidence( targets: [{ target_id: inspector.targetId }], }, }); + const paddedTarget = await client.callTool({ + name: "observe_javascript_runtime", + arguments: { + inspector_endpoint: inspector.endpoint, + target_id: ` ${inspector.targetId} `, + observation_ms: 0, + }, + }); + expect(parseMcpToolError(paddedTarget).error).toMatchObject({ + code: "target_unavailable", + }); + expect(inspector.commands).toEqual([]); const observedRuntime = await client.callTool({ name: "observe_javascript_runtime", arguments: {