202 lines
7.8 KiB
Python
202 lines
7.8 KiB
Python
|
|
"""Unit tests for C-32 (work-record registration: intake/decision kinds,
|
||
|
|
CUST-WP-0061-T02) — detection and file write-back only. The live
|
||
|
|
create-in-DB path is covered by the fix_repo integration path, proven
|
||
|
|
manually against a real repo/API (see CUST-WP-0061 progress notes); these
|
||
|
|
tests stay offline and use a synthetic registry fixture, matching the
|
||
|
|
conventions in test_work_record_check.py.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import sys
|
||
|
|
import textwrap
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import yaml
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||
|
|
|
||
|
|
from consistency_check import ( # noqa: E402
|
||
|
|
ConsistencyReport,
|
||
|
|
_check_work_record_registration,
|
||
|
|
_classify_work_record_kind,
|
||
|
|
_inject_yaml_block_field,
|
||
|
|
_load_work_record_kind_registry,
|
||
|
|
)
|
||
|
|
|
||
|
|
REGISTRY_YAML = textwrap.dedent(
|
||
|
|
"""
|
||
|
|
version: "0.1"
|
||
|
|
kinds:
|
||
|
|
- kind: intake
|
||
|
|
id_patterns: ['^[A-Z]+-IN-[0-9]{4}$']
|
||
|
|
legacy_patterns:
|
||
|
|
- pattern: '^AWQ-[0-9]{3}$'
|
||
|
|
source: binky AutopilotWorkQueue
|
||
|
|
- kind: decision
|
||
|
|
id_patterns: ['^[A-Z]+-DEC-[0-9]{4}-[0-9]{3}$']
|
||
|
|
legacy_patterns:
|
||
|
|
- pattern: '^DEC-[0-9]{4}-[0-9]{3}$'
|
||
|
|
source: binky DecisionQueue
|
||
|
|
- kind: engagement
|
||
|
|
id_patterns: ['^[A-Z]+-ENG-[0-9]{4}-[0-9]{3}$']
|
||
|
|
legacy_patterns:
|
||
|
|
- pattern: '^OH-[0-9]{4}-[0-9]{3}$'
|
||
|
|
source: binky OfficeHourQueue
|
||
|
|
- kind: workplan
|
||
|
|
id_patterns: ['^[A-Z]+-WP-[0-9]{4}$']
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def registry_file(tmp_path, monkeypatch):
|
||
|
|
reg = tmp_path / "work-record-types.yaml"
|
||
|
|
reg.write_text(REGISTRY_YAML, encoding="utf-8")
|
||
|
|
monkeypatch.setenv("WORK_RECORD_REGISTRY", str(reg))
|
||
|
|
return reg
|
||
|
|
|
||
|
|
|
||
|
|
class TestClassifyWorkRecordKind:
|
||
|
|
def test_classifies_intake(self, registry_file):
|
||
|
|
kinds = _load_work_record_kind_registry()
|
||
|
|
assert _classify_work_record_kind("BINKY-IN-0001", kinds) == "intake"
|
||
|
|
|
||
|
|
def test_classifies_legacy_awq(self, registry_file):
|
||
|
|
kinds = _load_work_record_kind_registry()
|
||
|
|
assert _classify_work_record_kind("AWQ-010", kinds) == "intake"
|
||
|
|
|
||
|
|
def test_classifies_decision(self, registry_file):
|
||
|
|
kinds = _load_work_record_kind_registry()
|
||
|
|
assert _classify_work_record_kind("DEC-2026-004", kinds) == "decision"
|
||
|
|
|
||
|
|
def test_unregistered_returns_none(self, registry_file):
|
||
|
|
kinds = _load_work_record_kind_registry()
|
||
|
|
assert _classify_work_record_kind("FOO-QX-001", kinds) is None
|
||
|
|
|
||
|
|
|
||
|
|
class TestCheckWorkRecordRegistration:
|
||
|
|
def _report(self, repo_dir: Path) -> ConsistencyReport:
|
||
|
|
report = ConsistencyReport(repo_slug="test", repo_path=str(repo_dir))
|
||
|
|
_check_work_record_registration(repo_dir, report)
|
||
|
|
return report
|
||
|
|
|
||
|
|
def test_intake_without_hub_id_is_flagged_fixable(self, tmp_path, registry_file):
|
||
|
|
(tmp_path / "queue.md").write_text(
|
||
|
|
"```yaml\nid: BINKY-IN-0001\ntitle: needs registration\n```\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
report = self._report(tmp_path)
|
||
|
|
assert len(report.issues) == 1
|
||
|
|
issue = report.issues[0]
|
||
|
|
assert issue.check_id == "C-32"
|
||
|
|
assert issue.fixable is True
|
||
|
|
assert issue._fix_context["kind"] == "intake"
|
||
|
|
assert issue._fix_context["rid"] == "BINKY-IN-0001"
|
||
|
|
|
||
|
|
def test_intake_with_hub_id_is_skipped(self, tmp_path, registry_file):
|
||
|
|
(tmp_path / "queue.md").write_text(
|
||
|
|
'```yaml\nid: BINKY-IN-0001\ntitle: already linked\n'
|
||
|
|
'state_hub_intake_id: "019f8000-0000-7000-8000-000000000000"\n```\n',
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
report = self._report(tmp_path)
|
||
|
|
assert report.issues == []
|
||
|
|
|
||
|
|
def test_decision_without_hub_id_is_flagged_fixable(self, tmp_path, registry_file):
|
||
|
|
(tmp_path / "DecisionQueue.md").write_text(
|
||
|
|
"```yaml\nid: DEC-2026-005\ntitle: a new decision\n```\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
report = self._report(tmp_path)
|
||
|
|
assert len(report.issues) == 1
|
||
|
|
assert report.issues[0]._fix_context["kind"] == "decision"
|
||
|
|
|
||
|
|
def test_engagement_without_hub_id_is_info_not_fixable(self, tmp_path, registry_file):
|
||
|
|
(tmp_path / "OfficeHourQueue.md").write_text(
|
||
|
|
"```yaml\nid: OH-2026-001\ntitle: bank call\n```\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
report = self._report(tmp_path)
|
||
|
|
assert len(report.issues) == 1
|
||
|
|
assert report.issues[0].severity == "INFO"
|
||
|
|
assert report.issues[0].fixable is False
|
||
|
|
|
||
|
|
def test_workplan_kind_is_untouched_by_c32(self, tmp_path, registry_file):
|
||
|
|
"""Workplans keep their own C-06 registration path; C-32 must not
|
||
|
|
double-register them."""
|
||
|
|
(tmp_path / "workplans" / "CUST-WP-0099-x.md").parent.mkdir(parents=True)
|
||
|
|
(tmp_path / "workplans" / "CUST-WP-0099-x.md").write_text(
|
||
|
|
"---\nid: CUST-WP-0099\n---\n```yaml\nid: CUST-WP-0099\ntitle: x\n```\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
report = self._report(tmp_path)
|
||
|
|
assert report.issues == []
|
||
|
|
|
||
|
|
def test_template_placeholder_ignored(self, tmp_path, registry_file):
|
||
|
|
(tmp_path / "AGENTS.md").write_text(
|
||
|
|
"```yaml\nid: BINKY-IN-NNNN\ntitle: template\n```\n", encoding="utf-8"
|
||
|
|
)
|
||
|
|
report = self._report(tmp_path)
|
||
|
|
assert report.issues == []
|
||
|
|
|
||
|
|
def test_no_op_when_registry_unavailable(self, tmp_path, monkeypatch):
|
||
|
|
monkeypatch.setenv("WORK_RECORD_REGISTRY", str(tmp_path / "missing.yaml"))
|
||
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path / "nohome")
|
||
|
|
(tmp_path / "queue.md").write_text(
|
||
|
|
"```yaml\nid: BINKY-IN-0001\ntitle: x\n```\n", encoding="utf-8"
|
||
|
|
)
|
||
|
|
report = self._report(tmp_path)
|
||
|
|
assert report.issues == []
|
||
|
|
|
||
|
|
|
||
|
|
class TestInjectYamlBlockField:
|
||
|
|
def test_injects_field_into_matching_block(self, tmp_path):
|
||
|
|
md = tmp_path / "queue.md"
|
||
|
|
md.write_text(
|
||
|
|
"```yaml\nid: BINKY-IN-0001\ntitle: needs registration\n```\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
changed = _inject_yaml_block_field(
|
||
|
|
md, "state_hub_intake_id", "019f8000-0000-7000-8000-000000000000", "BINKY-IN-0001"
|
||
|
|
)
|
||
|
|
assert changed is True
|
||
|
|
text = md.read_text()
|
||
|
|
assert 'state_hub_intake_id: "019f8000-0000-7000-8000-000000000000"' in text
|
||
|
|
# round-trips as valid yaml
|
||
|
|
block = text.split("```yaml\n", 1)[1].rsplit("```", 1)[0]
|
||
|
|
parsed = yaml.safe_load(block)
|
||
|
|
assert parsed["state_hub_intake_id"] == "019f8000-0000-7000-8000-000000000000"
|
||
|
|
|
||
|
|
def test_does_not_touch_other_blocks(self, tmp_path):
|
||
|
|
md = tmp_path / "queue.md"
|
||
|
|
md.write_text(
|
||
|
|
"```yaml\nid: BINKY-IN-0001\ntitle: first\n```\n"
|
||
|
|
"```yaml\nid: BINKY-IN-0002\ntitle: second\n```\n",
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
_inject_yaml_block_field(md, "state_hub_intake_id", "abc-123", "BINKY-IN-0001")
|
||
|
|
text = md.read_text()
|
||
|
|
assert "abc-123" in text
|
||
|
|
second_block = text.split("BINKY-IN-0002")[1]
|
||
|
|
assert "state_hub_intake_id" not in second_block
|
||
|
|
|
||
|
|
def test_no_op_when_id_not_found(self, tmp_path):
|
||
|
|
md = tmp_path / "queue.md"
|
||
|
|
original = "```yaml\nid: BINKY-IN-0001\ntitle: x\n```\n"
|
||
|
|
md.write_text(original, encoding="utf-8")
|
||
|
|
changed = _inject_yaml_block_field(md, "state_hub_intake_id", "abc-123", "BINKY-IN-9999")
|
||
|
|
assert changed is False
|
||
|
|
assert md.read_text() == original
|
||
|
|
|
||
|
|
def test_idempotent_when_field_already_set(self, tmp_path):
|
||
|
|
md = tmp_path / "queue.md"
|
||
|
|
md.write_text(
|
||
|
|
'```yaml\nid: BINKY-IN-0001\nstate_hub_intake_id: "already-there"\n```\n',
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
changed = _inject_yaml_block_field(md, "state_hub_intake_id", "new-value", "BINKY-IN-0001")
|
||
|
|
assert changed is False
|
||
|
|
assert "already-there" in md.read_text()
|
||
|
|
assert "new-value" not in md.read_text()
|