























@@ -15,6 +15,8 @@ type SelectableOverlay = {
1515};
1616type SetActivityStatusMock = ReturnType<typeof vi.fn> & ((text: string) => void);
1717type SetSessionMock = ReturnType<typeof vi.fn> & ((key: string) => Promise<void>);
18+type ConsumeCompletedRunMock = ReturnType<typeof vi.fn> & ((runId: string) => boolean);
19+type FlushPendingHistoryRefreshMock = ReturnType<typeof vi.fn> & (() => void);
18201921async function flushAsyncSelect() {
2022await new Promise<void>((resolve) => setImmediate(resolve));
@@ -81,6 +83,8 @@ function createHarness(params?: {
8183currentAgentId?: string;
8284currentSessionKey?: string;
8385abortActive?: AbortActiveMock;
86+consumeCompletedRunForPendingSend?: ConsumeCompletedRunMock;
87+flushPendingHistoryRefreshIfIdle?: FlushPendingHistoryRefreshMock;
8488}) {
8589const sendChat = params?.sendChat ?? vi.fn().mockResolvedValue({ runId: "r1" });
8690const getGatewayStatus = params?.getGatewayStatus ?? vi.fn().mockResolvedValue({});
@@ -101,6 +105,7 @@ function createHarness(params?: {
101105const refreshSessionInfo = params?.refreshSessionInfo ?? vi.fn().mockResolvedValue(undefined);
102106const applySessionInfoFromPatch = params?.applySessionInfoFromPatch ?? vi.fn();
103107const setActivityStatus = params?.setActivityStatus ?? (vi.fn() as SetActivityStatusMock);
108+const forgetLocalRunId = vi.fn();
104109const openOverlay = vi.fn();
105110const closeOverlay = vi.fn();
106111const requestExit = vi.fn();
@@ -150,8 +155,10 @@ function createHarness(params?: {
150155applySessionInfoFromPatch: applySessionInfoFromPatch as never,
151156 noteLocalRunId,
152157 noteLocalBtwRunId,
153-forgetLocalRunId: vi.fn(),
158+ forgetLocalRunId,
154159forgetLocalBtwRunId: vi.fn(),
160+consumeCompletedRunForPendingSend: params?.consumeCompletedRunForPendingSend,
161+flushPendingHistoryRefreshIfIdle: params?.flushPendingHistoryRefreshIfIdle,
155162 runAuthFlow,
156163 requestExit,
157164});
@@ -180,6 +187,7 @@ function createHarness(params?: {
180187 setActivityStatus,
181188 noteLocalRunId,
182189 noteLocalBtwRunId,
190+ forgetLocalRunId,
183191 requestExit,
184192 abortActive,
185193 state,
@@ -508,18 +516,21 @@ describe("tui command handlers", () => {
508516expect(addSystem).toHaveBeenCalledWith("agent set to work; use /crestodian to return");
509517});
510518511-it("defers local run binding until gateway events provide a real run id", async () => {
512-const { handleCommand, noteLocalRunId, state } = createHarness();
519+it("marks the generated runId as local before gateway events arrive", async () => {
520+const { handleCommand, sendChat, noteLocalRunId, state } = createHarness();
513521514522await handleCommand("/context detail");
515523516-expect(noteLocalRunId).not.toHaveBeenCalled();
524+const sentRunId = (firstMockArg(sendChat, "sendChat") as { runId: string }).runId;
525+expect(noteLocalRunId).toHaveBeenCalledWith(sentRunId);
517526expect(state.activeChatRunId).toBeNull();
518527expect(state.pendingOptimisticUserMessage).toBe(true);
519528});
520529521530it("tracks the in-flight runId so escape can abort during the wait", async () => {
522-const sendChat = vi.fn().mockResolvedValue({ runId: "ignored" });
531+const sendChat = vi.fn().mockImplementation(async (opts: { runId: string }) => ({
532+runId: opts.runId,
533+}));
523534const { handleCommand, state } = createHarness({ sendChat });
524535525536await handleCommand("hello");
@@ -531,6 +542,55 @@ describe("tui command handlers", () => {
531542expect(state.pendingChatRunId).toBe(sentRunId);
532543});
533544545+it("does not reintroduce the pending runId when an early event already consumed it", async () => {
546+const sendChat = vi.fn();
547+const { handleCommand, state } = createHarness({ sendChat });
548+sendChat.mockImplementation(async (opts: { runId: string }) => {
549+state.pendingOptimisticUserMessage = false;
550+return { runId: opts.runId };
551+});
552+553+await handleCommand("hello");
554+555+expect(state.pendingChatRunId).toBeNull();
556+expect(state.pendingOptimisticUserMessage).toBe(false);
557+});
558+559+it("tracks the backend-accepted runId when it differs from the generated runId", async () => {
560+const sendChat = vi.fn().mockResolvedValue({ runId: "run-accepted" });
561+const { handleCommand, state, noteLocalRunId, forgetLocalRunId } = createHarness({ sendChat });
562+563+await handleCommand("hello");
564+565+const sentRunId = (firstMockArg(sendChat, "sendChat") as { runId: string }).runId;
566+expect(state.pendingChatRunId).toBe("run-accepted");
567+expect(forgetLocalRunId).toHaveBeenCalledWith(sentRunId);
568+expect(noteLocalRunId).toHaveBeenCalledWith("run-accepted");
569+});
570+571+it("does not reintroduce a backend-accepted runId after an early terminal event", async () => {
572+const sendChat = vi.fn().mockResolvedValue({ runId: "run-accepted" });
573+const consumeCompletedRunForPendingSend = vi.fn((runId: string) => runId === "run-accepted");
574+const flushPendingHistoryRefreshIfIdle = vi.fn();
575+const { handleCommand, state, noteLocalRunId, forgetLocalRunId, setActivityStatus } =
576+createHarness({
577+ sendChat,
578+ consumeCompletedRunForPendingSend,
579+ flushPendingHistoryRefreshIfIdle,
580+});
581+582+await handleCommand("hello");
583+584+const sentRunId = (firstMockArg(sendChat, "sendChat") as { runId: string }).runId;
585+expect(consumeCompletedRunForPendingSend).toHaveBeenCalledWith("run-accepted");
586+expect(forgetLocalRunId).toHaveBeenCalledWith(sentRunId);
587+expect(noteLocalRunId).not.toHaveBeenCalledWith("run-accepted");
588+expect(state.pendingChatRunId).toBeNull();
589+expect(state.pendingOptimisticUserMessage).toBe(false);
590+expect(setActivityStatus).toHaveBeenCalledWith("idle");
591+expect(flushPendingHistoryRefreshIfIdle).toHaveBeenCalledTimes(1);
592+});
593+534594it("clears the pending runId if sendChat fails", async () => {
535595const sendChat = vi.fn().mockRejectedValue(new Error("boom"));
536596const { handleCommand, state } = createHarness({ sendChat });
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。