diff --git a/conductor/tracks/phase6_review_20260510/plan.md b/conductor/tracks/phase6_review_20260510/plan.md index 427dcc1..94fb434 100644 --- a/conductor/tracks/phase6_review_20260510/plan.md +++ b/conductor/tracks/phase6_review_20260510/plan.md @@ -7,7 +7,7 @@ - [x] Task: Conductor - User Manual Verification 'Phase 1' (Protocol in workflow.md) ## Phase 2: Feature Coverage (Core Logic) -- [~] Task: Write unit tests for C++ AST Tree Masking logic using samples from `gencpp/base/components`. +- [x] Task: Write unit tests for C++ AST Tree Masking logic using samples from `gencpp/base/components`. - [ ] Task: Write unit tests for 'Fuzzy Anchor' resolution across simulated file edits. - [ ] Task: Write unit tests for `HistoryManager` context snapshot roundtrips. - [ ] Task: Conductor - User Manual Verification 'Phase 2' (Protocol in workflow.md) diff --git a/tests/test_fuzzy_anchor.py b/tests/test_fuzzy_anchor.py new file mode 100644 index 0000000..e1918b8 --- /dev/null +++ b/tests/test_fuzzy_anchor.py @@ -0,0 +1,59 @@ +import pytest +from src.fuzzy_anchor import FuzzyAnchor + + +class TestFuzzyAnchor: + def test_create_slice_basic(self): + text = "line0\nline1\nline2\nline3\nline4\n" + result = FuzzyAnchor.create_slice(text, 2, 4) + assert "start_line" in result + assert "end_line" in result + assert "content_hash" in result + assert "start_context" in result + assert "end_context" in result + assert result["start_line"] == 2 + assert result["end_line"] == 4 + assert result["start_context"] == result["end_context"] + + def test_resolve_slice_exact_match(self): + text = "line0\nline1\nline2\nline3\nline4\n" + slc = FuzzyAnchor.create_slice(text, 2, 4) + result = FuzzyAnchor.resolve_slice(text, slc) + assert result is not None + start, end = result + assert start == 2 + assert end == 4 + + def test_resolve_slice_line_inserted_before(self): + original = "line0\nline1\nline2\nline3\nline4\n" + modified = "NEW\nline0\nline1\nline2\nline3\nline4\n" + slc = FuzzyAnchor.create_slice(original, 2, 4) + result = FuzzyAnchor.resolve_slice(modified, slc) + assert result is not None + start, end = result + assert start == 3 + assert end == 5 + + def test_resolve_slice_line_deleted_before_returns_none(self): + original = "line0\nline1\nline2\nline3\nline4\n" + modified = "line0\nline2\nline3\nline4\n" + slc = FuzzyAnchor.create_slice(original, 2, 4) + result = FuzzyAnchor.resolve_slice(modified, slc) + assert result is None + + def test_resolve_slice_multiple_lines_changed(self): + original = "line0\nline1\nline2\nline3\nline4\n" + modified = "a\nb\nc\nd\ne\nline0\nline1\nline2\nline3\nline4\n" + slc = FuzzyAnchor.create_slice(original, 1, 2) + result = FuzzyAnchor.resolve_slice(modified, slc) + assert result is not None + start, end = result + assert start == 6 + assert end == 7 + + def test_resolve_slice_anchor_mismatch_returns_none(self): + original = "alpha\nbeta\ngamma\ndelta\nepsilon\n" + modified = "foo\nbar\nbaz\ndelta\nepsilon\n" + slc = FuzzyAnchor.create_slice(original, 2, 3) + result = FuzzyAnchor.resolve_slice(modified, slc) + assert result is None diff --git a/tests/test_history_manager.py b/tests/test_history_manager.py new file mode 100644 index 0000000..6bbdbcf --- /dev/null +++ b/tests/test_history_manager.py @@ -0,0 +1,97 @@ +import pytest +from src.history import HistoryManager, UISnapshot + + +class TestHistoryManager: + def test_push_and_undo(self): + hm = HistoryManager(max_capacity=10) + hm.push({"value": 1}, "initial") + hm.push({"value": 2}, "change to 2") + assert hm.can_undo is True + result = hm.undo(current_state={"value": 2}) + assert result is not None + assert result.state["value"] == 2 + assert hm.can_undo is True + + def test_undo_and_redo(self): + hm = HistoryManager(max_capacity=10) + hm.push({"value": 1}, "initial") + hm.push({"value": 2}, "change to 2") + assert hm.can_redo is False + hm.undo(current_state={"value": 2}) + assert hm.can_redo is True + redone = hm.redo(current_state={"value": 1}) + assert redone is not None + assert redone.state["value"] == 2 + + def test_undo_no_history_returns_none(self): + hm = HistoryManager(max_capacity=10) + assert hm.can_undo is False + result = hm.undo(current_state={"value": 1}) + assert result is None + + def test_redo_no_history_returns_none(self): + hm = HistoryManager(max_capacity=10) + assert hm.can_redo is False + result = hm.redo(current_state={"value": 1}) + assert result is None + + def test_jump_to_undo(self): + hm = HistoryManager(max_capacity=10) + hm.push({"value": 1}, "initial") + hm.push({"value": 2}, "change to 2") + hm.push({"value": 3}, "change to 3") + result = hm.jump_to_undo(0, current_state={"value": 3}) + assert result is not None + assert result.state["value"] == 1 + assert hm.can_redo is True + + def test_get_history_returns_descriptions(self): + hm = HistoryManager(max_capacity=10) + hm.push({"value": 1}, "first") + hm.push({"value": 2}, "second") + hm.push({"value": 3}, "third") + history = hm.get_history() + assert len(history) == 3 + assert history[0]["description"] == "first" + assert history[1]["description"] == "second" + assert "timestamp" in history[0] + + def test_snapshot_roundtrip(self): + snap = UISnapshot( + ai_input="test input", + project_system_prompt="project prompt", + global_system_prompt="global prompt", + base_system_prompt="base prompt", + use_default_base_prompt=True, + temperature=0.5, + top_p=0.9, + max_tokens=8192, + auto_add_history=True, + disc_entries=[{"role": "user", "content": "hello"}], + files=[{"path": "a.txt"}], + screenshots=["screenshot.png"], + ) + d = snap.to_dict() + restored = UISnapshot.from_dict(d) + assert restored.ai_input == snap.ai_input + assert restored.project_system_prompt == snap.project_system_prompt + assert restored.global_system_prompt == snap.global_system_prompt + assert restored.base_system_prompt == snap.base_system_prompt + assert restored.use_default_base_prompt == snap.use_default_base_prompt + assert restored.temperature == snap.temperature + assert restored.top_p == snap.top_p + assert restored.max_tokens == snap.max_tokens + assert restored.auto_add_history == snap.auto_add_history + assert restored.disc_entries == snap.disc_entries + assert restored.files == snap.files + assert restored.screenshots == snap.screenshots + + def test_push_clears_redo_stack(self): + hm = HistoryManager(max_capacity=10) + hm.push({"value": 1}, "initial") + hm.push({"value": 2}, "change to 2") + hm.undo(current_state={"value": 2}) + assert hm.can_redo is True + hm.push({"value": 3}, "change to 3") + assert hm.can_redo is False