mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 02:32:07 +01:00
feat(recovery-card): W7 reconcile-forward + break-glass actions
Adds the task-page recovery affordance for workspace branch divergence: reconcile-forward when ancestry is safe, and a break-glass override flow that requires explicit confirmation and a reason. Includes client wiring for the existing reconcile-branch endpoint, issue-detail refresh after successful reconciliation, runtime-management gating for break-glass, and tests for action visibility, payloads, permission gating, and workspace-target selection. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
83f5f59842
commit
4f5abf6007
7 files changed
+643
-2
No files matched your search
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockApi = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./client", () => ({
|
||||
@@ -93,3 +94,30 @@ describe("executionWorkspacesApi.listSummaries", () => {
|
||||
expect(overview.items[0]!.linkedIssues[0]!.updatedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
describe("executionWorkspacesApi.reconcile", () => {
|
||||
beforeEach(() => {
|
||||
mockApi.post.mockReset();
|
||||
mockApi.post.mockResolvedValue({});
|
||||
});
|
||||
|
||||
// Regression pin (PAP-1705): the frontend path must match the reviewed, OpenAPI-documented
|
||||
// backend contract `POST /execution-workspaces/:id/reconcile-branch` (S4 / PAP-1586). A bare
|
||||
// `/reconcile` 404s both recovery-card actions. If the two sides drift, this test fails.
|
||||
it("posts forward reconcile to the /reconcile-branch route", async () => {
|
||||
await executionWorkspacesApi.reconcile("workspace-1", { mode: "forward" });
|
||||
|
||||
expect(mockApi.post).toHaveBeenCalledWith("/execution-workspaces/workspace-1/reconcile-branch", {
|
||||
mode: "forward",
|
||||
});
|
||||
});
|
||||
|
||||
it("posts break-glass override reconcile to the /reconcile-branch route", async () => {
|
||||
await executionWorkspacesApi.reconcile("workspace-1", { mode: "override", reason: "operator note" });
|
||||
|
||||
expect(mockApi.post).toHaveBeenCalledWith("/execution-workspaces/workspace-1/reconcile-branch", {
|
||||
mode: "override",
|
||||
reason: "operator note",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -115,4 +115,21 @@ export const executionWorkspacesApi = {
|
||||
sanitizeWorkspaceRuntimeControlTarget(target),
|
||||
),
|
||||
update: (id: string, data: Record<string, unknown>) => api.patch<ExecutionWorkspace>(`/execution-workspaces/${id}`, data),
|
||||
/**
|
||||
* Reconcile a git-worktree branch divergence via the S4 (`PAP-1586`) op.
|
||||
*
|
||||
* Hits `POST /execution-workspaces/:id/reconcile-branch`. That route is the reviewed,
|
||||
* OpenAPI-documented backend contract and already ships on `master`: it was merged ahead of this
|
||||
* client change in `server/src/routes/execution-workspaces.ts` (route registration:
|
||||
* `router.post("/execution-workspaces/:id/reconcile-branch", ...)`, landed in PR #9170, with the
|
||||
* `forward` auto-reconcile path in PR #9172). This client is therefore additive against an
|
||||
* existing endpoint, not a call to a missing one. Keep this path byte-identical to the backend
|
||||
* route; the drift is pinned by a regression test in `execution-workspaces.test.ts`.
|
||||
* - `mode: "forward"` — server re-verifies `ancestryVerdict === "ancestor"` (client hint is
|
||||
* never trusted); no `reason` needed.
|
||||
* - `mode: "override"` — audited break-glass; the server rejects agent actors, re-checks
|
||||
* `runtime:manage` permission, and requires a non-empty operator `reason`.
|
||||
*/
|
||||
reconcile: (id: string, body: { mode: "forward" } | { mode: "override"; reason: string }) =>
|
||||
api.post<ExecutionWorkspace>(`/execution-workspaces/${id}/reconcile-branch`, body),
|
||||
};
|
||||
Reference in new issue
Block a user