Tighten pending request controller contracts

This commit is contained in:
murashit 2026-06-14 18:47:58 +09:00
parent 5fb6212e92
commit 0becb8f39a
4 changed files with 21 additions and 26 deletions

View file

@ -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<string, string>) => 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();
}

View file

@ -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;

View file

@ -105,7 +105,7 @@ export function createConversationParts(
const pendingRequests = new PendingRequestController({
stateStore,
controller: refs.controller,
responder: refs.controller,
composerHasFocus: () => composerController.hasFocus(),
refreshLiveState: liveState.refresh,
});

View file

@ -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<T>(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();
});
});