Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Code
66afc2f7d2 fix(orchestrator): persist spec_path on spec-phase pass (ADR-004)
Some checks failed
test / contract-and-unit (pull_request) Failing after 15s
Spec-refiner writes data_dir/specs/<project>/<story>.spec.md and returns
the path in verdict feedback, but cycle.py:set_phase() never copied it
onto the row. Build phase worked only because _find_spec_file fallback
used the same slug convention by luck.

- Add 3-line branch in cycle.py Txn 3: when verdict=pass and phase=spec,
  copy verdict_feedback["spec_path"] into extra_fields.
- Add tests/test_spec_path_persistence.py with two AC tests:
  test_pass_verdict_persists_spec_path, test_non_pass_verdict_does_not_persist.
- Defense-in-depth fallback (_find_spec_file) remains unchanged.

Refs: wiki/decisions/ADR-004-persist-spec-path-on-pass.md
Refs: wiki/decisions/research/adr-004-spec-path-research.md
Story: S1-persist-spec-path-on-spec-pass
2026-06-27 02:25:15 +00:00
2 changed files with 92 additions and 0 deletions

View File

@@ -128,6 +128,12 @@ def tick() -> dict:
extra_fields["branch"] = verdict_feedback["branch"]
if "commit" in verdict_feedback:
extra_fields["base_commit"] = verdict_feedback["commit"]
# ADR-004: persist spec_path on spec-phase pass so build/review/dashboard
# can read it from the row instead of hitting disk every time. Schema
# already has the column; _find_spec_file fallback remains defense-in-depth.
if result["verdict"] == "pass" and item["phase"] == "spec":
if "spec_path" in verdict_feedback:
extra_fields["spec_path"] = verdict_feedback["spec_path"]
# Amendment §4: `spec_ambiguous` does NOT consume the autonomous budget.
# The claim already incremented attempts; roll it back so a human-blocked

View File

@@ -0,0 +1,86 @@
"""
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"