diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-15 16:56:39 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-15 16:56:39 -0500 |
| commit | c1d4e3c4a42abd01bc7ef83b1d6ae036ee32ef1d (patch) | |
| tree | 3e6dcc682cbf2311409e7f71d83a7d4088392068 /claude-templates/.ai/scripts/tests/test_cj_scan.py | |
| parent | 2b471da4bab014a2e096f63edc7aac235fc40fdd (diff) | |
| parent | 69c5e4ace81586c05dea6a9a3afd54dafa61a73b (diff) | |
| download | rulesets-c1d4e3c4a42abd01bc7ef83b1d6ae036ee32ef1d.tar.gz rulesets-c1d4e3c4a42abd01bc7ef83b1d6ae036ee32ef1d.zip | |
Merge commit '69c5e4ace81586c05dea6a9a3afd54dafa61a73b' as 'claude-templates'
Diffstat (limited to 'claude-templates/.ai/scripts/tests/test_cj_scan.py')
| -rw-r--r-- | claude-templates/.ai/scripts/tests/test_cj_scan.py | 250 |
1 files changed, 250 insertions, 0 deletions
diff --git a/claude-templates/.ai/scripts/tests/test_cj_scan.py b/claude-templates/.ai/scripts/tests/test_cj_scan.py new file mode 100644 index 0000000..7844474 --- /dev/null +++ b/claude-templates/.ai/scripts/tests/test_cj_scan.py @@ -0,0 +1,250 @@ +"""Tests for cj-scan.py — org-file cj-annotation scanner. + +The script parses an org file and emits JSON describing: +- cj_blocks: every cj annotation found (source-block or legacy-inline form) +- verify_tasks: every VERIFY heading + placement validity (top-level or first-level child only) +- unclosed_blocks: any source-block fence that opened but never closed +""" + +import json +import subprocess +from pathlib import Path + +import pytest + +SCRIPT = Path(__file__).parent.parent / "cj-scan.py" + + +@pytest.fixture +def run_scan(tmp_path): + """Write content to a temp org file and run cj-scan; return parsed JSON output.""" + def _run(content: str) -> dict: + f = tmp_path / "test.org" + f.write_text(content) + result = subprocess.run( + ["python3", str(SCRIPT), str(f)], + capture_output=True, + text=True, + check=True, + ) + return json.loads(result.stdout) + return _run + + +# ---------------------------------------------------------------------- +# cj-block detection +# ---------------------------------------------------------------------- + +class TestCjScanCjBlockDetection: + """Detection of cj annotations — source-block and legacy-inline forms.""" + + def test_cj_scan_source_block_single_detected(self, run_scan): + """Normal: a single source-block cj is detected with correct line range and body.""" + content = "* Section\n#+begin_src cj: comment\nplease check this\n#+end_src\n" + result = run_scan(content) + assert len(result["cj_blocks"]) == 1 + b = result["cj_blocks"][0] + assert b["form"] == "source-block" + assert b["body"] == "please check this" + assert b["start_line"] == 2 + assert b["end_line"] == 4 + + def test_cj_scan_source_block_multiline_body_preserved(self, run_scan): + """Normal: multi-line body is preserved with embedded newlines.""" + content = "* S\n#+begin_src cj: comment\nline 1\nline 2\nline 3\n#+end_src\n" + result = run_scan(content) + assert result["cj_blocks"][0]["body"] == "line 1\nline 2\nline 3" + + def test_cj_scan_multiple_source_blocks_each_detected(self, run_scan): + """Normal: multiple source-blocks in a file are detected as separate items.""" + content = ( + "* A\n#+begin_src cj: comment\nfirst\n#+end_src\n" + "* B\n#+begin_src cj: comment\nsecond\n#+end_src\n" + ) + result = run_scan(content) + assert len(result["cj_blocks"]) == 2 + bodies = [b["body"] for b in result["cj_blocks"]] + assert bodies == ["first", "second"] + + def test_cj_scan_legacy_inline_single_line_detected(self, run_scan): + """Normal: a legacy inline cj line is detected with form=legacy-inline.""" + content = "* Section\ncj: please check this\n" + result = run_scan(content) + assert len(result["cj_blocks"]) == 1 + b = result["cj_blocks"][0] + assert b["form"] == "legacy-inline" + assert b["body"] == "please check this" + assert b["start_line"] == 2 + assert b["end_line"] == 2 + + def test_cj_scan_mixed_forms_in_same_file(self, run_scan): + """Normal: source-block + legacy inline coexist; both detected as separate items.""" + content = ( + "* A\ncj: legacy form\n" + "* B\n#+begin_src cj: comment\nnew form\n#+end_src\n" + ) + result = run_scan(content) + assert len(result["cj_blocks"]) == 2 + forms = sorted(b["form"] for b in result["cj_blocks"]) + assert forms == ["legacy-inline", "source-block"] + + def test_cj_scan_empty_file_returns_empty_lists(self, run_scan): + """Boundary: empty file → empty cj_blocks and verify_tasks lists.""" + result = run_scan("") + assert result["cj_blocks"] == [] + assert result["verify_tasks"] == [] + assert result["unclosed_blocks"] == [] + + def test_cj_scan_no_cj_content_returns_empty_blocks(self, run_scan): + """Boundary: org file with no cj content → empty cj_blocks.""" + content = "* Section\n** TODO Task\nbody text\n** TODO Another\n" + result = run_scan(content) + assert result["cj_blocks"] == [] + + def test_cj_scan_block_before_any_heading_empty_chain(self, run_scan): + """Boundary: cj block at top of file (before any heading) → empty parent chain.""" + content = "#+begin_src cj: comment\ntop-level note\n#+end_src\n" + result = run_scan(content) + assert result["cj_blocks"][0]["parent_heading_chain"] == [] + assert result["cj_blocks"][0]["parent_depth"] == 0 + + @pytest.mark.parametrize("fence", [ + "#+begin_src cj: comment", + "#+begin_src cj:", + "#+begin_src cj: anything", + "#+BEGIN_SRC cj: comment", # case-insensitive + ]) + def test_cj_scan_source_block_fence_variants_all_recognized(self, run_scan, fence): + """Boundary: fence label and case variants are all valid forms.""" + content = f"* S\n{fence}\nbody\n#+end_src\n" + result = run_scan(content) + assert len(result["cj_blocks"]) == 1 + assert result["cj_blocks"][0]["body"] == "body" + + def test_cj_scan_unclosed_source_block_reported(self, run_scan): + """Error: a source-block that opens but never closes → reported in unclosed_blocks.""" + content = "* S\n#+begin_src cj: comment\nbody that never ends\n" + result = run_scan(content) + assert result["cj_blocks"] == [] + assert len(result["unclosed_blocks"]) == 1 + assert result["unclosed_blocks"][0]["start_line"] == 2 + + +# ---------------------------------------------------------------------- +# Parent heading chain reconstruction +# ---------------------------------------------------------------------- + +class TestCjScanParentChain: + """Parent heading chain construction — walking the org tree backward.""" + + def test_cj_scan_nested_parent_chain_three_levels(self, run_scan): + """Normal: cj block inside three nested headings → chain reflects all three.""" + content = ( + "* Work\n" + "** DOING [#A] Kostya's contract\n" + "*** VERIFY Question?\n" + "#+begin_src cj: comment\nanswer\n#+end_src\n" + ) + result = run_scan(content) + chain = result["cj_blocks"][0]["parent_heading_chain"] + assert len(chain) == 3 + assert chain[0] == {"depth": 1, "heading": "Work"} + assert chain[1] == {"depth": 2, "heading": "DOING [#A] Kostya's contract"} + assert chain[2] == {"depth": 3, "heading": "VERIFY Question?"} + assert result["cj_blocks"][0]["parent_depth"] == 3 + + def test_cj_scan_depth_skip_only_actual_ancestors(self, run_scan): + """Normal: heading depth skip (e.g., * then ***) → chain captures only present headings.""" + content = "* Section\n*** Deep child\n#+begin_src cj: comment\nbody\n#+end_src\n" + result = run_scan(content) + chain = result["cj_blocks"][0]["parent_heading_chain"] + assert [h["depth"] for h in chain] == [1, 3] + + def test_cj_scan_shallower_sibling_pops_deeper_frames(self, run_scan): + """Normal: when a shallower heading appears, deeper frames pop off the stack.""" + content = ( + "* A\n** A.1\n*** A.1.1\n" + "** B\n" + "#+begin_src cj: comment\nunder B\n#+end_src\n" + ) + result = run_scan(content) + chain = result["cj_blocks"][0]["parent_heading_chain"] + assert len(chain) == 2 + assert chain[0]["heading"] == "A" + assert chain[1]["heading"] == "B" + + +# ---------------------------------------------------------------------- +# VERIFY task detection + placement audit +# ---------------------------------------------------------------------- + +class TestCjScanVerifyPlacement: + """VERIFY task detection and placement audit per the canonical rule.""" + + def test_cj_scan_verify_at_depth_2_is_valid(self, run_scan): + """Normal: ** VERIFY (top-level) is valid placement.""" + content = "* Work\n** VERIFY [#C] Hayk's Farearth Evaluation :research:hayk:\n" + result = run_scan(content) + assert len(result["verify_tasks"]) == 1 + v = result["verify_tasks"][0] + assert v["depth"] == 2 + assert v["valid_depth"] is True + assert v["promotion_target"] is None + + def test_cj_scan_verify_at_depth_3_is_valid(self, run_scan): + """Normal: *** VERIFY (first-level child) is valid placement.""" + content = "* Work\n** TODO Parent\n*** VERIFY Question?\n" + result = run_scan(content) + v = result["verify_tasks"][0] + assert v["depth"] == 3 + assert v["valid_depth"] is True + + def test_cj_scan_verify_at_depth_4_invalid_promote_to_3(self, run_scan): + """Normal: **** VERIFY is buried; suggests promotion to depth 3.""" + content = "* W\n** P\n*** Q\n**** VERIFY Buried?\n" + result = run_scan(content) + v = result["verify_tasks"][0] + assert v["depth"] == 4 + assert v["valid_depth"] is False + assert v["promotion_target"] == 3 + + def test_cj_scan_verify_at_depth_6_invalid_promote_to_3(self, run_scan): + """Normal: ****** VERIFY at any deep level → promotion target is still 3.""" + content = "* W\n** P\n*** Q\n**** Q2\n***** Q3\n****** VERIFY Very buried?\n" + result = run_scan(content) + v = result["verify_tasks"][0] + assert v["depth"] == 6 + assert v["promotion_target"] == 3 + + def test_cj_scan_verify_at_depth_1_invalid_promote_to_2(self, run_scan): + """Boundary: * VERIFY at top-section depth → promotion target is 2 (top-level under section).""" + content = "* VERIFY Should-be-deeper\n" + result = run_scan(content) + v = result["verify_tasks"][0] + assert v["depth"] == 1 + assert v["valid_depth"] is False + assert v["promotion_target"] == 2 + + def test_cj_scan_verify_heading_with_priority_and_tags(self, run_scan): + """Boundary: VERIFY heading with priority cookie + tags → heading text captured fully.""" + content = "* W\n** VERIFY [#C] Hayk's Farearth Evaluation :research:hayk:\n" + result = run_scan(content) + v = result["verify_tasks"][0] + assert "Hayk's Farearth Evaluation" in v["heading"] + assert ":research:" in v["heading"] + + def test_cj_scan_no_verify_tasks_empty_list(self, run_scan): + """Boundary: file with only TODO/DOING headings → empty verify_tasks list.""" + content = "* W\n** TODO X\n*** DOING Y\n" + result = run_scan(content) + assert result["verify_tasks"] == [] + + def test_cj_scan_verify_word_in_body_is_not_a_task(self, run_scan): + """Error: the word VERIFY appearing in body prose is not detected as a task.""" + content = ( + "* Work\n" + "** TODO Important task\n" + "Body line mentioning VERIFY in prose.\n" + ) + result = run_scan(content) + assert result["verify_tasks"] == [] |
