Trim over-coupled chat tests

This commit is contained in:
murashit 2026-06-11 18:03:15 +09:00
parent a92d993ced
commit db50fb2ae5
8 changed files with 2 additions and 766 deletions

View file

@ -1,163 +0,0 @@
// @vitest-environment jsdom
import { describe, expect, it, vi } from "vitest";
import type { App, Component, EventRef } from "obsidian";
import type { RuntimeSnapshot } from "../../../../src/features/chat/runtime/effective-settings";
import { createChatStateStore } from "../../../../src/features/chat/state/reducer";
import { createChatViewControllers } from "../../../../src/features/chat/panel/composition";
import type { ChatControllerCompositionPorts } from "../../../../src/features/chat/panel/controller-ports";
import { ChatConnectionWorkTracker, ChatResumeWorkTracker, ChatViewDeferredTasks } from "../../../../src/features/chat/panel/lifecycle";
import { ChatMessageScrollIntentController } from "../../../../src/features/chat/panel/message-scroll-intent-controller";
import type { ComposerMetaViewModel } from "../../../../src/features/chat/panel/view-model";
import { DEFAULT_SETTINGS } from "../../../../src/settings/model";
describe("createChatViewControllers", () => {
it("constructs the chat controller graph with direct shell nodes", () => {
const controllers = createChatViewControllers(createPorts());
expect(controllers.connection.controller).toBeTruthy();
expect(controllers.inbound.controller).toBeTruthy();
expect(controllers.thread.resume).toBeTruthy();
expect(controllers.render.messages).toBeTruthy();
expect(controllers.composer.controller).toBeTruthy();
expect(controllers.render.controller).toBeTruthy();
});
});
function createPorts(): ChatControllerCompositionPorts {
const stateStore = createChatStateStore();
const deferredTasks = new ChatViewDeferredTasks(() => window);
const root = document.createElement("div");
return {
obsidian: {
app: createApp(),
owner: createOwner(),
viewId: "test-view",
registerEvent: vi.fn(),
registerPointerDown: vi.fn(),
archiveAdapter: () => ({
exists: vi.fn().mockResolvedValue(false),
mkdir: vi.fn().mockResolvedValue(undefined),
write: vi.fn().mockResolvedValue(undefined),
}),
},
plugin: {
settings: DEFAULT_SETTINGS,
vaultPath: "/vault",
openThreadInNewView: vi.fn().mockResolvedValue(undefined),
focusThreadInOpenView: vi.fn().mockResolvedValue(false),
openTurnDiff: vi.fn().mockResolvedValue(undefined),
notifyThreadArchived: vi.fn(),
notifyThreadRenamed: vi.fn(),
refreshThreadsViewLiveState: vi.fn(),
refreshSharedThreadListFromOpenSurface: vi.fn(),
applyThreadListSnapshot: vi.fn(),
refreshThreadList: vi.fn(async (fetchThreads: () => Promise<readonly []>) => fetchThreads()),
cachedThreadList: () => null,
publishAppServerMetadata: vi.fn(),
cachedAppServerMetadata: () => null,
},
state: {
stateStore,
getState: () => stateStore.getState(),
systemItem: (text) => ({ id: "system", kind: "system", role: "system", text }),
structuredSystemItem: (text, details) => ({ id: "system", kind: "system", role: "system", text, details }),
},
client: {
getClient: () => null,
setClient: vi.fn(),
clear: vi.fn(),
},
lifecycle: {
deferredTasks,
resumeWork: new ChatResumeWorkTracker(),
connectionWork: new ChatConnectionWorkTracker(),
messageScrollIntent: new ChatMessageScrollIntentController(),
getOpened: () => false,
setOpened: vi.fn(),
getClosing: () => false,
setClosing: vi.fn(),
invalidateConnectionWork: vi.fn(),
scheduleDeferredDiagnostics: vi.fn(),
clearDeferredDiagnostics: vi.fn(),
scheduleDeferredRestoredThreadHydration: vi.fn(),
clearDeferredRestoredThreadHydration: vi.fn(),
scheduleDeferredAppServerWarmup: vi.fn(),
},
render: {
panelRoot: () => root,
toolbarNode: () => null,
goalNode: () => null,
messagesNode: () => null,
composerNode: () => null,
closeToolbarPanelOnOutsidePointer: vi.fn(),
schedule: vi.fn(),
},
messages: {
pendingRequestsSignature: () => "",
},
composerView: {
composerPlaceholder: () => "",
composerMetaViewModel: () => composerMeta(),
},
runtime: {
runtimeSnapshot: () => ({}) as RuntimeSnapshot,
collaborationModeLabel: () => "Plan",
connectionDiagnosticDetails: () => [],
modelStatusLines: () => [],
effortStatusLines: () => [],
statusSummaryLines: () => [],
},
thread: {
ensureRestoredThreadLoaded: vi.fn().mockResolvedValue(false),
startNewThread: vi.fn().mockResolvedValue(undefined),
loadSharedThreadList: vi.fn().mockResolvedValue(undefined),
notifyIdentityChanged: vi.fn(),
refreshTabHeader: vi.fn(),
},
liveState: {
refresh: vi.fn(),
deferRefresh: vi.fn(),
},
scroll: {
forceBottom: vi.fn(),
followBottom: vi.fn(),
preservePosition: vi.fn(),
},
status: {
set: vi.fn(),
},
};
}
function createApp(): App {
return {
vault: {
adapter: {},
on: vi.fn(() => ({}) as EventRef),
},
workspace: {
getActiveFile: () => null,
openLinkText: vi.fn(),
},
} as unknown as App;
}
function createOwner(): Component {
return {} as Component;
}
function composerMeta(): ComposerMetaViewModel {
return {
fatal: null,
context: { cells: [], percent: "--%" },
statusSummary: "",
model: "",
effort: null,
planActive: false,
autoReviewActive: false,
fastActive: false,
};
}

View file

@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import {
activeTurnId,
@ -173,34 +173,6 @@ describe("chatReducer", () => {
expect(withHistoryPanel.ui.openDetails).toBe(withDiff.ui.openDetails);
});
it("returns the same state reference for no-op actions", () => {
const state = createChatState();
expect(chatReducer(state, { type: "connection/status-set", status: "Idle" })).toBe(state);
expect(chatReducer(state, { type: "request/approval-queued", approval: approval(1) })).not.toBe(state);
const queued = chatReducer(state, { type: "request/approval-queued", approval: approval(1) });
expect(chatReducer(queued, { type: "request/approval-queued", approval: approval(1) })).toBe(queued);
});
it("keeps slice actions scoped to their owning state group", () => {
const connectionState = createChatState();
expectOnlySliceReferenceChanged(
chatReducer(connectionState, { type: "connection/status-set", status: "Working" }),
connectionState,
"connection",
);
const uiState = createChatState();
expectOnlySliceReferenceChanged(chatReducer(uiState, { type: "ui/panel-set", panel: "history" }), uiState, "ui");
const transcriptState = createChatState();
expectOnlySliceReferenceChanged(
chatReducer(transcriptState, { type: "transcript/items-replaced", items: [message("next")] }),
transcriptState,
"transcript",
);
});
it("deduplicates reported logs while keeping reported log state immutable", () => {
const state = createChatState();
const item = { id: "log", kind: "system", role: "system", text: "once" } satisfies DisplayItem;
@ -497,22 +469,6 @@ describe("chatReducer", () => {
});
expect(panelB.getState().requests.userInputDrafts.get("2:note")).toBe("panel B answer");
});
it("notifies ChatStateStore subscribers only when the state reference changes", () => {
const store = createChatStateStore();
const listener = vi.fn();
const unsubscribe = store.subscribe(listener);
store.dispatch({ type: "connection/status-set", status: "Idle" });
expect(listener).not.toHaveBeenCalled();
store.dispatch({ type: "connection/status-set", status: "Working" });
expect(listener).toHaveBeenCalledTimes(1);
unsubscribe();
store.dispatch({ type: "connection/status-set", status: "Done" });
expect(listener).toHaveBeenCalledTimes(1);
});
});
function message(id: string): DisplayItem {
@ -523,17 +479,6 @@ function suggestion(display: string): ChatState["composer"]["suggestions"][numbe
return { display, detail: "Plan mode", replacement: display, start: 0, appendSpaceOnInsert: true };
}
function expectOnlySliceReferenceChanged(next: ChatState, previous: ChatState, changedKey: keyof ChatState): void {
expect(next).not.toBe(previous);
for (const key of Object.keys(previous) as (keyof ChatState)[]) {
if (key === changedKey) {
expect(next[key]).not.toBe(previous[key]);
} else {
expect(next[key]).toBe(previous[key]);
}
}
}
function approval(requestId: number): ChatState["requests"]["approvals"][number] {
return {
requestId,

View file

@ -101,19 +101,6 @@ describe("createChatThreadActions", () => {
expect(host.openThreadInCurrentPanel).not.toHaveBeenCalled();
});
it("does not open the fork in a new panel before archiving the source", async () => {
const client = clientMock();
const host = hostMock({ client, displayItems: turnItems() });
const controller = createChatThreadActions(host);
await controller.forkThreadFromTurn("source", "turn-2", true);
expect(client.forkThread).toHaveBeenCalledWith("source", "/vault");
expect(client.rollbackThread).toHaveBeenCalledWith("forked", 1);
expect(host.openThreadInNewView).not.toHaveBeenCalled();
expect(client.archiveThread).toHaveBeenCalledWith("source");
});
it("saves the source before replacing the panel during fork and archive", async () => {
const client = clientMock();
const adapter = archiveAdapterMock();
@ -158,22 +145,6 @@ describe("createChatThreadActions", () => {
expect(host.addSystemMessage).toHaveBeenCalledWith("archive failed");
});
it("replaces the source panel before notifying surfaces after fork and archive succeeds", async () => {
const client = clientMock();
const host = hostMock({ client, displayItems: turnItems() });
const controller = createChatThreadActions(host);
await controller.forkThreadFromTurn("source", "turn-3", true);
expect(client.archiveThread).toHaveBeenCalledWith("source");
expect(host.openThreadInCurrentPanel).toHaveBeenCalledWith("forked");
expect(host.notifyThreadArchived).toHaveBeenCalledWith("source");
const openOrder = host.openThreadInCurrentPanel.mock.invocationCallOrder[0];
const notifyOrder = host.notifyThreadArchived.mock.invocationCallOrder[0];
if (openOrder === undefined || notifyOrder === undefined) throw new Error("Expected open and archive notification calls.");
expect(openOrder).toBeLessThan(notifyOrder);
});
it("notifies surfaces when fork and archive succeeds but the fork cannot replace the source panel", async () => {
const client = clientMock();
const host = hostMock({ client, displayItems: turnItems() });

View file

@ -16,7 +16,6 @@ import {
messageStreamBlocks,
renderMessageBlockElement,
renderMessageStreamBlocksInAct,
renderUiRootInAct,
runningTurnLifecycle,
startingTurnLifecycle,
unmountUiRootInAct,
@ -93,58 +92,6 @@ describe("message stream rendering and message actions", () => {
unmountUiRootInAct(parent);
});
it("renders the history bar as a Preact block", () => {
const loadOlderTurns = vi.fn();
const [historyBlock] = messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: idleTurnLifecycle(),
historyCursor: "cursor",
loadingHistory: false,
displayItems: [],
openDetails: new Set(),
loadOlderTurns,
renderMarkdown: (element, text) => element.createDiv({ text }),
});
const parent = document.createElement("div");
expect(historyBlock.key).toBe("history-bar");
expect(historyBlock.node).not.toBeUndefined();
renderUiRootInAct(parent, historyBlock.node);
const button = expectPresent(parent.querySelector<HTMLButtonElement>("button"));
expect(parent.querySelector(".codex-panel__history-bar")).not.toBeNull();
expect(button.textContent).toBe("Load older");
expect(button.disabled).toBe(false);
button.click();
expect(loadOlderTurns).toHaveBeenCalledOnce();
unmountUiRootInAct(parent);
});
it("renders the empty message stream state as a Preact block", () => {
const [emptyBlock] = messageStreamBlocks({
activeThreadId: null,
turnLifecycle: idleTurnLifecycle(),
historyCursor: null,
loadingHistory: false,
displayItems: [],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
});
const parent = document.createElement("div");
expect(emptyBlock.key).toBe("empty");
expect(emptyBlock.node).not.toBeUndefined();
renderUiRootInAct(parent, emptyBlock.node);
const empty = expectPresent(parent.querySelector<HTMLElement>(".codex-panel__message--system"));
expect(empty.classList.contains("codex-panel__message")).toBe(true);
expect(empty.textContent).toBe("Send a message to start a conversation.");
unmountUiRootInAct(parent);
});
it("renders review result items as compact auto-review tool rows", () => {
const block = messageStreamBlocks({
activeThreadId: "thread",
@ -212,89 +159,6 @@ describe("message stream rendering and message actions", () => {
expect([...element.querySelectorAll(".codex-panel__output-title")].map((title) => title.textContent)).toEqual([]);
});
it("keeps tool result Preact details mounted in the message stream host", () => {
const parent = document.createElement("div");
const onDetailsToggle = vi.fn();
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: idleTurnLifecycle(),
historyCursor: null,
loadingHistory: false,
displayItems: [
{
id: "cmd-1",
kind: "command",
role: "tool",
text: "npm test",
command: "npm test",
cwd: "/vault",
status: "completed",
output: "ok",
executionState: "completed",
},
],
openDetails: new Set(),
onDetailsToggle,
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
);
const block = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:cmd-1"]'));
const result = expectPresent(block.querySelector<HTMLElement>(".codex-panel__tool-result"));
expect(result.classList.contains("codex-panel__execution--completed")).toBe(true);
expect(result.querySelector(".codex-panel__tool-result-header")?.textContent).toBe("command");
expect(result.querySelector(":scope > .codex-panel__tool-summary")?.textContent).toBe("npm test");
expect(result.querySelector(".codex-panel__tool-summary")?.textContent).toBe("npm test");
expect(result.querySelector(".codex-panel__meta-grid")?.textContent).toContain("commandnpm test");
expect(result.querySelector(".codex-panel__output-title")?.textContent).toBe("Output");
const details = expectPresent(result.querySelector<HTMLDetailsElement>("details"));
void act(() => {
details.open = true;
details.dispatchEvent(new Event("toggle", { bubbles: false }));
});
expect(onDetailsToggle).toHaveBeenCalledWith("cmd-1:command-details", true);
unmountUiRootInAct(parent);
});
it("renders file change diffs through the Preact tool result adapter", () => {
const parent = document.createElement("div");
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: idleTurnLifecycle(),
historyCursor: null,
loadingHistory: false,
workspaceRoot: "/vault",
displayItems: [
{
id: "file-1",
kind: "fileChange",
role: "tool",
text: "Changed 1 file",
status: "completed",
changes: [{ kind: "modified", path: "/vault/src/app.ts", diff: "-old\n+new" }],
},
],
openDetails: new Set(["file-1:file-change-details"]),
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
);
expect(parent.querySelector(".codex-panel__tool-summary")?.textContent).toBe("src/app.ts");
expect(parent.querySelector(".codex-panel-diff-file .codex-panel__output-title")?.textContent).toBe("modified src/app.ts");
expect([...parent.querySelectorAll(".codex-panel-diff__line")].map((line) => line.textContent)).toEqual(["old", "new"]);
unmountUiRootInAct(parent);
});
it("renders structured system result details as visible selectable meta rows", () => {
const renderMarkdown = vi.fn((parent: HTMLElement, text: string) => parent.createDiv({ text: `markdown:${text}` }));
const block = messageStreamBlocks({
@ -538,83 +402,6 @@ describe("message stream rendering and message actions", () => {
expect(onForkItem).toHaveBeenCalledWith(expect.objectContaining({ id: "a1" }), true);
});
it("keeps message Preact actions mounted in the message stream host", () => {
const parent = document.createElement("div");
const copyText = vi.fn();
const onImplementPlanItem = vi.fn();
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: idleTurnLifecycle(),
historyCursor: null,
loadingHistory: false,
displayItems: [
{
id: "p1",
kind: "message",
role: "assistant",
text: "Plan",
copyText: "Plan",
turnId: "turn-1",
messageKind: "proposedPlan",
messageState: "streaming",
},
],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
copyText,
canImplementPlanItem: () => true,
onImplementPlanItem,
}),
);
parent.querySelector<HTMLButtonElement>(".codex-panel__copy-message")?.click();
parent.querySelector<HTMLButtonElement>(".codex-panel__implement-plan")?.click();
expect(copyText).toHaveBeenCalledWith("Plan");
expect(onImplementPlanItem).toHaveBeenCalledWith(expect.objectContaining({ id: "p1" }));
expect(parent.querySelector('[data-codex-panel-block-key="item:p1"] .codex-panel__message--assistant')).not.toBeNull();
unmountUiRootInAct(parent);
});
it("renders message markdown through the Preact content adapter", () => {
const parent = document.createElement("div");
const renderMarkdown = vi.fn((element: HTMLElement, text: string) => {
element.createDiv({ text: `rendered:${text}` });
});
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: idleTurnLifecycle(),
historyCursor: null,
loadingHistory: false,
displayItems: [
{
id: "a1",
kind: "message",
role: "assistant",
text: "**answer**",
turnId: "turn-1",
messageKind: "assistantResponse",
messageState: "completed",
},
],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown,
}),
);
expect(renderMarkdown).toHaveBeenCalledWith(expect.any(HTMLElement), "**answer**");
expect(parent.querySelector(".codex-panel__message-content")?.textContent).toBe("rendered:**answer**");
unmountUiRootInAct(parent);
});
it("updates message content when a streaming plan delta completes", () => {
const parent = document.createElement("div");
const baseContext = {

View file

@ -561,40 +561,6 @@ describe("pending request renderer decisions", () => {
expect(consumeAutoFocus).not.toHaveBeenCalled();
});
it("keeps pending request Preact events mounted in the message stream host", () => {
const parent = document.createElement("div");
const approval = pendingApproval();
const resolveApproval = vi.fn();
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: idleTurnLifecycle(),
historyCursor: null,
loadingHistory: false,
displayItems: [
{ id: "a1", kind: "message", role: "assistant", text: "Waiting", messageKind: "assistantResponse", messageState: "completed" },
],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
pendingRequests: pendingRequestContext({
signature: "approval:1",
snapshot: emptyPendingRequestSnapshot({ approvals: [approval] }),
actions: pendingRequestActions({ resolveApproval }),
}),
}),
);
actEvent(() => {
parent.querySelector<HTMLButtonElement>(".codex-panel__pending-request-button")?.click();
});
expect(resolveApproval).toHaveBeenCalledWith(approval, "accept");
unmountUiRootInAct(parent);
});
it("removes pending request blocks when the signature clears", () => {
const parent = document.createElement("div");
const baseContext = {

View file

@ -83,7 +83,7 @@ export function installMessageViewportMetrics(
}) as typeof element.scrollTo;
}
export function renderUiRootInAct(parent: HTMLElement, node: UiNode): void {
function renderUiRootInAct(parent: HTMLElement, node: UiNode): void {
void act(() => {
renderUiRoot(parent, node);
});

View file

@ -1,7 +1,6 @@
// @vitest-environment jsdom
import { describe, expect, it, vi } from "vitest";
import { act } from "preact/test-utils";
import type { DisplayItem } from "../../../../../src/features/chat/display/types";
import { topLevelDetailsSummaries } from "../../../../support/dom";
@ -396,87 +395,6 @@ describe("work log renderer decisions", () => {
expect(element.querySelector(".codex-panel__output pre")?.textContent).toBe("feedback: ok");
});
it("keeps completed-turn activity group items mounted through Preact", () => {
const parent = document.createElement("div");
const onDetailsToggle = vi.fn();
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: idleTurnLifecycle(),
historyCursor: null,
loadingHistory: false,
displayItems: [
{ id: "u1", kind: "message", messageKind: "user", role: "user", text: "do it", turnId: "turn" },
{
id: "hook-1",
kind: "hook",
role: "tool",
text: "postToolUse: Formatted 1 file.",
toolLabel: "hook",
turnId: "turn",
status: "completed",
details: [
{
rows: [
{ key: "status", value: "completed" },
{ key: "event", value: "postToolUse" },
],
},
{ title: "Hook output", body: "feedback: ok" },
],
},
{
id: "agent-1",
kind: "agent",
role: "tool",
text: "Spawn agent",
turnId: "turn",
tool: "spawnAgent",
status: "completed",
senderThreadId: "parent",
receiverThreadIds: ["child"],
prompt: null,
model: null,
reasoningEffort: null,
agents: [{ threadId: "child", status: "completed", message: "Done" }],
},
{
id: "a1",
kind: "message",
role: "assistant",
text: "done",
turnId: "turn",
messageKind: "assistantResponse",
messageState: "completed",
},
],
openDetails: new Set(["turn:turn:activity", "hook-1:details"]),
onDetailsToggle,
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
);
const group = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="activity:turn-turn-activity"]'));
expect(group.querySelector(":scope > .codex-panel__activity-group > summary")?.textContent).toBe("Work details: agent, hook");
expect(group.querySelector(".codex-panel__tool-result .codex-panel__tool-result-header")?.textContent).toBe("hook");
expect(group.querySelector(".codex-panel__tool-result > .codex-panel__tool-summary")?.textContent).toBe(
"postToolUse: Formatted 1 file.",
);
expect(group.querySelector(".codex-panel__agent-activity .codex-panel__tool-summary")?.textContent).toBe("spawn child (completed)");
const details = expectPresent(group.querySelector<HTMLDetailsElement>(".codex-panel__activity-group"));
void act(() => {
details.open = false;
details.dispatchEvent(new Event("toggle", { bubbles: false }));
});
expect(onDetailsToggle).toHaveBeenCalledWith("turn:turn:activity", false);
unmountUiRootInAct(parent);
});
it("renders task progress items as a dedicated task list", () => {
const block = messageStreamBlocks({
activeThreadId: "thread",
@ -512,44 +430,6 @@ describe("work log renderer decisions", () => {
expect(element.textContent).toContain("[>]Patch UI");
});
it("keeps task progress Preact items mounted in the message stream host", () => {
const parent = document.createElement("div");
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: runningTurnLifecycle("turn"),
historyCursor: null,
loadingHistory: false,
displayItems: [
{
id: "plan-progress-turn",
kind: "taskProgress",
role: "tool",
text: "Plan\n[>] Patch UI",
turnId: "turn",
explanation: "Plan",
steps: [
{ step: "Inspect code", status: "completed" },
{ step: "Patch UI", status: "inProgress" },
],
status: "inProgress",
},
],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
);
const block = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="live-task:plan-progress-turn"]'));
expect(block.querySelector(".codex-panel__task-progress .codex-panel__message-role")?.textContent).toBe("tasks");
expect(block.textContent).toContain("[x]Inspect code");
expect(block.textContent).toContain("[>]Patch UI");
unmountUiRootInAct(parent);
});
it("renders active task progress with the shared bottom live blocks", () => {
const blocks = messageStreamBlocks({
activeThreadId: "thread",
@ -771,57 +651,6 @@ describe("work log renderer decisions", () => {
expect(element.textContent).toContain("childcompleted: Done");
});
it("keeps agent Preact details mounted in the message stream host", () => {
const parent = document.createElement("div");
const onDetailsToggle = vi.fn();
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: runningTurnLifecycle("turn"),
historyCursor: null,
loadingHistory: false,
displayItems: [
{
id: "agent-1",
kind: "agent",
role: "tool",
text: "Spawn agent",
turnId: "turn",
tool: "spawnAgent",
status: "completed",
senderThreadId: "parent",
receiverThreadIds: ["child"],
prompt: "Inspect the renderer.",
model: "gpt-5.5",
reasoningEffort: "high",
agents: [{ threadId: "child", status: "completed", message: "Done" }],
},
],
openDetails: new Set(),
onDetailsToggle,
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
);
const block = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:agent-1"]'));
expect(block.querySelector(".codex-panel__agent-activity .codex-panel__message-role")?.textContent).toBe("agent");
expect(block.querySelector(".codex-panel__tool-summary")?.textContent).toBe("spawn child: Inspect the renderer. (completed)");
expect([...block.querySelectorAll("details summary")].map((summary) => summary.textContent)).toEqual(["Details"]);
const details = expectPresent(block.querySelector<HTMLDetailsElement>("details"));
void act(() => {
details.open = true;
details.dispatchEvent(new Event("toggle", { bubbles: false }));
});
expect(onDetailsToggle).toHaveBeenCalledWith("agent-1:agent-details", true);
expect(expectPresent(block.querySelector(".codex-panel__agent-activity")).classList.contains("is-open")).toBe(true);
unmountUiRootInAct(parent);
});
it("keeps agent activity prompt previews visually constrained to one line", () => {
const block = messageStreamBlocks({
activeThreadId: "thread",
@ -944,74 +773,6 @@ describe("work log renderer decisions", () => {
expect(summary.textContent).not.toContain("donecompleted");
});
it("renders the compact live agent summary as a Preact block", () => {
const parent = document.createElement("div");
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: runningTurnLifecycle("turn"),
historyCursor: null,
loadingHistory: false,
displayItems: [
{
id: "agent-1",
kind: "agent",
role: "tool",
text: "Wait for agent",
turnId: "turn",
tool: "wait",
status: "running",
senderThreadId: "parent",
receiverThreadIds: ["done", "running"],
prompt: null,
model: null,
reasoningEffort: null,
agents: [
{ threadId: "done", status: "completed", message: null },
{ threadId: "running", status: "running", message: "Inspecting renderer" },
],
},
],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
);
const summary = expectPresent(parent.querySelector<HTMLElement>('[data-codex-panel-block-key="live-agents:turn"]'));
expect(summary.querySelector(".codex-panel__agent-summary")?.textContent).toContain("Agents 1 running, 1 done");
expect(summary.textContent).toContain("runningrunning: Inspecting renderer");
unmountUiRootInAct(parent);
});
it("renders active reasoning as a Preact message stream block", () => {
const parent = document.createElement("div");
renderMessageStreamBlocksInAct(
parent,
messageStreamBlocks({
activeThreadId: "thread",
turnLifecycle: runningTurnLifecycle("turn"),
historyCursor: null,
loadingHistory: false,
displayItems: [{ id: "reasoning-1", kind: "reasoning", role: "tool", text: "", turnId: "turn", status: "running" }],
openDetails: new Set(),
loadOlderTurns: vi.fn(),
renderMarkdown: (element, text) => element.createDiv({ text }),
}),
);
const reasoning = expectPresent(
parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:reasoning-1"] .codex-panel__reasoning'),
);
expect(reasoning.classList.contains("is-active")).toBe(true);
expect(reasoning.querySelector(".codex-panel__reasoning-role")?.textContent).toBe("reasoning");
expect(reasoning.querySelector(".codex-panel__reasoning-content")?.textContent).toBe("Reasoning...");
unmountUiRootInAct(parent);
});
it("renders context compaction as a one-line work item while running and after completion", () => {
const runningParent = document.createElement("div");
const item: DisplayItem = { id: "compact-1", kind: "contextCompaction", role: "tool", text: "Context compaction", turnId: "turn" };

View file

@ -195,37 +195,6 @@ describe("CodexChatView connection lifecycle", () => {
expect(siblingRoots.every((sibling) => !sibling.classList.contains("codex-panel") && sibling.childElementCount === 0)).toBe(true);
});
it("unmounts the chat shell from the view content root on close", async () => {
const view = await chatView();
const siblingRoots = Array.from(view.containerEl.children).filter((child) => child !== view.contentEl);
await view.onOpen();
await view.onClose();
expect(view.contentEl.classList.contains("codex-panel")).toBe(true);
expect(view.contentEl.childElementCount).toBe(0);
expect(siblingRoots.every((sibling) => !sibling.classList.contains("codex-panel") && sibling.childElementCount === 0)).toBe(true);
});
it("wires open lifecycle registrations through the view controller host", async () => {
const addEventListener = vi.spyOn(document, "addEventListener");
const cachedThreadList = vi.fn(() => null);
const cachedAppServerMetadata = vi.fn(() => null);
const host = chatHost({
cachedThreadList,
cachedAppServerMetadata,
});
const view = await chatView({ host });
await view.onOpen();
expect(addEventListener).toHaveBeenCalledWith("pointerdown", expect.any(Function));
expect(cachedThreadList).toHaveBeenCalledOnce();
expect(cachedAppServerMetadata).toHaveBeenCalledOnce();
addEventListener.mockRestore();
});
it("starts an empty thread when saving a toolbar goal from a blank panel", async () => {
vi.useFakeTimers();
const client = connectedClient({