diff --git a/src/features/chat/application/conversation/composer-submit-actions.ts b/src/features/chat/application/conversation/composer-submit-actions.ts index b8092882..260ec7d0 100644 --- a/src/features/chat/application/conversation/composer-submit-actions.ts +++ b/src/features/chat/application/conversation/composer-submit-actions.ts @@ -22,7 +22,7 @@ export interface ComposerSubmitActionsHost { sendTurnText(text: string, codexInputOverride?: CodexInput, referencedThread?: ReferencedThreadMetadata): Promise; }; connection: { - ensureConnected: () => Promise; + connectedClient: () => Promise; currentClient: () => AppServerClient | null; }; status: { @@ -52,11 +52,9 @@ async function sendMessage(host: ComposerSubmitActionsHost): Promise { const text = host.composer.trimmedDraft; if (!text) return; - await host.connection.ensureConnected(); - if (!host.connection.currentClient()) return; - const slashCommand = parseSlashCommand(text); if (slashCommand) { + if (!(await host.connection.connectedClient())) return; host.composer.setDraft("", { clearSuggestions: true }); const result = await host.slashCommandExecutor.execute(slashCommand.command, slashCommand.args); if (result?.composerDraft !== undefined) { diff --git a/src/features/chat/application/conversation/composition.ts b/src/features/chat/application/conversation/composition.ts index 2feab87d..770642cc 100644 --- a/src/features/chat/application/conversation/composition.ts +++ b/src/features/chat/application/conversation/composition.ts @@ -19,7 +19,7 @@ export interface ConversationTurnActionsContext { localItemIds: LocalIdSource; client: { currentClient: () => AppServerClient | null; - ensureConnected: () => Promise; + connectedClient: () => Promise; }; status: { set: (status: string) => void; @@ -64,8 +64,7 @@ interface ConversationThreadStarter { export interface PlanImplementationHost { stateStore: ChatStateStore; - currentClient(): AppServerClient | null; - ensureConnected(): Promise; + connectedClient(): Promise; sendTurnText(text: string): Promise; requestDefaultCollaborationModeForNextTurn(): void; } @@ -88,7 +87,7 @@ export function createConversationTurnActions( stateStore, vaultPath, localItemIds, - currentClient: client.currentClient, + connectedClient: client.connectedClient, ensureRestoredThreadLoaded: thread.ensureRestoredThreadLoaded, startThread: (preview) => refs.threadStarter.startThread(preview), notifyActiveThreadIdentityChanged: thread.notifyIdentityChanged, @@ -121,8 +120,7 @@ export function createConversationTurnActions( }; const planImplementationHost: PlanImplementationHost = { stateStore, - currentClient: client.currentClient, - ensureConnected: client.ensureConnected, + connectedClient: client.connectedClient, sendTurnText: (text) => turnSubmission.sendTurnText(text), requestDefaultCollaborationModeForNextTurn: () => { refs.runtimeSettings.requestDefaultCollaborationModeForNextTurn(); @@ -141,8 +139,8 @@ export function createConversationTurnActions( }, turnSubmission, connection: { + connectedClient: client.connectedClient, currentClient: client.currentClient, - ensureConnected: client.ensureConnected, }, status: { setStatus: status.set, @@ -168,8 +166,7 @@ async function startThreadForGoal(starter: ConversationThreadStarter, objective: export async function implementPlan(host: PlanImplementationHost, itemId: string): Promise { if (!canImplementPlanItemId(host.stateStore.getState(), itemId)) return; - await host.ensureConnected(); - if (!host.currentClient() || !activeThreadId(host.stateStore.getState())) return; + if (!(await host.connectedClient()) || !activeThreadId(host.stateStore.getState())) return; host.requestDefaultCollaborationModeForNextTurn(); host.stateStore.dispatch({ type: "ui/panel-set", panel: null }); diff --git a/src/features/chat/application/conversation/turn-submission-actions.ts b/src/features/chat/application/conversation/turn-submission-actions.ts index 63e297f4..3da78533 100644 --- a/src/features/chat/application/conversation/turn-submission-actions.ts +++ b/src/features/chat/application/conversation/turn-submission-actions.ts @@ -19,7 +19,7 @@ export interface TurnSubmissionActionsHost { stateStore: ChatStateStore; vaultPath: string; localItemIds: LocalIdSource; - currentClient: () => AppServerClient | null; + connectedClient: () => Promise; ensureRestoredThreadLoaded: () => Promise; startThread: (preview?: string) => Promise; notifyActiveThreadIdentityChanged: () => void; @@ -61,9 +61,9 @@ async function sendTurnText( codexInputOverride?: CodexInput, referencedThread?: ReferencedThreadMetadata, ): Promise { - if (!(await host.ensureRestoredThreadLoaded())) return; - const client = host.currentClient(); + const client = await host.connectedClient(); if (!client) return; + if (!(await host.ensureRestoredThreadLoaded())) return; const initialState = submissionStateSnapshot(host.stateStore.getState()); const plan = planTurnSubmission(initialState); diff --git a/src/features/chat/application/threads/goal-actions.ts b/src/features/chat/application/threads/goal-actions.ts index 9ac5e329..5257a2c4 100644 --- a/src/features/chat/application/threads/goal-actions.ts +++ b/src/features/chat/application/threads/goal-actions.ts @@ -16,7 +16,7 @@ export interface ThreadGoalSyncHost { } export interface GoalActionsHost extends ThreadGoalSyncHost { - ensureConnected: () => Promise; + connectedClient: () => Promise; startThread: (preview?: string, options?: { syncGoal?: boolean }) => Promise<{ threadId: string } | null>; } @@ -125,8 +125,7 @@ function setGoalStatus(host: GoalActionsHost, threadId: string, status: ThreadGo } async function clearGoal(host: GoalActionsHost, threadId: string): Promise { - await host.ensureConnected(); - const client = host.currentClient(); + const client = await host.connectedClient(); if (!client) return false; try { await client.clearThreadGoal(threadId); @@ -139,8 +138,7 @@ async function clearGoal(host: GoalActionsHost, threadId: string): Promise { - await host.ensureConnected(); - const client = host.currentClient(); + const client = await host.connectedClient(); if (!client) return false; try { return applyGoalIfActive(host, threadId, await setThreadGoal(client, threadId, params), { reportChange: true }); @@ -193,7 +191,8 @@ async function startThreadAndSaveObjective( plan: Extract, ): Promise { try { - await host.ensureConnected(); + const client = await host.connectedClient(); + if (!client) return false; const response = await host.startThread(plan.objective, { syncGoal: false }); const threadId = response?.threadId ?? null; return threadId ? await setObjective(host, threadId, plan.objective, plan.tokenBudget) : false; diff --git a/src/features/chat/application/threads/thread-management-actions.ts b/src/features/chat/application/threads/thread-management-actions.ts index ea5e909d..c93a5704 100644 --- a/src/features/chat/application/threads/thread-management-actions.ts +++ b/src/features/chat/application/threads/thread-management-actions.ts @@ -17,7 +17,7 @@ export interface ThreadManagementActionsHost { stateStore: ChatStateStore; vaultPath: string; operations: ThreadOperations; - ensureConnected: () => Promise; + connectedClient: () => Promise; currentClient: () => AppServerClient | null; addSystemMessage: (text: string) => void; setStatus: (status: string) => void; @@ -101,8 +101,7 @@ async function compactActiveThread(host: ThreadManagementActionsHost): Promise { - await host.ensureConnected(); - const client = host.currentClient(); + const client = await host.connectedClient(); if (!client) return; const initialActiveThreadId = threadManagementState(host).activeThread.id; try { @@ -148,8 +147,7 @@ async function forkThreadFromTurn( host.addSystemMessage(finishBeforeForkingThreadsMessage()); return; } - await host.ensureConnected(); - const client = host.currentClient(); + const client = await host.connectedClient(); if (!client) return; const initialActiveThreadId = threadManagementState(host).activeThread.id; @@ -215,8 +213,7 @@ async function rollbackThread(host: ThreadManagementActionsHost, threadId: strin host.addSystemMessage(interruptBeforeRollbackMessage()); return; } - await host.ensureConnected(); - const client = host.currentClient(); + const client = await host.connectedClient(); if (!client) return; const candidate = messageStreamRollbackCandidate(threadManagementState(host).messageStream); diff --git a/src/features/chat/host/session-graph.ts b/src/features/chat/host/session-graph.ts index dd44669b..9984ff93 100644 --- a/src/features/chat/host/session-graph.ts +++ b/src/features/chat/host/session-graph.ts @@ -170,6 +170,7 @@ interface ChatPanelComposerAndTurnInput { connection: ConnectionManager; localItemIds: LocalIdSource; ensureConnected: () => Promise; + connectedClient: () => Promise>; currentClient: CurrentAppServerClient; status: ChatPanelSessionStatus; inboundHandler: ChatInboundHandler; @@ -256,10 +257,14 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch const connectionController = controller; const { threads: serverThreads } = connectionBundle.serverActions; const ensureConnected = () => connectionController.ensureConnected(); + const connectedClient = async () => { + await ensureConnected(); + return currentClient(); + }; const refreshActiveThreads = () => connectionController.refreshActiveThreads(); const threadOperations = createSessionThreadOperations(environment, currentClient); const runtimeSettings = createSessionRuntimeSettingsActions(host, currentClient, status); - const goals = createSessionGoalActions(host, currentClient, localItemIds, ensureConnected, status, serverThreads); + const goals = createSessionGoalActions(host, currentClient, localItemIds, connectedClient, status, serverThreads); const rename = createSessionThreadRenameEditorActions(stateStore, threadOperations, titleService, ensureConnected, status); const threadLifecycle = createSessionThreadLifecycle( host, @@ -286,7 +291,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch }; const threadActionParts = createThreadActionParts(host, { operations: threadOperations, - ensureConnected, + connectedClient, currentClient, status, composerController, @@ -297,6 +302,7 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch connection, localItemIds, ensureConnected, + connectedClient, currentClient, status, inboundHandler, @@ -579,7 +585,7 @@ function createSessionGoalActions( host: ChatPanelSessionGraphHost, currentClient: CurrentAppServerClient, localItemIds: LocalIdSource, - ensureConnected: () => Promise, + connectedClient: () => Promise>, status: ChatPanelSessionStatus, serverThreads: ChatServerThreadActions, ): ChatPanelGoalActions { @@ -587,7 +593,7 @@ function createSessionGoalActions( stateStore: host.stateStore, currentClient, localItemIds, - ensureConnected, + connectedClient, startThread: (preview, options) => serverThreads.startThread(preview, options), addSystemMessage: (text) => { status.addSystemMessage(text); @@ -719,7 +725,7 @@ function createThreadActionParts( host: ChatPanelSessionGraphHost, input: { operations: ThreadOperations; - ensureConnected: () => Promise; + connectedClient: () => Promise>; currentClient: CurrentAppServerClient; status: ChatPanelSessionStatus; composerController: ChatComposerController; @@ -727,13 +733,13 @@ function createThreadActionParts( refreshActiveThreads: () => Promise; }, ): ChatPanelThreadActionParts { - const { operations, ensureConnected, currentClient, status, composerController, resume, refreshActiveThreads } = input; + const { operations, connectedClient, currentClient, status, composerController, resume, refreshActiveThreads } = input; const { environment, stateStore } = host; const threadManagementHost: ThreadManagementActionsHost = { stateStore, vaultPath: environment.plugin.settingsRef.vaultPath, operations, - ensureConnected, + connectedClient, currentClient, addSystemMessage: status.addSystemMessage, setStatus: status.set, @@ -777,6 +783,7 @@ function createComposerAndTurnActions( connection, localItemIds, ensureConnected, + connectedClient, currentClient, status, inboundHandler, @@ -831,7 +838,7 @@ function createComposerAndTurnActions( localItemIds, client: { currentClient, - ensureConnected, + connectedClient, }, status, runtime: { diff --git a/tests/features/chat/conversation/turns/composer-submit-actions.test.ts b/tests/features/chat/conversation/turns/composer-submit-actions.test.ts index 1457b1c7..4a999022 100644 --- a/tests/features/chat/conversation/turns/composer-submit-actions.test.ts +++ b/tests/features/chat/conversation/turns/composer-submit-actions.test.ts @@ -25,6 +25,7 @@ function createHost(draft: string) { const sendTurnText = vi.fn().mockResolvedValue(undefined); const execute = vi.fn().mockResolvedValue(undefined); const followBottom = vi.fn(); + const connectedClient = vi.fn().mockResolvedValue(client); const host = { stateStore, composer: { @@ -36,8 +37,8 @@ function createHost(draft: string) { slashCommandExecutor: { execute }, turnSubmission: { sendTurnText }, connection: { + connectedClient, currentClient: () => client, - ensureConnected: vi.fn().mockResolvedValue(undefined), }, status: { setStatus: vi.fn(), @@ -45,16 +46,17 @@ function createHost(draft: string) { }, scroll: { followBottom }, }; - return { host, execute, followBottom, interruptTurn, sendTurnText, setDraft, stateStore }; + return { host, connectedClient, execute, followBottom, interruptTurn, sendTurnText, setDraft, stateStore }; } describe("submitComposer", () => { it("sends plain drafts as turn text", async () => { - const { host, followBottom, sendTurnText } = createHost("hello"); + const { host, connectedClient, followBottom, sendTurnText } = createHost("hello"); await submitComposer(host); expect(followBottom).toHaveBeenCalledOnce(); + expect(connectedClient).not.toHaveBeenCalled(); expect(sendTurnText).toHaveBeenCalledWith("hello"); const [followBottomOrder] = followBottom.mock.invocationCallOrder; const [sendTurnTextOrder] = sendTurnText.mock.invocationCallOrder; @@ -65,24 +67,26 @@ describe("submitComposer", () => { }); it("executes slash commands and forwards command send results", async () => { - const { host, execute, followBottom, sendTurnText, setDraft } = createHost("/clear hello"); + const { host, connectedClient, execute, followBottom, sendTurnText, setDraft } = createHost("/clear hello"); execute.mockResolvedValue({ sendText: "hello" }); await submitComposer(host); expect(setDraft).toHaveBeenCalledWith("", { clearSuggestions: true }); + expect(connectedClient).toHaveBeenCalledOnce(); expect(execute).toHaveBeenCalledWith("clear", "hello"); expect(followBottom).toHaveBeenCalledOnce(); expect(sendTurnText).toHaveBeenCalledWith("hello", undefined, undefined); }); it("restores slash command composer drafts from command results", async () => { - const { host, execute, followBottom, sendTurnText, setDraft } = createHost("/goal edit"); + const { host, connectedClient, execute, followBottom, sendTurnText, setDraft } = createHost("/goal edit"); execute.mockResolvedValue({ composerDraft: "/goal set Current objective" }); await submitComposer(host); expect(setDraft).toHaveBeenCalledWith("", { clearSuggestions: true }); + expect(connectedClient).toHaveBeenCalledOnce(); expect(setDraft).toHaveBeenCalledWith("/goal set Current objective", { focus: true, clearSuggestions: true }); expect(followBottom).not.toHaveBeenCalled(); expect(sendTurnText).not.toHaveBeenCalled(); diff --git a/tests/features/chat/conversation/turns/plan-implementation.test.ts b/tests/features/chat/conversation/turns/plan-implementation.test.ts index aeedcc86..2a6a08fa 100644 --- a/tests/features/chat/conversation/turns/plan-implementation.test.ts +++ b/tests/features/chat/conversation/turns/plan-implementation.test.ts @@ -43,20 +43,19 @@ function resumeThread(stateStore: ChatStateStore, items: readonly MessageStreamI function createController({ client = {} as AppServerClient } = {}) { const stateStore = createChatStateStore(createChatState()); - const ensureConnected = vi.fn().mockResolvedValue(undefined); + const connectedClient = vi.fn().mockResolvedValue(client); const sendTurnText = vi.fn().mockResolvedValue(undefined); const requestDefaultCollaborationModeForNextTurn = vi.fn(() => { stateStore.dispatch({ type: "runtime/requested-collaboration-mode-set", collaborationMode: "default" }); }); const host: PlanImplementationHost = { stateStore, - currentClient: () => client, - ensureConnected, + connectedClient, sendTurnText, requestDefaultCollaborationModeForNextTurn, }; return { - ensureConnected, + connectedClient, host, requestDefaultCollaborationModeForNextTurn, sendTurnText, @@ -88,14 +87,14 @@ describe("implementPlan", () => { }); it("switches out of plan mode and submits the implementation prompt", async () => { - const { host, ensureConnected, requestDefaultCollaborationModeForNextTurn, sendTurnText, stateStore } = createController(); + const { host, connectedClient, requestDefaultCollaborationModeForNextTurn, sendTurnText, stateStore } = createController(); const plan = planItem("plan"); resumeThread(stateStore, [plan]); stateStore.dispatch({ type: "ui/panel-set", panel: "status-panel" }); await implementPlan(host, plan.id); - expect(ensureConnected).toHaveBeenCalledOnce(); + expect(connectedClient).toHaveBeenCalledOnce(); expect(requestDefaultCollaborationModeForNextTurn).toHaveBeenCalledOnce(); expect(stateStore.getState().runtime.selectedCollaborationMode).toBe("default"); expect(stateStore.getState().ui.toolbarPanel).toBeNull(); @@ -103,14 +102,14 @@ describe("implementPlan", () => { }); it("ignores stale plan items", async () => { - const { host, ensureConnected, sendTurnText, stateStore } = createController(); + const { host, connectedClient, sendTurnText, stateStore } = createController(); const first = planItem("first"); const latest = planItem("latest"); resumeThread(stateStore, [first, latest]); await implementPlan(host, first.id); - expect(ensureConnected).not.toHaveBeenCalled(); + expect(connectedClient).not.toHaveBeenCalled(); expect(sendTurnText).not.toHaveBeenCalled(); }); }); diff --git a/tests/features/chat/conversation/turns/turn-submission-actions.test.ts b/tests/features/chat/conversation/turns/turn-submission-actions.test.ts index 80481c6d..90ae7220 100644 --- a/tests/features/chat/conversation/turns/turn-submission-actions.test.ts +++ b/tests/features/chat/conversation/turns/turn-submission-actions.test.ts @@ -39,7 +39,7 @@ function createHost(overrides: TurnSubmissionHostOverrides = {}) { const host: TurnSubmissionActionsHost = { stateStore, vaultPath: "/vault", - currentClient: () => client, + connectedClient: vi.fn().mockResolvedValue(client), ensureRestoredThreadLoaded: vi.fn().mockResolvedValue(true), startThread: vi.fn().mockImplementation(async () => { stateStore.dispatch({ diff --git a/tests/features/chat/threads/goal-actions.test.ts b/tests/features/chat/threads/goal-actions.test.ts index 14188ee4..ab084084 100644 --- a/tests/features/chat/threads/goal-actions.test.ts +++ b/tests/features/chat/threads/goal-actions.test.ts @@ -20,7 +20,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage: vi.fn(), addGoalEvent: vi.fn(), @@ -43,7 +43,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage, addGoalEvent: vi.fn(), @@ -76,7 +76,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage, addGoalEvent, @@ -109,7 +109,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage, addGoalEvent: vi.fn(), @@ -137,7 +137,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage, addGoalEvent: vi.fn(), @@ -166,7 +166,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage, addGoalEvent, @@ -218,7 +218,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread, addSystemMessage: vi.fn(), addGoalEvent: vi.fn(), @@ -240,14 +240,14 @@ describe("createGoalActions", () => { it("rejects empty goal objective saves before connecting or starting a thread", async () => { const stateStore = createChatStateStore(chatStateFixture()); - const ensureConnected = vi.fn().mockResolvedValue(undefined); + const connectedClient = vi.fn().mockResolvedValue({ setThreadGoal: vi.fn() } as unknown as AppServerClient); const startThread = vi.fn().mockResolvedValue({ threadId: "thread" }); const addSystemMessage = vi.fn(); const controller = createGoalActions({ stateStore, currentClient: () => ({ setThreadGoal: vi.fn() }) as unknown as AppServerClient, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected, + connectedClient, startThread, addSystemMessage, addGoalEvent: vi.fn(), @@ -257,7 +257,7 @@ describe("createGoalActions", () => { await expect(controller.saveObjective(" ", null)).resolves.toBe(false); expect(addSystemMessage).toHaveBeenCalledWith("Goal objective cannot be empty."); - expect(ensureConnected).not.toHaveBeenCalled(); + expect(connectedClient).not.toHaveBeenCalled(); expect(startThread).not.toHaveBeenCalled(); }); @@ -273,7 +273,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage, addGoalEvent: vi.fn(), @@ -300,7 +300,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage, addGoalEvent: vi.fn(), @@ -325,7 +325,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage: vi.fn(), addGoalEvent, @@ -351,7 +351,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage, addGoalEvent, @@ -375,7 +375,7 @@ describe("createGoalActions", () => { stateStore, currentClient: () => client, localItemIds: createLocalIdSource({ nowMs: () => 1, seed: "goal" }), - ensureConnected: vi.fn().mockResolvedValue(undefined), + connectedClient: vi.fn().mockResolvedValue(client), startThread: vi.fn().mockResolvedValue({ threadId: "thread" }), addSystemMessage, addGoalEvent: vi.fn(), diff --git a/tests/features/chat/threads/thread-management-actions.test.ts b/tests/features/chat/threads/thread-management-actions.test.ts index f895766a..47af09b1 100644 --- a/tests/features/chat/threads/thread-management-actions.test.ts +++ b/tests/features/chat/threads/thread-management-actions.test.ts @@ -31,6 +31,7 @@ describe("thread management actions", () => { await controller.compactThread("source"); + expect(host.connectedClient).toHaveBeenCalledOnce(); expect(host.ensureConnected).toHaveBeenCalledOnce(); expect(client.compactThread).toHaveBeenCalledWith("source"); expect(host.addSystemMessage).toHaveBeenCalledWith("Compaction requested."); @@ -531,10 +532,15 @@ function hostMock({ const notifyThreadRenamed = vi.fn(); const showNotice = vi.fn(); const ensureConnected = vi.fn().mockResolvedValue(undefined); + const connectedClient = vi.fn(async () => { + await ensureConnected(); + return (currentClient ?? (() => client as unknown as AppServerClient))(); + }); return { stateStore, vaultPath: "/vault", ensureConnected, + connectedClient, currentClient: currentClient ?? (() => client as unknown as AppServerClient), addSystemMessage: vi.fn(), setStatus: vi.fn(),