mirror of
https://github.com/openclaw/lobster.git
synced 2026-08-14 00:48:09 +00:00
* fix: workflow run crashes when a step exits before reading its stdin A step command that exits during its startup (for example a shell script failing a preamble check under set -e) before draining a large piped stdin leaves the engine's pending stdin write to fail with EPIPE. The stdin socket had no 'error' listener, so Node raised an unhandled 'error' event and the entire lobster process crashed -- losing the approval gate, the resume token, and the step's real exit code and stderr. Ignore stdin write errors in both shell-step and stdlib exec spawns: the close handler already reports the true failure. Regression test drives a 300KB stdout through a fast-exiting step and asserts the run rejects with 'workflow command failed (1)' instead of crashing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * simplify: condense EPIPE inline comments to essential context The 5-line comment blocks explaining EPIPE behavior were excessive for 1-line handlers. The test comment describing the 300KB mechanism was similarly verbose. Trim each to the non-obvious why only. * simplify: remove EPIPE inline comments The child.stdin.on('error', () => {}) pattern is self-explanatory to Node.js developers; the rationale is fully documented in the PR body. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { promises as fsp } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
import { createDefaultRegistry } from "../src/commands/registry.js";
|
|
import { runWorkflowFile } from "../src/workflows/file.js";
|
|
|
|
async function runWorkflow(workflow: unknown) {
|
|
const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "lobster-stdin-epipe-"));
|
|
const stateDir = path.join(tmpDir, "state");
|
|
const filePath = path.join(tmpDir, "workflow.lobster");
|
|
await fsp.writeFile(filePath, JSON.stringify(workflow, null, 2), "utf8");
|
|
|
|
return runWorkflowFile({
|
|
filePath,
|
|
ctx: {
|
|
stdin: process.stdin,
|
|
stdout: process.stdout,
|
|
stderr: process.stderr,
|
|
env: { ...process.env, LOBSTER_STATE_DIR: stateDir },
|
|
mode: "tool",
|
|
registry: createDefaultRegistry(),
|
|
},
|
|
});
|
|
}
|
|
|
|
test("a step that exits before draining large stdin fails cleanly instead of crashing", async () => {
|
|
// 300KB exceeds the OS pipe buffer (64KB on Linux), so the write to the
|
|
// second step's stdin is still pending when that step exits without reading.
|
|
// Before the EPIPE guard, this crashed the engine instead of reporting the
|
|
// step failure.
|
|
await assert.rejects(
|
|
() =>
|
|
runWorkflow({
|
|
steps: [
|
|
{
|
|
id: "big",
|
|
command: "node -e \"process.stdout.write('x'.repeat(300000))\"",
|
|
},
|
|
{
|
|
id: "fast_fail",
|
|
command: 'node -e "process.exit(1)"',
|
|
stdin: "$big.stdout",
|
|
},
|
|
],
|
|
}),
|
|
/workflow command failed \(1\)/,
|
|
);
|
|
});
|