mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Trim over-coupled chat tests
This commit is contained in:
parent
a92d993ced
commit
db50fb2ae5
8 changed files with 2 additions and 766 deletions
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() });
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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" };
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue