mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Centralize stale probe and pending request guards
This commit is contained in:
parent
3b48ab0b9b
commit
91667051ff
9 changed files with 133 additions and 85 deletions
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { readToolInventory } from "../../../../app-server/tool-inventory";
|
||||
import { readRateLimitMetadataProbe } from "../../../../app-server/query/metadata-probes";
|
||||
import type { SharedServerMetadata } from "../../../../domain/server/metadata";
|
||||
import type { ChatServerActionHost } from "./host";
|
||||
import { captureChatServerActionClientScope, type ChatServerActionHost } from "./host";
|
||||
|
||||
interface RefreshServerDiagnosticsOptions {
|
||||
appServerMetadataSnapshot?: boolean;
|
||||
|
|
@ -50,8 +50,9 @@ async function refreshServerDiagnostics(
|
|||
host: ChatServerDiagnosticsActionsHost,
|
||||
options: RefreshServerDiagnosticsOptions = {},
|
||||
): Promise<boolean> {
|
||||
const client = host.currentClient();
|
||||
if (!client) return false;
|
||||
const scope = captureChatServerActionClientScope(host);
|
||||
if (!scope.client) return false;
|
||||
const client = scope.client;
|
||||
|
||||
const initialDiagnostics = currentMetadataDiagnostics(host);
|
||||
const state = host.stateStore.getState();
|
||||
|
|
@ -82,7 +83,7 @@ async function refreshServerDiagnostics(
|
|||
}
|
||||
|
||||
const [results, toolInventoryResult] = await Promise.all([Promise.all(probes), toolInventory]);
|
||||
if (host.currentClient() !== client) return false;
|
||||
if (scope.isStale()) return false;
|
||||
|
||||
let diagnostics = currentMetadataDiagnostics(host);
|
||||
for (const result of results) {
|
||||
|
|
|
|||
|
|
@ -6,3 +6,16 @@ export interface ChatServerActionHost {
|
|||
vaultPath: string;
|
||||
currentClient: () => AppServerClient | null;
|
||||
}
|
||||
|
||||
export interface ChatServerActionClientScope {
|
||||
client: AppServerClient | null;
|
||||
isStale: () => boolean;
|
||||
}
|
||||
|
||||
export function captureChatServerActionClientScope(host: ChatServerActionHost): ChatServerActionClientScope {
|
||||
const client = host.currentClient();
|
||||
return {
|
||||
client,
|
||||
isStale: () => client !== null && host.currentClient() !== client,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
import { isStaleAppServerSharedQueryContextError } from "../../../../app-server/query/shared-queries";
|
||||
import { cloneServerDiagnostics, diagnosticsWithProbe } from "../../../../domain/server/diagnostics";
|
||||
import type { SharedServerMetadata } from "../../../../domain/server/metadata";
|
||||
import type { ChatServerActionHost } from "./host";
|
||||
import { captureChatServerActionClientScope, type ChatServerActionHost } from "./host";
|
||||
|
||||
export interface ChatServerMetadataActionsHost extends ChatServerActionHost {
|
||||
updateAppServerMetadata: (updater: (metadata: SharedServerMetadata | null) => SharedServerMetadata | null) => SharedServerMetadata | null;
|
||||
|
|
@ -68,9 +68,9 @@ function applyAppServerMetadataSnapshot(host: ChatServerMetadataActionsHost): vo
|
|||
}
|
||||
|
||||
async function refreshSkills(host: ChatServerMetadataActionsHost, forceReload = false): Promise<SharedServerMetadata | null> {
|
||||
const client = host.currentClient();
|
||||
const skills = await readSkillMetadataProbe(client, host.vaultPath, forceReload);
|
||||
if (client && host.currentClient() !== client) return null;
|
||||
const scope = captureChatServerActionClientScope(host);
|
||||
const skills = await readSkillMetadataProbe(scope.client, host.vaultPath, forceReload);
|
||||
if (scope.isStale()) return null;
|
||||
const next = host.updateAppServerMetadata((metadata) => {
|
||||
if (!metadata) return null;
|
||||
return {
|
||||
|
|
@ -96,9 +96,9 @@ async function refreshRateLimits(
|
|||
host: ChatServerMetadataActionsHost,
|
||||
options: { preserveExistingOnFailure?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const client = host.currentClient();
|
||||
const rateLimit = await readRateLimitMetadataProbe(client);
|
||||
if (client && host.currentClient() !== client) return;
|
||||
const scope = captureChatServerActionClientScope(host);
|
||||
const rateLimit = await readRateLimitMetadataProbe(scope.client);
|
||||
if (scope.isStale()) return;
|
||||
const preserveExistingOnFailure = options.preserveExistingOnFailure === true;
|
||||
const next = updateRateLimitMetadata(host, rateLimit, { preserveRateLimitOnFailure: preserveExistingOnFailure });
|
||||
if (next) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
|
|||
import { runtimeConfigOrDefault } from "../../domain/runtime/effective";
|
||||
import { serviceTierRequestForThreadStart } from "../../application/runtime/thread-settings-update";
|
||||
import { resumedThreadAction } from "../../application/state/actions";
|
||||
import type { ChatServerActionHost } from "./host";
|
||||
import { captureChatServerActionClientScope, type ChatServerActionHost } from "./host";
|
||||
import type { ChatState } from "../../application/state/root-reducer";
|
||||
|
||||
interface StartedThreadSummary {
|
||||
|
|
@ -40,15 +40,15 @@ async function startThread(
|
|||
preview?: string,
|
||||
options: { syncGoal?: boolean } = {},
|
||||
): Promise<StartedThreadSummary | null> {
|
||||
const client = host.currentClient();
|
||||
if (!client) return null;
|
||||
const scope = captureChatServerActionClientScope(host);
|
||||
if (!scope.client) return null;
|
||||
const requestState = host.stateStore.getState();
|
||||
const serviceTier = serviceTierRequestForThreadStart(
|
||||
host.runtimeSnapshotForState(requestState),
|
||||
runtimeConfigOrDefault(requestState.connection.runtimeConfig),
|
||||
);
|
||||
const response = await client.startThread({ cwd: host.vaultPath, serviceTier });
|
||||
if (host.currentClient() !== client) return null;
|
||||
const response = await scope.client.startThread({ cwd: host.vaultPath, serviceTier });
|
||||
if (scope.isStale()) return null;
|
||||
const state = host.stateStore.getState();
|
||||
const fallbackPreview = preview?.trim();
|
||||
const activationResponse =
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
type McpElicitationAction,
|
||||
type PendingApproval,
|
||||
type PendingMcpElicitation,
|
||||
type PendingRequestId,
|
||||
type PendingUserInput,
|
||||
} from "../../domain/pending-requests/model";
|
||||
import { approvalResponse } from "../requests/approval";
|
||||
|
|
@ -70,10 +71,10 @@ export interface ChatInboundHandler {
|
|||
handleNotification(notification: ServerNotification): void;
|
||||
handleServerRequest(request: ServerRequest): void;
|
||||
handleAppServerLog(message: string): void;
|
||||
resolveApproval(approval: PendingApproval, action: ApprovalAction): void;
|
||||
resolveUserInput(input: PendingUserInput, answers: Record<string, string>): void;
|
||||
cancelUserInput(input: PendingUserInput): void;
|
||||
resolveMcpElicitation(elicitation: PendingMcpElicitation, action: McpElicitationAction): void;
|
||||
resolveApproval(requestId: PendingRequestId, action: ApprovalAction): void;
|
||||
resolveUserInput(requestId: PendingRequestId, answers: Record<string, string>): void;
|
||||
cancelUserInput(requestId: PendingRequestId): void;
|
||||
resolveMcpElicitation(requestId: PendingRequestId, action: McpElicitationAction): void;
|
||||
addSystemMessage(text: string): void;
|
||||
addStructuredSystemMessage(text: string, details: MessageStreamNoticeSection[]): void;
|
||||
addDedupedSystemMessage(text: string): void;
|
||||
|
|
@ -101,17 +102,17 @@ export function createChatInboundHandler(
|
|||
handleAppServerLog: (message) => {
|
||||
handleAppServerLog(context, message);
|
||||
},
|
||||
resolveApproval: (approval, action) => {
|
||||
resolveApproval(context, approval, action);
|
||||
resolveApproval: (requestId, action) => {
|
||||
resolveApproval(context, requestId, action);
|
||||
},
|
||||
resolveUserInput: (input, answers) => {
|
||||
resolveUserInput(context, input, answers);
|
||||
resolveUserInput: (requestId, answers) => {
|
||||
resolveUserInput(context, requestId, answers);
|
||||
},
|
||||
cancelUserInput: (input) => {
|
||||
cancelUserInput(context, input);
|
||||
cancelUserInput: (requestId) => {
|
||||
cancelUserInput(context, requestId);
|
||||
},
|
||||
resolveMcpElicitation: (elicitation, action) => {
|
||||
resolveMcpElicitation(context, elicitation, action);
|
||||
resolveMcpElicitation: (requestId, action) => {
|
||||
resolveMcpElicitation(context, requestId, action);
|
||||
},
|
||||
addSystemMessage: (text) => {
|
||||
addSystemMessage(context, text);
|
||||
|
|
@ -173,8 +174,9 @@ function handleAppServerLog(context: ChatInboundHandlerContext, message: string)
|
|||
}
|
||||
}
|
||||
|
||||
function resolveApproval(context: ChatInboundHandlerContext, approval: PendingApproval, action: ApprovalAction): void {
|
||||
if (!state(context).requests.approvals.includes(approval)) return;
|
||||
function resolveApproval(context: ChatInboundHandlerContext, requestId: PendingRequestId, action: ApprovalAction): void {
|
||||
const approval = pendingApproval(context, requestId);
|
||||
if (!approval) return;
|
||||
if (!context.actions.respondToServerRequest(approval.requestId, approvalResponse(approval, action))) {
|
||||
addSystemMessage(context, cannotSendApprovalResponseMessage());
|
||||
return;
|
||||
|
|
@ -182,8 +184,9 @@ function resolveApproval(context: ChatInboundHandlerContext, approval: PendingAp
|
|||
dispatch(context, { type: "request/resolved", requestId: approval.requestId, resultItem: createApprovalResultItem(approval, action) });
|
||||
}
|
||||
|
||||
function resolveUserInput(context: ChatInboundHandlerContext, input: PendingUserInput, answers: Record<string, string>): void {
|
||||
if (!state(context).requests.pendingUserInputs.includes(input)) return;
|
||||
function resolveUserInput(context: ChatInboundHandlerContext, requestId: PendingRequestId, answers: Record<string, string>): void {
|
||||
const input = pendingUserInput(context, requestId);
|
||||
if (!input) return;
|
||||
if (!context.actions.respondToServerRequest(input.requestId, userInputResponse(input, answers))) {
|
||||
addSystemMessage(context, cannotSendUserInputMessage());
|
||||
return;
|
||||
|
|
@ -195,8 +198,9 @@ function resolveUserInput(context: ChatInboundHandlerContext, input: PendingUser
|
|||
});
|
||||
}
|
||||
|
||||
function cancelUserInput(context: ChatInboundHandlerContext, input: PendingUserInput): void {
|
||||
if (!state(context).requests.pendingUserInputs.includes(input)) return;
|
||||
function cancelUserInput(context: ChatInboundHandlerContext, requestId: PendingRequestId): void {
|
||||
const input = pendingUserInput(context, requestId);
|
||||
if (!input) return;
|
||||
if (!context.actions.rejectServerRequest(input.requestId, -32000, userCancelledInputRequestMessage())) {
|
||||
addSystemMessage(context, cannotCancelUserInputMessage());
|
||||
return;
|
||||
|
|
@ -208,8 +212,9 @@ function cancelUserInput(context: ChatInboundHandlerContext, input: PendingUserI
|
|||
});
|
||||
}
|
||||
|
||||
function resolveMcpElicitation(context: ChatInboundHandlerContext, elicitation: PendingMcpElicitation, action: McpElicitationAction): void {
|
||||
if (!state(context).requests.pendingMcpElicitations.includes(elicitation)) return;
|
||||
function resolveMcpElicitation(context: ChatInboundHandlerContext, requestId: PendingRequestId, action: McpElicitationAction): void {
|
||||
const elicitation = pendingMcpElicitation(context, requestId);
|
||||
if (!elicitation) return;
|
||||
const content = action === "accept" ? contentForPendingMcpElicitation(elicitation, state(context).requests.mcpElicitationDrafts) : null;
|
||||
if (!context.actions.respondToServerRequest(elicitation.requestId, mcpElicitationResponse(action, content))) {
|
||||
addSystemMessage(context, cannotSendMcpElicitationMessage());
|
||||
|
|
@ -222,6 +227,18 @@ function resolveMcpElicitation(context: ChatInboundHandlerContext, elicitation:
|
|||
});
|
||||
}
|
||||
|
||||
function pendingApproval(context: ChatInboundHandlerContext, requestId: PendingRequestId): PendingApproval | null {
|
||||
return state(context).requests.approvals.find((approval) => approval.requestId === requestId) ?? null;
|
||||
}
|
||||
|
||||
function pendingUserInput(context: ChatInboundHandlerContext, requestId: PendingRequestId): PendingUserInput | null {
|
||||
return state(context).requests.pendingUserInputs.find((input) => input.requestId === requestId) ?? null;
|
||||
}
|
||||
|
||||
function pendingMcpElicitation(context: ChatInboundHandlerContext, requestId: PendingRequestId): PendingMcpElicitation | null {
|
||||
return state(context).requests.pendingMcpElicitations.find((elicitation) => elicitation.requestId === requestId) ?? null;
|
||||
}
|
||||
|
||||
function addSystemMessage(context: ChatInboundHandlerContext, text: string): void {
|
||||
dispatch(context, { type: "message-stream/system-item-added", item: createSystemItem(localItemId(context, "system"), text) });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import type { PendingRequestBlockActions, PendingRequestBlockState, PendingReque
|
|||
import type { ChatState } from "../state/root-reducer";
|
||||
|
||||
interface PendingRequestResponder {
|
||||
resolveApproval: (approval: PendingApproval, action: ApprovalAction) => void;
|
||||
resolveUserInput: (input: PendingUserInput, answers: Record<string, string>) => void;
|
||||
cancelUserInput: (input: PendingUserInput) => void;
|
||||
resolveMcpElicitation: (elicitation: PendingMcpElicitation, action: McpElicitationAction) => void;
|
||||
resolveApproval: (requestId: PendingRequestId, action: ApprovalAction) => void;
|
||||
resolveUserInput: (requestId: PendingRequestId, answers: Record<string, string>) => void;
|
||||
cancelUserInput: (requestId: PendingRequestId) => void;
|
||||
resolveMcpElicitation: (requestId: PendingRequestId, action: McpElicitationAction) => void;
|
||||
}
|
||||
|
||||
export interface PendingRequestActionsHost {
|
||||
|
|
@ -42,7 +42,7 @@ export function createPendingRequestActions(host: PendingRequestActionsHost): Pe
|
|||
const resolveApproval = (requestId: PendingRequestId, approvalAction: ApprovalAction): void => {
|
||||
const approval = pendingApproval(host, requestId);
|
||||
if (!approval) return;
|
||||
host.responder.resolveApproval(approval, approvalAction);
|
||||
host.responder.resolveApproval(requestId, approvalAction);
|
||||
commitRequestAction(host);
|
||||
};
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ export function createPendingRequestActions(host: PendingRequestActionsHost): Pe
|
|||
const input = pendingUserInput(host, requestId);
|
||||
if (!input) return;
|
||||
host.responder.resolveUserInput(
|
||||
input,
|
||||
requestId,
|
||||
answersForPendingUserInput(input, pendingRequestBlockState(host.stateStore.getState()).userInputDrafts),
|
||||
);
|
||||
commitRequestAction(host);
|
||||
|
|
@ -59,14 +59,14 @@ export function createPendingRequestActions(host: PendingRequestActionsHost): Pe
|
|||
const cancelUserInput = (requestId: PendingRequestId): void => {
|
||||
const input = pendingUserInput(host, requestId);
|
||||
if (!input) return;
|
||||
host.responder.cancelUserInput(input);
|
||||
host.responder.cancelUserInput(requestId);
|
||||
commitRequestAction(host);
|
||||
};
|
||||
|
||||
const resolveMcpElicitation = (requestId: PendingRequestId, action: McpElicitationAction): void => {
|
||||
const elicitation = pendingMcpElicitation(host, requestId);
|
||||
if (!elicitation) return;
|
||||
host.responder.resolveMcpElicitation(elicitation, action);
|
||||
host.responder.resolveMcpElicitation(requestId, action);
|
||||
commitRequestAction(host);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@ export interface ThreadManagementActions {
|
|||
rollbackThread: (threadId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface ThreadManagementActionScope {
|
||||
client: AppServerClient;
|
||||
targetThreadId: string;
|
||||
initialActiveThreadId: string | null;
|
||||
}
|
||||
|
||||
export function createThreadManagementActions(host: ThreadManagementActionsHost): ThreadManagementActions {
|
||||
return {
|
||||
compactActiveThread: () => compactActiveThread(host),
|
||||
|
|
@ -101,13 +107,11 @@ async function compactActiveThread(host: ThreadManagementActionsHost): Promise<v
|
|||
}
|
||||
|
||||
async function compactThread(host: ThreadManagementActionsHost, threadId: string): Promise<void> {
|
||||
const client = await host.connectedClient();
|
||||
if (!client) return;
|
||||
const initialActiveThreadId = threadManagementState(host).activeThread.id;
|
||||
const scope = await captureThreadManagementActionScope(host, threadId);
|
||||
if (!scope) return;
|
||||
try {
|
||||
await client.compactThread(threadId);
|
||||
if (host.currentClient() !== client) return;
|
||||
if (!threadManagementStillTargetsOriginalPanel(threadManagementState(host), initialActiveThreadId, threadId)) return;
|
||||
await scope.client.compactThread(threadId);
|
||||
if (!threadManagementScopeStillTargetsOriginalPanel(host, scope)) return;
|
||||
host.addSystemMessage(STATUS_COMPACTION_REQUESTED);
|
||||
host.setStatus(STATUS_COMPACTION_REQUESTED);
|
||||
} catch (error) {
|
||||
|
|
@ -147,10 +151,9 @@ async function forkThreadFromTurn(
|
|||
host.addSystemMessage(finishBeforeForkingThreadsMessage());
|
||||
return;
|
||||
}
|
||||
const client = await host.connectedClient();
|
||||
if (!client) return;
|
||||
const scope = await captureThreadManagementActionScope(host, threadId);
|
||||
if (!scope) return;
|
||||
|
||||
const initialActiveThreadId = threadManagementState(host).activeThread.id;
|
||||
const turnsToDrop = turnId ? messageStreamTurnsAfterTurnId(threadManagementState(host).messageStream, turnId) : 0;
|
||||
if (turnsToDrop === null) {
|
||||
host.addSystemMessage(selectedTurnNotFoundForForkMessage());
|
||||
|
|
@ -159,15 +162,15 @@ async function forkThreadFromTurn(
|
|||
|
||||
try {
|
||||
const sourceName = inheritedForkThreadName(threadId, threadManagementState(host).threadList.listedThreads);
|
||||
const forkedThread = await forkThreadOnAppServer(client, threadId, host.vaultPath);
|
||||
if (host.currentClient() !== client) return;
|
||||
const forkedThread = await forkThreadOnAppServer(scope.client, threadId, host.vaultPath);
|
||||
if (threadManagementScopeClientStale(host, scope)) return;
|
||||
const forkedThreadId = forkedThread.id;
|
||||
if (turnsToDrop > 0) {
|
||||
await rollbackThreadOnAppServer(client, forkedThreadId, turnsToDrop);
|
||||
if (host.currentClient() !== client) return;
|
||||
await rollbackThreadOnAppServer(scope.client, forkedThreadId, turnsToDrop);
|
||||
if (threadManagementScopeClientStale(host, scope)) return;
|
||||
}
|
||||
host.recordThreadForked(forkedThread);
|
||||
if (!threadManagementStillTargetsOriginalPanel(threadManagementState(host), initialActiveThreadId, threadId)) return;
|
||||
if (!threadManagementScopeStillTargetsOriginalPanel(host, scope)) return;
|
||||
if (sourceName) {
|
||||
try {
|
||||
if (!(await host.operations.renameThread(forkedThreadId, sourceName))) return;
|
||||
|
|
@ -213,8 +216,8 @@ async function rollbackThread(host: ThreadManagementActionsHost, threadId: strin
|
|||
host.addSystemMessage(interruptBeforeRollbackMessage());
|
||||
return;
|
||||
}
|
||||
const client = await host.connectedClient();
|
||||
if (!client) return;
|
||||
const scope = await captureThreadManagementActionScope(host, threadId);
|
||||
if (!scope) return;
|
||||
|
||||
const candidate = messageStreamRollbackCandidate(threadManagementState(host).messageStream);
|
||||
if (!candidate) {
|
||||
|
|
@ -224,9 +227,8 @@ async function rollbackThread(host: ThreadManagementActionsHost, threadId: strin
|
|||
|
||||
try {
|
||||
host.setStatus(STATUS_ROLLBACK_STARTING);
|
||||
const snapshot = await rollbackThreadOnAppServer(client, threadId);
|
||||
if (host.currentClient() !== client) return;
|
||||
if (!threadManagementStillTargetsPanel(threadManagementState(host), threadId)) return;
|
||||
const snapshot = await rollbackThreadOnAppServer(scope.client, threadId);
|
||||
if (!threadManagementScopeStillTargetsPanel(host, scope)) return;
|
||||
threadManagementDispatch(
|
||||
host,
|
||||
resumedThreadActionFromActiveRuntime({
|
||||
|
|
@ -261,11 +263,29 @@ function threadManagementDispatch(host: ThreadManagementActionsHost, action: Cha
|
|||
host.stateStore.dispatch(action);
|
||||
}
|
||||
|
||||
function threadManagementStillTargetsPanel(state: ChatState, threadId: string): boolean {
|
||||
return state.activeThread.id === threadId;
|
||||
async function captureThreadManagementActionScope(
|
||||
host: ThreadManagementActionsHost,
|
||||
targetThreadId: string,
|
||||
): Promise<ThreadManagementActionScope | null> {
|
||||
const client = await host.connectedClient();
|
||||
if (!client) return null;
|
||||
return {
|
||||
client,
|
||||
targetThreadId,
|
||||
initialActiveThreadId: threadManagementState(host).activeThread.id,
|
||||
};
|
||||
}
|
||||
|
||||
function threadManagementStillTargetsOriginalPanel(state: ChatState, initialThreadId: string | null, threadId: string): boolean {
|
||||
if (!initialThreadId) return true;
|
||||
return initialThreadId === threadId && state.activeThread.id === threadId;
|
||||
function threadManagementScopeClientStale(host: ThreadManagementActionsHost, scope: ThreadManagementActionScope): boolean {
|
||||
return host.currentClient() !== scope.client;
|
||||
}
|
||||
|
||||
function threadManagementScopeStillTargetsPanel(host: ThreadManagementActionsHost, scope: ThreadManagementActionScope): boolean {
|
||||
return !threadManagementScopeClientStale(host, scope) && threadManagementState(host).activeThread.id === scope.targetThreadId;
|
||||
}
|
||||
|
||||
function threadManagementScopeStillTargetsOriginalPanel(host: ThreadManagementActionsHost, scope: ThreadManagementActionScope): boolean {
|
||||
if (threadManagementScopeClientStale(host, scope)) return false;
|
||||
if (!scope.initialActiveThreadId) return true;
|
||||
return scope.initialActiveThreadId === scope.targetThreadId && threadManagementState(host).activeThread.id === scope.targetThreadId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ describe("PendingRequestActions", () => {
|
|||
|
||||
pendingRequests.resolveUserInput(input.requestId);
|
||||
|
||||
expect(resolveUserInput).toHaveBeenCalledWith(input, { direction: "Left" });
|
||||
expect(resolveUserInput).toHaveBeenCalledWith(input.requestId, { direction: "Left" });
|
||||
expect(refreshLiveState).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -759,7 +759,7 @@ describe("ChatInboundHandler", () => {
|
|||
});
|
||||
|
||||
expect(handler.currentState().requests.pendingUserInputs).toHaveLength(1);
|
||||
handler.resolveUserInput(expectPresent(handler.currentState().requests.pendingUserInputs[0]), { scope: "Narrow" });
|
||||
handler.resolveUserInput(42, { scope: "Narrow" });
|
||||
expect(respondToServerRequest).toHaveBeenCalledWith(42, { answers: { scope: { answers: ["Narrow"] } } });
|
||||
expect(handler.currentState().requests.pendingUserInputs).toEqual([]);
|
||||
expect(chatStateMessageStreamItems(handler.currentState()).at(-1)).toMatchObject({
|
||||
|
|
@ -788,7 +788,7 @@ describe("ChatInboundHandler", () => {
|
|||
},
|
||||
});
|
||||
|
||||
handler.cancelUserInput(expectPresent(handler.currentState().requests.pendingUserInputs[0]));
|
||||
handler.cancelUserInput(43);
|
||||
expect(rejectServerRequest).toHaveBeenCalledWith(43, -32000, "User cancelled input request.");
|
||||
expect(handler.currentState().requests.pendingUserInputs).toEqual([]);
|
||||
expect(chatStateMessageStreamItems(handler.currentState()).at(-1)).toMatchObject({
|
||||
|
|
@ -834,7 +834,7 @@ describe("ChatInboundHandler", () => {
|
|||
});
|
||||
|
||||
expect(handler.currentState().requests.pendingMcpElicitations).toHaveLength(1);
|
||||
handler.resolveMcpElicitation(expectPresent(handler.currentState().requests.pendingMcpElicitations[0]), "accept");
|
||||
handler.resolveMcpElicitation(45, "accept");
|
||||
|
||||
expect(respondToServerRequest).toHaveBeenCalledWith(45, {
|
||||
action: "accept",
|
||||
|
|
@ -873,7 +873,7 @@ describe("ChatInboundHandler", () => {
|
|||
},
|
||||
});
|
||||
|
||||
handler.resolveMcpElicitation(expectPresent(handler.currentState().requests.pendingMcpElicitations[0]), "decline");
|
||||
handler.resolveMcpElicitation(46, "decline");
|
||||
|
||||
expect(respondToServerRequest).toHaveBeenCalledWith(46, { action: "decline", content: null, _meta: null });
|
||||
expect(handler.currentState().requests.pendingMcpElicitations).toEqual([]);
|
||||
|
|
@ -884,7 +884,7 @@ describe("ChatInboundHandler", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("ignores stale requestUserInput objects with a reused request id", () => {
|
||||
it("ignores missing requestUserInput ids", () => {
|
||||
const state = chatStateFixture();
|
||||
const respondToServerRequest = vi.fn(() => true);
|
||||
const rejectServerRequest = vi.fn(() => true);
|
||||
|
|
@ -892,10 +892,9 @@ describe("ChatInboundHandler", () => {
|
|||
|
||||
handler.handleServerRequest(userInputRequest(44));
|
||||
const current = expectPresent(handler.currentState().requests.pendingUserInputs[0]);
|
||||
const stale = { ...current };
|
||||
|
||||
handler.resolveUserInput(stale, { note: "stale" });
|
||||
handler.cancelUserInput(stale);
|
||||
handler.resolveUserInput(45, { note: "stale" });
|
||||
handler.cancelUserInput(45);
|
||||
|
||||
expect(respondToServerRequest).not.toHaveBeenCalled();
|
||||
expect(rejectServerRequest).not.toHaveBeenCalled();
|
||||
|
|
@ -903,16 +902,15 @@ describe("ChatInboundHandler", () => {
|
|||
expect(chatStateMessageStreamItems(handler.currentState())).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores stale MCP elicitation objects with a reused request id", () => {
|
||||
it("ignores missing MCP elicitation ids", () => {
|
||||
const state = chatStateFixture();
|
||||
const respondToServerRequest = vi.fn(() => true);
|
||||
const handler = handlerForState(state, { respondToServerRequest });
|
||||
|
||||
handler.handleServerRequest(mcpElicitationRequest(47));
|
||||
const current = expectPresent(handler.currentState().requests.pendingMcpElicitations[0]);
|
||||
const stale = { ...current };
|
||||
|
||||
handler.resolveMcpElicitation(stale, "accept");
|
||||
handler.resolveMcpElicitation(48, "accept");
|
||||
|
||||
expect(respondToServerRequest).not.toHaveBeenCalled();
|
||||
expect(handler.currentState().requests.pendingMcpElicitations).toEqual([current]);
|
||||
|
|
@ -925,7 +923,7 @@ describe("ChatInboundHandler", () => {
|
|||
const handler = handlerForState(state, { respondToServerRequest });
|
||||
|
||||
handler.handleServerRequest(expectPresent(supportedApprovalRequests()[2]));
|
||||
handler.resolveApproval(expectPresent(handler.currentState().requests.approvals[0]), "accept-session");
|
||||
handler.resolveApproval(12, "accept-session");
|
||||
|
||||
expect(respondToServerRequest).toHaveBeenCalledWith(12, {
|
||||
scope: "session",
|
||||
|
|
@ -948,16 +946,15 @@ describe("ChatInboundHandler", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("ignores stale approval objects with a reused request id", () => {
|
||||
it("ignores missing approval ids", () => {
|
||||
const state = chatStateFixture();
|
||||
const respondToServerRequest = vi.fn(() => true);
|
||||
const handler = handlerForState(state, { respondToServerRequest });
|
||||
|
||||
handler.handleServerRequest(expectPresent(supportedApprovalRequests()[2]));
|
||||
const current = expectPresent(handler.currentState().requests.approvals[0]);
|
||||
const stale = { ...current };
|
||||
|
||||
handler.resolveApproval(stale, "accept-session");
|
||||
handler.resolveApproval(13, "accept-session");
|
||||
|
||||
expect(respondToServerRequest).not.toHaveBeenCalled();
|
||||
expect(handler.currentState().requests.approvals).toEqual([current]);
|
||||
|
|
@ -1090,7 +1087,7 @@ describe("ChatInboundHandler", () => {
|
|||
const handler = handlerForState(state, { respondToServerRequest });
|
||||
|
||||
handler.handleServerRequest(userInputRequest(55));
|
||||
handler.resolveUserInput(expectPresent(handler.currentState().requests.pendingUserInputs[0]), { note: "Later" });
|
||||
handler.resolveUserInput(55, { note: "Later" });
|
||||
|
||||
expect(handler.currentState().requests.pendingUserInputs).toHaveLength(1);
|
||||
expect(chatStateMessageStreamItems(handler.currentState())).toEqual([
|
||||
|
|
|
|||
Loading…
Reference in a new issue