mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:50:43 +00:00
fix: preserve mantis recordings on record errors (#78768)
This commit is contained in:
@@ -97,6 +97,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed.
|
||||
- Plugins/loader: preserve real compiled plugin module evaluation errors on the native fast path instead of treating every thrown `.js` module as a source-transform fallback miss. Thanks @vincentkoc.
|
||||
- Plugin SDK/fs-safe: expose reusable atomic replacement, sibling-temp writes, and cross-device move fallback helpers through `plugin-sdk/security-runtime`, and move OpenClaw's duplicated safe filesystem write paths onto the shared `@openclaw/fs-safe` package.
|
||||
- Plugin SDK/fs-safe: route browser, media, channel, and QA external output producers through staged fs-safe writes before final publication. (#78768)
|
||||
- Plugin SDK/fs-safe: rename the public temp workspace helpers to `tempWorkspace`, `withTempWorkspace`, `tempWorkspaceSync`, and `withTempWorkspaceSync`, matching the cleaner `@openclaw/fs-safe` API before the package is published.
|
||||
- Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
- Providers/OpenRouter: expand app-attribution categories so OpenClaw advertises coding, programming, writing, chat, and personal-agent usage on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
|
||||
@@ -199,6 +199,93 @@ describe("mantis visual task runtime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the video artifact when recording fails after writing output", async () => {
|
||||
const commands: { args: readonly string[]; command: string }[] = [];
|
||||
let stagedVideoPath = "";
|
||||
const runner = vi.fn(async (command: string, args: readonly string[]) => {
|
||||
commands.push({ command, args });
|
||||
if (command === "/tmp/crabbox" && args[0] === "warmup") {
|
||||
return { stdout: "ready lease cbx_abc123\n", stderr: "" };
|
||||
}
|
||||
if (command === "/tmp/crabbox" && args[0] === "inspect") {
|
||||
return {
|
||||
stdout: `${JSON.stringify({
|
||||
id: "cbx_abc123",
|
||||
provider: "hetzner",
|
||||
slug: "brisk-mantis",
|
||||
state: "active",
|
||||
})}\n`,
|
||||
stderr: "",
|
||||
};
|
||||
}
|
||||
if (command === "/tmp/crabbox" && args[0] === "record") {
|
||||
const outputPath = args[args.indexOf("--output") + 1];
|
||||
const outputDir = args[args.indexOf("--output-dir") + 1];
|
||||
stagedVideoPath = outputPath;
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
await fs.writeFile(outputPath, "mp4");
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
await fs.writeFile(path.join(outputDir, "visual-task.png"), "png");
|
||||
await fs.writeFile(
|
||||
path.join(outputDir, "mantis-visual-task-driver-result.json"),
|
||||
`${JSON.stringify({
|
||||
browserUrl: "https://example.net",
|
||||
finishedAt: "2026-05-04T12:00:05.000Z",
|
||||
matched: true,
|
||||
outputDir,
|
||||
screenshotPath: path.join(outputDir, "visual-task.png"),
|
||||
startedAt: "2026-05-04T12:00:01.000Z",
|
||||
status: "pass",
|
||||
vision: {
|
||||
mode: "metadata",
|
||||
timeoutMs: 120000,
|
||||
},
|
||||
})}\n`,
|
||||
);
|
||||
throw new Error("crabbox record failed after writing video");
|
||||
}
|
||||
return { stdout: "", stderr: "" };
|
||||
});
|
||||
|
||||
const result = await runMantisVisualTask({
|
||||
commandRunner: runner,
|
||||
crabboxBin: "/tmp/crabbox",
|
||||
env: { PATH: process.env.PATH },
|
||||
now: () => new Date("2026-05-04T12:00:00.000Z"),
|
||||
outputDir: ".artifacts/qa-e2e/mantis/visual-task-recording-preserved",
|
||||
repoRoot,
|
||||
settleMs: 0,
|
||||
visionMode: "metadata",
|
||||
});
|
||||
|
||||
expect(result.status).toBe("fail");
|
||||
expect(result.videoPath).toBe(
|
||||
path.join(
|
||||
repoRoot,
|
||||
".artifacts/qa-e2e/mantis/visual-task-recording-preserved/visual-task.mp4",
|
||||
),
|
||||
);
|
||||
await expect(fs.readFile(result.videoPath ?? "", "utf8")).resolves.toBe("mp4");
|
||||
await expect(fs.stat(stagedVideoPath)).rejects.toThrow();
|
||||
const summary = JSON.parse(await fs.readFile(result.summaryPath, "utf8")) as {
|
||||
artifacts?: { videoPath?: string };
|
||||
error?: string;
|
||||
recording?: { error?: string; required: boolean };
|
||||
status: string;
|
||||
};
|
||||
expect(summary).toMatchObject({
|
||||
artifacts: {
|
||||
videoPath: result.videoPath,
|
||||
},
|
||||
error: "crabbox record failed after writing video",
|
||||
recording: {
|
||||
error: "crabbox record failed after writing video",
|
||||
required: true,
|
||||
},
|
||||
status: "fail",
|
||||
});
|
||||
});
|
||||
|
||||
it("drives a lease, screenshots it, and verifies image-describe text", async () => {
|
||||
const commands: { args: readonly string[]; command: string }[] = [];
|
||||
const runner = vi.fn(async (command: string, args: readonly string[]) => {
|
||||
|
||||
@@ -291,22 +291,36 @@ async function runCommandWithExternalOutput(params: {
|
||||
command: string;
|
||||
cwd: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
preserveOutputOnError?: (params: { error: unknown; tempPath: string }) => Promise<boolean>;
|
||||
runner: CommandRunner;
|
||||
stdio?: "inherit" | "pipe";
|
||||
}) {
|
||||
return await writeExternalFileWithinRoot({
|
||||
}): Promise<void> {
|
||||
let deferredError: unknown;
|
||||
await writeExternalFileWithinRoot({
|
||||
rootDir: path.dirname(params.outputPath),
|
||||
path: path.basename(params.outputPath),
|
||||
write: async (tempPath) =>
|
||||
await runCommand({
|
||||
command: params.command,
|
||||
args: params.buildArgs(tempPath),
|
||||
cwd: params.cwd,
|
||||
env: params.env,
|
||||
runner: params.runner,
|
||||
stdio: params.stdio,
|
||||
}),
|
||||
write: async (tempPath) => {
|
||||
try {
|
||||
await runCommand({
|
||||
command: params.command,
|
||||
args: params.buildArgs(tempPath),
|
||||
cwd: params.cwd,
|
||||
env: params.env,
|
||||
runner: params.runner,
|
||||
stdio: params.stdio,
|
||||
});
|
||||
} catch (error) {
|
||||
if (await params.preserveOutputOnError?.({ error, tempPath })) {
|
||||
deferredError = error;
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
if (deferredError) {
|
||||
throw deferredError;
|
||||
}
|
||||
}
|
||||
|
||||
async function warmupCrabbox(params: {
|
||||
@@ -840,6 +854,8 @@ export async function runMantisVisualTask(
|
||||
],
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
preserveOutputOnError: async ({ tempPath }) =>
|
||||
(await pathExists(driverResultPath)) && (await nonEmptyFileExists(tempPath)),
|
||||
runner,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user