refactor: enforce P1.1 registry path ownership
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -55,20 +55,22 @@ async function temporaryRemote(): Promise<{ root: string; remote: string; source
|
||||
await git(source, ["init", "--initial-branch=main"]);
|
||||
await git(source, ["config", "user.name", "Workspace Registry Test"]);
|
||||
await git(source, ["config", "user.email", "workspace-registry@example.invalid"]);
|
||||
mkdirSync(join(source, "workspaces"));
|
||||
writeFileSync(join(source, "workspaces", "psd-clinical.yaml"), validYaml);
|
||||
writeFileSync(join(source, "workspaces", "research.yaml"), validYaml
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), "schema_version: 1\nworkspaces: [{id: psd-clinical, name: Policlinico San Donato}, {id: research, name: Policlinico San Donato}]\n");
|
||||
mkdirSync(join(source, "psd-clinical"), { recursive: true });
|
||||
mkdirSync(join(source, "research"), { recursive: true });
|
||||
writeFileSync(join(source, "psd-clinical", "workspace.yaml"), validYaml);
|
||||
writeFileSync(join(source, "research", "workspace.yaml"), validYaml
|
||||
.replace("id: psd-clinical", "id: research"));
|
||||
mkdirSync(join(source, "workspace-content", "research", "evidence"), { recursive: true });
|
||||
mkdirSync(join(source, "workspace-content", "other", "evidence"), { recursive: true });
|
||||
mkdirSync(join(source, "workspace-content", "blob"), { recursive: true });
|
||||
mkdirSync(join(source, "workspace-content", "link"), { recursive: true });
|
||||
writeFileSync(join(source, "workspace-content", "research", "evidence", "guide.md"), "guide v1\n");
|
||||
writeFileSync(join(source, "workspace-content", "other", "evidence", "other.md"), "other\n");
|
||||
writeFileSync(join(source, "workspace-content", "blob", "evidence"), "not a tree\n");
|
||||
symlinkSync("../research/evidence", join(source, "workspace-content", "link", "evidence"));
|
||||
symlinkSync("guide.md", join(source, "workspace-content", "research", "evidence", "nested-link"));
|
||||
await git(source, ["add", "workspaces", "workspace-content"]);
|
||||
mkdirSync(join(source, "research", "evidence"), { recursive: true });
|
||||
mkdirSync(join(source, "other", "evidence"), { recursive: true });
|
||||
mkdirSync(join(source, "blob"), { recursive: true });
|
||||
mkdirSync(join(source, "link"), { recursive: true });
|
||||
writeFileSync(join(source, "research", "evidence", "guide.md"), "guide v1\n");
|
||||
writeFileSync(join(source, "other", "evidence", "other.md"), "other\n");
|
||||
writeFileSync(join(source, "blob", "evidence"), "not a tree\n");
|
||||
symlinkSync("../research/evidence", join(source, "link", "evidence"));
|
||||
symlinkSync("guide.md", join(source, "research", "evidence", "nested-link"));
|
||||
await git(source, ["add", "-A"]);
|
||||
await git(source, ["commit", "-m", "Initial workspace"]);
|
||||
await git(source, ["remote", "add", "origin", remote]);
|
||||
await git(source, ["push", "origin", "main"]);
|
||||
@@ -112,19 +114,19 @@ test("accepts only a tree at the declared Evidence root for the requested revisi
|
||||
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
fixture.initialCommit,
|
||||
"workspace-content/research/evidence",
|
||||
"research/evidence",
|
||||
)).resolves.toBeUndefined();
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
fixture.initialCommit,
|
||||
"workspace-content/missing/evidence",
|
||||
"missing/evidence",
|
||||
)).rejects.toMatchObject({ code: "workspace_invalid" });
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
fixture.initialCommit,
|
||||
"workspace-content/blob/evidence",
|
||||
"blob/evidence",
|
||||
)).rejects.toMatchObject({ code: "workspace_invalid" });
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
fixture.initialCommit,
|
||||
"workspace-content/link/evidence",
|
||||
"link/evidence",
|
||||
)).rejects.toMatchObject({ code: "workspace_invalid" });
|
||||
});
|
||||
|
||||
@@ -136,7 +138,7 @@ test("redacts Git failures while checking an Evidence tree", async () => {
|
||||
|
||||
const error = await repository.assertTreeAtRevision(
|
||||
fixture.initialCommit,
|
||||
"workspace-content/research/evidence",
|
||||
"research/evidence",
|
||||
).catch((failure: unknown) => failure);
|
||||
expect(error).toMatchObject({ code: "git_unavailable", message: "Workspace Git operation failed" });
|
||||
expect((error as Error).message).not.toContain(fixture.root);
|
||||
@@ -150,7 +152,7 @@ test("classifies repository corruption as unavailable rather than invalid Eviden
|
||||
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
fixture.initialCommit,
|
||||
"workspace-content/research/evidence",
|
||||
"research/evidence",
|
||||
)).rejects.toMatchObject({ code: "git_unavailable", message: "Workspace Git operation failed" });
|
||||
});
|
||||
|
||||
@@ -158,28 +160,28 @@ test("binds Evidence tree validation to old and new content-only commits", async
|
||||
const fixture = await temporaryRemote();
|
||||
const repository = new GitWorkspaceRepository(config(join(fixture.root, "registry"), fixture.remote));
|
||||
await repository.bootstrap();
|
||||
writeFileSync(join(fixture.source, "workspace-content", "research", "evidence", "guide.md"), "guide v2\n");
|
||||
await git(fixture.source, ["add", "workspace-content/research/evidence/guide.md"]);
|
||||
writeFileSync(join(fixture.source, "research", "evidence", "guide.md"), "guide v2\n");
|
||||
await git(fixture.source, ["add", "research/evidence/guide.md"]);
|
||||
await git(fixture.source, ["commit", "-m", "Update Evidence content"]);
|
||||
await git(fixture.source, ["push", "origin", "main"]);
|
||||
const { stdout } = await runFile("git", ["rev-parse", "HEAD"], { cwd: fixture.source });
|
||||
const newCommit = stdout.trim();
|
||||
await repository.pull();
|
||||
const oldTree = (await runFile("git", ["rev-parse", `${fixture.initialCommit}:workspace-content/research/evidence`], {
|
||||
const oldTree = (await runFile("git", ["rev-parse", `${fixture.initialCommit}:research/evidence`], {
|
||||
cwd: fixture.source,
|
||||
})).stdout.trim();
|
||||
const newTree = (await runFile("git", ["rev-parse", `${newCommit}:workspace-content/research/evidence`], {
|
||||
const newTree = (await runFile("git", ["rev-parse", `${newCommit}:research/evidence`], {
|
||||
cwd: fixture.source,
|
||||
})).stdout.trim();
|
||||
|
||||
expect(newTree).not.toBe(oldTree);
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
fixture.initialCommit,
|
||||
"workspace-content/research/evidence",
|
||||
"research/evidence",
|
||||
)).resolves.toBeUndefined();
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
newCommit,
|
||||
"workspace-content/research/evidence",
|
||||
"research/evidence",
|
||||
)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -191,7 +193,7 @@ test("defers nested Evidence symlink containment to P6", async () => {
|
||||
// Task 2 validates only the declared root object. Recursive containment remains a P6 boundary.
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
fixture.initialCommit,
|
||||
"workspace-content/research/evidence",
|
||||
"research/evidence",
|
||||
)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -203,11 +205,11 @@ test("rejects malformed revisions and shell-like paths without executing them",
|
||||
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
"HEAD",
|
||||
"workspace-content/research/evidence",
|
||||
"research/evidence",
|
||||
)).rejects.toMatchObject({ code: "workspace_invalid" });
|
||||
await expect(repository.assertTreeAtRevision(
|
||||
fixture.initialCommit,
|
||||
`workspace-content/research/evidence;touch ${marker}`,
|
||||
`research/evidence;touch ${marker}`,
|
||||
)).rejects.toMatchObject({ code: "workspace_invalid" });
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
});
|
||||
@@ -298,3 +300,19 @@ test("parallel contenders recover a stale lock file without overlapping critical
|
||||
expect(results.filter((result) => result.status === "rejected")).toHaveLength(1);
|
||||
expect(maximum).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
test("creates a descriptor only when the exact curator path is absent", async () => {
|
||||
const fixture = await temporaryRemote();
|
||||
const repository = new GitWorkspaceRepository(config(join(fixture.root, "registry"), fixture.remote));
|
||||
await repository.bootstrap();
|
||||
const descriptorPath = "new-workspace/workspace.yaml";
|
||||
const descriptor = "curator descriptor\n";
|
||||
await expect(repository.createRegistryFile(descriptorPath, descriptor)).resolves.toBeUndefined();
|
||||
expect(readFileSync(join(fixture.root, "registry", "repo", descriptorPath), "utf8")).toBe(descriptor);
|
||||
await expect(repository.createRegistryFile(descriptorPath, "overwrite\n"))
|
||||
.rejects.toMatchObject({ code: "workspace_curator_owned" });
|
||||
expect(readFileSync(join(fixture.root, "registry", "repo", descriptorPath), "utf8")).toBe(descriptor);
|
||||
await expect(repository.createRegistryFile("workspace-docs/workspace.yaml", descriptor))
|
||||
.rejects.toMatchObject({ code: "workspace_invalid" });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user