spaces.create created the space first and only then checked the folder, so a folder that was missing or not a folder returned 400 but left the new space behind. The New project dialog shows that error, so fixing the path and submitting again created a second project with the same name. The folder is now resolved and checked right after the deployment-owner check, before the space exists, and the space stores that resolved path. wireProjectWorkspace takes the resolved folder instead of resolving it again after the space was created. The onboarding scan, its other caller, resolves and checks each repository folder the same way before creating the project, and skips a folder that cannot be linked. The authorization test has the deployment owner submit a missing folder and a file (both 400) and then the real folder under the same name, and checks that exactly one project with that name exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012HG4pFTUBDpuCS2Y44TV8x
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { PrismaClient } from "./client.js";
|
|
import {
|
|
InvalidProjectWorkspacePathError,
|
|
normalizeProjectWorkspacePath,
|
|
resolveProjectWorkspaceDirectory,
|
|
wireProjectWorkspace,
|
|
} from "./project-workspace.js";
|
|
|
|
vi.mock("@rakazo/core/git-checkout", () => ({
|
|
readGitCheckout: vi.fn(async () => ({
|
|
present: true,
|
|
remoteUrl: "https://github.com/example/demo.git",
|
|
defaultBranch: "main",
|
|
headCommit: "abc123",
|
|
dirty: false,
|
|
dirtyPaths: [],
|
|
})),
|
|
}));
|
|
|
|
describe("project workspace wiring", () => {
|
|
const roots: string[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const root of roots.splice(0)) {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function tempDir(prefix: string) {
|
|
const root = mkdtempSync(path.join(tmpdir(), prefix));
|
|
roots.push(root);
|
|
return root;
|
|
}
|
|
|
|
it("normalizes absolute workspace paths", () => {
|
|
const root = tempDir("project-workspace-normalize-");
|
|
expect(normalizeProjectWorkspacePath(root)).toBe(path.resolve(root));
|
|
});
|
|
|
|
it("rejects missing folders", async () => {
|
|
const missing = path.join(tmpdir(), `missing-${Date.now()}`);
|
|
await expect(resolveProjectWorkspaceDirectory(missing)).rejects.toBeInstanceOf(
|
|
InvalidProjectWorkspacePathError,
|
|
);
|
|
});
|
|
|
|
it("resolves a symlinked folder to the real folder behind it", async () => {
|
|
const root = tempDir("project-workspace-symlink-");
|
|
const real = path.join(root, "real");
|
|
mkdirSync(real);
|
|
symlinkSync(real, path.join(root, "link"));
|
|
expect(await resolveProjectWorkspaceDirectory(path.join(root, "link"))).toBe(
|
|
realpathSync(real),
|
|
);
|
|
});
|
|
|
|
it("persists workspace and git metadata on the space", async () => {
|
|
const root = tempDir("project-workspace-wire-");
|
|
mkdirSync(path.join(root, ".git"));
|
|
const updates: Array<Record<string, unknown>> = [];
|
|
const prisma = {
|
|
space: {
|
|
update: vi.fn(async ({ data }: { data: Record<string, unknown> }) => {
|
|
updates.push(data);
|
|
}),
|
|
},
|
|
};
|
|
|
|
const wired = await wireProjectWorkspace(prisma as unknown as Pick<PrismaClient, "space">, {
|
|
spaceId: "space-1",
|
|
workspacePath: await resolveProjectWorkspaceDirectory(root),
|
|
});
|
|
expect(wired.path).toBe(realpathSync(root));
|
|
expect(wired.git).toEqual({
|
|
remoteUrl: "https://github.com/example/demo.git",
|
|
defaultBranch: "main",
|
|
headCommit: "abc123",
|
|
dirty: false,
|
|
dirtyPaths: [],
|
|
});
|
|
expect(updates[0]).toMatchObject({
|
|
workspacePath: realpathSync(root),
|
|
gitRemoteUrl: "https://github.com/example/demo.git",
|
|
gitDefaultBranch: "main",
|
|
gitHeadCommit: "abc123",
|
|
});
|
|
});
|
|
});
|