fix(projects): scope legacy paths to hosts

This commit is contained in:
Mohamed Boudra
2026-07-29 10:20:55 +00:00
parent 1ff152edcd
commit fef479e749
3 changed files with 36 additions and 9 deletions

View File

@@ -0,0 +1,18 @@
import { describe, expect, test } from "vitest";
import { resolveProjectKey } from "./project-key";
describe("resolveProjectKey", () => {
test("keeps legacy path-shaped project IDs local to their host", () => {
const resolve = (serverId: string) =>
resolveProjectKey({ serverId, projectId: "/workspace/app" });
expect(resolve("host-a")).not.toBe(resolve("host-b"));
});
test("keeps recognized legacy remote project IDs shared across hosts", () => {
const resolve = (serverId: string) =>
resolveProjectKey({ serverId, projectId: "remote:github.com/acme/app" });
expect(resolve("host-a")).toBe(resolve("host-b"));
});
});

View File

@@ -5,9 +5,9 @@ export function resolveProjectKey(input: {
}): string {
if (input.projectKey) return input.projectKey;
// COMPAT(projectKey): added in v0.2.4 on 2026-07-28; remove after 2027-01-28.
// Older daemons used remote/path-shaped project IDs as their grouping key. New opaque IDs
// must remain host-local when the new field is absent.
return input.projectId.startsWith("prj_") ? frameHostProjectKey(input) : input.projectId;
// Older daemons used recognized remote-shaped project IDs as their grouping key. Their
// path-shaped and opaque IDs are host-local when the new field is absent.
return input.projectId.startsWith("remote:") ? input.projectId : frameHostProjectKey(input);
}
export function frameHostProjectKey(input: { serverId: string; projectId: string }): string {

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import type { ProjectPlacementPayload } from "@getpaseo/protocol/messages";
import { frameHostProjectKey } from "@/projects/project-key";
import type { WorkspaceDescriptor } from "@/stores/session-store";
import { buildProjects } from "./projects";
@@ -451,7 +452,11 @@ describe("buildProjects", () => {
const acme = result.projects.find(
(project) => project.projectKey === "remote:github.com/acme/app#subdir:packages/client",
);
const legacy = result.projects.find((project) => project.projectKey === "legacy-project");
const legacy = result.projects.find(
(project) =>
project.projectKey ===
frameHostProjectKey({ serverId: "legacy", projectId: "legacy-project" }),
);
expect(acme?.hosts[0]?.repoRoot).toBe("/worktrees/app/main/packages/client");
expect(legacy?.hosts[0]?.repoRoot).toBe("/repo/legacy");
@@ -584,8 +589,8 @@ describe("buildProjects", () => {
});
expect(result.projects.map((project) => project.projectKey)).toEqual([
"/repo/one",
"/repo/two",
frameHostProjectKey({ serverId: "local", projectId: "/repo/one" }),
frameHostProjectKey({ serverId: "local", projectId: "/repo/two" }),
]);
});
@@ -646,7 +651,7 @@ describe("buildProjects", () => {
"remote:gitlab.com/acme/api",
"remote:bitbucket.org/acme/cli",
"remote:github.com/acme/web",
"/repo/local",
frameHostProjectKey({ serverId: "local", projectId: "/repo/local" }),
]);
});
@@ -739,7 +744,9 @@ describe("buildProjects", () => {
expect(result.projects).toHaveLength(1);
const summary = result.projects[0];
expect(summary?.projectKey).toBe("legacy-project");
expect(summary?.projectKey).toBe(
frameHostProjectKey({ serverId: "old-daemon", projectId: "legacy-project" }),
);
expect(summary?.projectName).toBe("Legacy");
expect(summary?.githubUrl).toBeUndefined();
expect(summary?.hosts).toHaveLength(1);
@@ -770,7 +777,9 @@ describe("buildProjects", () => {
expect(result.projects).toHaveLength(1);
const summary = result.projects[0];
expect(summary?.projectKey).toBe("/repo/fresh");
expect(summary?.projectKey).toBe(
frameHostProjectKey({ serverId: "local", projectId: "/repo/fresh" }),
);
expect(summary?.totalWorkspaceCount).toBe(0);
expect(summary?.hosts).toHaveLength(1);
// repoRoot must be non-empty or the project settings screen treats the host