Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
229 lines
6.4 KiB
TypeScript
229 lines
6.4 KiB
TypeScript
import { render, screen } from "@testing-library/react";
|
|
import { createElement } from "react";
|
|
import { describe, expect, it } from "vitest";
|
|
import type { AgentGUIConversationSummary } from "./model/agentGuiConversationModel";
|
|
import {
|
|
ConversationMeta,
|
|
filterConversationSectionsBySearchMatches
|
|
} from "./agentGuiNodeViewConversation";
|
|
|
|
describe("filterConversationSectionsBySearchMatches", () => {
|
|
it("removes sections without exact conversation id matches", () => {
|
|
const matching = conversation("matching", 2);
|
|
const unrelated = conversation("unrelated", 1);
|
|
|
|
const filtered = filterConversationSectionsBySearchMatches(
|
|
[
|
|
{
|
|
id: "project:app",
|
|
kind: "project",
|
|
label: "App",
|
|
project: null,
|
|
items: [matching]
|
|
},
|
|
{
|
|
id: "conversations",
|
|
kind: "conversations",
|
|
label: "Chats",
|
|
project: null,
|
|
items: [unrelated]
|
|
}
|
|
],
|
|
[matching]
|
|
);
|
|
|
|
expect(filtered.map((section) => section.id)).toEqual(["project:app"]);
|
|
expect(filtered[0]?.items.map((item) => item.id)).toEqual(["matching"]);
|
|
});
|
|
});
|
|
|
|
describe("ConversationMeta", () => {
|
|
it("uses a prominent spinner stroke for working conversations", () => {
|
|
const nowMs = new Date("2026-06-05T12:00:00Z").getTime();
|
|
const item = conversation("working", nowMs, { status: "working" });
|
|
|
|
render(
|
|
createElement(ConversationMeta, {
|
|
item,
|
|
nowMs,
|
|
labels: relativeLabels
|
|
})
|
|
);
|
|
|
|
const spinner = screen.getByTestId("agent-gui-conversation-spinner");
|
|
expect(
|
|
spinner.querySelectorAll('circle[stroke-width="2.25"]')
|
|
).toHaveLength(2);
|
|
});
|
|
|
|
it("keeps waiting background work in the running presentation", () => {
|
|
const nowMs = new Date("2026-06-05T12:00:00Z").getTime();
|
|
const item = conversation("background-waiting", nowMs, {
|
|
status: "waiting",
|
|
needsUserAction: false
|
|
});
|
|
|
|
render(
|
|
createElement(ConversationMeta, {
|
|
item,
|
|
nowMs,
|
|
labels: relativeLabels
|
|
})
|
|
);
|
|
|
|
expect(
|
|
screen.getByTestId("agent-gui-conversation-meta-background-waiting")
|
|
).toHaveAttribute("data-kind", "loading");
|
|
expect(screen.getByTestId("agent-gui-conversation-spinner")).toBeVisible();
|
|
});
|
|
|
|
it("keeps the worktree mark to the left of the loading indicator", () => {
|
|
const nowMs = new Date("2026-06-05T12:00:00Z").getTime();
|
|
const item = conversation("working-worktree", nowMs, {
|
|
status: "working",
|
|
isolation: {
|
|
mode: "worktree",
|
|
worktreePath: "/state/worktrees/session",
|
|
branch: "tutti/session",
|
|
baseCommit: "abc123"
|
|
}
|
|
});
|
|
|
|
const { container } = render(
|
|
createElement(ConversationMeta, {
|
|
item,
|
|
nowMs,
|
|
labels: relativeLabels
|
|
})
|
|
);
|
|
|
|
const meta = container.querySelector(".agent-gui-node__conversation-meta");
|
|
expect(meta?.firstElementChild).toHaveClass(
|
|
"agent-gui-node__conversation-worktree-glyph"
|
|
);
|
|
expect(meta?.lastElementChild).toHaveAttribute(
|
|
"data-testid",
|
|
"agent-gui-conversation-spinner"
|
|
);
|
|
});
|
|
|
|
it("shows the ask indicator whenever a conversation needs user action", () => {
|
|
const nowMs = new Date("2026-06-05T12:00:00Z").getTime();
|
|
const item = conversation("user-action-waiting", nowMs, {
|
|
status: "working",
|
|
needsUserAction: true
|
|
});
|
|
|
|
render(
|
|
createElement(ConversationMeta, {
|
|
item,
|
|
nowMs,
|
|
labels: relativeLabels
|
|
})
|
|
);
|
|
|
|
expect(
|
|
screen.getByTestId("agent-gui-conversation-meta-user-action-waiting")
|
|
).toHaveAttribute("data-kind", "waiting");
|
|
expect(
|
|
screen.queryByTestId("agent-gui-conversation-spinner")
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("shows a failed conversation instead of a stale user-action indicator", () => {
|
|
const nowMs = new Date("2026-06-05T12:00:00Z").getTime();
|
|
const item = conversation("failed-with-stale-action", nowMs, {
|
|
status: "failed",
|
|
needsUserAction: true
|
|
});
|
|
|
|
render(
|
|
createElement(ConversationMeta, {
|
|
item,
|
|
nowMs,
|
|
labels: relativeLabels
|
|
})
|
|
);
|
|
|
|
expect(
|
|
screen.getByTestId("agent-gui-conversation-meta-failed-with-stale-action")
|
|
).toHaveAttribute("data-kind", "failed");
|
|
});
|
|
|
|
it("displays the same sort time used by conversation ordering", () => {
|
|
const nowMs = new Date("2026-06-05T12:00:00Z").getTime();
|
|
const item = conversation("updated-newer-sort-older", nowMs - 60 * 1000, {
|
|
sortTimeUnixMs: nowMs - 5 * 60 * 1000
|
|
});
|
|
|
|
render(
|
|
createElement(ConversationMeta, {
|
|
item,
|
|
nowMs,
|
|
labels: relativeLabels
|
|
})
|
|
);
|
|
|
|
expect(
|
|
screen.getByTestId("agent-gui-conversation-meta-updated-newer-sort-older")
|
|
).toHaveTextContent("5 minutes");
|
|
});
|
|
|
|
it("shows the worktree mark alongside relative time outside hover state", () => {
|
|
const nowMs = new Date("2026-06-05T12:00:00Z").getTime();
|
|
const item = conversation("worktree", nowMs - 5 * 60 * 1000, {
|
|
isolation: {
|
|
mode: "worktree",
|
|
worktreePath: "/state/worktrees/session",
|
|
branch: "tutti/session",
|
|
baseCommit: "abc123"
|
|
}
|
|
});
|
|
|
|
const { container } = render(
|
|
createElement(ConversationMeta, {
|
|
item,
|
|
nowMs,
|
|
labels: relativeLabels
|
|
})
|
|
);
|
|
|
|
expect(screen.getByText("5 minutes")).toBeVisible();
|
|
expect(
|
|
container.querySelector(".agent-gui-node__conversation-worktree-glyph")
|
|
).toBeInTheDocument();
|
|
const meta = screen.getByTestId("agent-gui-conversation-meta-worktree");
|
|
expect(meta.firstElementChild).toHaveClass(
|
|
"agent-gui-node__conversation-worktree-glyph"
|
|
);
|
|
expect(meta.lastElementChild).toHaveClass(
|
|
"agent-gui-node__conversation-time"
|
|
);
|
|
});
|
|
});
|
|
|
|
const relativeLabels = {
|
|
relativeTimeJustNow: "Just now",
|
|
relativeTimeMinutes: (value: number) => `${value} minutes`,
|
|
relativeTimeHours: (value: number) => `${value} hours`,
|
|
relativeTimeDays: (value: number) => `${value} days`,
|
|
relativeTimeMonths: (value: number) => `${value} months`,
|
|
relativeTimeYears: (value: number) => `${value} years`
|
|
};
|
|
|
|
function conversation(
|
|
id: string,
|
|
updatedAtUnixMs: number,
|
|
overrides: Partial<AgentGUIConversationSummary> = {}
|
|
): AgentGUIConversationSummary {
|
|
return {
|
|
id,
|
|
provider: "codex",
|
|
title: id,
|
|
status: "ready",
|
|
cwd: "/workspace",
|
|
updatedAtUnixMs,
|
|
...overrides
|
|
};
|
|
}
|