From 70c6c729be03b75df70492d1dd4a35df055a0cc2 Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Tue, 25 Aug 2026 20:28:27 -0500 Subject: [PATCH] daemon: healing a branch forces the refresh through enrichBranches #103 taught branch:enrich to treat a ticket-less entry as a miss, but enrichBranches carries the same short-circuit one level down -- allCached is keyed on the branch merely being present in the store -- so the heal re-entered the enricher and got the same incomplete entry back without ever reaching the ticket lookup. Healing now passes forceRefresh. Co-Authored-By: Claude Fable 5 --- lib/daemon/__tests__/branch-enrich-heal.test.ts | 16 ++++++++++++++++ lib/daemon/handlers/cache.ts | 14 +++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/daemon/__tests__/branch-enrich-heal.test.ts b/lib/daemon/__tests__/branch-enrich-heal.test.ts index 8061215a..b9e962f0 100644 --- a/lib/daemon/__tests__/branch-enrich-heal.test.ts +++ b/lib/daemon/__tests__/branch-enrich-heal.test.ts @@ -26,6 +26,22 @@ describe("branch:enrich heals an entry whose ticket never resolved", () => { expect(res.source).toBe("cache"); }); + test("healing forces a refresh, since enrichBranches also short-circuits on a cached branch", async () => { + let opts: any = null; + const ctx = makeCtx({ + b: { linearId: "ACME-1", ticket: null, mr: null, fetchedAt: Date.now() - HOUR }, + }); + await createCacheHandlers(ctx)["branch:enrich"]!({ + branch: "b", + repoPath: "/tmp/x", + enrich: async (_b: unknown, _r: unknown, o: unknown) => { + opts = o; + ctx.cache.entries.b.ticket = { identifier: "ACME-1" }; + }, + }); + expect(opts?.forceRefresh).toBe(true); + }); + test("an id resolved but no ticket is INCOMPLETE, and re-enriches", async () => { const ctx = makeCtx({ b: { linearId: "ACME-1", ticket: null, mr: null, fetchedAt: Date.now() - HOUR }, diff --git a/lib/daemon/handlers/cache.ts b/lib/daemon/handlers/cache.ts index 95e382b9..1823b702 100644 --- a/lib/daemon/handlers/cache.ts +++ b/lib/daemon/handlers/cache.ts @@ -70,11 +70,14 @@ export function createCacheHandlers(ctx: HandlerContext): HandlerMap { const repoPath = payload?.repoPath as string; const remoteUrl = payload?.remoteUrl as string | undefined; // Test seam: the enricher, so a test never reaches Linear or the forge. - const inject = payload?.enrich as (() => Promise) | undefined; + const inject = payload?.enrich as + | ((b: unknown, r: unknown, o: unknown) => Promise) + | undefined; if (!branch) return { ok: false, error: "missing branch" }; const cached = ctx.cache.entries[branch]; + const healing = !!cached; if (cached && !isIncomplete(cached)) { return { ok: true, data: cached, source: "cache" }; } @@ -82,11 +85,16 @@ export function createCacheHandlers(ctx: HandlerContext): HandlerMap { if (!repoPath) return { ok: false, error: "missing repoPath for cold enrichment" }; try { + // `forceRefresh` is load-bearing when healing: enrichBranches has its + // own all-cached short-circuit keyed on mere presence, so without it + // a re-enrich of a branch already in the store returns the same + // incomplete entry and never reaches the ticket lookup. + const opts = { silent: true, forceRefresh: healing }; if (inject) { - await inject(); + await inject([{ path: repoPath, branch }], remoteUrl, opts); } else { const { enrichBranches } = await import("../../enrich.ts"); - await enrichBranches([{ path: repoPath, branch }], remoteUrl, { silent: true }); + await enrichBranches([{ path: repoPath, branch }], remoteUrl, opts); } // enrichBranches wrote through the same singleton store in this