

























@@ -5,6 +5,7 @@ import path from "node:path";
55import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
66import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
77import type { OpenClawConfig } from "../../config/config.js";
8+import type { SessionAbortTargetResult } from "../../config/sessions/session-accessor.js";
89import {
910testing as abortTesting,
1011getAbortMemory,
@@ -13,7 +14,6 @@ import {
1314isAbortTrigger,
1415resetAbortMemoryForTest,
1516resolveAbortCutoffFromContext,
16-resolveSessionEntryForKey,
1717setAbortMemory,
1818stopSubagentsForRequester,
1919shouldSkipMessageByAbortCutoff,
@@ -382,32 +382,6 @@ describe("abort detection", () => {
382382).toBe(false);
383383});
384384385-it("resolves session entry when key exists in store", () => {
386-const store = {
387-"session-1": { sessionId: "abc", updatedAt: 0 },
388-} as const;
389-expect(resolveSessionEntryForKey(store, "session-1")).toEqual({
390-entry: store["session-1"],
391-key: "session-1",
392-});
393-expect(resolveSessionEntryForKey(store, "session-2")).toStrictEqual({});
394-expect(resolveSessionEntryForKey(undefined, "session-1")).toStrictEqual({});
395-});
396-397-it("resolves Telegram forum topic session when lookup key has different casing than store", () => {
398-// Store normalizes keys to lowercase; caller may pass mixed-case. /stop in topic must find entry.
399-const storeKey = "agent:main:telegram:group:-1001234567890:topic:99";
400-const lookupKey = "Agent:Main:Telegram:Group:-1001234567890:Topic:99";
401-const store = {
402-[storeKey]: { sessionId: "agent-topic-99", updatedAt: 0 },
403-} as Record<string, { sessionId: string; updatedAt: number }>;
404-// Direct lookup fails (store uses lowercase keys); normalization fallback must succeed.
405-expect(store[lookupKey]).toBeUndefined();
406-const result = resolveSessionEntryForKey(store, lookupKey);
407-expect(result.entry?.sessionId).toBe("agent-topic-99");
408-expect(result.key).toBe(storeKey);
409-});
410-411385it("fast-aborts even when text commands are disabled", async () => {
412386const { cfg } = await createAbortConfig({ commandsTextEnabled: false });
413387@@ -473,6 +447,240 @@ describe("abort detection", () => {
473447expectSessionLaneCleared(sessionKey);
474448});
475449450+it("fast-abort resolves canonical stored session identity before metadata persistence", async () => {
451+const storeKey = "agent:main:telegram:group:-1001234567890:topic:99";
452+const lookupKey = "Agent:Main:Telegram:Group:-1001234567890:Topic:99";
453+const sessionId = "agent-topic-99";
454+const { root, cfg } = await createAbortConfig({
455+sessionIdsByKey: { [storeKey]: sessionId },
456+});
457+enqueueQueuedFollowupRun({ root, cfg, sessionId, sessionKey: storeKey });
458+459+const result = await runStopCommand({
460+ cfg,
461+sessionKey: lookupKey,
462+from: "telegram:123",
463+to: "telegram:123",
464+});
465+466+expect(result.handled).toBe(true);
467+expect(runtimeAbortMocks.abortEmbeddedAgentRun).toHaveBeenCalledWith(sessionId);
468+expect(getFollowupQueueDepth(storeKey)).toBe(0);
469+expectSessionLaneCleared(storeKey);
470+});
471+472+it("fast-abort still stops active runs when abort metadata persistence fails", async () => {
473+const sessionKey = "telegram:persistence-failure";
474+const sessionId = "session-persistence-failure";
475+const activeSessionId = "active-persistence-failure";
476+const { root, cfg } = await createAbortConfig({
477+sessionIdsByKey: { [sessionKey]: sessionId },
478+});
479+runtimeAbortMocks.resolveActiveEmbeddedRunSessionId.mockReturnValue(activeSessionId);
480+abortTesting.setDepsForTests({
481+getAcpSessionManager: (() =>
482+({
483+resolveSession: acpManagerMocks.resolveSession,
484+cancelSession: acpManagerMocks.cancelSession,
485+}) as never) as never,
486+abortEmbeddedAgentRun: runtimeAbortMocks.abortEmbeddedAgentRun,
487+resolveActiveEmbeddedRunSessionId: runtimeAbortMocks.resolveActiveEmbeddedRunSessionId,
488+markSessionAbortTarget: vi.fn(async () => {
489+throw new Error("simulated persistence failure");
490+}),
491+getLatestSubagentRunByChildSessionKey:
492+subagentRegistryMocks.getLatestSubagentRunByChildSessionKey,
493+listSubagentRunsForController: subagentRegistryMocks.listSubagentRunsForRequester,
494+markSubagentRunTerminated: subagentRegistryMocks.markSubagentRunTerminated,
495+});
496+enqueueQueuedFollowupRun({ root, cfg, sessionId, sessionKey });
497+498+const result = await runStopCommand({
499+ cfg,
500+ sessionKey,
501+from: "telegram:123",
502+to: "telegram:123",
503+});
504+505+expect(result.handled).toBe(true);
506+expect(runtimeAbortMocks.abortEmbeddedAgentRun).toHaveBeenCalledWith(activeSessionId);
507+expect(getFollowupQueueDepth(sessionKey)).toBe(0);
508+expectSessionLaneCleared(sessionKey);
509+expect(getAbortMemory(sessionKey)).toBeUndefined();
510+});
511+512+it("fast-abort uses resolved target identity when abort metadata save fails", async () => {
513+const requestedKey = "Agent:Main:Telegram:Group:-1001234567890:Topic:99";
514+const canonicalKey = "agent:main:telegram:group:-1001234567890:topic:99";
515+const sessionId = "resolved-persistence-failure";
516+const { root, cfg } = await createAbortConfig();
517+abortTesting.setDepsForTests({
518+getAcpSessionManager: (() =>
519+({
520+resolveSession: acpManagerMocks.resolveSession,
521+cancelSession: acpManagerMocks.cancelSession,
522+}) as never) as never,
523+abortEmbeddedAgentRun: runtimeAbortMocks.abortEmbeddedAgentRun,
524+resolveActiveEmbeddedRunSessionId: runtimeAbortMocks.resolveActiveEmbeddedRunSessionId,
525+markSessionAbortTarget: vi.fn(async () => ({
526+entry: {
527+ sessionId,
528+updatedAt: 10,
529+},
530+persisted: false,
531+persistenceError: "simulated persistence failure",
532+ sessionId,
533+sessionKey: canonicalKey,
534+})),
535+resolveSessionAbortTarget: vi.fn(() => ({
536+entry: {
537+ sessionId,
538+updatedAt: 10,
539+},
540+ sessionId,
541+sessionKey: canonicalKey,
542+})),
543+getLatestSubagentRunByChildSessionKey:
544+subagentRegistryMocks.getLatestSubagentRunByChildSessionKey,
545+listSubagentRunsForController: subagentRegistryMocks.listSubagentRunsForRequester,
546+markSubagentRunTerminated: subagentRegistryMocks.markSubagentRunTerminated,
547+});
548+enqueueQueuedFollowupRun({ root, cfg, sessionId, sessionKey: canonicalKey });
549+550+const result = await runStopCommand({
551+ cfg,
552+sessionKey: requestedKey,
553+from: "telegram:123",
554+to: "telegram:123",
555+});
556+557+expect(result.handled).toBe(true);
558+expect(runtimeAbortMocks.abortEmbeddedAgentRun).toHaveBeenCalledWith(sessionId);
559+expect(getFollowupQueueDepth(canonicalKey)).toBe(0);
560+expectSessionLaneCleared(canonicalKey);
561+expect(getAbortMemory(canonicalKey)).toBeUndefined();
562+});
563+564+it("fast-abort uses abort memory when no persisted target entry exists", async () => {
565+const sessionKey = "telegram:missing-persistence-target";
566+const { cfg } = await createAbortConfig();
567+abortTesting.setDepsForTests({
568+getAcpSessionManager: (() =>
569+({
570+resolveSession: acpManagerMocks.resolveSession,
571+cancelSession: acpManagerMocks.cancelSession,
572+}) as never) as never,
573+abortEmbeddedAgentRun: runtimeAbortMocks.abortEmbeddedAgentRun,
574+resolveActiveEmbeddedRunSessionId: runtimeAbortMocks.resolveActiveEmbeddedRunSessionId,
575+markSessionAbortTarget: vi.fn(async () => null),
576+resolveSessionAbortTarget: vi.fn(() => null),
577+getLatestSubagentRunByChildSessionKey:
578+subagentRegistryMocks.getLatestSubagentRunByChildSessionKey,
579+listSubagentRunsForController: subagentRegistryMocks.listSubagentRunsForRequester,
580+markSubagentRunTerminated: subagentRegistryMocks.markSubagentRunTerminated,
581+});
582+583+const result = await runStopCommand({
584+ cfg,
585+ sessionKey,
586+from: "telegram:123",
587+to: "telegram:123",
588+});
589+590+expect(result.handled).toBe(true);
591+expect(getAbortMemory(sessionKey)).toBe(true);
592+});
593+594+it("fast-abort does not wait for abort metadata persistence before stopping runs", async () => {
595+const sessionKey = "telegram:slow-persistence";
596+const childKey = "agent:main:subagent:slow-persistence-child";
597+const sessionId = "session-slow-persistence";
598+const childSessionId = "session-slow-persistence-child";
599+const { root, cfg } = await createAbortConfig({
600+sessionIdsByKey: {
601+[childKey]: childSessionId,
602+[sessionKey]: sessionId,
603+},
604+});
605+let finishPersistence: (() => void) | undefined;
606+const persistenceStarted = new Promise<void>((resolveStarted) => {
607+abortTesting.setDepsForTests({
608+getAcpSessionManager: (() =>
609+({
610+resolveSession: acpManagerMocks.resolveSession,
611+cancelSession: acpManagerMocks.cancelSession,
612+}) as never) as never,
613+abortEmbeddedAgentRun: runtimeAbortMocks.abortEmbeddedAgentRun,
614+resolveActiveEmbeddedRunSessionId: runtimeAbortMocks.resolveActiveEmbeddedRunSessionId,
615+markSessionAbortTarget: vi.fn(
616+() =>
617+new Promise<SessionAbortTargetResult | null>((resolvePersistence) => {
618+resolveStarted();
619+finishPersistence = () => {
620+resolvePersistence({
621+entry: {
622+ sessionId,
623+updatedAt: 10,
624+},
625+persisted: true,
626+ sessionId,
627+ sessionKey,
628+});
629+};
630+}),
631+),
632+resolveSessionAbortTarget: vi.fn(() => ({
633+entry: {
634+ sessionId,
635+updatedAt: 10,
636+},
637+ sessionId,
638+ sessionKey,
639+})),
640+getLatestSubagentRunByChildSessionKey:
641+subagentRegistryMocks.getLatestSubagentRunByChildSessionKey,
642+listSubagentRunsForController: subagentRegistryMocks.listSubagentRunsForRequester,
643+markSubagentRunTerminated: subagentRegistryMocks.markSubagentRunTerminated,
644+});
645+});
646+enqueueQueuedFollowupRun({ root, cfg, sessionId, sessionKey });
647+subagentRegistryMocks.listSubagentRunsForRequester.mockReturnValueOnce([
648+{
649+runId: "slow-child-run",
650+childSessionKey: childKey,
651+requesterSessionKey: sessionKey,
652+requesterDisplayKey: sessionKey,
653+task: "slow child",
654+cleanup: "keep",
655+createdAt: Date.now(),
656+},
657+]);
658+659+const resultPromise = runStopCommand({
660+ cfg,
661+ sessionKey,
662+from: "telegram:123",
663+to: "telegram:123",
664+});
665+await persistenceStarted;
666+667+expect(runtimeAbortMocks.abortEmbeddedAgentRun).toHaveBeenCalledWith(sessionId);
668+expect(runtimeAbortMocks.abortEmbeddedAgentRun).toHaveBeenCalledWith(childSessionId);
669+expect(subagentRegistryMocks.markSubagentRunTerminated).toHaveBeenCalledWith({
670+childSessionKey: childKey,
671+reason: "killed",
672+runId: "slow-child-run",
673+});
674+expect(getFollowupQueueDepth(sessionKey)).toBe(0);
675+expectSessionLaneCleared(sessionKey);
676+677+finishPersistence?.();
678+await expect(resultPromise).resolves.toMatchObject({
679+aborted: true,
680+handled: true,
681+});
682+});
683+476684it("plain-language stop on ACP-bound session triggers ACP cancel", async () => {
477685const sessionKey = "agent:codex:acp:test-1";
478686const sessionId = "session-123";
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。