feat(FN-4748): complete Step 4 — add startup github tracking reconciliation

Fusion-Task-Id: FN-4748
Fusion-Task-Lineage: b540f985-c20f-438e-8290-a07531c6612c
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 10:01:32 -07:00
committed by gsxdsm
parent e9933cfa94
commit 2a802ab4c8
4 changed files with 160 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, vi, type Mock } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { TaskStore } from "@fusion/core";
import { GitHubTrackingReconciler } from "../github-tracking-reconciler.js";
import { GitHubTrackingReconciler, RECONCILE_CONCURRENCY_LIMIT } from "../github-tracking-reconciler.js";
const { mockGetIssue, mockSetIssueState } = vi.hoisted(() => ({
mockGetIssue: vi.fn(),
@@ -32,23 +32,82 @@ function createStore(tasks: Array<Record<string, unknown>>): TaskStore {
}
describe("GitHubTrackingReconciler", () => {
it("closes already-done tasks whose linked issue is still open", async () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("closes open issues for done tracked tasks", async () => {
mockResolveGithubTrackingAuth.mockReturnValue({ ok: true, auth: { mode: "token", token: "ghp_test" } });
mockGetIssue.mockResolvedValue({ state: "open" });
const store = createStore([{ id: "FN-1", status: "done", githubTracking: { enabled: true, issue: { owner: "o", repo: "r", number: 1 } } }]);
const result = await new GitHubTrackingReconciler().reconcile(store);
expect(mockSetIssueState).toHaveBeenCalledWith("o", "r", 1, "closed", "completed");
expect(result.closed).toBe(1);
});
it("skips closed issues and invalid tracking tasks", async () => {
mockResolveGithubTrackingAuth.mockReturnValue({ ok: true, auth: { mode: "token", token: "ghp_test" } });
mockGetIssue.mockResolvedValue({ state: "closed" });
const store = createStore([
{
id: "FN-1",
status: "done",
githubTracking: {
enabled: true,
issue: { owner: "owner", repo: "repo", number: 42 },
},
},
{ id: "FN-1", status: "done", githubTracking: { enabled: true, issue: { owner: "o", repo: "r", number: 1 } } },
{ id: "FN-2", status: "done", githubTracking: { enabled: false, issue: { owner: "o", repo: "r", number: 2 } } },
{ id: "FN-3", status: "done", githubTracking: { enabled: true, issue: { owner: "o", repo: "", number: 3 } } },
{ id: "FN-4", status: "todo", githubTracking: { enabled: true, issue: { owner: "o", repo: "r", number: 4 } } },
]);
const reconciler = new GitHubTrackingReconciler();
await reconciler.reconcile(store);
const result = await new GitHubTrackingReconciler().reconcile(store);
expect(mockSetIssueState).toHaveBeenCalledWith("owner", "repo", 42, "closed", "completed");
expect(result.closed).toBe(0);
expect(result.skipped).toBe(3);
expect(mockSetIssueState).not.toHaveBeenCalled();
});
it("logs and continues on per-issue errors", async () => {
mockResolveGithubTrackingAuth.mockReturnValue({ ok: true, auth: { mode: "token", token: "ghp_test" } });
mockGetIssue.mockRejectedValueOnce(new Error("boom"));
mockGetIssue.mockResolvedValueOnce({ state: "open" });
const store = createStore([
{ id: "FN-1", status: "done", githubTracking: { enabled: true, issue: { owner: "o", repo: "r", number: 1 } } },
{ id: "FN-2", status: "done", githubTracking: { enabled: true, issue: { owner: "o", repo: "r", number: 2 } } },
]);
const result = await new GitHubTrackingReconciler().reconcile(store);
expect(result.errors).toBe(1);
expect(result.closed).toBe(1);
expect((store.logEntry as any)).toHaveBeenCalledWith("FN-1", "Failed to reconcile GitHub tracking issue", "boom");
});
it("skips and logs when auth is unavailable", async () => {
mockResolveGithubTrackingAuth.mockReturnValue({ ok: false, message: "no auth" });
const store = createStore([{ id: "FN-1", status: "done", githubTracking: { enabled: true, issue: { owner: "o", repo: "r", number: 1 } } }]);
const result = await new GitHubTrackingReconciler().reconcile(store);
expect(result.skipped).toBe(1);
expect((store.logEntry as any)).toHaveBeenCalledWith("FN-1", "Skipped GitHub tracking issue reconciliation", "no auth");
});
it("respects concurrency cap", async () => {
mockResolveGithubTrackingAuth.mockReturnValue({ ok: true, auth: { mode: "token", token: "ghp_test" } });
let inFlight = 0;
let maxInFlight = 0;
mockGetIssue.mockImplementation(async () => {
inFlight += 1;
maxInFlight = Math.max(maxInFlight, inFlight);
await new Promise((resolve) => setTimeout(resolve, 10));
inFlight -= 1;
return { state: "closed" };
});
const tasks = Array.from({ length: 10 }, (_, i) => ({
id: `FN-${i + 1}`,
status: "done",
githubTracking: { enabled: true, issue: { owner: "o", repo: "r", number: i + 1 } },
}));
await new GitHubTrackingReconciler().reconcile(createStore(tasks));
expect(maxInFlight).toBeLessThanOrEqual(RECONCILE_CONCURRENCY_LIMIT);
});
});