ACTIVITY-WP-0043-T05: owner-return eligibility builder bound to scan revision
Build the planner's current_tasks from immutable Forge commit content at the C-25 scan SHA (no checkouts, ADR-002). Fails closed on revision mismatch, non-head scans, missing/malformed fields and duplicate identities; unresolved predecessors and decisions stay unknown (None). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 692292@bnt-lap001 Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
This commit is contained in:
parent
9741d5cc19
commit
c6df768ef0
4 changed files with 808 additions and 2 deletions
309
tests/test_owner_return_eligibility.py
Normal file
309
tests/test_owner_return_eligibility.py
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
"""Tests for the owner-return eligibility input builder (ACTIVITY-WP-0043-T05).
|
||||
|
||||
All Forge and State Hub HTTP is served by ``httpx.MockTransport``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from activity_core import owner_return_eligibility as ore
|
||||
from activity_core import owner_return_pickup as orp
|
||||
|
||||
REPO = "ops-warden"
|
||||
SHA = "a" * 40
|
||||
OLD = "b" * 40
|
||||
API = ore.DEFAULT_FORGE_API
|
||||
BASE = f"{API}/repos/coulomb/{REPO}"
|
||||
|
||||
WP1 = """---
|
||||
id: WARDEN-WP-0041
|
||||
status: active
|
||||
flavor: implementation
|
||||
depends_on: [WARDEN-WP-0040]
|
||||
---
|
||||
|
||||
## Owner return
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0041-T03
|
||||
status: wait
|
||||
priority: high
|
||||
needs_human: false
|
||||
blocked_on: "message-from:state-hub"
|
||||
depends_on: [T01, STATE-WP-0094-T03]
|
||||
wait_until: "2026-10-01"
|
||||
decision_id: "dec-1"
|
||||
```
|
||||
|
||||
```task
|
||||
id: T01
|
||||
status: done
|
||||
```
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0041-T04
|
||||
status: wait
|
||||
blocked_on: "message-from:state-hub"
|
||||
```
|
||||
"""
|
||||
|
||||
WP0 = """---
|
||||
id: WARDEN-WP-0040
|
||||
status: finished
|
||||
---
|
||||
"""
|
||||
|
||||
|
||||
def _files(**over):
|
||||
files = {
|
||||
"workplans/WARDEN-WP-0041-owner.md": WP1,
|
||||
"workplans/archived/261001-WARDEN-WP-0040-prior.md": WP0,
|
||||
}
|
||||
files.update(over)
|
||||
return {k: v for k, v in files.items() if v is not None}
|
||||
|
||||
|
||||
def _transport(files, *, sha=SHA, head=SHA, seen=None, fail=None):
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
url = str(request.url).split("?")[0]
|
||||
path = unquote(url)
|
||||
ref = request.url.params.get("ref")
|
||||
if seen is not None:
|
||||
seen.append((path, ref))
|
||||
if fail and fail in path:
|
||||
return httpx.Response(500)
|
||||
if path == BASE:
|
||||
return httpx.Response(200, json={"default_branch": "main"})
|
||||
if path == f"{BASE}/branches/main":
|
||||
return httpx.Response(200, json={"commit": {"id": head}})
|
||||
if path.startswith(f"{BASE}/git/commits/"):
|
||||
asked = path.rsplit("/", 1)[1]
|
||||
return httpx.Response(200, json={"sha": sha if asked == sha else "c" * 40})
|
||||
if path.startswith(f"{BASE}/contents/"):
|
||||
if ref != sha:
|
||||
return httpx.Response(404)
|
||||
directory = path[len(f"{BASE}/contents/"):]
|
||||
entries = [
|
||||
{"type": "file", "name": p.rsplit("/", 1)[1], "path": p}
|
||||
for p in files if p.rsplit("/", 1)[0] == directory
|
||||
]
|
||||
if not entries and not any(p.startswith(directory + "/") for p in files):
|
||||
return httpx.Response(404)
|
||||
if directory == "workplans":
|
||||
entries.append({"type": "dir", "name": "archived", "path": "workplans/archived"})
|
||||
return httpx.Response(200, json=entries)
|
||||
if path.startswith(f"{BASE}/raw/"):
|
||||
name = path[len(f"{BASE}/raw/"):]
|
||||
if ref != sha or name not in files:
|
||||
return httpx.Response(404)
|
||||
return httpx.Response(200, text=files[name])
|
||||
return httpx.Response(404)
|
||||
|
||||
return httpx.MockTransport(handler)
|
||||
|
||||
|
||||
def _build(files=None, *, decisions=None, external=None, **kw):
|
||||
transport = kw.pop("transport", None) or _transport(files or _files())
|
||||
client = httpx.Client(transport=transport)
|
||||
decisions = {"dec-1": "resolved"} if decisions is None else decisions
|
||||
return ore.build_current_tasks(
|
||||
target_repo=REPO,
|
||||
scan_revision=kw.pop("scan_revision", SHA),
|
||||
task_record_ids=kw.pop("task_record_ids", ["WARDEN-WP-0041-T03"]),
|
||||
client=client,
|
||||
decision_status=decisions.get,
|
||||
external_status=external,
|
||||
**kw,
|
||||
)
|
||||
|
||||
|
||||
def test_builds_complete_row_bound_to_revision():
|
||||
seen: list = []
|
||||
out = _build(transport=_transport(_files(), seen=seen),
|
||||
external={"STATE-WP-0094-T03": "done"}.get)
|
||||
assert out["schema"] == ore.SCHEMA
|
||||
assert out["source_revision"] == SHA
|
||||
row = out["current_tasks"]["WARDEN-WP-0041-T03"]
|
||||
assert row == {
|
||||
"target_repo": REPO,
|
||||
"task_record_id": "WARDEN-WP-0041-T03",
|
||||
"status": "wait",
|
||||
"workplan_status": "active",
|
||||
"workplan_flavor": "implementation",
|
||||
"needs_human": False,
|
||||
"blocked_on": "message-from:state-hub",
|
||||
"depends_on": ["WARDEN-WP-0040", "WARDEN-WP-0041-T01", "STATE-WP-0094-T03"],
|
||||
"predecessor_statuses": {
|
||||
"WARDEN-WP-0040": "finished",
|
||||
"WARDEN-WP-0041-T01": "done",
|
||||
"STATE-WP-0094-T03": "done",
|
||||
},
|
||||
"wait_until": "2026-10-01",
|
||||
"decision_id": "dec-1",
|
||||
"decision_status": "resolved",
|
||||
}
|
||||
content_reads = [(p, r) for p, r in seen if "/contents/" in p or "/raw/" in p]
|
||||
assert content_reads and all(ref == SHA for _, ref in content_reads)
|
||||
|
||||
|
||||
def test_unresolvable_predecessor_and_decision_stay_unknown():
|
||||
out = _build(decisions={})
|
||||
row = out["current_tasks"]["WARDEN-WP-0041-T03"]
|
||||
assert row["predecessor_statuses"]["STATE-WP-0094-T03"] is None
|
||||
assert "STATE-WP-0094-T03" in row["depends_on"]
|
||||
assert row["decision_status"] is None
|
||||
|
||||
|
||||
def test_external_resolver_exception_is_unknown_not_fatal():
|
||||
def boom(_):
|
||||
raise RuntimeError("down")
|
||||
row = _build(external=boom)["current_tasks"]["WARDEN-WP-0041-T03"]
|
||||
assert row["predecessor_statuses"]["STATE-WP-0094-T03"] is None
|
||||
|
||||
|
||||
def test_task_without_dependencies_has_explicit_workplan_gate_only():
|
||||
row = _build(task_record_ids=["WARDEN-WP-0041-T04"])["current_tasks"]["WARDEN-WP-0041-T04"]
|
||||
assert row["depends_on"] == ["WARDEN-WP-0040"]
|
||||
assert row["decision_id"] is None and row["wait_until"] is None
|
||||
assert row["needs_human"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("revision", ["main", "a" * 39, "A" * 40, None])
|
||||
def test_refuses_non_sha_revision(revision):
|
||||
with pytest.raises(ore.EligibilityError):
|
||||
_build(scan_revision=revision)
|
||||
|
||||
|
||||
def test_refuses_when_scan_is_not_current_head():
|
||||
with pytest.raises(ore.EligibilityError, match="no longer the default-branch head"):
|
||||
_build(transport=_transport(_files(), head=OLD))
|
||||
|
||||
|
||||
def test_stale_scan_allowed_only_when_head_check_disabled():
|
||||
out = _build(transport=_transport(_files(), head=OLD), require_current_head=False)
|
||||
assert out["source_revision"] == SHA
|
||||
|
||||
|
||||
def test_refuses_unknown_commit():
|
||||
with pytest.raises(ore.EligibilityError, match="does not match"):
|
||||
_build(transport=_transport(_files(), sha=OLD, head=SHA))
|
||||
|
||||
|
||||
def test_refuses_report_revision_mismatch():
|
||||
report = {"repo_slug": REPO, "source_revision": OLD, "issues": []}
|
||||
with pytest.raises(ore.EligibilityError, match="revision"):
|
||||
_build(report=report)
|
||||
with pytest.raises(ore.EligibilityError, match="different repository"):
|
||||
_build(report={"repo_slug": "other", "issues": []})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"old,new,match",
|
||||
[
|
||||
("status: wait\npriority: high", "priority: high", "missing authoritative status"),
|
||||
("needs_human: false", "needs_human: maybe", "needs_human"),
|
||||
('wait_until: "2026-10-01"', 'wait_until: "soon"', "wait_until"),
|
||||
("depends_on: [T01, STATE-WP-0094-T03]", "depends_on: {a: b}", "depends_on"),
|
||||
("id: WARDEN-WP-0041\nstatus: active", "id: WARDEN-WP-0041", "missing authoritative status"),
|
||||
],
|
||||
)
|
||||
def test_missing_or_malformed_fields_fail_closed(old, new, match):
|
||||
files = _files(**{"workplans/WARDEN-WP-0041-owner.md": WP1.replace(old, new, 1)})
|
||||
with pytest.raises(ore.EligibilityError, match=match):
|
||||
_build(files)
|
||||
|
||||
|
||||
def test_absent_and_duplicate_tasks_refuse():
|
||||
with pytest.raises(ore.EligibilityError, match="absent"):
|
||||
_build(task_record_ids=["WARDEN-WP-0041-T09"])
|
||||
dup = WP1.replace("id: WARDEN-WP-0041-T04", "id: WARDEN-WP-0041-T03")
|
||||
with pytest.raises(ore.EligibilityError, match="duplicate task"):
|
||||
_build(_files(**{"workplans/WARDEN-WP-0041-owner.md": dup}))
|
||||
|
||||
|
||||
def test_forge_failure_refuses():
|
||||
with pytest.raises(ore.EligibilityError):
|
||||
_build(transport=_transport(_files(), fail="/raw/"))
|
||||
|
||||
|
||||
def test_missing_workplans_directory_refuses():
|
||||
with pytest.raises(ore.EligibilityError, match="no workplans"):
|
||||
_build(transport=_transport({}))
|
||||
|
||||
|
||||
def test_state_hub_decision_resolver():
|
||||
def handler(request):
|
||||
if request.url.path.endswith("/decisions/dec-1"):
|
||||
return httpx.Response(200, json={"id": "dec-1", "status": "open"})
|
||||
return httpx.Response(404)
|
||||
resolve = ore.state_hub_decision_status(
|
||||
httpx.Client(transport=httpx.MockTransport(handler)), "http://hub"
|
||||
)
|
||||
assert resolve("dec-1") == "open"
|
||||
assert resolve("dec-2") is None
|
||||
|
||||
|
||||
def test_forge_head_resolver_pins_foreign_head_and_maps_prefix():
|
||||
foreign = "d" * 40
|
||||
files = {"workplans/STATE-WP-0094-x.md": "---\nid: STATE-WP-0094\nstatus: active\n---\n"
|
||||
"```task\nid: STATE-WP-0094-T03\nstatus: progress\n```\n"}
|
||||
fbase = f"{API}/repos/coulomb/state-hub"
|
||||
|
||||
def handler(request):
|
||||
path = unquote(str(request.url).split("?")[0])
|
||||
ref = request.url.params.get("ref")
|
||||
if path == fbase:
|
||||
return httpx.Response(200, json={"default_branch": "main"})
|
||||
if path == f"{fbase}/branches/main":
|
||||
return httpx.Response(200, json={"commit": {"id": foreign}})
|
||||
if path == f"{fbase}/contents/workplans" and ref == foreign:
|
||||
return httpx.Response(200, json=[{"type": "file", "name": "STATE-WP-0094-x.md",
|
||||
"path": "workplans/STATE-WP-0094-x.md"}])
|
||||
if path.startswith(f"{fbase}/raw/") and ref == foreign:
|
||||
return httpx.Response(200, text=files[path[len(f"{fbase}/raw/"):]])
|
||||
return httpx.Response(404)
|
||||
|
||||
resolve = ore.forge_head_status_resolver(
|
||||
httpx.Client(transport=httpx.MockTransport(handler)), {"STATE-WP-": "state-hub"}
|
||||
)
|
||||
assert resolve("STATE-WP-0094-T03") == "progress"
|
||||
assert resolve("STATE-WP-0094") == "active"
|
||||
assert resolve("STATE-WP-0094-T99") is None
|
||||
assert resolve("OTHER-WP-0001") is None
|
||||
|
||||
|
||||
_PLANNER = Path("/home/worsch/state-hub/scripts/owner_return_handoff.py")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _PLANNER.is_file(), reason="state-hub planner not checked out")
|
||||
def test_output_is_accepted_by_state_hub_planner():
|
||||
spec = importlib.util.spec_from_file_location("owner_return_handoff", _PLANNER)
|
||||
planner = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(planner)
|
||||
msg = "11111111-2222-3333-4444-555555555555"
|
||||
report = {"repo_slug": REPO, "result": "warn", "issues": [{
|
||||
"check_id": "C-25", "file_value": "message-from:state-hub",
|
||||
"owner_return": {"task_record_id": "WARDEN-WP-0041-T03", "message_ids": [msg]},
|
||||
}]}
|
||||
ok = _build(external={"STATE-WP-0094-T03": "done"}.get, report=report)
|
||||
plan = planner.plan_owner_returns(
|
||||
report, activity_definition_id=orp.ACTIVITY_DEFINITION_ID,
|
||||
current_tasks=ok["current_tasks"], observed_on=date(2026, 10, 4),
|
||||
)
|
||||
assert [c["disposition"] for c in plan] == ["reviewable"]
|
||||
assert plan[0]["triggering_event_id"] == orp.owner_return_event_id(
|
||||
REPO, "WARDEN-WP-0041-T03", msg)
|
||||
unknown = _build(report=report)
|
||||
plan = planner.plan_owner_returns(
|
||||
report, activity_definition_id=orp.ACTIVITY_DEFINITION_ID,
|
||||
current_tasks=unknown["current_tasks"], observed_on=date(2026, 10, 4),
|
||||
)
|
||||
assert plan[0]["disposition"] == "stopped"
|
||||
assert plan[0]["stop_reason"] == "predecessor_not_terminal_or_unknown"
|
||||
Loading…
Add table
Add a link
Reference in a new issue