mirror of https://github.com/openclaw/openclaw.git
test: stabilize windows task registry and exec timeouts
This commit is contained in:
parent
7ec3674b46
commit
9590e2ccae
|
|
@ -45,7 +45,7 @@ describe("runCommandWithTimeout", () => {
|
|||
expect(resolved.npm_config_fund).toBe("false");
|
||||
});
|
||||
|
||||
it("kills command when no output timeout elapses", async () => {
|
||||
it("kills command when no output timeout elapses", { timeout: 15_000 }, async () => {
|
||||
const result = await runCommandWithTimeout(
|
||||
[process.execPath, "-e", "setTimeout(() => {}, 5_000)"],
|
||||
{
|
||||
|
|
@ -59,18 +59,22 @@ describe("runCommandWithTimeout", () => {
|
|||
expect(result.code).not.toBe(0);
|
||||
});
|
||||
|
||||
it("reports global timeout termination when overall timeout elapses", async () => {
|
||||
const result = await runCommandWithTimeout(
|
||||
[process.execPath, "-e", "setTimeout(() => {}, 5_000)"],
|
||||
{
|
||||
timeoutMs: 200,
|
||||
},
|
||||
);
|
||||
it(
|
||||
"reports global timeout termination when overall timeout elapses",
|
||||
{ timeout: 15_000 },
|
||||
async () => {
|
||||
const result = await runCommandWithTimeout(
|
||||
[process.execPath, "-e", "setTimeout(() => {}, 5_000)"],
|
||||
{
|
||||
timeoutMs: 200,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.termination).toBe("timeout");
|
||||
expect(result.noOutputTimedOut).toBe(false);
|
||||
expect(result.code).not.toBe(0);
|
||||
});
|
||||
expect(result.termination).toBe("timeout");
|
||||
expect(result.noOutputTimedOut).toBe(false);
|
||||
expect(result.code).not.toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform === "win32")(
|
||||
"on Windows spawns node + npm-cli.js for npm argv to avoid spawn EINVAL",
|
||||
|
|
|
|||
|
|
@ -98,6 +98,19 @@ async function flushAsyncWork(times = 4) {
|
|||
}
|
||||
}
|
||||
|
||||
async function withTaskRegistryTempDir<T>(run: (root: string) => Promise<T>): Promise<T> {
|
||||
return await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
try {
|
||||
return await run(root);
|
||||
} finally {
|
||||
// Close the sqlite-backed registry before Windows temp-dir cleanup tries to remove it.
|
||||
resetTaskRegistryForTests();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe("task-registry", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
|
|
@ -116,7 +129,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("updates task status from lifecycle events", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -156,7 +169,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("summarizes task pressure by status and runtime", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -210,7 +223,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("delivers ACP completion to the requester channel when a delivery origin exists", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
hoisted.sendMessageMock.mockResolvedValue({
|
||||
|
|
@ -268,7 +281,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("records delivery failure and queues a session fallback when direct delivery misses", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
hoisted.sendMessageMock.mockRejectedValueOnce(new Error("telegram unavailable"));
|
||||
|
|
@ -314,7 +327,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("still wakes the parent when blocked delivery misses the outward channel", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
hoisted.sendMessageMock.mockRejectedValueOnce(new Error("telegram unavailable"));
|
||||
|
|
@ -351,7 +364,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("marks internal fallback delivery as session queued instead of delivered", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -389,7 +402,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("wakes the parent for blocked tasks even when delivery falls back to the session", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -421,7 +434,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("does not include internal progress detail in the terminal channel message", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
hoisted.sendMessageMock.mockResolvedValue({
|
||||
|
|
@ -472,7 +485,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("surfaces blocked outcomes separately from completed tasks", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
hoisted.sendMessageMock.mockResolvedValue({
|
||||
|
|
@ -513,7 +526,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("does not queue an unblock follow-up for ordinary completed tasks", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
hoisted.sendMessageMock.mockResolvedValue({
|
||||
|
|
@ -552,7 +565,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("keeps distinct task records when different producers share a runId", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -585,7 +598,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("suppresses duplicate ACP delivery when a preferred spawned task shares the runId", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
hoisted.sendMessageMock.mockResolvedValue({
|
||||
|
|
@ -638,7 +651,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("adopts preferred ACP spawn metadata when collapsing onto an earlier direct record", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -724,7 +737,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("collapses ACP run-owned task creation onto the existing spawned task", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -764,7 +777,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("delivers a terminal ACP update only once when multiple notifiers race", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
hoisted.sendMessageMock.mockResolvedValue({
|
||||
|
|
@ -809,7 +822,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("restores persisted tasks from disk on the next lookup", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -838,7 +851,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("indexes tasks by session key for latest and list lookups", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests({ persist: false });
|
||||
const nowSpy = vi.spyOn(Date, "now");
|
||||
|
|
@ -870,7 +883,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("projects inspection-time orphaned tasks as lost without mutating the registry", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -902,7 +915,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("marks orphaned tasks lost with cleanupAfter in a single maintenance pass", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
const now = Date.now();
|
||||
|
|
@ -935,7 +948,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("prunes old terminal tasks during maintenance sweeps", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
|
||||
|
|
@ -965,7 +978,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("previews and repairs missing cleanup timestamps during maintenance", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
const now = Date.now();
|
||||
|
|
@ -1012,7 +1025,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("summarizes inspectable task audit findings", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
const now = Date.now();
|
||||
|
|
@ -1060,7 +1073,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("delivers concise state-change updates only when notify policy requests them", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
hoisted.sendMessageMock.mockResolvedValue({
|
||||
|
|
@ -1179,7 +1192,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("keeps background ACP progress off the foreground lane and only sends a terminal notify", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
resetSystemEventsForTest();
|
||||
|
|
@ -1252,7 +1265,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("delivers a concise terminal failure message without internal ACP chatter", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
resetSystemEventsForTest();
|
||||
|
|
@ -1418,7 +1431,7 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("emits concise state-change updates without surfacing raw ACP chatter", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
resetTaskRegistryForTests();
|
||||
resetSystemEventsForTest();
|
||||
|
|
@ -1480,112 +1493,120 @@ describe("task-registry", () => {
|
|||
});
|
||||
|
||||
it("cancels ACP-backed tasks through the ACP session manager", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
const registry = await loadFreshTaskRegistryModulesForControlTest();
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
registry.resetTaskRegistryForTests();
|
||||
hoisted.cancelSessionMock.mockResolvedValue(undefined);
|
||||
try {
|
||||
hoisted.cancelSessionMock.mockResolvedValue(undefined);
|
||||
|
||||
const task = registry.createTaskRecord({
|
||||
runtime: "acp",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: {
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
},
|
||||
childSessionKey: "agent:codex:acp:child",
|
||||
runId: "run-cancel-acp",
|
||||
task: "Investigate issue",
|
||||
status: "running",
|
||||
deliveryStatus: "pending",
|
||||
});
|
||||
|
||||
const result = await registry.cancelTaskById({
|
||||
cfg: {} as never,
|
||||
taskId: task.taskId,
|
||||
});
|
||||
|
||||
expect(hoisted.cancelSessionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cfg: {},
|
||||
sessionKey: "agent:codex:acp:child",
|
||||
reason: "task-cancel",
|
||||
}),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
found: true,
|
||||
cancelled: true,
|
||||
task: expect.objectContaining({
|
||||
taskId: task.taskId,
|
||||
status: "cancelled",
|
||||
error: "Cancelled by operator.",
|
||||
}),
|
||||
});
|
||||
await waitForAssertion(() =>
|
||||
expect(hoisted.sendMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
const task = registry.createTaskRecord({
|
||||
runtime: "acp",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: {
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
content: "Background task cancelled: ACP background task (run run-canc).",
|
||||
},
|
||||
childSessionKey: "agent:codex:acp:child",
|
||||
runId: "run-cancel-acp",
|
||||
task: "Investigate issue",
|
||||
status: "running",
|
||||
deliveryStatus: "pending",
|
||||
});
|
||||
|
||||
const result = await registry.cancelTaskById({
|
||||
cfg: {} as never,
|
||||
taskId: task.taskId,
|
||||
});
|
||||
|
||||
expect(hoisted.cancelSessionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cfg: {},
|
||||
sessionKey: "agent:codex:acp:child",
|
||||
reason: "task-cancel",
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
found: true,
|
||||
cancelled: true,
|
||||
task: expect.objectContaining({
|
||||
taskId: task.taskId,
|
||||
status: "cancelled",
|
||||
error: "Cancelled by operator.",
|
||||
}),
|
||||
});
|
||||
await waitForAssertion(() =>
|
||||
expect(hoisted.sendMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
content: "Background task cancelled: ACP background task (run run-canc).",
|
||||
}),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
registry.resetTaskRegistryForTests();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels subagent-backed tasks through subagent control", async () => {
|
||||
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
|
||||
await withTaskRegistryTempDir(async (root) => {
|
||||
const registry = await loadFreshTaskRegistryModulesForControlTest();
|
||||
process.env.OPENCLAW_STATE_DIR = root;
|
||||
registry.resetTaskRegistryForTests();
|
||||
hoisted.killSubagentRunAdminMock.mockResolvedValue({
|
||||
found: true,
|
||||
killed: true,
|
||||
});
|
||||
try {
|
||||
hoisted.killSubagentRunAdminMock.mockResolvedValue({
|
||||
found: true,
|
||||
killed: true,
|
||||
});
|
||||
|
||||
const task = registry.createTaskRecord({
|
||||
runtime: "subagent",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: {
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
},
|
||||
childSessionKey: "agent:worker:subagent:child",
|
||||
runId: "run-cancel-subagent",
|
||||
task: "Investigate issue",
|
||||
status: "running",
|
||||
deliveryStatus: "pending",
|
||||
});
|
||||
|
||||
const result = await registry.cancelTaskById({
|
||||
cfg: {} as never,
|
||||
taskId: task.taskId,
|
||||
});
|
||||
|
||||
expect(hoisted.killSubagentRunAdminMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cfg: {},
|
||||
sessionKey: "agent:worker:subagent:child",
|
||||
}),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
found: true,
|
||||
cancelled: true,
|
||||
task: expect.objectContaining({
|
||||
taskId: task.taskId,
|
||||
status: "cancelled",
|
||||
error: "Cancelled by operator.",
|
||||
}),
|
||||
});
|
||||
await waitForAssertion(() =>
|
||||
expect(hoisted.sendMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
const task = registry.createTaskRecord({
|
||||
runtime: "subagent",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: {
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
content: "Background task cancelled: Subagent task (run run-canc).",
|
||||
},
|
||||
childSessionKey: "agent:worker:subagent:child",
|
||||
runId: "run-cancel-subagent",
|
||||
task: "Investigate issue",
|
||||
status: "running",
|
||||
deliveryStatus: "pending",
|
||||
});
|
||||
|
||||
const result = await registry.cancelTaskById({
|
||||
cfg: {} as never,
|
||||
taskId: task.taskId,
|
||||
});
|
||||
|
||||
expect(hoisted.killSubagentRunAdminMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cfg: {},
|
||||
sessionKey: "agent:worker:subagent:child",
|
||||
}),
|
||||
),
|
||||
);
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
found: true,
|
||||
cancelled: true,
|
||||
task: expect.objectContaining({
|
||||
taskId: task.taskId,
|
||||
status: "cancelled",
|
||||
error: "Cancelled by operator.",
|
||||
}),
|
||||
});
|
||||
await waitForAssertion(() =>
|
||||
expect(hoisted.sendMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
to: "telegram:123",
|
||||
content: "Background task cancelled: Subagent task (run run-canc).",
|
||||
}),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
registry.resetTaskRegistryForTests();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue