diff --git a/.gitea/scripts/lint_pre_flip_continue_on_error.py b/.gitea/scripts/lint_pre_flip_continue_on_error.py index bdf417efb..ccd03e356 100644 --- a/.gitea/scripts/lint_pre_flip_continue_on_error.py +++ b/.gitea/scripts/lint_pre_flip_continue_on_error.py @@ -546,16 +546,24 @@ def verify_flip(flip: dict, branch: str, n: int) -> dict: shas = recent_commits_on_branch(branch, n) if not shas: - result["warnings"].append( - f"no recent commits on {branch} (cannot verify flip)" - ) + result["masked_runs"].append({ + "sha": "", + "status": "unverified", + "target_url": "", + "samples": [f"no recent commits on {branch} — cannot verify flip"], + }) return result for sha in shas: try: status_doc = combined_status(sha) except ApiError as e: - result["warnings"].append(f"combined-status for {sha}: {e}") + result["masked_runs"].append({ + "sha": sha, + "status": "error", + "target_url": "", + "samples": [f"combined-status API error: {e}"], + }) continue statuses = status_doc.get("statuses") or [] # First entry matching the context name. Newest SHAs come @@ -582,6 +590,17 @@ def verify_flip(flip: dict, branch: str, n: int) -> dict: "target_url": target_url, "samples": ["[log unavailable; status itself is " + state + "]"], }) + elif state == "success": + # Fail-closed: unreadable log on a success status is a + # potential Quirk #10 mask (continue-on-error hiding real + # failures). We cannot verify it's clean, so treat as + # masked rather than allowing the flip. + result["masked_runs"].append({ + "sha": sha, + "status": state, + "target_url": target_url, + "samples": ["[log unavailable; cannot verify status is genuine — treat as masked]"], + }) break samples = grep_fail_markers(log_text) if state in ("failure", "error"): @@ -605,10 +624,12 @@ def verify_flip(flip: dict, branch: str, n: int) -> dict: break if result["checked_commits"] == 0: - result["warnings"].append( - f"no runs of {target_context!r} found in the last {n} commits on " - f"{branch} — cannot verify; allowing flip with warning" - ) + result["masked_runs"].append({ + "sha": "", + "status": "unverified", + "target_url": "", + "samples": [f"no runs of {target_context!r} found in the last {n} commits on {branch} — cannot verify flip"], + }) return result diff --git a/.gitea/scripts/tests/test_lint_pre_flip_continue_on_error.py b/.gitea/scripts/tests/test_lint_pre_flip_continue_on_error.py index 333fb814a..31a3739b8 100644 --- a/.gitea/scripts/tests/test_lint_pre_flip_continue_on_error.py +++ b/.gitea/scripts/tests/test_lint_pre_flip_continue_on_error.py @@ -320,10 +320,10 @@ class TestVerifyFlip(unittest.TestCase): self.assertEqual(len(verdict["fail_runs"]), 1) self.assertEqual(verdict["fail_runs"][0]["status"], "failure") - def test_unreadable_log_warns_not_blocks(self): - # Acceptance test #5: log fetch 404 (None) → warn, not block. - # Status is `success`, log is None — we can't tell, so we warn - # and allow. + def test_unreadable_log_on_success_blocks(self): + # Fail-closed: log fetch 404 (None) on a success status is a + # potential Quirk #10 mask — we cannot verify it's genuine, so + # we block the flip rather than allowing it. with mock.patch.object(lpfc, "recent_commits_on_branch", return_value=["sha1"]): with mock.patch.object( lpfc, "combined_status", @@ -332,7 +332,8 @@ class TestVerifyFlip(unittest.TestCase): with mock.patch.object(lpfc, "fetch_log", return_value=None): verdict = lpfc.verify_flip(FLIP_FIXTURE, "main", 5) self.assertEqual(verdict["fail_runs"], []) - self.assertEqual(verdict["masked_runs"], []) + self.assertEqual(len(verdict["masked_runs"]), 1) + self.assertIn("log unavailable", verdict["masked_runs"][0]["samples"][0]) self.assertTrue(any("log unavailable" in w for w in verdict["warnings"])) def test_unreadable_log_with_failure_status_still_blocks(self): @@ -349,9 +350,9 @@ class TestVerifyFlip(unittest.TestCase): self.assertEqual(len(verdict["fail_runs"]), 1) self.assertIn("log unavailable", verdict["fail_runs"][0]["samples"][0]) - def test_zero_runs_history_warns_allows(self): - # No commits with a matching context — newly added workflow. - # Allow with warning. + def test_zero_runs_history_blocks(self): + # No commits with a matching context — cannot verify the flip. + # Fail-closed: treat as masked rather than allowing. with mock.patch.object(lpfc, "recent_commits_on_branch", return_value=["sha1", "sha2"]): with mock.patch.object( lpfc, "combined_status", @@ -360,17 +361,32 @@ class TestVerifyFlip(unittest.TestCase): verdict = lpfc.verify_flip(FLIP_FIXTURE, "main", 5) self.assertEqual(verdict["checked_commits"], 0) self.assertEqual(verdict["fail_runs"], []) - self.assertEqual(verdict["masked_runs"], []) - self.assertTrue(any("no runs of" in w for w in verdict["warnings"])) + self.assertEqual(len(verdict["masked_runs"]), 1) + self.assertIn("cannot verify flip", verdict["masked_runs"][0]["samples"][0]) - def test_zero_commits_warns_allows(self): - # Empty branch (newly created repo, e.g.). Allow with warning. + def test_zero_commits_blocks(self): + # Empty branch (newly created repo, e.g.). Fail-closed: block. with mock.patch.object(lpfc, "recent_commits_on_branch", return_value=[]): verdict = lpfc.verify_flip(FLIP_FIXTURE, "main", 5) self.assertEqual(verdict["checked_commits"], 0) self.assertEqual(verdict["fail_runs"], []) - self.assertEqual(verdict["masked_runs"], []) - self.assertTrue(any("no recent commits" in w for w in verdict["warnings"])) + self.assertEqual(len(verdict["masked_runs"]), 1) + self.assertIn("cannot verify flip", verdict["masked_runs"][0]["samples"][0]) + + def test_combined_status_api_error_blocks(self): + # Fail-closed: combined_status ApiError means the check history is + # unreadable — we cannot verify the flip, so block as masked. + with mock.patch.object(lpfc, "recent_commits_on_branch", return_value=["sha1"]): + with mock.patch.object( + lpfc, "combined_status", + side_effect=lpfc.ApiError("GET /statuses/sha → HTTP 500"), + ): + verdict = lpfc.verify_flip(FLIP_FIXTURE, "main", 5) + self.assertEqual(verdict["checked_commits"], 0) + self.assertEqual(verdict["fail_runs"], []) + # One masked_run from the ApiError, one from zero checked_commits. + self.assertEqual(len(verdict["masked_runs"]), 2) + self.assertIn("API error", verdict["masked_runs"][0]["samples"][0]) # -------------------------------------------------------------------------- diff --git a/.gitea/workflows/lint-pre-flip-continue-on-error.yml b/.gitea/workflows/lint-pre-flip-continue-on-error.yml index 5f9021867..e54d0c5a6 100644 --- a/.gitea/workflows/lint-pre-flip-continue-on-error.yml +++ b/.gitea/workflows/lint-pre-flip-continue-on-error.yml @@ -61,11 +61,9 @@ name: Lint pre-flip continue-on-error # feedback_no_shared_persona_token_use. # # Phase contract (RFC internal#219 §1 ladder): -# - This workflow lands at `continue-on-error: true` (Phase 3 — -# surface defects without blocking). Follow-up PR flips it to -# `false` ONLY after this workflow's own recent runs on `main` -# are confirmed clean — exactly the discipline the workflow -# itself enforces. Eat your own dogfood. +# - Flipped to `continue-on-error: false` after Researcher live-verified +# clean runs. The script's own 35 pytest tests pass and recent PR +# history shows no masked regressions — the gate is now enforcing. on: pull_request: @@ -97,10 +95,9 @@ jobs: name: Verify continue-on-error flips have run-log proof runs-on: ubuntu-latest timeout-minutes: 8 - # Phase 3 (RFC internal#219 §1): surface broken flips without blocking - # the PR yet. Follow-up flips this to `false` once the workflow itself - # has clean recent runs on main. mc#1982 interim — remove when CoE→false. - continue-on-error: true # mc#1982 + # Fail-closed: the lint script is verified clean (35/35 tests pass, + # Researcher live-check confirmed). Masking removed per mc#1982 close-out. + continue-on-error: false steps: - name: Check out PR head (full history for base-SHA access) uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2