



















@@ -3,6 +3,7 @@ import type { ContextEngineRuntimeContext } from "../../context-engine/types.js"
33import { peekSystemEvents, resetSystemEventsForTest } from "../../infra/system-events.js";
44import {
55enqueueCommandInLane,
6+markGatewayDraining,
67resetCommandQueueStateForTest,
78} from "../../process/command-queue.js";
89import * as commandQueueModule from "../../process/command-queue.js";
@@ -957,6 +958,143 @@ describe("runContextEngineMaintenance", () => {
957958});
958959});
959960961+it("reports deferred maintenance schedule failure while gateway is draining", async () => {
962+await withStateDirEnv("openclaw-turn-maintenance-draining-", async () => {
963+resetCommandQueueStateForTest();
964+resetTaskRegistryForTests({ persist: false });
965+resetTaskFlowRegistryForTests({ persist: false });
966+967+const sessionKey = "agent:main:session-draining";
968+const maintain = vi.fn(async () => ({
969+changed: false,
970+bytesFreed: 0,
971+rewrittenEntries: 0,
972+}));
973+const onDeferredMaintenance = vi.fn();
974+const onDeferredMaintenanceFailure = vi.fn();
975+const backgroundEngine = {
976+info: {
977+id: "test",
978+name: "Test Engine",
979+turnMaintenanceMode: "background" as const,
980+},
981+ingest: async () => ({ ingested: true }),
982+assemble: async ({ messages }: { messages: unknown[] }) => ({
983+ messages,
984+estimatedTokens: 0,
985+}),
986+compact: async () => ({ ok: true, compacted: false }),
987+ maintain,
988+} as NonNullable<Parameters<typeof runContextEngineMaintenance>[0]["contextEngine"]>;
989+990+markGatewayDraining();
991+const result = await runContextEngineMaintenance({
992+contextEngine: backgroundEngine,
993+sessionId: "session-draining",
994+ sessionKey,
995+sessionFile: "/tmp/session-draining.jsonl",
996+reason: "turn",
997+ onDeferredMaintenance,
998+ onDeferredMaintenanceFailure,
999+});
1000+1001+expect(result).toBeUndefined();
1002+expect(onDeferredMaintenance).not.toHaveBeenCalled();
1003+expect(onDeferredMaintenanceFailure).toHaveBeenCalledOnce();
1004+expect(onDeferredMaintenanceFailure.mock.calls[0]?.[0]).toHaveProperty(
1005+"name",
1006+"GatewayDrainingError",
1007+);
1008+expect(maintain).not.toHaveBeenCalled();
1009+const tasks = listTasksForOwnerKey(sessionKey).filter(
1010+(task) => task.taskKind === TURN_MAINTENANCE_TASK_KIND,
1011+);
1012+expect(tasks).toEqual([]);
1013+});
1014+});
1015+1016+it("rejects coalesced deferred maintenance requests while gateway is draining", async () => {
1017+await withStateDirEnv("openclaw-turn-maintenance-draining-coalesced-", async () => {
1018+vi.useFakeTimers();
1019+try {
1020+resetCommandQueueStateForTest();
1021+resetTaskRegistryForTests({ persist: false });
1022+resetTaskFlowRegistryForTests({ persist: false });
1023+1024+const sessionKey = "agent:main:session-draining-coalesced";
1025+let releaseMaintenance: (() => void) | undefined;
1026+const maintain = vi.fn(async () => {
1027+await new Promise<void>((resolve) => {
1028+releaseMaintenance = resolve;
1029+});
1030+return {
1031+changed: false,
1032+bytesFreed: 0,
1033+rewrittenEntries: 0,
1034+};
1035+});
1036+const backgroundEngine = {
1037+info: {
1038+id: "test",
1039+name: "Test Engine",
1040+turnMaintenanceMode: "background" as const,
1041+},
1042+ingest: async () => ({ ingested: true }),
1043+assemble: async ({ messages }: { messages: unknown[] }) => ({
1044+ messages,
1045+estimatedTokens: 0,
1046+}),
1047+compact: async () => ({ ok: true, compacted: false }),
1048+ maintain,
1049+} as NonNullable<Parameters<typeof runContextEngineMaintenance>[0]["contextEngine"]>;
1050+const firstDeferred: Promise<void>[] = [];
1051+1052+await runContextEngineMaintenance({
1053+contextEngine: backgroundEngine,
1054+sessionId: "session-draining-coalesced",
1055+ sessionKey,
1056+sessionFile: "/tmp/session-draining-coalesced.jsonl",
1057+reason: "turn",
1058+onDeferredMaintenance: (promise) => {
1059+firstDeferred.push(promise);
1060+},
1061+});
1062+await waitForAssertion(() => expect(maintain).toHaveBeenCalledTimes(1));
1063+1064+const onDeferredMaintenance = vi.fn();
1065+const onDeferredMaintenanceFailure = vi.fn();
1066+markGatewayDraining();
1067+const result = await runContextEngineMaintenance({
1068+contextEngine: backgroundEngine,
1069+sessionId: "session-draining-coalesced",
1070+ sessionKey,
1071+sessionFile: "/tmp/session-draining-coalesced.jsonl",
1072+reason: "turn",
1073+ onDeferredMaintenance,
1074+ onDeferredMaintenanceFailure,
1075+});
1076+1077+expect(result).toBeUndefined();
1078+expect(onDeferredMaintenance).not.toHaveBeenCalled();
1079+expect(onDeferredMaintenanceFailure).toHaveBeenCalledOnce();
1080+expect(onDeferredMaintenanceFailure.mock.calls[0]?.[0]).toHaveProperty(
1081+"name",
1082+"GatewayDrainingError",
1083+);
1084+expect(maintain).toHaveBeenCalledTimes(1);
1085+1086+if (!releaseMaintenance) {
1087+throw new Error("Expected maintenance release callback to be initialized");
1088+}
1089+releaseMaintenance();
1090+await firstDeferred[0];
1091+} finally {
1092+resetCommandQueueStateForTest();
1093+vi.useRealTimers();
1094+}
1095+});
1096+});
1097+9601098it("replaces legacy active maintenance tasks that are missing a runId", async () => {
9611099await withStateDirEnv("openclaw-turn-maintenance-", async () => {
9621100vi.useFakeTimers();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。