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:
Nicky LeachandPaperclip authored and GitHub committed 2026-07-07 15:17:16 -07:00
1 parent 83f5f59842
commit 4f5abf6007
7 files changed
+643 -2

No files matched your search

+28
View File
@@ -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",
});
});
});
+17
View File
@@ -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),
};