



















@@ -1,16 +1,18 @@
11// @vitest-environment node
2-import { describe, expect, it, vi } from "vitest";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
33const {
44 refreshChatMock,
55 refreshChatAvatarMock,
66 refreshSlashCommandsMock,
77 loadChatHistoryMock,
8+ createSessionAndRefreshMock,
89 loadSessionsMock,
910} = vi.hoisted(() => ({
1011refreshChatMock: vi.fn(),
1112refreshChatAvatarMock: vi.fn(),
1213refreshSlashCommandsMock: vi.fn(),
1314loadChatHistoryMock: vi.fn(),
15+createSessionAndRefreshMock: vi.fn(),
1416loadSessionsMock: vi.fn(),
1517}));
1618@@ -28,10 +30,12 @@ vi.mock("./controllers/chat.ts", () => ({
2830}));
29313032vi.mock("./controllers/sessions.ts", () => ({
33+createSessionAndRefresh: createSessionAndRefreshMock,
3134loadSessions: loadSessionsMock,
3235}));
33363437import {
38+createChatSession,
3539isCronSessionKey,
3640parseSessionKey,
3741resolveAssistantAttachmentAuthToken,
@@ -44,6 +48,15 @@ import type { SessionsListResult } from "./types.ts";
44484549type SessionRow = SessionsListResult["sessions"][number];
465051+beforeEach(() => {
52+refreshChatMock.mockReset();
53+refreshChatAvatarMock.mockReset();
54+refreshSlashCommandsMock.mockReset();
55+loadChatHistoryMock.mockReset();
56+createSessionAndRefreshMock.mockReset();
57+loadSessionsMock.mockReset();
58+});
59+4760function row(overrides: Partial<SessionRow> & { key: string }): SessionRow {
4861return { kind: "direct", updatedAt: 0, ...overrides };
4962}
@@ -90,6 +103,55 @@ function createSettings(): AppViewState["settings"] {
90103};
91104}
92105106+function createChatSessionState(overrides: Partial<AppViewState> = {}) {
107+const settings = createSettings();
108+const state = {
109+sessionKey: "agent:ops:main",
110+chatMessage: "draft prompt",
111+chatAttachments: [{ id: "att-1", mimeType: "image/png", dataUrl: "data:image/png;base64,AAA" }],
112+chatMessages: [{ role: "assistant", content: "old" }],
113+chatToolMessages: [{ id: "tool-1" }],
114+chatStreamSegments: [],
115+chatThinkingLevel: null,
116+chatStream: null,
117+chatSideResult: null,
118+lastError: null,
119+compactionStatus: null,
120+fallbackStatus: null,
121+chatAvatarUrl: null,
122+chatAvatarSource: null,
123+chatAvatarStatus: null,
124+chatAvatarReason: null,
125+chatQueue: [],
126+chatRunId: null,
127+chatSending: false,
128+chatLoading: false,
129+chatSideResultTerminalRuns: new Set<string>(),
130+chatStreamStartedAt: null,
131+connected: true,
132+client: { request: vi.fn() },
133+sessionsLoading: false,
134+sessionsError: null,
135+sessionsResult: {
136+ts: 0,
137+path: "",
138+count: 1,
139+defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
140+sessions: [row({ key: "agent:ops:main" })],
141+},
142+ settings,
143+applySettings(next: typeof settings) {
144+state.settings = next;
145+},
146+loadAssistantIdentity: vi.fn(),
147+resetToolStream: vi.fn(),
148+resetChatScroll: vi.fn(),
149+resetChatInputHistoryNavigation: vi.fn(),
150+ ...overrides,
151+} as unknown as AppViewState;
152+return state;
153+}
154+93155/* ================================================================
94156 * parseSessionKey – low-level key → type / fallback mapping
95157 * ================================================================ */
@@ -493,6 +555,145 @@ describe("resolveSessionOptionGroups", () => {
493555});
494556});
495557558+describe("createChatSession", () => {
559+it("creates a dashboard session, switches to it, and preserves the current composer", async () => {
560+const state = createChatSessionState();
561+createSessionAndRefreshMock.mockResolvedValue("agent:ops:dashboard:new-chat");
562+refreshChatAvatarMock.mockResolvedValue(undefined);
563+refreshSlashCommandsMock.mockResolvedValue(undefined);
564+loadChatHistoryMock.mockResolvedValue(undefined);
565+loadSessionsMock.mockResolvedValue(undefined);
566+567+await createChatSession(state);
568+569+expect(createSessionAndRefreshMock).toHaveBeenCalledWith(
570+state,
571+{
572+agentId: "ops",
573+parentSessionKey: "agent:ops:main",
574+},
575+{
576+activeMinutes: 0,
577+limit: 0,
578+includeGlobal: true,
579+includeUnknown: true,
580+},
581+);
582+expect(state.sessionKey).toBe("agent:ops:dashboard:new-chat");
583+expect(state.settings.sessionKey).toBe("agent:ops:dashboard:new-chat");
584+expect(state.chatMessage).toBe("draft prompt");
585+expect(state.chatAttachments).toEqual([
586+{ id: "att-1", mimeType: "image/png", dataUrl: "data:image/png;base64,AAA" },
587+]);
588+expect(state.chatMessages).toEqual([]);
589+expect(loadChatHistoryMock).toHaveBeenCalledWith(state);
590+});
591+592+it("preserves draft and attachment edits made while session creation is in flight", async () => {
593+const state = createChatSessionState();
594+const updatedAttachments = [
595+{ id: "att-2", mimeType: "image/png", dataUrl: "data:image/png;base64,BBB" },
596+];
597+createSessionAndRefreshMock.mockImplementation(async () => {
598+state.chatMessage = "updated draft";
599+state.chatAttachments = updatedAttachments;
600+return "agent:ops:dashboard:new-chat";
601+});
602+refreshChatAvatarMock.mockResolvedValue(undefined);
603+refreshSlashCommandsMock.mockResolvedValue(undefined);
604+loadChatHistoryMock.mockResolvedValue(undefined);
605+loadSessionsMock.mockResolvedValue(undefined);
606+607+await createChatSession(state);
608+609+expect(state.sessionKey).toBe("agent:ops:dashboard:new-chat");
610+expect(state.chatMessage).toBe("updated draft");
611+expect(state.chatAttachments).toBe(updatedAttachments);
612+expect(loadChatHistoryMock).toHaveBeenCalledWith(state);
613+});
614+615+it("ignores a stale create response after the active session changes", async () => {
616+const state = createChatSessionState();
617+createSessionAndRefreshMock.mockImplementation(async () => {
618+state.sessionKey = "agent:ops:other";
619+return "agent:ops:dashboard:new-chat";
620+});
621+622+await createChatSession(state);
623+624+expect(state.sessionKey).toBe("agent:ops:other");
625+expect(state.chatMessage).toBe("draft prompt");
626+expect(state.chatMessages).toEqual([{ role: "assistant", content: "old" }]);
627+expect(loadChatHistoryMock).not.toHaveBeenCalled();
628+});
629+630+it("does not create or switch while a run is active", async () => {
631+const state = createChatSessionState({
632+chatRunId: "run-1",
633+chatQueue: [{ id: "queued-1", text: "follow up", createdAt: 1 }],
634+});
635+636+await createChatSession(state);
637+638+expect(createSessionAndRefreshMock).not.toHaveBeenCalled();
639+expect(state.sessionKey).toBe("agent:ops:main");
640+expect(state.chatMessage).toBe("draft prompt");
641+expect(state.chatQueue).toEqual([{ id: "queued-1", text: "follow up", createdAt: 1 }]);
642+expect(state.lastError).toBe(
643+"Start a new session after the active run or queued messages finish.",
644+);
645+});
646+647+it("shows feedback instead of clearing errors when session loading blocks creation", async () => {
648+const state = createChatSessionState({
649+sessionsLoading: true,
650+lastError: "previous error",
651+});
652+653+await createChatSession(state);
654+655+expect(createSessionAndRefreshMock).not.toHaveBeenCalled();
656+expect(state.sessionKey).toBe("agent:ops:main");
657+expect(state.chatMessage).toBe("draft prompt");
658+expect(state.lastError).toBe(
659+"Session list is still refreshing. Try New Chat again in a moment.",
660+);
661+});
662+663+it("shows creation failure feedback when creation is skipped without a session error", async () => {
664+const state = createChatSessionState({ lastError: "previous error" });
665+createSessionAndRefreshMock.mockResolvedValue(null);
666+667+await createChatSession(state);
668+669+expect(createSessionAndRefreshMock).toHaveBeenCalledTimes(1);
670+expect(state.sessionKey).toBe("agent:ops:main");
671+expect(state.chatMessage).toBe("draft prompt");
672+expect(state.sessionsError).toBeNull();
673+expect(state.lastError).toBe("New Chat could not create a new session. Try again in a moment.");
674+expect(loadChatHistoryMock).not.toHaveBeenCalled();
675+});
676+677+it("keeps refresh feedback when a queued session refresh skips creation", async () => {
678+const state = createChatSessionState({ lastError: "previous error" });
679+createSessionAndRefreshMock.mockImplementation(async () => {
680+state.sessionsLoading = true;
681+return null;
682+});
683+684+await createChatSession(state);
685+686+expect(createSessionAndRefreshMock).toHaveBeenCalledTimes(1);
687+expect(state.sessionKey).toBe("agent:ops:main");
688+expect(state.chatMessage).toBe("draft prompt");
689+expect(state.sessionsError).toBeNull();
690+expect(state.lastError).toBe(
691+"Session list is still refreshing. Try New Chat again in a moment.",
692+);
693+expect(loadChatHistoryMock).not.toHaveBeenCalled();
694+});
695+});
696+496697describe("switchChatSession", () => {
497698it("refreshes the chat avatar after clearing session-scoped state", async () => {
498699const settings = createSettings();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。