import { describe, expect, it } from "vitest"; import type { CandidateEvidenceItem, ProblemUnit } from "../src/types/stage2ProblemUnits"; import { LifecycleRegistry, resolveLifecycle } from "../src/services/lifecycleRuntime"; function buildProblemUnit(input: { id: string; type?: ProblemUnit["problem_unit_type"]; mechanismSummary?: string; businessDefectClass?: string; accounts?: string[]; actualState?: string; expectedState?: string; periodCloseRisk?: boolean; }): ProblemUnit { return { schema_version: "problem_unit_v0_1", problem_unit_id: input.id, problem_unit_type: input.type ?? "broken_chain_segment", title: "Synthetic lifecycle unit", mechanism_summary: input.mechanismSummary ?? "Synthetic lifecycle mechanism", business_defect_class: input.businessDefectClass ?? "broken_lifecycle", severity: { score: 0.64, grade: "medium" }, confidence: { score: 0.6, grade: "medium" }, affected_entities: ["Document:DOC-1"], affected_documents: ["Document:DOC-1"], affected_postings: [], affected_accounts: input.accounts ?? ["51"], affected_counterparties: [], affected_contracts: [], ...(input.actualState ? { actual_state: input.actualState } : {}), ...(input.expectedState ? { expected_state: input.expectedState } : {}), ...(input.periodCloseRisk ? { period_impact: { is_period_sensitive: true, impact_class: "close_risk" as const } } : {}), evidence_pack: ["cand-1"], entity_backlinks: [{ entity: "Document", id: "DOC-1" }], snapshot_limitations: [] }; } function buildCandidate(input: { id: string; anomalies?: string[]; relations?: string[]; confidence?: "high" | "medium" | "low"; }): CandidateEvidenceItem { return { schema_version: "candidate_evidence_v0_1", candidate_id: input.id, route: "hybrid_store_plus_live", source_ref: { schema_version: "evidence_source_ref_v1", namespace: "snapshot_2020", entity: "Document", id: "DOC-1", period: "2020-06", canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-06" }, relation_pattern_hits: input.relations ?? [], anomaly_patterns: input.anomalies ?? [], entity_backlinks: [{ entity: "Document", id: "DOC-1" }], confidence_hint: input.confidence ?? "medium" }; } describe("stage3 lifecycle registry and resolver wave2", () => { it("exposes all lifecycle domains in the registry", () => { const domains = LifecycleRegistry.listDomains(); expect(domains).toEqual([ "bank_settlement", "customer_settlement", "deferred_expense", "fixed_asset", "vat_flow", "period_close" ]); for (const domain of domains) { const model = LifecycleRegistry.getDomain(domain); expect(model.lifecycle_domain).toBe(domain); expect(model.states.length).toBeGreaterThan(0); expect(model.defects.some((definition) => definition.defect_code === "stale_active_state")).toBe(true); } }); it("infers lifecycle domain for all covered stage3 domains", () => { const cases: Array<{ name: string; unit: ProblemUnit; candidates: CandidateEvidenceItem[]; expectedDomain: string; }> = [ { name: "bank settlement", unit: buildProblemUnit({ id: "domain-bank", accounts: ["51"], mechanismSummary: "bank settlement reconciliation" }), candidates: [buildCandidate({ id: "cand-bank", relations: ["payment_to_settlement"] })], expectedDomain: "bank_settlement" }, { name: "customer settlement", unit: buildProblemUnit({ id: "domain-customer", accounts: ["62"], mechanismSummary: "customer receivable chain" }), candidates: [buildCandidate({ id: "cand-customer", relations: ["settlement_to_invoice"] })], expectedDomain: "customer_settlement" }, { name: "deferred expense", unit: buildProblemUnit({ id: "domain-97", accounts: ["97"], mechanismSummary: "deferred writeoff path" }), candidates: [buildCandidate({ id: "cand-97", relations: ["deferred_writeoff"] })], expectedDomain: "deferred_expense" }, { name: "fixed asset", unit: buildProblemUnit({ id: "domain-os", accounts: ["01"], mechanismSummary: "fixed asset depreciation" }), candidates: [buildCandidate({ id: "cand-os", relations: ["depreciation_register_movement"] })], expectedDomain: "fixed_asset" }, { name: "vat flow", unit: buildProblemUnit({ id: "domain-vat", accounts: ["68"], mechanismSummary: "vat deduction chain" }), candidates: [buildCandidate({ id: "cand-vat", anomalies: ["cross_branch_inconsistency"] })], expectedDomain: "vat_flow" }, { name: "period close", unit: buildProblemUnit({ id: "domain-close", type: "period_risk_cluster", mechanismSummary: "period close blocker", periodCloseRisk: true }), candidates: [buildCandidate({ id: "cand-close", anomalies: ["period_close_risk"] })], expectedDomain: "period_close" } ]; for (const item of cases) { const resolution = resolveLifecycle({ unit: item.unit, candidates: item.candidates }); expect(resolution.lifecycle_domain, item.name).toBe(item.expectedDomain); } }); it("normalizes unknown explicit states against registry and records limitations", () => { const resolution = resolveLifecycle({ unit: buildProblemUnit({ id: "normalize-invalid-states", accounts: ["01"], mechanismSummary: "fixed asset depreciation", actualState: "legacy_state_unmapped", expectedState: "legacy_target_unmapped" }), candidates: [buildCandidate({ id: "cand-normalize", relations: ["depreciation_register_movement"] })] }); expect(resolution.lifecycle_domain).toBe("fixed_asset"); expect(resolution.resolved_current_state).toBe("depreciation_active"); expect(resolution.resolved_expected_state).toBe("disposed"); expect(resolution.snapshot_limitations).toContain("actual_state_not_in_registry_normalized"); expect(resolution.snapshot_limitations).toContain("expected_state_not_in_registry_normalized"); }); it("infers missing transition from registry transition path", () => { const resolution = resolveLifecycle({ unit: buildProblemUnit({ id: "missing-transition", accounts: ["51"], actualState: "bank_recorded", expectedState: "settlement_closed" }), candidates: [buildCandidate({ id: "cand-missing", anomalies: ["missing_link", "no_continuation"] })] }); expect(resolution.missing_transitions[0]).toBe("bank_recorded->settlement_closed"); }); it("builds previous state chain from registry model", () => { const resolution = resolveLifecycle({ unit: buildProblemUnit({ id: "previous-chain", accounts: ["51"], actualState: "bank_recorded", expectedState: "settlement_closed" }), candidates: [buildCandidate({ id: "cand-prev", relations: ["payment_to_settlement"] })] }); expect(resolution.resolved_previous_states).toEqual(["initiated_payment"]); }); });