fix(runtime): preserve exact selected target identities

This commit is contained in:
morluto committed 2026-10-26 15:09:04 +08:00
1 parent d9e12c957b
commit 44b716f2f2
13 files changed
+146 -45

No files matched your search

+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
}),
]);
+1 -1
View File
@@ -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,
});
+1 -1
View File
@@ -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;
@@ -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),
});
@@ -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") {
+1 -1
View File
@@ -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,
});
+1 -1
View File
@@ -37,7 +37,7 @@ export type WebScreenshotArtifact = z.infer<typeof webScreenshotArtifactSchema>;
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<
@@ -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);
});
@@ -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();
+34
View File
@@ -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);
+20
View File
@@ -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<void> => {
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);
};
@@ -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: {