fix(cli): await browser scenario cleanup on interrupts

Translate browser scenario CLI interrupts into cancellation and await owned provider cleanup. Preserve authenticated observations and cancelled steps with conventional exit codes 130/143.

Refs #1771.
This commit is contained in:
Ysopwnable authored and GitHub committed 2026-10-11 17:33:57 +08:00
1 parent 0bdc7a40ac
commit aaeadf2d4c
3 files changed
+258 -49

No files matched your search

+55 -49
View File
@@ -2,6 +2,7 @@ import { Cli } from "incur";
import { z } from "zod";
import { captureBrowserScenario } from "./application/BrowserScenarioCaptureService.js";
import { withCommandCancellation } from "./cli/commandCancellation.js";
import { CLI_COMMANDS } from "./cliCommandNames.js";
import { parseCliJsonInput, resolveCliJsonPaths } from "./cliJsonInput.js";
import { logCliCommand } from "./cliLogging.js";
@@ -31,58 +32,63 @@ export const registerBrowserScenarioCommands = (
.describe("Inline browser scenario JSON or JSON file path"),
}),
run: ({ args }) =>
logCliCommand(logger, CLI_COMMANDS.captureBrowserScenario, async () => {
const input = await parseCliJsonInput(args.inputJson, OPERATION);
if (!input.ok) return input.error;
const scenario = browserScenarioSchema.safeParse(
resolveCliJsonPaths(input.value, [["browser", "executable_path"]]),
);
if (!scenario.success)
return cliError(
new AnalysisInputError(
OPERATION,
{ cause: scenario.error },
projectInputIssues(scenario.error.issues, input.value),
),
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.captureBrowserScenario, async () => {
const input = await parseCliJsonInput(args.inputJson, OPERATION);
if (!input.ok) return input.error;
const scenario = browserScenarioSchema.safeParse(
resolveCliJsonPaths(input.value, [["browser", "executable_path"]]),
);
// Load Playwright only for this command, not during CLI startup.
const { createBrowserScenarioProvider } =
await import("./composition/browserScenario.js");
const provider = createBrowserScenarioProvider(environment);
let result:
| Awaited<ReturnType<typeof captureBrowserScenario>>
| undefined;
try {
result = await captureBrowserScenario(provider, scenario.data);
return result.ok ? result.value : cliError(result.error);
} finally {
if (!scenario.success)
return cliError(
new AnalysisInputError(
OPERATION,
{ cause: scenario.error },
projectInputIssues(scenario.error.issues, input.value),
),
);
// Load Playwright only for this command, not during CLI startup.
const { createBrowserScenarioProvider } =
await import("./composition/browserScenario.js");
const provider = createBrowserScenarioProvider(environment);
let result:
| Awaited<ReturnType<typeof captureBrowserScenario>>
| undefined;
try {
await provider?.close?.();
} catch (cause: unknown) {
// Failed captures already retain their primary error and partial
// observations. A cleanup retry must not replace that outcome.
if (result?.ok)
throw new BrowserObservationError(OPERATION, "cleanup_failed", {
cause,
cleanup:
cause instanceof AnalysisError && cause.cleanup !== undefined
? cause.cleanup
: {
reason:
cause instanceof Error
? cause.message
: String(cause),
resources:
cause instanceof AnalysisError &&
cause.cleanupResources.length > 0
? cause.cleanupResources
: ["browser_transport"],
},
partialObservation: result.value,
});
result = await captureBrowserScenario(provider, scenario.data, {
signal,
});
return result.ok ? result.value : cliError(result.error);
} finally {
try {
await provider?.close?.();
} catch (cause: unknown) {
// Failed captures already retain their primary error and partial
// observations. A cleanup retry must not replace that outcome.
if (result?.ok)
throw new BrowserObservationError(OPERATION, "cleanup_failed", {
cause,
cleanup:
cause instanceof AnalysisError &&
cause.cleanup !== undefined
? cause.cleanup
: {
reason:
cause instanceof Error
? cause.message
: String(cause),
resources:
cause instanceof AnalysisError &&
cause.cleanupResources.length > 0
? cause.cleanupResources
: ["browser_transport"],
},
partialObservation: result.value,
});
}
}
}
}),
}),
),
});
};