返回 CodeWhale
test_ci_migration_wiring.py
根目录 / scripts / test_ci_migration_wiring.py
1 #!/usr/bin/env python3
2 """Hermetic tests for the FEAT-015 migration-gate CI wiring.
3
4 Verifies `.github/workflows/ci.yml` keeps both migration checker commands
5 (self-tests then live scan) under the same `heavy` condition as the existing
6 command-contract boundary step, and that the boundary step itself remains
7 intact.
8 """
9
10 from __future__ import annotations
11
12 import unittest
13 from pathlib import Path
14
15 ROOT = Path(__file__).resolve().parents[1]
16 CI_PATH = ROOT / ".github" / "workflows" / "ci.yml"
17
18
19 def load_ci() -> str:
20 return CI_PATH.read_text(encoding="utf-8")
21
22
23 def boundary_step_block(ci: str) -> str:
24 """Extract the 'Check command-contract prototype boundary' step block."""
25 marker = "Check command-contract prototype boundary"
26 start = ci.index(marker)
27 step_start = ci.rindex("- name:", 0, start)
28 # The step ends at the next "- name:" after the marker.
29 next_step = ci.index("- name:", start + len(marker))
30 return ci[step_start:next_step]
31
32
33 def migration_step_block(ci: str) -> str:
34 marker = "Check command migration manifest"
35 start = ci.index(marker)
36 step_start = ci.rindex("- name:", 0, start)
37 next_step = ci.index("- name:", start + len(marker))
38 return ci[step_start:next_step]
39
40
41 class CiWiringTests(unittest.TestCase):
42 def test_boundary_step_still_present(self) -> None:
43 ci = load_ci()
44 self.assertIn("Check command-contract prototype boundary", ci)
45 block = boundary_step_block(ci)
46 self.assertIn("test_check_command_crate_boundaries.py", block)
47 self.assertIn("check-command-crate-boundaries.py", block)
48
49 def test_migration_self_test_present(self) -> None:
50 ci = load_ci()
51 block = migration_step_block(ci)
52 self.assertIn("test_check_command_migration_manifest.py", block)
53
54 def test_migration_live_scan_present(self) -> None:
55 ci = load_ci()
56 block = migration_step_block(ci)
57 self.assertIn("check-command-migration-manifest.py", block)
58
59 def test_migration_live_scan_receives_fetched_baseline(self) -> None:
60 block = migration_step_block(load_ci())
61 self.assertIn("PR_BASE_SHA", block)
62 self.assertIn("PUSH_BEFORE_SHA", block)
63 self.assertIn("git fetch --no-tags origin", block)
64 self.assertNotIn(
65 "--depth",
66 block,
67 "baseline fetch must preserve the full ancestry used by later CI range checks",
68 )
69 self.assertIn('--baseline-ref "${baseline}"', block)
70
71 def test_migration_commands_are_ordered_self_test_first(self) -> None:
72 ci = load_ci()
73 block = migration_step_block(ci)
74 self.assertLess(
75 block.index("test_check_command_migration_manifest.py"),
76 block.index("check-command-migration-manifest.py"),
77 "checker self-tests must run before the live migration scan",
78 )
79
80 def test_migration_step_uses_heavy_condition(self) -> None:
81 ci = load_ci()
82 block = migration_step_block(ci)
83 self.assertIn("needs.changes.outputs.heavy == 'true'", block)
84
85 def test_boundary_step_condition_matches_migration_step(self) -> None:
86 ci = load_ci()
87 boundary = boundary_step_block(ci)
88 migration = migration_step_block(ci)
89 self.assertIn("needs.changes.outputs.heavy == 'true'", boundary)
90 self.assertEqual(
91 "needs.changes.outputs.heavy == 'true'" in boundary,
92 "needs.changes.outputs.heavy == 'true'" in migration,
93 )
94
95 def test_migration_step_does_not_remove_boundary_step(self) -> None:
96 ci = load_ci()
97 # Both steps must coexist (the migration step is added beside, never
98 # replacing, the boundary step).
99 self.assertLess(
100 ci.index("Check command-contract prototype boundary"),
101 ci.index("Check command migration manifest"),
102 )
103
104 def test_main_ci_concurrency_is_keyed_by_sha(self) -> None:
105 ci = load_ci()
106 self.assertIn("github.workflow, github.sha", ci)
107 self.assertIn("ci-pr-{0}", ci)
108 self.assertNotIn(
109 "github.event.pull_request.number || github.ref",
110 ci,
111 "main must not share one concurrency group per ref — pending runs get cancelled",
112 )
113 self.assertIn("name: Safety gate", ci)
114 self.assertIn("Hermetic safety and authorization tests", ci)
115
116 def test_safety_gate_is_hermetic_for_config_home(self) -> None:
117 ci = load_ci()
118 start = ci.index("Hermetic safety and authorization tests")
119 next_step = ci.index("- name:", start + 1)
120 block = ci[start:next_step]
121 self.assertIn(
122 "sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-tui "
123 "--lib --locked -- auto_review authority sandbox", block
124 )
125 self.assertIn(
126 "sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-execpolicy --locked",
127 block,
128 )
129 self.assertNotIn("CODEWHALE_HOME:", block)
130 self.assertIn("sh scripts/with-hermetic-test-home.test.sh", ci)
131
132 def test_valid_wiring_passes_all_assertions(self) -> None:
133 # The live workflow must satisfy every structural invariant above.
134 ci = load_ci()
135 self.assertIn("test_check_command_migration_manifest.py", ci)
136 self.assertIn("check-command-migration-manifest.py", ci)
137 self.assertIn("needs.changes.outputs.heavy == 'true'", ci)
138
139
140 if __name__ == "__main__":
141 unittest.main()
142
142 lines PYTHON