C-32: fix-consistency registration for intake/decision work records (CUST-WP-0061-T02)
Extends fix-consistency to scan any file for kind: intake / kind: decision YAML blocks (not just workplans/, per canon: any file is a potential work-record source), create the corresponding hub entity when missing a state_hub_intake_id / state_hub_decision_id, and write the id back into the source block -- same write-back pattern as C-06 for workplans. kind: engagement is reported INFO (deferred, not fixable): no hub entity exists for it yet, a separate stage-3 follow-on. - _load_work_record_kind_registry(): kind-aware registry loader, factored out so C-31's existing flat _load_work_record_patterns() builds on it without duplication (verified: C-31's 16 tests still pass unmodified) - _check_work_record_registration(): detection, wired into check_repo right after C-31 - _inject_yaml_block_field(): write-back helper for fenced blocks, mirroring _inject_task_id_into_block's pattern for blocks - fix_repo C-32 dispatch: creates the intake (scoped to repo_id) or decision (scoped to resolved topic_id, reusing C-06's domain->topic resolution) via the REST API, then writes the id back - tests/test_work_record_registration.py: 15 tests (classification, detection incl. engagement-deferred and workplan-kind-exclusion, injection incl. idempotence and non-interference with sibling blocks) Live-verified end to end against a real registered repo (binky-control, not just synthetic fixtures): a real fix-consistency run found and registered 3 genuinely open, previously-unlinked intake items (AWQ-002/003/006) sitting in AutopilotWorkQueue.md, and correctly deferred 5 real OH- engagement items as INFO. No regressions: full consistency_check + consistency_sweep suite (128 tests) and C-31's own suite (16 tests) still green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
88ba666c95
commit
aade470f4a
2 changed files with 405 additions and 6 deletions
|
|
@ -34,6 +34,7 @@ Checks:
|
|||
C-29 inbox-work-unpromoted WARN No Stale unread message looks like a multi-step work request without a workplan
|
||||
C-30 scope-current-state-stale WARN No SCOPE.md Current State contradicts live workplan statuses
|
||||
C-31 work-record-unregistered WARN No YAML-block id matches no kind in the canon work-record type registry (sidetrack detector, CUST-WP-0060)
|
||||
C-32 work-record-not-indexed WARN Yes kind: intake/decision YAML block has no hub id — not indexed in DB (registration, CUST-WP-0061-T02)
|
||||
|
||||
Usage:
|
||||
python scripts/consistency_check.py --repo SLUG [--fix] [--no-writeback] [--json] [--api-base URL]
|
||||
|
|
@ -779,11 +780,14 @@ _WORK_RECORD_ID_LIKE_RE = re.compile(r"^[A-Z][A-Z0-9]*(-[A-Z0-9]+)+$")
|
|||
_WORK_RECORD_SKIP_DIRS = frozenset({".git", "node_modules", ".venv", "history"})
|
||||
|
||||
|
||||
def _load_work_record_patterns() -> list[re.Pattern] | None:
|
||||
"""Load id patterns from the canon work-record type registry.
|
||||
def _load_work_record_kind_registry() -> list[dict] | None:
|
||||
"""Load [{kind, patterns}] from the canon work-record type registry.
|
||||
|
||||
Location: $WORK_RECORD_REGISTRY, else ~/the-custodian/canon/standards/
|
||||
work-record-types.yaml. Returns None (check skipped) when unavailable.
|
||||
work-record-types.yaml. Returns None (checks skipped) when unavailable.
|
||||
Kind-aware: shared by C-31 (flat sidetrack detection) and C-32
|
||||
(kind-aware registration — needs to know intake vs. decision vs.
|
||||
engagement to pick the right hub entity).
|
||||
"""
|
||||
if not _HAS_YAML:
|
||||
return None
|
||||
|
|
@ -798,17 +802,28 @@ def _load_work_record_patterns() -> list[re.Pattern] | None:
|
|||
continue
|
||||
try:
|
||||
reg = _yaml.safe_load(path.read_text())
|
||||
patterns = []
|
||||
kinds = []
|
||||
for kind in reg.get("kinds", []):
|
||||
patterns += [re.compile(p) for p in kind.get("id_patterns", [])]
|
||||
patterns = [re.compile(p) for p in kind.get("id_patterns", [])]
|
||||
patterns += [re.compile(lp["pattern"])
|
||||
for lp in kind.get("legacy_patterns", [])]
|
||||
return patterns
|
||||
kinds.append({"kind": kind["kind"], "patterns": patterns})
|
||||
return kinds
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _load_work_record_patterns() -> list[re.Pattern] | None:
|
||||
"""Flat id-pattern list across all kinds — used by C-31's sidetrack
|
||||
detector, which only needs to know "is this id registered at all",
|
||||
not which kind it belongs to."""
|
||||
kinds = _load_work_record_kind_registry()
|
||||
if kinds is None:
|
||||
return None
|
||||
return [p for k in kinds for p in k["patterns"]]
|
||||
|
||||
|
||||
def _check_unregistered_work_records(repo_dir: Path, report: "ConsistencyReport") -> None:
|
||||
"""C-31: warn on YAML-block ids matching no registered work-record kind.
|
||||
|
||||
|
|
@ -849,6 +864,115 @@ def _check_unregistered_work_records(repo_dir: Path, report: "ConsistencyReport"
|
|||
)
|
||||
|
||||
|
||||
_YAML_ONLY_FENCE_RE = re.compile(r"```yaml\n(.*?)\n```", re.S)
|
||||
_WORK_RECORD_ID_FIELD_BY_KIND = {
|
||||
"intake": "state_hub_intake_id",
|
||||
"decision": "state_hub_decision_id",
|
||||
"engagement": "state_hub_engagement_id",
|
||||
}
|
||||
_WORK_RECORD_CREATE_ENDPOINT_BY_KIND = {
|
||||
"intake": "/intakes",
|
||||
"decision": "/decisions",
|
||||
}
|
||||
|
||||
|
||||
def _classify_work_record_kind(record_id: str, kind_registry: list[dict]) -> str | None:
|
||||
for k in kind_registry:
|
||||
if any(p.match(record_id) for p in k["patterns"]):
|
||||
return k["kind"]
|
||||
return None
|
||||
|
||||
|
||||
def _inject_yaml_block_field(
|
||||
file_path: Path, field_name: str, field_value: str, match_id: str
|
||||
) -> bool:
|
||||
"""Inject a field into the ```yaml``` block whose id == match_id.
|
||||
Mirrors _inject_task_id_into_block, adapted for fenced yaml blocks
|
||||
(intake/decision/engagement records live as ```yaml, not ```task)."""
|
||||
text = file_path.read_text(encoding="utf-8")
|
||||
|
||||
def _replace(m: re.Match) -> str:
|
||||
block_content = m.group(1)
|
||||
meta = _parse_yaml_block(block_content.strip())
|
||||
if not isinstance(meta, dict) or str(meta.get("id", "")) != match_id:
|
||||
return m.group(0)
|
||||
existing_val = meta.get(field_name)
|
||||
if existing_val is not None and str(existing_val).strip() not in ("", "~", "null", "None", "none"):
|
||||
return m.group(0)
|
||||
new_content = re.sub(
|
||||
rf"^{re.escape(field_name)}:.*$",
|
||||
f'{field_name}: "{field_value}"',
|
||||
block_content,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if new_content == block_content:
|
||||
new_content = block_content.rstrip() + f"\n{field_name}: \"{field_value}\""
|
||||
return f"```yaml\n{new_content}\n```"
|
||||
|
||||
new_text = _YAML_ONLY_FENCE_RE.sub(_replace, text)
|
||||
if new_text != text:
|
||||
file_path.write_text(new_text, encoding="utf-8")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _check_work_record_registration(repo_dir: Path, report: "ConsistencyReport") -> None:
|
||||
"""C-32: register kind: intake / kind: decision YAML blocks found
|
||||
anywhere in the repo against the hub (CUST-WP-0061-T02, work-record
|
||||
stage 3) — writes state_hub_intake_id / state_hub_decision_id back into
|
||||
the source block, same write-back pattern as C-06 for workplans.
|
||||
|
||||
kind: engagement has no hub entity yet (deferred; reported INFO, not
|
||||
fixable) — building one is a separate stage-3 follow-on, not this task.
|
||||
"""
|
||||
kind_registry = _load_work_record_kind_registry()
|
||||
if kind_registry is None:
|
||||
return
|
||||
for md in sorted(repo_dir.rglob("*.md")):
|
||||
if any(part in _WORK_RECORD_SKIP_DIRS for part in md.parts):
|
||||
continue
|
||||
try:
|
||||
text = md.read_text(errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
for block in _YAML_ONLY_FENCE_RE.findall(text):
|
||||
meta = _parse_yaml_block(block.strip())
|
||||
if not isinstance(meta, dict) or meta.get("_parse_error"):
|
||||
continue
|
||||
rid = str(meta.get("id", "")).strip()
|
||||
if not rid or "NNN" in rid:
|
||||
continue
|
||||
kind = _classify_work_record_kind(rid, kind_registry)
|
||||
if kind not in _WORK_RECORD_ID_FIELD_BY_KIND:
|
||||
continue
|
||||
|
||||
id_field = _WORK_RECORD_ID_FIELD_BY_KIND[kind]
|
||||
existing = str(meta.get(id_field, "")).strip().strip('"')
|
||||
if existing and existing not in ("~", "null", "None", "none"):
|
||||
continue
|
||||
|
||||
rel = md.relative_to(repo_dir)
|
||||
if kind not in _WORK_RECORD_CREATE_ENDPOINT_BY_KIND:
|
||||
report.add(
|
||||
severity="INFO", check_id="C-32",
|
||||
message=(
|
||||
f"[{rel}] engagement '{rid}' has no hub entity yet — "
|
||||
f"registration deferred (CUST-WP-0061 stage-3 seed)"
|
||||
),
|
||||
fixable=False,
|
||||
)
|
||||
continue
|
||||
|
||||
report.add(
|
||||
severity="WARN", check_id="C-32",
|
||||
message=f"[{rel}] {kind} '{rid}' has no {id_field} — not indexed in DB",
|
||||
file_path=str(rel),
|
||||
file_value=rid,
|
||||
fixable=True,
|
||||
_fix_context={"md_path": md, "kind": kind, "rid": rid, "meta": meta},
|
||||
)
|
||||
|
||||
|
||||
def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = None) -> ConsistencyReport:
|
||||
"""Run all consistency checks for a registered repo."""
|
||||
repo = _api_get(api_base, f"/repos/{repo_slug}", return_error=True)
|
||||
|
|
@ -915,6 +1039,9 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
|
|||
# C-31: work-record sidetrack detector (canon work-record-types registry)
|
||||
_check_unregistered_work_records(repo_dir, report)
|
||||
|
||||
# C-32: intake/decision work-record registration (CUST-WP-0061-T02)
|
||||
_check_work_record_registration(repo_dir, report)
|
||||
|
||||
# C-01: workplans/ directory missing
|
||||
if not workplans_dir.is_dir():
|
||||
report.add(
|
||||
|
|
@ -2588,6 +2715,77 @@ def fix_repo(
|
|||
f" ! task {t_id} not created: {t_data.get('_error', t_data)}"
|
||||
)
|
||||
|
||||
elif issue.check_id == "C-32":
|
||||
md_path = ctx["md_path"]
|
||||
kind = ctx["kind"]
|
||||
rid = ctx["rid"]
|
||||
meta = ctx["meta"]
|
||||
id_field = _WORK_RECORD_ID_FIELD_BY_KIND[kind]
|
||||
title = str(meta.get("title") or rid)
|
||||
|
||||
repo_record = _api_get(api_base, f"/repos/{repo_slug}")
|
||||
if not repo_record:
|
||||
report.fixes_applied.append(
|
||||
f"C-32 SKIP {rid}: could not look up repo '{repo_slug}'"
|
||||
)
|
||||
continue
|
||||
created = None
|
||||
if kind == "intake":
|
||||
payload = {
|
||||
"title": title,
|
||||
"repo_id": repo_record.get("id"),
|
||||
"description": meta.get("description"),
|
||||
"lane": meta.get("lane", "green"),
|
||||
"origin": meta.get("origin"),
|
||||
"origin_ref": meta.get("origin_ref"),
|
||||
"source_repo_path": str(md_path.relative_to(Path(report.repo_path))),
|
||||
}
|
||||
created = _api_post(api_base, "/intakes", payload)
|
||||
elif kind == "decision":
|
||||
repo_market_domain = str(repo_record.get("domain_slug") or "").strip()
|
||||
topic_domain = resolve_topic_domain_slug(
|
||||
repo_market_domain, repo_market_domain=repo_market_domain or None
|
||||
)
|
||||
topics = _api_get(api_base, "/topics")
|
||||
topic_id = None
|
||||
if isinstance(topics, list):
|
||||
for t in topics:
|
||||
if t.get("domain_slug") == topic_domain:
|
||||
topic_id = t["id"]
|
||||
break
|
||||
if topic_id is None:
|
||||
report.fixes_applied.append(
|
||||
f"C-32 SKIP {rid}: no topic found for domain '{topic_domain}'"
|
||||
)
|
||||
continue
|
||||
payload = {
|
||||
"title": title,
|
||||
"topic_id": topic_id,
|
||||
"decision_type": meta.get("decision_type", "pending"),
|
||||
"description": meta.get("description"),
|
||||
"rationale": meta.get("agent_recommendation") or meta.get("rationale"),
|
||||
}
|
||||
created = _api_post(api_base, "/decisions", payload)
|
||||
|
||||
if not created or "_error" in created:
|
||||
report.fixes_applied.append(
|
||||
f"C-32 FAIL {rid}: could not create {kind} in DB: "
|
||||
f"{(created or {}).get('_error', 'no response')}"
|
||||
)
|
||||
continue
|
||||
|
||||
new_id = created["id"]
|
||||
if _inject_yaml_block_field(md_path, id_field, new_id, rid):
|
||||
report.fixes_applied.append(
|
||||
f"C-32 fixed: created {kind} {new_id[:8]}… for {rid}, "
|
||||
f"wrote {id_field} to {md_path.name}"
|
||||
)
|
||||
else:
|
||||
report.fixes_applied.append(
|
||||
f"C-32 WARN {rid}: created {kind} {new_id[:8]}… but "
|
||||
f"failed to write {id_field} back to {md_path.name}"
|
||||
)
|
||||
|
||||
elif issue.check_id == "C-09":
|
||||
ws_id = ctx["ws_id"]
|
||||
correct_repo_id = ctx["correct_repo_id"]
|
||||
|
|
|
|||
201
tests/test_work_record_registration.py
Normal file
201
tests/test_work_record_registration.py
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
"""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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue