From 0becb8f39a401c764935f07e5ec61aa2450674b5 Mon Sep 17 00:00:00 2001 From: murashit Date: Sun, 14 Jun 2026 18:47:58 +0900 Subject: [PATCH] Tighten pending request controller contracts --- .../pending-requests/controller.ts | 15 ++++++++---- .../chat/application/state/reducer.ts | 6 ++--- src/features/chat/host/conversation.ts | 2 +- .../pending-requests/controller.test.ts | 24 ++++++------------- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/features/chat/application/pending-requests/controller.ts b/src/features/chat/application/pending-requests/controller.ts index a495c583..4b1dd5f7 100644 --- a/src/features/chat/application/pending-requests/controller.ts +++ b/src/features/chat/application/pending-requests/controller.ts @@ -1,5 +1,4 @@ import type { ChatStateStore } from "../state/reducer"; -import type { ChatInboundController } from "../../app-server/inbound/controller"; import { pendingRequestFocusSignature } from "../../domain/pending-requests/signatures"; import { pendingRequestBlockState } from "./snapshot"; import { @@ -10,9 +9,15 @@ import { } from "../../domain/pending-requests/model"; import type { PendingRequestBlockActions, PendingRequestBlockState, PendingRequestId } from "./block"; +interface PendingRequestResponder { + resolveApproval: (approval: PendingApproval, action: ApprovalAction) => void; + resolveUserInput: (input: PendingUserInput, answers: Record) => void; + cancelUserInput: (input: PendingUserInput) => void; +} + export interface PendingRequestControllerHost { stateStore: ChatStateStore; - controller: ChatInboundController; + responder: PendingRequestResponder; composerHasFocus: () => boolean; refreshLiveState: () => void; } @@ -55,14 +60,14 @@ export class PendingRequestController { resolveApproval(requestId: PendingRequestId, action: ApprovalAction): void { const approval = this.pendingApproval(requestId); if (!approval) return; - this.host.controller.resolveApproval(approval, action); + this.host.responder.resolveApproval(approval, action); this.commitRequestAction(); } resolveUserInput(requestId: PendingRequestId): void { const input = this.pendingUserInput(requestId); if (!input) return; - this.host.controller.resolveUserInput( + this.host.responder.resolveUserInput( input, answersForPendingUserInput(input, pendingRequestBlockState(this.host.stateStore.getState()).userInputDrafts), ); @@ -72,7 +77,7 @@ export class PendingRequestController { cancelUserInput(requestId: PendingRequestId): void { const input = this.pendingUserInput(requestId); if (!input) return; - this.host.controller.cancelUserInput(input); + this.host.responder.cancelUserInput(input); this.commitRequestAction(); } diff --git a/src/features/chat/application/state/reducer.ts b/src/features/chat/application/state/reducer.ts index 60809759..f747975f 100644 --- a/src/features/chat/application/state/reducer.ts +++ b/src/features/chat/application/state/reducer.ts @@ -26,7 +26,7 @@ import { type ChatRuntimeState, } from "../../domain/runtime/state"; import type { RequestedServiceTier } from "../../domain/runtime/pending-settings"; -import type { RequestId } from "../../../../app-server/connection/rpc-messages"; +import type { PendingRequestId } from "../../domain/pending-requests/model"; import type { ComposerSuggestion } from "../composer/suggestions"; import type { MessageStreamItem } from "../../domain/message-stream/items"; import type { @@ -278,7 +278,7 @@ export type ChatAction = ChatTransitionAction | ChatSliceAction; interface RequestResolvedAction { type: "request/resolved"; - requestId: RequestId; + requestId: PendingRequestId; resultItem?: MessageStreamItem; } @@ -1036,7 +1036,7 @@ function clearAllRequestDisclosures(state: ChatUiState): ChatUiState { }); } -function clearResolvedRequestDisclosures(state: ChatUiState, requestId: RequestId): ChatUiState { +function clearResolvedRequestDisclosures(state: ChatUiState, requestId: PendingRequestId): ChatUiState { const id = String(requestId); const approvalDetails = filterStringSet(state.disclosures.approvalDetails, (key) => !key.startsWith(`${id}:`)); if (approvalDetails === state.disclosures.approvalDetails) return state; diff --git a/src/features/chat/host/conversation.ts b/src/features/chat/host/conversation.ts index 197faba2..b1c5dd3e 100644 --- a/src/features/chat/host/conversation.ts +++ b/src/features/chat/host/conversation.ts @@ -105,7 +105,7 @@ export function createConversationParts( const pendingRequests = new PendingRequestController({ stateStore, - controller: refs.controller, + responder: refs.controller, composerHasFocus: () => composerController.hasFocus(), refreshLiveState: liveState.refresh, }); diff --git a/tests/features/chat/conversation/pending-requests/controller.test.ts b/tests/features/chat/conversation/pending-requests/controller.test.ts index 88925ed5..86b51af3 100644 --- a/tests/features/chat/conversation/pending-requests/controller.test.ts +++ b/tests/features/chat/conversation/pending-requests/controller.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { ChatInboundController } from "../../../../../src/features/chat/app-server/inbound/controller"; import { createChatState, createChatStateStore } from "../../../../../src/features/chat/application/state/reducer"; import { PendingRequestController } from "../../../../../src/features/chat/application/pending-requests/controller"; import { toPendingUserInput } from "../../../../../src/features/chat/app-server/requests/user-input"; @@ -14,23 +13,15 @@ function expectPresent(value: T | null | undefined): T { describe("PendingRequestController", () => { it("resolves user input from immutable draft state and refreshes the host", () => { const stateStore = createChatStateStore(createChatState()); - const respondToServerRequest = vi.fn().mockReturnValue(true); + const resolveUserInput = vi.fn(); const refreshLiveState = vi.fn(); - const controller = new ChatInboundController(stateStore, { - refreshThreads: vi.fn(), - refreshRateLimits: vi.fn(), - refreshSkills: vi.fn(), - publishAppServerMetadata: vi.fn(), - maybeNameThread: vi.fn(), - notifyThreadArchived: vi.fn(), - notifyThreadRenamed: vi.fn(), - recordMcpStartupStatus: vi.fn(), - respondToServerRequest, - rejectServerRequest: vi.fn(), - }); const pendingRequests = new PendingRequestController({ stateStore, - controller, + responder: { + resolveApproval: vi.fn(), + resolveUserInput, + cancelUserInput: vi.fn(), + }, composerHasFocus: () => false, refreshLiveState, }); @@ -40,8 +31,7 @@ describe("PendingRequestController", () => { pendingRequests.resolveUserInput(input.requestId); - expect(respondToServerRequest).toHaveBeenCalledWith(7, { answers: { direction: { answers: ["Left"] } } }); - expect(stateStore.getState().requests.pendingUserInputs).toEqual([]); + expect(resolveUserInput).toHaveBeenCalledWith(input, { direction: "Left" }); expect(refreshLiveState).toHaveBeenCalledOnce(); }); });