feat: activate workspaces from the root catalog and bootstrap-only publication

This commit is contained in:
2026-08-11 14:49:46 +02:00
parent 86af45acb4
commit 25ec236f1f
10 changed files with 358 additions and 242 deletions
+130 -96
View File
@@ -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");
});