87 lines
3.3 KiB
Python
87 lines
3.3 KiB
Python
"""
|
|
Unit tests for ADR-004: persist `spec_path` on spec-phase pass.
|
|
|
|
Story: S1 — Persist spec_path on spec-phase pass
|
|
ADR: wiki/decisions/ADR-004-persist-spec-path-on-pass.md
|
|
|
|
Contract:
|
|
- verdict=pass + phase=spec => spec_path from feedback is written to the row.
|
|
- verdict != pass + phase=spec => spec_path is unchanged.
|
|
|
|
These tests drive `cycle.tick()` against the real test Postgres (conftest's
|
|
default `db-test` service) and stub `phases.refine_spec` so the LLM is
|
|
never called. The other moveable parts (wiki, relay) are also stubbed so
|
|
the test is hermetic.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from conftest import get_row, insert_work_item
|
|
from damascus import cycle, phases, relay, wiki
|
|
|
|
|
|
def _stub_phase_returning(verdict: str, feedback: dict):
|
|
"""Build a fake phases.refine_spec that returns a fixed verdict/feedback."""
|
|
def fake_refine_spec(cur, item):
|
|
print(f"DEBUG stub: item phase={item.get('phase')!r} id={item.get('id')!r}")
|
|
print(f"DEBUG stub: returning verdict={verdict!r} feedback={feedback!r}")
|
|
return {"verdict": verdict, "feedback": feedback}
|
|
return fake_refine_spec
|
|
|
|
|
|
def _run_tick_with_stub(monkeypatch, verdict: str, feedback: dict) -> None:
|
|
"""Run one orchestrator tick with the spec phase stubbed.
|
|
|
|
wiki.init_wiki and relay.post are no-ops so the test does not touch
|
|
the host filesystem or any external service.
|
|
"""
|
|
monkeypatch.setattr(wiki, "init_wiki", lambda: None)
|
|
monkeypatch.setattr(relay, "post", lambda line: None)
|
|
monkeypatch.setattr(phases, "refine_spec", _stub_phase_returning(verdict, feedback))
|
|
|
|
out = cycle.tick()
|
|
print(f"DEBUG tick: claimed={out['claimed']!r} transition={out['transition']!r}")
|
|
assert out["claimed"] is not None, "tick did not claim a row"
|
|
assert out["transition"]["verdict"] == verdict
|
|
|
|
|
|
def test_pass_verdict_persists_spec_path(monkeypatch):
|
|
"""AC: On verdict=pass in spec phase, work_items.spec_path equals
|
|
the absolute path returned in verdict_feedback."""
|
|
rid = insert_work_item(phase="spec", story_id="S1-pass", title="Persist spec path on pass")
|
|
expected_path = "/data/specs/wh40k-pc/S1-pass.spec.md"
|
|
|
|
_run_tick_with_stub(monkeypatch, "pass", {
|
|
"spec_path": expected_path,
|
|
"preview": "# Goal\n...",
|
|
})
|
|
|
|
row = get_row(rid)
|
|
assert row["spec_path"] == expected_path, (
|
|
f"spec_path not persisted on pass: row has {row['spec_path']!r}, "
|
|
f"expected {expected_path!r}"
|
|
)
|
|
# The phase should have advanced spec -> build (the contract for pass).
|
|
assert row["phase"] == "build"
|
|
|
|
|
|
def test_non_pass_verdict_does_not_persist(monkeypatch):
|
|
"""AC: On a non-pass verdict in spec phase, work_items.spec_path is unchanged.
|
|
For a freshly-inserted row, spec_path starts NULL and stays NULL."""
|
|
rid = insert_work_item(phase="spec", story_id="S1-nopass",
|
|
title="Spec ambiguous case")
|
|
|
|
_run_tick_with_stub(monkeypatch, "spec_ambiguous", {
|
|
"issue_id": "test-issue-id",
|
|
"preview": "# Goal\n...",
|
|
})
|
|
|
|
row = get_row(rid)
|
|
assert row["spec_path"] is None, (
|
|
f"spec_path must be unchanged on non-pass; row has {row['spec_path']!r}"
|
|
)
|
|
# spec_ambiguous rolls back the attempts increment AND routes to
|
|
# awaiting_human (contract per ADR-004 + design doc §5).
|
|
assert row["phase"] == "awaiting_human"
|