import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../infra/heartbeat-wake.js", () => ({ requestHeartbeatNow: vi.fn(), })); vi.mock("../infra/system-events.js", () => ({ enqueueSystemEvent: vi.fn(), })); import { requestHeartbeatNow } from "../infra/heartbeat-wake.js"; import { enqueueSystemEvent } from "../infra/system-events.js"; import { buildExecExitOutcome, emitExecSystemEvent, formatExecFailureReason, } from "./bash-tools.exec-runtime.js"; const requestHeartbeatNowMock = vi.mocked(requestHeartbeatNow); const enqueueSystemEventMock = vi.mocked(enqueueSystemEvent); describe("emitExecSystemEvent", () => { beforeEach(() => { requestHeartbeatNowMock.mockClear(); enqueueSystemEventMock.mockClear(); }); it("scopes heartbeat wake to the event session key", () => { emitExecSystemEvent("Exec finished", { sessionKey: "agent:ops:main", contextKey: "exec:run-1", }); expect(enqueueSystemEventMock).toHaveBeenCalledWith("Exec finished", { sessionKey: "agent:ops:main", contextKey: "exec:run-1", }); expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ reason: "exec-event", sessionKey: "agent:ops:main", }); }); it("keeps wake unscoped for non-agent session keys", () => { emitExecSystemEvent("Exec finished", { sessionKey: "global", contextKey: "exec:run-global", }); expect(enqueueSystemEventMock).toHaveBeenCalledWith("Exec finished", { sessionKey: "global", contextKey: "exec:run-global", }); expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ reason: "exec-event", }); }); it("ignores events without a session key", () => { emitExecSystemEvent("Exec finished", { sessionKey: " ", contextKey: "exec:run-2", }); expect(enqueueSystemEventMock).not.toHaveBeenCalled(); expect(requestHeartbeatNowMock).not.toHaveBeenCalled(); }); }); describe("formatExecFailureReason", () => { it("formats timeout guidance with the configured timeout", () => { expect( formatExecFailureReason({ failureKind: "overall-timeout", exitSignal: "SIGKILL", timeoutSec: 45, }), ).toContain("45 seconds"); }); it("formats shell failures without timeout-specific guidance", () => { expect( formatExecFailureReason({ failureKind: "shell-command-not-found", exitSignal: null, timeoutSec: 45, }), ).toBe("Command not found"); }); }); describe("buildExecExitOutcome", () => { it("keeps non-zero normal exits in the completed path", () => { expect( buildExecExitOutcome({ exit: { reason: "exit", exitCode: 1, exitSignal: null, durationMs: 123, stdout: "", stderr: "", timedOut: false, noOutputTimedOut: false, }, aggregated: "done", durationMs: 123, timeoutSec: 30, }), ).toMatchObject({ status: "completed", exitCode: 1, aggregated: "done\n\n(Command exited with code 1)", }); }); it("classifies timed out exits as failures with a reason", () => { expect( buildExecExitOutcome({ exit: { reason: "overall-timeout", exitCode: null, exitSignal: "SIGKILL", durationMs: 123, stdout: "", stderr: "", timedOut: true, noOutputTimedOut: false, }, aggregated: "", durationMs: 123, timeoutSec: 30, }), ).toMatchObject({ status: "failed", failureKind: "overall-timeout", timedOut: true, reason: expect.stringContaining("30 seconds"), }); }); });