From 7b9787a75d60f2bb06889963c825cbb22e7e91e5 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 29 Jul 2026 10:59:01 +0000 Subject: [PATCH] fix(projects): resolve legacy mutation aliases --- .../src/projects/project-settings-target.test.ts | 13 +++++++++++++ .../app/src/projects/project-settings-target.ts | 4 ++-- packages/app/src/stores/session-store.ts | 1 + packages/server/src/server/session.ts | 11 +++++++---- .../server/src/server/session.workspaces.test.ts | 14 ++++++++------ 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/app/src/projects/project-settings-target.test.ts b/packages/app/src/projects/project-settings-target.test.ts index d271030d1..fbd709d22 100644 --- a/packages/app/src/projects/project-settings-target.test.ts +++ b/packages/app/src/projects/project-settings-target.test.ts @@ -52,6 +52,19 @@ describe("project settings target", () => { expect(first).not.toBe(second); }); + it("preserves whitespace in opaque project IDs", () => { + const withoutTrailingSpace = resolveHostProjectSettingsRouteKey({ + serverId: "host-a", + projectId: "/repo/foo", + }); + const withTrailingSpace = resolveHostProjectSettingsRouteKey({ + serverId: "host-a", + projectId: "/repo/foo ", + }); + + expect(withTrailingSpace).not.toBe(withoutTrailingSpace); + }); + it("prefers an online host for a generic grouped settings route", () => { const groupedProject = { projectKey: "remote:github.com/acme/app", diff --git a/packages/app/src/projects/project-settings-target.ts b/packages/app/src/projects/project-settings-target.ts index dcdcbb53b..a18110ce4 100644 --- a/packages/app/src/projects/project-settings-target.ts +++ b/packages/app/src/projects/project-settings-target.ts @@ -9,8 +9,8 @@ export function resolveHostProjectSettingsRouteKey(host: { serverId: string; projectId?: string; }): string | null { - const projectId = host.projectId?.trim(); - if (!projectId) return null; + const projectId = host.projectId; + if (!projectId?.trim()) return null; return frameHostProjectKey({ serverId: host.serverId, projectId }); } diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 17e58da35..c838ee662 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -170,6 +170,7 @@ export function normalizeWorkspaceDescriptor( return { id: normalizeWorkspaceOpaqueId(payload.id) ?? payload.id, projectId: payload.projectId, + // COMPAT(projectKey): added in v0.2.4 on 2026-07-29; remove after 2027-01-29. projectKey: payload.projectKey ?? payload.project?.projectKey ?? null, projectDisplayName: payload.projectDisplayName, projectCustomName: payload.projectCustomName ?? null, diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 56c939712..8533e7d83 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -125,6 +125,7 @@ import { deriveWorkspaceDisplayName, } from "./workspace-registry-model.js"; import { resolveWorkspaceIdForPath } from "./resolve-workspace-id-for-path.js"; +import { resolveProjectReference } from "./project-reference.js"; import { resolveProjectDisplayName, resolveWorkspaceDisplayName, @@ -2588,7 +2589,7 @@ export class Session { ); try { - const existing = await this.projectRegistry.get(projectId); + const existing = await resolveProjectReference(projectId, this.projectRegistry); if (!existing) { this.emit({ type: "project.rename.response", @@ -2632,7 +2633,7 @@ export class Session { // resolved name lands in the UI immediately. const workspaces = await this.workspaceRegistry.list(); const affectedWorkspaceIds = workspaces - .filter((workspace) => workspace.projectId === projectId) + .filter((workspace) => workspace.projectId === existing.projectId) .map((workspace) => workspace.workspaceId); if (affectedWorkspaceIds.length > 0) { await this.emitWorkspaceUpdatesForWorkspaceIds(affectedWorkspaceIds); @@ -2671,8 +2672,10 @@ export class Session { this.sessionLogger.info({ projectId, requestId }, "session: project.remove.request"); try { + const project = await resolveProjectReference(projectId, this.projectRegistry); + const resolvedProjectId = project?.projectId ?? projectId; const projectWorkspaces = (await this.workspaceRegistry.list()).filter( - (workspace) => workspace.projectId === projectId, + (workspace) => workspace.projectId === resolvedProjectId, ); const activeWorkspaceIds = projectWorkspaces .filter((workspace) => !workspace.archivedAt) @@ -2700,7 +2703,7 @@ export class Session { removedWorkspaceIds.push(workspaceId); } - await this.projectRegistry.remove(projectId); + await this.projectRegistry.remove(resolvedProjectId); } finally { if (activeWorkspaceIds.length > 0) { this.clearWorkspaceArchiving(activeWorkspaceIds); diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 226cab842..9259cb670 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -3406,6 +3406,7 @@ test("project.remove.request archives active workspaces and removes the project }); const project = createPersistedProjectRecord({ projectId: "proj-remove-with-workspace", + projectKey: "remote:github.com/acme/remove-with-workspace", rootPath: REPO_CWD, kind: "git", displayName: "repo", @@ -3470,7 +3471,7 @@ test("project.remove.request archives active workspaces and removes the project await session.handleMessage({ type: "project.remove.request", - projectId: project.projectId, + projectId: project.projectKey, requestId: "req-remove-project", }); @@ -3482,7 +3483,7 @@ test("project.remove.request archives active workspaces and removes the project }); expect(findByType(emitted, "project.remove.response")?.payload).toEqual({ requestId: "req-remove-project", - projectId: project.projectId, + projectId: project.projectKey, accepted: true, removedWorkspaceIds: [workspace.workspaceId], error: null, @@ -3491,7 +3492,7 @@ test("project.remove.request archives active workspaces and removes the project expect(workspaceUpdates.at(-1)?.payload).toEqual({ kind: "remove", id: workspace.workspaceId, - removedProjectId: project.projectId, + removedProjectId: project.projectKey, }); }); @@ -7392,7 +7393,8 @@ test("project.rename.request stores customName and emits an updated workspace de ); const project = createPersistedProjectRecord({ - projectId: "remote:github.com/acme/repo", + projectId: "prj_rename", + projectKey: "remote:github.com/acme/repo", rootPath: REPO_CWD, kind: "git", displayName: "acme/repo", @@ -7430,7 +7432,7 @@ test("project.rename.request stores customName and emits an updated workspace de await session.handleMessage({ type: "project.rename.request", - projectId: project.projectId, + projectId: project.projectKey, customName: " My Fork ", requestId: "req-rename-1", }); @@ -7438,7 +7440,7 @@ test("project.rename.request stores customName and emits an updated workspace de const response = findByType(emitted, "project.rename.response"); expect(response?.payload).toEqual({ requestId: "req-rename-1", - projectId: project.projectId, + projectId: project.projectKey, accepted: true, customName: "My Fork", error: null,