Make workspace IDs opaque, independent of the filesystem path (#1503)
* Make workspace IDs opaque, independent of the filesystem path Workspace IDs were the resolved checkout/worktree path, so code could treat an ID as a path: prefix matching, deriving directories from it, falling back to a path when a lookup missed. IDs are now opaque - compared only by exact equality and never used as a path. Anything path-shaped resolves the workspace record first and reads its cwd. New workspaces get a generated `wks_` ID; existing path-shaped IDs are read from disk and never regenerated, so there is no migration and no change to the wire or persisted schemas. Groundwork for running multiple workspaces in a single directory. * Keep attachment scope key stable and align refetch test The attachment scope key keeps `workspace=` instead of `wsid=` so existing persisted drafts are not orphaned; the rename was cosmetic. The SDK refetch test no longer asserts a filter, matching refetch fetching one page and selecting by id client-side. * Fix live agent updates in directories without a registered workspace The opaque-ID work removed buildProjectPlacementForCwd's directory fallback, so an agent running in a folder with no registered workspace (e.g. a fresh non-git dir) produced a null placement. forwardAgentUpdate then threw "Workspace not found", the error was swallowed by its catch, and no agent_update was emitted — live model/thinking switches and status updates silently stopped. Caught by the live-preferences e2e suite. The fallback builds a directory-scoped project placement keyed by the path. That key is a project grouping key (non-git projects group by path), not a workspace id, so it stays within the opaque-id rule. * Always run server worktree archive, even without a resolved workspace Archiving bailed out entirely when the workspace was not found in the client store, so a race or stale state could make "archive" do nothing server-side with only a console.warn. The server archive is keyed by worktreePath, which is always available, so it now runs regardless; only the optimistic client-side updates (keyed by workspace id) are gated on the workspace being resolved. * Expect a directory-scoped placement for unregistered agent dirs This unit test asserted the no-placement behavior reverted in the live agent-update fix, which had broken live model/thinking switching. Update it to expect the directory-scoped placement now emitted for an agent in a directory with no registered workspace. * Fix opaque workspace routing in app E2E * Preserve workspace IDs during partial bootstrap * Fix archive flows for opaque workspace IDs * Stop treating opaque workspace IDs as filesystem paths Workspace IDs are opaque (wks_<hex>), but several call sites still passed the id where a directory was expected, which broke those flows for opaque IDs. The branch switcher sent the id as a cwd to branch/stash/checkout git operations, and server archive/reconcile cleanup keyed git-watch and subscription teardown by id, leaking that state. Git and filesystem operations now take the workspace directory; the opaque id is used only for identity and cache keys. Archive/reconcile cleanup routes through a single teardownArchivedWorkspace helper that keeps the key split explicit: runtime store by id, git watch and subscription by cwd. Path-derived grouping keys are renamed to directory keys, and the helpers are split into workspace-identity and workspace-directory so the id-vs-path boundary is obvious.
This commit is contained in:
@@ -490,8 +490,29 @@ function createSessionForWorkspaceTests(
|
||||
notifyAgentState: () => {},
|
||||
}),
|
||||
agentStorage: asAgentStorage({
|
||||
list: async () => [],
|
||||
get: async () => null,
|
||||
list: async () => [
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-repo-running",
|
||||
projectId: "proj-repo-running",
|
||||
cwd: REPO_CWD,
|
||||
kind: "directory",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
}),
|
||||
],
|
||||
get: async (workspaceId: string) =>
|
||||
workspaceId === "ws-repo-running"
|
||||
? createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-repo-running",
|
||||
projectId: "proj-repo-running",
|
||||
cwd: REPO_CWD,
|
||||
kind: "directory",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
})
|
||||
: null,
|
||||
upsert: async () => {},
|
||||
}),
|
||||
projectRegistry: {
|
||||
@@ -506,8 +527,29 @@ function createSessionForWorkspaceTests(
|
||||
workspaceRegistry: {
|
||||
initialize: async () => {},
|
||||
existsOnDisk: async () => true,
|
||||
list: async () => [],
|
||||
get: async () => null,
|
||||
list: async () => [
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-repo-running",
|
||||
projectId: "proj-repo-running",
|
||||
cwd: REPO_CWD,
|
||||
kind: "directory",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
}),
|
||||
],
|
||||
get: async (workspaceId: string) =>
|
||||
workspaceId === "ws-repo-running"
|
||||
? createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-repo-running",
|
||||
projectId: "proj-repo-running",
|
||||
cwd: REPO_CWD,
|
||||
kind: "directory",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
})
|
||||
: null,
|
||||
upsert: async () => {},
|
||||
archive: async () => {},
|
||||
remove: async () => {},
|
||||
@@ -853,10 +895,10 @@ test("agent_update placement does not refresh git snapshots", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("agent_update emits fallback placement when no workspace is registered", async () => {
|
||||
test("agent_update emits a directory-scoped placement when no workspace is registered", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const getSnapshot = vi.fn(async () => {
|
||||
throw new Error("getSnapshot should not be called for fallback agent_update placement");
|
||||
throw new Error("getSnapshot should not be called for unregistered agent_update placement");
|
||||
});
|
||||
const session = asTestSession(
|
||||
createSessionForWorkspaceTests({
|
||||
@@ -886,17 +928,15 @@ test("agent_update emits fallback placement when no workspace is registered", as
|
||||
);
|
||||
|
||||
expect(getSnapshot).not.toHaveBeenCalled();
|
||||
expect(emitted.find((message) => message.type === "agent_update")?.payload).toMatchObject({
|
||||
kind: "upsert",
|
||||
project: {
|
||||
projectKey: UNREGISTERED_CWD,
|
||||
projectName: "unregistered",
|
||||
checkout: {
|
||||
cwd: UNREGISTERED_CWD,
|
||||
isGit: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
// An agent in a directory with no registered workspace must still emit an
|
||||
// upsert (live model/thinking switches depend on this). It gets a directory-
|
||||
// scoped placement keyed by the path — a project key, not a workspace id.
|
||||
const update = emitted.find((message) => message.type === "agent_update");
|
||||
if (update?.type !== "agent_update" || update.payload.kind !== "upsert") {
|
||||
throw new Error("expected an agent_update upsert");
|
||||
}
|
||||
expect(update.payload.agent.id).toBe("agent-1");
|
||||
expect(update.payload.project?.checkout.isGit).toBe(false);
|
||||
});
|
||||
|
||||
test("archive emits an authoritative agent_update upsert for subscribed clients", async () => {
|
||||
@@ -1158,7 +1198,7 @@ test("workspace clear attention can clear multiple workspaces in one request", a
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const workspaces = [
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: "/tmp/repo-a",
|
||||
workspaceId: "ws-repo-a",
|
||||
projectId: "/tmp/repo-a",
|
||||
cwd: "/tmp/repo-a",
|
||||
kind: "directory",
|
||||
@@ -1167,7 +1207,7 @@ test("workspace clear attention can clear multiple workspaces in one request", a
|
||||
updatedAt: "2026-03-30T15:00:00.000Z",
|
||||
}),
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: "/tmp/repo-b",
|
||||
workspaceId: "ws-repo-b",
|
||||
projectId: "/tmp/repo-b",
|
||||
cwd: "/tmp/repo-b",
|
||||
kind: "directory",
|
||||
@@ -2570,8 +2610,29 @@ test("workspace update stream keeps persisted workspace visible after agents sto
|
||||
workspaceRegistry: {
|
||||
initialize: async () => {},
|
||||
existsOnDisk: async () => true,
|
||||
list: async () => [],
|
||||
get: async () => null,
|
||||
list: async () => [
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-repo-running",
|
||||
projectId: "proj-repo-running",
|
||||
cwd: REPO_CWD,
|
||||
kind: "directory",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
}),
|
||||
],
|
||||
get: async (workspaceId: string) =>
|
||||
workspaceId === "ws-repo-running"
|
||||
? createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-repo-running",
|
||||
projectId: "proj-repo-running",
|
||||
cwd: REPO_CWD,
|
||||
kind: "directory",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
})
|
||||
: null,
|
||||
upsert: async () => {},
|
||||
archive: async () => {},
|
||||
remove: async () => {},
|
||||
@@ -2619,7 +2680,7 @@ test("workspace update stream keeps persisted workspace visible after agents sto
|
||||
session.buildWorkspaceDescriptorMap = async () =>
|
||||
new Map([
|
||||
[
|
||||
REPO_CWD,
|
||||
"ws-repo-running",
|
||||
{
|
||||
id: "ws-repo-running",
|
||||
projectId: "proj-repo-running",
|
||||
@@ -2638,7 +2699,7 @@ test("workspace update stream keeps persisted workspace visible after agents sto
|
||||
session.buildWorkspaceDescriptorMap = async () =>
|
||||
new Map([
|
||||
[
|
||||
REPO_CWD,
|
||||
"ws-repo-running",
|
||||
{
|
||||
id: "ws-repo-running",
|
||||
projectId: "proj-repo-running",
|
||||
@@ -2736,8 +2797,8 @@ test("create paseo worktree request returns a registered workspace descriptor",
|
||||
const workspaces = new Map();
|
||||
const projects = new Map();
|
||||
session.paseoHome = paseoHome;
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
@@ -2775,7 +2836,8 @@ test("create paseo worktree request returns a registered workspace descriptor",
|
||||
name: "worktree-123",
|
||||
status: "done",
|
||||
});
|
||||
expect(response?.payload.workspace?.id).toContain(path.join("worktree-123"));
|
||||
expect(response?.payload.workspace?.id).toMatch(/^wks_[0-9a-f]{16}$/);
|
||||
expect(response?.payload.workspace?.workspaceDirectory).toContain(path.join("worktree-123"));
|
||||
expect(workspaces.has(response?.payload.workspace?.id ?? "")).toBe(true);
|
||||
expect(projects.has(response?.payload.workspace?.projectId ?? "")).toBe(true);
|
||||
});
|
||||
@@ -2875,8 +2937,8 @@ test("open_project_request registers a workspace before any agent exists", async
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -2904,10 +2966,13 @@ test("open_project_request registers a workspace before any agent exists", async
|
||||
requestId: "req-open",
|
||||
});
|
||||
|
||||
expect(workspaces.get(REPO_CWD)).toBeTruthy();
|
||||
const registeredWorkspace = Array.from(workspaces.values()).find(
|
||||
(workspace) => workspace.cwd === REPO_CWD,
|
||||
);
|
||||
expect(registeredWorkspace).toBeTruthy();
|
||||
const response = findByType(emitted, "open_project_response");
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace?.id).toBe(REPO_CWD);
|
||||
expect(response?.payload.workspace?.id).toBe(registeredWorkspace?.workspaceId);
|
||||
});
|
||||
|
||||
test("import_agent_request registers a workspace for a never-seen cwd", async () => {
|
||||
@@ -2927,8 +2992,8 @@ test("import_agent_request registers a workspace for a never-seen cwd", async ()
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -2971,15 +3036,22 @@ test("import_agent_request registers a workspace for a never-seen cwd", async ()
|
||||
pendingUpdatesByWorkspaceId: new Map(),
|
||||
lastEmittedByWorkspaceId: new Map(),
|
||||
};
|
||||
session.buildWorkspaceDescriptorMap = async () =>
|
||||
new Map([
|
||||
session.buildWorkspaceDescriptorMap = async () => {
|
||||
const workspace = Array.from(workspaces.values()).find(
|
||||
(candidate) => candidate.cwd === importedCwd,
|
||||
);
|
||||
if (!workspace) {
|
||||
return new Map();
|
||||
}
|
||||
return new Map([
|
||||
[
|
||||
importedCwd,
|
||||
workspace.workspaceId,
|
||||
{
|
||||
id: importedCwd,
|
||||
projectId: importedCwd,
|
||||
id: workspace.workspaceId,
|
||||
projectId: workspace.projectId,
|
||||
projectDisplayName: "imported-project",
|
||||
projectRootPath: importedCwd,
|
||||
workspaceDirectory: importedCwd,
|
||||
projectKind: "non_git",
|
||||
workspaceKind: "directory",
|
||||
name: "imported-project",
|
||||
@@ -2988,6 +3060,7 @@ test("import_agent_request registers a workspace for a never-seen cwd", async ()
|
||||
},
|
||||
],
|
||||
]);
|
||||
};
|
||||
|
||||
await session.handleMessage({
|
||||
type: "import_agent_request",
|
||||
@@ -2997,12 +3070,17 @@ test("import_agent_request registers a workspace for a never-seen cwd", async ()
|
||||
cwd: importedCwd,
|
||||
});
|
||||
|
||||
expect(workspaces.get(importedCwd)).toBeTruthy();
|
||||
const importedWorkspace = Array.from(workspaces.values()).find(
|
||||
(workspace) => workspace.cwd === importedCwd,
|
||||
);
|
||||
expect(importedWorkspace).toBeTruthy();
|
||||
const workspaceUpdates = filterByType(emitted, "workspace_update");
|
||||
expect(workspaceUpdates.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
workspaceUpdates.some(
|
||||
(update) => update.payload.kind === "upsert" && update.payload.workspace.id === importedCwd,
|
||||
(update) =>
|
||||
update.payload.kind === "upsert" &&
|
||||
update.payload.workspace.workspaceDirectory === importedCwd,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -3023,8 +3101,8 @@ test("open_project_response returns immediately even when the GitHub fetch is sl
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -3062,7 +3140,8 @@ test("open_project_response returns immediately even when the GitHub fetch is sl
|
||||
|
||||
const response = findByType(emitted, "open_project_response");
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace?.id).toBe(cwd);
|
||||
expect(response?.payload.workspace?.id).toMatch(/^wks_[0-9a-f]{16}$/);
|
||||
expect(response?.payload.workspace?.workspaceDirectory).toBe(cwd);
|
||||
expect(response?.payload.workspace?.gitRuntime).toBeUndefined();
|
||||
expect(response?.payload.workspace?.githubRuntime).toBeUndefined();
|
||||
|
||||
@@ -3089,8 +3168,8 @@ test("open_project_request emits a workspace_update with githubRuntime once the
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -3143,7 +3222,7 @@ test("open_project_request emits a workspace_update with githubRuntime once the
|
||||
.map((update) => update.payload)
|
||||
.filter(
|
||||
(payload): payload is WorkspaceUpsertPayload =>
|
||||
payload.kind === "upsert" && payload.workspace.id === cwd,
|
||||
payload.kind === "upsert" && payload.workspace.workspaceDirectory === cwd,
|
||||
)
|
||||
.find((payload) => payload.workspace.githubRuntime?.pullRequest);
|
||||
expect(upsertedWithGitHub?.workspace.githubRuntime?.pullRequest).toEqual(
|
||||
@@ -3155,6 +3234,7 @@ interface WorkspaceUpsertPayload {
|
||||
kind: "upsert";
|
||||
workspace: {
|
||||
id: string;
|
||||
workspaceDirectory?: string;
|
||||
githubRuntime?: {
|
||||
pullRequest?: { url?: string } | null;
|
||||
} | null;
|
||||
@@ -3202,8 +3282,8 @@ test("open_project_request does not match a new child directory to an existing p
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -3220,9 +3300,11 @@ test("open_project_request does not match a new child directory to an existing p
|
||||
|
||||
const response = findByType(emitted, "open_project_response");
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace?.id).toBe(worktree);
|
||||
expect(response?.payload.workspace?.id).toMatch(/^wks_[0-9a-f]{16}$/);
|
||||
expect(response?.payload.workspace?.workspaceDirectory).toBe(worktree);
|
||||
expect(workspaces.get(worktree)).toBeTruthy();
|
||||
expect(Array.from(workspaces.values()).some((workspace) => workspace.cwd === worktree)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("open_project_request does not unarchive an archived parent workspace for a new child directory", async () => {
|
||||
@@ -3269,8 +3351,8 @@ test("open_project_request does not unarchive an archived parent workspace for a
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -3287,7 +3369,7 @@ test("open_project_request does not unarchive an archived parent workspace for a
|
||||
|
||||
const response = findByType(emitted, "open_project_response");
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace?.id).toBe(worktree);
|
||||
expect(response?.payload.workspace?.id).toMatch(/^wks_[0-9a-f]{16}$/);
|
||||
expect(response?.payload.workspace?.workspaceDirectory).toBe(worktree);
|
||||
expect(workspaces.get(home)?.archivedAt).toBe(archivedAt);
|
||||
expect(projects.get(home)?.archivedAt).toBe(archivedAt);
|
||||
@@ -3308,6 +3390,7 @@ test("open_project_request reclassifies an archived directory workspace when git
|
||||
);
|
||||
const remoteProjectId = "remote:github.com/getpaseo/paseo";
|
||||
const archivedAt = "2026-04-24T09:48:36.168Z";
|
||||
const workspaceId = "ws-desktop-daemon-settings";
|
||||
|
||||
projects.set(
|
||||
cwd,
|
||||
@@ -3322,9 +3405,9 @@ test("open_project_request reclassifies an archived directory workspace when git
|
||||
}),
|
||||
);
|
||||
workspaces.set(
|
||||
cwd,
|
||||
workspaceId,
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: cwd,
|
||||
workspaceId,
|
||||
projectId: cwd,
|
||||
cwd,
|
||||
kind: "directory",
|
||||
@@ -3344,8 +3427,8 @@ test("open_project_request reclassifies an archived directory workspace when git
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -3386,9 +3469,9 @@ test("open_project_request reclassifies an archived directory workspace when git
|
||||
expect(response?.payload.workspace?.projectId).toBe(remoteProjectId);
|
||||
expect(response?.payload.workspace?.workspaceKind).toBe("worktree");
|
||||
expect(projects.get(remoteProjectId)?.kind).toBe("git");
|
||||
expect(workspaces.get(cwd)?.projectId).toBe(remoteProjectId);
|
||||
expect(workspaces.get(cwd)?.kind).toBe("worktree");
|
||||
expect(workspaces.get(cwd)?.displayName).toBe("feature/desktop-daemon-settings");
|
||||
expect(workspaces.get(workspaceId)?.projectId).toBe(remoteProjectId);
|
||||
expect(workspaces.get(workspaceId)?.kind).toBe("worktree");
|
||||
expect(workspaces.get(workspaceId)?.displayName).toBe("feature/desktop-daemon-settings");
|
||||
});
|
||||
|
||||
test("open_project_request reclassifies an active directory workspace when git metadata becomes available", async () => {
|
||||
@@ -3427,10 +3510,12 @@ test("open_project_request reclassifies an active directory workspace when git m
|
||||
updatedAt: "2026-04-24T09:40:00.000Z",
|
||||
}),
|
||||
);
|
||||
const workspaceId = "ws-desktop-daemon-settings-active";
|
||||
const repoWorkspaceId = "ws-paseo-main";
|
||||
workspaces.set(
|
||||
cwd,
|
||||
workspaceId,
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: cwd,
|
||||
workspaceId,
|
||||
projectId: cwd,
|
||||
cwd,
|
||||
kind: "directory",
|
||||
@@ -3440,9 +3525,9 @@ test("open_project_request reclassifies an active directory workspace when git m
|
||||
}),
|
||||
);
|
||||
workspaces.set(
|
||||
repoRoot,
|
||||
repoWorkspaceId,
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: repoRoot,
|
||||
workspaceId: repoWorkspaceId,
|
||||
projectId: repoRoot,
|
||||
cwd: repoRoot,
|
||||
kind: "local_checkout",
|
||||
@@ -3461,8 +3546,8 @@ test("open_project_request reclassifies an active directory workspace when git m
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -3502,9 +3587,9 @@ test("open_project_request reclassifies an active directory workspace when git m
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace?.projectId).toBe(repoRoot);
|
||||
expect(response?.payload.workspace?.workspaceKind).toBe("worktree");
|
||||
expect(workspaces.get(cwd)?.projectId).toBe(repoRoot);
|
||||
expect(workspaces.get(cwd)?.kind).toBe("worktree");
|
||||
expect(workspaces.get(cwd)?.displayName).toBe("feature/desktop-daemon-settings");
|
||||
expect(workspaces.get(workspaceId)?.projectId).toBe(repoRoot);
|
||||
expect(workspaces.get(workspaceId)?.kind).toBe("worktree");
|
||||
expect(workspaces.get(workspaceId)?.displayName).toBe("feature/desktop-daemon-settings");
|
||||
});
|
||||
|
||||
test("open_project_request groups a plain git worktree under an existing repo project", async () => {
|
||||
@@ -3554,8 +3639,8 @@ test("open_project_request groups a plain git worktree under an existing repo pr
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -3595,8 +3680,11 @@ test("open_project_request groups a plain git worktree under an existing repo pr
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace?.projectId).toBe(repoRoot);
|
||||
expect(response?.payload.workspace?.workspaceKind).toBe("worktree");
|
||||
expect(workspaces.get(cwd)?.projectId).toBe(repoRoot);
|
||||
expect(workspaces.get(cwd)?.kind).toBe("worktree");
|
||||
const worktreeWorkspace = Array.from(workspaces.values()).find(
|
||||
(workspace) => workspace.cwd === cwd,
|
||||
);
|
||||
expect(worktreeWorkspace?.projectId).toBe(repoRoot);
|
||||
expect(worktreeWorkspace?.kind).toBe("worktree");
|
||||
});
|
||||
|
||||
test("open_project_request unarchives an existing archived workspace and project", async () => {
|
||||
@@ -3606,6 +3694,7 @@ test("open_project_request unarchives an existing archived workspace and project
|
||||
const workspaces = new Map<string, ReturnType<typeof createPersistedWorkspaceRecord>>();
|
||||
|
||||
const cwd = REPO_CWD;
|
||||
const workspaceId = "ws-repo-archived";
|
||||
projects.set(
|
||||
cwd,
|
||||
createPersistedProjectRecord({
|
||||
@@ -3619,9 +3708,9 @@ test("open_project_request unarchives an existing archived workspace and project
|
||||
}),
|
||||
);
|
||||
workspaces.set(
|
||||
cwd,
|
||||
workspaceId,
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: cwd,
|
||||
workspaceId,
|
||||
projectId: cwd,
|
||||
cwd,
|
||||
kind: "directory",
|
||||
@@ -3641,8 +3730,8 @@ test("open_project_request unarchives an existing archived workspace and project
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -3657,11 +3746,11 @@ test("open_project_request unarchives an existing archived workspace and project
|
||||
requestId: "req-open-unarchive",
|
||||
});
|
||||
|
||||
expect(workspaces.get(cwd)?.archivedAt).toBeNull();
|
||||
expect(workspaces.get(workspaceId)?.archivedAt).toBeNull();
|
||||
expect(projects.get(cwd)?.archivedAt).toBeNull();
|
||||
const response = findByType(emitted, "open_project_response");
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace?.id).toBe(cwd);
|
||||
expect(response?.payload.workspace?.id).toBe(workspaceId);
|
||||
});
|
||||
|
||||
test.skip("open_project_request collapses a git subdirectory onto the repo root workspace", async () => {
|
||||
@@ -3681,8 +3770,8 @@ test.skip("open_project_request collapses a git subdirectory onto the repo root
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
) => {
|
||||
@@ -3842,8 +3931,8 @@ test.skip("opening a new worktree reconciles older local workspaces into the rem
|
||||
if (!existing) return;
|
||||
projects.set(projectId, { ...existing, archivedAt, updatedAt: archivedAt });
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
@@ -3948,8 +4037,8 @@ test.skip("fetch_workspaces_request reconciles remote URL changes for existing w
|
||||
if (!existing) return;
|
||||
projects.set(projectId, { ...existing, archivedAt, updatedAt: archivedAt });
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
@@ -4044,8 +4133,8 @@ test.skip("reconcile archives stale subdirectory workspace records when collapsi
|
||||
) => {
|
||||
projects.set(record.projectId, record);
|
||||
};
|
||||
session.workspaceRegistry.get = async (workspaceId: string) =>
|
||||
workspaces.get(workspaceId) ?? null;
|
||||
session.workspaceRegistry.get = async (lookupWorkspaceId: string) =>
|
||||
workspaces.get(lookupWorkspaceId) ?? null;
|
||||
session.workspaceRegistry.list = async () => Array.from(workspaces.values());
|
||||
session.workspaceRegistry.upsert = async (
|
||||
record: ReturnType<typeof createPersistedWorkspaceRecord>,
|
||||
@@ -5040,7 +5129,7 @@ test("resolveRegisteredWorkspaceIdForCwd does not match home directory as a pref
|
||||
const home = homedir();
|
||||
const childCwd = path.join(home, "projects/new-app");
|
||||
const homeWorkspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: home,
|
||||
workspaceId: "ws-home",
|
||||
projectId: "proj-home",
|
||||
cwd: home,
|
||||
kind: "directory",
|
||||
@@ -5049,6 +5138,6 @@ test("resolveRegisteredWorkspaceIdForCwd does not match home directory as a pref
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(session.resolveRegisteredWorkspaceIdForCwd(childCwd, [homeWorkspace])).toBe(childCwd);
|
||||
expect(session.resolveRegisteredWorkspaceIdForCwd(home, [homeWorkspace])).toBe(home);
|
||||
expect(session.resolveRegisteredWorkspaceIdForCwd(childCwd, [homeWorkspace])).toBeNull();
|
||||
expect(session.resolveRegisteredWorkspaceIdForCwd(home, [homeWorkspace])).toBe("ws-home");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user