buildConversations learns the org's CEO (c_suite, the runtime predicate) and ranks the viewer's own direct conversation with them above recency — counterpart resolution already works from the viewer's seat, so agent-to- agent CEO chats never match. The row carries a CEO badge and, before the first exchange, invites asking about progress, goals, or direction. Being first, it is also the default conversation the page opens on.
106 lines
4.4 KiB
TypeScript
106 lines
4.4 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { buildConversations } from "../../src/renderer/src/pages/chat/chat-helpers";
|
|
import type {
|
|
AgentRecord,
|
|
ChatMembershipRecord,
|
|
ChatMessageRecord,
|
|
ChatSpaceRecord,
|
|
EmployeeRecord,
|
|
} from "../../src/shared/domain";
|
|
|
|
const ORG = "org_1";
|
|
const FOUNDER = "emp_founder";
|
|
const CEO = "agt_ceo";
|
|
const ENGINEER = "agt_engineer";
|
|
|
|
function space(id: string, spaceType: string, name: string, createdAt: string): ChatSpaceRecord {
|
|
return { id, orgId: ORG, spaceType, name, topic: "t", visibility: "restricted", retentionPolicy: "standard", createdAt };
|
|
}
|
|
|
|
function member(spaceId: string, principalType: "employee" | "agent", principalId: string): ChatMembershipRecord {
|
|
return { spaceId, principalType, principalId, role: "member", muteState: "unmuted" };
|
|
}
|
|
|
|
function message(id: string, spaceId: string, senderId: string, createdAt: string): ChatMessageRecord {
|
|
return {
|
|
id, orgId: ORG, spaceId, threadId: `thr_${id}`, senderType: "agent", senderId,
|
|
bodyMd: "hello", bodyStructuredJson: "{}", deliveryState: "sent", createdAt, editedAt: null, deletedAt: null,
|
|
};
|
|
}
|
|
|
|
function agent(id: string, name: string, roleLevel: string): AgentRecord {
|
|
return {
|
|
id, orgId: ORG, name, role: name, department: "Executive", modelProviderId: null, modelId: null,
|
|
budgetCents: 0, managerAgentId: roleLevel === "c_suite" ? null : CEO, title: name, roleLevel,
|
|
modelStrategy: "explicit", workspaceId: null, heartbeatEnabled: false, heartbeatIntervalSec: 0,
|
|
lifecycle: "active", avatarEmoji: null, avatarColor: null, avatarImage: null, createdAt: "2026-01-01T00:00:00Z",
|
|
};
|
|
}
|
|
|
|
const employees: EmployeeRecord[] = [{
|
|
id: FOUNDER, orgId: ORG, displayName: "Yang", role: "founder",
|
|
avatarEmoji: null, avatarColor: null, avatarImage: null, createdAt: "2026-01-01T00:00:00Z",
|
|
}];
|
|
const agents: AgentRecord[] = [agent(CEO, "CEO", "c_suite"), agent(ENGINEER, "Dana", "staff")];
|
|
|
|
// The founder⇄CEO conversation is the OLDEST and QUIETEST space; the busy
|
|
// channel and the CEO⇄engineer conversation both out-recency it. Pinning
|
|
// must beat recency, and only from the founder's own seat.
|
|
function fixtures() {
|
|
const spaces = [
|
|
space("spc_ceo_dm", "direct", "Yang · CEO", "2026-01-01T00:00:00Z"),
|
|
space("spc_general", "company", "general", "2026-01-02T00:00:00Z"),
|
|
space("spc_agents_dm", "direct", "CEO · Dana", "2026-01-03T00:00:00Z"),
|
|
];
|
|
const memberships = [
|
|
member("spc_ceo_dm", "employee", FOUNDER),
|
|
member("spc_ceo_dm", "agent", CEO),
|
|
member("spc_general", "employee", FOUNDER),
|
|
member("spc_general", "agent", CEO),
|
|
member("spc_general", "agent", ENGINEER),
|
|
member("spc_agents_dm", "agent", CEO),
|
|
member("spc_agents_dm", "agent", ENGINEER),
|
|
];
|
|
const messages = [
|
|
message("m1", "spc_ceo_dm", CEO, "2026-01-04T00:00:00Z"),
|
|
message("m2", "spc_general", ENGINEER, "2026-01-05T00:00:00Z"),
|
|
message("m3", "spc_agents_dm", CEO, "2026-01-06T00:00:00Z"),
|
|
];
|
|
return { spaces, memberships, messages };
|
|
}
|
|
|
|
function build(overrides: Partial<Parameters<typeof buildConversations>[0]> = {}) {
|
|
const { spaces, memberships, messages } = fixtures();
|
|
return buildConversations({
|
|
spaces, memberships, messages, readStates: [], employees, agents,
|
|
currentEmployeeId: FOUNDER, ceoAgentId: CEO, ...overrides,
|
|
});
|
|
}
|
|
|
|
describe("conversation pinning", () => {
|
|
test("the founder's CEO conversation outranks recency and sits first", () => {
|
|
const conversations = build();
|
|
expect(conversations.map((item) => item.space.id)).toEqual(["spc_ceo_dm", "spc_agents_dm", "spc_general"]);
|
|
expect(conversations[0].isCeo).toBe(true);
|
|
expect(conversations[0].title).toBe("CEO");
|
|
});
|
|
|
|
test("a CEO⇄agent conversation is not the founder's pinned line", () => {
|
|
const conversations = build();
|
|
const agentsDm = conversations.find((item) => item.space.id === "spc_agents_dm");
|
|
expect(agentsDm?.isCeo).toBe(false);
|
|
});
|
|
|
|
test("without a CEO the list is pure recency", () => {
|
|
const conversations = build({ ceoAgentId: null });
|
|
expect(conversations.every((item) => !item.isCeo)).toBe(true);
|
|
expect(conversations.map((item) => item.space.id)).toEqual(["spc_agents_dm", "spc_general", "spc_ceo_dm"]);
|
|
});
|
|
|
|
test("the pinned row still counts unread like any conversation", () => {
|
|
const conversations = build();
|
|
expect(conversations[0].unread).toBe(1);
|
|
expect(conversations[0].preview).toContain("CEO:");
|
|
});
|
|
});
|