mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(projects): restore project key as grouping identity
Random host-local project IDs had replaced the persisted cross-host value carried by projectKey. Keep projectId for routing while reconciliation owns and persists projectKey for grouping.
This commit is contained in:
@@ -222,9 +222,9 @@ test.describe("Project remove", () => {
|
||||
expect(readded.error).toBeNull();
|
||||
expect(readded.project).not.toBeNull();
|
||||
readdedProjectId = readded.project?.projectId ?? "";
|
||||
const readdedProjectGroupKey = readded.project?.projectGroupKey ?? "";
|
||||
const readdedProjectKey = readded.project?.projectKey ?? "";
|
||||
expect(readdedProjectId).not.toBe(workspace.projectId);
|
||||
expect(readdedProjectGroupKey).toBe(workspace.projectKey);
|
||||
expect(readdedProjectKey).toBe(workspace.projectKey);
|
||||
expect(readded.project?.projectDisplayName).toBe(workspace.projectDisplayName);
|
||||
|
||||
await page.reload();
|
||||
@@ -233,7 +233,7 @@ test.describe("Project remove", () => {
|
||||
await expect(projectRow).toContainText(workspace.projectDisplayName);
|
||||
await expect(projectRow).not.toContainText(workspace.repoPath);
|
||||
await expect(
|
||||
page.getByTestId(`sidebar-project-new-workspace-row-${readdedProjectGroupKey}`),
|
||||
page.getByTestId(`sidebar-project-new-workspace-row-${readdedProjectKey}`),
|
||||
).toBeVisible({ timeout: 30_000 });
|
||||
} finally {
|
||||
if (readdedProjectId) {
|
||||
|
||||
@@ -46,9 +46,9 @@ function requireWorkspace(payload: WorkspacePayload) {
|
||||
}
|
||||
|
||||
function openedProjectFromWorkspace(workspace: WorkspaceDescriptor): OpenedProject {
|
||||
const projectKey = workspace.projectGroupKey ?? workspace.project?.projectGroupKey;
|
||||
const projectKey = workspace.projectKey ?? workspace.project?.projectKey;
|
||||
if (!projectKey) {
|
||||
throw new Error(`Workspace ${workspace.id} has no project group key`);
|
||||
throw new Error(`Workspace ${workspace.id} has no project key`);
|
||||
}
|
||||
return {
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -8,8 +8,8 @@ export interface SeedWorkspaceDescriptor {
|
||||
id: string;
|
||||
name: string;
|
||||
projectId: string;
|
||||
projectGroupKey?: string;
|
||||
project?: { projectGroupKey?: string };
|
||||
projectKey?: string;
|
||||
project?: { projectKey?: string };
|
||||
projectDisplayName: string;
|
||||
projectRootPath: string;
|
||||
workspaceDirectory: string;
|
||||
@@ -27,7 +27,7 @@ export interface SeedDaemonClient {
|
||||
addProject(cwd: string): Promise<{
|
||||
project: {
|
||||
projectId: string;
|
||||
projectGroupKey?: string;
|
||||
projectKey?: string;
|
||||
projectDisplayName: string;
|
||||
projectRootPath: string;
|
||||
} | null;
|
||||
@@ -213,9 +213,9 @@ export async function seedWorkspace(options: {
|
||||
throw new Error(created.error ?? `Failed to create workspace ${project.path}`);
|
||||
}
|
||||
const workspace = created.workspace;
|
||||
const projectKey = workspace.projectGroupKey ?? workspace.project?.projectGroupKey;
|
||||
const projectKey = workspace.projectKey ?? workspace.project?.projectKey;
|
||||
if (!projectKey) {
|
||||
throw new Error(`Created workspace ${workspace.id} has no project group key`);
|
||||
throw new Error(`Created workspace ${workspace.id} has no project key`);
|
||||
}
|
||||
return {
|
||||
client,
|
||||
|
||||
@@ -65,12 +65,9 @@ async function seedPaseoWorkspaceWithOpenCodeSession(): Promise<OpenCodeImportSc
|
||||
throw new Error(createdWorkspace.error ?? `Failed to create workspace ${PASEO_REPO_PATH}`);
|
||||
}
|
||||
const projectKey =
|
||||
createdWorkspace.workspace.projectGroupKey ??
|
||||
createdWorkspace.workspace.project?.projectGroupKey;
|
||||
createdWorkspace.workspace.projectKey ?? createdWorkspace.workspace.project?.projectKey;
|
||||
if (!projectKey) {
|
||||
throw new Error(
|
||||
`Created workspace ${createdWorkspace.workspace.id} has no project group key`,
|
||||
);
|
||||
throw new Error(`Created workspace ${createdWorkspace.workspace.id} has no project key`);
|
||||
}
|
||||
return {
|
||||
prompt,
|
||||
|
||||
@@ -78,27 +78,27 @@ async function openGroupedProjectSettings(page: Page): Promise<void> {
|
||||
await expect(page.getByTestId("host-picker")).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function readPersistedProjectGroupKey(host: IsolatedHostDaemon): Promise<unknown> {
|
||||
async function readPersistedProjectKey(host: IsolatedHostDaemon): Promise<unknown> {
|
||||
const projectsPath = path.join(host.paseoHome, "projects", "projects.json");
|
||||
const projects = JSON.parse(await readFile(projectsPath, "utf8")) as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
return projects[0]?.projectGroupKey;
|
||||
return projects[0]?.projectKey;
|
||||
}
|
||||
|
||||
async function removePersistedProjectGroupKeys(host: IsolatedHostDaemon): Promise<void> {
|
||||
async function removePersistedProjectKeys(host: IsolatedHostDaemon): Promise<void> {
|
||||
const projectsPath = path.join(host.paseoHome, "projects", "projects.json");
|
||||
const projects = JSON.parse(await readFile(projectsPath, "utf8")) as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
for (const project of projects) {
|
||||
delete project.projectGroupKey;
|
||||
delete project.projectKey;
|
||||
}
|
||||
await writeFile(projectsPath, JSON.stringify(projects));
|
||||
const persisted = JSON.parse(await readFile(projectsPath, "utf8")) as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
expect(persisted.every((project) => !("projectGroupKey" in project))).toBe(true);
|
||||
expect(persisted.every((project) => !("projectKey" in project))).toBe(true);
|
||||
}
|
||||
|
||||
async function rewritePersistedProjectId(
|
||||
@@ -147,8 +147,8 @@ async function createReconciliationFixture(options?: { sharedLegacyProjectId?: s
|
||||
let secondary = await createProject(secondaryClient, secondaryRepo, secondaryHost.serverId);
|
||||
await primaryClient.close();
|
||||
await secondaryClient.close();
|
||||
await removePersistedProjectGroupKeys(primaryHost);
|
||||
await removePersistedProjectGroupKeys(secondaryHost);
|
||||
await removePersistedProjectKeys(primaryHost);
|
||||
await removePersistedProjectKeys(secondaryHost);
|
||||
if (options?.sharedLegacyProjectId) {
|
||||
await Promise.all([
|
||||
rewritePersistedProjectId(primaryHost, primary.projectId, options.sharedLegacyProjectId),
|
||||
@@ -256,7 +256,7 @@ test.describe("Sidebar project grouping", () => {
|
||||
await expectOneProjectContainsBothWorkspaces(page, crossHostProject);
|
||||
});
|
||||
|
||||
test("groups persisted projects missing group keys after app boot", async ({
|
||||
test("groups persisted projects missing project keys after app boot", async ({
|
||||
page,
|
||||
reconciledCrossHostProject,
|
||||
}) => {
|
||||
@@ -286,10 +286,10 @@ test.describe("Sidebar project grouping", () => {
|
||||
});
|
||||
await expectOneProjectContainsBothWorkspaces(page, reconciledCrossHostProject);
|
||||
await expect
|
||||
.poll(() => readPersistedProjectGroupKey(reconciledCrossHostProject.primaryHost))
|
||||
.poll(() => readPersistedProjectKey(reconciledCrossHostProject.primaryHost))
|
||||
.toBe("remote:github.com/paseo-e2e/grouped-project");
|
||||
await expect
|
||||
.poll(() => readPersistedProjectGroupKey(reconciledCrossHostProject.secondaryHost))
|
||||
.poll(() => readPersistedProjectKey(reconciledCrossHostProject.secondaryHost))
|
||||
.toBe("remote:github.com/paseo-e2e/grouped-project");
|
||||
});
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ interface RestartDaemonClient {
|
||||
name: string;
|
||||
status: string;
|
||||
workspaceDirectory: string;
|
||||
projectGroupKey?: string;
|
||||
project?: { projectGroupKey?: string };
|
||||
projectKey?: string;
|
||||
project?: { projectKey?: string };
|
||||
}>;
|
||||
}>;
|
||||
fetchAgents(options?: { scope?: "active" }): Promise<{
|
||||
@@ -467,7 +467,7 @@ test.describe("Workspace model restart regressions", () => {
|
||||
(workspace) => workspace.id === seeded.workspaceA,
|
||||
);
|
||||
const reconciledProjectKey =
|
||||
reconciledWorkspace?.projectGroupKey ?? reconciledWorkspace?.project?.projectGroupKey;
|
||||
reconciledWorkspace?.projectKey ?? reconciledWorkspace?.project?.projectKey;
|
||||
if (!reconciledProjectKey) {
|
||||
throw new Error(`Workspace ${seeded.workspaceA} was not reconciled with a project key`);
|
||||
}
|
||||
|
||||
@@ -27,13 +27,13 @@ describe("selectActiveGitWorkspaceProject", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the persisted project group key for the settings route", () => {
|
||||
it("uses the persisted project key for the settings route", () => {
|
||||
expect(
|
||||
selectActiveGitWorkspaceProject(
|
||||
"server-1",
|
||||
gitWorkspace({
|
||||
projectId: "prj_local",
|
||||
projectGroupKey: "remote:github.com/acme/project",
|
||||
projectKey: "remote:github.com/acme/project",
|
||||
}),
|
||||
),
|
||||
).toMatchObject({
|
||||
@@ -101,7 +101,7 @@ describe("buildWorktreeSetupCalloutPolicy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the action route stable when the structural group key changes", () => {
|
||||
it("keeps the action route stable when the structural project key changes", () => {
|
||||
expect(
|
||||
buildWorktreeSetupCalloutPolicy({
|
||||
serverId: "server-1",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { PaseoConfigRaw } from "@getpaseo/protocol/messages";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
import { resolveProjectGroupKey } from "@/projects/project-group-key";
|
||||
import { resolveProjectKey } from "@/projects/project-key";
|
||||
import { resolveHostProjectSettingsRouteKey } from "@/projects/project-settings-target";
|
||||
import { buildProjectSettingsRoute } from "@/utils/host-routes";
|
||||
|
||||
export interface WorktreeSetupWorkspaceInput {
|
||||
projectId: string;
|
||||
projectGroupKey?: string | null;
|
||||
projectKey?: string | null;
|
||||
projectKind: string;
|
||||
projectRootPath: string;
|
||||
}
|
||||
@@ -43,10 +43,10 @@ export function selectActiveGitWorkspaceProject(
|
||||
}
|
||||
|
||||
const projectId = workspace.projectId.trim();
|
||||
const projectKey = resolveProjectGroupKey({
|
||||
const projectKey = resolveProjectKey({
|
||||
serverId,
|
||||
projectId,
|
||||
projectGroupKey: workspace.projectGroupKey,
|
||||
projectKey: workspace.projectKey,
|
||||
});
|
||||
const repoRoot = workspace.projectRootPath.trim();
|
||||
if (!projectId || !repoRoot) {
|
||||
|
||||
@@ -115,14 +115,14 @@ function workspace(input: {
|
||||
name: string;
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
projectGroupKey?: string;
|
||||
projectKey?: string;
|
||||
status?: WorkspaceDescriptor["status"];
|
||||
statusEnteredAt?: Date | null;
|
||||
}): WorkspaceDescriptor {
|
||||
return {
|
||||
id: input.id,
|
||||
projectId: input.projectId,
|
||||
projectGroupKey: input.projectGroupKey,
|
||||
projectKey: input.projectKey,
|
||||
projectDisplayName: input.projectDisplayName,
|
||||
projectRootPath: `/repo/${input.projectId}`,
|
||||
workspaceDirectory: `/repo/${input.projectId}/${input.id}`,
|
||||
@@ -422,7 +422,7 @@ describe("shared sidebar workspace model", () => {
|
||||
id: "clone-a",
|
||||
name: "main",
|
||||
projectId: "prj_a",
|
||||
projectGroupKey: "remote:github.com/acme/app",
|
||||
projectKey: "remote:github.com/acme/app",
|
||||
projectDisplayName: "acme/app",
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
WorkspaceStructureProject,
|
||||
} from "@/projects/workspace-structure";
|
||||
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
|
||||
import { resolveProjectGroupKey } from "@/projects/project-group-key";
|
||||
import { resolveProjectKey } from "@/projects/project-key";
|
||||
import type { WorkspaceAgentActivity } from "@/utils/workspace-agent-activity";
|
||||
import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity";
|
||||
|
||||
@@ -148,10 +148,10 @@ export function createSidebarWorkspaceEntry(input: {
|
||||
}): SidebarWorkspaceEntry {
|
||||
const projectKey =
|
||||
input.projectKey ??
|
||||
resolveProjectGroupKey({
|
||||
resolveProjectKey({
|
||||
serverId: input.serverId,
|
||||
projectId: input.workspace.projectId,
|
||||
projectGroupKey: input.workspace.projectGroupKey,
|
||||
projectKey: input.workspace.projectKey,
|
||||
});
|
||||
const effectiveStatus = deriveEffectiveWorkspaceStatus(input);
|
||||
return {
|
||||
|
||||
@@ -93,7 +93,7 @@ describe("openProjectDirectly", () => {
|
||||
serverId: SERVER_ID,
|
||||
project: {
|
||||
projectId: "project-1",
|
||||
projectGroupKey: null,
|
||||
projectKey: null,
|
||||
projectDisplayName: "project",
|
||||
projectCustomName: null,
|
||||
projectKind: "git",
|
||||
@@ -192,7 +192,7 @@ describe("cloneGithubProjectDirectly", () => {
|
||||
project: {
|
||||
...projectPayload,
|
||||
projectCustomName: null,
|
||||
projectGroupKey: null,
|
||||
projectKey: null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
WorkspaceStructureHostPlacement,
|
||||
WorkspaceStructureProject,
|
||||
} from "@/projects/workspace-structure";
|
||||
import { resolveProjectGroupKey } from "@/projects/project-group-key";
|
||||
import { resolveProjectKey } from "@/projects/project-key";
|
||||
|
||||
export interface HostProjectListItem {
|
||||
projectKey: string;
|
||||
@@ -75,10 +75,10 @@ export function hostProjectFromWorkspace(input: {
|
||||
if (!input.workspace) {
|
||||
return null;
|
||||
}
|
||||
const projectKey = resolveProjectGroupKey({
|
||||
const projectKey = resolveProjectKey({
|
||||
serverId: input.serverId,
|
||||
projectId: input.workspace.projectId.trim(),
|
||||
projectGroupKey: input.workspace.projectGroupKey,
|
||||
projectKey: input.workspace.projectKey,
|
||||
});
|
||||
const iconWorkingDir = input.workspace.projectRootPath.trim();
|
||||
if (!projectKey || !iconWorkingDir) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export function resolveProjectGroupKey(input: {
|
||||
export function resolveProjectKey(input: {
|
||||
serverId: string;
|
||||
projectId: string;
|
||||
projectGroupKey?: string | null;
|
||||
projectKey?: string | null;
|
||||
}): string {
|
||||
if (input.projectGroupKey) return input.projectGroupKey;
|
||||
// COMPAT(projectGroupKey): added in v0.2.4 on 2026-07-28; remove after 2027-01-28.
|
||||
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;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { frameHostProjectKey } from "./project-group-key";
|
||||
import { frameHostProjectKey } from "./project-key";
|
||||
|
||||
interface ProjectSettingsTarget {
|
||||
projectKey: string;
|
||||
|
||||
@@ -11,7 +11,7 @@ function workspace(input: {
|
||||
return {
|
||||
id: input.id,
|
||||
projectId: input.projectId ?? `project-${input.id}`,
|
||||
projectGroupKey: "remote:github.com/acme/app",
|
||||
projectKey: "remote:github.com/acme/app",
|
||||
projectDisplayName: input.projectName,
|
||||
projectCustomName: input.projectCustomName,
|
||||
projectRootPath: `/repo/${input.id}`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EmptyProjectDescriptor, WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
|
||||
import { frameHostProjectKey, resolveProjectGroupKey } from "@/projects/project-group-key";
|
||||
import { frameHostProjectKey, resolveProjectKey } from "@/projects/project-key";
|
||||
|
||||
export interface WorkspaceStructureHostPlacement {
|
||||
serverId: string;
|
||||
@@ -65,23 +65,21 @@ interface MaterializedWorkspaceStructureSession {
|
||||
emptyProjects: EmptyProjectDescriptor[];
|
||||
}
|
||||
|
||||
function findAmbiguousProjectGroupKeys(
|
||||
sessions: MaterializedWorkspaceStructureSession[],
|
||||
): Set<string> {
|
||||
function findAmbiguousProjectKeys(sessions: MaterializedWorkspaceStructureSession[]): Set<string> {
|
||||
const projectIdsByHostByGroupKey = new Map<string, Map<string, Set<string>>>();
|
||||
for (const session of sessions) {
|
||||
const projects = [
|
||||
...session.emptyProjects.map((project) => ({
|
||||
projectId: project.projectId,
|
||||
projectGroupKey: project.projectGroupKey,
|
||||
projectKey: project.projectKey,
|
||||
})),
|
||||
...session.workspaces.map((workspace) => ({
|
||||
projectId: workspace.projectId,
|
||||
projectGroupKey: workspace.projectGroupKey,
|
||||
projectKey: workspace.projectKey,
|
||||
})),
|
||||
];
|
||||
for (const project of projects) {
|
||||
const groupKey = resolveProjectGroupKey({ serverId: session.serverId, ...project });
|
||||
const groupKey = resolveProjectKey({ serverId: session.serverId, ...project });
|
||||
const byHost = projectIdsByHostByGroupKey.get(groupKey) ?? new Map();
|
||||
const projectIds = byHost.get(session.serverId) ?? new Set();
|
||||
projectIds.add(project.projectId);
|
||||
@@ -97,13 +95,13 @@ function findAmbiguousProjectGroupKeys(
|
||||
);
|
||||
}
|
||||
|
||||
function resolveUnambiguousProjectGroupKey(input: {
|
||||
function resolveUnambiguousProjectKey(input: {
|
||||
serverId: string;
|
||||
projectId: string;
|
||||
projectGroupKey?: string | null;
|
||||
projectKey?: string | null;
|
||||
ambiguousGroupKeys: ReadonlySet<string>;
|
||||
}): string {
|
||||
const groupKey = resolveProjectGroupKey(input);
|
||||
const groupKey = resolveProjectKey(input);
|
||||
return input.ambiguousGroupKeys.has(groupKey) ? frameHostProjectKey(input) : groupKey;
|
||||
}
|
||||
|
||||
@@ -115,7 +113,7 @@ export function buildWorkspaceStructureProjects(input: {
|
||||
workspaces: [...session.workspaces],
|
||||
emptyProjects: [...(session.emptyProjects ?? [])],
|
||||
}));
|
||||
const ambiguousGroupKeys = findAmbiguousProjectGroupKeys(sessions);
|
||||
const ambiguousGroupKeys = findAmbiguousProjectKeys(sessions);
|
||||
const byProject = new Map<
|
||||
string,
|
||||
{
|
||||
@@ -131,10 +129,10 @@ export function buildWorkspaceStructureProjects(input: {
|
||||
|
||||
for (const session of sessions) {
|
||||
for (const emptyProject of session.emptyProjects) {
|
||||
const projectKey = resolveUnambiguousProjectGroupKey({
|
||||
const projectKey = resolveUnambiguousProjectKey({
|
||||
serverId: session.serverId,
|
||||
projectId: emptyProject.projectId,
|
||||
projectGroupKey: emptyProject.projectGroupKey,
|
||||
projectKey: emptyProject.projectKey,
|
||||
ambiguousGroupKeys,
|
||||
});
|
||||
const placement = {
|
||||
@@ -169,10 +167,10 @@ export function buildWorkspaceStructureProjects(input: {
|
||||
}
|
||||
|
||||
for (const workspace of session.workspaces) {
|
||||
const projectKey = resolveUnambiguousProjectGroupKey({
|
||||
const projectKey = resolveUnambiguousProjectKey({
|
||||
serverId: session.serverId,
|
||||
projectId: workspace.projectId,
|
||||
projectGroupKey: workspace.projectGroupKey,
|
||||
projectKey: workspace.projectKey,
|
||||
ambiguousGroupKeys,
|
||||
});
|
||||
const existing = byProject.get(projectKey);
|
||||
|
||||
@@ -103,7 +103,7 @@ it("commits the authoritative snapshot before buffered project updates", () => {
|
||||
kind: "upsert",
|
||||
project: {
|
||||
projectId: "attached",
|
||||
projectGroupKey: "remote:github.com/acme/attached",
|
||||
projectKey: "remote:github.com/acme/attached",
|
||||
projectDisplayName: "Renamed attached project",
|
||||
projectCustomName: "Personal name",
|
||||
projectRootPath: "/moved/attached",
|
||||
@@ -125,14 +125,14 @@ it("commits the authoritative snapshot before buffered project updates", () => {
|
||||
|
||||
const session = useSessionStore.getState().sessions[serverId];
|
||||
expect(session?.workspaces.get(attachedMain.id)).toMatchObject({
|
||||
projectGroupKey: "remote:github.com/acme/attached",
|
||||
projectKey: "remote:github.com/acme/attached",
|
||||
projectDisplayName: "Renamed attached project",
|
||||
projectCustomName: "Personal name",
|
||||
projectRootPath: "/moved/attached",
|
||||
projectKind: "directory",
|
||||
});
|
||||
expect(session?.workspaces.get(attachedFeature.id)).toMatchObject({
|
||||
projectGroupKey: "remote:github.com/acme/attached",
|
||||
projectKey: "remote:github.com/acme/attached",
|
||||
projectDisplayName: "Renamed attached project",
|
||||
projectRootPath: "/moved/attached",
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ function applyProjectDelta(
|
||||
hasAttachedWorkspace = true;
|
||||
snapshot.workspaces.set(workspaceId, {
|
||||
...workspace,
|
||||
projectGroupKey: project.projectGroupKey,
|
||||
projectKey: project.projectKey,
|
||||
projectDisplayName: project.projectDisplayName,
|
||||
projectCustomName: project.projectCustomName,
|
||||
projectRootPath: project.projectRootPath,
|
||||
|
||||
@@ -193,7 +193,7 @@ function serializeWorkspace(workspace: WorkspaceDescriptor): WorkspaceDescriptor
|
||||
return {
|
||||
id: workspace.id,
|
||||
projectId: workspace.projectId,
|
||||
...(workspace.projectGroupKey ? { projectGroupKey: workspace.projectGroupKey } : {}),
|
||||
...(workspace.projectKey ? { projectKey: workspace.projectKey } : {}),
|
||||
projectDisplayName: workspace.projectDisplayName,
|
||||
projectCustomName: workspace.projectCustomName ?? null,
|
||||
projectRootPath: workspace.projectRootPath,
|
||||
|
||||
@@ -138,7 +138,7 @@ export interface Agent {
|
||||
export interface WorkspaceDescriptor {
|
||||
id: string;
|
||||
projectId: string;
|
||||
projectGroupKey?: string | null;
|
||||
projectKey?: string | null;
|
||||
projectDisplayName: string;
|
||||
projectCustomName?: string | null;
|
||||
projectRootPath: string;
|
||||
@@ -170,7 +170,7 @@ export function normalizeWorkspaceDescriptor(
|
||||
return {
|
||||
id: normalizeWorkspaceOpaqueId(payload.id) ?? payload.id,
|
||||
projectId: payload.projectId,
|
||||
projectGroupKey: payload.projectGroupKey ?? payload.project?.projectGroupKey ?? null,
|
||||
projectKey: payload.projectKey ?? payload.project?.projectKey ?? null,
|
||||
projectDisplayName: payload.projectDisplayName,
|
||||
projectCustomName: payload.projectCustomName ?? null,
|
||||
projectRootPath: payload.projectRootPath,
|
||||
@@ -197,7 +197,7 @@ export function normalizeWorkspaceDescriptor(
|
||||
|
||||
export interface EmptyProjectDescriptor {
|
||||
projectId: string;
|
||||
projectGroupKey?: string | null;
|
||||
projectKey?: string | null;
|
||||
projectDisplayName: string;
|
||||
projectCustomName: string | null;
|
||||
projectRootPath: string;
|
||||
@@ -209,7 +209,7 @@ export function normalizeEmptyProjectDescriptor(
|
||||
): EmptyProjectDescriptor {
|
||||
return {
|
||||
projectId: payload.projectId,
|
||||
projectGroupKey: payload.projectGroupKey ?? null,
|
||||
projectKey: payload.projectKey ?? null,
|
||||
projectDisplayName: payload.projectDisplayName,
|
||||
projectCustomName: payload.projectCustomName ?? null,
|
||||
projectRootPath: payload.projectRootPath,
|
||||
@@ -257,7 +257,7 @@ function emptyProjectDescriptorFromWorkspace(
|
||||
): EmptyProjectDescriptor {
|
||||
return {
|
||||
projectId: workspace.projectId,
|
||||
...(workspace.projectGroupKey ? { projectGroupKey: workspace.projectGroupKey } : {}),
|
||||
...(workspace.projectKey ? { projectKey: workspace.projectKey } : {}),
|
||||
projectDisplayName: workspace.projectDisplayName,
|
||||
projectCustomName: workspace.projectCustomName ?? null,
|
||||
projectRootPath: workspace.projectRootPath,
|
||||
|
||||
@@ -30,7 +30,7 @@ function workspace(input: {
|
||||
repoRoot: string;
|
||||
project?: ProjectPlacementPayload;
|
||||
projectId?: string;
|
||||
projectGroupKey?: string;
|
||||
projectKey?: string;
|
||||
projectCustomName?: string | null;
|
||||
projectName?: string;
|
||||
remoteUrl?: string | null;
|
||||
@@ -40,7 +40,7 @@ function workspace(input: {
|
||||
return {
|
||||
id: input.id,
|
||||
projectId: input.projectId ?? input.project?.projectKey ?? input.repoRoot,
|
||||
projectGroupKey: input.projectGroupKey,
|
||||
projectKey: input.projectKey,
|
||||
projectDisplayName: input.projectName ?? input.project?.projectName ?? "Project",
|
||||
projectCustomName: input.projectCustomName,
|
||||
projectRootPath: input.repoRoot,
|
||||
@@ -68,25 +68,21 @@ function workspace(input: {
|
||||
}
|
||||
|
||||
describe("buildProjects", () => {
|
||||
it("groups distinct host-local project IDs by their persisted group key", () => {
|
||||
const projectGroupKey = "remote:github.com/acme/app";
|
||||
it("groups distinct host-local project IDs by their persisted project key", () => {
|
||||
const projectKey = "remote:github.com/acme/app";
|
||||
const result = buildProjects({
|
||||
hosts: [
|
||||
{
|
||||
serverId: "desktop",
|
||||
serverName: "Desktop",
|
||||
isOnline: true,
|
||||
workspaces: [
|
||||
workspace({ id: "main-a", repoRoot: "/a", projectId: "prj_a", projectGroupKey }),
|
||||
],
|
||||
workspaces: [workspace({ id: "main-a", repoRoot: "/a", projectId: "prj_a", projectKey })],
|
||||
},
|
||||
{
|
||||
serverId: "laptop",
|
||||
serverName: "Laptop",
|
||||
isOnline: true,
|
||||
workspaces: [
|
||||
workspace({ id: "main-b", repoRoot: "/b", projectId: "prj_b", projectGroupKey }),
|
||||
],
|
||||
workspaces: [workspace({ id: "main-b", repoRoot: "/b", projectId: "prj_b", projectKey })],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -98,8 +94,8 @@ describe("buildProjects", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps independent same-host clones separate when their group key is ambiguous", () => {
|
||||
const projectGroupKey = "remote:github.com/acme/app";
|
||||
it("keeps independent same-host clones separate when their project key is ambiguous", () => {
|
||||
const projectKey = "remote:github.com/acme/app";
|
||||
const result = buildProjects({
|
||||
hosts: [
|
||||
{
|
||||
@@ -107,8 +103,8 @@ describe("buildProjects", () => {
|
||||
serverName: "Desktop",
|
||||
isOnline: true,
|
||||
workspaces: [
|
||||
workspace({ id: "clone-a", repoRoot: "/a", projectId: "prj_a", projectGroupKey }),
|
||||
workspace({ id: "clone-b", repoRoot: "/b", projectId: "prj_b", projectGroupKey }),
|
||||
workspace({ id: "clone-a", repoRoot: "/a", projectId: "prj_a", projectKey }),
|
||||
workspace({ id: "clone-b", repoRoot: "/b", projectId: "prj_b", projectKey }),
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -122,7 +118,7 @@ describe("buildProjects", () => {
|
||||
});
|
||||
|
||||
it("uses one ambiguity scope across every host", () => {
|
||||
const projectGroupKey = "remote:github.com/acme/app";
|
||||
const projectKey = "remote:github.com/acme/app";
|
||||
const result = buildProjects({
|
||||
hosts: [
|
||||
{
|
||||
@@ -130,8 +126,8 @@ describe("buildProjects", () => {
|
||||
serverName: "Desktop",
|
||||
isOnline: true,
|
||||
workspaces: [
|
||||
workspace({ id: "clone-a", repoRoot: "/a", projectId: "prj_a", projectGroupKey }),
|
||||
workspace({ id: "clone-b", repoRoot: "/b", projectId: "prj_b", projectGroupKey }),
|
||||
workspace({ id: "clone-a", repoRoot: "/a", projectId: "prj_a", projectKey }),
|
||||
workspace({ id: "clone-b", repoRoot: "/b", projectId: "prj_b", projectKey }),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -139,21 +135,21 @@ describe("buildProjects", () => {
|
||||
serverName: "Laptop",
|
||||
isOnline: true,
|
||||
workspaces: [
|
||||
workspace({ id: "clone-c", repoRoot: "/c", projectId: "prj_c", projectGroupKey }),
|
||||
workspace({ id: "clone-c", repoRoot: "/c", projectId: "prj_c", projectKey }),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.projects.map((project) => project.projectKey).sort()).toEqual([
|
||||
"host:desktop:project:prj_a",
|
||||
"host:desktop:project:prj_b",
|
||||
"host:laptop:project:prj_c",
|
||||
"host:6:laptop:project:5:prj_c",
|
||||
"host:7:desktop:project:5:prj_a",
|
||||
"host:7:desktop:project:5:prj_b",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps host-local metadata when the grouping key differs from the project ID", () => {
|
||||
const projectGroupKey = "remote:github.com/acme/app";
|
||||
const projectKey = "remote:github.com/acme/app";
|
||||
const result = buildProjects({
|
||||
hosts: [
|
||||
{
|
||||
@@ -165,7 +161,7 @@ describe("buildProjects", () => {
|
||||
id: "main",
|
||||
repoRoot: "/repo/app",
|
||||
projectId: "prj_app",
|
||||
projectGroupKey,
|
||||
projectKey,
|
||||
projectName: "acme/app",
|
||||
projectCustomName: "My App",
|
||||
}),
|
||||
@@ -175,14 +171,14 @@ describe("buildProjects", () => {
|
||||
});
|
||||
|
||||
expect(result.projects[0]).toMatchObject({
|
||||
projectKey: projectGroupKey,
|
||||
projectKey: projectKey,
|
||||
projectName: "acme/app",
|
||||
projectCustomName: "My App",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps custom-name state on the host that owns it", () => {
|
||||
const projectGroupKey = "remote:github.com/acme/app";
|
||||
const projectKey = "remote:github.com/acme/app";
|
||||
const result = buildProjects({
|
||||
hosts: [
|
||||
{
|
||||
@@ -194,7 +190,7 @@ describe("buildProjects", () => {
|
||||
id: "desktop-main",
|
||||
repoRoot: "/desktop/app",
|
||||
projectId: "prj_desktop",
|
||||
projectGroupKey,
|
||||
projectKey,
|
||||
projectName: "My App",
|
||||
projectCustomName: "My App",
|
||||
}),
|
||||
@@ -209,7 +205,7 @@ describe("buildProjects", () => {
|
||||
id: "laptop-main",
|
||||
repoRoot: "/laptop/app",
|
||||
projectId: "prj_laptop",
|
||||
projectGroupKey,
|
||||
projectKey,
|
||||
projectName: "acme/app",
|
||||
projectCustomName: null,
|
||||
}),
|
||||
@@ -235,7 +231,7 @@ describe("buildProjects", () => {
|
||||
emptyProjects: [
|
||||
{
|
||||
projectId: "prj_app",
|
||||
projectGroupKey: "remote:github.com/acme/app",
|
||||
projectKey: "remote:github.com/acme/app",
|
||||
projectDisplayName: "My Empty Project",
|
||||
projectCustomName: "My Empty Project",
|
||||
projectRootPath: "/repo/app",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { EmptyProjectDescriptor, WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { buildHostProjectList, type HostProjectListItem } from "@/projects/host-project-model";
|
||||
import { buildWorkspaceStructureProjects } from "@/projects/workspace-structure";
|
||||
import { resolveProjectGroupKey } from "@/projects/project-group-key";
|
||||
import { resolveProjectKey } from "@/projects/project-key";
|
||||
|
||||
export interface WorkspaceSummary {
|
||||
id: string;
|
||||
@@ -238,10 +238,10 @@ function attachHostWorkspaces(
|
||||
for (const workspace of host.workspaces) {
|
||||
const key =
|
||||
projectKeyByProjectId.get(workspace.projectId) ??
|
||||
resolveProjectGroupKey({
|
||||
resolveProjectKey({
|
||||
serverId: host.serverId,
|
||||
projectId: workspace.projectId,
|
||||
projectGroupKey: workspace.projectGroupKey,
|
||||
projectKey: workspace.projectKey,
|
||||
});
|
||||
groups.get(key)?.hostsByServerId.get(host.serverId)?.workspaces.push(workspace);
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ function createLegacyWorkspace(
|
||||
return {
|
||||
id: workspaceDirectory,
|
||||
projectId: entry.project.projectKey,
|
||||
projectGroupKey: entry.project.projectGroupKey ?? null,
|
||||
projectKey: entry.project.projectKey ?? null,
|
||||
projectDisplayName: entry.project.projectName,
|
||||
projectCustomName: null,
|
||||
projectRootPath,
|
||||
|
||||
Reference in New Issue
Block a user