mirror of
https://github.com/openclaw/openclaw.git
synced 2026-11-05 23:33:06 +00:00
test(gateway): verify live task cancellation in release E2E (#139135)
Require offline CLI cancellation to fail visibly without mutating persisted state. Exercise authenticated RPC and CLI cancellation against a real held provider request, preserve cancellation provenance, and verify later heartbeat work succeeds. Follow up the execution ownership repair in #118358.
This commit is contained in:
parent
6c1ebc03ad
commit
48406f2a41
2 changed files with 61 additions and 25 deletions
|
|
@ -97,9 +97,14 @@ describe("task operations product boundary", () => {
|
|||
it("runs persisted task operations through CLI, chat, notification, audit, and maintenance", async () => {
|
||||
await withOpenClawTestState(
|
||||
{
|
||||
layout: "state-only",
|
||||
layout: "home",
|
||||
scenario: "minimal",
|
||||
prefix: "openclaw-task-operations-e2e-",
|
||||
env: {
|
||||
OPENCLAW_GATEWAY_TOKEN: undefined,
|
||||
OPENCLAW_GATEWAY_PASSWORD: undefined,
|
||||
OPENCLAW_GATEWAY_URL: undefined,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
resetTaskOperationsRuntime();
|
||||
|
|
@ -265,19 +270,26 @@ describe("task operations product boundary", () => {
|
|||
|
||||
const cancel = createRuntime();
|
||||
await tasksCancelCommand({ lookup: operatorTask.taskId }, cancel.runtime);
|
||||
expect(cancel.errors).toEqual([]);
|
||||
expect(cancel.exits).toEqual([]);
|
||||
expect(cancel.logs).toEqual([
|
||||
`Cancelled ${operatorTask.taskId} (cli) run run-a07-operator.`,
|
||||
// A persisted row cannot stand in for the Gateway's live execution owner.
|
||||
expect(cancel.errors).toEqual([
|
||||
expect.stringContaining(
|
||||
"CLI task cancellation requires the live Gateway tasks.cancel path:",
|
||||
),
|
||||
]);
|
||||
expect(cancel.errors[0]).toContain("requires credentials before opening a websocket");
|
||||
expect(cancel.exits).toEqual([1]);
|
||||
expect(cancel.logs).toEqual([]);
|
||||
resetTaskRegistryForTests({ persist: false });
|
||||
reloadTaskRegistryFromStore();
|
||||
expect(requireTask(operatorTask.taskId)).toMatchObject({
|
||||
status: "cancelled",
|
||||
error: "Cancelled by operator.",
|
||||
status: "running",
|
||||
});
|
||||
|
||||
const cancelledShow = createRuntime();
|
||||
await tasksShowCommand({ lookup: operatorTask.taskId }, cancelledShow.runtime);
|
||||
expect(cancelledShow.logs.join("\n")).toContain("status: cancelled");
|
||||
expect(requireTask(operatorTask.taskId).error).toBeUndefined();
|
||||
|
||||
const retainedShow = createRuntime();
|
||||
await tasksShowCommand({ lookup: operatorTask.taskId }, retainedShow.runtime);
|
||||
expect(retainedShow.logs.join("\n")).toContain("status: running");
|
||||
} finally {
|
||||
clearHeartbeat();
|
||||
resetTaskOperationsRuntime();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
TasksListResult,
|
||||
} from "../../../../packages/gateway-protocol/src/index.js";
|
||||
import { withFastReplyConfig } from "../../../../src/auto-reply/reply/get-reply-fast-path.test-support.js";
|
||||
import { tasksCancelCommand } from "../../../../src/commands/tasks.js";
|
||||
import {
|
||||
clearConfigCache,
|
||||
clearRuntimeConfigSnapshot,
|
||||
|
|
@ -25,6 +26,7 @@ import { resetAgentEventsForTest } from "../../../../src/infra/agent-events.js";
|
|||
import { resetSystemEventsForTest } from "../../../../src/infra/system-events.js";
|
||||
import { resetTaskRegistryForTests } from "../../../../src/tasks/task-runtime.test-helpers.js";
|
||||
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../../../../src/test-utils/env.js";
|
||||
import { createRuntimeEnv } from "../../../../src/test-utils/plugin-runtime-env.js";
|
||||
import { writeOpenAiResponsesSse } from "../../../helpers/openai-responses-sse.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../../../helpers/temp-dir.js";
|
||||
|
||||
|
|
@ -33,6 +35,7 @@ const ISOLATED_GATEWAY_ENV_KEYS = [
|
|||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_CONFIG_PATH",
|
||||
"OPENCLAW_GATEWAY_TOKEN",
|
||||
"OPENCLAW_GATEWAY_URL",
|
||||
"OPENCLAW_TEST_GATEWAY_OVERRIDE_TOKEN",
|
||||
"OPENCLAW_TEST_RUNTIME_OVERRIDE_TOKEN",
|
||||
"OPENCLAW_TEST_MINIMAL_GATEWAY",
|
||||
|
|
@ -94,10 +97,10 @@ describe("Gateway task and automation RPCs", () => {
|
|||
beforeEach(resetGatewayState);
|
||||
afterEach(resetGatewayState);
|
||||
|
||||
it(
|
||||
"persists cron CRUD, wakes the heartbeat, and controls an agent-created task",
|
||||
it.each(["RPC", "CLI"] as const)(
|
||||
"persists cron CRUD, wakes the heartbeat, and cancels an agent-created task through %s",
|
||||
{ timeout: 90_000 },
|
||||
async () => {
|
||||
async (cancelSurface) => {
|
||||
const envSnapshot = captureEnv([...ISOLATED_GATEWAY_ENV_KEYS]);
|
||||
const tempHome = tempDirs.make("openclaw-gateway-automation-");
|
||||
const stateDir = path.join(tempHome, ".openclaw");
|
||||
|
|
@ -131,6 +134,7 @@ describe("Gateway task and automation RPCs", () => {
|
|||
setTestEnvValue(key, value);
|
||||
}
|
||||
deleteTestEnvValue("OPENCLAW_CONFIG_PATH");
|
||||
deleteTestEnvValue("OPENCLAW_GATEWAY_URL");
|
||||
deleteTestEnvValue("OPENCLAW_TEST_MINIMAL_GATEWAY");
|
||||
|
||||
const taskPrompt = nextId("create-tracked-task");
|
||||
|
|
@ -335,22 +339,42 @@ describe("Gateway task and automation RPCs", () => {
|
|||
prompt: taskPrompt,
|
||||
},
|
||||
});
|
||||
const cancelled = await client.request<TasksCancelResult>("tasks.cancel", {
|
||||
taskId,
|
||||
reason: "Gateway RPC automation evidence complete",
|
||||
});
|
||||
expect(cancelled).toMatchObject({
|
||||
found: true,
|
||||
cancelled: true,
|
||||
task: { id: taskId, status: "cancelled" },
|
||||
});
|
||||
await expect
|
||||
.poll(() => providerRequests.some((body) => JSON.stringify(body).includes(taskPrompt)), {
|
||||
timeout: 10_000,
|
||||
interval: 50,
|
||||
})
|
||||
.toBe(true);
|
||||
const cancellationReason =
|
||||
cancelSurface === "RPC"
|
||||
? "Gateway RPC automation evidence complete"
|
||||
: "Cancelled by operator.";
|
||||
if (cancelSurface === "RPC") {
|
||||
const cancelled = await client.request<TasksCancelResult>("tasks.cancel", {
|
||||
taskId,
|
||||
reason: cancellationReason,
|
||||
});
|
||||
expect(cancelled).toMatchObject({
|
||||
found: true,
|
||||
cancelled: true,
|
||||
task: { id: taskId, status: "cancelled" },
|
||||
});
|
||||
} else {
|
||||
const runtime = createRuntimeEnv();
|
||||
await tasksCancelCommand({ lookup: taskId }, runtime);
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
expect(runtime.log).toHaveBeenCalledExactlyOnceWith(
|
||||
`Cancelled ${taskId} (cli) run ${runId}.`,
|
||||
);
|
||||
}
|
||||
await expect(
|
||||
client.request<TasksGetResult>("tasks.get", { taskId }),
|
||||
).resolves.toMatchObject({
|
||||
task: {
|
||||
id: taskId,
|
||||
status: "cancelled",
|
||||
error: "Gateway RPC automation evidence complete",
|
||||
error: cancellationReason,
|
||||
},
|
||||
});
|
||||
const releaseResponse = releaseTaskResponse;
|
||||
|
|
@ -364,14 +388,14 @@ describe("Gateway task and automation RPCs", () => {
|
|||
{ runId: started.runId, timeoutMs: 30_000 },
|
||||
{ timeoutMs: 35_000 },
|
||||
);
|
||||
expect(agentWait).toMatchObject({ status: "ok" });
|
||||
expect(agentWait).toMatchObject({ status: "error", stopReason: "rpc" });
|
||||
await expect(
|
||||
client.request<TasksGetResult>("tasks.get", { taskId }),
|
||||
).resolves.toMatchObject({
|
||||
task: {
|
||||
id: taskId,
|
||||
status: "cancelled",
|
||||
error: "Gateway RPC automation evidence complete",
|
||||
error: cancellationReason,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue