353 lines
11 KiB
TypeScript
353 lines
11 KiB
TypeScript
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<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
|
|
): 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);
|
|
});
|
|
});
|