diff --git a/src/engine/engine.test.ts b/src/engine/engine.test.ts index 3ac0b42..0fa1b2f 100644 --- a/src/engine/engine.test.ts +++ b/src/engine/engine.test.ts @@ -396,6 +396,10 @@ describe("cross-sheet references", () => { expect(r[0].value).toBeNull(); expect(r[0].error).toBe('no variable "nope" in "Budget"'); }); + it("ordinary successful line leaves LineResult.error undefined", () => { + const r = eng.evaluateDocument("$100", resolver); + expect(r[0].error).toBeUndefined(); + }); it("propagates a resolver-reported circular reference", () => { const cyc = (): XRefResolution => ({ ok: false, reason: "circular reference" }); const r = eng.evaluateDocument("@A.total", cyc); diff --git a/src/workspace.test.ts b/src/workspace.test.ts index 8b4255a..31c6fd1 100644 --- a/src/workspace.test.ts +++ b/src/workspace.test.ts @@ -82,6 +82,20 @@ describe("Workspace cycle detection", () => { expect(r[1].value).toBeNull(); expect(r[1].error).toBe("circular reference"); }); + + it("a two-sheet cycle reports a stable error across repeated queries (no stale cache poisoning)", () => { + const { ws } = makeWorkspace({ + a: { title: "A", text: "z = @B.total" }, + b: { title: "B", text: "z = @A.total" }, + }); + const first = ws.evaluateSheet("dash", "@A.z"); + const second = ws.evaluateSheet("dash", "@A.z"); + expect(first[0].value).toBeNull(); + expect(second[0].value).toBeNull(); + expect(first[0].error).toBeDefined(); + expect(second[0].error).toBeDefined(); + expect(second[0].error).toBe(first[0].error); + }); }); describe("Workspace.renameSheet", () => { diff --git a/src/workspace.ts b/src/workspace.ts index 45c726e..c387051 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -138,7 +138,12 @@ export class Workspace { } } const exports: SheetExports = { vars, total: this.engine.totalValueOf(results), last }; - this.cache.set(sheet.id, exports); + // A cycle-interrupted pass produces incomplete/wrong exports (dropped + // assignments, missing total) — don't let it poison the cache. Re-evaluate + // fresh on every query until the cycle is actually broken by an edit. + if (!results.some((r) => r.error === "circular reference")) { + this.cache.set(sheet.id, exports); + } return exports; } }