FN-5792: fix GitHub auto-close reconciliation for soft-deleted tasks

Ensure GitHub issue auto-close reconciliation still processes soft-deleted and archived tracked tasks after restart.

- Add pagination-aware reconcile scanning in core store with combined deleted+archived coverage and hasMore metadata.
- Update dashboard GitHub tracking reconciler and route handling to consume paged reconcile batches safely.
- Expand reconcile tests (core + dashboard) with restart/periodic sweep and source-issue scenarios to lock behavior.
- Add release changeset and dashboard guide note for the reconciliation sweep behavior.

Files changed:
 .changeset/fn-5792-github-tracking-sweep.md        |   5 +
 docs/dashboard-guide.md                            |   1 +
 .../store-github-tracking-reconcile.test.ts        |  45 ++++++++-
 packages/core/src/store.ts                         |  32 ++++--
 .../github-source-issue-reconciler.test.ts         |  22 ++--
 ...ithub-tracking-periodic-reconcile-sweep.test.ts |  90 +++++++++++++++++
 .../__tests__/github-tracking-reconciler.test.ts   | 111 +++++----------------
 .../dashboard/src/github-tracking-reconciler.ts    |  19 ++--
 .../dashboard/src/routes/register-git-github.ts    |  57 +++++++++--
 9 files changed, 257 insertions(+), 125 deletions(-)

Fusion-Task-Id: FN-5792

Fusion-Task-Lineage: 259f07cf-3470-47c7-b47a-86d212d44d52
This commit is contained in:
gsxdsm
2026-05-31 17:23:26 -07:00
parent 716f3964c5
commit ffadb0c77f
9 changed files with 257 additions and 125 deletions

View File

@@ -64,7 +64,7 @@ describe("TaskStore.listTasksForGithubTrackingReconcile", () => {
const softDeletedWithoutTracking = await store.createTask({ description: "soft deleted no tracking" });
await store.deleteTask(softDeletedWithoutTracking.id);
const tasks = await store.listTasksForGithubTrackingReconcile();
const { tasks, hasMore } = await store.listTasksForGithubTrackingReconcile();
const byId = new Map(tasks.map((task) => [task.id, task]));
expect(byId.has(softDeleted.id)).toBe(true);
@@ -76,13 +76,52 @@ describe("TaskStore.listTasksForGithubTrackingReconcile", () => {
expect(byId.has(activeTracked.id)).toBe(false);
expect(byId.has(softDeletedWithoutTracking.id)).toBe(false);
expect(hasMore).toBe(false);
});
it("returns empty results when nothing matches", async () => {
const task = await store.createTask({ description: "no tracking" });
await store.moveTask(task.id, "todo");
const tasks = await store.listTasksForGithubTrackingReconcile();
expect(tasks).toEqual([]);
const result = await store.listTasksForGithubTrackingReconcile();
expect(result).toEqual({ tasks: [], hasMore: false });
});
it("paginates across soft-deleted and archived tracked entries", async () => {
for (let i = 0; i < 3; i += 1) {
const task = await store.createTask({ description: `deleted ${i}` });
await store.updateGithubTracking(task.id, { enabled: true });
await store.deleteTask(task.id);
}
for (let i = 0; i < 3; i += 1) {
const task = await store.createTask({ description: `archived ${i}` });
await store.updateGithubTracking(task.id, { enabled: true });
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
await store.moveTask(task.id, "in-review");
await store.moveTask(task.id, "done");
await store.archiveTask(task.id);
}
const page1 = await store.listTasksForGithubTrackingReconcile({ offset: 0, limit: 2 });
const page2 = await store.listTasksForGithubTrackingReconcile({ offset: 2, limit: 2 });
const page3 = await store.listTasksForGithubTrackingReconcile({ offset: 4, limit: 2 });
expect(page1.tasks).toHaveLength(2);
expect(page2.tasks).toHaveLength(2);
expect(page3.tasks).toHaveLength(2);
const seen = new Set([...page1.tasks, ...page2.tasks, ...page3.tasks].map((task) => task.id));
expect(seen.size).toBe(6);
expect(page1.hasMore).toBe(true);
expect(page2.hasMore).toBe(true);
expect(page3.hasMore).toBe(false);
const activeTracked = await store.createTask({ description: "active tracked no reconcile" });
await store.updateGithubTracking(activeTracked.id, { enabled: true });
const finalPage = await store.listTasksForGithubTrackingReconcile({ offset: 0, limit: 20 });
expect(finalPage.tasks.some((task) => task.id === activeTracked.id)).toBe(false);
});
});