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
1172 lines
32 KiB
TypeScript
1172 lines
32 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { AgentGUIConversationSummary } from "./agentGuiConversationModel";
|
|
import type { ConversationSection } from "../agentGuiNodeViewConversation";
|
|
import {
|
|
conversationRailActiveOverlayCountsTowardTotal,
|
|
conversationRailSectionActiveConversationId,
|
|
conversationRailSectionHeaderVisibility,
|
|
insertConversationRailSectionOverlay,
|
|
conversationSummariesRenderEqual,
|
|
isConversationRailInitialLoadPending,
|
|
projectConversationRailMemberships,
|
|
projectConversationRailSearchSections,
|
|
projectConversationRailSectionsByExactKey,
|
|
projectConversationRailSectionsWithActiveConversation,
|
|
planRuntimeRailMembershipRefresh,
|
|
projectRuntimeSectionsToConversationRailMemberships,
|
|
projectConversationRailSectionsWithTransientConversations,
|
|
resolveConversationRailActiveConversation
|
|
} from "./agentGuiConversationRail";
|
|
import type { ConversationRailSectionMembership } from "./agentGuiConversationRail";
|
|
import { preserveConversationRailSectionTemplates } from "./agentGuiConversationRailSectionTemplates";
|
|
|
|
const railLabels = {
|
|
sectionConversations: "Conversations",
|
|
sectionPinned: "Pinned"
|
|
};
|
|
|
|
describe("conversationRailSectionHeaderVisibility", () => {
|
|
it("keeps one Pinned title and places Projects before Chats when every project is pinned", () => {
|
|
const sections: ConversationSection[] = [
|
|
{
|
|
id: "project:/pinned",
|
|
items: [],
|
|
kind: "project",
|
|
label: "Pinned project",
|
|
project: {
|
|
id: "pinned-project",
|
|
label: "Pinned project",
|
|
path: "/pinned",
|
|
pinnedAtUnixMs: 10,
|
|
sectionKey: "project:/pinned"
|
|
}
|
|
},
|
|
{
|
|
id: "conversations",
|
|
items: [],
|
|
kind: "conversations",
|
|
label: "Chats",
|
|
project: null
|
|
}
|
|
];
|
|
|
|
expect(conversationRailSectionHeaderVisibility(sections, 0)).toEqual({
|
|
showPinnedHeader: true,
|
|
showProjectsHeader: false
|
|
});
|
|
expect(conversationRailSectionHeaderVisibility(sections, 1)).toEqual({
|
|
showPinnedHeader: false,
|
|
showProjectsHeader: true
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("conversationRailSectionActiveConversationId", () => {
|
|
it("projects selection only into the section that owns the row", () => {
|
|
const first = section([conversation("first")])[0]!;
|
|
const second = {
|
|
...first,
|
|
id: "project:/second",
|
|
items: [conversation("second")]
|
|
};
|
|
|
|
expect(
|
|
[first, second].map((railSection) =>
|
|
conversationRailSectionActiveConversationId({
|
|
activeConversation: null,
|
|
activeConversationId: "second",
|
|
section: railSection
|
|
})
|
|
)
|
|
).toEqual([null, "second"]);
|
|
});
|
|
|
|
it("keeps a selected overlay active in its projected section", () => {
|
|
const railSection = section([])[0]!;
|
|
const activeConversation = conversation("historical");
|
|
|
|
expect(
|
|
conversationRailSectionActiveConversationId({
|
|
activeConversation,
|
|
activeConversationId: activeConversation.id,
|
|
section: railSection
|
|
})
|
|
).toBe(activeConversation.id);
|
|
});
|
|
});
|
|
|
|
function conversation(
|
|
id: string,
|
|
pinnedAtUnixMs: number | null = null
|
|
): AgentGUIConversationSummary {
|
|
return {
|
|
agentTargetId: "local:codex",
|
|
cwd: "/workspace",
|
|
id,
|
|
pinnedAtUnixMs,
|
|
provider: "codex",
|
|
railSectionKey: "project:/workspace",
|
|
status: "ready",
|
|
title: id,
|
|
updatedAtUnixMs: 1
|
|
};
|
|
}
|
|
|
|
function section(
|
|
items: readonly AgentGUIConversationSummary[]
|
|
): ConversationSection[] {
|
|
return [
|
|
{
|
|
id: "project:/workspace",
|
|
items: [...items],
|
|
kind: "project",
|
|
label: "Workspace",
|
|
project: {
|
|
id: "workspace",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/workspace"
|
|
}
|
|
}
|
|
];
|
|
}
|
|
|
|
describe("isConversationRailInitialLoadPending", () => {
|
|
it("blocks only while the first runtime membership page is unresolved", () => {
|
|
expect(
|
|
isConversationRailInitialLoadPending({
|
|
pending: true,
|
|
runtimeSectionsEnabled: true,
|
|
sections: null
|
|
})
|
|
).toBe(true);
|
|
expect(
|
|
isConversationRailInitialLoadPending({
|
|
pending: true,
|
|
runtimeSectionsEnabled: true,
|
|
sections: []
|
|
})
|
|
).toBe(false);
|
|
expect(
|
|
isConversationRailInitialLoadPending({
|
|
pending: false,
|
|
runtimeSectionsEnabled: true,
|
|
sections: null
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
function membership(
|
|
items: readonly AgentGUIConversationSummary[]
|
|
): ConversationRailSectionMembership[] {
|
|
return [
|
|
{
|
|
id: "project:/workspace",
|
|
kind: "project",
|
|
project: section([])[0]?.project ?? null,
|
|
sessionIds: items.map((item) => item.id)
|
|
}
|
|
];
|
|
}
|
|
|
|
describe("planRuntimeRailMembershipRefresh", () => {
|
|
it("re-renders a row when its leading mention kind becomes available", () => {
|
|
const plain = conversation("session-1");
|
|
|
|
expect(
|
|
conversationSummariesRenderEqual(plain, {
|
|
...plain,
|
|
titleLeadingMentionKind: "task"
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("treats hydration of an already-loaded historical row as entity detail", () => {
|
|
const recent = conversation("recent");
|
|
const historical = conversation("historical");
|
|
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
loadedSections: membership([recent, historical]),
|
|
next: [recent, historical],
|
|
previous: [recent]
|
|
})
|
|
).toEqual({ kind: "none" });
|
|
});
|
|
|
|
it("ignores summary ordering changes", () => {
|
|
const first = conversation("first");
|
|
const second = conversation("second");
|
|
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
loadedSections: membership([first, second]),
|
|
next: [second, first],
|
|
previous: [first, second]
|
|
})
|
|
).toEqual({ kind: "none" });
|
|
});
|
|
|
|
it("refreshes for a newly discovered session outside loaded pages", () => {
|
|
const recent = conversation("recent");
|
|
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
loadedSections: membership([recent]),
|
|
next: [conversation("brand-new"), recent],
|
|
previous: [recent]
|
|
})
|
|
).toEqual({
|
|
kind: "refresh_pages",
|
|
pageIds: ["project:/workspace"],
|
|
reconcilingSessionIds: [],
|
|
refreshSearch: false
|
|
});
|
|
});
|
|
|
|
it("refreshes for removal and pin membership changes", () => {
|
|
const first = conversation("first");
|
|
const second = conversation("second");
|
|
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
loadedSections: membership([first, second]),
|
|
next: [first],
|
|
previous: [first, second]
|
|
})
|
|
).toEqual({
|
|
kind: "refresh_pages",
|
|
pageIds: ["project:/workspace"],
|
|
reconcilingSessionIds: [],
|
|
refreshSearch: false
|
|
});
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
loadedSections: membership([first]),
|
|
next: [conversation("first", 100)],
|
|
previous: [first]
|
|
})
|
|
).toEqual({
|
|
kind: "refresh_pages",
|
|
pageIds: ["project:/workspace", "pinned"],
|
|
reconcilingSessionIds: [],
|
|
refreshSearch: false
|
|
});
|
|
});
|
|
|
|
it("ignores pending creation add and removal", () => {
|
|
const recent = conversation("recent");
|
|
const pending = {
|
|
...conversation("pending"),
|
|
pendingCreation: true
|
|
};
|
|
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
loadedSections: membership([recent]),
|
|
next: [pending, recent],
|
|
previous: [recent]
|
|
})
|
|
).toEqual({ kind: "none" });
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
loadedSections: membership([recent]),
|
|
next: [recent],
|
|
previous: [pending, recent]
|
|
})
|
|
).toEqual({ kind: "none" });
|
|
});
|
|
|
|
it("does not treat selected detail hydration as rail membership", () => {
|
|
const recent = conversation("recent");
|
|
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
activeConversationId: "selected-historical",
|
|
loadedSections: membership([recent]),
|
|
next: [conversation("selected-historical"), recent],
|
|
previous: [recent]
|
|
})
|
|
).toEqual({ kind: "none" });
|
|
});
|
|
|
|
it("refreshes and preserves display membership when pending creation becomes canonical", () => {
|
|
const recent = conversation("recent");
|
|
const pending = {
|
|
...conversation("new-session"),
|
|
pendingCreation: true
|
|
};
|
|
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
activeConversationId: "new-session",
|
|
loadedSections: membership([recent]),
|
|
next: [conversation("new-session"), recent],
|
|
previous: [pending, recent]
|
|
})
|
|
).toEqual({
|
|
kind: "refresh_pages",
|
|
pageIds: ["project:/workspace"],
|
|
reconcilingSessionIds: ["new-session"],
|
|
refreshSearch: false
|
|
});
|
|
});
|
|
|
|
it("refreshes search only when a title changes under an active query", () => {
|
|
const previous = conversation("session-1");
|
|
|
|
expect(
|
|
planRuntimeRailMembershipRefresh({
|
|
loadedSections: membership([previous]),
|
|
next: [{ ...previous, title: "Renamed" }],
|
|
previous: [previous],
|
|
searchActive: true
|
|
})
|
|
).toEqual({
|
|
kind: "refresh_pages",
|
|
pageIds: [],
|
|
reconcilingSessionIds: [],
|
|
refreshSearch: true
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("projectRuntimeSectionsToConversationRailMemberships", () => {
|
|
it("preserves the daemon-owned project section key", () => {
|
|
const memberships = projectRuntimeSectionsToConversationRailMemberships({
|
|
sections: [
|
|
{
|
|
hasMore: false,
|
|
kind: "project",
|
|
sectionKey: "project:authoritative",
|
|
sessions: [],
|
|
totalCount: 0,
|
|
userProject: {
|
|
createdAtUnixMs: 1,
|
|
id: "project-1",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:authoritative",
|
|
updatedAtUnixMs: 2
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
expect(memberships[0]?.project?.sectionKey).toBe("project:authoritative");
|
|
expect(memberships[0]?.project?.pinnedAtUnixMs).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("projectConversationRailMemberships", () => {
|
|
it("requires exact agreement between the session key and section key", () => {
|
|
const exact = conversation("exact");
|
|
const conflicting = {
|
|
...conversation("conflicting"),
|
|
railSectionKey: "conversations"
|
|
};
|
|
|
|
const projected = projectConversationRailMemberships({
|
|
conversations: [exact, conflicting],
|
|
labels: {
|
|
sectionConversations: "Conversations",
|
|
sectionPinned: "Pinned"
|
|
},
|
|
sections: membership([exact, conflicting])
|
|
});
|
|
|
|
expect(projected[0]?.items.map((item) => item.id)).toEqual(["exact"]);
|
|
});
|
|
|
|
it("reorders loaded ordinary rows from current turn sort times", () => {
|
|
const previouslyRecent = {
|
|
...conversation("previously-recent"),
|
|
sortTimeUnixMs: 100
|
|
};
|
|
const continuedHistorical = {
|
|
...conversation("continued-historical"),
|
|
sortTimeUnixMs: 200
|
|
};
|
|
|
|
const projected = projectConversationRailMemberships({
|
|
conversations: [previouslyRecent, continuedHistorical],
|
|
labels: railLabels,
|
|
sections: membership([previouslyRecent, continuedHistorical])
|
|
});
|
|
|
|
expect(projected[0]?.items.map((item) => item.id)).toEqual([
|
|
"continued-historical",
|
|
"previously-recent"
|
|
]);
|
|
});
|
|
|
|
it("keeps ordinary row order stable when only streaming freshness changes", () => {
|
|
const first = {
|
|
...conversation("first"),
|
|
sortTimeUnixMs: 200,
|
|
updatedAtUnixMs: 1
|
|
};
|
|
const second = {
|
|
...conversation("second"),
|
|
sortTimeUnixMs: 100,
|
|
updatedAtUnixMs: 2
|
|
};
|
|
const sections = membership([first, second]);
|
|
|
|
const projected = projectConversationRailMemberships({
|
|
conversations: [
|
|
{ ...first, updatedAtUnixMs: 3 },
|
|
{ ...second, updatedAtUnixMs: 4 }
|
|
],
|
|
labels: railLabels,
|
|
sections
|
|
});
|
|
|
|
expect(projected[0]?.items.map((item) => item.id)).toEqual([
|
|
"first",
|
|
"second"
|
|
]);
|
|
});
|
|
|
|
it("preserves daemon-owned pinned membership order", () => {
|
|
const first = {
|
|
...conversation("pinned-first", 10),
|
|
railSectionKey: "conversations",
|
|
sortTimeUnixMs: 100
|
|
};
|
|
const second = {
|
|
...conversation("pinned-second", 20),
|
|
railSectionKey: "conversations",
|
|
sortTimeUnixMs: 200
|
|
};
|
|
|
|
const projected = projectConversationRailMemberships({
|
|
conversations: [first, second],
|
|
labels: railLabels,
|
|
sections: [
|
|
{
|
|
id: "pinned",
|
|
kind: "pinned",
|
|
project: null,
|
|
sessionIds: [first.id, second.id]
|
|
}
|
|
]
|
|
});
|
|
|
|
expect(projected[0]?.items.map((item) => item.id)).toEqual([
|
|
"pinned-first",
|
|
"pinned-second"
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("projectConversationRailSectionsWithTransientConversations", () => {
|
|
const labels = {
|
|
sectionConversations: "Conversations",
|
|
sectionPinned: "Pinned"
|
|
};
|
|
|
|
it("keeps every running canonical session visible while its page is stale", () => {
|
|
const canonical = section([conversation("older")]);
|
|
const project = canonical[0]?.project ?? null;
|
|
const first = {
|
|
...conversation("running-1"),
|
|
project,
|
|
status: "working" as const,
|
|
sortTimeUnixMs: 30,
|
|
updatedAtUnixMs: 30
|
|
};
|
|
const second = {
|
|
...conversation("running-2"),
|
|
project,
|
|
status: "waiting" as const,
|
|
sortTimeUnixMs: 20,
|
|
updatedAtUnixMs: 20
|
|
};
|
|
|
|
const projected = projectConversationRailSectionsWithTransientConversations(
|
|
{
|
|
conversations: [first, second],
|
|
labels,
|
|
reconcilingSessionIds: [],
|
|
sections: canonical
|
|
}
|
|
);
|
|
|
|
expect(projected[0]?.items.map((item) => item.id)).toEqual([
|
|
"running-1",
|
|
"running-2",
|
|
"older"
|
|
]);
|
|
expect(projected[0]?.items.map((item) => item.projectionSource)).toEqual([
|
|
"runtime_overlay",
|
|
"runtime_overlay",
|
|
undefined
|
|
]);
|
|
});
|
|
|
|
it("keeps pending rows out of project sections until exact membership arrives", () => {
|
|
const canonical = section([conversation("canonical")]);
|
|
const project = canonical[0]?.project ?? null;
|
|
const first = {
|
|
...conversation("session-1"),
|
|
project,
|
|
projectionSource: "pending_activation" as const,
|
|
railSectionKey: undefined,
|
|
sortTimeUnixMs: 10,
|
|
updatedAtUnixMs: 10
|
|
};
|
|
const second = {
|
|
...conversation("session-2"),
|
|
project,
|
|
projectionSource: "pending_activation" as const,
|
|
railSectionKey: undefined,
|
|
sortTimeUnixMs: 20,
|
|
updatedAtUnixMs: 20
|
|
};
|
|
|
|
const projected = projectConversationRailSectionsWithTransientConversations(
|
|
{
|
|
conversations: [second, first],
|
|
labels,
|
|
reconcilingSessionIds: [],
|
|
sections: canonical
|
|
}
|
|
);
|
|
|
|
expect(projected[0]?.items.map((item) => item.id)).toEqual(["canonical"]);
|
|
expect(projected).toHaveLength(1);
|
|
expect(canonical[0]?.items.map((item) => item.id)).toEqual(["canonical"]);
|
|
});
|
|
|
|
it("keeps a canonicalized pending row visible until daemon membership reconciles", () => {
|
|
const canonical = section([conversation("older")]);
|
|
const project = canonical[0]?.project ?? null;
|
|
const confirmed = {
|
|
...conversation("session-1"),
|
|
project,
|
|
sortTimeUnixMs: 20,
|
|
updatedAtUnixMs: 20
|
|
};
|
|
const nextPending = {
|
|
...conversation("session-2"),
|
|
project,
|
|
projectionSource: "pending_activation" as const,
|
|
railSectionKey: undefined,
|
|
sortTimeUnixMs: 30,
|
|
updatedAtUnixMs: 30
|
|
};
|
|
|
|
const projected = projectConversationRailSectionsWithTransientConversations(
|
|
{
|
|
conversations: [nextPending, confirmed],
|
|
labels,
|
|
reconcilingSessionIds: [confirmed.id],
|
|
sections: canonical
|
|
}
|
|
);
|
|
|
|
expect(projected).toHaveLength(1);
|
|
expect(projected[0]?.items.map((item) => item.id)).toEqual([
|
|
"session-1",
|
|
"older"
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("insertConversationRailSectionOverlay", () => {
|
|
it("inserts an active overlay without reordering server rows", () => {
|
|
expect(
|
|
insertConversationRailSectionOverlay(
|
|
"project",
|
|
[
|
|
{ ...conversation("server-first"), updatedAtUnixMs: 10 },
|
|
{ ...conversation("server-second"), updatedAtUnixMs: 30 }
|
|
],
|
|
{ ...conversation("new-active"), updatedAtUnixMs: 20 }
|
|
).map((item) => item.id)
|
|
).toEqual(["new-active", "server-first", "server-second"]);
|
|
});
|
|
});
|
|
|
|
describe("projectConversationRailSectionsWithActiveConversation", () => {
|
|
const labels = {
|
|
sectionConversations: "Conversations",
|
|
sectionPinned: "Pinned"
|
|
};
|
|
|
|
it("keeps canonical sections unchanged when the active row is loaded", () => {
|
|
const active = conversation("active");
|
|
const sections = section([active]);
|
|
|
|
const projected = projectConversationRailSectionsWithActiveConversation({
|
|
activeConversation: active,
|
|
labels: railLabels,
|
|
sections
|
|
});
|
|
|
|
expect(projected.sections).toBe(sections);
|
|
expect(projected.activeOverlay).toEqual({
|
|
conversation: {
|
|
...active,
|
|
project: sections[0]?.project
|
|
},
|
|
sectionId: "project:/workspace"
|
|
});
|
|
});
|
|
|
|
it("reuses the active row when its project is render-equal to the section project", () => {
|
|
const active = {
|
|
...conversation("active"),
|
|
project: {
|
|
id: "workspace",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/workspace"
|
|
}
|
|
};
|
|
|
|
const projected = projectConversationRailSectionsWithActiveConversation({
|
|
activeConversation: active,
|
|
labels: railLabels,
|
|
sections: section([active])
|
|
});
|
|
|
|
expect(projected.activeOverlay?.conversation).toBe(active);
|
|
});
|
|
|
|
it("uses the active row's exact key instead of its resolved project", () => {
|
|
const active = {
|
|
...conversation("active"),
|
|
project: {
|
|
id: "workspace",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/workspace"
|
|
},
|
|
railSectionKey: "conversations"
|
|
};
|
|
const sections = section([conversation("recent")]);
|
|
|
|
const projected = projectConversationRailSectionsWithActiveConversation({
|
|
activeConversation: active,
|
|
labels,
|
|
sections
|
|
});
|
|
|
|
expect(projected.activeOverlay?.conversation.id).toBe("active");
|
|
expect(projected.activeOverlay?.sectionId).toBe("conversations");
|
|
expect(projected.activeOverlay?.conversation.project).toBeNull();
|
|
expect(projected.sections.map((item) => item.id)).toEqual([
|
|
"project:/workspace",
|
|
"conversations"
|
|
]);
|
|
expect(sections[0]?.items.map((item) => item.id)).toEqual(["recent"]);
|
|
});
|
|
|
|
it("creates an exact project overlay when its keyed section is not loaded", () => {
|
|
const active = {
|
|
...conversation("active"),
|
|
project: {
|
|
id: "other",
|
|
label: "Other",
|
|
path: "/other",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/other"
|
|
},
|
|
railSectionKey: "project:/other"
|
|
};
|
|
|
|
const projected = projectConversationRailSectionsWithActiveConversation({
|
|
activeConversation: active,
|
|
labels,
|
|
sections: section([conversation("recent")])
|
|
});
|
|
|
|
expect(projected.sections.map((item) => item.id)).toEqual([
|
|
"project:/workspace",
|
|
"project:/other"
|
|
]);
|
|
expect(projected.sections[1]?.items).toEqual([]);
|
|
expect(projected.activeOverlay).toEqual({
|
|
conversation: active,
|
|
sectionId: "project:/other"
|
|
});
|
|
});
|
|
|
|
it("does not project an active row whose backend key is absent", () => {
|
|
const active = {
|
|
...conversation("active"),
|
|
railSectionKey: undefined
|
|
};
|
|
const sections = section([conversation("recent")]);
|
|
|
|
const projected = projectConversationRailSectionsWithActiveConversation({
|
|
activeConversation: active,
|
|
labels,
|
|
sections
|
|
});
|
|
|
|
expect(projected).toEqual({ activeOverlay: null, sections });
|
|
});
|
|
});
|
|
|
|
describe("projectConversationRailSearchSections", () => {
|
|
it("uses exact loaded membership and never infers missing membership from cwd", () => {
|
|
const loaded = conversation("loaded");
|
|
const resolvedProject = section([])[0]?.project ?? null;
|
|
const missing = {
|
|
...conversation("missing"),
|
|
cwd: "/workspace/packages/app",
|
|
project: resolvedProject,
|
|
railSectionKey: "conversations"
|
|
};
|
|
|
|
const groups = projectConversationRailSearchSections({
|
|
conversations: [loaded, missing],
|
|
labels: {
|
|
sectionConversations: "Conversations",
|
|
sectionPinned: "Pinned"
|
|
},
|
|
sections: section([loaded])
|
|
});
|
|
|
|
expect(
|
|
groups.map((group) => [group.id, group.items.map((item) => item.id)])
|
|
).toEqual([
|
|
["project:/workspace", ["loaded"]],
|
|
["conversations", ["missing"]]
|
|
]);
|
|
});
|
|
|
|
it("preserves pinned and authoritative section order", () => {
|
|
const projectA = conversation("project-a");
|
|
const projectB = {
|
|
...conversation("project-b"),
|
|
railSectionKey: "project:/b",
|
|
project: {
|
|
id: "b",
|
|
label: "B",
|
|
path: "/b",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/b"
|
|
}
|
|
};
|
|
const pinned = {
|
|
...conversation("pinned"),
|
|
pinnedAtUnixMs: 1,
|
|
updatedAtUnixMs: 0
|
|
};
|
|
|
|
const groups = projectConversationRailSearchSections({
|
|
conversations: [projectA, projectB, pinned],
|
|
labels: railLabels,
|
|
sections: [
|
|
{
|
|
id: "project:/b",
|
|
kind: "project",
|
|
label: "B",
|
|
project: projectB.project,
|
|
items: []
|
|
},
|
|
...section([])
|
|
]
|
|
});
|
|
|
|
expect(groups.map((group) => group.id)).toEqual([
|
|
"pinned",
|
|
"project:/b",
|
|
"project:/workspace"
|
|
]);
|
|
});
|
|
|
|
it("keeps every project template visible when search matches no rows", () => {
|
|
const groups = projectConversationRailSearchSections({
|
|
conversations: [],
|
|
labels: railLabels,
|
|
sections: [
|
|
{
|
|
id: "project:/b",
|
|
kind: "project",
|
|
label: "B",
|
|
project: {
|
|
id: "b",
|
|
label: "B",
|
|
path: "/b",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/b"
|
|
},
|
|
items: []
|
|
},
|
|
...section([]),
|
|
{
|
|
id: "conversations",
|
|
kind: "conversations",
|
|
label: "Conversations",
|
|
project: null,
|
|
items: []
|
|
}
|
|
]
|
|
});
|
|
|
|
expect(groups.map((group) => group.id)).toEqual([
|
|
"project:/b",
|
|
"project:/workspace",
|
|
"conversations"
|
|
]);
|
|
expect(groups.every((group) => group.items.length === 0)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("projectConversationRailSectionsByExactKey", () => {
|
|
it("orders pinned sessions, pinned projects, ordinary projects, then Chats without duplicating sessions", () => {
|
|
const pinnedSession = {
|
|
...conversation("pinned-session", 100),
|
|
project: null
|
|
};
|
|
const pinnedProject = {
|
|
id: "pinned-project",
|
|
label: "Pinned project",
|
|
path: "/pinned",
|
|
pinnedAtUnixMs: 90,
|
|
sectionKey: "project:/pinned"
|
|
};
|
|
const ordinaryProject = {
|
|
id: "ordinary-project",
|
|
label: "Ordinary project",
|
|
path: "/ordinary",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/ordinary"
|
|
};
|
|
const pinnedProjectSession = {
|
|
...conversation("pinned-project-session"),
|
|
project: pinnedProject,
|
|
railSectionKey: pinnedProject.sectionKey
|
|
};
|
|
|
|
const groups = projectConversationRailSectionsByExactKey({
|
|
conversations: [pinnedProjectSession, pinnedSession],
|
|
labels: railLabels,
|
|
userProjects: [ordinaryProject, pinnedProject],
|
|
includeEmptySections: true
|
|
});
|
|
|
|
expect(groups.map((group) => group.id)).toEqual([
|
|
"pinned",
|
|
"project:/pinned",
|
|
"project:/ordinary",
|
|
"conversations"
|
|
]);
|
|
expect(
|
|
groups.flatMap((group) => group.items.map((item) => item.id))
|
|
).toEqual(["pinned-session", "pinned-project-session"]);
|
|
});
|
|
|
|
it("uses only exact backend keys and keeps empty authoritative sections", () => {
|
|
const projectSummary = {
|
|
id: "workspace",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/workspace"
|
|
};
|
|
const exactProject = {
|
|
...conversation("exact-project"),
|
|
project: projectSummary
|
|
};
|
|
const conversations = {
|
|
...conversation("general"),
|
|
cwd: "/workspace/nested",
|
|
project: projectSummary,
|
|
railSectionKey: "conversations"
|
|
};
|
|
const missingKey = {
|
|
...conversation("missing-key"),
|
|
project: projectSummary,
|
|
railSectionKey: undefined
|
|
};
|
|
|
|
const groups = projectConversationRailSectionsByExactKey({
|
|
conversations: [conversations, missingKey, exactProject],
|
|
labels: railLabels,
|
|
userProjects: [
|
|
{
|
|
...projectSummary,
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 1,
|
|
lastUsedAtUnixMs: 1
|
|
},
|
|
{
|
|
id: "empty",
|
|
label: "Empty",
|
|
path: "/empty",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/empty",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 1,
|
|
lastUsedAtUnixMs: 1
|
|
}
|
|
],
|
|
includeEmptySections: true
|
|
});
|
|
|
|
expect(
|
|
groups.map((group) => [group.id, group.items.map((item) => item.id)])
|
|
).toEqual([
|
|
["project:/workspace", ["exact-project"]],
|
|
["project:/empty", []],
|
|
["conversations", ["general"]]
|
|
]);
|
|
});
|
|
|
|
it("keeps empty user-project templates when filtering loaded rows", () => {
|
|
const groups = projectConversationRailSectionsByExactKey({
|
|
conversations: [],
|
|
labels: railLabels,
|
|
userProjects: [
|
|
{
|
|
id: "workspace",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/workspace"
|
|
},
|
|
{
|
|
id: "empty",
|
|
label: "Empty",
|
|
path: "/empty",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/empty"
|
|
}
|
|
],
|
|
includeEmptySections: false
|
|
});
|
|
|
|
expect(groups.map((group) => group.id)).toEqual([
|
|
"project:/workspace",
|
|
"project:/empty",
|
|
"conversations"
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("preserveConversationRailSectionTemplates", () => {
|
|
it("stable-partitions pinned projects before ordinary projects", () => {
|
|
const groups = preserveConversationRailSectionTemplates({
|
|
labels: railLabels,
|
|
sections: [],
|
|
userProjects: [
|
|
{
|
|
id: "ordinary",
|
|
label: "Ordinary",
|
|
path: "/ordinary",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/ordinary"
|
|
},
|
|
{
|
|
id: "pinned",
|
|
label: "Pinned project",
|
|
path: "/pinned",
|
|
pinnedAtUnixMs: 10,
|
|
sectionKey: "project:/pinned"
|
|
}
|
|
]
|
|
});
|
|
|
|
expect(groups.map((group) => group.id)).toEqual([
|
|
"project:/pinned",
|
|
"project:/ordinary",
|
|
"conversations"
|
|
]);
|
|
});
|
|
|
|
it("keeps project and conversations sections visible when membership is empty", () => {
|
|
const groups = preserveConversationRailSectionTemplates({
|
|
labels: railLabels,
|
|
sections: [],
|
|
userProjects: [
|
|
{
|
|
id: "workspace",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/workspace",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 1,
|
|
lastUsedAtUnixMs: 1
|
|
}
|
|
]
|
|
});
|
|
|
|
expect(
|
|
groups.map((group) => [group.id, group.items.map((item) => item.id)])
|
|
).toEqual([
|
|
["project:/workspace", []],
|
|
["conversations", []]
|
|
]);
|
|
});
|
|
|
|
it("preserves exact projected items without inferring missing membership", () => {
|
|
const exact = conversation("exact");
|
|
const groups = preserveConversationRailSectionTemplates({
|
|
labels: railLabels,
|
|
sections: section([exact]),
|
|
userProjects: [
|
|
{
|
|
id: "workspace",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/workspace",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 1,
|
|
lastUsedAtUnixMs: 1
|
|
},
|
|
{
|
|
id: "empty",
|
|
label: "Empty",
|
|
path: "/empty",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/empty",
|
|
createdAtUnixMs: 1,
|
|
updatedAtUnixMs: 1,
|
|
lastUsedAtUnixMs: 1
|
|
}
|
|
]
|
|
});
|
|
|
|
expect(
|
|
groups.map((group) => [group.id, group.items.map((item) => item.id)])
|
|
).toEqual([
|
|
["project:/workspace", ["exact"]],
|
|
["project:/empty", []],
|
|
["conversations", []]
|
|
]);
|
|
});
|
|
|
|
it("drops project templates outside userProjects and keeps fixed sections", () => {
|
|
const pinned = {
|
|
...conversation("pinned"),
|
|
pinnedAtUnixMs: 2
|
|
};
|
|
const groups = preserveConversationRailSectionTemplates({
|
|
labels: railLabels,
|
|
sections: [
|
|
{
|
|
id: "pinned",
|
|
kind: "pinned",
|
|
label: "Pinned",
|
|
project: null,
|
|
items: [pinned]
|
|
},
|
|
...section([conversation("known")]),
|
|
{
|
|
id: "project:/removed",
|
|
kind: "project",
|
|
label: "Removed",
|
|
project: {
|
|
id: "removed",
|
|
label: "Removed",
|
|
path: "/removed",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/removed"
|
|
},
|
|
items: [
|
|
{
|
|
...conversation("removed"),
|
|
railSectionKey: "project:/removed"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: "conversations",
|
|
kind: "conversations",
|
|
label: "Conversations",
|
|
project: null,
|
|
items: []
|
|
}
|
|
],
|
|
userProjects: [
|
|
{
|
|
id: "workspace",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/workspace"
|
|
}
|
|
]
|
|
});
|
|
|
|
expect(groups.map((group) => group.id)).toEqual([
|
|
"pinned",
|
|
"project:/workspace",
|
|
"conversations"
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("resolveConversationRailActiveConversation", () => {
|
|
it("prefers the rail-projected selected entity over the controller copy", () => {
|
|
const controllerActive = conversation("active");
|
|
const projectedActive = {
|
|
...controllerActive,
|
|
project: {
|
|
id: "workspace",
|
|
label: "Workspace",
|
|
path: "/workspace",
|
|
pinnedAtUnixMs: 0,
|
|
sectionKey: "project:/workspace"
|
|
}
|
|
};
|
|
|
|
expect(
|
|
resolveConversationRailActiveConversation({
|
|
activeConversation: controllerActive,
|
|
activeConversationId: controllerActive.id,
|
|
conversations: [projectedActive]
|
|
})
|
|
).toBe(projectedActive);
|
|
});
|
|
|
|
it("keeps the controller entity when the selected row is outside the filter", () => {
|
|
const controllerActive = conversation("active");
|
|
|
|
expect(
|
|
resolveConversationRailActiveConversation({
|
|
activeConversation: controllerActive,
|
|
activeConversationId: controllerActive.id,
|
|
conversations: [conversation("other")]
|
|
})
|
|
).toBe(controllerActive);
|
|
});
|
|
});
|
|
|
|
describe("conversationRailActiveOverlayCountsTowardTotal", () => {
|
|
it("does not count an active canonical entity when its section row is a runtime overlay", () => {
|
|
const active = conversation("active");
|
|
|
|
expect(
|
|
conversationRailActiveOverlayCountsTowardTotal({
|
|
activeConversation: active,
|
|
matchesFilter: true,
|
|
sectionItems: [{ ...active, projectionSource: "runtime_overlay" }]
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("counts an active matching entity that is absent from the section", () => {
|
|
expect(
|
|
conversationRailActiveOverlayCountsTowardTotal({
|
|
activeConversation: conversation("active"),
|
|
matchesFilter: true,
|
|
sectionItems: [conversation("older")]
|
|
})
|
|
).toBe(true);
|
|
});
|
|
});
|