{"version":3,"file":"todo-engine.test.d.ts","sourceRoot":"","sources":["../../../src/core/todo/todo-engine.test.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Unit tests for the durable TODO engine:\n *   revision/hash, deterministic rebase, idempotency, status transitions,\n *   and progress-aware loop detection.\n */\nimport { describe, expect, it } from \"vitest\";\nimport {\n\tallowedTransitions,\n\tcomputeStateHash,\n\thashIntent,\n\tTodoEngine,\n\ttype TodoEngineState,\n\tvalidateTransition,\n} from \"./todo-engine.js\";\n\nconst item = (id: string, content = id, status: \"pending\" | \"in_progress\" | \"completed\" = \"pending\") => ({\n\tid,\n\tcontent,\n\tactiveForm: `Working on ${content}`,\n\tstatus,\n});\n\ndescribe(\"state hash and revision\", () => {\n\tit(\"computeStateHash is deterministic and order-independent\", () => {\n\t\tconst a = [item(\"a\"), item(\"b\")];\n\t\tconst b = [item(\"b\"), item(\"a\")];\n\t\texpect(computeStateHash(a)).toBe(computeStateHash(b));\n\t\texpect(computeStateHash([item(\"a\"), item(\"b\")])).not.toBe(computeStateHash([item(\"a\"), item(\"c\")]));\n\t});\n\n\tit(\"same hash implies same content; different content -> different hash\", () => {\n\t\texpect(computeStateHash([item(\"a\")])).toBe(computeStateHash([item(\"a\")]));\n\t\texpect(computeStateHash([item(\"a\")])).not.toBe(computeStateHash([item(\"a\", \"other\")]));\n\t});\n});\n\ndescribe(\"status transitions\", () => {\n\tit(\"allows forward transitions and forbids terminal reopen\", () => {\n\t\texpect(validateTransition(\"pending\", \"in_progress\").ok).toBe(true);\n\t\texpect(validateTransition(\"in_progress\", \"completed\").ok).toBe(true);\n\t\texpect(validateTransition(\"completed\", \"pending\").ok).toBe(false);\n\t\texpect(validateTransition(\"cancelled\", \"in_progress\").ok).toBe(false);\n\t});\n\n\tit(\"repeating current status is idempotent\", () => {\n\t\texpect(validateTransition(\"completed\", \"completed\").ok).toBe(true);\n\t\texpect(validateTransition(\"pending\", \"pending\").ok).toBe(true);\n\t});\n\n\tit(\"exposes allowed transitions\", () => {\n\t\texpect(allowedTransitions(\"pending\").has(\"blocked\")).toBe(true);\n\t\texpect(allowedTransitions(\"completed\").has(\"pending\")).toBe(false);\n\t});\n});\n\ndescribe(\"deterministic rebase\", () => {\n\tconst engine = () => new TodoEngine(\"s\");\n\n\tit(\"rebases when the intent targets a different item than the concurrent change\", () => {\n\t\tconst e = engine();\n\t\tconst base = [item(\"a\"), item(\"b\")];\n\t\te.recordReadSnapshot(1, base);\n\t\t// concurrent: item b changed\n\t\tconst current = [item(\"a\"), { ...item(\"b\"), content: \"b2\" }];\n\t\tconst r = e.rebase(1, 2, current, [{ id: \"a\", status: \"completed\" }]);\n\t\texpect(r.status).toBe(\"rebased\");\n\t\texpect(r.conflictItemIds).toEqual([]);\n\t\texpect(r.preservedConcurrentChanges).toEqual([]);\n\t});\n\n\tit(\"rebases when a new item was created concurrently\", () => {\n\t\tconst e = engine();\n\t\tconst base = [item(\"a\")];\n\t\te.recordReadSnapshot(1, base);\n\t\tconst current = [item(\"a\"), item(\"z\")];\n\t\tconst r = e.rebase(1, 2, current, [{ id: \"a\", status: \"completed\" }]);\n\t\texpect(r.status).toBe(\"rebased\");\n\t\texpect(r.conflictItemIds).toEqual([]);\n\t});\n\n\tit(\"treats repeated completion as idempotent\", () => {\n\t\tconst e = engine();\n\t\tconst base = [item(\"a\", \"A\", \"completed\")];\n\t\te.recordReadSnapshot(1, base);\n\t\tconst current = [item(\"a\", \"A\", \"completed\")];\n\t\tconst r = e.rebase(1, 1, current, [{ id: \"a\", status: \"completed\" }]);\n\t\texpect(r.status).toBe(\"already_applied\");\n\t\texpect(r.conflictItemIds).toEqual([]);\n\t});\n\n\tit(\"conflicts when the same content was edited differently\", () => {\n\t\tconst e = engine();\n\t\tconst base = [item(\"a\", \"original\")];\n\t\te.recordReadSnapshot(1, base);\n\t\tconst current = [item(\"a\", \"concurrent-edit\")];\n\t\tconst r = e.rebase(1, 2, current, [{ id: \"a\", content: \"model-edit\" }]);\n\t\texpect(r.status).toBe(\"conflict\");\n\t\texpect(r.conflictItemIds).toContain(\"a\");\n\t});\n\n\tit(\"conflicts when the item was removed concurrently\", () => {\n\t\tconst e = engine();\n\t\tconst base = [item(\"a\"), item(\"b\")];\n\t\te.recordReadSnapshot(1, base);\n\t\tconst current = [item(\"b\")];\n\t\tconst r = e.rebase(1, 2, current, [{ id: \"a\", status: \"completed\" }]);\n\t\texpect(r.status).toBe(\"conflict\");\n\t\texpect(r.conflictItemIds).toContain(\"a\");\n\t});\n\n\tit(\"conflicts when reopening a terminal item\", () => {\n\t\tconst e = engine();\n\t\tconst base = [item(\"a\", \"A\", \"completed\")];\n\t\te.recordReadSnapshot(1, base);\n\t\tconst current = [item(\"a\", \"A\", \"completed\")];\n\t\tconst r = e.rebase(1, 1, current, [{ id: \"a\", status: \"in_progress\" }]);\n\t\texpect(r.status).toBe(\"conflict\");\n\t\texpect(r.conflictItemIds).toContain(\"a\");\n\t});\n});\n\ndescribe(\"idempotency\", () => {\n\tit(\"records and detects an exact retry\", () => {\n\t\tconst e = new TodoEngine(\"s\");\n\t\tconst key = `${e.scopeId}|${hashIntent([{ id: \"a\", status: \"completed\" }])}|3`;\n\t\texpect(e.lookupApplied(key)).toBe(false);\n\t\te.recordApplied(key, 4);\n\t\texpect(e.lookupApplied(key)).toBe(true);\n\t});\n\n\tit(\"is bounded (does not grow unboundedly)\", () => {\n\t\tconst e = new TodoEngine(\"s\", { maxLedger: 3 });\n\t\tfor (let i = 0; i < 10; i++) {\n\t\t\te.recordApplied(`k${i}`, i);\n\t\t}\n\t\texpect(e.getDiagnostics().ledgerCount).toBeLessThanOrEqual(3);\n\t});\n\n\tit(\"restores applied intents and snapshots after a process restart\", () => {\n\t\tlet durableState: TodoEngineState | undefined;\n\t\tconst persistence = {\n\t\t\tload: () => durableState,\n\t\t\tsave: (state: TodoEngineState) => {\n\t\t\t\tdurableState = state;\n\t\t\t},\n\t\t};\n\t\tconst first = new TodoEngine(\"s\", {}, Date.now, persistence);\n\t\tfirst.recordState(4, [item(\"a\")]);\n\t\tfirst.recordApplied(\"intent-a\", 5);\n\n\t\tconst resumed = new TodoEngine(\"s\", {}, Date.now, persistence);\n\t\texpect(resumed.getCurrentRevision()).toBe(5);\n\t\texpect(resumed.lookupApplied(\"intent-a\")).toBe(true);\n\t\texpect(resumed.rebase(4, 5, [item(\"a\")], [{ id: \"a\", status: \"completed\" }]).status).toBe(\"rebased\");\n\t});\n});\n\ndescribe(\"progress-aware loop detection\", () => {\n\tit(\"blocks after 3 identical consecutive no-progress failures\", () => {\n\t\tconst e = new TodoEngine(\"s\", { maxNoProgressFailures: 3 });\n\t\tconst fp = { scopeId: \"s\", errorCode: \"TODO_X\", intentHash: \"h\", requestedRevision: 1, currentRevision: 1 };\n\t\texpect(e.registerFailure(fp).blocked).toBe(false);\n\t\texpect(e.registerFailure(fp).blocked).toBe(false);\n\t\tconst third = e.registerFailure(fp);\n\t\texpect(third.blocked).toBe(true);\n\t\texpect(third.consecutive).toBe(3);\n\t});\n\n\tit(\"intervening progress resets the chain\", () => {\n\t\tconst e = new TodoEngine(\"s\", { maxNoProgressFailures: 3 });\n\t\tconst fp = { scopeId: \"s\", errorCode: \"TODO_X\", intentHash: \"h\", requestedRevision: 1, currentRevision: 1 };\n\t\te.registerFailure(fp);\n\t\te.registerFailure(fp);\n\t\te.recordProgress();\n\t\tconst again = e.registerFailure(fp);\n\t\texpect(again.blocked).toBe(false);\n\t\texpect(again.consecutive).toBe(1);\n\t});\n\n\tit(\"a newer todo read resets the chain\", () => {\n\t\tconst e = new TodoEngine(\"s\");\n\t\tconst fp = { scopeId: \"s\", errorCode: \"TODO_X\", intentHash: \"h\", requestedRevision: 1, currentRevision: 1 };\n\t\te.registerFailure(fp);\n\t\te.registerFailure(fp);\n\t\te.recordTodoRead(2); // newer revision -> progress\n\t\texpect(e.registerFailure(fp).consecutive).toBe(1);\n\t});\n\n\tit(\"an identical (non-newer) todo read does NOT reset the chain\", () => {\n\t\tconst e = new TodoEngine(\"s\");\n\t\te.recordTodoRead(1); // baseline read at revision 1\n\t\tconst fp = { scopeId: \"s\", errorCode: \"TODO_X\", intentHash: \"h\", requestedRevision: 1, currentRevision: 1 };\n\t\te.registerFailure(fp);\n\t\te.registerFailure(fp);\n\t\te.recordTodoRead(1); // same revision as baseline -> no progress\n\t\texpect(e.registerFailure(fp).consecutive).toBe(3);\n\t});\n});\n"]}