Files
2026-10-15 08:57:23 +02:00

223 lines
8.5 KiB
Python

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Tests for recon_kit.py."""
import io
import json
import sys
import tempfile
import unittest
from contextlib import redirect_stderr, redirect_stdout
from datetime import date
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from recon_kit import add_months, main, parse_date, slugify
REPORT = """---
title: 'market research: widgets'
---
# Report
The market is growing fast [1] and pricing clusters at $20 [2].
An uncited assertion sits here [4].
```
[9] inside a fence never counts
```
## Source appendix
| [n] | Supports | Publisher | Pub date | Accessed | Confidence |
| --- | --- | --- | --- | --- | --- |
| [1] | market growth | [Gartner](https://example.com/g) | 2026-01 | 2026-07-01 | high |
| [2] | pricing | [Acme](javascript:alert(1)) | 2026-05 | 2026-07-01 | medium |
| [3] | never cited | https://example.com/x | 2025-11 | 2026-07-01 | low |
"""
MEMLOG = """---
topic: widgets
updated: 2026-07-22T10:00
---
- (decision) plan approved
- (source) round 1 batch: 4 sources
- (claim) ref=[1] status=verified class=size/growth pub=2026-01 — market growing 12% CAGR
- (claim) ref=[2] status=unverified class=pricing pub=2026-05 — pricing clusters at $20
- (claim) ref=[2] status=verified class=pricing pub=2026-05 — confirmed by second source
- (claim) status=unverified class=behavior pub=2025-03 — users churn at day 8
- (event) dimension 1 complete
"""
def run(argv):
buf = io.StringIO()
with redirect_stdout(buf):
code = main(argv)
return code, json.loads(buf.getvalue())
class CitationsTest(unittest.TestCase):
def test_cross_check(self):
with tempfile.TemporaryDirectory() as tmp:
report = Path(tmp) / "report.md"
report.write_text(REPORT, encoding="utf-8")
code, result = run(["citations", str(report)])
self.assertEqual(result["dangling_markers"], [4])
self.assertEqual(result["orphaned_rows"], [3])
self.assertNotIn(9, result["markers"]) # fenced content ignored
self.assertEqual(code, 1)
def run_text(argv, text, name="report.md"):
with tempfile.TemporaryDirectory() as tmp:
f = Path(tmp) / name
f.write_text(text, encoding="utf-8")
return run([argv[0], str(f), *argv[1:]])
class CitationsEdgeTest(unittest.TestCase):
def test_a_bare_number_table_is_not_the_source_appendix(self):
text = (
"Growth is fast [1].\n\n| Rank | Vendor |\n| --- | --- |\n| 1 | Acme |\n| 2 | Beta |\n| 3 | Gamma |\n\n"
"| [n] | Supports |\n| --- | --- |\n| [1] | growth |\n"
)
code, result = run_text(["citations"], text)
self.assertEqual(result["appendix_rows"], [1])
self.assertTrue(result["ok"])
self.assertEqual(code, 0)
def test_tilde_and_long_backtick_fences(self):
text = (
"~~~\n[7] tilde fenced\n~~~\n\n"
"````\n```\n[8] still fenced after an inner three-backtick line\n```\n````\n\n"
"Read after the close [1].\n\n| [n] | Supports |\n| --- | --- |\n| [1] | x |\n"
)
code, result = run_text(["citations"], text)
self.assertEqual(result["markers"], [1])
self.assertTrue(result["ok"])
def test_a_fence_nested_in_a_list_item_hides_its_markers(self):
text = (
"- Example:\n\n ```\n [5] inside a list-nested fence\n ```\n\n"
"Cited [1].\n\n| [n] | Supports |\n| --- | --- |\n| [1] | x |\n"
)
code, result = run_text(["citations"], text)
self.assertEqual(result["markers"], [1])
self.assertTrue(result["ok"])
class TallyTest(unittest.TestCase):
def test_last_status_wins_per_ref(self):
with tempfile.TemporaryDirectory() as tmp:
log = Path(tmp) / "memlog.md"
log.write_text(MEMLOG, encoding="utf-8")
code, result = run(["tally", str(log)])
self.assertEqual(result["by_type"]["claim"], 4)
self.assertEqual(result["claims"], {"unverified": 1, "verified": 2})
self.assertEqual(result["claims_total"], 3) # ref=[2] counted once
self.assertEqual(code, 0)
class StalenessTest(unittest.TestCase):
def test_dates(self):
self.assertEqual(parse_date("2026-01"), date(2026, 1, 1))
self.assertEqual(add_months(date(2026, 1, 31), 1), date(2026, 2, 28))
def test_windows(self):
claims = json.dumps(
[
{"claim": "sizing", "class": "size/growth", "pub_date": "2024-06"},
{"claim": "pricing", "class": "pricing", "pub_date": "2026-06"},
{"claim": "odd", "class": "unmapped", "pub_date": "2026-06"},
]
)
with tempfile.TemporaryDirectory() as tmp:
f = Path(tmp) / "claims.json"
f.write_text(claims, encoding="utf-8")
code, result = run(
[
"staleness",
str(f),
"--windows",
'{"size/growth": 18, "pricing": 3}',
"--today",
"2026-07-22",
]
)
self.assertEqual(result["stale_count"], 1) # sizing recheck 2025-12 < today
self.assertEqual(result["earliest_recheck"], "2025-12-01")
self.assertEqual(result["no_window_classes"], ["unmapped"])
self.assertEqual(code, 1)
def test_malformed_claims_exit_2(self):
for payload in ('{"x": 1}', '{"claims": [5]}'):
err = io.StringIO()
with tempfile.TemporaryDirectory() as tmp:
f = Path(tmp) / "claims.json"
f.write_text(payload, encoding="utf-8")
with redirect_stderr(err), redirect_stdout(io.StringIO()):
code = main(["staleness", str(f), "--windows", '{"pricing": 3}'])
self.assertEqual(code, 2, payload)
self.assertTrue(err.getvalue().startswith("error: "), payload)
def test_stdin_is_read_as_utf8_whatever_the_locale(self):
claims = json.dumps([{"claim": "café prices", "class": "pricing", "pub_date": "2026-06"}], ensure_ascii=False)
stdin = io.TextIOWrapper(io.BytesIO(claims.encode("utf-8")), encoding="latin-1")
original = sys.stdin
sys.stdin = stdin
try:
code, result = run(["staleness", "-", "--windows", '{"pricing": 3}', "--today", "2026-07-01"])
finally:
sys.stdin = original
self.assertEqual(result["claims"][0]["claim"], "café prices")
self.assertEqual(code, 0)
class SlugTest(unittest.TestCase):
def test_deterministic_folder(self):
self.assertEqual(slugify("Créme Brûlée: AI Tools!"), "creme-brulee-ai-tools")
code, result = run(["slug", "SMB Accounting SaaS", "--type", "market"])
self.assertEqual(result["folder"], "research-smb-accounting-saas")
self.assertEqual(code, 0)
pattern = "{research_type}-{topic_slug}-{date}"
code, result = run(
["slug", "SMB Accounting SaaS", "--type", "market", "--date", "2026-07-22", "--pattern", pattern]
)
self.assertEqual(result["folder"], "market-smb-accounting-saas-2026-07-22")
self.assertEqual(code, 0)
class EscapeSourcesTest(unittest.TestCase):
def test_escaping_and_url_validation(self):
with tempfile.TemporaryDirectory() as tmp:
report = Path(tmp) / "report.md"
report.write_text(REPORT, encoding="utf-8")
code, result = run(["escape-sources", str(report)])
self.assertEqual(result["rows"], 3)
self.assertTrue(any(u.startswith("javascript:") for u in result["invalid_urls"]))
self.assertNotIn("javascript:", result["html"]) # never linked
self.assertIn('href="https://example.com/g"', result["html"])
self.assertIn('id="src-1"', result["html"])
self.assertEqual(code, 1)
def test_urls_keep_balanced_parentheses(self):
text = (
"| [n] | Supports | Publisher |\n| --- | --- | --- |\n"
"| [1] | a | [Rust](https://en.wikipedia.org/wiki/Rust_(programming_language)) |\n"
"| [2] | b | https://en.wikipedia.org/wiki/Go_(programming_language) |\n"
)
code, result = run_text(["escape-sources"], text)
self.assertIn('href="https://en.wikipedia.org/wiki/Rust_(programming_language)"', result["html"])
self.assertIn('href="https://en.wikipedia.org/wiki/Go_(programming_language)"', result["html"])
self.assertEqual(result["invalid_urls"], [])
self.assertEqual(code, 0)
if __name__ == "__main__":
unittest.main()