feat(KB-007): add manual plan approval setting

- Add requirePlanApproval setting to core types and dashboard config
- Add approve-plan and reject-plan API endpoints with handlers
- Add approve/reject UI buttons to TaskDetailModal for awaiting-approval tasks
- Update triage processor to check requirePlanApproval and set awaiting-approval status
- Add comprehensive tests for API endpoints, UI components, and triage logic
- Update AGENTS.md with documentation for the new setting
This commit is contained in:
gsxdsm
2026-03-29 19:23:30 -07:00
parent 9060979a15
commit aa8a78f023
12 changed files with 770 additions and 7 deletions

View File

@@ -298,3 +298,75 @@ describe("fetchGitRemotes", () => {
await expect(fetchGitRemotes()).rejects.toThrow("Failed to execute git command");
});
});
// --- Plan approval API tests ---
import { approvePlan, rejectPlan } from "./api";
describe("approvePlan", () => {
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("approves plan and returns updated task", async () => {
const approvedTask: Task = {
...FAKE_DETAIL,
column: "todo",
status: undefined,
};
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, approvedTask));
const result = await approvePlan("KB-001");
expect(result.column).toBe("todo");
expect(result.status).toBeUndefined();
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/approve-plan", {
headers: { "Content-Type": "application/json" },
method: "POST",
});
});
it("throws on error response", async () => {
globalThis.fetch = vi.fn().mockReturnValue(
mockFetchResponse(false, { error: "Task must be in 'triage' column to approve plan" }, 400)
);
await expect(approvePlan("KB-001")).rejects.toThrow("triage");
});
});
describe("rejectPlan", () => {
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("rejects plan and returns updated task", async () => {
const rejectedTask: Task = {
...FAKE_DETAIL,
column: "triage",
status: undefined,
};
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, rejectedTask));
const result = await rejectPlan("KB-001");
expect(result.column).toBe("triage");
expect(result.status).toBeUndefined();
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/KB-001/reject-plan", {
headers: { "Content-Type": "application/json" },
method: "POST",
});
});
it("throws on error response", async () => {
globalThis.fetch = vi.fn().mockReturnValue(
mockFetchResponse(false, { error: "Task must have status 'awaiting-approval' to reject plan" }, 400)
);
await expect(rejectPlan("KB-001")).rejects.toThrow("awaiting-approval");
});
});