Files
paseo/packages/app/src/stores/workspace-setup-store.test.ts
T
Mohamed Boudra d63132a37d fix(creation): prevent duplicate workspaces and agents on retries (#4442)
* fix(creation): prevent duplicate workspaces and agents on retries

* test(creation): cover plugin hooks across receipt replays

* fix(creation): recover draft retries without accepting partial workspaces

* refactor(creation): name receipt inputs and tag compatibility gate

* fix(file-observer): track files announced by native change events

* fix(creation): keep initial prompts in daemon-owned creation requests

* fix(creation): reuse connection-owned progress and recovery

* test(creation): adapt handoff gates to combined requests

* ci(desktop): avoid xz compression for smoke packages

* ci(desktop): pass valid RPM compression through FPM config

* refactor(creation): give legacy and modern requests one identity owner

* fix(creation): log observer failures without interrupting creation
2026-09-14 16:55:42 +02:00

375 lines
11 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import {
shouldSeedWorkspaceSetupTab,
shouldShowWorkspaceSetup,
useWorkspaceSetupStore,
type WorkspaceSetupSnapshot,
type WorkspaceSetupStatusClient,
type WorkspaceSetupStatusResult,
} from "./workspace-setup-store";
function makeSnapshot(
status: WorkspaceSetupSnapshot["status"],
error: string | null = null,
): WorkspaceSetupSnapshot {
return {
workspaceId: "workspace-1",
status,
detail: {
type: "worktree_setup",
worktreePath: "/Users/test/project",
branchName: "main",
log: "",
commands: [],
},
error,
updatedAt: Date.now(),
};
}
const DEFAULT_SNAPSHOT: WorkspaceSetupStatusResult["snapshot"] = {
status: "running",
detail: {
type: "worktree_setup",
worktreePath: "/Users/test/project",
branchName: "main",
log: "",
commands: [],
},
error: null,
};
function setupResult(
workspaceId: string,
snapshot: WorkspaceSetupStatusResult["snapshot"] = DEFAULT_SNAPSHOT,
): WorkspaceSetupStatusResult {
return { requestId: "req-1", workspaceId, snapshot };
}
function makeClient(handler: (workspaceId: string) => Promise<WorkspaceSetupStatusResult>) {
const calls: string[] = [];
const client: WorkspaceSetupStatusClient = {
fetchWorkspaceSetupStatus: (workspaceId) => {
calls.push(workspaceId);
return handler(workspaceId);
},
};
return { client, calls };
}
function createDeferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
function flush() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
function storedSnapshots() {
return Object.values(useWorkspaceSetupStore.getState().snapshots);
}
function resolveDefault(workspaceId: string): Promise<WorkspaceSetupStatusResult> {
return Promise.resolve(setupResult(workspaceId));
}
function rejectThenResolve() {
let attempt = 0;
return (workspaceId: string): Promise<WorkspaceSetupStatusResult> => {
attempt += 1;
if (attempt === 1) {
return Promise.reject(new Error("boom"));
}
return resolveDefault(workspaceId);
};
}
function nullThenResolve() {
let attempt = 0;
return (workspaceId: string): Promise<WorkspaceSetupStatusResult> => {
attempt += 1;
if (attempt === 1) {
return Promise.resolve(setupResult(workspaceId, null));
}
return resolveDefault(workspaceId);
};
}
function mismatchThenResolve() {
let attempt = 0;
return (workspaceId: string): Promise<WorkspaceSetupStatusResult> => {
attempt += 1;
if (attempt === 1) {
return Promise.resolve(setupResult("999"));
}
return resolveDefault(workspaceId);
};
}
function ensureSetupStatus(client: WorkspaceSetupStatusClient) {
useWorkspaceSetupStore.getState().ensureSetupStatus({
serverId: "server-1",
workspaceId: "42",
client,
});
}
describe("workspace-setup-store", () => {
beforeEach(() => {
useWorkspaceSetupStore.setState({
pendingWorkspaceSetup: null,
snapshots: {},
requestedKeys: new Set(),
surfacedFailedSetupKeys: new Set(),
});
});
it("tracks deferred workspace setup by source directory and optional workspace id", () => {
useWorkspaceSetupStore.getState().beginWorkspaceSetup({
serverId: "server-1",
sourceDirectory: "/Users/test/project",
sourceWorkspaceId: "42",
displayName: "project",
creationMethod: "open_project",
});
expect(useWorkspaceSetupStore.getState().pendingWorkspaceSetup).toEqual({
serverId: "server-1",
sourceDirectory: "/Users/test/project",
sourceWorkspaceId: "42",
displayName: "project",
creationMethod: "open_project",
creationId: expect.any(String),
worktreeSlug: expect.any(String),
});
});
it("clears pending setup state", () => {
useWorkspaceSetupStore.getState().beginWorkspaceSetup({
serverId: "server-1",
sourceDirectory: "/Users/test/project",
creationMethod: "create_worktree",
});
useWorkspaceSetupStore.getState().clearWorkspaceSetup();
expect(useWorkspaceSetupStore.getState().pendingWorkspaceSetup).toBeNull();
});
it("hides empty successful setup snapshots", () => {
expect(
shouldShowWorkspaceSetup({
workspaceId: "workspace-1",
status: "completed",
detail: {
type: "worktree_setup",
worktreePath: "/Users/test/project",
branchName: "main",
log: "",
commands: [],
},
error: null,
updatedAt: Date.now(),
}),
).toBe(false);
});
it("shows setup snapshots with commands or errors", () => {
expect(
shouldShowWorkspaceSetup({
workspaceId: "workspace-1",
status: "completed",
detail: {
type: "worktree_setup",
worktreePath: "/Users/test/project",
branchName: "main",
log: "done\n",
commands: [
{
index: 1,
command: "npm install",
cwd: "/Users/test/project",
log: "done\n",
status: "completed",
exitCode: 0,
},
],
},
error: null,
updatedAt: Date.now(),
}),
).toBe(true);
expect(
shouldShowWorkspaceSetup({
workspaceId: "workspace-1",
status: "failed",
detail: {
type: "worktree_setup",
worktreePath: "/Users/test/project",
branchName: "main",
log: "",
commands: [],
},
error: "Failed to parse paseo.json",
updatedAt: Date.now(),
}),
).toBe(true);
});
it("seeds a setup tab for failed or blocked setup", () => {
expect(shouldSeedWorkspaceSetupTab(null)).toBe(false);
expect(shouldSeedWorkspaceSetupTab(makeSnapshot("running"))).toBe(false);
expect(shouldSeedWorkspaceSetupTab(makeSnapshot("completed"))).toBe(false);
expect(shouldSeedWorkspaceSetupTab(makeSnapshot("failed", "Setup failed"))).toBe(true);
expect(shouldSeedWorkspaceSetupTab(makeSnapshot("blocked"))).toBe(true);
});
it("claims one failed setup surface until a later setup lifecycle begins", () => {
const store = useWorkspaceSetupStore.getState();
const identity = { serverId: "server-1", workspaceId: "42" };
store.upsertProgress({
serverId: identity.serverId,
payload: { ...makeSnapshot("failed", "Setup failed"), workspaceId: identity.workspaceId },
});
expect(store.claimFailedSetupSurface(identity)).toBe(true);
expect(useWorkspaceSetupStore.getState().claimFailedSetupSurface(identity)).toBe(false);
useWorkspaceSetupStore.getState().upsertProgress({
serverId: identity.serverId,
payload: { ...makeSnapshot("running"), workspaceId: identity.workspaceId },
});
useWorkspaceSetupStore.getState().upsertProgress({
serverId: identity.serverId,
payload: {
...makeSnapshot("failed", "Setup failed again"),
workspaceId: identity.workspaceId,
},
});
expect(useWorkspaceSetupStore.getState().claimFailedSetupSurface(identity)).toBe(true);
});
it("ensureSetupStatus fetches setup status once and stores the snapshot", async () => {
const { client, calls } = makeClient(resolveDefault);
ensureSetupStatus(client);
await flush();
expect(calls).toEqual(["42"]);
expect(storedSnapshots()).toEqual([
expect.objectContaining({ workspaceId: "42", status: "running" }),
]);
});
it("ensureSetupStatus does not refetch while a request is in flight", async () => {
const deferred = createDeferred<WorkspaceSetupStatusResult>();
const { client, calls } = makeClient(() => deferred.promise);
ensureSetupStatus(client);
ensureSetupStatus(client);
expect(calls).toEqual(["42"]);
deferred.resolve(setupResult("42"));
await flush();
ensureSetupStatus(client);
expect(calls).toEqual(["42"]);
});
it("ensureSetupStatus skips fetching when a snapshot already exists", () => {
useWorkspaceSetupStore.getState().upsertProgress({
serverId: "server-1",
payload: { workspaceId: "42", ...DEFAULT_SNAPSHOT },
});
const { client, calls } = makeClient(resolveDefault);
ensureSetupStatus(client);
expect(calls).toEqual([]);
});
it("ensureSetupStatus ignores a response for a different workspace", async () => {
const { client } = makeClient(() => Promise.resolve(setupResult("999")));
ensureSetupStatus(client);
await flush();
expect(storedSnapshots()).toHaveLength(0);
});
it("ensureSetupStatus does not store a snapshot when the response snapshot is null", async () => {
const { client } = makeClient((workspaceId) => Promise.resolve(setupResult(workspaceId, null)));
ensureSetupStatus(client);
await flush();
expect(storedSnapshots()).toHaveLength(0);
});
it("ensureSetupStatus retries after a null-snapshot response", async () => {
const { client, calls } = makeClient(nullThenResolve());
ensureSetupStatus(client);
await flush();
expect(calls).toEqual(["42"]);
expect(storedSnapshots()).toHaveLength(0);
ensureSetupStatus(client);
await flush();
expect(calls).toEqual(["42", "42"]);
expect(storedSnapshots()).toHaveLength(1);
});
it("ensureSetupStatus retries after a mismatched-workspace response", async () => {
const { client, calls } = makeClient(mismatchThenResolve());
ensureSetupStatus(client);
await flush();
expect(calls).toEqual(["42"]);
expect(storedSnapshots()).toHaveLength(0);
ensureSetupStatus(client);
await flush();
expect(calls).toEqual(["42", "42"]);
expect(storedSnapshots()).toHaveLength(1);
});
it("ensureSetupStatus clears the in-flight marker on error so a later call retries", async () => {
const { client, calls } = makeClient(rejectThenResolve());
ensureSetupStatus(client);
await flush();
expect(calls).toEqual(["42"]);
expect(storedSnapshots()).toHaveLength(0);
ensureSetupStatus(client);
await flush();
expect(calls).toEqual(["42", "42"]);
expect(storedSnapshots()).toHaveLength(1);
});
it("ensureSetupStatus retries after the workspace is removed", async () => {
const { client, calls } = makeClient(resolveDefault);
ensureSetupStatus(client);
await flush();
expect(calls).toEqual(["42"]);
useWorkspaceSetupStore.getState().removeWorkspace({ serverId: "server-1", workspaceId: "42" });
ensureSetupStatus(client);
await flush();
expect(calls).toEqual(["42", "42"]);
});
});