state-hub/tests/test_promote_intake.py
tegwick 3dbbc753bc
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 3s
CUST-WP-0061-T03: the promotion transition (statehub promote-intake)
The mechanism named in canon/standards/work-record-types_v0.1.md:
"Promotion is a first-class transition... manual transcription of an
intake item into other kinds is a process defect." This is what AWQ-010
needed and didn't have -- a human/agent had to notice, transcribe, and
re-register it by hand. One call now does what that manual pass did.

scripts/promote_intake.py: intake.routed -> workplan | task | decision |
engagement.
  - workplan: new ADR-001 file at workplans/{ID}-{slug}.md, registered
    against the hub (repo+topic resolution, POST /workplans, frontmatter
    id write-back)
  - task: appended as a ```task``` block to an existing --workplan-file,
    registered via POST /tasks, reuses the existing
    _inject_task_id_into_block writeback helper
  - decision: appended as a ```yaml``` block with a fresh
    {PREFIX}-DEC-{YYYY}-{NNN} id to --target-file, registered the same
    way C-32 registers decisions (reuses _inject_yaml_block_field)
  - engagement: appended as a ```yaml``` block with a fresh
    {PREFIX}-ENG-{YYYY}-{NNN} id -- file-only, no hub entity exists yet
    (same honest deferral as C-32), reported not silently skipped

In every case the intake is closed with outcome=promoted and
promoted_to=<new canonical id>; the new record carries an
origin: "intake:<id>" back-link.

Wired as `statehub promote-intake <intake-id> --to <kind> --repo-slug
<slug> --repo-path <path> --domain <domain> [--target-file ...]
[--workplan-file ...]`, matching the CLI shape named in the workplan text.

17 tests: pure helpers (_slugify, _next_number, _append_yaml_block,
frontmatter injection) offline; full promote_intake() flow with the hub
API mocked.

Live-verified against the real running API/DB and a real repo
(binky-control), not just mocks -- and the live proof caught a real bug:
the first workplan-promotion run silently produced a false success (the
intake was closed outcome=promoted, but /workplans/ actually 422'd on a
missing repo_id that the code never resolved, so no workstream was ever
created). Fixed to resolve repo_id via /repos/{slug} and to raise loudly
on registration failure instead of writing a half-registered file
silently; locked in as two regression tests. Re-verified clean:
workplan promotion (CLI direct + through `statehub promote-intake`
itself) and decision promotion both proven end-to-end against the live
hub, with all scratch artefacts (files + hub rows) cleaned up afterward.
No regressions: full state-hub suite (271 tests across
test_promote_intake, test_intake, test_work_record_registration,
test_work_record_check, test_routers_core, test_consistency_check,
test_consistency_sweep, test_mcp_smoke, test_mcp_write_tools) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 01:06:01 +02:00

238 lines
10 KiB
Python

"""Tests for the promotion transition (CUST-WP-0061-T03,
scripts/promote_intake.py): intake.routed -> workplan | task | decision |
engagement.
Pure file-manipulation logic (_slugify, _next_number, _append_yaml_block,
_inject_field_in_frontmatter) is tested offline. The full promote_intake()
flow is tested with the hub API layer mocked (monkeypatched _api_get/
_api_post) so these run without a live server; the real end-to-end proof
(actual DB writes) is documented in CUST-WP-0061's progress notes, mirroring
the live-verification discipline used for T01/T02.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
import promote_intake as pi # noqa: E402
class TestSlugify:
def test_lowercases_and_hyphenates(self):
assert pi._slugify("Qonto MCP integration: bank account") == "qonto-mcp-integration-bank-account"
def test_truncates_long_titles(self):
long_title = "a " * 100
assert len(pi._slugify(long_title)) <= 60
def test_empty_title_gets_placeholder(self):
assert pi._slugify("!!!") == "untitled"
class TestNextNumber:
def test_finds_max_and_increments(self, tmp_path):
(tmp_path / "a.md").write_text("id: CUST-WP-0005\nid: CUST-WP-0012\n")
(tmp_path / "b.md").write_text("id: CUST-WP-0003\n")
import re
n = pi._next_number(tmp_path, re.compile(r"CUST-WP-(\d+)"))
assert n == 13
def test_empty_repo_starts_at_one(self, tmp_path):
import re
n = pi._next_number(tmp_path, re.compile(r"CUST-WP-(\d+)"))
assert n == 1
def test_skips_skip_dirs(self, tmp_path):
import re
skipped = tmp_path / ".git"
skipped.mkdir()
(skipped / "a.md").write_text("id: CUST-WP-9999\n")
n = pi._next_number(tmp_path, re.compile(r"CUST-WP-(\d+)"))
assert n == 1
class TestAppendYamlBlock:
def test_creates_new_file(self, tmp_path):
target = tmp_path / "DecisionQueue.md"
pi._append_yaml_block(target, {"id": "X-DEC-2026-001", "title": "test"}, origin_intake_id="abc-123")
text = target.read_text()
assert "id: \"X-DEC-2026-001\"" in text
assert 'origin: "intake:abc-123"' in text
def test_appends_to_existing_file(self, tmp_path):
target = tmp_path / "DecisionQueue.md"
target.write_text("# Decision Queue\n\nSome existing content.\n")
pi._append_yaml_block(target, {"id": "X-DEC-2026-002", "title": "second"}, origin_intake_id="def-456")
text = target.read_text()
assert "Some existing content." in text
assert "X-DEC-2026-002" in text
class TestInjectFieldInFrontmatter:
def test_injects_field_into_frontmatter(self, tmp_path):
f = tmp_path / "wp.md"
f.write_text("---\nid: CUST-WP-0099\ntitle: x\n---\n\nBody.\n")
pi._inject_field_in_frontmatter(f, "state_hub_workstream_id", "019f0000-0000-7000-8000-000000000000")
text = f.read_text()
assert 'state_hub_workstream_id: "019f0000-0000-7000-8000-000000000000"' in text
assert "Body." in text
class TestPromoteIntakeMocked:
"""Full promote_intake() flow with the hub API mocked."""
def _fake_routed_intake(self, **overrides):
base = {
"id": "019f0000-0000-7000-8000-000000000001",
"title": "Qonto MCP mailing — actionable",
"status": "routed",
"lane": "green",
"description": "found in mail triage",
}
base.update(overrides)
return base
def test_rejects_intake_not_routed(self, tmp_path, monkeypatch):
monkeypatch.setattr(pi, "_api_get", lambda *a, **k: self._fake_routed_intake(status="open"))
with pytest.raises(pi.PromotionError, match="not 'routed'"):
pi.promote_intake("http://x", "019f0000", "workplan", tmp_path, "testrepo", "infotech")
def test_promote_to_workplan_writes_file_and_closes_intake(self, tmp_path, monkeypatch):
calls = []
def fake_get(api_base, path, *a, **k):
if path.startswith("/intakes/"):
return self._fake_routed_intake()
if path == "/topics":
return [{"id": "019ftopic", "domain_slug": "infotech"}]
if path == "/repos/testrepo":
return {"id": "019frepo", "slug": "testrepo"}
return None
def fake_post(api_base, path, body):
calls.append((path, body))
if path == "/workplans":
return {"id": "019fworkplan", "title": body["title"]}
if path.startswith("/intakes/") and path.endswith("/close"):
return {"id": "019f0000", "status": "closed", "outcome": "promoted"}
return {"id": "019fgeneric"}
monkeypatch.setattr(pi, "_api_get", fake_get)
monkeypatch.setattr(pi, "_api_post", fake_post)
result = pi.promote_intake(
"http://x", "019f0000-0000-7000-8000-000000000001", "workplan",
tmp_path, "testrepo", "infotech",
)
assert result["to_kind"] == "workplan"
new_file = tmp_path / result["location"]
assert new_file.is_file()
text = new_file.read_text()
assert "Qonto MCP mailing" in text
assert 'state_hub_workstream_id: "019fworkplan"' in text
assert "intake:019f0000-0000-7000-8000-000000000001" in text
close_calls = [c for c in calls if c[0].endswith("/close")]
assert len(close_calls) == 1
assert close_calls[0][1]["outcome"] == "promoted"
assert close_calls[0][1]["promoted_to"] == result["canonical_id"]
def test_promote_to_workplan_raises_loudly_when_repo_lookup_fails(self, tmp_path, monkeypatch):
"""Regression: the live proof against binky-control (2026-07-21) found
/workplans/ requires repo_id, which promote_to_workplan originally
never resolved -- the POST 422'd and the failure was silently
swallowed (file written, no state_hub_workstream_id, no error).
Must raise, not write a half-registered file silently."""
def fake_get(api_base, path, *a, **k):
if path.startswith("/intakes/"):
return self._fake_routed_intake()
if path == "/topics":
return [{"id": "019ftopic", "domain_slug": "infotech"}]
if path == "/repos/testrepo":
return {"_error": "404: not found"}
return None
monkeypatch.setattr(pi, "_api_get", fake_get)
monkeypatch.setattr(pi, "_api_post", lambda *a, **k: {"id": "x"})
with pytest.raises(pi.PromotionError, match="could not look up repo"):
pi.promote_intake("http://x", "019f0000", "workplan", tmp_path, "testrepo", "infotech")
def test_promote_to_workplan_raises_loudly_when_workstream_post_fails(self, tmp_path, monkeypatch):
def fake_get(api_base, path, *a, **k):
if path.startswith("/intakes/"):
return self._fake_routed_intake()
if path == "/topics":
return [{"id": "019ftopic", "domain_slug": "infotech"}]
if path == "/repos/testrepo":
return {"id": "019frepo"}
return None
def fake_post(api_base, path, body):
if path == "/workplans":
return {"_error": "422: repo_id required"}
return {"id": "x"}
monkeypatch.setattr(pi, "_api_get", fake_get)
monkeypatch.setattr(pi, "_api_post", fake_post)
with pytest.raises(pi.PromotionError, match="workstream registration failed"):
pi.promote_intake("http://x", "019f0000", "workplan", tmp_path, "testrepo", "infotech")
def test_promote_to_workplan_refuses_existing_file(self, tmp_path, monkeypatch):
monkeypatch.setattr(pi, "_api_get", lambda *a, **k: self._fake_routed_intake())
wp_dir = tmp_path / "workplans"
wp_dir.mkdir()
# pre-create the file the promotion would try to write
prefix = pi.infer_wp_prefix(tmp_path, "testrepo")
(wp_dir / f"{prefix}-0001-qonto-mcp-mailing-actionable.md").write_text("already here")
def fake_post(api_base, path, body):
return {"id": "x"}
monkeypatch.setattr(pi, "_api_post", fake_post)
with pytest.raises(pi.PromotionError, match="already exists"):
pi.promote_intake("http://x", "019f0000", "workplan", tmp_path, "testrepo", "infotech")
def test_promote_to_decision_requires_target_file(self, tmp_path, monkeypatch):
monkeypatch.setattr(pi, "_api_get", lambda *a, **k: self._fake_routed_intake())
with pytest.raises(pi.PromotionError, match="--target-file"):
pi.promote_intake("http://x", "019f0000", "decision", tmp_path, "testrepo", "infotech")
def test_promote_to_task_requires_workplan_file(self, tmp_path, monkeypatch):
monkeypatch.setattr(pi, "_api_get", lambda *a, **k: self._fake_routed_intake())
with pytest.raises(pi.PromotionError, match="--workplan-file"):
pi.promote_intake("http://x", "019f0000", "task", tmp_path, "testrepo", "infotech")
def test_promote_to_engagement_is_file_only_no_hub_call(self, tmp_path, monkeypatch):
post_calls = []
def fake_get(api_base, path, *a, **k):
return self._fake_routed_intake()
def fake_post(api_base, path, body):
post_calls.append(path)
if path.startswith("/intakes/") and path.endswith("/close"):
return {"id": "x", "status": "closed"}
return {"id": "x"}
monkeypatch.setattr(pi, "_api_get", fake_get)
monkeypatch.setattr(pi, "_api_post", fake_post)
result = pi.promote_intake(
"http://x", "019f0000", "engagement", tmp_path, "testrepo", "infotech",
target_file="OfficeHourQueue.md",
)
assert result["to_kind"] == "engagement"
target = tmp_path / "OfficeHourQueue.md"
assert target.is_file()
assert "queued" in target.read_text()
# only the intake-close call should have hit the API — no
# engagement-creation endpoint exists yet
non_close_calls = [c for c in post_calls if not c.endswith("/close")]
assert non_close_calls == []