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"]);
|
||||
|
||||
Reference in New Issue
Block a user