




















@@ -187,6 +187,206 @@ describe("post-compaction loop guard wired into runEmbeddedAgent", () => {
187187expect(attemptSignalReason).toBeInstanceOf(PostCompactionLoopPersistedError);
188188});
189189190+it("releases the lane after a post-compaction abort when the backend ignores cancellation", async () => {
191+vi.useFakeTimers();
192+let settleIgnoredAttempt: ((value: ReturnType<typeof makeAttemptResult>) => void) | undefined;
193+let resolveAttemptAborted: (() => void) | undefined;
194+const attemptAbortedPromise = new Promise<void>((resolve) => {
195+resolveAttemptAborted = resolve;
196+});
197+try {
198+const overflowError = makeOverflowError();
199+let attemptAborted = false;
200+mockedRunEmbeddedAttempt.mockResolvedValueOnce(
201+makeAttemptResult({ promptError: overflowError }),
202+);
203+mockedRunEmbeddedAttempt.mockImplementationOnce(async (attemptParams: unknown) => {
204+const { abortSignal, onToolOutcome } = attemptParams as {
205+abortSignal?: AbortSignal;
206+onToolOutcome?: ToolOutcomeObserver;
207+};
208+for (let i = 0; i < 3; i += 1) {
209+await executeWrappedToolOutcome(
210+"gateway",
211+{ action: "lookup", path: "x" },
212+"identical-result",
213+onToolOutcome,
214+);
215+}
216+attemptAborted = abortSignal?.aborted ?? false;
217+resolveAttemptAborted?.();
218+return await new Promise((resolve) => {
219+settleIgnoredAttempt = resolve;
220+});
221+});
222+mockedCompactDirect.mockResolvedValueOnce(
223+makeCompactionSuccess({
224+summary: "Compacted session",
225+firstKeptEntryId: "entry-5",
226+tokensBefore: 150000,
227+}),
228+);
229+230+const run = runEmbeddedAgent({
231+ ...baseParams,
232+runId: "run-post-compaction-abort-lane-release",
233+timeoutMs: 48 * 60 * 60 * 1000,
234+});
235+let settled = false;
236+void run
237+.finally(() => {
238+settled = true;
239+})
240+.catch(() => {});
241+242+await attemptAbortedPromise;
243+expect(attemptAborted).toBe(true);
244+await vi.advanceTimersByTimeAsync(30_001);
245+246+expect(settled).toBe(true);
247+await expect(run).rejects.toMatchObject({ name: "CommandLaneTaskTimeoutError" });
248+} finally {
249+settleIgnoredAttempt?.(makeAttemptResult());
250+vi.useRealTimers();
251+}
252+});
253+254+it("keeps a native lane alive while a tool is still running", async () => {
255+vi.useFakeTimers();
256+let settleAttempt: ((value: ReturnType<typeof makeAttemptResult>) => void) | undefined;
257+let resolveAttemptStarted: (() => void) | undefined;
258+const attemptStarted = new Promise<void>((resolve) => {
259+resolveAttemptStarted = resolve;
260+});
261+try {
262+mockedRunEmbeddedAttempt.mockImplementationOnce(async (attemptParams: unknown) => {
263+const { onAttemptTimeoutArmed } = attemptParams as {
264+onAttemptTimeoutArmed?: () => void;
265+};
266+resolveAttemptStarted?.();
267+onAttemptTimeoutArmed?.();
268+return await new Promise((resolve) => {
269+settleAttempt = resolve;
270+});
271+});
272+273+const run = runEmbeddedAgent({
274+ ...baseParams,
275+runId: "run-native-tool-heartbeat",
276+timeoutMs: 1,
277+agentHarnessRuntimeOverride: "openclaw",
278+});
279+let settled = false;
280+void run
281+.finally(() => {
282+settled = true;
283+})
284+.catch(() => {});
285+286+await vi.advanceTimersByTimeAsync(0);
287+await attemptStarted;
288+await vi.advanceTimersByTimeAsync(30_001);
289+290+expect(settled).toBe(false);
291+settleAttempt?.(makeAttemptResult());
292+await expect(run).resolves.toMatchObject({ meta: { aborted: false } });
293+} finally {
294+vi.useRealTimers();
295+}
296+});
297+298+it("releases a native lane when a timed-out attempt ignores cancellation", async () => {
299+vi.useFakeTimers();
300+let settleAttempt: ((value: ReturnType<typeof makeAttemptResult>) => void) | undefined;
301+let resolveAttemptStarted: (() => void) | undefined;
302+const attemptStarted = new Promise<void>((resolve) => {
303+resolveAttemptStarted = resolve;
304+});
305+try {
306+mockedRunEmbeddedAttempt.mockImplementationOnce(async (attemptParams: unknown) => {
307+const { onAttemptTimeoutArmed, onAttemptTimeout } = attemptParams as {
308+onAttemptTimeoutArmed?: () => void;
309+onAttemptTimeout?: (reason: Error) => void;
310+};
311+resolveAttemptStarted?.();
312+onAttemptTimeoutArmed?.();
313+onAttemptTimeout?.(new Error("attempt timed out"));
314+return await new Promise((resolve) => {
315+settleAttempt = resolve;
316+});
317+});
318+319+const run = runEmbeddedAgent({
320+ ...baseParams,
321+runId: "run-native-timeout-lane-release",
322+timeoutMs: 48 * 60 * 60 * 1000,
323+agentHarnessRuntimeOverride: "openclaw",
324+});
325+let settled = false;
326+void run
327+.finally(() => {
328+settled = true;
329+})
330+.catch(() => {});
331+332+await vi.advanceTimersByTimeAsync(0);
333+await attemptStarted;
334+await vi.advanceTimersByTimeAsync(30_001);
335+336+expect(settled).toBe(true);
337+await expect(run).rejects.toMatchObject({ name: "CommandLaneTaskTimeoutError" });
338+} finally {
339+settleAttempt?.(makeAttemptResult());
340+vi.useRealTimers();
341+}
342+});
343+344+it("releases a native lane when an explicit abort ignores cancellation", async () => {
345+vi.useFakeTimers();
346+let settleAttempt: ((value: ReturnType<typeof makeAttemptResult>) => void) | undefined;
347+let resolveAttemptStarted: (() => void) | undefined;
348+const attemptStarted = new Promise<void>((resolve) => {
349+resolveAttemptStarted = resolve;
350+});
351+try {
352+mockedRunEmbeddedAttempt.mockImplementationOnce(async (attemptParams: unknown) => {
353+const { onAttemptTimeoutArmed, onAttemptAbort } = attemptParams as {
354+onAttemptTimeoutArmed?: () => void;
355+onAttemptAbort?: () => void;
356+};
357+resolveAttemptStarted?.();
358+onAttemptTimeoutArmed?.();
359+onAttemptAbort?.();
360+return await new Promise((resolve) => {
361+settleAttempt = resolve;
362+});
363+});
364+365+const run = runEmbeddedAgent({
366+ ...baseParams,
367+runId: "run-native-abort-lane-release",
368+timeoutMs: 48 * 60 * 60 * 1000,
369+agentHarnessRuntimeOverride: "openclaw",
370+});
371+let settled = false;
372+void run
373+.finally(() => {
374+settled = true;
375+})
376+.catch(() => {});
377+378+await vi.advanceTimersByTimeAsync(0);
379+await attemptStarted;
380+await vi.advanceTimersByTimeAsync(30_001);
381+382+expect(settled).toBe(true);
383+await expect(run).rejects.toMatchObject({ name: "CommandLaneTaskTimeoutError" });
384+} finally {
385+settleAttempt?.(makeAttemptResult());
386+vi.useRealTimers();
387+}
388+});
389+190390it("does not abort when the result hash changes across post-compaction attempts (progress was made)", async () => {
191391const overflowError = makeOverflowError();
192392// Attempt 1: overflow → triggers compaction.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。