fix: route workspace CRUD through addressed publication

This commit is contained in:
2026-08-11 18:30:06 +02:00
parent 4a0ec61da3
commit 29a2e507a9
5 changed files with 120 additions and 35 deletions
+21 -8
View File
@@ -116,7 +116,7 @@ const revision: WorkspaceRevision = {
snapshotPath: "/registry/snapshots/psd-clinical.yaml",
};
type RegistryFake = Pick<WorkspaceRegistry, "bootstrap" | "pull" | "list" | "read" | "publish"> & { ensureBootstrapAddressed: ReturnType<typeof vi.fn>; publishAddressed: ReturnType<typeof vi.fn> };
type RegistryFake = Pick<WorkspaceRegistry, "read" | "recoveryIdentity"> & { ensureBootstrapAddressed: ReturnType<typeof vi.fn>; publishAddressed: ReturnType<typeof vi.fn>; snapshotPath: ReturnType<typeof vi.fn> };
function registryFake(overrides: Partial<RegistryFake> = {}): RegistryFake {
return {
@@ -128,9 +128,10 @@ function registryFake(overrides: Partial<RegistryFake> = {}): RegistryFake {
})),
list: vi.fn(async () => [revision]),
read: vi.fn(async () => ({ workspace, revision })),
publish: vi.fn(async () => revision),
recoveryIdentity: vi.fn(() => ({ operation: "registry_bootstrap", requestSha256: "c".repeat(64), installationIdentitySha256: "d".repeat(64), repositoryIdentitySha256: "e".repeat(64), remoteRefIdentitySha256: "f".repeat(64) })),
ensureBootstrapAddressed: vi.fn(async () => ({ kind: "already_active", snapshot: { schemaVersion: 1, commit: revision.commit, manifestSha256: "a".repeat(64), workspaces: [{ workspaceId: revision.id, revision: revision.commit, descriptorBlob: revision.blob, manifestSha256: "b".repeat(64) }] } })),
publishAddressed: vi.fn(async () => ({ plan: { targetCommit: revision.commit, targetManifestSha256: "a".repeat(64), targetWorkspaces: [{ workspaceId: revision.id, revision: revision.commit, descriptorBlob: revision.blob, manifestSha256: "b".repeat(64) }] } })),
snapshotPath: vi.fn(() => revision.snapshotPath),
...overrides,
};
}
@@ -278,7 +279,7 @@ test.each([
});
expect(response.body).not.toMatch(/migration_required|schema version/i);
}
expect(registry.publish).not.toHaveBeenCalled();
expect(registry.publishAddressed).not.toHaveBeenCalled();
});
test("runs diagnostics for a schema v3 workspace without external semantic bindings", async () => {
@@ -332,7 +333,7 @@ test("reports missing Evidence binding through the real test route without chang
variable,
})]));
expect(read).toHaveBeenCalledTimes(1);
expect(registry.publish).not.toHaveBeenCalled();
expect(registry.publishAddressed).not.toHaveBeenCalled();
expect(revision).toMatchObject({ commit: "a".repeat(40), blob: "b".repeat(40) });
} finally {
if (previous === undefined) delete process.env[variable];
@@ -352,7 +353,7 @@ test("returns a 409 field conflict instead of overwriting a changed workspace",
remote: { ...workspace, workspace: { ...workspace.workspace, description: "Remote description" } },
},
);
const registry = registryFake({ publish: vi.fn(async () => { throw conflict; }) });
const registry = registryFake({ publishAddressed: vi.fn(async () => { throw conflict; }) });
const app = appFor(registry);
const staleUpdate = {
action: "update",
@@ -378,7 +379,7 @@ test("returns a 409 field conflict instead of overwriting a changed workspace",
test("maps a stale registry commit to HTTP 409 without conflict payloads", async () => {
const registry = registryFake({
publish: vi.fn(async () => {
publishAddressed: vi.fn(async () => {
throw new WorkspaceRegistryError("workspace_stale", "Workspace revision is stale");
}),
});
@@ -395,6 +396,18 @@ test("maps a stale registry commit to HTTP 409 without conflict payloads", async
expect(res.json()).toEqual({ code: "workspace_stale", message: "Workspace revision is stale." });
});
test("publishes accepted CRUD through the addressed request and returns its terminal revision", async () => {
const publishAddressed = vi.fn(async (request: any) => ({
plan: { targetCommit: revision.commit, targetManifestSha256: "a".repeat(64), targetWorkspaces: [{ workspaceId: request.mutation.workspace.workspace.id, revision: revision.commit, descriptorBlob: revision.blob, manifestSha256: "b".repeat(64) }] },
}));
const registry = registryFake({ publishAddressed });
const app = appFor(registry);
const response = await app.inject({ method: "POST", url: "/workspaces/publish", payload: { action: "create", workspace, baseCommit: revision.commit } });
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ revision: { id: workspace.workspace.id, commit: revision.commit, blob: revision.blob, snapshotPath: revision.snapshotPath } });
expect(publishAddressed).toHaveBeenCalledWith(expect.objectContaining({ mode: "create", operation: "registry_pull", mutation: expect.objectContaining({ action: "create", baseCommit: revision.commit }) }));
});
test("exports generated public artifacts without secret values", async () => {
const app = appFor(registryFake());
@@ -415,7 +428,7 @@ test("rejects a zip-slip import without publishing or writing a checkout file",
expect(res.statusCode).toBe(400);
expect(res.json()).toMatchObject({ code: "workspace_invalid" });
expect(registry.publish).not.toHaveBeenCalled();
expect(registry.publishAddressed).not.toHaveBeenCalled();
});
test("imports an exact generated bundle only as a browser draft", async () => {
@@ -426,7 +439,7 @@ test("imports an exact generated bundle only as a browser draft", async () => {
expect(res.statusCode).toBe(200);
expect(res.json()).toMatchObject({ draft: { workspace } });
expect(registry.publish).not.toHaveBeenCalled();
expect(registry.publishAddressed).not.toHaveBeenCalled();
});