mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d217f583cf |
@@ -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);
|
||||
Reference in New Issue
Block a user