216 lines
6.5 KiB
TypeScript
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
|
|
);
|
|
});
|
|
});
|