mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
test(chat): cover pending request UI boundaries
This commit is contained in:
parent
93c4ba50a1
commit
46ecf3addd
2 changed files with 176 additions and 0 deletions
|
|
@ -1030,6 +1030,73 @@ describe("ChatInboundHandler", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "approval response",
|
||||
actions: { respondToServerRequest: vi.fn(() => false) },
|
||||
exercise(handler: TestChatInboundHandler) {
|
||||
handler.handleServerRequest(expectPresent(supportedApprovalRequests()[2]));
|
||||
handler.resolveApproval(12, "decline");
|
||||
},
|
||||
expectedMessages: ["Could not send approval response because Codex app-server is not connected."],
|
||||
pending(handler: TestChatInboundHandler) {
|
||||
return handler.currentState().requests.approvals.length;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user-input cancellation",
|
||||
actions: { rejectServerRequest: vi.fn(() => false) },
|
||||
exercise(handler: TestChatInboundHandler) {
|
||||
handler.handleServerRequest(userInputRequest(56));
|
||||
handler.cancelUserInput(56);
|
||||
},
|
||||
expectedMessages: ["Could not cancel user input because Codex app-server is not connected."],
|
||||
pending(handler: TestChatInboundHandler) {
|
||||
return handler.currentState().requests.pendingUserInputs.length;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MCP response",
|
||||
actions: { respondToServerRequest: vi.fn(() => false) },
|
||||
exercise(handler: TestChatInboundHandler) {
|
||||
handler.handleServerRequest(mcpElicitationRequest(57));
|
||||
handler.resolveMcpElicitation(57, "cancel");
|
||||
},
|
||||
expectedMessages: ["Could not send MCP request response because Codex app-server is not connected."],
|
||||
pending(handler: TestChatInboundHandler) {
|
||||
return handler.currentState().requests.pendingMcpElicitations.length;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "current-time response",
|
||||
actions: { respondToServerRequest: vi.fn(() => false) },
|
||||
exercise(handler: TestChatInboundHandler) {
|
||||
handler.handleServerRequest(currentTimeRequest(58, "thread"));
|
||||
},
|
||||
expectedMessages: ["Could not send current time because Codex app-server is not connected."],
|
||||
pending: null,
|
||||
},
|
||||
{
|
||||
name: "unsupported-request rejection",
|
||||
actions: { rejectServerRequest: vi.fn(() => false) },
|
||||
exercise(handler: TestChatInboundHandler) {
|
||||
handler.handleServerRequest(unsupportedToolCallRequest(59));
|
||||
},
|
||||
expectedMessages: [
|
||||
"Rejected unsupported app-server request: item/tool/call",
|
||||
"Could not reject app-server request because Codex app-server is not connected.",
|
||||
],
|
||||
pending: null,
|
||||
},
|
||||
])("reports failed $name delivery without resolving pending actions", ({ actions, exercise, expectedMessages, pending }) => {
|
||||
const handler = handlerForState(chatStateFixture(), actions);
|
||||
|
||||
exercise(handler);
|
||||
|
||||
if (pending) expect(pending(handler)).toBe(1);
|
||||
expect(chatStateThreadStreamItems(handler.currentState()).map((item) => ("text" in item ? item.text : ""))).toEqual(expectedMessages);
|
||||
});
|
||||
|
||||
it("clears pending request state when app-server resolves a request", () => {
|
||||
let state = chatStateFixture();
|
||||
state = chatStateWith(state, { activeThread: { id: "thread-active" } });
|
||||
|
|
@ -2071,6 +2138,14 @@ function currentTimeRequest(id: number, threadId: string): ServerRequest {
|
|||
};
|
||||
}
|
||||
|
||||
function unsupportedToolCallRequest(id: number): ServerRequest {
|
||||
return {
|
||||
id,
|
||||
method: "item/tool/call",
|
||||
params: { threadId: "thread", turnId: "turn", callId: "call", namespace: null, tool: "tool", arguments: {} },
|
||||
};
|
||||
}
|
||||
|
||||
function unknownRequest(): ServerRequest {
|
||||
return {
|
||||
id: 27,
|
||||
|
|
|
|||
|
|
@ -604,6 +604,107 @@ describe("pending request renderer decisions", () => {
|
|||
unmountUiRootInAct(parent);
|
||||
});
|
||||
|
||||
it("keeps mixed MCP form fields wired to their drafts and supports declining the request", () => {
|
||||
const parent = document.createElement("div");
|
||||
const resolveMcpElicitation = vi.fn();
|
||||
const setMcpElicitationDraft = vi.fn();
|
||||
const elicitation = pendingMcpElicitationSnapshot({
|
||||
requestId: 54,
|
||||
params: {
|
||||
threadId: "thread",
|
||||
turnId: null,
|
||||
serverName: "github",
|
||||
mode: "form",
|
||||
message: "Configure the issue",
|
||||
meta: null,
|
||||
fields: [
|
||||
{ id: "notify", title: "Notify", description: null, type: "boolean", required: false, defaultValue: false },
|
||||
{
|
||||
id: "priority",
|
||||
title: "Priority",
|
||||
description: null,
|
||||
type: "single-select",
|
||||
required: true,
|
||||
options: [
|
||||
{ value: "low", label: "Low" },
|
||||
{ value: "high", label: "High" },
|
||||
],
|
||||
defaultValue: "low",
|
||||
},
|
||||
{ id: "estimate", title: "Estimate", description: null, type: "number", required: false, defaultValue: 1.5 },
|
||||
{ id: "note", title: "Note", description: null, type: "string", required: false, defaultValue: "" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
renderThreadStreamBlocksInAct(
|
||||
parent,
|
||||
threadStreamBlocks({
|
||||
items: [
|
||||
{ id: "a1", kind: "dialogue", role: "assistant", text: "Done", dialogueKind: "assistantResponse", dialogueState: "completed" },
|
||||
],
|
||||
pendingRequests: pendingRequestContext({
|
||||
signature: "mcp:54",
|
||||
snapshot: emptyPendingRequestBlockSnapshot({ pendingMcpElicitations: [elicitation] }),
|
||||
actions: pendingRequestActions({ resolveMcpElicitation, setMcpElicitationDraft }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const fields = [...parent.querySelectorAll<HTMLElement>(".codex-panel__mcp-elicitation-field")];
|
||||
actEvent(() => {
|
||||
expectPresent(fields[0]?.querySelector<HTMLInputElement>(".codex-panel__mcp-elicitation-checkbox")).click();
|
||||
expectPresent(fields[1]?.querySelectorAll<HTMLInputElement>(".codex-panel__mcp-elicitation-radio").item(1)).click();
|
||||
changeInputValue(expectPresent(fields[2]?.querySelector<HTMLInputElement>(".codex-panel__mcp-elicitation-input")), "3.5");
|
||||
changeInputValue(expectPresent(fields[3]?.querySelector<HTMLInputElement>(".codex-panel__mcp-elicitation-input")), "Ship today");
|
||||
expectPresent(
|
||||
[...parent.querySelectorAll<HTMLButtonElement>(".codex-panel__pending-request-button")].find(
|
||||
(button) => button.textContent === "Decline",
|
||||
),
|
||||
).click();
|
||||
});
|
||||
|
||||
expect(setMcpElicitationDraft.mock.calls).toEqual([
|
||||
["54:mcp:notify", "true"],
|
||||
["54:mcp:priority", "high"],
|
||||
["54:mcp:estimate", "3.5"],
|
||||
["54:mcp:note", "Ship today"],
|
||||
]);
|
||||
expect(resolveMcpElicitation).toHaveBeenCalledWith(54, "decline");
|
||||
unmountUiRootInAct(parent);
|
||||
});
|
||||
|
||||
it("routes user-input cancellation and approval detail expansion from the shared request block", () => {
|
||||
const parent = document.createElement("div");
|
||||
const cancelUserInput = vi.fn();
|
||||
const setApprovalDetailsExpanded = vi.fn();
|
||||
const approval = pendingApproval();
|
||||
const input = pendingUserInput();
|
||||
|
||||
renderPendingRequestNode(
|
||||
parent,
|
||||
[approval],
|
||||
[input],
|
||||
{ values: new Map() },
|
||||
new Set(),
|
||||
pendingRequestActions({ cancelUserInput, setApprovalDetailsExpanded }),
|
||||
);
|
||||
|
||||
const details = expectPresent(parent.querySelector<HTMLDetailsElement>(".codex-panel__approval-details"));
|
||||
actEvent(() => {
|
||||
details.open = true;
|
||||
details.dispatchEvent(new Event("toggle", { bubbles: true }));
|
||||
expectPresent(
|
||||
[...parent.querySelectorAll<HTMLButtonElement>(".codex-panel__user-input .codex-panel__pending-request-button")].find(
|
||||
(button) => button.textContent === "Cancel",
|
||||
),
|
||||
).click();
|
||||
});
|
||||
|
||||
expect(setApprovalDetailsExpanded).toHaveBeenCalledWith(approval.requestId, true);
|
||||
expect(cancelUserInput).toHaveBeenCalledWith(input.requestId);
|
||||
});
|
||||
|
||||
it("does not consume pending request autofocus while building thread stream blocks", () => {
|
||||
const consumeAutoFocus = vi.fn(() => true);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue