NODEDC_1C/llm_normalizer/backend/tests/lifecycleRegistryResolverWa...

211 lines
7.3 KiB
TypeScript

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"]);
});
});