test(agents): cover exec non-zero exits

This commit is contained in:
Sebastian
2026-02-16 23:12:06 -05:00
parent 4b40bdb98e
commit f8adfcf60e
2 changed files with 31 additions and 0 deletions

View File

@@ -59,6 +59,7 @@ Docs: https://docs.openclaw.ai
- Auto-reply/Subagents: propagate group context (`groupId`, `groupChannel`, `space`) when spawning via `/subagents spawn`, matching tool-triggered subagent spawn behavior.
- Subagents: cap announce retry loops with max attempts and expiry to prevent infinite retry spam after deferred announces. (#18444)
- Agents/Tools/exec: add a preflight guard that detects likely shell env var injection (e.g. `$DM_JSON`, `$TMPDIR`) in Python/Node scripts before execution, preventing recurring cron failures and wasted tokens when models emit mixed shell+language source. (#12836)
- Agents/Tools/exec: treat normal non-zero exit codes as completed and append the exit code to tool output to avoid false tool-failure warnings. (#18425)
- Agents/Tools: make loop detection progress-aware and phased by hard-blocking known `process(action=poll|log)` no-progress loops, warning on generic identical-call repeats, warning + no-progress-blocking ping-pong alternation loops (10/20), coalescing repeated warning spam into threshold buckets (including canonical ping-pong pairs), adding a global circuit breaker at 30 no-progress repeats, and emitting structured diagnostic `tool.loop` warning/error events for loop actions. (#16808) Thanks @akramcodez and @beca-oc.
- Agents/Tools: scope the `message` tool schema to the active channel so Telegram uses `buttons` and Discord uses `components`. (#18215) Thanks @obviyus.
- Agents/Image tool: replace Anthropic-incompatible union schema with explicit `image` (single) and `images` (multi) parameters, keeping tool schemas `anyOf`/`oneOf`/`allOf`-free while preserving multi-image analysis support. (#18551, #18566) Thanks @aldoeliacim.

View File

@@ -315,6 +315,36 @@ describe("exec tool backgrounding", () => {
});
});
describe("exec exit codes", () => {
const originalShell = process.env.SHELL;
beforeEach(() => {
if (!isWin && defaultShell) {
process.env.SHELL = defaultShell;
}
});
afterEach(() => {
if (!isWin) {
process.env.SHELL = originalShell;
}
});
it("treats non-zero exits as completed and appends exit code", async () => {
const command = isWin
? joinCommands(["Write-Output nope", "exit 1"])
: joinCommands(["echo nope", "exit 1"]);
const result = await execTool.execute("call1", { command });
expect(result.details.status).toBe("completed");
expect(result.details.exitCode).toBe(1);
const text = normalizeText(result.content.find((c) => c.type === "text")?.text);
expect(text).toContain("nope");
expect(text).toContain("Command exited with code 1");
});
});
describe("exec notifyOnExit", () => {
it("enqueues a system event when a backgrounded exec exits", async () => {
const tool = createExecTool({