import { afterEach, describe, expect, it } from "vitest"; import type { DaemonClient } from "@server/client/daemon-client"; import type { WorkspaceDescriptorPayload } from "@server/shared/messages"; import { normalizeWorkspaceDescriptor, useSessionStore, type WorkspaceDescriptor, } from "./session-store"; import { patchWorkspaceScripts } from "../contexts/session-workspace-scripts"; function createWorkspace( input: Partial & Pick, ): WorkspaceDescriptor { return { id: input.id, projectId: input.projectId ?? "project-1", projectDisplayName: input.projectDisplayName ?? "Project 1", projectRootPath: input.projectRootPath ?? "/repo", workspaceDirectory: input.workspaceDirectory ?? "/repo", projectKind: input.projectKind ?? "git", workspaceKind: input.workspaceKind ?? "local_checkout", name: input.name ?? "main", status: input.status ?? "done", archivingAt: input.archivingAt ?? null, diffStat: input.diffStat ?? null, scripts: input.scripts ?? [], }; } afterEach(() => { useSessionStore.getState().clearSession("test-server"); }); function initializeTestSession(): void { useSessionStore.getState().initializeSession("test-server", null as unknown as DaemonClient); } function getTestSessionReferences() { const state = useSessionStore.getState(); const session = state.sessions["test-server"]; if (!session) { throw new Error("test session is not initialized"); } return { sessions: state.sessions, session, workspaces: session.workspaces, }; } describe("normalizeWorkspaceDescriptor", () => { it("normalizes workspace scripts and invalid activity timestamps", () => { const scripts = [ { scriptName: "web", type: "service" as const, hostname: "web.paseo.localhost", port: 3000, proxyUrl: "http://web.paseo.localhost:6767", lifecycle: "running" as const, health: "healthy" as const, exitCode: null, terminalId: null, }, ]; const workspace = normalizeWorkspaceDescriptor({ id: "1", projectId: "1", projectDisplayName: "Project 1", projectRootPath: "/repo", workspaceDirectory: "/repo", projectKind: "git", workspaceKind: "checkout", name: "main", archivingAt: null, status: "running", activityAt: "not-a-date", diffStat: null, scripts, }); expect(workspace.scripts).toEqual([ { scriptName: "web", type: "service", hostname: "web.paseo.localhost", port: 3000, proxyUrl: "http://web.paseo.localhost:6767", lifecycle: "running", health: "healthy", exitCode: null, terminalId: null, }, ]); expect(workspace.scripts).not.toBe(scripts); }); it("defaults missing scripts to an empty array", () => { const payload = { id: "1", projectId: "1", projectDisplayName: "Project 1", projectRootPath: "/repo", workspaceDirectory: "/repo", projectKind: "git", workspaceKind: "checkout", name: "main", archivingAt: null, status: "done", activityAt: null, diffStat: null, scripts: [], } as WorkspaceDescriptorPayload; const workspace = normalizeWorkspaceDescriptor(payload); expect(workspace.scripts).toEqual([]); }); it("defaults missing archivingAt to null", () => { const payload = { id: "1", projectId: "1", projectDisplayName: "Project 1", projectRootPath: "/repo", workspaceDirectory: "/repo", projectKind: "git", workspaceKind: "checkout", name: "main", status: "done", activityAt: null, diffStat: null, scripts: [], } as unknown as WorkspaceDescriptorPayload; const workspace = normalizeWorkspaceDescriptor(payload); expect(workspace.archivingAt).toBeNull(); }); it("preserves project placement from workspace descriptor payloads", () => { const workspace = normalizeWorkspaceDescriptor({ id: "1", projectId: "remote:github.com/acme/app", projectDisplayName: "acme/app", projectRootPath: "/repo/app", workspaceDirectory: "/repo/app", projectKind: "git", workspaceKind: "local_checkout", name: "main", archivingAt: null, status: "done", activityAt: null, diffStat: null, scripts: [], project: { projectKey: "remote:github.com/acme/app", projectName: "acme/app", checkout: { cwd: "/repo/app", isGit: true, currentBranch: "main", remoteUrl: "https://github.com/acme/app.git", worktreeRoot: "/repo/app", isPaseoOwnedWorktree: false, mainRepoRoot: null, }, }, }); expect(workspace.project).toEqual({ projectKey: "remote:github.com/acme/app", projectName: "acme/app", checkout: { cwd: "/repo/app", isGit: true, currentBranch: "main", remoteUrl: "https://github.com/acme/app.git", worktreeRoot: "/repo/app", isPaseoOwnedWorktree: false, mainRepoRoot: null, }, }); }); }); describe("mergeWorkspaces", () => { it("preserves scripts on merged workspace entries", () => { const store = useSessionStore.getState(); store.initializeSession("test-server", null as unknown as DaemonClient); store.setWorkspaces( "test-server", new Map([["/repo/main", createWorkspace({ id: "/repo/main", scripts: [] })]]), ); store.mergeWorkspaces("test-server", [ createWorkspace({ id: "/repo/main", scripts: [ { scriptName: "web", type: "service", hostname: "web.paseo.localhost", port: 3000, proxyUrl: "http://web.paseo.localhost:6767", lifecycle: "running", health: "healthy", exitCode: null, terminalId: null, }, ], }), ]); expect(store.getSession("test-server")?.workspaces.get("/repo/main")?.scripts).toEqual([ { scriptName: "web", type: "service", hostname: "web.paseo.localhost", port: 3000, proxyUrl: "http://web.paseo.localhost:6767", lifecycle: "running", health: "healthy", exitCode: null, terminalId: null, }, ]); }); it("preserves identity when merging content-equal workspace descriptors", () => { const store = useSessionStore.getState(); initializeTestSession(); const workspace = createWorkspace({ id: "/repo/main" }); store.mergeWorkspaces("test-server", [workspace]); const first = getTestSessionReferences(); store.mergeWorkspaces("test-server", [{ ...workspace, scripts: [...workspace.scripts] }]); const second = getTestSessionReferences(); expect(second.sessions).toBe(first.sessions); expect(second.session).toBe(first.session); expect(second.workspaces).toBe(first.workspaces); expect(second.workspaces.get("/repo/main")).toBe(first.workspaces.get("/repo/main")); }); it("preserves unaffected workspace entry identity when one workspace changes", () => { const store = useSessionStore.getState(); initializeTestSession(); const workspaceA = createWorkspace({ id: "/repo/a", name: "main" }); const workspaceB = createWorkspace({ id: "/repo/b", name: "feature" }); store.mergeWorkspaces("test-server", [workspaceA, workspaceB]); const before = getTestSessionReferences(); const beforeA = before.workspaces.get("/repo/a"); const beforeB = before.workspaces.get("/repo/b"); store.mergeWorkspaces("test-server", [{ ...workspaceA, status: "running" }]); const after = getTestSessionReferences(); expect(after.sessions).not.toBe(before.sessions); expect(after.session).not.toBe(before.session); expect(after.workspaces).not.toBe(before.workspaces); expect(after.workspaces.get("/repo/a")).not.toBe(beforeA); expect(after.workspaces.get("/repo/b")).toBe(beforeB); }); it("uses incoming null diff stat as authoritative", () => { const store = useSessionStore.getState(); initializeTestSession(); const workspace = createWorkspace({ id: "/repo/main", diffStat: { additions: 2, deletions: 1 }, }); store.mergeWorkspaces("test-server", [workspace]); const before = getTestSessionReferences(); store.mergeWorkspaces("test-server", [{ ...workspace, diffStat: null }]); const after = getTestSessionReferences(); expect(after.sessions).not.toBe(before.sessions); expect(after.session).not.toBe(before.session); expect(after.workspaces).not.toBe(before.workspaces); expect(after.workspaces.get(workspace.id)?.diffStat).toBeNull(); }); }); describe("setWorkspaces", () => { it("preserves identity when replacing workspaces with content-equal entries", () => { const store = useSessionStore.getState(); initializeTestSession(); const workspace = createWorkspace({ id: "/repo/main" }); store.setWorkspaces("test-server", new Map([[workspace.id, workspace]])); const before = getTestSessionReferences(); store.setWorkspaces( "test-server", new Map([[workspace.id, { ...workspace, scripts: [...workspace.scripts] }]]), ); const after = getTestSessionReferences(); expect(after.sessions).toBe(before.sessions); expect(after.session).toBe(before.session); expect(after.workspaces).toBe(before.workspaces); expect(after.workspaces.get(workspace.id)).toBe(before.workspaces.get(workspace.id)); }); }); describe("removeWorkspace", () => { it("preserves identity when removing a missing workspace", () => { const store = useSessionStore.getState(); initializeTestSession(); const workspace = createWorkspace({ id: "/repo/main" }); store.setWorkspaces("test-server", new Map([[workspace.id, workspace]])); const before = getTestSessionReferences(); store.removeWorkspace("test-server", "/repo/missing"); const after = getTestSessionReferences(); expect(after.sessions).toBe(before.sessions); expect(after.session).toBe(before.session); expect(after.workspaces).toBe(before.workspaces); }); }); describe("patchWorkspaceScripts", () => { it("preserves workspace entry identity when scripts are content-equal", () => { const script = { scriptName: "web", type: "service" as const, hostname: "web.paseo.localhost", port: 3000, proxyUrl: "http://web.paseo.localhost:6767", lifecycle: "running" as const, health: "healthy" as const, exitCode: null, terminalId: null, }; const workspace = createWorkspace({ id: "/repo/main", scripts: [script] }); const current = new Map([[workspace.id, workspace]]); const next = patchWorkspaceScripts(current, { workspaceId: workspace.id, scripts: [{ ...script }], }); expect(next).toBe(current); expect(next.get(workspace.id)).toBe(workspace); }); });