Compare commits

...
Author SHA1 Message Date
paseo-bot[bot] d217f583cf test: reproduce a workspace creation response overwriting a live pin (#5447)
Both tests fail on main. The daemon test shows the create response carrying
pinnedAt: null after a workspace_update that pinned the workspace; the replica
test shows the app applying that response over the pinned row.
2026-09-26 17:23:54 +02:00
2 changed files with 144 additions and 16 deletions
@@ -12,7 +12,10 @@ import {
} from "@/contexts/session-workspace-upserts";
import { WorkspaceDirectoryReplica } from "./workspace-replica";
function workspace(id: string, projectId = "project"): WorkspaceDescriptorPayload {
function workspace(
id: string,
projectId = "project"
): WorkspaceDescriptorPayload {
return {
id,
projectId,
@@ -47,11 +50,16 @@ it("commits workspace and project-parent state with filtered removals", () => {
{
workspaces: new Map([
["kept", normalizeWorkspaceDescriptor(workspace("kept"))],
["filtered", normalizeWorkspaceDescriptor(workspace("filtered", "filtered-project"))],
[
"filtered",
normalizeWorkspaceDescriptor(
workspace("filtered", "filtered-project")
),
],
]),
projects: new Map([[empty.projectId, empty]]),
},
[{ kind: "remove", id: "filtered", removedProjectId: "filtered-project" }],
[{ kind: "remove", id: "filtered", removedProjectId: "filtered-project" }]
);
const session = useSessionStore.getState().sessions[serverId];
@@ -65,10 +73,16 @@ it("commits the authoritative snapshot before buffered project updates", () => {
const store = useSessionStore.getState();
store.initializeSession(serverId, null as unknown as DaemonClient);
const replica = new WorkspaceDirectoryReplica(serverId);
const attachedMain = normalizeWorkspaceDescriptor(workspace("attached-main", "attached"));
const attachedFeature = normalizeWorkspaceDescriptor(workspace("attached-feature", "attached"));
const attachedMain = normalizeWorkspaceDescriptor(
workspace("attached-main", "attached")
);
const attachedFeature = normalizeWorkspaceDescriptor(
workspace("attached-feature", "attached")
);
const removed = normalizeWorkspaceDescriptor(workspace("removed", "removed"));
const unrelated = normalizeWorkspaceDescriptor(workspace("unrelated", "unrelated"));
const unrelated = normalizeWorkspaceDescriptor(
workspace("unrelated", "unrelated")
);
const staleAttachedProject = normalizeProjectDescriptor({
projectId: "attached",
projectDisplayName: "Stale attached project",
@@ -124,7 +138,7 @@ it("commits the authoritative snapshot before buffered project updates", () => {
},
},
{ kind: "remove", projectId: "removed" },
],
]
);
const session = useSessionStore.getState().sessions[serverId];
@@ -178,9 +192,11 @@ it("preserves unchanged project identity when another project changes", () => {
["second", second],
]),
},
[],
[]
);
const previousSecond = useSessionStore.getState().sessions[serverId]?.projects.get("second");
const previousSecond = useSessionStore
.getState()
.sessions[serverId]?.projects.get("second");
replica.commitSnapshot(
{
@@ -190,12 +206,12 @@ it("preserves unchanged project identity when another project changes", () => {
["second", { ...second }],
]),
},
[],
[]
);
expect(useSessionStore.getState().sessions[serverId]?.projects.get("second")).toBe(
previousSecond,
);
expect(
useSessionStore.getState().sessions[serverId]?.projects.get("second")
).toBe(previousSecond);
store.clearSession(serverId);
});
@@ -205,7 +221,10 @@ it("does not invent a null-key project from a workspace update", () => {
store.initializeSession(serverId, null as unknown as DaemonClient);
const replica = new WorkspaceDirectoryReplica(serverId);
replica.applyDelta({ kind: "upsert", workspace: workspace("main", "fresh-project") });
replica.applyDelta({
kind: "upsert",
workspace: workspace("main", "fresh-project"),
});
const session = useSessionStore.getState().sessions[serverId];
expect(session?.workspaces.has("main")).toBe(true);
@@ -222,11 +241,40 @@ it("does not restore a targeted cached workspace while its archive is pending",
markWorkspaceArchivePending({ serverId, workspaceId });
try {
replica.commitCachedWorkspace(normalizeWorkspaceDescriptor(workspace(workspaceId)), undefined);
replica.commitCachedWorkspace(
normalizeWorkspaceDescriptor(workspace(workspaceId)),
undefined
);
expect(useSessionStore.getState().sessions[serverId]?.workspaces.has(workspaceId)).toBe(false);
expect(
useSessionStore.getState().sessions[serverId]?.workspaces.has(workspaceId)
).toBe(false);
} finally {
clearWorkspaceArchivePending({ serverId, workspaceId });
store.clearSession(serverId);
}
});
it("keeps a live pin when the older workspace creation response arrives after it (#5447)", () => {
const serverId = "creation-response-after-pin";
const store = useSessionStore.getState();
store.initializeSession(serverId, null as unknown as DaemonClient);
const replica = new WorkspaceDirectoryReplica(serverId);
const created = workspace("created");
const pinnedAt = "2026-09-26T12:29:32.206Z";
replica.applyDelta({ kind: "upsert", workspace: created });
replica.applyDelta({ kind: "upsert", workspace: { ...created, pinnedAt } });
replica.acceptWorkspaces([
{
...normalizeWorkspaceDescriptor({ ...created, pinnedAt: null }),
status: "running",
},
]);
const row = useSessionStore
.getState()
.sessions[serverId]?.workspaces.get("created");
expect(row?.pinnedAt).toBe(pinnedAt);
store.clearSession(serverId);
});
@@ -0,0 +1,80 @@
import { test, expect } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { DaemonClient } from "./test-utils/index.js";
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
// #5447: a second client pins a workspace as soon as it sees it appear, while
// the creating client is still waiting for its initial agent.
test("workspace.create response reflects a pin made while the initial agent was starting", async () => {
const daemon = await createTestPaseoDaemon();
const directory = mkdtempSync(path.join(tmpdir(), "workspace-pin-order-"));
const creator = new DaemonClient({
url: `ws://127.0.0.1:${daemon.port}/ws`,
appVersion: "0.9.2",
});
const pinner = new DaemonClient({
url: `ws://127.0.0.1:${daemon.port}/ws`,
appVersion: "0.9.2",
});
const seen: Array<{ source: string; pinnedAt: string | null | undefined }> =
[];
try {
await creator.connect();
await pinner.connect();
await creator.fetchWorkspaces({ subscribe: {} });
await pinner.fetchWorkspaces({ subscribe: {} });
const pinned = new Set<string>();
pinner.subscribeRawMessages((message) => {
if (
message.type !== "workspace_update" ||
message.payload.kind !== "upsert"
)
return;
const id = message.payload.workspace.id;
if (pinned.has(id)) return;
pinned.add(id);
void pinner.setWorkspacePinned(id, true);
});
creator.subscribeRawMessages((message) => {
if (
message.type === "workspace_update" &&
message.payload.kind === "upsert"
) {
seen.push({
source: "workspace_update",
pinnedAt: message.payload.workspace.pinnedAt,
});
}
if (message.type === "workspace.create.response") {
seen.push({
source: "create_response",
pinnedAt: message.payload.workspace?.pinnedAt,
});
}
});
const created = await creator.createWorkspace({
source: { kind: "directory", path: directory },
agent: { provider: "claude", cwd: directory, modeId: "default" },
});
expect(created.error).toBeNull();
const liveBeforeResponse = seen
.slice(
0,
seen.findIndex((entry) => entry.source === "create_response")
)
.filter((entry) => entry.source === "workspace_update")
.at(-1);
expect(liveBeforeResponse?.pinnedAt).toBeTruthy();
expect(created.workspace?.pinnedAt).toBe(liveBeforeResponse?.pinnedAt);
} finally {
await creator.close().catch(() => undefined);
await pinner.close().catch(() => undefined);
await daemon.close();
rmSync(directory, { recursive: true, force: true });
}
}, 180000);