
















@@ -2,6 +2,9 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2233const requestHeartbeatNowMock = vi.hoisted(() => vi.fn());
44const enqueueSystemEventMock = vi.hoisted(() => vi.fn());
5+const supervisorMock = vi.hoisted(() => ({
6+spawn: vi.fn(),
7+}));
5869vi.mock("../infra/heartbeat-wake.js", () => ({
710requestHeartbeatNow: requestHeartbeatNowMock,
@@ -11,22 +14,38 @@ vi.mock("../infra/system-events.js", () => ({
1114enqueueSystemEvent: enqueueSystemEventMock,
1215}));
131617+vi.mock("../process/supervisor/index.js", () => ({
18+getProcessSupervisor: () => ({
19+spawn: supervisorMock.spawn,
20+}),
21+}));
22+23+let markBackgrounded: typeof import("./bash-process-registry.js").markBackgrounded;
1424let buildExecExitOutcome: typeof import("./bash-tools.exec-runtime.js").buildExecExitOutcome;
1525let detectCursorKeyMode: typeof import("./bash-tools.exec-runtime.js").detectCursorKeyMode;
1626let emitExecSystemEvent: typeof import("./bash-tools.exec-runtime.js").emitExecSystemEvent;
1727let formatExecFailureReason: typeof import("./bash-tools.exec-runtime.js").formatExecFailureReason;
1828let resolveExecTarget: typeof import("./bash-tools.exec-runtime.js").resolveExecTarget;
29+let runExecProcess: typeof import("./bash-tools.exec-runtime.js").runExecProcess;
19302031beforeAll(async () => {
32+({ markBackgrounded } = await import("./bash-process-registry.js"));
2133({
2234 buildExecExitOutcome,
2335 detectCursorKeyMode,
2436 emitExecSystemEvent,
2537 formatExecFailureReason,
2638 resolveExecTarget,
39+ runExecProcess,
2740} = await import("./bash-tools.exec-runtime.js"));
2841});
294243+beforeEach(() => {
44+requestHeartbeatNowMock.mockClear();
45+enqueueSystemEventMock.mockClear();
46+supervisorMock.spawn.mockReset();
47+});
48+3049describe("detectCursorKeyMode", () => {
3150it("returns null when no toggle found", () => {
3251expect(detectCursorKeyMode("hello world")).toBe(null);
@@ -295,6 +314,84 @@ describe("resolveExecTarget", () => {
295314});
296315});
297316317+describe("exec notifyOnExit suppression", () => {
318+async function runBackgroundedExit(params: {
319+reason: "manual-cancel" | "overall-timeout";
320+stdout?: string;
321+}) {
322+supervisorMock.spawn.mockImplementationOnce(
323+async (input: { onStdout?: (chunk: string) => void }) => {
324+if (params.stdout) {
325+input.onStdout?.(params.stdout);
326+}
327+return {
328+runId: "run-1",
329+startedAtMs: Date.now(),
330+pid: 123,
331+wait: async () => {
332+await new Promise((resolve) => setImmediate(resolve));
333+return {
334+reason: params.reason,
335+exitCode: null,
336+exitSignal: "SIGKILL",
337+durationMs: 10,
338+stdout: "",
339+stderr: "",
340+timedOut: params.reason === "overall-timeout",
341+noOutputTimedOut: false,
342+};
343+},
344+cancel: vi.fn(),
345+};
346+},
347+);
348+349+const run = await runExecProcess({
350+command: "sleep 999",
351+workdir: "/tmp",
352+env: {},
353+usePty: false,
354+warnings: [],
355+maxOutput: 1000,
356+pendingMaxOutput: 1000,
357+notifyOnExit: true,
358+notifyOnExitEmptySuccess: false,
359+sessionKey: "agent:main:main",
360+timeoutSec: null,
361+});
362+markBackgrounded(run.session);
363+return await run.promise;
364+}
365+366+it("keeps manual-cancelled no-output background execs silent", async () => {
367+const outcome = await runBackgroundedExit({ reason: "manual-cancel" });
368+369+expect(outcome.status).toBe("failed");
370+expect(enqueueSystemEventMock).not.toHaveBeenCalled();
371+expect(requestHeartbeatNowMock).not.toHaveBeenCalled();
372+});
373+374+it("notifies for manual-cancelled background execs with output", async () => {
375+await runBackgroundedExit({ reason: "manual-cancel", stdout: "partial output\n" });
376+377+expect(enqueueSystemEventMock).toHaveBeenCalledWith(
378+expect.stringContaining("partial output"),
379+expect.objectContaining({ sessionKey: "agent:main:main" }),
380+);
381+expect(requestHeartbeatNowMock).toHaveBeenCalled();
382+});
383+384+it("still notifies for no-output background exec timeouts", async () => {
385+await runBackgroundedExit({ reason: "overall-timeout" });
386+387+expect(enqueueSystemEventMock).toHaveBeenCalledWith(
388+expect.stringContaining("Exec failed"),
389+expect.objectContaining({ sessionKey: "agent:main:main" }),
390+);
391+expect(requestHeartbeatNowMock).toHaveBeenCalled();
392+});
393+});
394+298395describe("emitExecSystemEvent", () => {
299396beforeEach(() => {
300397requestHeartbeatNowMock.mockClear();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。