mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
* 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
375 lines
11 KiB
TypeScript
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"]);
|
|
});
|
|
});
|