diff --git a/docs/product-catalog.json b/docs/product-catalog.json index 01f67a6b3..d351def6b 100644 --- a/docs/product-catalog.json +++ b/docs/product-catalog.json @@ -675,9 +675,9 @@ "mcp_prompts": 6 }, "digests": { - "tools_sha256": "8816080083d86cb4c05db8d1ef452938abef2e6fdeead566252592ffbb55d828", + "tools_sha256": "c77a3b4740fc2e78172e1d6757b1915d2b638185d8e87d2f1885bb41ce538813", "prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d", - "combined_sha256": "9b4e896d2585645f1ed1f9f3eef270cf798f08035c70091178b7bd6119b22bf5", + "combined_sha256": "e652c2ec4cde078a23735789c1d01cb1b9d316c9af7708e86f2c5c31b37b0e51", "providers_sha256": "aea15f517c822d80918ccaba57a4fa06557f484cf27b8acbaed4e1a660fac896" } } diff --git a/docs/verification/managed-conformance-manifest.json b/docs/verification/managed-conformance-manifest.json index d427f4a88..c62a0db7f 100644 --- a/docs/verification/managed-conformance-manifest.json +++ b/docs/verification/managed-conformance-manifest.json @@ -1,5 +1,5 @@ { - "manifest_id": "ecm_82b2f9406099c2d152fcfa020c3acb23deb22d82405d42a610553e2bd0da94cc", + "manifest_id": "ecm_aa4a1c78be657e901c87cc215a703938cfe54d7a54411cbfd64872de3ce78b93", "verifier": { "id": "rea-managed-conformance", "version": "1" @@ -13,7 +13,7 @@ "skill_digests": [ { "skill_id": "reverse-engineer-anything", - "sha256": "77c5acdd9315573e4a1a0843e4ca643626674d2872729fd5db491261d386f110" + "sha256": "57f28ace603b9eb5d6d9ea64d68f563ea8a4833e6a53dd7b4b40ce5fb3ac56e1" } ], "claims": [ diff --git a/skills/reverse-engineer-anything/SKILL.md b/skills/reverse-engineer-anything/SKILL.md index fe96353d4..5932d0631 100644 --- a/skills/reverse-engineer-anything/SKILL.md +++ b/skills/reverse-engineer-anything/SKILL.md @@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi metadata: version: "27" tool_count: 133 - catalog_digest: "9b4e896d2585645f1ed1f9f3eef270cf798f08035c70091178b7bd6119b22bf5" + catalog_digest: "e652c2ec4cde078a23735789c1d01cb1b9d316c9af7708e86f2c5c31b37b0e51" --- # REA diff --git a/src/browser/PlaywrightScenarioActions.test.ts b/src/browser/PlaywrightScenarioActions.test.ts index 2acd4322d..53ffd685d 100644 --- a/src/browser/PlaywrightScenarioActions.test.ts +++ b/src/browser/PlaywrightScenarioActions.test.ts @@ -7,7 +7,7 @@ import { browserScenarioSchema } from "../domain/browserScenario.js"; it("uses exact accessible-name matching without a caller confirmation field", async () => { const scenario = browserScenarioSchema.parse({ - browser: { mode: "launch", executable_path: "chrome" }, + browser: { mode: "launch", executable_path: "/opt/chromium/chrome" }, start_url: { url: "https://app.example.test/" }, actions: [ { diff --git a/src/cli/electronCommands.ts b/src/cli/electronCommands.ts index adfce2e09..e91b1ff63 100644 --- a/src/cli/electronCommands.ts +++ b/src/cli/electronCommands.ts @@ -18,7 +18,7 @@ import { projectInputIssues } from "../domain/inputIssueProjection.js"; import type { JsonValue } from "../domain/jsonValue.js"; import type { Logger } from "../logger.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; -import { parseCliJsonInput } from "../cliJsonInput.js"; +import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js"; import { electronPageInspectionOptions, javascriptApplicationOptions, @@ -57,7 +57,11 @@ const registerElectronActiveCommand = ( ); if (!input.ok) return input.error; const parsed = electronActiveObservationInputSchema.safeParse( - input.value, + resolveCliJsonPaths(input.value, [ + ["executable_path"], + ["application_path"], + ["application_root"], + ]), ); if (!parsed.success) return inputError( diff --git a/src/cli/firmwareCommands.ts b/src/cli/firmwareCommands.ts index 961b44386..ad08c7804 100644 --- a/src/cli/firmwareCommands.ts +++ b/src/cli/firmwareCommands.ts @@ -1,4 +1,5 @@ import { z } from "incur"; +import { resolve } from "node:path"; import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js"; import { createFirmwareAnalysisProvider } from "../composition/firmware.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; @@ -38,7 +39,8 @@ export const registerFirmwareCommands = ( execute( CLI_COMMANDS.inspectFirmwareRegions, "inspect_firmware_regions", - args, + // CLI paths are operator-relative; resolve before shared validation. + { path: resolve(args.path) }, ), }); cli.command(CLI_COMMANDS.extractFirmware, { @@ -81,8 +83,8 @@ export const registerFirmwareCommands = ( }), run: ({ args, options }) => execute(CLI_COMMANDS.extractFirmware, "extract_firmware", { - path: args.path, - output_directory: args.outputDirectory, + path: resolve(args.path), + output_directory: resolve(args.outputDirectory), ...(options.offset === undefined && options.length === undefined ? {} : { range: { offset: options.offset, length: options.length } }), diff --git a/src/cliBrowserScenarioCommands.ts b/src/cliBrowserScenarioCommands.ts index 46f928741..23984effb 100644 --- a/src/cliBrowserScenarioCommands.ts +++ b/src/cliBrowserScenarioCommands.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { captureBrowserScenario } from "./application/BrowserScenarioCaptureService.js"; import { createBrowserScenarioProvider } from "./composition/browserScenario.js"; import { CLI_COMMANDS } from "./cliCommandNames.js"; -import { parseCliJsonInput } from "./cliJsonInput.js"; +import { parseCliJsonInput, resolveCliJsonPaths } from "./cliJsonInput.js"; import { logCliCommand } from "./cliLogging.js"; import { AnalysisInputError } from "./domain/analysisErrorCore.js"; import { projectAnalysisError } from "./domain/analysisErrorProjection.js"; @@ -32,7 +32,9 @@ export const registerBrowserScenarioCommands = ( logCliCommand(logger, CLI_COMMANDS.captureBrowserScenario, async () => { const input = await parseCliJsonInput(args.inputJson, OPERATION); if (!input.ok) return input.error; - const scenario = browserScenarioSchema.safeParse(input.value); + const scenario = browserScenarioSchema.safeParse( + resolveCliJsonPaths(input.value, [["browser", "executable_path"]]), + ); if (!scenario.success) return cliError( new AnalysisInputError( diff --git a/src/cliJsonInput.ts b/src/cliJsonInput.ts index f6356c71e..4a29c292a 100644 --- a/src/cliJsonInput.ts +++ b/src/cliJsonInput.ts @@ -1,4 +1,5 @@ import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; import { AnalysisInputError } from "./domain/analysisErrorCore.js"; import { projectAnalysisError } from "./domain/analysisErrorProjection.js"; @@ -33,6 +34,40 @@ export const parseCliJsonInput = async ( } }; +/** + * Resolve named string fields of CLI-supplied JSON against the operator + * working directory. Each entry is a key path from the document root; only + * fields holding strings are resolved, everything else passes through + * untouched. Shared MCP contracts reject relative paths, so CLI workflows + * resolve operator-relative values before validation. + */ +export const resolveCliJsonPaths = ( + value: unknown, + paths: ReadonlyArray>, +): unknown => { + let resolved = value; + for (const keys of paths) resolved = resolveJsonPath(resolved, keys); + return resolved; +}; + +const resolveJsonPath = ( + value: unknown, + keys: ReadonlyArray, +): unknown => { + const [head, ...tail] = keys; + if (typeof value !== "object" || value === null || Array.isArray(value)) + return value; + const entries = Object.entries(value).map( + ([key, entry]: readonly [string, unknown]): readonly [string, unknown] => { + if (key !== head) return [key, entry]; + if (tail.length === 0) + return [key, typeof entry === "string" ? resolve(entry) : entry]; + return [key, resolveJsonPath(entry, tail)]; + }, + ); + return Object.fromEntries(entries); +}; + const cannotBeAnExistingFile = (cause: unknown): boolean => typeof cause === "object" && cause !== null && diff --git a/src/contracts/sessionLifecycleInputs.ts b/src/contracts/sessionLifecycleInputs.ts index 7a62d107e..8e3b7d698 100644 --- a/src/contracts/sessionLifecycleInputs.ts +++ b/src/contracts/sessionLifecycleInputs.ts @@ -15,9 +15,21 @@ const snapshotPathSchema = z "Absolute local filesystem path for the analysis snapshot; relative paths are rejected.", ); +/** Caller-supplied analysis target path; must name the target absolutely. */ +const binaryTargetPathSchema = z + .string() + .min(1) + .refine(isAbsoluteLocalPath, { + message: + "path must be an absolute local filesystem path (for example /tmp/fixture.bin or C:\\analysis\\fixture.bin)", + }) + .describe( + "Absolute local filesystem path for the analysis target; relative paths are rejected.", + ); + /** Input contract for opening a target with an optional staged snapshot. */ export const openBinaryInputSchema = z.object({ - path: z.string().min(1), + path: binaryTargetPathSchema, format: executableFormatHintSchema .optional() .describe( diff --git a/src/contracts/sessionPathInputs.test.ts b/src/contracts/sessionPathInputs.test.ts index 4d4755418..ac332e5d9 100644 --- a/src/contracts/sessionPathInputs.test.ts +++ b/src/contracts/sessionPathInputs.test.ts @@ -1,11 +1,16 @@ import { describe, expect, it } from "vitest"; +import { browserScenarioBrowserSchema } from "../domain/browserScenarioValues.js"; +import { firmwareInputSchemas } from "../domain/firmware/firmwareAnalysis.js"; +import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js"; import { managedArtifactInputSchema } from "./managed/managedToolContracts.js"; import { exportEvidenceBundleInputSchema } from "./sessionToolContracts.js"; import { closeBinaryInputSchema, openBinaryInputSchema, } from "./sessionLifecycleInputs.js"; +import { importEvidenceBundleInputSchema } from "./sessionToolSchemas.js"; +import { binarySessionInputSchema } from "./sessionStatusContract.js"; const openBinaryAbsolute = (snapshot_path: string) => openBinaryInputSchema.safeParse({ path: "/tmp/fixture", snapshot_path }); @@ -119,3 +124,151 @@ describe.runIf(process.platform === "win32")( }); }, ); + +describe("open binary target path requires an absolute local path", () => { + it("rejects a relative target path", () => { + expect( + openBinaryInputSchema.safeParse({ path: "fixtures/app.bin" }).success, + ).toBe(false); + }); + + it("accepts an absolute target path", () => { + expect( + openBinaryInputSchema.safeParse({ path: "/tmp/fixture.bin" }).success, + ).toBe(true); + }); +}); + +describe("evidence bundle import path requires an absolute local path", () => { + it("rejects a relative bundle path", () => { + expect( + importEvidenceBundleInputSchema.safeParse({ path: "evidence.json" }) + .success, + ).toBe(false); + }); + + it("accepts an absolute bundle path", () => { + expect( + importEvidenceBundleInputSchema.safeParse({ path: "/tmp/evidence.json" }) + .success, + ).toBe(true); + }); +}); + +describe("firmware input path requires an absolute local path", () => { + it("rejects a relative firmware path when inspecting regions", () => { + expect( + firmwareInputSchemas.inspect_firmware_regions.safeParse({ + path: "firmware.bin", + }).success, + ).toBe(false); + }); + + it("rejects a relative firmware path when extracting", () => { + expect( + firmwareInputSchemas.extract_firmware.safeParse({ + path: "firmware.bin", + output_directory: "/tmp/firmware-output", + }).success, + ).toBe(false); + }); + + it("accepts an absolute firmware path", () => { + expect( + firmwareInputSchemas.inspect_firmware_regions.safeParse({ + path: "/tmp/firmware.bin", + }).success, + ).toBe(true); + }); +}); + +describe("browser executable path requires an absolute local path", () => { + it("rejects a bare executable name", () => { + expect( + browserScenarioBrowserSchema.safeParse({ + mode: "launch", + executable_path: "chrome", + }).success, + ).toBe(false); + }); + + it("accepts an absolute executable path", () => { + expect( + browserScenarioBrowserSchema.safeParse({ + mode: "launch", + executable_path: "/opt/chromium/chrome", + }).success, + ).toBe(true); + }); +}); + +describe("electron observation paths require absolute local paths", () => { + const valid = { + executable_path: "/Applications/Electron.app/Contents/MacOS/Electron", + application_path: "/tmp/electron-app/main.js", + }; + + it("rejects a relative executable path", () => { + expect( + electronActiveObservationInputSchema.safeParse({ + ...valid, + executable_path: "Electron", + }).success, + ).toBe(false); + }); + + it("rejects a relative application path", () => { + expect( + electronActiveObservationInputSchema.safeParse({ + ...valid, + application_path: "main.js", + }).success, + ).toBe(false); + }); + + it("rejects a relative application root", () => { + expect( + electronActiveObservationInputSchema.safeParse({ + ...valid, + application_root: "app", + }).success, + ).toBe(false); + }); + + it("accepts absolute observation paths", () => { + expect( + electronActiveObservationInputSchema.safeParse({ + ...valid, + application_root: "/tmp/electron-app", + }).success, + ).toBe(true); + }); + + it("still accepts omitting the application root", () => { + expect(electronActiveObservationInputSchema.safeParse(valid).success).toBe( + true, + ); + }); +}); + +describe("expected server path requires an absolute local path", () => { + it("rejects a relative server path", () => { + expect( + binarySessionInputSchema.safeParse({ + expected_server_path: "dist/main.js", + }).success, + ).toBe(false); + }); + + it("accepts an absolute server path", () => { + expect( + binarySessionInputSchema.safeParse({ + expected_server_path: "/opt/rea/dist/main.js", + }).success, + ).toBe(true); + }); + + it("still accepts omitting the server path", () => { + expect(binarySessionInputSchema.safeParse({}).success).toBe(true); + }); +}); diff --git a/src/contracts/sessionStatusContract.ts b/src/contracts/sessionStatusContract.ts index c19c73a3b..b06807c17 100644 --- a/src/contracts/sessionStatusContract.ts +++ b/src/contracts/sessionStatusContract.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { isAbsoluteLocalPath } from "../domain/localPath.js"; + /** Optional expectations used to compare the live session with its caller. */ export const binarySessionInputSchema = z.object({ expected_package_version: z.string().min(1).optional(), @@ -7,5 +9,15 @@ export const binarySessionInputSchema = z.object({ .string() .regex(/^[a-f0-9]{64}$/u) .optional(), - expected_server_path: z.string().min(1).optional(), + expected_server_path: z + .string() + .min(1) + .refine(isAbsoluteLocalPath, { + message: + "expected_server_path must be an absolute local filesystem path (for example /opt/rea/dist/main.js)", + }) + .describe( + "Absolute local filesystem path expected for the running server; relative paths are rejected.", + ) + .optional(), }); diff --git a/src/contracts/sessionToolSchemas.ts b/src/contracts/sessionToolSchemas.ts index 39d444d0d..87afe287a 100644 --- a/src/contracts/sessionToolSchemas.ts +++ b/src/contracts/sessionToolSchemas.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { isAbsoluteLocalPath } from "../domain/localPath.js"; + import { artifactComparisonInputSchema } from "../domain/artifactComparison.js"; import { bundleComparisonInputSchema } from "../domain/bundleComparison.js"; import { callPathInputSchema } from "../domain/callPath.js"; @@ -36,7 +38,16 @@ export const addressContextInputSchema = z.strictObject({ /** Session-owned Evidence bundle import options. */ export const importEvidenceBundleInputSchema = z.strictObject({ - path: z.string().min(1), + path: z + .string() + .min(1) + .refine(isAbsoluteLocalPath, { + message: + "path must be an absolute local filesystem path (for example /tmp/evidence.json or C:\\rea\\evidence.json)", + }) + .describe( + "Absolute local filesystem path for the evidence bundle to import; relative paths are rejected.", + ), }); /** Evidence references for deterministic process comparison. */ diff --git a/src/contracts/toolContractExamples.ts b/src/contracts/toolContractExamples.ts index 6a47936e2..2135a8543 100644 --- a/src/contracts/toolContractExamples.ts +++ b/src/contracts/toolContractExamples.ts @@ -55,7 +55,7 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly< open_binary: { path: "/tmp/fixture" }, export_evidence_bundle: { path: "/tmp/evidence.json" }, inspect_address_context: { address: "0x1000" }, - import_evidence_bundle: { path: "evidence.json" }, + import_evidence_bundle: { path: "/tmp/evidence.json" }, capture_process_scenario: { executable: "/usr/bin/true", working_directory: "/tmp", diff --git a/src/domain/browserScenario.test.ts b/src/domain/browserScenario.test.ts index c69beeaef..a5527c4a1 100644 --- a/src/domain/browserScenario.test.ts +++ b/src/domain/browserScenario.test.ts @@ -56,7 +56,7 @@ it("does not require duplicate origin scope declarations", () => { const scenario = browserScenarioSchema.parse({ browser: { mode: "launch", - executable_path: "chrome", + executable_path: "/opt/chromium/chrome", }, start_url: { url: "https://app.example.test/" }, actions: [ diff --git a/src/domain/browserScenarioValues.ts b/src/domain/browserScenarioValues.ts index a2cf1b97c..6313c7ca9 100644 --- a/src/domain/browserScenarioValues.ts +++ b/src/domain/browserScenarioValues.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { isAbsoluteLocalPath } from "./localPath.js"; import { browserNetworkContentSelectionSchema } from "./browserNetworkEvidence.js"; import { @@ -11,7 +12,14 @@ export const scenarioIdentifierSchema = z .string() .regex(/^[A-Za-z][A-Za-z0-9._-]*$/u); -const browserExecutablePathSchema = z.string().trim().min(1); +const browserExecutablePathSchema = z + .string() + .trim() + .min(1) + .refine(isAbsoluteLocalPath, { + message: + "executable_path must be an absolute local filesystem path (for example /opt/chromium/chrome or C:\\chromium\\chrome.exe)", + }); const browserScenarioBaseUrlSchema = z .string() @@ -67,7 +75,9 @@ export type BrowserScenarioUrl = z.infer; export const browserScenarioBrowserSchema = z.discriminatedUnion("mode", [ z.strictObject({ mode: z.literal("launch"), - executable_path: browserExecutablePathSchema, + executable_path: browserExecutablePathSchema.describe( + "Absolute local filesystem path for the browser executable; relative paths are rejected.", + ), headless: z .boolean() .default(true) diff --git a/src/domain/firmware/firmwareAnalysis.ts b/src/domain/firmware/firmwareAnalysis.ts index 31bb12c3f..8a7f1f5de 100644 --- a/src/domain/firmware/firmwareAnalysis.ts +++ b/src/domain/firmware/firmwareAnalysis.ts @@ -1,11 +1,21 @@ import { z } from "zod"; import { digestSchema } from "../digests.js"; import { jsonValueSchema } from "../jsonValue.js"; +import { isAbsoluteLocalPath } from "../localPath.js"; const bytes = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER); const range = z.strictObject({ offset: bytes, length: bytes.positive() }); const target = { - path: z.string().min(1).describe("Local firmware file; never executed"), + path: z + .string() + .min(1) + .refine(isAbsoluteLocalPath, { + message: + "path must be an absolute local filesystem path (for example /tmp/firmware.bin or C:\\firmware\\firmware.bin)", + }) + .describe( + "Absolute local firmware file; never executed. Relative paths are rejected.", + ), }; /** Explicit firmware inspection and extraction intent, independent of engines. */ diff --git a/src/domain/javascript/electronActiveObservation.ts b/src/domain/javascript/electronActiveObservation.ts index 02d60b748..6c1391266 100644 --- a/src/domain/javascript/electronActiveObservation.ts +++ b/src/domain/javascript/electronActiveObservation.ts @@ -1,6 +1,11 @@ import { z } from "zod"; -const pathInputSchema = z.string().trim().min(1); +import { isAbsoluteLocalPath } from "../localPath.js"; + +const pathInputSchema = z.string().trim().min(1).refine(isAbsoluteLocalPath, { + message: + "Electron executable, application, and root paths must be absolute local filesystem paths (for example /Applications/Electron.app/Contents/MacOS/Electron)", +}); const absolutePathSchema = z .string() .min(1) @@ -60,9 +65,17 @@ const actionSchema = z.discriminatedUnion("kind", [ /** Input for one explicit, provider-owned Electron runtime experiment. */ export const electronActiveObservationInputSchema = z.strictObject({ - executable_path: pathInputSchema, - application_path: pathInputSchema, - application_root: pathInputSchema.optional(), + executable_path: pathInputSchema.describe( + "Absolute local filesystem path for the Electron executable; relative paths are rejected.", + ), + application_path: pathInputSchema.describe( + "Absolute local filesystem path for the Electron application; relative paths are rejected.", + ), + application_root: pathInputSchema + .optional() + .describe( + "Absolute local filesystem root for application-relative paths; omit to derive it from the application path. Relative paths are rejected.", + ), args: z.array(z.string()).default([]), actions: z.array(actionSchema).default([]), }); diff --git a/tests/acceptance/nonBinaryComposition.test.ts b/tests/acceptance/nonBinaryComposition.test.ts index a5229ebf0..f7c28fa41 100644 --- a/tests/acceptance/nonBinaryComposition.test.ts +++ b/tests/acceptance/nonBinaryComposition.test.ts @@ -89,6 +89,24 @@ cliTest.skipIf(process.platform !== "linux")( }, ); +cliTest.skipIf(process.platform !== "linux")( + "resolves relative firmware CLI paths against the operator working directory", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-firmware-cli-relative-"); + await writeFile(join(root, "input.bin"), "firmware fixture"); + const cliResult = await cli.run({ + arguments: ["inspect-firmware-regions", "input.bin", "--json"], + cwd: root, + environment: { REA_LOG_LEVEL: "silent" }, + timeoutMs: 10_000, + }); + // A relative path must pass input validation and reach the provider, + // which reports the missing firmware engine — not a validation error. + expect(cliResult.exitCode).toBe(1); + expect(cliResult.json).toMatchObject({ code: "capability_unavailable" }); + }, +); + cliTest( "preserves optional JavaScript recovery configuration through CLI and MCP", async ({ cli }) => { diff --git a/tests/boundary/filesystem/cliJsonInput.test.ts b/tests/boundary/filesystem/cliJsonInput.test.ts index 1a11ef459..41e101d47 100644 --- a/tests/boundary/filesystem/cliJsonInput.test.ts +++ b/tests/boundary/filesystem/cliJsonInput.test.ts @@ -1,9 +1,12 @@ import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { isAbsolute, join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; -import { parseCliJsonInput } from "../../../src/cliJsonInput.js"; +import { + parseCliJsonInput, + resolveCliJsonPaths, +} from "../../../src/cliJsonInput.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; describe("CLI JSON input", () => { @@ -51,3 +54,41 @@ describe("CLI JSON input", () => { }); }); }); + +describe("resolveCliJsonPaths", () => { + it("resolves named relative fields against the operator working directory", () => { + expect( + resolveCliJsonPaths({ executable_path: "chrome" }, [["executable_path"]]), + ).toEqual({ executable_path: resolve("chrome") }); + expect(isAbsolute(resolve("chrome"))).toBe(true); + }); + + it("leaves absolute fields unchanged", () => { + const value = { path: "/tmp/evidence.json" }; + expect(resolveCliJsonPaths(value, [["path"]])).toEqual(value); + }); + + it("resolves nested key paths and preserves sibling fields", () => { + expect( + resolveCliJsonPaths( + { + browser: { mode: "launch", executable_path: "chrome" }, + actions: [], + }, + [["browser", "executable_path"]], + ), + ).toEqual({ + browser: { mode: "launch", executable_path: resolve("chrome") }, + actions: [], + }); + }); + + it("passes through non-objects, missing keys, and non-string fields", () => { + expect(resolveCliJsonPaths(null, [["path"]])).toBe(null); + expect(resolveCliJsonPaths("text", [["path"]])).toBe("text"); + expect(resolveCliJsonPaths({ other: 1 }, [["path"]])).toEqual({ + other: 1, + }); + expect(resolveCliJsonPaths({ path: 42 }, [["path"]])).toEqual({ path: 42 }); + }); +});