fix(cli): preserve caller-selected JSON paths
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
This commit is contained in:
1 parent
690fc51854
commit
1bfa60ac20
8 files changed
+422
-13
No files matched your search
@@ -3,7 +3,7 @@ import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { mkdir, writeFile, rm, access } from "node:fs/promises";
|
||||
import { mkdir, writeFile, rm, access, symlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import type {
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
} from "playwright-core";
|
||||
|
||||
import { browserScenarioSchema } from "../domain/browserScenario.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import {
|
||||
attachedScenarioLeaseKey,
|
||||
failBrowserScenarioOperation,
|
||||
@@ -91,6 +92,49 @@ it("passes the selected environment to the actual browser launch boundary", asyn
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"passes the caller-selected symlink-parent executable to the launch boundary",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-browser-caller-path-");
|
||||
try {
|
||||
await mkdir(join(root, "real/inner"), { recursive: true });
|
||||
await symlink(join(root, "real/inner"), join(root, "link"));
|
||||
const selected = absoluteCallerPath("link/../browser", { cwd: root });
|
||||
let launchedExecutable: string | undefined;
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: selected },
|
||||
start_url: { url: "https://example.test" },
|
||||
actions: [
|
||||
{ step_id: "settle", action: "wait_for_timeout", duration_ms: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
await expect(
|
||||
openPlaywrightScenarioBrowser(
|
||||
scenario,
|
||||
{},
|
||||
{
|
||||
launcher: {
|
||||
connectOverCDP: async () => {
|
||||
throw new Error("Unexpected connection");
|
||||
},
|
||||
launchPersistentContext: async (_profile, options) => {
|
||||
launchedExecutable = options?.executablePath;
|
||||
throw new Error("fake launch boundary reached");
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("fake launch boundary reached");
|
||||
|
||||
expect(launchedExecutable).toBe(selected);
|
||||
expect(selected).toBe(`${root}/link/../browser`);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps Termux execution support when launching Chromium on Android", async () => {
|
||||
Object.defineProperty(process, "platform", { value: "android" });
|
||||
vi.stubEnv("PREFIX", "/data/data/com.termux/files/usr");
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "../artifacts/AnalysisCleanupLease.js";
|
||||
import { chmod, mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type {
|
||||
Browser,
|
||||
@@ -717,7 +717,7 @@ export const openPlaywrightScenarioBrowser = async (
|
||||
try {
|
||||
await chmod(profilePath, 0o700);
|
||||
context = await launcher.launchPersistentContext(profilePath, {
|
||||
executablePath: resolve(scenario.browser.executable_path),
|
||||
executablePath: scenario.browser.executable_path,
|
||||
headless: scenario.browser.headless,
|
||||
acceptDownloads: false,
|
||||
viewport: {
|
||||
|
||||
@@ -24,16 +24,22 @@ import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnal
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import type { ElectronActiveObservationPort } from "../application/javascript/ElectronActiveObservationPort.js";
|
||||
|
||||
/** Register CLI equivalents of the Electron MCP tools. */
|
||||
export const registerElectronCommands = (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
logger: Logger,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
options: {
|
||||
readonly createActiveProvider?: (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
) => ElectronActiveObservationPort;
|
||||
} = {},
|
||||
): void => {
|
||||
registerElectronObservationCommands(cli, logger);
|
||||
registerElectronFuseCommand(cli, logger);
|
||||
registerElectronActiveCommand(cli, logger, environment);
|
||||
registerElectronActiveCommand(cli, logger, environment, options);
|
||||
registerJavaScriptApplicationCommand(cli, logger);
|
||||
registerJavaScriptRuntimeReconciliationCommand(cli, logger);
|
||||
};
|
||||
@@ -86,6 +92,11 @@ const registerElectronActiveCommand = (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
logger: Logger,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
options: {
|
||||
readonly createActiveProvider?: (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
) => ElectronActiveObservationPort;
|
||||
},
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.captureElectronScenario, {
|
||||
description: "Run one owned Electron scenario with cancellable actions",
|
||||
@@ -122,9 +133,13 @@ const registerElectronActiveCommand = (
|
||||
);
|
||||
const { captureElectronScenario } =
|
||||
await import("../application/javascript/ElectronActiveObservationService.js");
|
||||
const { createElectronScenarioProvider } =
|
||||
await import("../composition/electronScenario.js");
|
||||
const provider = createElectronScenarioProvider(environment);
|
||||
const provider =
|
||||
options.createActiveProvider === undefined
|
||||
? await import("../composition/electronScenario.js").then(
|
||||
({ createElectronScenarioProvider }) =>
|
||||
createElectronScenarioProvider(environment),
|
||||
)
|
||||
: options.createActiveProvider(environment);
|
||||
const result = await finishAnalysisCommand(
|
||||
() => captureElectronScenario(provider, parsed.data, { signal }),
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
|
||||
import { BrowserObservationError } from "./domain/browserObservationError.js";
|
||||
import { analysisInputErrorFromIssues } from "./domain/inputIssueProjection.js";
|
||||
import { browserScenarioSchema } from "./domain/browserScenario.js";
|
||||
import type { BrowserScenarioCapturePort } from "./application/BrowserScenarioCapturePort.js";
|
||||
import type { JsonValue } from "./domain/jsonValue.js";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
@@ -21,6 +22,11 @@ export const registerBrowserScenarioCommands = (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
logger: Logger,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
options: {
|
||||
readonly createProvider?: (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
) => BrowserScenarioCapturePort;
|
||||
} = {},
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.captureBrowserScenario, {
|
||||
description:
|
||||
@@ -48,9 +54,13 @@ export const registerBrowserScenarioCommands = (
|
||||
),
|
||||
);
|
||||
// Load Playwright only for this command, not during CLI startup.
|
||||
const { createBrowserScenarioProvider } =
|
||||
await import("./composition/browserScenario.js");
|
||||
const provider = createBrowserScenarioProvider(environment);
|
||||
const provider =
|
||||
options.createProvider === undefined
|
||||
? await import("./composition/browserScenario.js").then(
|
||||
({ createBrowserScenarioProvider }) =>
|
||||
createBrowserScenarioProvider(environment),
|
||||
)
|
||||
: options.createProvider(environment);
|
||||
let result:
|
||||
| Awaited<ReturnType<typeof captureBrowserScenario>>
|
||||
| undefined;
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { resolve } from "node:path";
|
||||
import { absoluteCallerPath } from "./filesystem/callerPath.js";
|
||||
|
||||
import { sealOwnedJsonInput } from "./application/OwnedJsonInput.js";
|
||||
import { readJsonInputFile } from "./application/JsonInputFile.js";
|
||||
@@ -141,7 +141,7 @@ const resolveJsonPath = (
|
||||
typeof entry === "string" &&
|
||||
entry.trim().length > 0 &&
|
||||
!URL_REFERENCE.test(entry)
|
||||
? resolve(entry)
|
||||
? absoluteCallerPath(entry)
|
||||
: entry,
|
||||
];
|
||||
return [key, resolveJsonPath(entry, tail)];
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { resolve } from "node:path";
|
||||
import { mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
@@ -57,6 +59,50 @@ describe("CLI JSON path normalization", () => {
|
||||
expect(input.browser.executable_path).toBe(" browser ");
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"keeps POSIX symlink-parent traversal for each selected JSON path",
|
||||
async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-json-caller-path-"));
|
||||
const originalCwd = process.cwd();
|
||||
try {
|
||||
await mkdir(join(root, "real/inner"), { recursive: true });
|
||||
await symlink(join(root, "real/inner"), join(root, "link"));
|
||||
process.chdir(root);
|
||||
const electronInput = {
|
||||
executable_path: "link/../tool",
|
||||
application_path: "link/../app",
|
||||
application_root: "link/../root",
|
||||
};
|
||||
const normalizedElectron = resolveCliJsonPaths(electronInput, paths);
|
||||
expect(normalizedElectron).toEqual({
|
||||
executable_path: `${root}/link/../tool`,
|
||||
application_path: `${root}/link/../app`,
|
||||
application_root: `${root}/link/../root`,
|
||||
});
|
||||
expect(electronInput.executable_path).toBe("link/../tool");
|
||||
expect(
|
||||
electronActiveObservationInputSchema.safeParse(normalizedElectron)
|
||||
.success,
|
||||
).toBe(true);
|
||||
const browserInput = {
|
||||
browser: { executable_path: "link/../browser", label: "keep" },
|
||||
};
|
||||
expect(
|
||||
resolveCliJsonPaths(browserInput, [["browser", "executable_path"]]),
|
||||
).toEqual({
|
||||
browser: {
|
||||
executable_path: `${root}/link/../browser`,
|
||||
label: "keep",
|
||||
},
|
||||
});
|
||||
expect(browserInput.browser.executable_path).toBe("link/../browser");
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"file:///opt/electron",
|
||||
"FILE:electron",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { mkdir, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { toolContract } from "../../src/contracts/toolContracts.js";
|
||||
import { parseEvidence } from "../../src/domain/evidence.js";
|
||||
import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js";
|
||||
import {
|
||||
analysisViewJavaScriptEvidence,
|
||||
analysisViewLayoutEvidence,
|
||||
} from "../fixtures/analysisView.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest
|
||||
.skipIf(process.platform === "win32")
|
||||
.for(["relative", "absolute"] as const)(
|
||||
"reads selected analysis-view Evidence through a %s caller path containing a symlink parent",
|
||||
async (pathKind, { cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-analysis-view-caller-path-",
|
||||
);
|
||||
await mkdir(join(root, "real", "inner"), { recursive: true });
|
||||
await symlink("real/inner", join(root, "link"));
|
||||
|
||||
const selected = analysisViewJavaScriptEvidence();
|
||||
const decoy = analysisViewLayoutEvidence();
|
||||
expect(selected.evidence_id).not.toBe(decoy.evidence_id);
|
||||
await writeFile(
|
||||
join(root, "real", "evidence.json"),
|
||||
JSON.stringify(selected),
|
||||
);
|
||||
await writeFile(join(root, "evidence.json"), JSON.stringify(decoy));
|
||||
|
||||
const sourcePath =
|
||||
pathKind === "relative"
|
||||
? "link/../evidence.json"
|
||||
: `${root}/link/../evidence.json`;
|
||||
const queryPath = join(root, "query.json");
|
||||
await writeFile(
|
||||
queryPath,
|
||||
JSON.stringify({
|
||||
source: { kind: "evidence-file", path: sourcePath },
|
||||
view: { kind: "summary" },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-analysis-view", queryPath, "--json"],
|
||||
cwd: root,
|
||||
});
|
||||
expect(result.exitCode, result.stdout + result.stderr).toBe(0);
|
||||
|
||||
const contractResult = toolContract(
|
||||
"inspect_analysis_view",
|
||||
).outputSchema.parse(result.json);
|
||||
const output = parseEvidence(contractResult);
|
||||
const view = analysisViewResultSchema.parse(output.normalized_result);
|
||||
expect(view.kind).toBe("summary");
|
||||
expect(view.parent_evidence_id).toBe(selected.evidence_id);
|
||||
expect(output.evidence_links).toEqual([selected.evidence_id]);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,232 @@
|
||||
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 });
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user