|
21 | 21 | WorkflowStep, |
22 | 22 | ) |
23 | 23 | from deepwork.review.config import ReviewRule, ReviewTask |
| 24 | +from deepwork.review.instructions import INSTRUCTIONS_DIR, compute_review_id |
24 | 25 |
|
25 | 26 | # --------------------------------------------------------------------------- |
26 | 27 | # Helpers |
@@ -518,7 +519,7 @@ def test_returns_feedback_when_json_schema_fails(self, tmp_path: Path) -> None: |
518 | 519 | assert "JSON schema validation failed" in result |
519 | 520 | assert "finished_step" in result |
520 | 521 |
|
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). |
522 | 523 | # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES |
523 | 524 | def test_returns_review_instructions_when_reviews_exist(self, tmp_path: Path) -> None: |
524 | 525 | """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) -> |
573 | 574 | assert "Quality reviews are required" in result |
574 | 575 | assert "step_write_output_report" in result |
575 | 576 |
|
576 | | - # THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.5.7). |
| 577 | + # THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.5.8). |
577 | 578 | # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES |
578 | 579 | def test_returns_none_when_all_reviews_already_passed(self, tmp_path: Path) -> None: |
579 | 580 | """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: |
1170 | 1171 | ) |
1171 | 1172 |
|
1172 | 1173 | 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