Spaces:
Sleeping
Sleeping
| """ | |
| FALSIFY core-logic tests — belief revision without any API key or database. | |
| Each test builds a small graph via the :class:`~tests.conftest.FakeGraph` fixture, | |
| runs the real cascade/forget code, and asserts on the resulting truth-states. These | |
| cover the correctness cases enumerated in REQUIREMENTS.md §4.3: | |
| * direct refutation (test_direct_refutation) | |
| * forward cascade invalidates conclusion (test_forward_cascade_invalidates) | |
| * diamond / partial refutation (test_diamond_partial_refutation) | |
| * non-critical dependency survives (test_non_critical_dependency_survives) | |
| * cycle safety (termination) (test_cycle_safety) | |
| * surgical forget of orphans only (test_forget_orphan_keeps_provenance) | |
| * hypothesis demote/promote (test_promote_competing_hypothesis) | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| from falsify.edges import DEPENDS_ON, SUPERSEDES, SUPPORTS | |
| from falsify.models import TruthState | |
| from falsify.tasks.cascade_forget import cascade_forget | |
| from falsify.tasks.propagate_refutation import ( | |
| promote_competing_hypothesis, | |
| propagate_refutation, | |
| ) | |
| pytestmark = pytest.mark.asyncio | |
| async def _alignment(g, nid: str): | |
| entry = g.truth.get(str(nid)) | |
| return entry["truth_alignment"] if entry else None | |
| async def test_direct_refutation(fake_graph): | |
| """Refuting an evidence node sets its truth_alignment to ['refuted'].""" | |
| g = fake_graph | |
| g.add_node("E", claim="the March QA report documents the defect") | |
| res = await propagate_refutation(["E"]) | |
| assert res.refuted == ["E"] | |
| assert await _alignment(g, "E") == [TruthState.REFUTED.value] | |
| async def test_forward_cascade_invalidates(fake_graph): | |
| """A→B→C depends_on chain: refuting the base evidence invalidates the chain. | |
| Topology (depends_on points Conclusion→Evidence): | |
| C --depends_on(critical)--> B --depends_on(critical)--> E(evidence) | |
| Refuting E must invalidate B, then C. | |
| """ | |
| g = fake_graph | |
| g.add_node("E", claim="base evidence") | |
| g.add_node("B", statement="mid conclusion") | |
| g.add_node("C", statement="top conclusion") | |
| g.add_edge("B", "E", DEPENDS_ON, critical=True) | |
| g.add_edge("C", "B", DEPENDS_ON, critical=True) | |
| res = await propagate_refutation(["E"]) | |
| assert await _alignment(g, "E") == [TruthState.REFUTED.value] | |
| assert await _alignment(g, "B") == [TruthState.INVALIDATED.value] | |
| assert await _alignment(g, "C") == [TruthState.INVALIDATED.value] | |
| assert set(res.invalidated) == {"B", "C"} | |
| async def test_diamond_partial_refutation(fake_graph): | |
| """A conclusion with two critical supporters survives losing only one. | |
| K depends_on E1 (critical) AND E2 (critical). Refuting only E1 must keep K alive | |
| (E2 still supports it); refuting E2 as well then invalidates K. | |
| """ | |
| g = fake_graph | |
| g.add_node("E1", claim="evidence one") | |
| g.add_node("E2", claim="evidence two") | |
| g.add_node("K", statement="conclusion on both") | |
| g.add_edge("K", "E1", DEPENDS_ON, critical=True) | |
| g.add_edge("K", "E2", DEPENDS_ON, critical=True) | |
| # First refutation: K keeps an alive critical supporter (E2) -> stays alive. | |
| await propagate_refutation(["E1"]) | |
| assert await _alignment(g, "E1") == [TruthState.REFUTED.value] | |
| assert await _alignment(g, "K") == [TruthState.ALIVE.value] | |
| # Second refutation: K loses its last critical supporter -> invalidated. | |
| await propagate_refutation(["E2"]) | |
| assert await _alignment(g, "K") == [TruthState.INVALIDATED.value] | |
| async def test_non_critical_dependency_survives(fake_graph): | |
| """Refuting a NON-critical dependency weakens but does not invalidate.""" | |
| g = fake_graph | |
| g.add_node("E", claim="soft evidence") | |
| g.add_node("K", statement="conclusion softly resting on E") | |
| g.add_edge("K", "E", DEPENDS_ON, critical=False) | |
| res = await propagate_refutation(["E"]) | |
| assert await _alignment(g, "K") == [TruthState.ALIVE.value] | |
| assert "K" in res.weakened | |
| assert "K" not in res.invalidated | |
| async def test_cycle_safety(fake_graph): | |
| """A cyclic depends_on graph terminates (visited set) and doesn't hang.""" | |
| g = fake_graph | |
| g.add_node("E", claim="evidence") | |
| g.add_node("X", statement="X") | |
| g.add_node("Y", statement="Y") | |
| # Cycle among conclusions, all critically resting on E and each other. | |
| g.add_edge("X", "E", DEPENDS_ON, critical=True) | |
| g.add_edge("Y", "X", DEPENDS_ON, critical=True) | |
| g.add_edge("X", "Y", DEPENDS_ON, critical=True) | |
| res = await propagate_refutation(["E"]) # must return, not loop forever | |
| assert await _alignment(g, "E") == [TruthState.REFUTED.value] | |
| assert "X" in res.invalidated and "Y" in res.invalidated | |
| async def test_forget_orphan_keeps_provenance(fake_graph): | |
| """cascade_forget deletes an orphaned invalidated conclusion but keeps the | |
| refuted evidence that is a supersedes-anchor (provenance tombstone).""" | |
| g = fake_graph | |
| g.add_node("E", claim="refuted evidence") | |
| g.add_node("K", statement="orphaned conclusion") | |
| g.add_node("NEW", claim="the new fact") | |
| g.add_edge("K", "E", DEPENDS_ON, critical=True) | |
| g.add_edge("NEW", "E", SUPERSEDES, confidence=0.9) # NEW (alive) supersedes E | |
| # Run the cascade, then forget. | |
| prop = await propagate_refutation(["E"]) | |
| forget = await cascade_forget(prop.affected) | |
| # K is orphaned (no alive consumer) -> deleted from both stores. | |
| assert "K" in forget.forgotten | |
| assert "K" in g.deleted | |
| assert g.deleted_from_collections # vector collections were targeted | |
| # E is refuted but retained as the supersedes provenance anchor. | |
| assert "E" in forget.retained_provenance | |
| assert "E" not in g.deleted | |
| async def test_promote_competing_hypothesis(fake_graph): | |
| """When A's only support dies, A is superseded and rival B is promoted.""" | |
| g = fake_graph | |
| g.add_node("E_a", claim="evidence for A") | |
| g.add_node("E_b", claim="evidence for B") | |
| g.add_node("A", statement="hypothesis A") | |
| g.add_node("B", statement="hypothesis B") | |
| g.add_edge("E_a", "A", SUPPORTS, weight=0.8) | |
| g.add_edge("E_b", "B", SUPPORTS, weight=0.7) | |
| # Refute A's evidence, then re-score hypotheses. | |
| prop = await propagate_refutation(["E_a"]) | |
| actions = await promote_competing_hypothesis(["E_a"], prop.epoch) | |
| assert actions.get("A") == "superseded" | |
| assert actions.get("B") == "promoted" | |
| assert await _alignment(g, "A") == [TruthState.SUPERSEDED.value] | |