feat: activate workspaces from the root catalog and bootstrap-only publication
This commit is contained in:
@@ -127,6 +127,9 @@ function registryFake(overrides: Partial<RegistryFake> = {}): RegistryFake {
|
||||
branch: "main", head: revision.commit, ahead: 0, behind: 0, degraded: false,
|
||||
})),
|
||||
list: vi.fn(async () => [revision]),
|
||||
listCatalog: vi.fn(async () => [{
|
||||
id: "psd-clinical", name: "Policlinico San Donato", configurationState: "ready", revision,
|
||||
}]),
|
||||
read: vi.fn(async () => ({ workspace, revision })),
|
||||
publish: vi.fn(async () => revision),
|
||||
...overrides,
|
||||
@@ -232,7 +235,7 @@ test("lists compatible workspace summaries and reads a validated workspace", asy
|
||||
|
||||
expect(list.statusCode).toBe(200);
|
||||
expect(list.json()).toEqual([expect.objectContaining({
|
||||
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "Policlinico San Donato",
|
||||
id: "psd-clinical", name: "psd-clinical", file: "psd-clinical/workspace.yaml", displayName: "Policlinico San Donato", configurationState: "ready",
|
||||
})]);
|
||||
expect(detail.statusCode).toBe(200);
|
||||
expect(detail.json()).toMatchObject({ workspace, revision });
|
||||
@@ -455,7 +458,7 @@ function withEvidence(
|
||||
}
|
||||
|
||||
const filesystemEvidenceWorkspace = withEvidence({
|
||||
type: "filesystem", uri: "workspace-content/psd-clinical/evidence",
|
||||
type: "filesystem", uri: "psd-clinical/evidence",
|
||||
});
|
||||
const httpEvidenceWorkspace = withEvidence({
|
||||
type: "http",
|
||||
@@ -480,12 +483,21 @@ async function createRealRouteFixture(
|
||||
await realGit(author, ["init", "--initial-branch=main"]);
|
||||
await realGit(author, ["config", "user.name", "Workspace Route Test"]);
|
||||
await realGit(author, ["config", "user.email", "workspace-route@example.invalid"]);
|
||||
mkdirSync(join(author, "workspaces"));
|
||||
writeFileSync(join(author, "workspaces", "psd-clinical.yaml"), serializeWorkspaceYaml(initialWorkspace));
|
||||
const catalogName = initialWorkspace.workspace.name;
|
||||
const catalogDescription = initialWorkspace.workspace.description;
|
||||
writeFileSync(join(author, "thoth-workspaces.yaml"), [
|
||||
"schema_version: 1",
|
||||
"workspaces:",
|
||||
` - id: psd-clinical\n name: ${catalogName}${catalogDescription ? `\n description: ${catalogDescription}` : ""}`,
|
||||
` - id: research-clinical\n name: Research Clinical${catalogDescription ? `\n description: ${catalogDescription}` : ""}`,
|
||||
` - id: missing-evidence\n name: Missing Evidence${catalogDescription ? `\n description: ${catalogDescription}` : ""}`,
|
||||
].join("\n") + "\n");
|
||||
mkdirSync(join(author, "psd-clinical"), { recursive: true });
|
||||
writeFileSync(join(author, "psd-clinical", "workspace.yaml"), serializeWorkspaceYaml(initialWorkspace));
|
||||
if (initialWorkspace.evidence?.source.type === "filesystem") {
|
||||
mkdirSync(join(author, "workspace-content", "psd-clinical", "evidence"), { recursive: true });
|
||||
mkdirSync(join(author, "psd-clinical", "evidence"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(author, "workspace-content", "psd-clinical", "evidence", "guide.md"),
|
||||
join(author, "psd-clinical", "evidence", "guide.md"),
|
||||
EVIDENCE_FILE_BYTES,
|
||||
);
|
||||
}
|
||||
@@ -545,7 +557,7 @@ afterEach(() => {
|
||||
|
||||
test.each([
|
||||
{
|
||||
source: { type: "filesystem", uri: "workspace-content/psd-clinical/evidence" },
|
||||
source: { type: "filesystem", uri: "psd-clinical/evidence" },
|
||||
expectedVariables: [],
|
||||
},
|
||||
{
|
||||
@@ -582,7 +594,7 @@ test.each([
|
||||
.map(({ name }: { name: string }) => name)).toEqual(expectedVariables);
|
||||
});
|
||||
|
||||
test("real publish create/update, pull, list, and read preserve a complete Evidence descriptor", async () => {
|
||||
test("real bootstrap create, curator push/pull, list, and read preserve a complete Evidence descriptor", async () => {
|
||||
const fixture = await createRealRouteFixture(httpEvidenceWorkspace);
|
||||
const status = await fixture.app.inject({ method: "GET", url: "/workspace-registry/status" });
|
||||
const created = validateWorkspaceDescriptor({
|
||||
@@ -597,57 +609,52 @@ test("real publish create/update, pull, list, and read preserve a complete Evide
|
||||
method: "POST", url: "/workspaces/publish",
|
||||
payload: { action: "create", workspace: created, baseCommit: status.json().head },
|
||||
});
|
||||
expect(create.statusCode).toBe(200);
|
||||
const createdRevision = create.json().revision as WorkspaceRevision;
|
||||
const updated = validateWorkspaceDescriptor({
|
||||
|
||||
await realGit(fixture.author, ["pull", "--ff-only", "origin", "main"]);
|
||||
const remotelyEdited = validateWorkspaceDescriptor({
|
||||
...created,
|
||||
evidence: {
|
||||
...created.evidence,
|
||||
policy: { max_chunk_chars: 8_192, retain_published_generations: 7 },
|
||||
},
|
||||
});
|
||||
|
||||
const update = await fixture.app.inject({
|
||||
method: "POST", url: "/workspaces/publish",
|
||||
payload: {
|
||||
action: "update", workspace: updated,
|
||||
baseCommit: createdRevision.commit, baseBlob: createdRevision.blob,
|
||||
},
|
||||
});
|
||||
expect(update.statusCode).toBe(200);
|
||||
await realGit(fixture.author, ["pull", "--ff-only", "origin", "main"]);
|
||||
const remotelyEdited = validateWorkspaceDescriptor({
|
||||
...updated,
|
||||
evidence: {
|
||||
...updated.evidence,
|
||||
policy: { max_chunk_chars: 9_001, retain_published_generations: 9 },
|
||||
},
|
||||
});
|
||||
writeFileSync(
|
||||
join(fixture.author, "workspaces", "research-clinical.yaml"),
|
||||
join(fixture.author, "research-clinical", "workspace.yaml"),
|
||||
serializeWorkspaceYaml(remotelyEdited),
|
||||
);
|
||||
await realGit(fixture.author, ["add", "workspaces/research-clinical.yaml"]);
|
||||
await realGit(fixture.author, ["add", "research-clinical/workspace.yaml"]);
|
||||
await realGit(fixture.author, ["commit", "-m", "Remote Evidence-only descriptor edit"]);
|
||||
await realGit(fixture.author, ["push", "origin", "main"]);
|
||||
const remoteCommit = await realGit(fixture.author, ["rev-parse", "HEAD"]);
|
||||
|
||||
const update = await fixture.app.inject({
|
||||
method: "POST", url: "/workspaces/publish",
|
||||
payload: {
|
||||
action: "update", workspace: remotelyEdited,
|
||||
baseCommit: createdRevision.commit, baseBlob: createdRevision.blob,
|
||||
},
|
||||
});
|
||||
expect(update.statusCode).toBe(409);
|
||||
expect(update.json()).toMatchObject({ code: "workspace_curator_owned" });
|
||||
|
||||
const pull = await fixture.app.inject({ method: "POST", url: "/workspace-registry/pull" });
|
||||
const list = await fixture.app.inject({ method: "GET", url: "/workspaces" });
|
||||
const read = await fixture.app.inject({ method: "GET", url: "/workspaces/research-clinical" });
|
||||
|
||||
expect(status.statusCode).toBe(200);
|
||||
expect(create.statusCode).toBe(200);
|
||||
expect(update.statusCode).toBe(200);
|
||||
expect(pull.statusCode).toBe(200);
|
||||
expect(pull.json().head).toBe(remoteCommit);
|
||||
expect(list.statusCode).toBe(200);
|
||||
expect(list.json().find(({ id }: { id: string }) => id === "research-clinical").workspace)
|
||||
.toEqual(remotelyEdited);
|
||||
const summary = list.json().find(({ id }: { id: string }) => id === "research-clinical");
|
||||
expect(summary.configurationState).toBe("ready");
|
||||
expect(summary.revision.commit).toBe(remoteCommit);
|
||||
expect(read.statusCode).toBe(200);
|
||||
expect(read.json().workspace).toEqual(remotelyEdited);
|
||||
});
|
||||
|
||||
test("real route reports a safe field for an Evidence-only concurrent edit", async () => {
|
||||
test("real route refuses curator-owned updates with a safe 409 after an Evidence-only concurrent edit", async () => {
|
||||
const fixture = await createRealRouteFixture(httpEvidenceWorkspace);
|
||||
await fixture.registry.bootstrap();
|
||||
const base = await fixture.registry.read("psd-clinical");
|
||||
@@ -655,8 +662,8 @@ test("real route reports a safe field for an Evidence-only concurrent edit", asy
|
||||
{ ...httpEvidenceWorkspace.evidence!.source },
|
||||
{ max_chunk_chars: 9_000, retain_published_generations: 3 },
|
||||
);
|
||||
writeFileSync(join(fixture.author, "workspaces", "psd-clinical.yaml"), serializeWorkspaceYaml(remote));
|
||||
await realGit(fixture.author, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
writeFileSync(join(fixture.author, "psd-clinical", "workspace.yaml"), serializeWorkspaceYaml(remote));
|
||||
await realGit(fixture.author, ["add", "psd-clinical/workspace.yaml"]);
|
||||
await realGit(fixture.author, ["commit", "-m", "Change Evidence policy only"]);
|
||||
await realGit(fixture.author, ["push", "origin", "main"]);
|
||||
const local = withEvidence(
|
||||
@@ -673,16 +680,13 @@ test("real route reports a safe field for an Evidence-only concurrent edit", asy
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(409);
|
||||
expect(response.json()).toMatchObject({
|
||||
code: "workspace_conflict", fields: ["evidence.policy.max_chunk_chars"],
|
||||
});
|
||||
expect(response.json()).toMatchObject({ code: "workspace_curator_owned" });
|
||||
expect(response.body).not.toContain(SECRET_CANARY);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["absolute", "/tmp/CANARY-EVIDENCE-ROUTE-SECRET-DO-NOT-LEAK"],
|
||||
["traversal", "workspace-content/psd-clinical/../CANARY-EVIDENCE-ROUTE-SECRET-DO-NOT-LEAK"],
|
||||
["cross-workspace", "workspace-content/research/evidence"],
|
||||
["traversal", "psd-clinical/../CANARY-EVIDENCE-ROUTE-SECRET-DO-NOT-LEAK"],
|
||||
["cross-workspace", "research/evidence"],
|
||||
])("real publish rejects %s filesystem Evidence paths without changing HEAD", async (_label, uri) => {
|
||||
const fixture = await createRealRouteFixture();
|
||||
await fixture.registry.bootstrap();
|
||||
@@ -747,7 +751,7 @@ test("real publish and pull fail safely when the contextual Evidence Git tree is
|
||||
...workspace.semantic_index,
|
||||
vector_store: { ...workspace.semantic_index.vector_store, collection: "missing-evidence" },
|
||||
},
|
||||
evidence: { source: { type: "filesystem", uri: "workspace-content/missing-evidence/evidence" } },
|
||||
evidence: { source: { type: "filesystem", uri: "missing-evidence/evidence" } },
|
||||
});
|
||||
const publish = await fixture.app.inject({
|
||||
method: "POST", url: "/workspaces/publish",
|
||||
@@ -755,10 +759,11 @@ test("real publish and pull fail safely when the contextual Evidence Git tree is
|
||||
});
|
||||
expect(publish.statusCode).toBe(400);
|
||||
expect(publish.json()).toEqual({ code: "workspace_invalid", message: "Workspace request or bundle is invalid." });
|
||||
expect(publish.json()).toEqual({ code: "workspace_invalid", message: "Workspace request or bundle is invalid." });
|
||||
expect(await realGit(fixture.author, ["--git-dir", fixture.remote, "rev-parse", "HEAD"]))
|
||||
.toBe(fixture.initialCommit);
|
||||
|
||||
rmSync(join(fixture.author, "workspace-content", "psd-clinical", "evidence"), { recursive: true });
|
||||
rmSync(join(fixture.author, "psd-clinical", "evidence"), { recursive: true });
|
||||
await realGit(fixture.author, ["add", "-A"]);
|
||||
await realGit(fixture.author, ["commit", "-m", "Remove Evidence tree"]);
|
||||
await realGit(fixture.author, ["push", "origin", "main"]);
|
||||
|
||||
@@ -42,7 +42,7 @@ function withFilesystemEvidence(source: string, id = "psd-clinical"): string {
|
||||
return source.concat(`evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
uri: workspace-content/${id}/evidence
|
||||
uri: ${id}/evidence
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -177,6 +177,14 @@ async function gitOutput(cwd: string, args: string[]): Promise<string> {
|
||||
return stdout.trim();
|
||||
}
|
||||
|
||||
function catalogYaml(entries: Array<{ id: string; name: string; description?: string }>): string {
|
||||
return `schema_version: 1
|
||||
workspaces:
|
||||
${entries.map((entry) => ` - id: ${entry.id}
|
||||
name: ${entry.name}${entry.description ? `\n description: ${entry.description}` : ""}`).join("\n")}
|
||||
`;
|
||||
}
|
||||
|
||||
async function fixture(workspaceSource = validYaml): Promise<{
|
||||
root: string; remote: string; source: string; initialCommit: string;
|
||||
}> {
|
||||
@@ -189,16 +197,22 @@ async function fixture(workspaceSource = validYaml): Promise<{
|
||||
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"), workspaceSource);
|
||||
const workspace = workspaceSource.includes("schema_version: 3")
|
||||
? parseWorkspaceYaml(workspaceSource) : undefined;
|
||||
mkdirSync(join(source, "psd-clinical"), { recursive: true });
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), catalogYaml([
|
||||
{ id: "psd-clinical", name: workspace?.workspace.name ?? "Policlinico San Donato", ...(workspace?.workspace.description ? { description: workspace.workspace.description } : {}) },
|
||||
{ id: "research", name: "research" },
|
||||
]));
|
||||
writeFileSync(join(source, "psd-clinical", "workspace.yaml"), workspaceSource);
|
||||
if (workspaceSource.includes("type: filesystem")) {
|
||||
mkdirSync(join(source, "workspace-content", "psd-clinical", "evidence"), { recursive: true });
|
||||
mkdirSync(join(source, "workspace-content", "research", "evidence"), { recursive: true });
|
||||
writeFileSync(join(source, "workspace-content", "psd-clinical", "evidence", "guide.md"), "guide v1\n");
|
||||
writeFileSync(join(source, "workspace-content", "research", "evidence", "guide.md"), "research guide\n");
|
||||
await git(source, ["add", "workspaces", "workspace-content"]);
|
||||
mkdirSync(join(source, "psd-clinical", "evidence"), { recursive: true });
|
||||
mkdirSync(join(source, "research", "evidence"), { recursive: true });
|
||||
writeFileSync(join(source, "psd-clinical", "evidence", "guide.md"), "guide v1\n");
|
||||
writeFileSync(join(source, "research", "evidence", "guide.md"), "research guide\n");
|
||||
await git(source, ["add", "-A"]);
|
||||
} else {
|
||||
await git(source, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
await git(source, ["add", "thoth-workspaces.yaml", "psd-clinical/workspace.yaml"]);
|
||||
}
|
||||
await git(source, ["commit", "-m", "Initial workspace"]);
|
||||
await git(source, ["remote", "add", "origin", remote]);
|
||||
@@ -219,10 +233,11 @@ async function contentOnlyFixture(): Promise<{
|
||||
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"]);
|
||||
const evidence = join(source, "workspace-content", "p1-filesystem", "evidence");
|
||||
const evidence = join(source, "p1-filesystem", "evidence");
|
||||
mkdirSync(evidence, { recursive: true });
|
||||
writeFileSync(join(evidence, "guide.md"), "curated content\n");
|
||||
await git(source, ["add", "workspace-content"]);
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), catalogYaml([{ id: "p1-filesystem", name: "p1-filesystem" }]));
|
||||
await git(source, ["add", "-A"]);
|
||||
await git(source, ["commit", "-m", "Bootstrap curated content"]);
|
||||
await git(source, ["remote", "add", "origin", remote]);
|
||||
await git(source, ["push", "origin", "main"]);
|
||||
@@ -242,11 +257,16 @@ async function multiWorkspaceFixture(workspaces: Record<string, string>): Promis
|
||||
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"));
|
||||
const entries = [];
|
||||
for (const [id, workspaceSource] of Object.entries(workspaces)) {
|
||||
writeFileSync(join(source, "workspaces", `${id}.yaml`), workspaceSource);
|
||||
const workspace = workspaceSource.includes("schema_version: 3") ? parseWorkspaceYaml(workspaceSource) : undefined;
|
||||
entries.push({ id, name: workspace.workspace.name, ...(workspace.workspace.description ? { description: workspace.workspace.description } : {}) });
|
||||
mkdirSync(join(source, id), { recursive: true });
|
||||
writeFileSync(join(source, id, "workspace.yaml"), workspaceSource);
|
||||
if (workspaceSource.includes("type: filesystem")) mkdirSync(join(source, id, "evidence"), { recursive: true });
|
||||
}
|
||||
await git(source, ["add", "workspaces"]);
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), catalogYaml(entries));
|
||||
await git(source, ["add", "-A"]);
|
||||
await git(source, ["commit", "-m", "Initial workspaces"]);
|
||||
await git(source, ["remote", "add", "origin", remote]);
|
||||
await git(source, ["push", "origin", "main"]);
|
||||
@@ -306,8 +326,8 @@ async function checkoutStatus(checkout: string): Promise<{ porcelain: string; di
|
||||
}
|
||||
|
||||
async function pushInvalidWorkspace(source: string): Promise<void> {
|
||||
writeFileSync(join(source, "workspaces", "psd-clinical.yaml"), "workspace: invalid\n");
|
||||
await git(source, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
writeFileSync(join(source, "psd-clinical", "workspace.yaml"), "workspace: invalid\n");
|
||||
await git(source, ["add", "psd-clinical/workspace.yaml"]);
|
||||
await git(source, ["commit", "-m", "Invalid workspace"]);
|
||||
await git(source, ["push", "origin", "main"]);
|
||||
}
|
||||
@@ -353,12 +373,15 @@ function persistedState(root: string, commit: string): { active: any; manifest:
|
||||
};
|
||||
}
|
||||
|
||||
test("allows first API publication and delete-last from a content-only registry base", async () => {
|
||||
test("allows bootstrap creation from a catalog-only base and refuses later curator-owned writes", async () => {
|
||||
const remote = await contentOnlyFixture();
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
|
||||
|
||||
await expect(registry.bootstrap()).resolves.toMatchObject({ head: remote.initialCommit });
|
||||
await expect(registry.list()).resolves.toEqual([]);
|
||||
await expect(registry.listCatalog()).resolves.toEqual([
|
||||
expect.objectContaining({ id: "p1-filesystem", configurationState: "configuration_required" }),
|
||||
]);
|
||||
|
||||
const created = await registry.publish({
|
||||
action: "create",
|
||||
@@ -366,16 +389,23 @@ test("allows first API publication and delete-last from a content-only registry
|
||||
baseCommit: remote.initialCommit,
|
||||
});
|
||||
expect(created).toMatchObject({ id: "p1-filesystem" });
|
||||
await expect(registry.list()).resolves.toEqual([
|
||||
expect.objectContaining({ id: "p1-filesystem", commit: created!.commit }),
|
||||
]);
|
||||
await expect(registry.listCatalog()).resolves.toEqual([
|
||||
expect.objectContaining({ id: "p1-filesystem", configurationState: "ready", revision: created }),
|
||||
]);
|
||||
|
||||
await expect(registry.publish({
|
||||
action: "delete",
|
||||
id: "p1-filesystem",
|
||||
baseCommit: created!.commit,
|
||||
baseBlob: created!.blob,
|
||||
})).resolves.toBeUndefined();
|
||||
await expect(registry.list()).resolves.toEqual([]);
|
||||
})).rejects.toMatchObject({ code: "workspace_curator_owned" });
|
||||
await expect(registry.list()).resolves.toEqual([
|
||||
expect.objectContaining({ id: "p1-filesystem" }),
|
||||
]);
|
||||
});
|
||||
|
||||
test("bootstraps a checkout and activates a validated immutable snapshot", async () => {
|
||||
const remote = await fixture();
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
|
||||
@@ -411,7 +441,7 @@ test("publishes a filesystem descriptor only when its Evidence tree exists in th
|
||||
const root = join(remote.root, "registry");
|
||||
const registry = new WorkspaceRegistry(config(root, remote.remote));
|
||||
await registry.bootstrap();
|
||||
const evidencePath = "workspace-content/research/evidence";
|
||||
const evidencePath = "research/evidence";
|
||||
const initialTree = await gitOutput(remote.root, [
|
||||
"--git-dir", remote.remote, "rev-parse", `${remote.initialCommit}:${evidencePath}`,
|
||||
]);
|
||||
@@ -424,7 +454,7 @@ test("publishes a filesystem descriptor only when its Evidence tree exists in th
|
||||
|
||||
expect(created?.commit).not.toBe(remote.initialCommit);
|
||||
await expect(runFile("git", [
|
||||
"--git-dir", remote.remote, "cat-file", "-e", `${created!.commit}:workspaces/research.yaml`,
|
||||
"--git-dir", remote.remote, "cat-file", "-e", `${created!.commit}:research/workspace.yaml`,
|
||||
], { cwd: remote.root })).resolves.toBeDefined();
|
||||
await expect(runFile("git", [
|
||||
"--git-dir", remote.remote, "cat-file", "-e", `${created!.commit}:${evidencePath}/guide.md`,
|
||||
@@ -451,10 +481,10 @@ test.each(["missing", "blob"])(
|
||||
const remote = await fixture(withFilesystemEvidence(validYaml));
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
|
||||
await registry.bootstrap();
|
||||
const evidenceRoot = join(remote.source, "workspace-content", "psd-clinical", "evidence");
|
||||
const evidenceRoot = join(remote.source, "psd-clinical", "evidence");
|
||||
rmSync(evidenceRoot, { recursive: true, force: true });
|
||||
if (invalidKind === "blob") writeFileSync(evidenceRoot, "not a tree\n");
|
||||
await git(remote.source, ["add", "-A", "workspace-content/psd-clinical/evidence"]);
|
||||
await git(remote.source, ["add", "-A", "psd-clinical/evidence"]);
|
||||
await git(remote.source, ["commit", "-m", `Make Evidence root ${invalidKind}`]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
const invalidCommit = await gitOutput(remote.source, ["rev-parse", "HEAD"]);
|
||||
@@ -471,10 +501,10 @@ test("activation validates filesystem Evidence against its exact safeHead rather
|
||||
const remote = await fixture(withFilesystemEvidence(validYaml));
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
|
||||
await registry.bootstrap();
|
||||
rmSync(join(remote.source, "workspace-content", "psd-clinical", "evidence"), {
|
||||
rmSync(join(remote.source, "psd-clinical", "evidence"), {
|
||||
recursive: true, force: true,
|
||||
});
|
||||
await git(remote.source, ["add", "-A", "workspace-content/psd-clinical/evidence"]);
|
||||
await git(remote.source, ["add", "-A", "psd-clinical/evidence"]);
|
||||
await git(remote.source, ["commit", "-m", "Remove current Evidence root"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
const invalidHead = await gitOutput(remote.source, ["rev-parse", "HEAD"]);
|
||||
@@ -496,7 +526,7 @@ test("creates an immutable descriptor revision for a content-only Evidence commi
|
||||
const registry = new WorkspaceRegistry(config(root, remote.remote));
|
||||
await registry.bootstrap();
|
||||
const initial = await registry.read("psd-clinical");
|
||||
const evidencePath = "workspace-content/psd-clinical/evidence";
|
||||
const evidencePath = "psd-clinical/evidence";
|
||||
const initialTree = await gitOutput(remote.source, ["rev-parse", `${remote.initialCommit}:${evidencePath}`]);
|
||||
writeFileSync(join(remote.source, evidencePath, "guide.md"), "guide v2\n");
|
||||
await git(remote.source, ["add", `${evidencePath}/guide.md`]);
|
||||
@@ -524,9 +554,9 @@ test("rejects a stale API update after a content-only Evidence commit", async ()
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
|
||||
await registry.bootstrap();
|
||||
const initial = await registry.read("psd-clinical");
|
||||
const guide = join(remote.source, "workspace-content", "psd-clinical", "evidence", "guide.md");
|
||||
const guide = join(remote.source, "psd-clinical", "evidence", "guide.md");
|
||||
writeFileSync(guide, "curator content\n");
|
||||
await git(remote.source, ["add", "workspace-content/psd-clinical/evidence/guide.md"]);
|
||||
await git(remote.source, ["add", "psd-clinical/evidence/guide.md"]);
|
||||
await git(remote.source, ["commit", "-m", "Curator Evidence update"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
const curatorCommit = await gitOutput(remote.source, ["rev-parse", "HEAD"]);
|
||||
@@ -536,7 +566,7 @@ test("rejects a stale API update after a content-only Evidence commit", async ()
|
||||
workspace: filesystemWorkspace("psd-clinical"),
|
||||
baseCommit: initial.revision.commit,
|
||||
baseBlob: initial.revision.blob,
|
||||
})).rejects.toMatchObject({ code: "workspace_stale" });
|
||||
})).rejects.toMatchObject({ code: "workspace_curator_owned" });
|
||||
expect(await gitOutput(remote.root, ["--git-dir", remote.remote, "rev-parse", "HEAD"])).toBe(curatorCommit);
|
||||
});
|
||||
|
||||
@@ -545,10 +575,10 @@ test("keeps content-only historical descriptor revisions distinguishable by comm
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
|
||||
await registry.bootstrap();
|
||||
writeFileSync(
|
||||
join(remote.source, "workspace-content", "psd-clinical", "evidence", "guide.md"),
|
||||
join(remote.source, "psd-clinical", "evidence", "guide.md"),
|
||||
"historical content\n",
|
||||
);
|
||||
await git(remote.source, ["add", "workspace-content/psd-clinical/evidence/guide.md"]);
|
||||
await git(remote.source, ["add", "psd-clinical/evidence/guide.md"]);
|
||||
await git(remote.source, ["commit", "-m", "Retained Evidence update"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
const contentCommit = await gitOutput(remote.source, ["rev-parse", "HEAD"]);
|
||||
@@ -563,14 +593,14 @@ test("keeps content-only historical descriptor revisions distinguishable by comm
|
||||
expect(oldPinned.workspace).toEqual(newPinned.workspace);
|
||||
});
|
||||
|
||||
test("publishes create, update, and delete with the configured Git author identity", async () => {
|
||||
test("publishes one bootstrap descriptor with the configured Git author identity and refuses curator-owned mutation", async () => {
|
||||
const remote = await fixture();
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote, {
|
||||
gitAuthorName: "Configured Workspace Publisher",
|
||||
gitAuthorEmail: "publisher@example.invalid",
|
||||
}));
|
||||
await registry.bootstrap();
|
||||
const createdWorkspace = workspaceWith("research-registry", { name: "Research registry" });
|
||||
const createdWorkspace = workspaceWith("research");
|
||||
|
||||
const created = await registry.publish({
|
||||
action: "create",
|
||||
@@ -578,89 +608,80 @@ test("publishes create, update, and delete with the configured Git author identi
|
||||
baseCommit: remote.initialCommit,
|
||||
});
|
||||
|
||||
expect(created).toMatchObject({ id: "research-registry", commit: expect.stringMatching(/^[0-9a-f]{40}$/) });
|
||||
expect(created).toMatchObject({ id: "research", commit: expect.stringMatching(/^[0-9a-f]{40}$/) });
|
||||
expect(await gitOutput(remote.root, ["--git-dir", remote.remote, "log", "-1", "--format=%an <%ae>"])).toBe(
|
||||
"Configured Workspace Publisher <publisher@example.invalid>",
|
||||
);
|
||||
await expect(runFile("git", ["--git-dir", remote.remote, "cat-file", "-e", "HEAD:workspace-docs/research-registry/README.md"], {
|
||||
await expect(runFile("git", ["--git-dir", remote.remote, "cat-file", "-e", "HEAD:workspace-docs/research/README.md"], {
|
||||
cwd: remote.root,
|
||||
})).resolves.toBeDefined();
|
||||
|
||||
const updated = await registry.publish({
|
||||
const before = await gitOutput(remote.root, ["--git-dir", remote.remote, "rev-parse", "HEAD"]);
|
||||
await expect(registry.publish({
|
||||
action: "update",
|
||||
workspace: workspaceWith("research-registry", { description: "Updated workspace description" }),
|
||||
workspace: workspaceWith("research", { name: "Research", description: "Updated workspace description" }),
|
||||
baseCommit: created!.commit,
|
||||
baseBlob: created!.blob,
|
||||
});
|
||||
|
||||
expect(updated).toMatchObject({ id: "research-registry" });
|
||||
expect(await gitOutput(remote.root, ["--git-dir", remote.remote, "show", "HEAD:workspaces/research-registry.yaml"])).toContain(
|
||||
"description: Updated workspace description",
|
||||
);
|
||||
|
||||
})).rejects.toMatchObject({ code: "workspace_curator_owned" });
|
||||
await expect(registry.publish({
|
||||
action: "delete",
|
||||
id: "research-registry",
|
||||
baseCommit: updated!.commit,
|
||||
baseBlob: updated!.blob,
|
||||
})).resolves.toBeUndefined();
|
||||
await expect(runFile("git", ["--git-dir", remote.remote, "cat-file", "-e", "HEAD:workspaces/research-registry.yaml"], {
|
||||
id: "research",
|
||||
baseCommit: created!.commit,
|
||||
baseBlob: created!.blob,
|
||||
})).rejects.toMatchObject({ code: "workspace_curator_owned" });
|
||||
expect(await gitOutput(remote.root, ["--git-dir", remote.remote, "rev-parse", "HEAD"])).toBe(before);
|
||||
await expect(runFile("git", ["--git-dir", remote.remote, "cat-file", "-e", "HEAD:research/workspace.yaml"], {
|
||||
cwd: remote.root,
|
||||
})).rejects.toBeDefined();
|
||||
})).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test("reports stale publish conflicts with expected and actual revisions", async () => {
|
||||
test("rejects curator-owned update after a curator push and leaves the active snapshot intact", async () => {
|
||||
const remote = await fixture();
|
||||
const root = join(remote.root, "registry");
|
||||
const registry = new WorkspaceRegistry(config(root, remote.remote));
|
||||
await registry.bootstrap();
|
||||
const initial = await registry.read("psd-clinical");
|
||||
writeFileSync(join(remote.source, "workspaces", "psd-clinical.yaml"), validYaml.replace(
|
||||
writeFileSync(join(remote.source, "psd-clinical", "workspace.yaml"), validYaml.replace(
|
||||
"schema: datawarehouse", "schema: analytics",
|
||||
));
|
||||
await git(remote.source, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
await git(remote.source, ["add", "psd-clinical/workspace.yaml"]);
|
||||
await git(remote.source, ["commit", "-m", "Change dwh schema"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
const actualCommit = await gitOutput(remote.source, ["rev-parse", "HEAD"]);
|
||||
const actualBlob = await gitOutput(remote.source, ["rev-parse", "HEAD:workspaces/psd-clinical.yaml"]);
|
||||
|
||||
await registry.pull();
|
||||
await expect(registry.publish({
|
||||
action: "update",
|
||||
workspace: workspaceWith("psd-clinical", { description: "Local stale change" }),
|
||||
baseCommit: initial.revision.commit,
|
||||
baseBlob: initial.revision.blob,
|
||||
})).rejects.toMatchObject({
|
||||
code: "workspace_conflict",
|
||||
fields: ["dwh.schema"],
|
||||
expected: { commit: initial.revision.commit, blob: initial.revision.blob },
|
||||
actual: { commit: actualCommit, blob: actualBlob },
|
||||
})).rejects.toMatchObject({ code: "workspace_curator_owned" });
|
||||
await expect(registry.read("psd-clinical")).resolves.toMatchObject({
|
||||
revision: { commit: actualCommit },
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
["adds", validYaml, withDwhRestDiagnostic(validYaml)],
|
||||
["removes", withDwhRestDiagnostic(validYaml), validYaml],
|
||||
])("reports an optional diagnostics branch when the registry %s it", async (_operation, baseSource, remoteSource) => {
|
||||
])("pulls a curator change that %s a diagnostics branch without API rewrite", async (_operation, baseSource, remoteSource) => {
|
||||
const remote = await fixture(baseSource);
|
||||
const registry = new WorkspaceRegistry(config(join(remote.root, "registry"), remote.remote));
|
||||
await registry.bootstrap();
|
||||
const initial = await registry.read("psd-clinical");
|
||||
writeFileSync(join(remote.source, "workspaces", "psd-clinical.yaml"), remoteSource);
|
||||
await git(remote.source, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
writeFileSync(join(remote.source, "psd-clinical", "workspace.yaml"), remoteSource);
|
||||
await git(remote.source, ["add", "psd-clinical/workspace.yaml"]);
|
||||
await git(remote.source, ["commit", "-m", `Registry ${_operation} diagnostic branch`]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
|
||||
await registry.pull();
|
||||
const updated = await registry.read("psd-clinical");
|
||||
expect(updated.revision.commit).not.toBe(initial.revision.commit);
|
||||
await expect(registry.publish({
|
||||
action: "update",
|
||||
workspace: workspaceWith("psd-clinical", { description: "Local stale change" }),
|
||||
baseCommit: initial.revision.commit,
|
||||
baseBlob: initial.revision.blob,
|
||||
})).rejects.toMatchObject({
|
||||
code: "workspace_conflict",
|
||||
fields: ["dwh.supported_transports", "diagnostics"],
|
||||
});
|
||||
})).rejects.toMatchObject({ code: "workspace_curator_owned" });
|
||||
});
|
||||
|
||||
test("restores a clean checkout after a failed commit and retries publication", async () => {
|
||||
const remote = await fixture();
|
||||
const root = join(remote.root, "registry");
|
||||
@@ -670,7 +691,7 @@ test("restores a clean checkout after a failed commit and retries publication",
|
||||
chmodSync(objects, 0o500);
|
||||
const request = {
|
||||
action: "create" as const,
|
||||
workspace: workspaceWith("commit-recovery"),
|
||||
workspace: workspaceWith("research"),
|
||||
baseCommit: remote.initialCommit,
|
||||
};
|
||||
|
||||
@@ -681,7 +702,7 @@ test("restores a clean checkout after a failed commit and retries publication",
|
||||
}
|
||||
expect(await checkoutStatus(join(root, "repo"))).toEqual({ porcelain: "", divergence: "0\t0" });
|
||||
await expect(registry.pull()).resolves.toMatchObject({ head: remote.initialCommit });
|
||||
await expect(registry.publish(request)).resolves.toMatchObject({ id: "commit-recovery" });
|
||||
await expect(registry.publish(request)).resolves.toMatchObject({ id: "research" });
|
||||
});
|
||||
|
||||
test("resets an ahead checkout after a rejected push and retries publication", async () => {
|
||||
@@ -693,7 +714,7 @@ test("resets an ahead checkout after a rejected push and retries publication", a
|
||||
writeFileSync(hook, "#!/bin/sh\nexit 1\n", { mode: 0o755 });
|
||||
const request = {
|
||||
action: "create" as const,
|
||||
workspace: workspaceWith("push-recovery"),
|
||||
workspace: workspaceWith("research"),
|
||||
baseCommit: remote.initialCommit,
|
||||
};
|
||||
|
||||
@@ -701,7 +722,7 @@ test("resets an ahead checkout after a rejected push and retries publication", a
|
||||
expect(await checkoutStatus(join(root, "repo"))).toEqual({ porcelain: "", divergence: "0\t0" });
|
||||
rmSync(hook);
|
||||
await expect(registry.pull()).resolves.toMatchObject({ head: remote.initialCommit });
|
||||
await expect(registry.publish(request)).resolves.toMatchObject({ id: "push-recovery" });
|
||||
await expect(registry.publish(request)).resolves.toMatchObject({ id: "research" });
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -722,8 +743,8 @@ test("writes only state-free revisions and never exposes revision state", async
|
||||
await registry.bootstrap();
|
||||
|
||||
const initial = persistedState(root, remote.initialCommit);
|
||||
expect(Object.keys(initial.active).sort()).toEqual(["head", "revisions"]);
|
||||
expect(Object.keys(initial.manifest).sort()).toEqual(["files", "head", "revisions"]);
|
||||
expect(Object.keys(initial.active).sort()).toEqual(["catalog", "head", "revisions"]);
|
||||
expect(Object.keys(initial.manifest).sort()).toEqual(["catalog", "files", "head", "revisions"]);
|
||||
expect(Object.keys(initial.active.revisions[0]).sort()).toEqual(["blob", "commit", "id", "snapshotPath"]);
|
||||
expect(Object.keys(initial.manifest.revisions[0]).sort()).toEqual(["blob", "commit", "id", "snapshotPath"]);
|
||||
|
||||
@@ -733,10 +754,9 @@ test("writes only state-free revisions and never exposes revision state", async
|
||||
expect(read.revision).not.toHaveProperty("state");
|
||||
|
||||
const published = await registry.publish({
|
||||
action: "update",
|
||||
workspace: workspaceWith("psd-clinical", { name: "State-free revision" }),
|
||||
action: "create",
|
||||
workspace: workspaceWith("research"),
|
||||
baseCommit: remote.initialCommit,
|
||||
baseBlob: listed[0]!.blob,
|
||||
});
|
||||
const updated = persistedState(root, published!.commit);
|
||||
expect(updated.active.revisions[0]).not.toHaveProperty("state");
|
||||
@@ -827,10 +847,13 @@ test("normalizes operational state in retained historical snapshots without rewr
|
||||
const root = join(remote.root, "registry");
|
||||
const registry = new WorkspaceRegistry(config(root, remote.remote));
|
||||
await registry.bootstrap();
|
||||
writeFileSync(join(remote.source, "workspaces", "psd-clinical.yaml"), validYaml.replace(
|
||||
writeFileSync(join(remote.source, "psd-clinical", "workspace.yaml"), validYaml.replace(
|
||||
"name: Policlinico San Donato", "name: Current workspace",
|
||||
));
|
||||
await git(remote.source, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
writeFileSync(join(remote.source, "thoth-workspaces.yaml"), catalogYaml([
|
||||
{ id: "psd-clinical", name: "Current workspace" }, { id: "research", name: "Research" },
|
||||
]));
|
||||
await git(remote.source, ["add", "-A"]);
|
||||
await git(remote.source, ["commit", "-m", "Update active workspace"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
await registry.pull();
|
||||
@@ -907,17 +930,17 @@ test("rejects duplicate schema v3 collection ownership and keeps the previous ac
|
||||
await registry.bootstrap();
|
||||
|
||||
writeFileSync(
|
||||
join(remote.source, "workspaces", "research-clinical.yaml"),
|
||||
join(remote.source, "research-clinical", "workspace.yaml"),
|
||||
v3Yaml
|
||||
.replace("id: psd-clinical", "id: research-clinical")
|
||||
.replace("name: Policlinico San Donato", "name: Research Clinical")
|
||||
.replace("collection: psd-clinical", "collection: shared"),
|
||||
);
|
||||
writeFileSync(
|
||||
join(remote.source, "workspaces", "psd-clinical.yaml"),
|
||||
join(remote.source, "psd-clinical", "workspace.yaml"),
|
||||
v3Yaml.replace("collection: psd-clinical", "collection: shared"),
|
||||
);
|
||||
await git(remote.source, ["add", "workspaces"]);
|
||||
await git(remote.source, ["add", "-A"]);
|
||||
await git(remote.source, ["commit", "-m", "Duplicate collection ownership"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
|
||||
@@ -939,10 +962,13 @@ test("retains a historical snapshot while a resumable manifest still references
|
||||
const registry = new WorkspaceRegistry(config(root, remote.remote));
|
||||
await registry.bootstrap();
|
||||
|
||||
writeFileSync(join(remote.source, "workspaces", "psd-clinical.yaml"), validYaml.replace(
|
||||
writeFileSync(join(remote.source, "psd-clinical", "workspace.yaml"), validYaml.replace(
|
||||
"name: Policlinico San Donato", "name: Updated Policlinico San Donato",
|
||||
));
|
||||
await git(remote.source, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
writeFileSync(join(remote.source, "thoth-workspaces.yaml"), catalogYaml([
|
||||
{ id: "psd-clinical", name: "Updated Policlinico San Donato" }, { id: "research", name: "Research" },
|
||||
]));
|
||||
await git(remote.source, ["add", "-A"]);
|
||||
await git(remote.source, ["commit", "-m", "Update workspace"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
const currentCommit = await gitOutput(remote.source, ["rev-parse", "HEAD"]);
|
||||
@@ -964,10 +990,13 @@ test("a session revision lease survives stale retention scans until its manifest
|
||||
await registry.bootstrap();
|
||||
const lease = await registry.acquireSessionRevision("psd-clinical");
|
||||
|
||||
writeFileSync(join(remote.source, "workspaces", "psd-clinical.yaml"), validYaml.replace(
|
||||
writeFileSync(join(remote.source, "psd-clinical", "workspace.yaml"), validYaml.replace(
|
||||
"name: Policlinico San Donato", "name: Concurrent revision",
|
||||
));
|
||||
await git(remote.source, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
writeFileSync(join(remote.source, "thoth-workspaces.yaml"), catalogYaml([
|
||||
{ id: "psd-clinical", name: "Concurrent revision" }, { id: "research", name: "Research" },
|
||||
]));
|
||||
await git(remote.source, ["add", "-A"]);
|
||||
await git(remote.source, ["commit", "-m", "Publish while session is starting"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
await registry.pull();
|
||||
@@ -990,15 +1019,20 @@ test("lists operational descriptors retained after their workspace was removed f
|
||||
const registry = new WorkspaceRegistry(config(root, remote.remote));
|
||||
await registry.bootstrap();
|
||||
|
||||
writeFileSync(join(remote.source, "workspaces", "archive-only.yaml"), validYaml.replace(
|
||||
mkdirSync(join(remote.source, "archive-only"), { recursive: true });
|
||||
writeFileSync(join(remote.source, "archive-only", "workspace.yaml"), validYaml.replace(
|
||||
"id: psd-clinical", "id: archive-only",
|
||||
).replace("collection: psd-clinical", "collection: archive-only"));
|
||||
await git(remote.source, ["add", "workspaces/archive-only.yaml"]);
|
||||
writeFileSync(join(remote.source, "thoth-workspaces.yaml"), catalogYaml([
|
||||
{ id: "psd-clinical", name: "Policlinico San Donato" }, { id: "research", name: "Research" },
|
||||
{ id: "archive-only", name: "Policlinico San Donato" },
|
||||
]));
|
||||
await git(remote.source, ["add", "-A"]);
|
||||
await git(remote.source, ["commit", "-m", "Add retained workspace"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
await registry.pull();
|
||||
|
||||
rmSync(join(remote.source, "workspaces", "psd-clinical.yaml"));
|
||||
rmSync(join(remote.source, "psd-clinical", "workspace.yaml"));
|
||||
await git(remote.source, ["add", "-u"]);
|
||||
await git(remote.source, ["commit", "-m", "Remove original workspace"]);
|
||||
await git(remote.source, ["push", "origin", "main"]);
|
||||
@@ -1050,12 +1084,12 @@ test("rejects a locally-ahead checkout instead of activating local-only content"
|
||||
const registry = new WorkspaceRegistry(config(root, remote.remote));
|
||||
await registry.bootstrap();
|
||||
const checkout = join(root, "repo");
|
||||
writeFileSync(join(checkout, "workspaces", "psd-clinical.yaml"), validYaml.replace(
|
||||
writeFileSync(join(checkout, "psd-clinical", "workspace.yaml"), validYaml.replace(
|
||||
"name: Policlinico San Donato", "name: Local only workspace",
|
||||
));
|
||||
await git(checkout, ["config", "user.name", "Workspace Registry Test"]);
|
||||
await git(checkout, ["config", "user.email", "workspace-registry@example.invalid"]);
|
||||
await git(checkout, ["add", "workspaces/psd-clinical.yaml"]);
|
||||
await git(checkout, ["add", "psd-clinical/workspace.yaml"]);
|
||||
await git(checkout, ["commit", "-m", "Local-only workspace"]);
|
||||
|
||||
await expect(registry.pull()).rejects.toMatchObject({ code: "git_non_fast_forward" });
|
||||
@@ -1143,10 +1177,10 @@ test("snapshots canonical Evidence artifacts at the active commit without copyin
|
||||
|
||||
expect(status.head).toBe(remote.initialCommit);
|
||||
const committedDescriptor = parseWorkspaceYaml(await gitOutput(remote.source, [
|
||||
"show", `${remote.initialCommit}:workspaces/psd-clinical.yaml`,
|
||||
"show", `${remote.initialCommit}:psd-clinical/workspace.yaml`,
|
||||
])) as CanonicalWorkspace;
|
||||
const committedBlob = await gitOutput(remote.source, [
|
||||
"rev-parse", `${remote.initialCommit}:workspaces/psd-clinical.yaml`,
|
||||
"rev-parse", `${remote.initialCommit}:psd-clinical/workspace.yaml`,
|
||||
]);
|
||||
expect(active.revision.blob).toBe(committedBlob);
|
||||
expect(expectedFiles["psd-clinical.yaml"]).toBe(serializeWorkspaceYaml(committedDescriptor));
|
||||
@@ -1164,7 +1198,7 @@ test("snapshots canonical Evidence artifacts at the active commit without copyin
|
||||
}
|
||||
expect(JSON.stringify(manifest)).not.toContain("workspace-content/");
|
||||
expect(readdirSync(snapshotDirectory).some((name) => name === "workspace-content")).toBe(false);
|
||||
expect(readFileSync(join(remote.source, "workspace-content/psd-clinical/evidence/guide.md"), "utf8"))
|
||||
expect(readFileSync(join(remote.source, "psd-clinical/evidence/guide.md"), "utf8"))
|
||||
.toBe("guide v1\n");
|
||||
});
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ llm_policy:
|
||||
const filesystemWorkspace = `${canonicalWorkspace}evidence:
|
||||
source:
|
||||
type: filesystem
|
||||
uri: workspace-content/psd-clinical/evidence
|
||||
uri: psd-clinical/evidence
|
||||
`;
|
||||
|
||||
function evidenceWorkspace(source: string, policy = ""): string {
|
||||
@@ -77,9 +77,10 @@ async function fixture(workspaceSource = filesystemWorkspace) {
|
||||
await git(source, ["init", "--initial-branch=main"]);
|
||||
await git(source, ["config", "user.name", "Runtime Handoff Test"]);
|
||||
await git(source, ["config", "user.email", "runtime-handoff@example.invalid"]);
|
||||
mkdirSync(join(source, "workspaces"));
|
||||
writeFileSync(join(source, "workspaces", "psd-clinical.yaml"), workspaceSource);
|
||||
const evidenceRoot = join(source, "workspace-content", "psd-clinical", "evidence");
|
||||
writeFileSync(join(source, "thoth-workspaces.yaml"), "schema_version: 1\nworkspaces: [{id: psd-clinical, name: Runtime handoff}]\n");
|
||||
mkdirSync(join(source, "psd-clinical"), { recursive: true });
|
||||
writeFileSync(join(source, "psd-clinical", "workspace.yaml"), workspaceSource);
|
||||
const evidenceRoot = join(source, "psd-clinical", "evidence");
|
||||
mkdirSync(evidenceRoot, { recursive: true });
|
||||
writeFileSync(join(evidenceRoot, "guide.md"), "# Immutable revision evidence\n");
|
||||
await git(source, ["add", "."]);
|
||||
@@ -174,7 +175,6 @@ test("separate runtime leases hand off byte-identical revision Evidence configs
|
||||
f.registryConfig.root,
|
||||
"snapshots",
|
||||
f.revision.commit,
|
||||
"workspace-content",
|
||||
"psd-clinical",
|
||||
"evidence",
|
||||
);
|
||||
@@ -228,10 +228,10 @@ test("real Evidence-content-only commit changes runtime identity and root with i
|
||||
const first = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
|
||||
const descriptorBefore = readFileSync(f.revision.snapshotPath, "utf8");
|
||||
writeFileSync(
|
||||
join(f.source, "workspace-content", "psd-clinical", "evidence", "guide.md"),
|
||||
join(f.source, "psd-clinical", "evidence", "guide.md"),
|
||||
"# Content-only revision two\n",
|
||||
);
|
||||
await git(f.source, ["add", "workspace-content/psd-clinical/evidence/guide.md"]);
|
||||
await git(f.source, ["add", "psd-clinical/evidence/guide.md"]);
|
||||
await git(f.source, ["commit", "-m", "Update Evidence content only"]);
|
||||
await git(f.source, ["push", "origin", "main"]);
|
||||
await f.registry.pull();
|
||||
@@ -250,7 +250,6 @@ test("real Evidence-content-only commit changes runtime identity and root with i
|
||||
f.registryConfig.root,
|
||||
"snapshots",
|
||||
current.commit,
|
||||
"workspace-content",
|
||||
"psd-clinical",
|
||||
"evidence",
|
||||
));
|
||||
|
||||
@@ -193,7 +193,7 @@ function evidenceRender(
|
||||
test("renders filesystem Evidence below the immutable revision content root with default policy", () => {
|
||||
const yaml = evidenceRender({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
});
|
||||
const rendered = parse(yaml);
|
||||
|
||||
@@ -201,7 +201,7 @@ test("renders filesystem Evidence below the immutable revision content root with
|
||||
expect(rendered.evidence).toEqual({
|
||||
sources: [{
|
||||
type: "filesystem",
|
||||
root: `/srv/registry/snapshots/${evidenceRevision}/workspace-content/psd-clinical/evidence`,
|
||||
root: `/srv/registry/snapshots/${evidenceRevision}/psd-clinical/evidence`,
|
||||
patterns: ["**/*.md"],
|
||||
max_bytes: 10_485_760,
|
||||
}],
|
||||
@@ -400,7 +400,7 @@ test.each([
|
||||
test("is byte deterministic and revision-bound for descriptor-identical content-only commits", () => {
|
||||
const source = {
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
};
|
||||
const first = evidenceRender(source);
|
||||
expect(evidenceRender(source)).toBe(first);
|
||||
@@ -424,7 +424,7 @@ test("is byte deterministic and revision-bound for descriptor-identical content-
|
||||
expect(next).not.toBe(first);
|
||||
expect(nextParsed.runtime_identity.workspace_revision).toBe(nextRevision);
|
||||
expect(nextParsed.evidence.sources[0].root).toBe(
|
||||
`/srv/registry/snapshots/${nextRevision}/workspace-content/psd-clinical/evidence`,
|
||||
`/srv/registry/snapshots/${nextRevision}/psd-clinical/evidence`,
|
||||
);
|
||||
expect(nextParsed.evidence.sources[0].root).not.toBe(firstParsed.evidence.sources[0].root);
|
||||
});
|
||||
|
||||
@@ -164,7 +164,7 @@ evidence:
|
||||
const evidenceVariable = (suffix: string) => `THT_WS_PSD_CLINICAL_EVIDENCE_${suffix}`;
|
||||
|
||||
test.each([
|
||||
{ type: "filesystem", uri: "workspace-content/psd-clinical/evidence" },
|
||||
{ type: "filesystem", uri: "psd-clinical/evidence" },
|
||||
{ type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "none" },
|
||||
{ type: "s3", uri: "s3://clinical-evidence/published/", credentials: "ambient" },
|
||||
])("does not resolve Evidence variables for $type modes without file credentials", (source) => {
|
||||
|
||||
@@ -103,7 +103,7 @@ test.each([
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ type: "filesystem", uri: "workspace-content/psd-clinical/evidence" },
|
||||
{ type: "filesystem", uri: "psd-clinical/evidence" },
|
||||
{ type: "http", uris: ["https://evidence.example.test/guide.md"], authentication: "none" },
|
||||
{ type: "s3", uri: "s3://clinical-evidence/published/", credentials: "ambient" },
|
||||
])("omits Evidence installation variables for $type modes without file credentials", (source) => {
|
||||
@@ -134,7 +134,7 @@ llm_policy: { allowed: [zai/glm-5.2] }
|
||||
const evidenceSources = [
|
||||
{
|
||||
label: "filesystem",
|
||||
source: { type: "filesystem", uri: "workspace-content/psd-clinical/evidence" },
|
||||
source: { type: "filesystem", uri: "psd-clinical/evidence" },
|
||||
variables: [],
|
||||
},
|
||||
{
|
||||
@@ -207,11 +207,11 @@ test("documents the S3 session token file as optional", () => {
|
||||
|
||||
test("documents same-revision filesystem ownership without claiming P1 materialization", () => {
|
||||
const descriptor = parseWorkspaceYaml(
|
||||
`${renderWorkspaceWithoutEvidence()}evidence:\n source: { type: filesystem, uri: workspace-content/psd-clinical/evidence }\n`,
|
||||
`${renderWorkspaceWithoutEvidence()}evidence:\n source: { type: filesystem, uri: psd-clinical/evidence }\n`,
|
||||
);
|
||||
const docs = renderWorkspaceDocs(descriptor).markdown;
|
||||
|
||||
expect(docs).toContain("`workspace-content/psd-clinical/evidence`");
|
||||
expect(docs).toContain("`psd-clinical/evidence`");
|
||||
expect(docs).toMatch(/same Git revision/i);
|
||||
expect(docs).toMatch(/P6.*materializ/i);
|
||||
expect(docs).toMatch(/containment.*symlink/i);
|
||||
|
||||
@@ -297,7 +297,7 @@ const validEvidenceSources = [
|
||||
name: "filesystem with explicit values",
|
||||
source: {
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
patterns: ["documents/**/*.pdf", "notes/*.md"],
|
||||
max_bytes: 12_000_000,
|
||||
},
|
||||
@@ -374,14 +374,14 @@ test.each(validEvidenceSources)("accepts evidence source: $name", ({ source }) =
|
||||
test("applies filesystem and policy defaults to the canonical descriptor", () => {
|
||||
const parsed = validateWorkspaceDescriptor(withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
}));
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
evidence: {
|
||||
source: {
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
patterns: ["**/*.md"],
|
||||
max_bytes: 10 * 1024 * 1024,
|
||||
},
|
||||
@@ -397,7 +397,7 @@ test("keeps evidence optional on schema v3", () => {
|
||||
test("serializes defaulted evidence canonically and parses it without loss", () => {
|
||||
const canonical = validateWorkspaceDescriptor(withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
}));
|
||||
if (canonical.workspace.schema_version !== 3) throw new Error("expected schema v3");
|
||||
|
||||
@@ -405,16 +405,16 @@ test("serializes defaulted evidence canonically and parses it without loss", ()
|
||||
});
|
||||
|
||||
const invalidFilesystemPaths = [
|
||||
"/workspace-content/psd-clinical/evidence",
|
||||
"workspace-content/../psd-clinical/evidence",
|
||||
"workspace-content/./psd-clinical/evidence",
|
||||
"workspace-content//psd-clinical/evidence",
|
||||
"workspace-content/psd-clinical/evidence/..",
|
||||
"workspace-content\\psd-clinical\\evidence",
|
||||
"workspace-content/psd-clinical/evidence\u0000",
|
||||
"workspace-content/other-workspace/evidence",
|
||||
"workspace-content/psd-clinical",
|
||||
"workspace-content/psd-clinical/evidence/nested",
|
||||
"/psd-clinical/evidence",
|
||||
"../psd-clinical/evidence",
|
||||
"./psd-clinical/evidence",
|
||||
"/psd-clinical/evidence",
|
||||
"psd-clinical/evidence/..",
|
||||
"\\psd-clinical\\evidence",
|
||||
"psd-clinical/evidence\u0000",
|
||||
"other-workspace/evidence",
|
||||
"psd-clinical",
|
||||
"psd-clinical/evidence/nested",
|
||||
];
|
||||
|
||||
test.each(invalidFilesystemPaths)("rejects unsafe or noncanonical filesystem URI %#", (uri) => {
|
||||
@@ -426,27 +426,27 @@ const invalidPatterns = ["", "/absolute", "../escape", ".", "folder/./file", "fo
|
||||
test.each(invalidPatterns)("rejects unsafe evidence glob %#", (pattern) => {
|
||||
expectSafeEvidenceError(withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
patterns: [pattern],
|
||||
}), /evidence.*source.*patterns/i);
|
||||
});
|
||||
|
||||
test("rejects empty and duplicate filesystem patterns", () => {
|
||||
const source = { type: "filesystem", uri: "workspace-content/psd-clinical/evidence" };
|
||||
const source = { type: "filesystem", uri: "psd-clinical/evidence" };
|
||||
expectSafeEvidenceError(withEvidence({ ...source, patterns: [] }), /patterns/i);
|
||||
expectSafeEvidenceError(withEvidence({ ...source, patterns: ["**/*.pdf", "**/*.pdf"] }), /patterns/i);
|
||||
});
|
||||
|
||||
test.each(["ftp", "git", "unknown"])("rejects unsupported evidence discriminator %s", (type) => {
|
||||
expectSafeEvidenceError(withEvidence({ type, uri: "workspace-content/psd-clinical/evidence" }), /evidence.*source.*type/i);
|
||||
expectSafeEvidenceError(withEvidence({ type, uri: "psd-clinical/evidence" }), /evidence.*source.*type/i);
|
||||
});
|
||||
|
||||
test("rejects unknown evidence keys", () => {
|
||||
expectSafeEvidenceError({ ...withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
}), evidence: {
|
||||
source: { type: "filesystem", uri: "workspace-content/psd-clinical/evidence", mystery: true },
|
||||
source: { type: "filesystem", uri: "psd-clinical/evidence", mystery: true },
|
||||
policy: explicitPolicy,
|
||||
mystery: true,
|
||||
} }, /unrecognized|mystery/i);
|
||||
@@ -460,7 +460,7 @@ test.each(credentialFields)("rejects credential-shaped evidence field %s without
|
||||
const canary = `CANARY-${field}-DO-NOT-LEAK`;
|
||||
expectSafeEvidenceError(withEvidence({
|
||||
type: "filesystem",
|
||||
uri: "workspace-content/psd-clinical/evidence",
|
||||
uri: "psd-clinical/evidence",
|
||||
[field]: canary,
|
||||
}), /evidence.*source/i, canary);
|
||||
});
|
||||
@@ -545,6 +545,6 @@ test.each([
|
||||
["retain_published_generations", Number.MAX_SAFE_INTEGER + 1],
|
||||
] as const)("rejects invalid evidence policy bound %s=%s", (field, value) => {
|
||||
expectSafeEvidenceError(withEvidence({
|
||||
type: "filesystem", uri: "workspace-content/psd-clinical/evidence",
|
||||
type: "filesystem", uri: "psd-clinical/evidence",
|
||||
}, { ...explicitPolicy, [field]: value }), new RegExp(field, "i"));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user