feat(FN-3279): refresh review state on manual trigger and restore roadmap e

Adds review tab refresh UX, metadata, and API (FN-3279) alongside permanent-agent action gating coordination tools (FN-3724), and introduces two new workspace plugin packages: `fusion-plugin-even-cards` (board/task cards with auth and board-routes) and `fusion-plugin-even-realities-glasses` (cards,

Fusion-Task-Id: FN-3279
This commit is contained in:
Fusion
2026-05-08 12:54:47 -07:00
committed by gsxdsm
parent 11cd2342af
commit cb0b01b3d5
13 changed files with 628 additions and 43 deletions

View File

@@ -1012,8 +1012,58 @@ describe("GitHubClient", () => {
const snapshot = await client.getPrReviewSnapshot("owner", "repo", 1);
expect(snapshot.items).toHaveLength(2);
expect(snapshot.summary?.reviewDecision).toBe("CHANGES_REQUESTED");
expect(snapshot.prInfo.number).toBe(1);
expect(snapshot.commentCount).toBe(1);
expect(snapshot.summary?.reviewers[0]).toEqual(expect.objectContaining({ login: "octocat", state: "CHANGES_REQUESTED" }));
});
it("falls back to API review details when gh fails and token is available", async () => {
mockRunGhJsonAsync.mockImplementation(() => {
throw new Error("gh down");
});
const clientWithToken = new GitHubClient("ghp_token");
const mockFetch = vi.fn()
.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: {
repository: {
pullRequest: {
reviewDecision: "APPROVED",
comments: { nodes: [{ id: "C_1", body: "lgtm", createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:01Z", url: "https://example.com/c1", author: { login: "bot" } }] },
reviews: { nodes: [{ id: "R_1", state: "APPROVED", body: "good", submittedAt: "2024-01-01T00:00:00Z", url: "https://example.com/r1", author: { login: "reviewer" } }] },
},
},
},
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: {
repository: {
pullRequest: {
number: 1,
url: "https://github.com/owner/repo/pull/1",
title: "PR",
state: "OPEN",
reviewDecision: "APPROVED",
baseRefName: "main",
headRefName: "fn/fn-1",
comments: { totalCount: 1 },
commits: { nodes: [{ commit: { statusCheckRollup: { contexts: { nodes: [] } } } }] },
},
},
},
}),
});
global.fetch = mockFetch as any;
const snapshot = await clientWithToken.getPrReviewSnapshot("owner", "repo", 1);
expect(snapshot.summary?.reviewDecision).toBe("APPROVED");
expect(snapshot.items).toHaveLength(2);
vi.restoreAllMocks();
});
});
describe("mergePr", () => {

View File

@@ -2137,6 +2137,16 @@ describe("POST /tasks/:id/review/refresh", () => {
vi.spyOn(GitHubClient.prototype, "getPrReviewSnapshot").mockResolvedValue({
decision: "CHANGES_REQUESTED",
checks: [],
prInfo: {
url: "https://github.com/owner/repo/pull/42",
number: 42,
status: "open",
title: "PR",
headBranch: "fusion/fn-1",
baseBranch: "main",
commentCount: 1,
},
commentCount: 1,
summary: {
reviewDecision: "CHANGES_REQUESTED",
reviewers: [],
@@ -2153,10 +2163,42 @@ describe("POST /tasks/:id/review/refresh", () => {
expect(res.status).toBe(200);
expect((store.updateTask as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
"FN-001",
expect.objectContaining({ reviewState: expect.objectContaining({ source: "pull-request" }) }),
expect.objectContaining({
reviewState: expect.objectContaining({
source: "pull-request",
refreshSource: "manual",
refreshStatus: "ready",
lastRefreshedAt: expect.any(String),
}),
}),
);
});
it("returns scoped refresh error payload in PR mode when GitHub refresh fails", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
...FAKE_TASK_DETAIL,
prInfo: {
url: "https://github.com/owner/repo/pull/42",
number: 42,
status: "open",
title: "PR",
headBranch: "fusion/fn-1",
baseBranch: "main",
commentCount: 0,
},
reviewState: { source: "pull-request", items: [], addressing: [] },
});
vi.spyOn(GitHubClient.prototype, "getPrReviewSnapshot").mockRejectedValue(new Error("GitHub outage"));
const res = await REQUEST(buildApp(), "POST", "/api/tasks/FN-001/review/refresh", JSON.stringify({}), {
"content-type": "application/json",
});
expect(res.status).toBe(200);
expect(res.body.reviewState.refreshStatus).toBe("error");
expect(res.body.reviewState.refreshError).toContain("GitHub outage");
});
it("refreshes direct-mode review payload without PR", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
...FAKE_TASK_DETAIL,
@@ -2167,13 +2209,34 @@ describe("POST /tasks/:id/review/refresh", () => {
},
});
const getSnapshotSpy = vi.spyOn(GitHubClient.prototype, "getPrReviewSnapshot");
const res = await REQUEST(buildApp(), "POST", "/api/tasks/FN-001/review/refresh", JSON.stringify({}), {
"content-type": "application/json",
});
expect(res.status).toBe(200);
expect((store.updateTask as ReturnType<typeof vi.fn>)).toHaveBeenCalled();
expect((store.updateTask as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
"FN-001",
expect.objectContaining({
reviewState: expect.objectContaining({
source: "reviewer-agent",
refreshSource: "manual",
refreshStatus: "ready",
lastRefreshedAt: expect.any(String),
}),
}),
);
expect(res.body.reviewState.source).toBe("reviewer-agent");
expect(getSnapshotSpy).not.toHaveBeenCalled();
});
it("returns 404 when task is missing", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(Object.assign(new Error("not found"), { code: "ENOENT" }));
const res = await REQUEST(buildApp(), "POST", "/api/tasks/FN-404/review/refresh", JSON.stringify({}), {
"content-type": "application/json",
});
expect(res.status).toBe(404);
});
});