Skip to content

Commit 3a6954d

Browse files
nhortonclaude
andauthored
feat: explicit requirement and tests for quality gate review caching (#345)
* feat: add explicit requirement and integration tests for quality gate review caching JOBS-REQ-004.5.7 now explicitly requires that workflow quality gate reviews skip files with existing .passed markers (per REVIEW-REQ-009), rather than noting it parenthetically. Two integration tests exercise the actual caching mechanism end-to-end through run_quality_gate without mocking write_instruction_files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update stale traceability refs after requirement renumbering, add changelog - JOBS-REQ-004.5.8 (was .7) and JOBS-REQ-004.5.9 (was .8) traceability comments updated in test file - ruff formatting fix - Changelog entries for unreleased section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9cecd9d commit 3a6954d

3 files changed

Lines changed: 140 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Integration tests for quality gate review caching (JOBS-REQ-004.5.7)
13+
1214
### Changed
1315

16+
- JOBS-REQ-004.5.7 strengthened to explicit MUST requirement for skipping already-passed reviews
17+
1418
### Fixed
1519

1620
### Removed

specs/deepwork/jobs/JOBS-REQ-004-quality-review-system.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ The quality review system evaluates step outputs against defined quality criteri
4545
3. Dynamic rules (from step reviews) MUST be matched against all output file paths via `match_files_to_rules()`, regardless of git change status.
4646
4. If `get_changed_files()` fails, `.deepreview` matching MUST be skipped (no `.deepreview` tasks produced). Dynamic rules MUST be unaffected.
4747
5. All matched tasks (dynamic + `.deepreview`) MUST be combined.
48-
6. Combined tasks MUST be passed to `write_instruction_files()`, which honors `.passed` marker files.
49-
7. If `write_instruction_files()` returns no task files (all already passed), `run_quality_gate()` MUST return `None`.
50-
8. Remaining task files MUST be formatted via `format_for_claude()`.
48+
6. Combined tasks MUST be passed to `write_instruction_files()`.
49+
7. `write_instruction_files()` MUST skip any review task whose `review_id` has a corresponding `.passed` marker file (per REVIEW-REQ-009). If a file was edited in the PR but a prior review already passed for that exact content (same rule, same files, same content hash), the review MUST NOT run again.
50+
8. If `write_instruction_files()` returns no task files (all already passed), `run_quality_gate()` MUST return `None`.
51+
9. Remaining task files MUST be formatted via `format_for_claude()`.
5152

5253
### JOBS-REQ-004.6: Review Guidance Output
5354

tests/unit/jobs/mcp/test_quality_gate.py

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
WorkflowStep,
2222
)
2323
from deepwork.review.config import ReviewRule, ReviewTask
24+
from deepwork.review.instructions import INSTRUCTIONS_DIR, compute_review_id
2425

2526
# ---------------------------------------------------------------------------
2627
# Helpers
@@ -518,7 +519,7 @@ def test_returns_feedback_when_json_schema_fails(self, tmp_path: Path) -> None:
518519
assert "JSON schema validation failed" in result
519520
assert "finished_step" in result
520521

521-
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.1.3, JOBS-REQ-004.5.8, JOBS-REQ-004.6.1).
522+
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.1.3, JOBS-REQ-004.5.9, JOBS-REQ-004.6.1).
522523
# YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES
523524
def test_returns_review_instructions_when_reviews_exist(self, tmp_path: Path) -> None:
524525
"""When dynamic rules produce tasks, review instructions are returned."""
@@ -573,7 +574,7 @@ def test_returns_review_instructions_when_reviews_exist(self, tmp_path: Path) ->
573574
assert "Quality reviews are required" in result
574575
assert "step_write_output_report" in result
575576

576-
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.5.7).
577+
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.5.8).
577578
# YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES
578579
def test_returns_none_when_all_reviews_already_passed(self, tmp_path: Path) -> None:
579580
"""If write_instruction_files returns empty (all .passed), result is None."""
@@ -1170,3 +1171,132 @@ def test_no_deepreview_rules_and_no_output_files_skips_matching(self, tmp_path:
11701171
)
11711172

11721173
assert result is None
1174+
1175+
1176+
class TestQualityGatePassCaching:
1177+
"""Tests for JOBS-REQ-004.5.7: quality gate skips reviews with .passed markers."""
1178+
1179+
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.5.7).
1180+
# YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES
1181+
def test_skips_review_when_passed_marker_exists_for_unchanged_file(
1182+
self, tmp_path: Path
1183+
) -> None:
1184+
"""Quality gate returns None when a .passed marker exists for the exact content.
1185+
1186+
This is an integration test that does NOT mock write_instruction_files —
1187+
it creates a real .passed marker file and verifies the caching mechanism
1188+
works end-to-end through the quality gate.
1189+
"""
1190+
review = ReviewBlock(strategy="individual", instructions="Check it")
1191+
arg = StepArgument(name="report", description="Report", type="file_path")
1192+
output_ref = StepOutputRef(argument_name="report", required=True, review=review)
1193+
step = WorkflowStep(name="write", outputs={"report": output_ref})
1194+
job, workflow = _make_job(tmp_path, [arg], step)
1195+
1196+
# Create the output file with known content
1197+
report_path = tmp_path / "report.md"
1198+
report_path.write_text("Report content here")
1199+
1200+
# Build the ReviewTask that the quality gate would produce
1201+
task = ReviewTask(
1202+
rule_name="step_write_output_report",
1203+
files_to_review=[str(report_path)],
1204+
instructions="Check it",
1205+
agent_name=None,
1206+
)
1207+
1208+
# Compute the review_id for this task and create a .passed marker
1209+
review_id = compute_review_id(task, tmp_path)
1210+
instructions_dir = tmp_path / INSTRUCTIONS_DIR
1211+
instructions_dir.mkdir(parents=True, exist_ok=True)
1212+
(instructions_dir / f"{review_id}.passed").write_bytes(b"")
1213+
1214+
with (
1215+
patch(
1216+
"deepwork.jobs.mcp.quality_gate.load_all_rules",
1217+
return_value=([], []),
1218+
),
1219+
patch(
1220+
"deepwork.jobs.mcp.quality_gate.match_files_to_rules",
1221+
return_value=[task],
1222+
),
1223+
):
1224+
result = run_quality_gate(
1225+
step=step,
1226+
job=job,
1227+
workflow=workflow,
1228+
outputs={"report": str(report_path)},
1229+
input_values={},
1230+
work_summary=None,
1231+
project_root=tmp_path,
1232+
)
1233+
1234+
assert result is None
1235+
1236+
# THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.5.7).
1237+
# YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES
1238+
def test_reruns_review_when_file_content_changes_after_pass(self, tmp_path: Path) -> None:
1239+
"""Quality gate runs review again when file content changes after a prior pass.
1240+
1241+
Verifies that changing file content produces a different review_id,
1242+
so the old .passed marker no longer applies.
1243+
"""
1244+
review = ReviewBlock(strategy="individual", instructions="Check it")
1245+
arg = StepArgument(name="report", description="Report", type="file_path")
1246+
output_ref = StepOutputRef(argument_name="report", required=True, review=review)
1247+
step = WorkflowStep(name="write", outputs={"report": output_ref})
1248+
job, workflow = _make_job(tmp_path, [arg], step)
1249+
1250+
report_path = tmp_path / "report.md"
1251+
1252+
# First: create the file with original content and mark the review as passed
1253+
report_path.write_text("Original content")
1254+
task_v1 = ReviewTask(
1255+
rule_name="step_write_output_report",
1256+
files_to_review=[str(report_path)],
1257+
instructions="Check it",
1258+
agent_name=None,
1259+
)
1260+
review_id_v1 = compute_review_id(task_v1, tmp_path)
1261+
instructions_dir = tmp_path / INSTRUCTIONS_DIR
1262+
instructions_dir.mkdir(parents=True, exist_ok=True)
1263+
(instructions_dir / f"{review_id_v1}.passed").write_bytes(b"")
1264+
1265+
# Now: change the file content (simulating a new edit in the PR)
1266+
report_path.write_text("Updated content with new edits")
1267+
1268+
# The task from match_files_to_rules will have the same rule/files
1269+
task_v2 = ReviewTask(
1270+
rule_name="step_write_output_report",
1271+
files_to_review=[str(report_path)],
1272+
instructions="Check it",
1273+
agent_name=None,
1274+
)
1275+
1276+
with (
1277+
patch(
1278+
"deepwork.jobs.mcp.quality_gate.load_all_rules",
1279+
return_value=([], []),
1280+
),
1281+
patch(
1282+
"deepwork.jobs.mcp.quality_gate.match_files_to_rules",
1283+
return_value=[task_v2],
1284+
),
1285+
patch(
1286+
"deepwork.jobs.mcp.quality_gate.format_for_claude",
1287+
return_value="formatted review instructions",
1288+
),
1289+
):
1290+
result = run_quality_gate(
1291+
step=step,
1292+
job=job,
1293+
workflow=workflow,
1294+
outputs={"report": str(report_path)},
1295+
input_values={},
1296+
work_summary=None,
1297+
project_root=tmp_path,
1298+
)
1299+
1300+
# Review MUST run again because content changed
1301+
assert result is not None
1302+
assert "Quality reviews are required" in result

0 commit comments

Comments
 (0)