Add generic issue-linked board approvals

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-04-06 10:36:31 -05:00
parent 6b4f3b56e4
commit 365b6d9bd8
10 changed files with 345 additions and 39 deletions

View file

@ -4,7 +4,7 @@ import { act } from "react";
import type { ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { MemoryRouter } from "react-router-dom";
import type { Agent } from "@paperclipai/shared";
import type { Agent, Approval } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CommentThread } from "./CommentThread";
@ -33,6 +33,25 @@ vi.mock("./InlineEntitySelector", () => ({
InlineEntitySelector: () => null,
}));
vi.mock("./ApprovalCard", () => ({
ApprovalCard: ({
approval,
onApprove,
onReject,
}: {
approval: Approval;
onApprove?: () => void;
onReject?: () => void;
}) => (
<div>
<div>{approval.type}</div>
<div>{String(approval.payload.title ?? "")}</div>
{onApprove ? <button type="button" onClick={onApprove}>Approve</button> : null}
{onReject ? <button type="button" onClick={onReject}>Reject</button> : null}
</div>
),
}));
vi.mock("@/plugins/slots", () => ({
PluginSlotOutlet: () => null,
}));
@ -144,4 +163,75 @@ describe("CommentThread", () => {
root.unmount();
});
});
it("renders linked approvals inline in the timeline", () => {
const root = createRoot(container);
const agent: Agent = {
id: "agent-1",
companyId: "company-1",
name: "CodexCoder",
urlKey: "codexcoder",
role: "engineer",
title: null,
icon: "code",
status: "active",
reportsTo: null,
capabilities: null,
adapterType: "process",
adapterConfig: {},
runtimeConfig: {},
budgetMonthlyCents: 0,
spentMonthlyCents: 0,
pauseReason: null,
pausedAt: null,
permissions: { canCreateAgents: false },
lastHeartbeatAt: null,
metadata: null,
createdAt: new Date("2026-03-11T00:00:00.000Z"),
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
};
const approval: Approval = {
id: "approval-1",
companyId: "company-1",
type: "request_board_approval",
requestedByAgentId: "agent-1",
requestedByUserId: null,
status: "pending",
payload: {
title: "Approve hosting spend",
text: "Estimated monthly cost is $42.",
},
decisionNote: null,
decidedByUserId: null,
decidedAt: null,
createdAt: new Date("2026-03-11T09:00:00.000Z"),
updatedAt: new Date("2026-03-11T09:00:00.000Z"),
};
act(() => {
root.render(
<MemoryRouter>
<CommentThread
comments={[]}
linkedApprovals={[approval]}
agentMap={new Map([["agent-1", agent]])}
onAdd={async () => {}}
onApproveApproval={async () => {}}
onRejectApproval={async () => {}}
/>
</MemoryRouter>,
);
});
const approvalRow = container.querySelector("#approval-approval-1") as HTMLDivElement | null;
expect(approvalRow).not.toBeNull();
expect(container.textContent).toContain("request_board_approval");
expect(container.textContent).toContain("Approve hosting spend");
expect(container.textContent).toContain("Approve");
expect(container.textContent).toContain("Reject");
act(() => {
root.unmount();
});
});
});