#include #include #include "types.hpp" using namespace ucwm; using nlohmann::json; // ───────────────────────────────────────────────────────────────────────────── // CanonicalObject // ───────────────────────────────────────────────────────────────────────────── TEST_CASE("CanonicalObject — construction and field access", "[types][object]") { CanonicalObject obj; obj.object_id = "E1"; obj.object_kind = ObjectKind::Entity; obj.status = ObjectStatus::Active; obj.confidence = 0.97; obj.canonical_label = "Alice"; obj.aliases = {"she", "her"}; obj.surface_span = SurfaceSpan{"Alice", 0, 5}; obj.source_module = "entity_specialist"; REQUIRE(obj.object_id == "E1"); REQUIRE(obj.object_kind == ObjectKind::Entity); REQUIRE(obj.status == ObjectStatus::Active); REQUIRE_THAT(obj.confidence, Catch::Matchers::WithinAbs(0.97, 1e-9)); REQUIRE(obj.canonical_label.has_value()); REQUIRE(*obj.canonical_label == "Alice"); REQUIRE(obj.aliases.size() == 2); REQUIRE(obj.surface_span.has_value()); REQUIRE(obj.surface_span->start == 0); REQUIRE(obj.surface_span->end == 5); REQUIRE_FALSE(obj.merged_into.has_value()); REQUIRE(obj.facet_refs.empty()); } TEST_CASE("CanonicalObject — absent optionals stay absent", "[types][object]") { CanonicalObject obj; obj.object_id = "E2"; obj.object_kind = ObjectKind::Event; obj.confidence = 0.5; REQUIRE_FALSE(obj.surface_span.has_value()); REQUIRE_FALSE(obj.canonical_label.has_value()); REQUIRE_FALSE(obj.merged_into.has_value()); REQUIRE_FALSE(obj.split_from.has_value()); REQUIRE_FALSE(obj.source_module.has_value()); } TEST_CASE("CanonicalObject — JSON round-trip", "[types][object][json]") { CanonicalObject original; original.object_id = "E1"; original.object_kind = ObjectKind::Entity; original.status = ObjectStatus::Active; original.confidence = 0.97; original.canonical_label = "Alice"; original.aliases = {"she", "her"}; original.surface_span = SurfaceSpan{"Alice", 0, 5}; original.facet_refs = {"F1", "F2"}; original.source_module = "entity_specialist"; json j = original; CanonicalObject restored = j.get(); REQUIRE(restored.object_id == original.object_id); REQUIRE(restored.object_kind == original.object_kind); REQUIRE(restored.status == original.status); REQUIRE_THAT(restored.confidence, Catch::Matchers::WithinAbs(original.confidence, 1e-9)); REQUIRE(restored.canonical_label == original.canonical_label); REQUIRE(restored.aliases == original.aliases); REQUIRE(restored.surface_span.has_value()); REQUIRE(restored.surface_span->text == "Alice"); REQUIRE(restored.facet_refs == original.facet_refs); REQUIRE(restored.source_module == original.source_module); } TEST_CASE("CanonicalObject — absent optional not written to JSON", "[types][object][json]") { CanonicalObject obj; obj.object_id = "E3"; obj.object_kind = ObjectKind::Quantity; obj.confidence = 0.99; json j = obj; REQUIRE_FALSE(j.contains("surface_span")); REQUIRE_FALSE(j.contains("canonical_label")); REQUIRE_FALSE(j.contains("merged_into")); } // ───────────────────────────────────────────────────────────────────────────── // Constraint // ───────────────────────────────────────────────────────────────────────────── TEST_CASE("Constraint — construction and field access", "[types][constraint]") { Constraint c; c.constraint_id = "C1"; c.constraint_type = "quantity_difference"; c.argument_refs = {"Q_result", "Q1", "Q2"}; c.polarity = Polarity::Positive; c.strength = ConstraintStrength::Hard; c.status = ConstraintStatus::Unresolved; c.expression = "quantity(Q_result) = quantity(Q1) - quantity(Q2)"; c.confidence = 0.99; c.source_module = "quantity_specialist"; REQUIRE(c.constraint_id == "C1"); REQUIRE(c.constraint_type == "quantity_difference"); REQUIRE(c.argument_refs.size() == 3); REQUIRE(c.strength == ConstraintStrength::Hard); REQUIRE(c.status == ConstraintStatus::Unresolved); REQUIRE(c.expression.has_value()); REQUIRE(c.confidence.has_value()); REQUIRE_FALSE(c.probability.has_value()); } TEST_CASE("Constraint — JSON round-trip", "[types][constraint][json]") { Constraint original; original.constraint_id = "C3"; original.constraint_type = "before"; original.argument_refs = {"EV1", "EV2"}; original.polarity = Polarity::Positive; original.strength = ConstraintStrength::Soft; original.status = ConstraintStatus::Resolved; original.source_module = "temporal_specialist"; original.expression = "before(EV1, EV2)"; json j = original; Constraint restored = j.get(); REQUIRE(restored.constraint_id == original.constraint_id); REQUIRE(restored.constraint_type == original.constraint_type); REQUIRE(restored.argument_refs == original.argument_refs); REQUIRE(restored.polarity == original.polarity); REQUIRE(restored.strength == original.strength); REQUIRE(restored.status == original.status); REQUIRE(restored.expression == original.expression); } // ───────────────────────────────────────────────────────────────────────────── // Facets and FacetData variant // ───────────────────────────────────────────────────────────────────────────── TEST_CASE("Facet — TemporalFacetData construction and kind detection", "[types][facet]") { TemporalFacetData data; data.time_point = "t0"; data.temporal_order_refs = {"C_temporal"}; data.is_recurring = false; Facet f; f.facet_id = "F_EV_temporal"; f.object_ref = "EV_initial"; f.confidence = 0.90; f.source_module = "temporal_specialist"; f.data = data; REQUIRE(kind_of(f.data) == FacetKind::Temporal); REQUIRE(std::holds_alternative(f.data)); auto& d = std::get(f.data); REQUIRE(d.time_point.has_value()); REQUIRE(*d.time_point == "t0"); REQUIRE(d.temporal_order_refs.size() == 1); REQUIRE_FALSE(d.is_recurring); } TEST_CASE("Facet — QuantityFacetData with numeric value", "[types][facet]") { QuantityFacetData data; data.value = 5.0; data.unit = "apples"; data.is_exact = true; Facet f; f.facet_id = "F_Q1"; f.object_ref = "Q1"; f.confidence = 0.99; f.source_module = "quantity_specialist"; f.data = data; REQUIRE(kind_of(f.data) == FacetKind::Quantity); auto& d = std::get(f.data); REQUIRE(d.value.has_value()); REQUIRE(std::holds_alternative(*d.value)); REQUIRE_THAT(std::get(*d.value), Catch::Matchers::WithinAbs(5.0, 1e-9)); REQUIRE(*d.unit == "apples"); REQUIRE(d.is_exact); } TEST_CASE("Facet — QuantityFacetData with symbolic value", "[types][facet]") { QuantityFacetData data; data.value = std::string{"Q1 - Q2"}; data.unit = "apples"; data.is_exact = false; Facet f; f.facet_id = "F_Q_final"; f.object_ref = "Q_alice_final"; f.confidence = 0.90; f.source_module = "quantity_specialist"; f.data = data; auto& d = std::get(f.data); REQUIRE(std::holds_alternative(*d.value)); REQUIRE(std::get(*d.value) == "Q1 - Q2"); } TEST_CASE("Facet — QuantityFacetData with null value", "[types][facet]") { QuantityFacetData data; data.unit = "apples"; data.is_exact = false; // value intentionally left empty Facet f; f.facet_id = "F_Q_unresolved"; f.object_ref = "Q_alice_final"; f.confidence = 0.80; f.source_module = "quantity_specialist"; f.data = data; auto& d = std::get(f.data); REQUIRE_FALSE(d.value.has_value()); } TEST_CASE("Facet — JSON round-trip with TemporalFacetData", "[types][facet][json]") { TemporalFacetData data; data.time_point = "t1"; data.temporal_order_refs = {"C1", "C2"}; data.is_recurring = false; Facet original; original.facet_id = "F_temporal"; original.object_ref = "EV_transfer"; original.confidence = 0.88; original.source_module = "temporal_specialist"; original.data = data; json j = original; REQUIRE(j["facet_kind"] == "temporal"); REQUIRE(j["facet_id"] == "F_temporal"); REQUIRE(j["time_point"] == "t1"); Facet restored = j.get(); REQUIRE(restored.facet_id == original.facet_id); REQUIRE(kind_of(restored.data) == FacetKind::Temporal); auto& d = std::get(restored.data); REQUIRE(*d.time_point == "t1"); REQUIRE(d.temporal_order_refs.size() == 2); } TEST_CASE("Facet — JSON round-trip with QuantityFacetData numeric value", "[types][facet][json]") { QuantityFacetData data; data.value = 3.0; data.unit = "apples"; data.is_exact = true; Facet original; original.facet_id = "F_Q_resolved"; original.object_ref = "Q_alice_final"; original.confidence = 0.99; original.source_module = "quantity_specialist"; original.data = data; json j = original; REQUIRE(j["facet_kind"] == "quantity"); REQUIRE(j["value"] == 3.0); REQUIRE(j["unit"] == "apples"); REQUIRE(j["is_exact"] == true); Facet restored = j.get(); auto& d = std::get(restored.data); REQUIRE(std::holds_alternative(*d.value)); REQUIRE_THAT(std::get(*d.value), Catch::Matchers::WithinAbs(3.0, 1e-9)); REQUIRE(*d.unit == "apples"); REQUIRE(d.is_exact); } // ───────────────────────────────────────────────────────────────────────────── // GateSignal // ───────────────────────────────────────────────────────────────────────────── TEST_CASE("GateSignal — construction", "[types][gate]") { GateSignal g; g.gate_id = "has_quantity"; g.activated = true; g.confidence = 0.99; g.evidence_spans = {EvidenceSpan{"5 apples", 10, 18}}; g.method = GateMethod::Rule; REQUIRE(g.gate_id == "has_quantity"); REQUIRE(g.activated); REQUIRE_THAT(g.confidence, Catch::Matchers::WithinAbs(0.99, 1e-9)); REQUIRE(g.evidence_spans.size() == 1); REQUIRE(g.evidence_spans[0].text == "5 apples"); REQUIRE(g.method.has_value()); REQUIRE(*g.method == GateMethod::Rule); } TEST_CASE("GateSignal — JSON round-trip", "[types][gate][json]") { GateSignal original; original.gate_id = "has_entity_reference"; original.activated = true; original.confidence = 0.95; original.method = GateMethod::Rule; original.evidence_spans = {EvidenceSpan{"Alice", 0, 5}, EvidenceSpan{"Bob", 29, 32}}; json j = original; GateSignal restored = j.get(); REQUIRE(restored.gate_id == original.gate_id); REQUIRE(restored.activated == original.activated); REQUIRE_THAT(restored.confidence, Catch::Matchers::WithinAbs(original.confidence, 1e-9)); REQUIRE(restored.evidence_spans.size() == 2); REQUIRE(restored.evidence_spans[0].text == "Alice"); REQUIRE(restored.method.has_value()); REQUIRE(*restored.method == GateMethod::Rule); } // ───────────────────────────────────────────────────────────────────────────── // WorldState // ───────────────────────────────────────────────────────────────────────────── TEST_CASE("WorldState — construction with objects", "[types][worldstate]") { WorldState ws; ws.state_id = "ws_001"; ws.stage = WorldStateStage::PostProposal; ws.input_text = "Alice had 5 apples."; CanonicalObject alice; alice.object_id = "E1"; alice.object_kind = ObjectKind::Entity; alice.confidence = 0.97; ws.objects["E1"] = alice; CanonicalObject q1; q1.object_id = "Q1"; q1.object_kind = ObjectKind::Quantity; q1.confidence = 0.99; ws.objects["Q1"] = q1; REQUIRE(ws.objects.size() == 2); REQUIRE(ws.objects.count("E1") == 1); REQUIRE(ws.objects.at("E1").object_kind == ObjectKind::Entity); REQUIRE(ws.facets.empty()); REQUIRE(ws.constraints.empty()); REQUIRE(ws.input_text.has_value()); REQUIRE(*ws.input_text == "Alice had 5 apples."); REQUIRE_FALSE(ws.synthesis_answer.has_value()); } TEST_CASE("WorldState — JSON round-trip", "[types][worldstate][json]") { WorldState ws; ws.state_id = "ws_roundtrip"; ws.stage = WorldStateStage::PostProposal; ws.input_text = "Alice had 5 apples."; CanonicalObject alice; alice.object_id = "E1"; alice.object_kind = ObjectKind::Entity; alice.status = ObjectStatus::Active; alice.confidence = 0.97; alice.canonical_label = "Alice"; alice.source_module = "entity_specialist"; ws.objects["E1"] = alice; Facet fq; fq.facet_id = "F_Q1"; fq.object_ref = "Q1"; fq.confidence = 0.99; fq.source_module = "quantity_specialist"; QuantityFacetData qd; qd.value = 5.0; qd.unit = "apples"; qd.is_exact = true; fq.data = qd; ws.facets["F_Q1"] = fq; json j = ws; WorldState restored = j.get(); REQUIRE(restored.state_id == ws.state_id); REQUIRE(restored.stage == ws.stage); REQUIRE(restored.input_text == ws.input_text); REQUIRE(restored.objects.size() == 1); REQUIRE(restored.objects.count("E1") == 1); REQUIRE(restored.objects.at("E1").canonical_label == alice.canonical_label); REQUIRE(restored.facets.size() == 1); REQUIRE(restored.facets.count("F_Q1") == 1); REQUIRE(kind_of(restored.facets.at("F_Q1").data) == FacetKind::Quantity); } TEST_CASE("WorldState — stage enum JSON values", "[types][worldstate][json]") { WorldState ws; ws.state_id = "ws_stage_test"; ws.stage = WorldStateStage::PostGate; REQUIRE(json(ws)["stage"] == "post_gate"); ws.stage = WorldStateStage::Resolved; REQUIRE(json(ws)["stage"] == "resolved"); ws.stage = WorldStateStage::Synthesized; REQUIRE(json(ws)["stage"] == "synthesized"); } // ───────────────────────────────────────────────────────────────────────────── // Enum serialization spot checks // ───────────────────────────────────────────────────────────────────────────── TEST_CASE("Enum JSON values match schema strings", "[types][enum][json]") { REQUIRE(json(ObjectKind::Entity) == "entity"); REQUIRE(json(ObjectKind::RelationInstance) == "relation_instance"); REQUIRE(json(ConstraintStrength::Probabilistic) == "probabilistic"); REQUIRE(json(ConstraintStatus::Contradicted) == "contradicted"); REQUIRE(json(Polarity::Negative) == "negative"); REQUIRE(json(TruthStatus::True_) == "true"); REQUIRE(json(TruthStatus::False_) == "false"); REQUIRE(json(TransferType::Initial) == "initial"); REQUIRE(json(ExecutionMode::ConflictChecking) == "conflict_checking"); REQUIRE(json(ErrorKind::InsufficientData) == "insufficient_data"); } TEST_CASE("SpecialistError — construction and JSON round-trip", "[types][error][json]") { SpecialistError err; err.error_kind = ErrorKind::Ambiguous; err.affected_refs = {"E1", "E2"}; err.description = "Could not resolve coreference between E1 and E2"; json j = err; REQUIRE(j["error_kind"] == "ambiguous"); SpecialistError restored = j.get(); REQUIRE(restored.error_kind == ErrorKind::Ambiguous); REQUIRE(restored.affected_refs.size() == 2); REQUIRE(restored.description == err.description); }