mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(projects): harden project reference identity
This commit is contained in:
@@ -33,6 +33,21 @@ describe("deriveProjectKey", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves queries that distinguish generic HTTP remotes", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
mainRepoRoot: null,
|
||||
});
|
||||
|
||||
expect(derive("https://git.example.com/acme/app.git?tenant=a")).not.toBe(
|
||||
derive("https://git.example.com/acme/app.git?tenant=b"),
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes the default SSH port", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
|
||||
@@ -123,6 +123,7 @@ function parseUrlRemote(remoteUrl: string): RemoteLocation | null {
|
||||
const preserveLeadingSlash = isSsh && !forgeHost;
|
||||
let remotePath = parsed.pathname || null;
|
||||
if (remotePath && isSsh) remotePath += `${parsed.search}${parsed.hash}`;
|
||||
if (remotePath && !isSsh && !forgeHost) remotePath += parsed.search;
|
||||
if (remotePath && !preserveLeadingSlash) remotePath = remotePath.replace(/^\/+/, "");
|
||||
if (!host || !remotePath) return null;
|
||||
return {
|
||||
|
||||
47
packages/server/src/server/project-reference.test.ts
Normal file
47
packages/server/src/server/project-reference.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import type { PersistedProjectRecord } from "./workspace-registry.js";
|
||||
import { resolveProjectReference } from "./project-reference.js";
|
||||
|
||||
function project(input: {
|
||||
projectId: string;
|
||||
projectKey: string;
|
||||
rootPath: string;
|
||||
archivedAt?: string | null;
|
||||
}): PersistedProjectRecord {
|
||||
return {
|
||||
projectId: input.projectId,
|
||||
projectKey: input.projectKey,
|
||||
rootPath: input.rootPath,
|
||||
kind: "git",
|
||||
displayName: "acme/app",
|
||||
customName: null,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
archivedAt: input.archivedAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolveProjectReference", () => {
|
||||
test("resolves an active opaque project when an archived legacy project owns the direct id", async () => {
|
||||
const reference = "remote:github.com/acme/app";
|
||||
const archivedLegacyProject = project({
|
||||
projectId: reference,
|
||||
projectKey: reference,
|
||||
rootPath: "/old/app",
|
||||
archivedAt: "2026-02-01T00:00:00.000Z",
|
||||
});
|
||||
const activeProject = project({
|
||||
projectId: "opaque-project-id",
|
||||
projectKey: reference,
|
||||
rootPath: "/new/app",
|
||||
});
|
||||
const registry = {
|
||||
get: vi.fn().mockResolvedValue(archivedLegacyProject),
|
||||
list: vi.fn().mockResolvedValue([archivedLegacyProject, activeProject]),
|
||||
};
|
||||
|
||||
await expect(resolveProjectReference(reference, registry, ["/new/app"])).resolves.toEqual(
|
||||
activeProject,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ export async function resolveProjectReference(
|
||||
pathHints: readonly string[] = [],
|
||||
): Promise<PersistedProjectRecord | null> {
|
||||
const direct = await projectRegistry.get(reference);
|
||||
if (direct) return direct;
|
||||
if (direct && !direct.archivedAt) return direct;
|
||||
|
||||
// COMPAT(projectKeyAsProjectId): added in v0.2.4 on 2026-07-29; remove after 2027-01-29.
|
||||
// Older clients return ProjectPlacementPayload.projectKey in workspace source.projectId.
|
||||
|
||||
Reference in New Issue
Block a user