Keep POSIX symlink-parent traversal at the shared JSON input boundary and forward the selected browser executable without a second lexical normalization. Verify Electron, browser, and Evidence-file consumers through registered commands and the built CLI. Refs #5103
233 lines
7.7 KiB
TypeScript
233 lines
7.7 KiB
TypeScript
import {
|
|
chmod,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
realpath,
|
|
rm,
|
|
symlink,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { Cli } from "incur";
|
|
import pino from "pino";
|
|
import { expect, it } from "vitest";
|
|
|
|
import type { BrowserScenarioCapturePort } from "../../../src/application/BrowserScenarioCapturePort.js";
|
|
import type { ElectronActiveObservationPort } from "../../../src/application/javascript/ElectronActiveObservationPort.js";
|
|
import { registerBrowserScenarioCommands } from "../../../src/cliBrowserScenarioCommands.js";
|
|
import { registerAnalysisViewCommands } from "../../../src/cli/analysisViewCommands.js";
|
|
import { registerElectronCommands } from "../../../src/cli/electronCommands.js";
|
|
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { err } from "../../../src/domain/result.js";
|
|
import {
|
|
analysisViewJavaScriptEvidence,
|
|
analysisViewLayoutEvidence,
|
|
} from "../../fixtures/analysisView.js";
|
|
|
|
const providerIdentity = {
|
|
id: "test.path-boundary",
|
|
name: "Path boundary test provider",
|
|
version: null,
|
|
} as const;
|
|
|
|
const serveJson = async (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
command: string,
|
|
input: unknown,
|
|
): Promise<unknown> => {
|
|
let stdout = "";
|
|
await cli.serve([command, JSON.stringify(input), "--json"], {
|
|
env: {},
|
|
stdout: (text) => {
|
|
stdout += text;
|
|
},
|
|
exit: () => undefined,
|
|
});
|
|
return JSON.parse(stdout) as unknown;
|
|
};
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"keeps caller-selected JSON paths through the public CLI and provider boundaries",
|
|
async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-cli-json-path-"));
|
|
const originalCwd = process.cwd();
|
|
const originalExitCode = process.exitCode;
|
|
try {
|
|
await mkdir(join(root, "real/inner"), { recursive: true });
|
|
await mkdir(join(root, "real/app"), { recursive: true });
|
|
await mkdir(join(root, "real/root"), { recursive: true });
|
|
await mkdir(join(root, "app"), { recursive: true });
|
|
await mkdir(join(root, "root"), { recursive: true });
|
|
await writeFile(join(root, "real/tool"), "selected executable");
|
|
await writeFile(join(root, "tool"), "lexical sibling");
|
|
await chmod(join(root, "real/tool"), 0o700);
|
|
await chmod(join(root, "tool"), 0o700);
|
|
await symlink(join(root, "real/inner"), join(root, "link"));
|
|
process.chdir(root);
|
|
|
|
const electronInputs: string[][] = [];
|
|
const electronProvider: ElectronActiveObservationPort = {
|
|
identity: () => providerIdentity,
|
|
capture: async (input) => {
|
|
electronInputs.push([
|
|
input.executable_path,
|
|
input.application_path,
|
|
input.application_root,
|
|
]);
|
|
return err(
|
|
new AnalysisCapabilityUnavailableError(
|
|
providerIdentity.id,
|
|
"capture_electron_scenario",
|
|
"intentional inert test provider",
|
|
),
|
|
);
|
|
},
|
|
close: async () => undefined,
|
|
};
|
|
const electronCli = Cli.create("rea-path-electron-test", {
|
|
sync: false,
|
|
});
|
|
registerElectronCommands(
|
|
electronCli,
|
|
pino({ level: "silent" }),
|
|
{},
|
|
{
|
|
createActiveProvider: () => electronProvider,
|
|
},
|
|
);
|
|
for (const path of ["link/../", `${root}/link/../`]) {
|
|
const output = await serveJson(
|
|
electronCli,
|
|
"capture-electron-scenario",
|
|
{
|
|
executable_path: `${path}tool`,
|
|
application_path: `${path}app`,
|
|
application_root: `${path}root`,
|
|
actions: [],
|
|
},
|
|
);
|
|
expect(output).toMatchObject({ code: "capability_unavailable" });
|
|
}
|
|
const expectedElectronPaths = [
|
|
await realpath(join(root, "real/tool")),
|
|
await realpath(join(root, "real/app")),
|
|
await realpath(join(root, "real/root")),
|
|
];
|
|
expect(electronInputs).toEqual([
|
|
expectedElectronPaths,
|
|
expectedElectronPaths,
|
|
]);
|
|
|
|
const browserInputs: string[] = [];
|
|
const browserProvider: BrowserScenarioCapturePort = {
|
|
identity: () => providerIdentity,
|
|
captureScenario: async (scenario) => {
|
|
if (scenario.browser.mode === "launch")
|
|
browserInputs.push(scenario.browser.executable_path);
|
|
return err(
|
|
new AnalysisCapabilityUnavailableError(
|
|
providerIdentity.id,
|
|
"capture_browser_scenario",
|
|
"intentional inert test provider",
|
|
),
|
|
);
|
|
},
|
|
close: async () => undefined,
|
|
};
|
|
const browserCli = Cli.create("rea-path-browser-test", {
|
|
sync: false,
|
|
});
|
|
registerBrowserScenarioCommands(
|
|
browserCli,
|
|
pino({ level: "silent" }),
|
|
{},
|
|
{ createProvider: () => browserProvider },
|
|
);
|
|
for (const executablePath of [
|
|
"link/../browser",
|
|
`${root}/link/../browser`,
|
|
]) {
|
|
const output = await serveJson(browserCli, "capture-browser-scenario", {
|
|
browser: { mode: "launch", executable_path: executablePath },
|
|
start_url: { url: "https://example.test" },
|
|
actions: [
|
|
{
|
|
step_id: "settle",
|
|
action: "wait_for_timeout",
|
|
duration_ms: 1,
|
|
},
|
|
],
|
|
});
|
|
expect(output).toMatchObject({ code: "capability_unavailable" });
|
|
}
|
|
expect(browserInputs).toEqual([
|
|
`${root}/link/../browser`,
|
|
`${root}/link/../browser`,
|
|
]);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
process.exitCode = originalExitCode;
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"reads the caller-selected Evidence file through the registered CLI",
|
|
async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-cli-evidence-path-"));
|
|
const originalCwd = process.cwd();
|
|
const originalExitCode = process.exitCode;
|
|
try {
|
|
await mkdir(join(root, "real/inner"), { recursive: true });
|
|
await symlink(join(root, "real/inner"), join(root, "link"));
|
|
process.chdir(root);
|
|
const selectedEvidence = analysisViewLayoutEvidence();
|
|
const siblingEvidence = analysisViewJavaScriptEvidence();
|
|
await mkdir(join(root, "real"), { recursive: true });
|
|
await writeFile(
|
|
join(root, "real/evidence.json"),
|
|
JSON.stringify(selectedEvidence),
|
|
);
|
|
await writeFile(
|
|
join(root, "evidence.json"),
|
|
JSON.stringify(siblingEvidence),
|
|
);
|
|
expect(
|
|
parseEvidence(
|
|
JSON.parse(await readFile(`${root}/link/../evidence.json`, "utf8")),
|
|
).evidence_id,
|
|
).toBe(selectedEvidence.evidence_id);
|
|
const analysisViewCli = Cli.create("rea-path-evidence-test", {
|
|
sync: false,
|
|
});
|
|
registerAnalysisViewCommands(analysisViewCli, pino({ level: "silent" }));
|
|
for (const path of [
|
|
"link/../evidence.json",
|
|
`${root}/link/../evidence.json`,
|
|
]) {
|
|
const output = parseEvidence(
|
|
await serveJson(analysisViewCli, "inspect-analysis-view", {
|
|
source: { kind: "evidence-file", path },
|
|
view: { kind: "summary" },
|
|
}),
|
|
);
|
|
expect(output.parameters.parent_evidence_id, path).toBe(
|
|
selectedEvidence.evidence_id,
|
|
);
|
|
expect(output.evidence_links, path).toEqual([
|
|
selectedEvidence.evidence_id,
|
|
]);
|
|
}
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
process.exitCode = originalExitCode;
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
},
|
|
);
|