Clarify repeated test assertions

This commit is contained in:
murashit 2026-06-24 11:44:40 +09:00
parent b148ada1d0
commit e57eb45933
7 changed files with 156 additions and 148 deletions

View file

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

View file

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

View file

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

View file

@ -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", () => {

View file

@ -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(() => {

View file

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

View file

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