fix: resolve workspace publish conflicts

This commit is contained in:
2026-08-04 06:57:19 +02:00
parent b75b3af28e
commit 234b40e7cf
7 changed files with 218 additions and 20 deletions
@@ -23,6 +23,8 @@ const request: PublishWorkspaceRequest = {
const conflict: WorkspaceConflict = {
code: "workspace_conflict",
fields: ["semantic_index.embedding.model"],
expected: { commit: "a".repeat(40), blob: "b".repeat(40) },
actual: { commit: "c".repeat(40), blob: "d".repeat(40) },
base: workspace,
local: { ...workspace, semantic_index: { ...workspace.semantic_index, embedding: { ...workspace.semantic_index.embedding, model: "local-model" } } },
remote: { ...workspace, semantic_index: { ...workspace.semantic_index, embedding: { ...workspace.semantic_index.embedding, model: "remote-model" } } },
@@ -40,7 +42,7 @@ test("validates a draft and requires a separate confirmation before publishing",
publishCalls += 1;
return HttpResponse.json({ revision: { id: "psd-clinical", commit: "c".repeat(40), blob: "d".repeat(40), snapshotPath: "/safe", state: "operational" } });
}));
render(<WorkspacePublishDialog open request={request} onOpenChange={vi.fn()} onPublished={published} onPull={vi.fn()} onReload={vi.fn()} />);
render(<WorkspacePublishDialog open request={request} onOpenChange={vi.fn()} onPublished={published} onResolved={vi.fn()} onPull={vi.fn()} onReload={vi.fn()} />);
expect(screen.getByRole("button", { name: "Publish" })).toBeDisabled();
await user.click(screen.getByRole("button", { name: "Validate draft" }));
@@ -61,7 +63,7 @@ test("shows a field-level conflict and never overwrites the remote workspace", a
published = true;
return HttpResponse.json({ ...conflict, message: "Workspace changed in the registry." }, { status: 409 });
}));
render(<WorkspacePublishDialog open request={request} onOpenChange={vi.fn()} onPublished={vi.fn()} onPull={vi.fn()} onReload={vi.fn()} />);
render(<WorkspacePublishDialog open request={request} onOpenChange={vi.fn()} onPublished={vi.fn()} onResolved={vi.fn()} onPull={vi.fn()} onReload={vi.fn()} />);
await user.click(screen.getByRole("button", { name: "Validate draft" }));
await user.click(await screen.findByRole("button", { name: "Publish" }));
@@ -70,18 +72,41 @@ test("shows a field-level conflict and never overwrites the remote workspace", a
expect(await screen.findByText("semantic_index.embedding.model")).toBeVisible();
expect(screen.getByText("local-model")).toBeVisible();
expect(screen.getByText("remote-model")).toBeVisible();
expect(screen.getByRole("button", { name: "Pull latest registry" })).toBeVisible();
expect(screen.getByRole("button", { name: "Reload workspace" })).toBeVisible();
expect(screen.queryByRole("button", { name: /use local|use remote|confirm publish/i })).not.toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Use your draft for semantic_index.embedding.model" })).toBeVisible();
expect(screen.getByRole("radio", { name: "Use registry value for semantic_index.embedding.model" })).toBeVisible();
expect(screen.getByRole("button", { name: "Save revised draft" })).toBeDisabled();
expect(published).toBe(true);
});
test("saves explicit local choices as a rebased draft and does not republish it", async () => {
const user = userEvent.setup();
const saved = vi.fn();
let publishCalls = 0;
server.use(http.post("http://localhost:8787/workspaces/publish", () => {
publishCalls += 1;
return HttpResponse.json({ ...conflict, message: "Workspace changed in the registry." }, { status: 409 });
}));
render(<WorkspacePublishDialog open request={request} onOpenChange={vi.fn()} onPublished={vi.fn()} onResolved={saved} onPull={vi.fn()} onReload={vi.fn()} />);
await user.click(screen.getByRole("button", { name: "Validate draft" }));
await user.click(await screen.findByRole("button", { name: "Publish" }));
await user.click(screen.getByRole("button", { name: "Confirm publish" }));
await user.click(await screen.findByRole("radio", { name: "Use your draft for semantic_index.embedding.model" }));
await user.click(screen.getByRole("button", { name: "Save revised draft" }));
expect(saved).toHaveBeenCalledWith(expect.objectContaining({
baseCommit: "c".repeat(40), baseBlob: "d".repeat(40),
workspace: expect.objectContaining({ semantic_index: expect.objectContaining({ embedding: expect.objectContaining({ model: "local-model" }) }) }),
}));
expect(publishCalls).toBe(1);
});
test("keeps a conflict open and redacts a failed registry pull", async () => {
const user = userEvent.setup();
server.use(http.post("http://localhost:8787/workspaces/publish", () => HttpResponse.json({
...conflict, message: "Workspace changed in the registry.",
}, { status: 409 })));
render(<WorkspacePublishDialog open request={request} onOpenChange={vi.fn()} onPublished={vi.fn()} onPull={vi.fn().mockRejectedValue(new Error("token=secret"))} onReload={vi.fn()} />);
render(<WorkspacePublishDialog open request={request} onOpenChange={vi.fn()} onPublished={vi.fn()} onResolved={vi.fn()} onPull={vi.fn().mockRejectedValue(new Error("token=secret"))} onReload={vi.fn()} />);
await user.click(screen.getByRole("button", { name: "Validate draft" }));
await user.click(await screen.findByRole("button", { name: "Publish" }));