211 lines
7.3 KiB
TypeScript
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"]);
|
|
});
|
|
});
|