mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Clarify repeated test assertions
This commit is contained in:
parent
b148ada1d0
commit
e57eb45933
7 changed files with 156 additions and 148 deletions
|
|
@ -146,7 +146,7 @@ describe("composer suggestions", () => {
|
|||
});
|
||||
|
||||
it("uses recent files only for empty wikilink suggestions", () => {
|
||||
expect(wikiLinkSuggestions("", notes).map((suggestion) => suggestion.replacement)).toEqual([
|
||||
expect(suggestionReplacements(wikiLinkSuggestions("", notes))).toEqual([
|
||||
"[[projects/Alpha]]",
|
||||
"[[thoughts/Alpha]]",
|
||||
"[[Assets/Diagram.png]]",
|
||||
|
|
@ -195,7 +195,7 @@ describe("composer suggestions", () => {
|
|||
it("suggests headings inside a completed wikilink without suggesting block references", () => {
|
||||
const heading = expectPresent(activeComposerSuggestions("[[Beta Note#impl", notes, [])[0]);
|
||||
|
||||
expect(activeComposerSuggestions("[[Beta Note#", notes, []).map((suggestion) => suggestion.replacement)).toEqual([
|
||||
expect(suggestionReplacements(activeComposerSuggestions("[[Beta Note#", notes, []))).toEqual([
|
||||
"[[Beta Note#Overview]]",
|
||||
"[[Beta Note#Implementation Details]]",
|
||||
]);
|
||||
|
|
@ -298,17 +298,21 @@ describe("composer suggestions", () => {
|
|||
];
|
||||
|
||||
expect(
|
||||
activeComposerSuggestions("/archive ", notes, [], threads, [], null, { activeThreadId: "019abcde-0000-7000-8000-000000000002" }).map(
|
||||
(suggestion) => suggestion.replacement,
|
||||
suggestionReplacements(
|
||||
activeComposerSuggestions("/archive ", notes, [], threads, [], null, {
|
||||
activeThreadId: "019abcde-0000-7000-8000-000000000002",
|
||||
}),
|
||||
),
|
||||
).toEqual(["019abcde-0000-7000-8000-000000000002", "019abcde-0000-7000-8000-000000000001", "019abcde-0000-7000-8000-000000000003"]);
|
||||
expect(
|
||||
activeComposerSuggestions("/rename ", notes, [], threads, [], null, { activeThreadId: "019abcde-0000-7000-8000-000000000002" })[0]
|
||||
?.replacement,
|
||||
).toBe("019abcde-0000-7000-8000-000000000002");
|
||||
expect(
|
||||
activeComposerSuggestions("/archive ", notes, [], threads, [], null, { activeThreadId: null }).map((item) => item.replacement),
|
||||
).toEqual(["019abcde-0000-7000-8000-000000000001", "019abcde-0000-7000-8000-000000000002", "019abcde-0000-7000-8000-000000000003"]);
|
||||
expect(suggestionReplacements(activeComposerSuggestions("/archive ", notes, [], threads, [], null, { activeThreadId: null }))).toEqual([
|
||||
"019abcde-0000-7000-8000-000000000001",
|
||||
"019abcde-0000-7000-8000-000000000002",
|
||||
"019abcde-0000-7000-8000-000000000003",
|
||||
]);
|
||||
expect(
|
||||
activeComposerSuggestions("/archive latest", notes, [], threads, [], null, {
|
||||
activeThreadId: "019abcde-0000-7000-8000-000000000002",
|
||||
|
|
@ -324,9 +328,10 @@ describe("composer suggestions", () => {
|
|||
];
|
||||
const activeThreadId = "019abcde-0000-7000-8000-000000000002";
|
||||
|
||||
expect(activeComposerSuggestions("/resume ", notes, [], threads, [], null, { activeThreadId }).map((item) => item.replacement)).toEqual(
|
||||
["019abcde-0000-7000-8000-000000000001", "019abcde-0000-7000-8000-000000000003"],
|
||||
);
|
||||
expect(suggestionReplacements(activeComposerSuggestions("/resume ", notes, [], threads, [], null, { activeThreadId }))).toEqual([
|
||||
"019abcde-0000-7000-8000-000000000001",
|
||||
"019abcde-0000-7000-8000-000000000003",
|
||||
]);
|
||||
expect(activeComposerSuggestions("/refer current", notes, [], threads, [], null, { activeThreadId })).toEqual([]);
|
||||
expect(activeComposerSuggestions("/archive current", notes, [], threads, [], null, { activeThreadId })[0]?.replacement).toBe(
|
||||
activeThreadId,
|
||||
|
|
@ -348,13 +353,7 @@ describe("composer suggestions", () => {
|
|||
});
|
||||
|
||||
it("suggests slash subcommands from command definitions", () => {
|
||||
expect(activeComposerSuggestions("/goal ", notes, []).map((suggestion) => suggestion.replacement)).toEqual([
|
||||
"set",
|
||||
"edit",
|
||||
"pause",
|
||||
"resume",
|
||||
"clear",
|
||||
]);
|
||||
expect(suggestionReplacements(activeComposerSuggestions("/goal ", notes, []))).toEqual(["set", "edit", "pause", "resume", "clear"]);
|
||||
expect(activeComposerSuggestions("/goal p", notes, [])[0]).toMatchObject({
|
||||
display: "pause",
|
||||
detail: "/goal pause - Pause the current thread goal.",
|
||||
|
|
@ -385,7 +384,7 @@ describe("composer suggestions", () => {
|
|||
replacement: "gpt-5.4-mini",
|
||||
appendSpaceOnInsert: true,
|
||||
});
|
||||
expect(activeComposerSuggestions("/model ", notes, [], [], models).map((suggestion) => suggestion.replacement)).toEqual([
|
||||
expect(suggestionReplacements(activeComposerSuggestions("/model ", notes, [], [], models))).toEqual([
|
||||
"default",
|
||||
"gpt-5.4-mini",
|
||||
"gpt-5.5",
|
||||
|
|
@ -404,9 +403,12 @@ describe("composer suggestions", () => {
|
|||
replacement: "high",
|
||||
appendSpaceOnInsert: true,
|
||||
});
|
||||
expect(
|
||||
activeComposerSuggestions("/reasoning ", notes, [], [], models, "gpt-5.4-mini").map((suggestion) => suggestion.replacement),
|
||||
).toEqual(["default", "minimal", "low", "medium"]);
|
||||
expect(suggestionReplacements(activeComposerSuggestions("/reasoning ", notes, [], [], models, "gpt-5.4-mini"))).toEqual([
|
||||
"default",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
]);
|
||||
expect(activeComposerSuggestions("/reasoning x", notes, [], [], models, "gpt-5.4-mini")).toEqual([]);
|
||||
expect(activeComposerSuggestions("/reasoning high", notes, [], [], models, "gpt-5.5")).toEqual([]);
|
||||
expect(activeComposerSuggestions("/reasoning high ", notes, [], [], models, "gpt-5.5")).toEqual([]);
|
||||
|
|
@ -480,3 +482,7 @@ describe("composer suggestions", () => {
|
|||
expect(composerSuggestionSignature("/stat", 5)).not.toBe(dismissed);
|
||||
});
|
||||
});
|
||||
|
||||
function suggestionReplacements(suggestions: readonly { replacement: string }[]): string[] {
|
||||
return suggestions.map((suggestion) => suggestion.replacement);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import type { ServerNotification, ServerRequest } from "../../../../../src/app-s
|
|||
import { chatStateFixture, chatStateWith } from "../../support/state";
|
||||
|
||||
const activeScope = { activeThreadId: "thread-active", activeTurnId: "turn-active" };
|
||||
type RouteScope = Parameters<typeof routeServerNotification>[1];
|
||||
type NotificationRouteKind = ReturnType<typeof routeServerNotification>["kind"];
|
||||
type RequestRouteKind = ReturnType<typeof routeServerRequest>["kind"];
|
||||
|
||||
describe("chat inbound routing", () => {
|
||||
it("keeps routed notification methods covered by matching planners", () => {
|
||||
|
|
@ -39,9 +42,9 @@ describe("chat inbound routing", () => {
|
|||
},
|
||||
} satisfies Extract<ServerNotification, { method: "turn/completed" }>;
|
||||
|
||||
expect(routeServerNotification(notification, activeScope).kind).toBe("turnLifecycle");
|
||||
expect(routeServerNotification(notification, { activeThreadId: "thread-other", activeTurnId: "turn-active" }).kind).toBe("inactive");
|
||||
expect(routeServerNotification(notification, { activeThreadId: "thread-active", activeTurnId: "turn-other" }).kind).toBe("inactive");
|
||||
expectNotificationRouteKind(notification, "turnLifecycle");
|
||||
expectNotificationRouteKind(notification, "inactive", { activeThreadId: "thread-other", activeTurnId: "turn-active" });
|
||||
expectNotificationRouteKind(notification, "inactive", { activeThreadId: "thread-active", activeTurnId: "turn-other" });
|
||||
});
|
||||
|
||||
it("routes thread-started notifications as global thread catalog events", () => {
|
||||
|
|
@ -50,10 +53,8 @@ describe("chat inbound routing", () => {
|
|||
params: { thread: threadSnapshot("thread-active") },
|
||||
} satisfies Extract<ServerNotification, { method: "thread/started" }>;
|
||||
|
||||
expect(routeServerNotification(notification, activeScope).kind).toBe("threadLifecycle");
|
||||
expect(routeServerNotification(notification, { activeThreadId: "thread-other", activeTurnId: "turn-active" }).kind).toBe(
|
||||
"threadLifecycle",
|
||||
);
|
||||
expectNotificationRouteKind(notification, "threadLifecycle");
|
||||
expectNotificationRouteKind(notification, "threadLifecycle", { activeThreadId: "thread-other", activeTurnId: "turn-active" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
@ -64,16 +65,12 @@ describe("chat inbound routing", () => {
|
|||
{ name: "MCP elicitation", request: mcpElicitationRequest(), kind: "mcpElicitation" },
|
||||
{ name: "current time", request: currentTimeRequest("thread-active"), kind: "currentTime" },
|
||||
{ name: "dynamic tool call", request: dynamicToolCallRequest(), kind: "unsupported" },
|
||||
])("classifies $name server requests and extracts scope", ({ request, kind }) => {
|
||||
expect(routeServerRequest(request, activeScope).kind).toBe(kind);
|
||||
] as const)("classifies $name server requests and extracts scope", ({ request, kind }) => {
|
||||
expectRequestRouteKind(request, kind);
|
||||
if ("turnId" in request.params) {
|
||||
expect(
|
||||
routeServerRequest({ ...request, params: { ...request.params, turnId: "turn-other" } } as ServerRequest, activeScope).kind,
|
||||
).toBe("inactive");
|
||||
expectRequestRouteKind({ ...request, params: { ...request.params, turnId: "turn-other" } } as ServerRequest, "inactive");
|
||||
}
|
||||
expect(
|
||||
routeServerRequest({ ...request, params: { ...request.params, threadId: "thread-other" } } as ServerRequest, activeScope).kind,
|
||||
).toBe("inactive");
|
||||
expectRequestRouteKind({ ...request, params: { ...request.params, threadId: "thread-other" } } as ServerRequest, "inactive");
|
||||
});
|
||||
|
||||
it("marks scoped messages inactive when the thread or turn does not match", () => {
|
||||
|
|
@ -86,63 +83,55 @@ describe("chat inbound routing", () => {
|
|||
params: { threadId: "thread-active", turnId: "turn-other", itemId: "item", delta: "ignored" },
|
||||
} satisfies Extract<ServerNotification, { method: "item/agentMessage/delta" }>;
|
||||
|
||||
expect(routeServerNotification(otherThread, activeScope).kind).toBe("inactive");
|
||||
expect(routeServerNotification(otherTurn, activeScope).kind).toBe("inactive");
|
||||
expectNotificationRouteKind(otherThread, "inactive");
|
||||
expectNotificationRouteKind(otherTurn, "inactive");
|
||||
});
|
||||
|
||||
it("marks delayed stream updates inactive after the active thread returns to idle", () => {
|
||||
const idleActiveThreadScope = { activeThreadId: "thread-active", activeTurnId: null };
|
||||
|
||||
expect(routeServerNotification(agentDeltaNotification(), idleActiveThreadScope).kind).toBe("inactive");
|
||||
expectNotificationRouteKind(agentDeltaNotification(), "inactive", idleActiveThreadScope);
|
||||
});
|
||||
|
||||
it("marks delayed turn-scoped requests inactive after the active thread returns to idle", () => {
|
||||
const idleActiveThreadScope = { activeThreadId: "thread-active", activeTurnId: null };
|
||||
const request = userInputRequest({ threadId: "thread-active" });
|
||||
|
||||
expect(routeServerRequest(request, idleActiveThreadScope).kind).toBe("inactive");
|
||||
expectRequestRouteKind(request, "inactive", idleActiveThreadScope);
|
||||
});
|
||||
|
||||
it("routes thread catalog notifications even when another thread is active", () => {
|
||||
expect(routeServerNotification({ method: "thread/archived", params: { threadId: "thread-other" } }, activeScope).kind).toBe(
|
||||
"threadLifecycle",
|
||||
);
|
||||
expect(
|
||||
routeServerNotification({ method: "thread/name/updated", params: { threadId: "thread-other", threadName: "Renamed" } }, activeScope)
|
||||
.kind,
|
||||
).toBe("threadLifecycle");
|
||||
expect(routeServerNotification({ method: "thread/unarchived", params: { threadId: "thread-other" } }, activeScope).kind).toBe(
|
||||
expectNotificationRouteKind({ method: "thread/archived", params: { threadId: "thread-other" } }, "threadLifecycle");
|
||||
expectNotificationRouteKind(
|
||||
{ method: "thread/name/updated", params: { threadId: "thread-other", threadName: "Renamed" } },
|
||||
"threadLifecycle",
|
||||
);
|
||||
expectNotificationRouteKind({ method: "thread/unarchived", params: { threadId: "thread-other" } }, "threadLifecycle");
|
||||
});
|
||||
|
||||
it("keeps active-thread, broadcast, and targeted-thread notification routing distinct", () => {
|
||||
expect(routeServerNotification(threadSettingsUpdatedNotification(), activeScope).kind).toBe("threadLifecycle");
|
||||
expect(
|
||||
routeServerNotification(
|
||||
{
|
||||
method: "thread/settings/updated",
|
||||
params: { ...threadSettingsUpdatedNotification().params, threadId: "thread-other" },
|
||||
},
|
||||
activeScope,
|
||||
).kind,
|
||||
).toBe("inactive");
|
||||
expectNotificationRouteKind(threadSettingsUpdatedNotification(), "threadLifecycle");
|
||||
expectNotificationRouteKind(
|
||||
{
|
||||
method: "thread/settings/updated",
|
||||
params: { ...threadSettingsUpdatedNotification().params, threadId: "thread-other" },
|
||||
},
|
||||
"inactive",
|
||||
);
|
||||
|
||||
expect(routeServerNotification({ method: "skills/changed", params: {} }, activeScope).kind).toBe("diagnosticStatus");
|
||||
expect(routeServerNotification(threadArchivedNotification("thread-other"), activeScope).kind).toBe("threadLifecycle");
|
||||
expectNotificationRouteKind({ method: "skills/changed", params: {} }, "diagnosticStatus");
|
||||
expectNotificationRouteKind(threadArchivedNotification("thread-other"), "threadLifecycle");
|
||||
});
|
||||
|
||||
it("keeps active-thread-only lifecycle notifications scoped to the active thread", () => {
|
||||
const notification = threadSettingsUpdatedNotification();
|
||||
expect(
|
||||
routeServerNotification(
|
||||
{
|
||||
method: "thread/settings/updated",
|
||||
params: { ...notification.params, threadId: "thread-other" },
|
||||
},
|
||||
activeScope,
|
||||
).kind,
|
||||
).toBe("inactive");
|
||||
expectNotificationRouteKind(
|
||||
{
|
||||
method: "thread/settings/updated",
|
||||
params: { ...notification.params, threadId: "thread-other" },
|
||||
},
|
||||
"inactive",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
@ -150,8 +139,8 @@ describe("chat inbound routing", () => {
|
|||
{ name: "user input", request: userInputRequest(), kind: "userInput" },
|
||||
{ name: "MCP elicitation", request: mcpElicitationRequest(), kind: "mcpElicitation" },
|
||||
{ name: "current time", request: currentTimeRequest("thread-active"), kind: "currentTime" },
|
||||
])("classifies $name before unsupported requests", ({ request, kind }) => {
|
||||
expect(routeServerRequest(request, { activeThreadId: null, activeTurnId: null }).kind).toBe(kind);
|
||||
] as const)("classifies $name before unsupported requests", ({ request, kind }) => {
|
||||
expectRequestRouteKind(request, kind, { activeThreadId: null, activeTurnId: null });
|
||||
});
|
||||
|
||||
it("classifies inactive requests before request-family handling", () => {
|
||||
|
|
@ -166,7 +155,7 @@ describe("chat inbound routing", () => {
|
|||
request,
|
||||
})),
|
||||
)("keeps unscoped unsupported request $name out of active-turn routing", ({ request }) => {
|
||||
expect(routeServerRequest(request, activeScope).kind).toBe("unsupported");
|
||||
expectRequestRouteKind(request, "unsupported");
|
||||
});
|
||||
|
||||
it("routes unknown runtime server requests to the unknown fallback", () => {
|
||||
|
|
@ -176,16 +165,14 @@ describe("chat inbound routing", () => {
|
|||
params: { threadId: "thread-active", turnId: "turn-active" },
|
||||
} as unknown as ServerRequest;
|
||||
|
||||
expect(routeServerRequest(request, activeScope).kind).toBe("unknown");
|
||||
expect(
|
||||
routeServerRequest(
|
||||
{
|
||||
...request,
|
||||
params: { threadId: "thread-other", turnId: "turn-active" },
|
||||
} as unknown as ServerRequest,
|
||||
activeScope,
|
||||
).kind,
|
||||
).toBe("inactive");
|
||||
expectRequestRouteKind(request, "unknown");
|
||||
expectRequestRouteKind(
|
||||
{
|
||||
...request,
|
||||
params: { threadId: "thread-other", turnId: "turn-active" },
|
||||
} as unknown as ServerRequest,
|
||||
"inactive",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
@ -197,8 +184,8 @@ describe("chat inbound routing", () => {
|
|||
{ name: "server request resolved", notification: serverRequestResolvedNotification(), kind: "requestResolved" },
|
||||
{ name: "MCP startup status", notification: mcpStartupStatusNotification(), kind: "diagnosticStatus" },
|
||||
{ name: "warning", notification: warningNotification(), kind: "userVisibleNotice" },
|
||||
])("classifies $name notifications without mutating state", ({ notification, kind }) => {
|
||||
expect(routeServerNotification(notification, activeScope).kind).toBe(kind);
|
||||
] as const)("classifies $name notifications without mutating state", ({ notification, kind }) => {
|
||||
expectNotificationRouteKind(notification, kind);
|
||||
});
|
||||
|
||||
it("leaves unhandled app-server notifications explicit", () => {
|
||||
|
|
@ -219,16 +206,14 @@ describe("chat inbound routing", () => {
|
|||
params: { threadId: "thread-active", turnId: "turn-active" },
|
||||
} as unknown as ServerNotification;
|
||||
|
||||
expect(routeServerNotification(notification, activeScope).kind).toBe("unhandled");
|
||||
expect(
|
||||
routeServerNotification(
|
||||
{
|
||||
...notification,
|
||||
params: { threadId: "thread-active", turnId: "turn-other" },
|
||||
} as unknown as ServerNotification,
|
||||
activeScope,
|
||||
).kind,
|
||||
).toBe("inactive");
|
||||
expectNotificationRouteKind(notification, "unhandled");
|
||||
expectNotificationRouteKind(
|
||||
{
|
||||
...notification,
|
||||
params: { threadId: "thread-active", turnId: "turn-other" },
|
||||
} as unknown as ServerNotification,
|
||||
"inactive",
|
||||
);
|
||||
});
|
||||
|
||||
it("safely ignores unknown runtime notifications in the planner", () => {
|
||||
|
|
@ -249,25 +234,37 @@ describe("chat inbound routing", () => {
|
|||
{ name: "terminal interaction", notification: terminalInteractionNotification },
|
||||
{ name: "model verification", notification: modelVerificationNotification },
|
||||
])("still scopes unhandled turn notification $name", ({ notification }) => {
|
||||
expect(routeServerNotification(notification("thread-active", "turn-active"), activeScope).kind).toBe("unhandled");
|
||||
expect(routeServerNotification(notification("thread-other", "turn-active"), activeScope).kind).toBe("inactive");
|
||||
expect(routeServerNotification(notification("thread-active", "turn-other"), activeScope).kind).toBe("inactive");
|
||||
expectNotificationRouteKind(notification("thread-active", "turn-active"), "unhandled");
|
||||
expectNotificationRouteKind(notification("thread-other", "turn-active"), "inactive");
|
||||
expectNotificationRouteKind(notification("thread-active", "turn-other"), "inactive");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "thread status changed", notification: threadStatusChangedNotification },
|
||||
{ name: "thread closed", notification: threadClosedNotification },
|
||||
])("still scopes unhandled thread lifecycle notification $name", ({ notification }) => {
|
||||
expect(routeServerNotification(notification("thread-active"), activeScope).kind).toBe("unhandled");
|
||||
expect(routeServerNotification(notification("thread-other"), activeScope).kind).toBe("inactive");
|
||||
expectNotificationRouteKind(notification("thread-active"), "unhandled");
|
||||
expectNotificationRouteKind(notification("thread-other"), "inactive");
|
||||
});
|
||||
|
||||
it("scopes MCP startup status notifications when app-server provides a thread id", () => {
|
||||
expect(routeServerNotification(mcpStartupStatusNotificationForThread("thread-active"), activeScope).kind).toBe("diagnosticStatus");
|
||||
expect(routeServerNotification(mcpStartupStatusNotificationForThread("thread-other"), activeScope).kind).toBe("inactive");
|
||||
expectNotificationRouteKind(mcpStartupStatusNotificationForThread("thread-active"), "diagnosticStatus");
|
||||
expectNotificationRouteKind(mcpStartupStatusNotificationForThread("thread-other"), "inactive");
|
||||
});
|
||||
});
|
||||
|
||||
function expectNotificationRouteKind(
|
||||
notification: ServerNotification,
|
||||
expectedKind: NotificationRouteKind,
|
||||
scope: RouteScope = activeScope,
|
||||
): void {
|
||||
expect(routeServerNotification(notification, scope).kind).toBe(expectedKind);
|
||||
}
|
||||
|
||||
function expectRequestRouteKind(request: ServerRequest, expectedKind: RequestRouteKind, scope: RouteScope = activeScope): void {
|
||||
expect(routeServerRequest(request, scope).kind).toBe(expectedKind);
|
||||
}
|
||||
|
||||
function commandApprovalRequest(): ServerRequest {
|
||||
return {
|
||||
id: 1,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ function approvalActionOptions(approval: NonNullable<ReturnType<typeof toPending
|
|||
).actions;
|
||||
}
|
||||
|
||||
function approvalActionLabels(approval: NonNullable<ReturnType<typeof toPendingApproval>>): (string | null)[] {
|
||||
return approvalActionOptions(approval).map((option) => option.label);
|
||||
}
|
||||
|
||||
function approvalResponseAt(approval: NonNullable<ReturnType<typeof toPendingApproval>>, index: number): unknown {
|
||||
const option = expectPresent(approvalActionOptions(approval)[index]);
|
||||
return approvalResponse(approval, option.action);
|
||||
}
|
||||
|
||||
describe("approval model", () => {
|
||||
it("classifies command approvals and builds v2 decisions", () => {
|
||||
const request: ServerRequest = {
|
||||
|
|
@ -50,8 +59,8 @@ describe("approval model", () => {
|
|||
|
||||
expect(approval.title).toBe("Command approval");
|
||||
expect(approval.summary).toBe("npm run build");
|
||||
expect(approvalActionOptions(approval).map((option) => option.label)).toEqual(["Allow", "Allow session", "Deny", "Cancel"]);
|
||||
expect(approvalResponse(approval, expectPresent(approvalActionOptions(approval)[1]).action)).toEqual({ decision: "acceptForSession" });
|
||||
expect(approvalActionLabels(approval)).toEqual(["Allow", "Allow session", "Deny", "Cancel"]);
|
||||
expect(approvalResponseAt(approval, 1)).toEqual({ decision: "acceptForSession" });
|
||||
});
|
||||
|
||||
it("builds permission grants only for accept actions", () => {
|
||||
|
|
@ -129,9 +138,9 @@ describe("approval model", () => {
|
|||
expect(options.map((option) => option.label)).toEqual(["Allow network rule", "Allow network rule", "Deny"]);
|
||||
expect(options.map((option) => option.className)).toEqual(["", "", "mod-warning"]);
|
||||
expect(new Set(options.map((option) => option.id)).size).toBe(options.length);
|
||||
expect(approvalResponse(approval, expectPresent(options[0]).action)).toEqual({ decision: allowRegistryDecision });
|
||||
expect(approvalResponse(approval, expectPresent(options[1]).action)).toEqual({ decision: allowApiDecision });
|
||||
expect(approvalResponse(approval, expectPresent(options[2]).action)).toEqual({ decision: "decline" });
|
||||
expect(approvalResponseAt(approval, 0)).toEqual({ decision: allowRegistryDecision });
|
||||
expect(approvalResponseAt(approval, 1)).toEqual({ decision: allowApiDecision });
|
||||
expect(approvalResponseAt(approval, 2)).toEqual({ decision: "decline" });
|
||||
expect(createApprovalResultItem(approval, expectPresent(options[0]).action)).toMatchObject({
|
||||
approval: { status: "allowed for session" },
|
||||
});
|
||||
|
|
@ -171,7 +180,7 @@ describe("approval model", () => {
|
|||
className: "mod-warning",
|
||||
},
|
||||
]);
|
||||
expect(approvalResponse(approval, expectPresent(options[0]).action)).toEqual({ decision: futureDecision });
|
||||
expect(approvalResponseAt(approval, 0)).toEqual({ decision: futureDecision });
|
||||
});
|
||||
|
||||
it("falls back to generic command approval actions when app-server omits decisions", () => {
|
||||
|
|
@ -195,7 +204,7 @@ describe("approval model", () => {
|
|||
}),
|
||||
);
|
||||
|
||||
expect(approvalActionOptions(approval).map((option) => option.label)).toEqual(["Allow", "Allow session", "Deny", "Cancel"]);
|
||||
expect(approvalActionLabels(approval)).toEqual(["Allow", "Allow session", "Deny", "Cancel"]);
|
||||
expect(approvalResponse(approval, "accept-session")).toEqual({ decision: "acceptForSession" });
|
||||
});
|
||||
|
||||
|
|
@ -431,10 +440,9 @@ describe("approval model", () => {
|
|||
({ request, acceptSession, cancel }) => {
|
||||
const approval = expectPresent(toPendingApproval(request));
|
||||
if (approval.kind === "command") {
|
||||
const options = approvalActionOptions(approval);
|
||||
expect(approvalResponse(approval, expectPresent(options[0]).action)).toEqual(acceptSession);
|
||||
expect(approvalResponse(approval, expectPresent(options[1]).action)).toEqual(cancel);
|
||||
expect(approvalResponse(approval, expectPresent(options[2]).action)).toEqual({ decision: "decline" });
|
||||
expect(approvalResponseAt(approval, 0)).toEqual(acceptSession);
|
||||
expect(approvalResponseAt(approval, 1)).toEqual(cancel);
|
||||
expect(approvalResponseAt(approval, 2)).toEqual({ decision: "decline" });
|
||||
return;
|
||||
}
|
||||
expect(approvalResponse(approval, "accept-session")).toEqual(acceptSession);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { implementPlanTargetFromState } from "../../../../../src/features/chat/a
|
|||
import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/message-stream/content-events";
|
||||
import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer";
|
||||
import { deferred } from "../../../../support/async";
|
||||
import { topLevelDetailsSummaries } from "../../../../support/dom";
|
||||
import { attributeValues, textContents, topLevelDetailsSummaries } from "../../../../support/dom";
|
||||
import "./setup";
|
||||
import {
|
||||
expectPresent,
|
||||
|
|
@ -71,11 +71,11 @@ describe("message stream rendering and message action menu", () => {
|
|||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
[...parent.querySelectorAll<HTMLElement>("[data-codex-panel-block-key]")].map((element) =>
|
||||
element.getAttribute("data-codex-panel-block-key"),
|
||||
),
|
||||
).toEqual(["item:u1", "activity:turn-t1-activity", "item:a1"]);
|
||||
expect(attributeValues(parent, "[data-codex-panel-block-key]", "data-codex-panel-block-key")).toEqual([
|
||||
"item:u1",
|
||||
"activity:turn-t1-activity",
|
||||
"item:a1",
|
||||
]);
|
||||
const activitySummary = parent.querySelector<HTMLElement>('[data-codex-panel-block-key="activity:turn-t1-activity"] summary');
|
||||
expect(activitySummary?.textContent).toBe("Work details");
|
||||
expect(activitySummary?.tabIndex).toBe(-1);
|
||||
|
|
@ -205,13 +205,13 @@ describe("message stream rendering and message action menu", () => {
|
|||
|
||||
expect(element.classList.contains("codex-panel__execution--completed")).toBe(true);
|
||||
expect(topLevelDetailsSummaries(element)).toEqual(["auto-review"]);
|
||||
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["auto-review"]);
|
||||
expect(textContents(element, "details summary")).toEqual(["auto-review"]);
|
||||
expect(element.textContent).not.toContain("Details");
|
||||
expect(element.textContent).not.toContain("▶Review");
|
||||
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("statusapproved");
|
||||
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("actionapply patch");
|
||||
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("filessrc/ui/detail-view.ts\nsrc/ui/message-stream.ts");
|
||||
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual([]);
|
||||
expect(textContents(element, ".codex-panel__output-title")).toEqual([]);
|
||||
});
|
||||
|
||||
it("renders structured system result details as visible selectable meta rows", () => {
|
||||
|
|
@ -251,10 +251,7 @@ describe("message stream rendering and message action menu", () => {
|
|||
expect(element.querySelector(".codex-panel__output-title")).toBeNull();
|
||||
expect(element.querySelector(".codex-panel__meta-grid")).toBeNull();
|
||||
expect(element.querySelectorAll(".codex-panel__system-result-grid")).toHaveLength(1);
|
||||
expect([...element.querySelectorAll(".codex-panel__system-result-heading")].map((heading) => heading.textContent)).toEqual([
|
||||
"Thread",
|
||||
"Runtime",
|
||||
]);
|
||||
expect(textContents(element, ".codex-panel__system-result-heading")).toEqual(["Thread", "Runtime"]);
|
||||
expect(element.querySelector(".codex-panel__system-result-grid")?.textContent).toContain("/helpShow available Codex slash commands.");
|
||||
expect(element.querySelector(".codex-panel__system-result-grid")?.textContent).toContain(
|
||||
"/resume [thread]Resume a recent Codex thread.",
|
||||
|
|
@ -985,9 +982,9 @@ describe("message stream rendering and message action menu", () => {
|
|||
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("npm run check (exit 1)");
|
||||
expect(element.querySelector(".codex-panel__stream-summary")?.getAttribute("title")).toBeNull();
|
||||
expect(topLevelDetailsSummaries(element)).toEqual(["command"]);
|
||||
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["command"]);
|
||||
expect(textContents(element, "details summary")).toEqual(["command"]);
|
||||
expect(element.textContent).not.toContain("Details");
|
||||
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual(["Output"]);
|
||||
expect(textContents(element, ".codex-panel__output-title")).toEqual(["Output"]);
|
||||
expect(element.querySelector(".codex-panel__output pre")?.textContent).toBe("stderr details");
|
||||
expect(element.querySelector("details")?.hasAttribute("open")).toBe(false);
|
||||
});
|
||||
|
|
@ -1043,7 +1040,7 @@ describe("message stream rendering and message action menu", () => {
|
|||
|
||||
const element = renderMessageBlockElement(block);
|
||||
|
||||
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["read"]);
|
||||
expect(textContents(element, "details summary")).toEqual(["read"]);
|
||||
});
|
||||
|
||||
it("derives command summaries from semantic command targets instead of item text", () => {
|
||||
|
|
@ -1091,12 +1088,9 @@ describe("message stream rendering and message action menu", () => {
|
|||
|
||||
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("src/main.ts");
|
||||
expect(topLevelDetailsSummaries(element)).toEqual(["file change"]);
|
||||
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["file change"]);
|
||||
expect(textContents(element, "details summary")).toEqual(["file change"]);
|
||||
expect(element.textContent).not.toContain("Details");
|
||||
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual([
|
||||
"update src/main.ts",
|
||||
"Patch output",
|
||||
]);
|
||||
expect(textContents(element, ".codex-panel__output-title")).toEqual(["update src/main.ts", "Patch output"]);
|
||||
});
|
||||
|
||||
it("derives file change summaries from changes and status instead of item text", () => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
import type { PendingApproval, PendingMcpElicitation, PendingUserInput } from "../../../../../src/domain/pending-requests/model";
|
||||
import type { PendingRequestBlockContext } from "../../../../../src/features/chat/ui/message-stream/context";
|
||||
import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items";
|
||||
import { changeInputValue } from "../../../../support/dom";
|
||||
import { changeInputValue, textContents } from "../../../../support/dom";
|
||||
import "./setup";
|
||||
import {
|
||||
actEvent,
|
||||
|
|
@ -210,7 +210,7 @@ describe("pending request renderer decisions", () => {
|
|||
expect(parent.querySelector(".codex-panel__approval-details summary")?.textContent).toBe("Request details");
|
||||
expect(parent.querySelector<HTMLElement>(".codex-panel__approval-details summary")?.tabIndex).toBe(-1);
|
||||
expect(parent.querySelector(".codex-panel__user-input .codex-panel__pending-request-title")?.textContent).toBe("Codex needs input");
|
||||
expect([...parent.querySelectorAll(".codex-panel__pending-request-button")].map((button) => button.textContent)).toEqual([
|
||||
expect(textContents(parent, ".codex-panel__pending-request-button")).toEqual([
|
||||
"Allow",
|
||||
"Allow session",
|
||||
"Deny",
|
||||
|
|
@ -352,7 +352,7 @@ describe("pending request renderer decisions", () => {
|
|||
);
|
||||
|
||||
const buttons = [...parent.querySelectorAll<HTMLButtonElement>(".codex-panel__pending-request-button")];
|
||||
expect(buttons.map((button) => button.textContent)).toEqual(["Allow network rule", "Deny"]);
|
||||
expect(textContents(parent, ".codex-panel__pending-request-button")).toEqual(["Allow network rule", "Deny"]);
|
||||
const allowButton = buttons.at(0);
|
||||
if (!allowButton) throw new Error("Missing allow button");
|
||||
actEvent(() => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items";
|
||||
import { topLevelDetailsSummaries } from "../../../../support/dom";
|
||||
import { textContents, topLevelDetailsSummaries } from "../../../../support/dom";
|
||||
import "./setup";
|
||||
import {
|
||||
expectPresent,
|
||||
|
|
@ -40,13 +40,10 @@ describe("message stream item renderer decisions", () => {
|
|||
|
||||
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("123");
|
||||
expect(topLevelDetailsSummaries(element)).toEqual(["github.pull_request_read"]);
|
||||
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["github.pull_request_read"]);
|
||||
expect(textContents(element, "details summary")).toEqual(["github.pull_request_read"]);
|
||||
expect(element.querySelector<HTMLElement>("details summary")?.tabIndex).toBe(-1);
|
||||
expect(element.textContent).not.toContain("Details");
|
||||
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual([
|
||||
"Arguments JSON",
|
||||
"Result JSON",
|
||||
]);
|
||||
expect(textContents(element, ".codex-panel__output-title")).toEqual(["Arguments JSON", "Result JSON"]);
|
||||
});
|
||||
|
||||
it("renders steering activity as a compact two-line tool summary", () => {
|
||||
|
|
@ -220,13 +217,13 @@ describe("message stream item renderer decisions", () => {
|
|||
|
||||
expect(topLevelDetailsSummaries(element)).toEqual(["hook"]);
|
||||
expect(element.classList.contains("codex-panel__execution--completed")).toBe(true);
|
||||
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["hook"]);
|
||||
expect(textContents(element, "details summary")).toEqual(["hook"]);
|
||||
expect(element.querySelector(".codex-panel__stream-summary")?.textContent).toBe("postToolUse: Formatted 1 file.");
|
||||
expect(element.textContent).not.toContain("Details");
|
||||
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("statuscompleted");
|
||||
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("eventpostToolUse");
|
||||
expect(element.querySelector(".codex-panel__meta-grid")?.textContent).toContain("messageFormatted 1 file.");
|
||||
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual(["Hook output"]);
|
||||
expect(textContents(element, ".codex-panel__output-title")).toEqual(["Hook output"]);
|
||||
expect(element.querySelector(".codex-panel__output pre")?.textContent).toBe("feedback: ok");
|
||||
});
|
||||
|
||||
|
|
@ -497,7 +494,7 @@ describe("message stream item renderer decisions", () => {
|
|||
expect(element.querySelector(".codex-panel__message-role")?.textContent).toBe("agent");
|
||||
const summary = expectPresent(element.querySelector<HTMLElement>(".codex-panel__stream-summary"));
|
||||
expect(summary.textContent).toBe("spawn child: Inspect the renderer. (completed)");
|
||||
expect([...element.querySelectorAll("details summary")].map((detailsSummary) => detailsSummary.textContent)).toEqual(["agent"]);
|
||||
expect(textContents(element, "details summary")).toEqual(["agent"]);
|
||||
expect(element.textContent).toContain("targetchild");
|
||||
expect(element.textContent).toContain("PromptInspect the renderer.");
|
||||
expect(element.textContent).toContain("childcompleted: Done");
|
||||
|
|
@ -559,13 +556,11 @@ describe("message stream item renderer decisions", () => {
|
|||
|
||||
const element = renderMessageBlockElement(block);
|
||||
|
||||
const agentRows = [...element.querySelectorAll(".codex-panel__meta-grid dt, .codex-panel__meta-grid dd")].map(
|
||||
(node) => node.textContent,
|
||||
);
|
||||
const agentRows = textContents(element, ".codex-panel__meta-grid dt, .codex-panel__meta-grid dd");
|
||||
expect(agentRows).toContain("019e061e");
|
||||
expect(agentRows).toContain("completed: Done");
|
||||
expect(agentRows.join("")).not.toContain("a".repeat(180));
|
||||
expect([...element.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["agent"]);
|
||||
expect(textContents(element, "details summary")).toEqual(["agent"]);
|
||||
expect(element.querySelector<HTMLElement>("details summary")?.tabIndex).toBe(-1);
|
||||
expect(element.textContent).toContain("Agent output 019e061e");
|
||||
expect(element.textContent).toContain(longMessage);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ export function topLevelDetailsSummaries(element: HTMLElement): (string | null)[
|
|||
.map((details) => details.querySelector("summary")?.textContent ?? null);
|
||||
}
|
||||
|
||||
export function textContents(element: ParentNode, selector: string): (string | null)[] {
|
||||
return [...element.querySelectorAll(selector)].map((node) => node.textContent);
|
||||
}
|
||||
|
||||
export function attributeValues(element: ParentNode, selector: string, attribute: string): (string | null)[] {
|
||||
return [...element.querySelectorAll<HTMLElement>(selector)].map((node) => node.getAttribute(attribute));
|
||||
}
|
||||
|
||||
export function changeInputValue(input: HTMLInputElement | HTMLTextAreaElement, value: string): void {
|
||||
const prototype = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
||||
const valueDescriptor = Object.getOwnPropertyDescriptor(prototype, "value");
|
||||
|
|
|
|||
Loading…
Reference in a new issue