Files
paseo/packages/app/src/screens/workspace/workspace-tab-model.test.ts
T

216 lines
6.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Agent } from "@/stores/session-store";
import { deriveWorkspaceTabModel } from "@/screens/workspace/workspace-tab-model";
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
function makeAgent(input: {
id: string;
provider?: Agent["provider"];
title?: string | null;
createdAt?: Date;
lastActivityAt?: Date;
requiresAttention?: boolean;
attentionReason?: Agent["attentionReason"];
}): Agent {
const createdAt = input.createdAt ?? new Date("2026-03-04T00:00:00.000Z");
const lastActivityAt = input.lastActivityAt ?? createdAt;
return {
serverId: "srv",
id: input.id,
provider: input.provider ?? "codex",
status: "idle",
createdAt,
updatedAt: createdAt,
lastUserMessageAt: null,
lastActivityAt,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: {
provider: input.provider ?? "codex",
sessionId: null,
},
title: input.title ?? null,
cwd: "/repo/worktree",
model: null,
thinkingOptionId: null,
labels: {},
requiresAttention: input.requiresAttention ?? false,
attentionReason: input.attentionReason ?? null,
attentionTimestamp: null,
archivedAt: null,
};
}
describe("deriveWorkspaceTabModel", () => {
it("derives agent and terminal tabs from domain state, not UI membership", () => {
const model = deriveWorkspaceTabModel({
workspaceAgents: [
makeAgent({ id: "agent-a", title: "Build API" }),
makeAgent({ id: "agent-b", title: "" }),
],
terminals: [{ id: "term-1", name: "shell" }],
uiTabs: [],
tabOrder: [],
});
expect(model.tabs.map((tab) => tab.descriptor.tabId)).toEqual([
"agent_agent-a",
"agent_agent-b",
"terminal_term-1",
]);
const firstAgent = model.tabs[0]?.descriptor;
const secondAgent = model.tabs[1]?.descriptor;
expect(firstAgent?.kind === "agent" ? firstAgent.titleState : null).toBe("ready");
expect(secondAgent?.kind === "agent" ? secondAgent.titleState : null).toBe("loading");
expect(secondAgent?.label).toBe("");
});
it("keeps draft and file tabs as explicit UI tabs", () => {
const uiTabs: WorkspaceTab[] = [
{
tabId: "draft_123",
target: { kind: "draft", draftId: "draft_123" },
createdAt: 1,
},
{
tabId: "file_/repo/worktree/README.md",
target: { kind: "file", path: "/repo/worktree/README.md" },
createdAt: 2,
},
];
const model = deriveWorkspaceTabModel({
workspaceAgents: [makeAgent({ id: "agent-a", title: "A" })],
terminals: [],
uiTabs,
tabOrder: ["draft_123", "agent_agent-a", "file_/repo/worktree/README.md"],
});
expect(model.tabs.map((tab) => tab.descriptor.kind)).toEqual(["draft", "agent", "file"]);
});
it("applies stored order and appends newly-derived tabs deterministically", () => {
const model = deriveWorkspaceTabModel({
workspaceAgents: [makeAgent({ id: "agent-a" }), makeAgent({ id: "agent-b" })],
terminals: [{ id: "term-1", name: "zsh" }],
uiTabs: [],
tabOrder: ["terminal_term-1", "agent_agent-b"],
});
expect(model.tabs.map((tab) => tab.descriptor.tabId)).toEqual([
"terminal_term-1",
"agent_agent-b",
"agent_agent-a",
]);
});
it("uses focused tab when present, otherwise falls back to first tab", () => {
const base = {
workspaceAgents: [makeAgent({ id: "agent-a" }), makeAgent({ id: "agent-b" })],
terminals: [],
uiTabs: [],
tabOrder: ["agent_agent-a", "agent_agent-b"],
};
expect(
deriveWorkspaceTabModel({
...base,
focusedTabId: "agent_agent-b",
}).activeTabId
).toBe("agent_agent-b");
expect(
deriveWorkspaceTabModel({
...base,
focusedTabId: "agent_agent-b",
}).activeTabId
).toBe("agent_agent-b");
});
it("re-resolves active content for a new workspace when prior focused tab is not available", () => {
const model = deriveWorkspaceTabModel({
workspaceAgents: [makeAgent({ id: "workspace-b-agent", title: "B" })],
terminals: [],
uiTabs: [],
tabOrder: ["agent_workspace-b-agent"],
focusedTabId: "agent_workspace-a-agent",
});
expect(model.activeTabId).toBe("agent_workspace-b-agent");
expect(model.activeTab?.target).toEqual({
kind: "agent",
agentId: "workspace-b-agent",
});
});
it("covers regression: non-archived attention agent remains visible even if UI tabs omitted it", () => {
const offending = makeAgent({
id: "offender",
title: "Needs permission",
requiresAttention: true,
attentionReason: "permission",
});
const model = deriveWorkspaceTabModel({
workspaceAgents: [offending],
terminals: [],
uiTabs: [
{
tabId: "draft_123",
target: { kind: "draft", draftId: "draft_123" },
createdAt: 1,
},
],
tabOrder: ["draft_123"],
});
expect(model.tabs.some((tab) => tab.descriptor.tabId === "agent_offender")).toBe(true);
});
it("includes older attention and failure agents in workspace tabs when session data is complete", () => {
const olderPermissionAgent = makeAgent({
id: "agent-permission-old",
title: "Need permission",
createdAt: new Date("2026-01-15T00:00:00.000Z"),
requiresAttention: true,
attentionReason: "permission",
});
const olderFailedAgent = makeAgent({
id: "agent-failed-old",
title: "Failed run",
createdAt: new Date("2026-01-10T00:00:00.000Z"),
requiresAttention: true,
attentionReason: "error",
});
const newerAgent = makeAgent({
id: "agent-recent",
title: "Recent work",
createdAt: new Date("2026-03-04T00:00:00.000Z"),
});
const model = deriveWorkspaceTabModel({
workspaceAgents: [newerAgent, olderPermissionAgent, olderFailedAgent],
terminals: [],
uiTabs: [],
tabOrder: [],
});
expect(model.tabs.some((tab) => tab.descriptor.tabId === "agent_agent-permission-old")).toBe(
true
);
expect(model.tabs.some((tab) => tab.descriptor.tabId === "agent_agent-failed-old")).toBe(
true
);
});
});