Some checks failed
CI / skinny-install (aco) (push) Successful in 53s
CI / lint-test (push) Successful in 1m15s
CI / skinny-install (api) (push) Successful in 26s
CI / skinny-install (bcda) (push) Successful in 29s
CI / skinny-install (bib) (push) Successful in 31s
CI / skinny-install (bls) (push) Successful in 27s
CI / skinny-install (ccw) (push) Successful in 30s
CI / skinny-install (cli) (push) Successful in 30s
CI / skinny-install (cms) (push) Successful in 26s
CI / skinny-install (conf) (push) Successful in 26s
CI / skinny-install (opps) (push) Successful in 28s
CI / skinny-install (perf) (push) Successful in 29s
CI / skinny-install (pfs) (push) Successful in 27s
Infra CI / notebooks (push) Successful in 7s
Infra CI / zotero (push) Successful in 7s
CI / skinny-install (rex) (push) Successful in 27s
Infra CI / docs (push) Failing after 9s
Infra CI / api (push) Successful in 8s
Infra CI / mc (push) Successful in 6s
Package Supply Chain / pkg-supply-chain (push) Successful in 52s
CI / lint-test (pull_request) Successful in 1m18s
CI / skinny-install (aco) (pull_request) Successful in 44s
CI / skinny-install (api) (pull_request) Successful in 24s
CI / skinny-install (bcda) (pull_request) Successful in 32s
CI / skinny-install (bib) (pull_request) Successful in 30s
CI / skinny-install (bls) (pull_request) Successful in 25s
CI / skinny-install (ccw) (pull_request) Successful in 23s
CI / skinny-install (cli) (pull_request) Successful in 30s
CI / skinny-install (cms) (pull_request) Successful in 27s
CI / skinny-install (conf) (pull_request) Successful in 30s
CI / skinny-install (opps) (pull_request) Successful in 29s
CI / skinny-install (perf) (pull_request) Successful in 32s
CI / skinny-install (pfs) (pull_request) Successful in 33s
Infra CI / notebooks (pull_request) Successful in 7s
Infra CI / zotero (pull_request) Successful in 6s
Infra CI / docs (pull_request) Failing after 6s
CI / skinny-install (rex) (pull_request) Successful in 27s
Infra CI / api (pull_request) Successful in 6s
Infra CI / mc (pull_request) Successful in 6s
4-layer architecture for tracking coverage as semantic AST nodes rather than raw line numbers: parse → enrich → runtime → plan. Stable node identities, Ruff/ty diagnostic attachment, coverage mapping, priority scoring heuristic, and drift-resistant rolling state persistence. 39 tests, ruff clean.
92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
"""Tests for sem.state — rolling state persistence."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from sem.nodes import NodeKind, SemanticNode, Span
|
|
from sem.state import (
|
|
apply_state,
|
|
load_state,
|
|
record_attempt,
|
|
save_state,
|
|
sync_state,
|
|
)
|
|
|
|
|
|
def _node(nid: str, src_hash: str = "abc123") -> SemanticNode:
|
|
return SemanticNode(
|
|
node_id=nid,
|
|
kind=NodeKind.FUNCTION,
|
|
span=Span(start_line=1, end_line=5),
|
|
module="mod",
|
|
source_hash=src_hash,
|
|
)
|
|
|
|
|
|
class TestPersistence:
|
|
def test_round_trip(self, tmp_path):
|
|
p = tmp_path / "state.json"
|
|
state = {"node_a": {"status": "covered", "attempts": 1}}
|
|
save_state(state, p)
|
|
loaded = load_state(p)
|
|
assert loaded["node_a"]["status"] == "covered"
|
|
|
|
def test_load_missing_file(self, tmp_path):
|
|
assert load_state(tmp_path / "nope.json") == {}
|
|
|
|
|
|
class TestSync:
|
|
def test_new_node_gets_fresh_entry(self):
|
|
nodes = [_node("n1")]
|
|
result = sync_state(nodes, {})
|
|
assert "n1" in result
|
|
assert result["n1"]["status"] == "unexplored"
|
|
|
|
def test_unchanged_node_keeps_history(self):
|
|
nodes = [_node("n1", "abc")]
|
|
old = {"n1": {"status": "covered", "attempts": 3, "source_hash": "abc"}}
|
|
result = sync_state(nodes, old)
|
|
assert result["n1"]["status"] == "covered"
|
|
assert result["n1"]["attempts"] == 3
|
|
|
|
def test_changed_hash_resets_status(self):
|
|
nodes = [_node("n1", "new_hash")]
|
|
old = {"n1": {"status": "covered", "attempts": 3, "source_hash": "old_hash"}}
|
|
result = sync_state(nodes, old)
|
|
assert result["n1"]["status"] == "unexplored"
|
|
assert result["n1"]["attempts"] == 0
|
|
|
|
def test_stale_entries_removed(self):
|
|
nodes = [_node("n1")]
|
|
old = {"n1": {"source_hash": "abc123"}, "n_gone": {"source_hash": "x"}}
|
|
result = sync_state(nodes, old)
|
|
assert "n_gone" not in result
|
|
|
|
|
|
class TestApplyState:
|
|
def test_pushes_state_to_nodes(self):
|
|
node = _node("n1")
|
|
state = {"n1": {"status": "partial", "attempts": 2}}
|
|
apply_state([node], state)
|
|
assert node.status == "partial"
|
|
assert node.attempts == 2
|
|
|
|
|
|
class TestRecordAttempt:
|
|
def test_increments_attempts(self):
|
|
state = {"n1": {"status": "unexplored", "attempts": 0, "notes": []}}
|
|
record_attempt(state, "n1", "partial", "needs None input")
|
|
assert state["n1"]["attempts"] == 1
|
|
assert state["n1"]["last_result"] == "partial"
|
|
assert "needs None input" in state["n1"]["notes"]
|
|
|
|
def test_covered_sets_status(self):
|
|
state = {"n1": {"status": "unexplored", "attempts": 0, "notes": []}}
|
|
record_attempt(state, "n1", "covered")
|
|
assert state["n1"]["status"] == "covered"
|
|
|
|
def test_new_node_creates_entry(self):
|
|
state = {}
|
|
record_attempt(state, "new", "partial")
|
|
assert "new" in state
|
|
assert state["new"]["attempts"] == 1
|