99f391f379
Co-authored-by: Alice <alice@prismshadow.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
139 lines
6.0 KiB
TypeScript
139 lines
6.0 KiB
TypeScript
/**
|
|
* Workspace grouping for the chat sidebar (pure logic):
|
|
* - named Workspaces group by exact path, labeled by basename (full path kept for
|
|
* tooltips), newest group first;
|
|
* - auto temp Workspaces (`<agentDir>/workspaces/tmp-<8hex>`, the shape produced by
|
|
* core's createTempWorkspace) are all merged into ONE trailing temp group — an
|
|
* empty path (defensive; the server always backfills the resolved dir) counts too;
|
|
* - sessions inside every group are re-sorted newest first: the flat store list
|
|
* concatenates per-Agent server responses, so its order isn't globally chronological.
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import type { SessionInfo } from "@prismshadow/penguin-server/api";
|
|
import {
|
|
TEMP_WORKSPACE_GROUP_KEY,
|
|
groupSessionsByWorkspace,
|
|
isTempWorkspace,
|
|
workspaceGroupKey,
|
|
workspaceLabel,
|
|
} from "../src/lib/session-grouping";
|
|
|
|
let seq = 0;
|
|
function session(
|
|
workspace: string,
|
|
createdAt: string,
|
|
over: { sessionId?: string; agentId?: string; archived?: boolean } = {},
|
|
): SessionInfo {
|
|
seq += 1;
|
|
return {
|
|
sessionId: over.sessionId ?? `session-${seq}`,
|
|
projectId: "proj",
|
|
agentId: over.agentId ?? "default_agent",
|
|
provider: "custom",
|
|
modelId: "claude-4-8",
|
|
workspace,
|
|
approvalMode: "allow-all",
|
|
createdAt,
|
|
status: "idle",
|
|
pendingApprovalCount: 0,
|
|
hasTrace: false,
|
|
archived: over.archived ?? false,
|
|
};
|
|
}
|
|
|
|
const TEMP_A = "/data/proj/agents/default_agent/workspaces/tmp-1a2b3c4d";
|
|
const TEMP_B = "/data/proj/agents/agent_helper/workspaces/tmp-00ff00aa";
|
|
|
|
describe("isTempWorkspace (auto temp directory pattern from core's createTempWorkspace)", () => {
|
|
it("matches <...>/workspaces/tmp-<8hex> with either path separator, and the empty path", () => {
|
|
expect(isTempWorkspace(TEMP_A)).toBe(true);
|
|
expect(isTempWorkspace("C:\\pg\\data\\proj\\agents\\a\\workspaces\\tmp-00ff00aa")).toBe(true);
|
|
expect(isTempWorkspace("")).toBe(true);
|
|
expect(isTempWorkspace(" ")).toBe(true);
|
|
});
|
|
|
|
it("rejects named directories and near misses", () => {
|
|
expect(isTempWorkspace("/srv/repo")).toBe(false);
|
|
// tmp-<8hex> without a workspaces/ parent is a user directory that just looks similar
|
|
expect(isTempWorkspace("/srv/tmp-1a2b3c4d")).toBe(false);
|
|
// non-hex / wrong-length ids
|
|
expect(isTempWorkspace("/x/workspaces/tmp-XYZWQPRS")).toBe(false);
|
|
expect(isTempWorkspace("/x/workspaces/tmp-1a2b3c4")).toBe(false);
|
|
expect(isTempWorkspace("/x/workspaces/tmp-1a2b3c4d5")).toBe(false);
|
|
// a subdirectory below a temp Workspace is not itself the temp Workspace
|
|
expect(isTempWorkspace(`${TEMP_A}/nested`)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("workspaceGroupKey / workspaceLabel", () => {
|
|
it("named paths key by the (trimmed) path itself; temp and empty paths share the sentinel", () => {
|
|
expect(workspaceGroupKey("/srv/repo")).toBe("/srv/repo");
|
|
expect(workspaceGroupKey(" /srv/repo ")).toBe("/srv/repo");
|
|
expect(workspaceGroupKey(TEMP_A)).toBe(TEMP_WORKSPACE_GROUP_KEY);
|
|
expect(workspaceGroupKey("")).toBe(TEMP_WORKSPACE_GROUP_KEY);
|
|
});
|
|
|
|
it("labels are the last path segment; the filesystem root yields '/'", () => {
|
|
expect(workspaceLabel("/srv/penguin/repo")).toBe("repo");
|
|
expect(workspaceLabel("/srv/repo/")).toBe("repo");
|
|
expect(workspaceLabel("/")).toBe("/");
|
|
expect(workspaceLabel("C:\\work\\site")).toBe("site");
|
|
});
|
|
});
|
|
|
|
describe("groupSessionsByWorkspace", () => {
|
|
it("empty input yields no groups", () => {
|
|
expect(groupSessionsByWorkspace([])).toEqual([]);
|
|
});
|
|
|
|
it("groups by path across Agents, labels by basename, and keeps the full path for tooltips", () => {
|
|
const a1 = session("/srv/alpha", "2026-07-01T10:00:00.000Z", { agentId: "default_agent" });
|
|
const a2 = session("/srv/alpha", "2026-07-03T10:00:00.000Z", { agentId: "agent_helper" });
|
|
const groups = groupSessionsByWorkspace([a1, a2]);
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0]).toMatchObject({
|
|
key: "/srv/alpha",
|
|
label: "alpha",
|
|
fullPath: "/srv/alpha",
|
|
temp: false,
|
|
});
|
|
// Newest first even though the two sessions came from different Agents' lists.
|
|
expect(groups[0]!.sessions.map((s) => s.sessionId)).toEqual([a2.sessionId, a1.sessionId]);
|
|
});
|
|
|
|
it("merges every temp Workspace into one trailing group, after named groups sorted by newest session", () => {
|
|
const oldAlpha = session("/srv/alpha", "2026-07-01T10:00:00.000Z");
|
|
const newAlpha = session("/srv/alpha", "2026-07-06T10:00:00.000Z");
|
|
const beta = session("/srv/beta", "2026-07-05T10:00:00.000Z");
|
|
const temp1 = session(TEMP_A, "2026-07-02T10:00:00.000Z");
|
|
// The newest session overall is a temp one: the temp group still stays last.
|
|
const temp2 = session(TEMP_B, "2026-07-07T10:00:00.000Z");
|
|
const groups = groupSessionsByWorkspace([oldAlpha, beta, temp1, newAlpha, temp2]);
|
|
expect(groups.map((g) => g.key)).toEqual(["/srv/alpha", "/srv/beta", TEMP_WORKSPACE_GROUP_KEY]);
|
|
const temp = groups[2]!;
|
|
expect(temp).toMatchObject({ temp: true, label: "", fullPath: null });
|
|
expect(temp.sessions.map((s) => s.sessionId)).toEqual([temp2.sessionId, temp1.sessionId]);
|
|
expect(groups[0]!.sessions.map((s) => s.sessionId)).toEqual([
|
|
newAlpha.sessionId,
|
|
oldAlpha.sessionId,
|
|
]);
|
|
});
|
|
|
|
it("named-only input has no temp group; temp-only input yields just the temp group", () => {
|
|
expect(
|
|
groupSessionsByWorkspace([session("/srv/alpha", "2026-07-01T10:00:00.000Z")]),
|
|
).toHaveLength(1);
|
|
const tempOnly = groupSessionsByWorkspace([session(TEMP_A, "2026-07-01T10:00:00.000Z")]);
|
|
expect(tempOnly).toHaveLength(1);
|
|
expect(tempOnly[0]!.key).toBe(TEMP_WORKSPACE_GROUP_KEY);
|
|
});
|
|
|
|
it("keeps archived sessions in their group (the sidebar splits active/archived per group)", () => {
|
|
const active = session("/srv/alpha", "2026-07-02T10:00:00.000Z");
|
|
const archived = session("/srv/alpha", "2026-07-01T10:00:00.000Z", { archived: true });
|
|
const groups = groupSessionsByWorkspace([archived, active]);
|
|
expect(groups).toHaveLength(1);
|
|
expect(groups[0]!.sessions).toHaveLength(2);
|
|
});
|
|
});
|