Aaryan Kumar
deploy to hugging face
1605cbb
Raw
History Blame Contribute Delete
6.54 kB
"""
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]