429 lines
19 KiB
Python
429 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""wiki_claim_gate.py 회귀 고정 테스트 — check_markdown_write 행동 동치 (refactor 전후 동일)."""
|
|
import importlib.util
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
_SPEC = importlib.util.spec_from_file_location(
|
|
"wcg", str(Path(__file__).with_name("wiki_claim_gate.py")))
|
|
wcg = importlib.util.module_from_spec(_SPEC)
|
|
_SPEC.loader.exec_module(wcg)
|
|
|
|
CLAIMS_TABLE = (
|
|
"## Claims Extracted\n"
|
|
"| Claim ID | Claim | Evidence quote | Strength | Applies to | Does not prove |\n"
|
|
"|---|---|---|---|---|---|\n"
|
|
"| C1 | x | q | company-case-study | a | b |\n"
|
|
)
|
|
USAGE = "## Usage Boundaries\n- x\n"
|
|
DEM = (
|
|
"## Decision Evidence Map\n"
|
|
"| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |\n"
|
|
"|---|---|---|---|---|\n"
|
|
"| D1 | x | C1 | company-case-study | none |\n"
|
|
)
|
|
CTV = "## 검증해야 할 주장 / Claims To Verify\n- v\n"
|
|
|
|
|
|
class TestSourceNote(unittest.TestCase):
|
|
def test_missing_claims_table_blocks(self):
|
|
f = wcg.check_markdown_write("raw/official-docs/x.md", "# t\n" + USAGE)
|
|
self.assertTrue(any("Claims Extracted" in m for m in f))
|
|
|
|
def test_complete_source_note_passes(self):
|
|
f = wcg.check_markdown_write("raw/official-docs/x.md", "# t\n" + CLAIMS_TABLE + USAGE)
|
|
self.assertEqual(f, [])
|
|
|
|
def test_company_blog_same_rule(self):
|
|
f = wcg.check_markdown_write("raw/company-tech-blogs/x.md", "# t\n본문")
|
|
self.assertTrue(any("Claims Extracted" in m for m in f))
|
|
|
|
|
|
class TestBranchNote(unittest.TestCase):
|
|
def test_missing_dem_blocks(self):
|
|
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", "# t\n" + CTV)
|
|
self.assertTrue(any("Decision Evidence Map" in m for m in f))
|
|
|
|
def test_complete_branch_note_passes(self):
|
|
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", "# t\n" + DEM + CTV)
|
|
self.assertEqual(f, [])
|
|
|
|
def test_officially_supported_without_strength_blocks(self):
|
|
body = "# t\n" + DEM + CTV + "\n이 기능은 officially supported 된다.\n"
|
|
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", body)
|
|
self.assertTrue(any("official" in m.lower() for m in f))
|
|
|
|
def test_officially_supported_with_strength_passes(self):
|
|
body = "# t\n" + DEM + CTV + "\nofficially supported (official-vendor-doc).\n"
|
|
f = wcg.check_markdown_write("raw/branch-notes/feature-x.md", body)
|
|
self.assertEqual(f, [])
|
|
|
|
|
|
class TestConceptNote(unittest.TestCase):
|
|
def test_missing_claim_backed_blocks(self):
|
|
f = wcg.check_markdown_write("wiki/concepts/x.md", "# t\n본문")
|
|
self.assertTrue(any("Claim-backed Knowledge" in m for m in f))
|
|
|
|
|
|
class TestSpecReport(unittest.TestCase):
|
|
def test_complete_verdict_without_traceability_blocks(self):
|
|
f = wcg.check_markdown_write("docs/superpowers/specs/2026-01-01-x-report.md",
|
|
"Verdict: COMPLETE\n근거 없음")
|
|
self.assertTrue(any("traceability" in m.lower() for m in f))
|
|
|
|
|
|
INVEST_SOURCES = (
|
|
"## 출처 / Sources\n"
|
|
"| # | 제목 | 출처 등급 | URL | 발행/조사일 |\n"
|
|
"|---|---|---|---|---|\n"
|
|
"| S1 | x | official | url | 2025 |\n"
|
|
)
|
|
INVEST_QUOTES = '## 핵심 인용 / Key quotes (verbatim)\n> [S1] "원문"\n'
|
|
INVEST_CLAIMS = (
|
|
"## Claims Extracted / 추출된 주장\n"
|
|
"| Claim ID | Claim | Evidence quote | Strength | 적용 조건 | 증명 못 하는 것 |\n"
|
|
"|---|---|---|---|---|---|\n"
|
|
'| C1 | x | [S1] "q" | official | a | b |\n'
|
|
)
|
|
|
|
|
|
class TestInvestResearch(unittest.TestCase):
|
|
def test_missing_claims_blocks(self):
|
|
f = wcg.check_markdown_write("raw/invest-research/2026-06-08-x.md",
|
|
"# t\n" + INVEST_SOURCES + INVEST_QUOTES)
|
|
self.assertTrue(any("Claims Extracted" in m for m in f))
|
|
|
|
def test_missing_sources_blocks(self):
|
|
f = wcg.check_markdown_write("raw/invest-research/2026-06-08-x.md",
|
|
"# t\n" + INVEST_QUOTES + INVEST_CLAIMS)
|
|
self.assertTrue(any("출처" in m or "Sources" in m for m in f))
|
|
|
|
def test_missing_verbatim_blocks(self):
|
|
f = wcg.check_markdown_write("raw/invest-research/2026-06-08-x.md",
|
|
"# t\n" + INVEST_SOURCES + INVEST_CLAIMS)
|
|
self.assertTrue(any("핵심 인용" in m for m in f))
|
|
|
|
def test_complete_passes(self):
|
|
f = wcg.check_markdown_write("raw/invest-research/2026-06-08-x.md",
|
|
"# t\n" + INVEST_SOURCES + INVEST_QUOTES + INVEST_CLAIMS)
|
|
self.assertEqual(f, [])
|
|
|
|
|
|
class TestUnrelatedPath(unittest.TestCase):
|
|
def test_non_gated_path_passes(self):
|
|
f = wcg.check_markdown_write("raw/lectures/x.md", "# anything\n")
|
|
self.assertEqual(f, [])
|
|
|
|
def test_non_md_passes(self):
|
|
f = wcg.check_markdown_write("raw/official-docs/x.txt", "anything")
|
|
self.assertEqual(f, [])
|
|
|
|
|
|
import tempfile as _tf
|
|
|
|
PROJ_OK = "# t\n## 실제 구현 내용 (`actually-implemented`)\n- x\n## Sources\n- `[[raw/x]]`\n"
|
|
|
|
|
|
class TestWikiProjectsGate(unittest.TestCase):
|
|
"""P1-7: wiki/projects 증거 구조 게이트 + named-hub 면제."""
|
|
|
|
def test_missing_sections_blocks(self):
|
|
f = wcg.check_markdown_write("wiki/projects/ca-tmpl/x.md", "# t\n본문만")
|
|
self.assertTrue(any("실제 구현 내용" in m for m in f))
|
|
self.assertTrue(any("Sources" in m for m in f))
|
|
|
|
def test_complete_passes(self):
|
|
f = wcg.check_markdown_write("wiki/projects/ca-tmpl/x.md", PROJ_OK)
|
|
self.assertEqual(f, [])
|
|
|
|
def test_named_hub_exempt(self):
|
|
with _tf.TemporaryDirectory() as d:
|
|
root = Path(d)
|
|
(root / "wiki" / "projects" / "myproj").mkdir(parents=True)
|
|
f = wcg.check_markdown_write("wiki/projects/myproj.md", "# hub\nMOC만", root=root)
|
|
self.assertEqual(f, [])
|
|
|
|
|
|
def _mk_canonical(root, rel, status):
|
|
p = root / (rel + ".md")
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(f"---\ntitle: x\nstatus: {status}\n---\n# t\n", encoding="utf-8")
|
|
|
|
|
|
class TestDerivedGate(unittest.TestCase):
|
|
"""P1-7/8: 파생 산출물 canonical 경유 + 원천 status 게이트."""
|
|
|
|
def test_no_sources_section_blocks(self):
|
|
f = wcg.check_markdown_write("wiki/blog/x.md", "# t\n본문")
|
|
self.assertTrue(any("Sources" in m for m in f))
|
|
|
|
def test_no_canonical_link_blocks(self):
|
|
f = wcg.check_markdown_write("wiki/interview/x.md", "# t\n## Sources\n- 외부 링크만\n")
|
|
self.assertTrue(any("canonical wikilink" in m for m in f))
|
|
|
|
def test_portfolio_requires_projects_link(self):
|
|
f = wcg.check_markdown_write(
|
|
"wiki/portfolio/x.md", "# t\n## Sources\n- [[wiki/concepts/a]]\n")
|
|
self.assertTrue(any("wiki/projects" in m for m in f))
|
|
|
|
def test_draft_canonical_source_blocks(self):
|
|
with _tf.TemporaryDirectory() as d:
|
|
root = Path(d)
|
|
_mk_canonical(root, "wiki/concepts/a", "draft")
|
|
f = wcg.check_markdown_write(
|
|
"wiki/blog/x.md", "# t\n## Sources\n- [[wiki/concepts/a]]\n", root=root)
|
|
self.assertTrue(any("status 게이트" in m for m in f))
|
|
|
|
def test_reviewed_canonical_source_passes(self):
|
|
with _tf.TemporaryDirectory() as d:
|
|
root = Path(d)
|
|
_mk_canonical(root, "wiki/concepts/a", "reviewed")
|
|
f = wcg.check_markdown_write(
|
|
"wiki/blog/x.md", "# t\n## Sources\n- [[wiki/concepts/a]]\n", root=root)
|
|
self.assertEqual(f, [])
|
|
|
|
def test_mixed_one_draft_blocks(self):
|
|
with _tf.TemporaryDirectory() as d:
|
|
root = Path(d)
|
|
_mk_canonical(root, "wiki/concepts/a", "verified")
|
|
_mk_canonical(root, "wiki/projects/b", "draft")
|
|
f = wcg.check_markdown_write(
|
|
"wiki/interview/x.md",
|
|
"# t\n## Sources\n- [[wiki/concepts/a]]\n- [[wiki/projects/b]]\n", root=root)
|
|
self.assertTrue(any("wiki/projects/b" in m for m in f))
|
|
|
|
def test_explainer_status_exempt(self):
|
|
with _tf.TemporaryDirectory() as d:
|
|
root = Path(d)
|
|
_mk_canonical(root, "wiki/concepts/a", "draft")
|
|
f = wcg.check_markdown_write(
|
|
"wiki/explainer/x.md", "# t\n## Sources\n- [[wiki/concepts/a]]\n", root=root)
|
|
self.assertEqual(f, []) # explainer 는 canonical 경유만, status 면제
|
|
|
|
|
|
INVEST_DAILY_BASE = (
|
|
"# t\n## 고정 체크리스트 (매일 동일)\n"
|
|
"| 자산군 | 핵심 지표 | 값 / 방향 | 출처 | 조사시점 |\n"
|
|
"|---|---|---|---|---|\n"
|
|
"{rows}\n"
|
|
"## 출처 / Sources (deep-research 조사 기록)\n- x\n"
|
|
)
|
|
|
|
|
|
class TestInvestDailyGate(unittest.TestCase):
|
|
"""P1-7: invest-daily 수치행 출처/조사시점 강제 (빈 행은 허용)."""
|
|
|
|
def test_value_without_source_blocks(self):
|
|
text = INVEST_DAILY_BASE.format(rows="| 금리 | 미 10Y | 4.4% ↑ | | |")
|
|
f = wcg.check_markdown_write("raw/invest-daily/2026-06-10.md", text)
|
|
self.assertTrue(any("출처 비어있음" in m for m in f))
|
|
|
|
def test_value_without_time_blocks(self):
|
|
text = INVEST_DAILY_BASE.format(rows="| 금리 | 미 10Y | 4.4% ↑ | [x](https://a) | |")
|
|
f = wcg.check_markdown_write("raw/invest-daily/2026-06-10.md", text)
|
|
self.assertTrue(any("조사시점 비어있음" in m for m in f))
|
|
|
|
def test_empty_row_allowed(self):
|
|
text = INVEST_DAILY_BASE.format(rows="| 금리 | 미 10Y | | | |")
|
|
f = wcg.check_markdown_write("raw/invest-daily/2026-06-10.md", text)
|
|
self.assertEqual(f, [])
|
|
|
|
def test_complete_row_passes(self):
|
|
text = INVEST_DAILY_BASE.format(
|
|
rows="| 금리 | 미 10Y | 4.4% ↑ | [FRED](https://a) | 2026-06-10 09:00 KST |")
|
|
f = wcg.check_markdown_write("raw/invest-daily/2026-06-10.md", text)
|
|
self.assertEqual(f, [])
|
|
|
|
def test_missing_sections_blocks(self):
|
|
f = wcg.check_markdown_write("raw/invest-daily/2026-06-10.md", "# t\n본문만")
|
|
self.assertTrue(any("고정 체크리스트" in m for m in f))
|
|
|
|
|
|
def _run_main_stop(message):
|
|
ev = {"hook_event_name": "Stop", "last_assistant_message": message}
|
|
return _sp.run(["python3", _GATE, "--main-stop"], input=_json.dumps(ev),
|
|
capture_output=True, text=True)
|
|
|
|
|
|
class TestMainStopGate(unittest.TestCase):
|
|
"""P1-9: Claude main agent Stop — fenced wiki-stats 만 검증 (COMPLETE trap 미적용)."""
|
|
|
|
def test_imbalanced_stats_blocks(self):
|
|
msg = "끝.\n```wiki-stats\nagent: branch-spec\nfound: 9\nprocessed: 7\ndropped: 0\n```"
|
|
self.assertEqual(_run_main_stop(msg).returncode, 2)
|
|
|
|
def test_balanced_stats_allows(self):
|
|
msg = "끝.\n```wiki-stats\nagent: branch-spec\nfound: 9\nprocessed: 7\ndropped: 2\ndropped_reason: 2 deferred\n```"
|
|
self.assertEqual(_run_main_stop(msg).returncode, 0)
|
|
|
|
def test_complete_without_traceability_allows(self):
|
|
# main agent 의 메타 대화 ("Verdict: COMPLETE" 인용) 는 차단하지 않는다.
|
|
self.assertEqual(_run_main_stop("훅이 Verdict: COMPLETE 를 검사한다").returncode, 0)
|
|
|
|
def test_malformed_verdict_block_allows(self):
|
|
# wiki-verdict 는 main-stop 검증 범위 밖 (judge 는 항상 subagent).
|
|
msg = "예시:\n```wiki-verdict\nagent: x\nverdict: ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"
|
|
self.assertEqual(_run_main_stop(msg).returncode, 0)
|
|
|
|
def test_no_marker_allows(self):
|
|
self.assertEqual(_run_main_stop("일반 응답").returncode, 0)
|
|
|
|
|
|
import json as _json
|
|
import subprocess as _sp
|
|
|
|
_GATE = str(Path(__file__).with_name("wiki_claim_gate.py"))
|
|
|
|
|
|
def _run_stop(message):
|
|
ev = {"hook_event_name": "SubagentStop", "last_assistant_message": message}
|
|
return _sp.run(["python3", _GATE], input=_json.dumps(ev), capture_output=True, text=True)
|
|
|
|
|
|
class TestSubagentStopVerdict(unittest.TestCase):
|
|
def test_malformed_verdict_blocks(self):
|
|
msg = "리뷰 끝.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"
|
|
self.assertEqual(_run_stop(msg).returncode, 2)
|
|
|
|
def test_valid_verdict_allows(self):
|
|
msg = "리뷰 끝.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 0\nshould_fix: 0\nadvisory: 0\n```"
|
|
self.assertEqual(_run_stop(msg).returncode, 0)
|
|
|
|
def test_no_marker_allows(self):
|
|
self.assertEqual(_run_stop("그냥 일반 subagent 출력, 마커 없음").returncode, 0)
|
|
|
|
def test_worker_status_done_allows(self):
|
|
# wiki-source-summarizer 성공 출력은 `**Status:** DONE` — branch-traceability 와 무관.
|
|
# bare DONE 으로 Decision Evidence Map / UNSUPPORTED_DECISION 을 요구하면 안 됨.
|
|
msg = "raw 자료 생성 완료.\n## Claims Extracted\n| Claim ID | ... |\n\n**Status:** DONE"
|
|
self.assertEqual(_run_stop(msg).returncode, 0)
|
|
|
|
def test_audit_verdict_complete_without_traceability_blocks(self):
|
|
# 감사/리뷰 완료 주장(Verdict: COMPLETE)은 여전히 traceability 누락 시 차단.
|
|
msg = "# Audit\n**Verdict:** COMPLETE\n근거 없음"
|
|
self.assertEqual(_run_stop(msg).returncode, 2)
|
|
|
|
|
|
def _run_stop_as(agent_type, message):
|
|
ev = {"hook_event_name": "SubagentStop", "agent_type": agent_type,
|
|
"last_assistant_message": message}
|
|
return _sp.run(["python3", _GATE], input=_json.dumps(ev), capture_output=True, text=True)
|
|
|
|
|
|
class TestSubagentStopAgentTypeScoping(unittest.TestCase):
|
|
"""P0-1: 위키 출력 계약은 WIKI_AGENT_TYPES 에만 적용 — 범용 subagent 오차단 방지.
|
|
(실측 재현 2026-06-10: Explore 가 'Verdict: COMPLETE' 한 마디로 차단 → 이탈 재시도)"""
|
|
|
|
def test_non_wiki_agent_complete_allows(self):
|
|
r = _run_stop_as("Explore", "PROBE OK — Verdict: COMPLETE")
|
|
self.assertEqual(r.returncode, 0)
|
|
|
|
def test_non_wiki_agent_malformed_verdict_block_allows(self):
|
|
# 보고서에 인용된 (모순된) 예시 블록도 범용 에이전트에선 차단 사유가 아님.
|
|
msg = "감사 예시:\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"
|
|
self.assertEqual(_run_stop_as("general-purpose", msg).returncode, 0)
|
|
|
|
def test_wiki_agent_complete_without_traceability_blocks(self):
|
|
msg = "# Audit\n**Verdict:** COMPLETE\n근거 없음"
|
|
self.assertEqual(_run_stop_as("wiki-research-lane", msg).returncode, 2)
|
|
|
|
def test_wiki_agent_malformed_verdict_blocks(self):
|
|
msg = "리뷰 끝.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"
|
|
self.assertEqual(_run_stop_as("branch-depth-auditor", msg).returncode, 2)
|
|
|
|
def test_missing_agent_type_still_validates(self):
|
|
# 타 플랫폼(Gemini AfterAgent 등) — agent_type 부재 시 기존 보수적 검증 유지.
|
|
msg = "# Audit\n**Verdict:** COMPLETE\n근거 없음"
|
|
self.assertEqual(_run_stop(msg).returncode, 2)
|
|
|
|
|
|
class TestRetryRevalidation(unittest.TestCase):
|
|
"""P2-22: stop_hook_active(재시도)에도 위키 에이전트 스키마 위반은 계속 차단."""
|
|
|
|
def _run_retry(self, agent_type, message):
|
|
ev = {"hook_event_name": "SubagentStop", "agent_type": agent_type,
|
|
"stop_hook_active": True, "last_assistant_message": message}
|
|
return _sp.run(["python3", _GATE], input=_json.dumps(ev),
|
|
capture_output=True, text=True)
|
|
|
|
def test_retry_malformed_verdict_still_blocks(self):
|
|
msg = "리뷰.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"
|
|
self.assertEqual(self._run_retry("branch-depth-auditor", msg).returncode, 2)
|
|
|
|
def test_retry_valid_passes(self):
|
|
msg = "리뷰.\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: not-ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"
|
|
self.assertEqual(self._run_retry("branch-depth-auditor", msg).returncode, 0)
|
|
|
|
def test_retry_non_wiki_agent_still_scoped_out(self):
|
|
self.assertEqual(self._run_retry("Explore", "Verdict: COMPLETE").returncode, 0)
|
|
|
|
def test_main_stop_retry_keeps_one_retry(self):
|
|
# main_stop_gate 는 one-retry 유지 (대화 흐름 보호).
|
|
ev = {"hook_event_name": "Stop", "stop_hook_active": True,
|
|
"last_assistant_message": "x\n```wiki-stats\nagent: a\nfound: 2\nprocessed: 1\ndropped: 0\n```"}
|
|
r = _sp.run(["python3", _GATE, "--main-stop"], input=_json.dumps(ev),
|
|
capture_output=True, text=True)
|
|
self.assertEqual(r.returncode, 0)
|
|
|
|
|
|
def _run_gate(event, *extra_args):
|
|
return _sp.run(["python3", _GATE, *extra_args], input=_json.dumps(event),
|
|
capture_output=True, text=True)
|
|
|
|
|
|
class TestAntigravityMode(unittest.TestCase):
|
|
def _write_event(self, rel, content):
|
|
return {"hook_event_name": "PreToolUse", "tool_name": "Write",
|
|
"tool_input": {"file_path": rel, "content": content}}
|
|
|
|
def test_deny_emits_decision_json_exit0(self):
|
|
ev = self._write_event("raw/branch-notes/x.md", "# t\n본문만, DEM 없음")
|
|
r = _run_gate(ev, "--antigravity")
|
|
self.assertEqual(r.returncode, 0) # antigravity: exit 0, deny via JSON
|
|
out = _json.loads(r.stdout)
|
|
self.assertEqual(out["decision"], "deny")
|
|
self.assertIn("reason", out)
|
|
|
|
def test_allow_emits_decision_json(self):
|
|
body = ("# t\n## Decision Evidence Map\n"
|
|
"| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |\n"
|
|
"|---|---|---|---|---|\n| D1 | x | C1 | company-case-study | none |\n"
|
|
"## 검증해야 할 주장 / Claims To Verify\n- v\n")
|
|
ev = self._write_event("raw/branch-notes/x.md", body)
|
|
r = _run_gate(ev, "--antigravity")
|
|
self.assertEqual(r.returncode, 0)
|
|
self.assertEqual(_json.loads(r.stdout)["decision"], "allow")
|
|
|
|
def test_afteragent_prompt_response_verdict_deny(self):
|
|
# Gemini AfterAgent: 에이전트 출력은 prompt_response, 이벤트명 AfterAgent
|
|
ev = {"hook_event_name": "AfterAgent",
|
|
"prompt_response": "x\n```wiki-verdict\nagent: branch-depth-auditor\nverdict: ready\nblocking: 3\nshould_fix: 0\nadvisory: 0\n```"}
|
|
r = _run_gate(ev, "--antigravity")
|
|
self.assertEqual(r.returncode, 0)
|
|
self.assertEqual(_json.loads(r.stdout)["decision"], "deny")
|
|
|
|
def test_non_antigravity_still_exit2(self):
|
|
# 회귀: --antigravity 없으면 Claude exit-code 규약 그대로
|
|
ev = self._write_event("raw/branch-notes/x.md", "# t\n본문만, DEM 없음")
|
|
self.assertEqual(_run_gate(ev).returncode, 2)
|
|
|
|
|
|
class TestSubagentStopStats(unittest.TestCase):
|
|
def test_imbalanced_stats_blocks(self):
|
|
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 0\n```"
|
|
self.assertEqual(_run_stop(msg).returncode, 2)
|
|
|
|
def test_balanced_stats_allows(self):
|
|
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 10\nprocessed: 10\ndropped: 0\n```"
|
|
self.assertEqual(_run_stop(msg).returncode, 0)
|
|
|
|
def test_dropped_without_reason_blocks(self):
|
|
msg = "x\n```wiki-stats\nagent: coverage-auditor\nfound: 12\nprocessed: 10\ndropped: 2\n```"
|
|
self.assertEqual(_run_stop(msg).returncode, 2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|