mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Add regression tests for hook and display ordering
This commit is contained in:
parent
35d9c2ea47
commit
d59a307f8a
3 changed files with 242 additions and 0 deletions
|
|
@ -856,6 +856,51 @@ describe("display block grouping keeps work logs subordinate to conversation mes
|
|||
expect(blocks[1]).toMatchObject({ summary: "Work details: command, thought" });
|
||||
});
|
||||
|
||||
it("groups completed hook and review logs before the final assistant message", () => {
|
||||
const items: DisplayItem[] = [
|
||||
{ id: "u1", kind: "message", role: "user", text: "do it", turnId: "t1" },
|
||||
{
|
||||
id: "hook-1",
|
||||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "userPromptSubmit: Saving jj baseline",
|
||||
toolLabel: "hook",
|
||||
turnId: "t1",
|
||||
status: "completed",
|
||||
},
|
||||
commandItem("c1", "npm test", "t1"),
|
||||
{ id: "r1", kind: "reasoning", role: "tool", text: "thinking", turnId: "t1", status: "completed" },
|
||||
{
|
||||
id: "review-1",
|
||||
kind: "reviewResult",
|
||||
role: "tool",
|
||||
text: "Auto-review approved: npm test",
|
||||
turnId: "t1",
|
||||
state: "completed",
|
||||
},
|
||||
{
|
||||
id: "review-2",
|
||||
kind: "reviewResult",
|
||||
role: "tool",
|
||||
text: "Auto-review approved: npm test",
|
||||
turnId: "t1",
|
||||
state: "completed",
|
||||
},
|
||||
{ id: "a1", kind: "message", role: "assistant", text: "done", turnId: "t1" },
|
||||
];
|
||||
|
||||
const blocks = displayBlocksForItems(items, null);
|
||||
|
||||
expect(blocks.map((block) => (block.type === "item" ? block.item.id : block.id))).toEqual(["u1", "turn-t1-activity", "a1"]);
|
||||
expect(blocks[1]).toMatchObject({
|
||||
summary: "Work details: command, hook, thought, 2 reviews",
|
||||
items: [{ id: "hook-1" }, { id: "c1" }, { id: "r1" }, { id: "review-1" }, { id: "review-2" }],
|
||||
});
|
||||
expect(blocks[2]).toMatchObject({
|
||||
item: { id: "a1", autoReviewSummaries: ["Auto-review approved: npm test", "Auto-review approved: npm test"] },
|
||||
});
|
||||
});
|
||||
|
||||
it("hides empty completed reasoning items", () => {
|
||||
const items: DisplayItem[] = [
|
||||
{ id: "u1", kind: "message", role: "user", text: "do it", turnId: "t1" },
|
||||
|
|
|
|||
|
|
@ -440,6 +440,75 @@ describe("PanelController", () => {
|
|||
expect(state.pendingTurnStart?.promptSubmitHookItemIds).toEqual(["hook-hook-1-1"]);
|
||||
});
|
||||
|
||||
it("keeps pre-turn prompt submit hooks through turn start and completed-turn reconciliation", () => {
|
||||
const state = createPanelState();
|
||||
state.activeThreadId = "thread-active";
|
||||
state.pendingTurnStart = { anchorItemId: "local-user-1", promptSubmitHookItemIds: [] };
|
||||
state.displayItems = [{ id: "local-user-1", kind: "message", role: "user", text: "hello", markdown: true }];
|
||||
const controller = controllerForState(state);
|
||||
|
||||
controller.handleNotification({
|
||||
method: "hook/completed",
|
||||
params: { threadId: "thread-active", turnId: null, run: promptSubmitHookRun("hook-1", 1n) },
|
||||
} satisfies Extract<ServerNotification, { method: "hook/completed" }>);
|
||||
|
||||
expect(state.displayItems.map((item) => item.id)).toEqual(["local-user-1", "hook-hook-1-1"]);
|
||||
expect(state.displayItems[1]?.turnId).toBeUndefined();
|
||||
expect(state.pendingTurnStart?.promptSubmitHookItemIds).toEqual(["hook-hook-1-1"]);
|
||||
|
||||
controller.handleNotification({
|
||||
method: "turn/started",
|
||||
params: {
|
||||
threadId: "thread-active",
|
||||
turn: {
|
||||
id: "turn-active",
|
||||
status: "inProgress",
|
||||
startedAt: 1,
|
||||
completedAt: null,
|
||||
durationMs: null,
|
||||
error: null,
|
||||
itemsView: "full",
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
} satisfies Extract<ServerNotification, { method: "turn/started" }>);
|
||||
|
||||
expect(state.displayItems.map((item) => item.id)).toEqual(["local-user-1", "hook-hook-1-1"]);
|
||||
expect(state.displayItems.find((item) => item.id === "local-user-1")).not.toHaveProperty("turnId");
|
||||
expect(state.displayItems.find((item) => item.id === "hook-hook-1-1")).toMatchObject({ turnId: "turn-active" });
|
||||
expect(state.pendingTurnStart).toBeNull();
|
||||
|
||||
controller.handleNotification({
|
||||
method: "turn/completed",
|
||||
params: {
|
||||
threadId: "thread-active",
|
||||
turn: {
|
||||
id: "turn-active",
|
||||
status: "completed",
|
||||
startedAt: 1,
|
||||
completedAt: 2,
|
||||
durationMs: 1,
|
||||
error: null,
|
||||
itemsView: "full",
|
||||
items: [
|
||||
{ type: "userMessage", id: "u1", content: [{ type: "text", text: "hello", text_elements: [] }] },
|
||||
{ type: "agentMessage", id: "a1", text: "done", phase: "final_answer", memoryCitation: null },
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies Extract<ServerNotification, { method: "turn/completed" }>);
|
||||
|
||||
expect(state.displayItems.map((item) => item.id)).toEqual(["hook-hook-1-1", "u1", "a1"]);
|
||||
expect(state.displayItems).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: "u1", text: "hello", turnId: "turn-active" }),
|
||||
expect.objectContaining({ id: "hook-hook-1-1", kind: "hook", turnId: "turn-active" }),
|
||||
expect.objectContaining({ id: "a1", text: "done", turnId: "turn-active" }),
|
||||
]),
|
||||
);
|
||||
expect(state.displayItems.some((item) => item.id === "local-user-1")).toBe(false);
|
||||
});
|
||||
|
||||
it("stores account rate limit updates outside thread scope", () => {
|
||||
const state = createPanelState();
|
||||
const controller = controllerForState(state);
|
||||
|
|
@ -1141,6 +1210,25 @@ function thread(id: string, cwd: string): Thread {
|
|||
};
|
||||
}
|
||||
|
||||
function promptSubmitHookRun(id: string, startedAt: bigint): Extract<ServerNotification, { method: "hook/completed" }>["params"]["run"] {
|
||||
return {
|
||||
id,
|
||||
eventName: "userPromptSubmit",
|
||||
handlerType: "command",
|
||||
executionMode: "sync",
|
||||
scope: "turn",
|
||||
sourcePath: "/vault/.codex/hooks.json",
|
||||
source: "project",
|
||||
displayOrder: 1n,
|
||||
status: "completed",
|
||||
statusMessage: "Saving jj baseline",
|
||||
startedAt,
|
||||
completedAt: startedAt + 1n,
|
||||
durationMs: 1n,
|
||||
entries: [],
|
||||
};
|
||||
}
|
||||
|
||||
function unsupportedRequests(): ServerRequest[] {
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -100,6 +100,68 @@ describe("message stream block identity and message actions", () => {
|
|||
expect([...parent.children]).toEqual([first, second]);
|
||||
});
|
||||
|
||||
it("inserts completed-turn activity groups without replacing stable conversation nodes", () => {
|
||||
const parent = document.createElement("div");
|
||||
const signatures = new Map<string, string>();
|
||||
const baseContext = {
|
||||
activeThreadId: "thread",
|
||||
activeTurnId: null,
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
busy: false,
|
||||
openDetails: new Set<string>(),
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (element: HTMLElement, text: string) => element.createDiv({ text }),
|
||||
renderTextWithWikiLinks: (element: HTMLElement, text: string) => element.createDiv({ text }),
|
||||
};
|
||||
|
||||
syncMessageRenderBlocks(
|
||||
parent,
|
||||
messageRenderBlocks({
|
||||
...baseContext,
|
||||
displayItems: [
|
||||
{ id: "u1", kind: "message", role: "user", text: "do it", turnId: "t1", markdown: true },
|
||||
{ id: "a1", kind: "message", role: "assistant", text: "done", turnId: "t1", markdown: true },
|
||||
],
|
||||
}),
|
||||
signatures,
|
||||
);
|
||||
const userNode = parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:u1"]');
|
||||
const assistantNode = parent.querySelector<HTMLElement>('[data-codex-panel-block-key="item:a1"]');
|
||||
|
||||
syncMessageRenderBlocks(
|
||||
parent,
|
||||
messageRenderBlocks({
|
||||
...baseContext,
|
||||
displayItems: [
|
||||
{ id: "u1", kind: "message", role: "user", text: "do it", turnId: "t1", markdown: true },
|
||||
{
|
||||
id: "hook-1",
|
||||
kind: "hook",
|
||||
role: "tool",
|
||||
text: "userPromptSubmit: Saving jj baseline",
|
||||
toolLabel: "hook",
|
||||
turnId: "t1",
|
||||
status: "completed",
|
||||
},
|
||||
{ id: "a1", kind: "message", role: "assistant", text: "done", turnId: "t1", markdown: true },
|
||||
],
|
||||
}),
|
||||
signatures,
|
||||
);
|
||||
|
||||
expect(parent.querySelector('[data-codex-panel-block-key="item:u1"]')).toBe(userNode);
|
||||
expect(parent.querySelector('[data-codex-panel-block-key="item:a1"]')).toBe(assistantNode);
|
||||
expect([...parent.children].map((element) => element.getAttribute("data-codex-panel-block-key"))).toEqual([
|
||||
"item:u1",
|
||||
"activity:turn-t1-activity",
|
||||
"item:a1",
|
||||
]);
|
||||
expect(parent.querySelector('[data-codex-panel-block-key="activity:turn-t1-activity"] summary')?.textContent).toBe(
|
||||
"Work details: hook",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not invalidate generic tool blocks when only the workspace root changes", () => {
|
||||
const item = {
|
||||
id: "tool",
|
||||
|
|
@ -1031,6 +1093,53 @@ describe("work log renderer decisions", () => {
|
|||
expect(element.querySelector(".codex-panel__output pre")?.textContent).toBe("feedback: ok");
|
||||
});
|
||||
|
||||
it("renders hook metadata when the hook is inside a completed-turn activity group", () => {
|
||||
const blocks = messageRenderBlocks({
|
||||
activeThreadId: "thread",
|
||||
activeTurnId: null,
|
||||
historyCursor: null,
|
||||
loadingHistory: false,
|
||||
busy: false,
|
||||
displayItems: [
|
||||
{ id: "u1", kind: "message", role: "user", text: "do it", turnId: "turn", markdown: true },
|
||||
{
|
||||
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" },
|
||||
{ key: "message", value: "Formatted 1 file." },
|
||||
],
|
||||
},
|
||||
{ title: "Hook output", body: "feedback: ok" },
|
||||
],
|
||||
},
|
||||
{ id: "a1", kind: "message", role: "assistant", text: "done", turnId: "turn", markdown: true },
|
||||
],
|
||||
openDetails: new Set(["turn:turn:activity", "hook-1"]),
|
||||
loadOlderTurns: vi.fn(),
|
||||
renderMarkdown: (parent, text) => parent.createDiv({ text }),
|
||||
renderTextWithWikiLinks: (parent, text) => parent.createDiv({ text }),
|
||||
});
|
||||
|
||||
const element = blocks.find((block) => block.key === "activity:turn-turn-activity")?.render();
|
||||
|
||||
expect(element).toBeDefined();
|
||||
expect(element?.querySelector(":scope > summary")?.textContent).toBe("Work details: hook");
|
||||
expect(element?.querySelector(".codex-panel__tool-summary")?.textContent).toBe("postToolUse: Formatted 1 file.");
|
||||
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__output-title")?.textContent).toBe("Hook output");
|
||||
expect(element?.querySelector(".codex-panel__output pre")?.textContent).toBe("feedback: ok");
|
||||
});
|
||||
|
||||
it("renders task progress items as a dedicated task list", () => {
|
||||
const block = messageRenderBlocks({
|
||||
activeThreadId: "thread",
|
||||
|
|
|
|||
Loading…
Reference in a new issue