

























@@ -532,6 +532,79 @@ describe("tui-event-handlers: handleAgentEvent", () => {
532532expect(chatLog.updateAssistant).toHaveBeenLastCalledWith("continued", "run-active");
533533});
534534535+it("clears stale streaming when an orphan final arrives and no tracked run remains", () => {
536+const { state, setActivityStatus, handleChatEvent } = createHandlersHarness({
537+state: { activeChatRunId: "run-stale", activityStatus: "streaming" },
538+});
539+540+handleChatEvent({
541+runId: "run-orphan",
542+sessionKey: state.currentSessionKey,
543+state: "final",
544+message: { content: [{ type: "text", text: "done" }] },
545+});
546+547+expect(state.activeChatRunId).toBeNull();
548+expect(setActivityStatus).toHaveBeenCalledWith("idle");
549+});
550+551+it("flushes deferred history reload after stale streaming clear makes the TUI idle", () => {
552+const { state, loadHistory, noteLocalRunId, setActivityStatus, handleChatEvent } =
553+createHandlersHarness({
554+state: { activeChatRunId: "run-stale", activityStatus: "streaming" },
555+});
556+557+noteLocalRunId("run-local-empty");
558+loadHistory.mockImplementation(() => {
559+expect(state.activeChatRunId).toBeNull();
560+expect(state.activityStatus).toBe("idle");
561+});
562+563+handleChatEvent({
564+runId: "run-local-empty",
565+sessionKey: state.currentSessionKey,
566+state: "final",
567+});
568+569+expect(state.activeChatRunId).toBeNull();
570+expect(state.activityStatus).toBe("idle");
571+expect(setActivityStatus).toHaveBeenCalledWith("idle");
572+expect(loadHistory).toHaveBeenCalledTimes(1);
573+});
574+575+it("does not surface inactive orphan final failures as the global status", () => {
576+const { state, setActivityStatus, handleChatEvent } = createHandlersHarness({
577+state: { activeChatRunId: "run-stale", activityStatus: "streaming" },
578+});
579+580+handleChatEvent({
581+runId: "run-orphan-error",
582+sessionKey: state.currentSessionKey,
583+state: "final",
584+message: { content: [{ type: "text", text: "failed" }], stopReason: "error" },
585+});
586+587+expect(state.activeChatRunId).toBeNull();
588+expect(setActivityStatus).toHaveBeenCalledWith("idle");
589+expect(setActivityStatus).not.toHaveBeenCalledWith("error");
590+});
591+592+it("does not force idle for an inactive final while another tracked run is active", () => {
593+const { state, setActivityStatus, handleChatEvent } = createConcurrentRunHarness("partial");
594+state.activityStatus = "streaming";
595+setActivityStatus.mockClear();
596+597+handleChatEvent({
598+runId: "run-other",
599+sessionKey: state.currentSessionKey,
600+state: "final",
601+message: { content: [{ type: "text", text: "other final" }] },
602+});
603+604+expect(state.activeChatRunId).toBe("run-active");
605+expect(setActivityStatus).not.toHaveBeenCalledWith("idle");
606+});
607+535608it("suppresses non-local empty final placeholders during concurrent runs", () => {
536609const { state, chatLog, loadHistory, handleChatEvent } =
537610createConcurrentRunHarness("local stream");
@@ -715,15 +788,24 @@ describe("tui-event-handlers: streaming watchdog", () => {
715788const btw = createMockBtwPresenter();
716789const tui = { requestRender: vi.fn() } as unknown as MockTui & HandlerTui;
717790const setActivityStatus = vi.fn();
791+const loadHistory = vi.fn();
792+const localRunIds = new Set<string>();
793+const noteLocalRunId = (runId: string) => {
794+localRunIds.add(runId);
795+};
718796const handlers = createEventHandlers({
719797 chatLog,
720798 btw,
721799 tui,
722800 state,
723801 setActivityStatus,
802+ loadHistory,
803+ noteLocalRunId,
804+isLocalRunId: localRunIds.has.bind(localRunIds),
805+forgetLocalRunId: localRunIds.delete.bind(localRunIds),
724806streamingWatchdogMs: options?.streamingWatchdogMs,
725807});
726-return { state, chatLog, tui, setActivityStatus, handlers };
808+return { state, chatLog, tui, setActivityStatus, loadHistory, noteLocalRunId, handlers };
727809};
728810729811it("resets activityStatus to idle when no stream delta arrives for the watchdog window", () => {
@@ -750,6 +832,37 @@ describe("tui-event-handlers: streaming watchdog", () => {
750832handlers.dispose?.();
751833});
752834835+it("flushes a deferred history reload when the watchdog clears the active run", () => {
836+const { state, loadHistory, noteLocalRunId, setActivityStatus, handlers } = createHarness({
837+streamingWatchdogMs: 5_000,
838+});
839+840+handlers.handleChatEvent({
841+runId: "run-stuck",
842+sessionKey: state.currentSessionKey,
843+state: "delta",
844+message: { content: "hello" },
845+} satisfies ChatEvent);
846+847+noteLocalRunId("run-local-empty");
848+handlers.handleChatEvent({
849+runId: "run-local-empty",
850+sessionKey: state.currentSessionKey,
851+state: "final",
852+} satisfies ChatEvent);
853+854+expect(loadHistory).not.toHaveBeenCalled();
855+856+vi.advanceTimersByTime(5_001);
857+858+expect(state.activeChatRunId).toBeNull();
859+expect(state.activityStatus).toBe("idle");
860+expect(setActivityStatus).toHaveBeenLastCalledWith("idle");
861+expect(loadHistory).toHaveBeenCalledTimes(1);
862+863+handlers.dispose?.();
864+});
865+753866it("refreshes the watchdog window on each new stream delta", () => {
754867const { state, setActivityStatus, handlers } = createHarness({
755868streamingWatchdogMs: 5_000,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。