| 1 | #!/usr/bin/env python3 |
| 2 | """Hermetic tests for the FEAT-014 command-contract boundary gate.""" |
| 3 | |
| 4 | from __future__ import annotations |
| 5 | |
| 6 | import importlib.util |
| 7 | import sys |
| 8 | import tempfile |
| 9 | import unittest |
| 10 | from pathlib import Path |
| 11 | |
| 12 | ROOT = Path(__file__).resolve().parents[1] |
| 13 | SCRIPT = ROOT / "scripts" / "check-command-crate-boundaries.py" |
| 14 | SPEC = importlib.util.spec_from_file_location("command_boundary", SCRIPT) |
| 15 | assert SPEC and SPEC.loader |
| 16 | mod = importlib.util.module_from_spec(SPEC) |
| 17 | sys.modules[SPEC.name] = mod |
| 18 | SPEC.loader.exec_module(mod) |
| 19 | |
| 20 | |
| 21 | def valid_graph() -> dict[str, set[str]]: |
| 22 | return { |
| 23 | "codewhale-command-contract": {"codewhale-core"}, |
| 24 | "codewhale-core": set(), |
| 25 | "codewhale-secrets": {"codewhale-paths"}, |
| 26 | "codewhale-paths": set(), |
| 27 | "codewhale-tui": set(), |
| 28 | } |
| 29 | |
| 30 | |
| 31 | class DependencyTests(unittest.TestCase): |
| 32 | def test_leaf_graph_passes(self) -> None: |
| 33 | self.assertEqual(mod.check_dependency_graph(valid_graph()), []) |
| 34 | |
| 35 | def test_direct_tui_edge_fails(self) -> None: |
| 36 | graph = valid_graph() |
| 37 | graph["codewhale-command-contract"].add("codewhale-tui") |
| 38 | self.assertEqual(len(mod.check_dependency_graph(graph)), 1) |
| 39 | |
| 40 | def test_transitive_tui_edge_fails(self) -> None: |
| 41 | graph = valid_graph() |
| 42 | graph["codewhale-core"].add("codewhale-tui") |
| 43 | self.assertEqual(len(mod.check_dependency_graph(graph)), 1) |
| 44 | |
| 45 | def test_missing_contract_fails(self) -> None: |
| 46 | graph = valid_graph() |
| 47 | del graph["codewhale-command-contract"] |
| 48 | violations = mod.check_dependency_graph(graph) |
| 49 | self.assertEqual(len(violations), 1) |
| 50 | self.assertIn("missing", str(violations[0])) |
| 51 | |
| 52 | def test_missing_sanitizer_fails(self) -> None: |
| 53 | graph = valid_graph() |
| 54 | del graph["codewhale-secrets"] |
| 55 | violations = mod.check_dependency_graph(graph) |
| 56 | self.assertEqual(len(violations), 1) |
| 57 | self.assertIn("codewhale-secrets", str(violations[0])) |
| 58 | |
| 59 | def test_sanitizer_reaching_tui_fails(self) -> None: |
| 60 | # The shared sanitizer is consumed by portable command helpers, so a TUI |
| 61 | # edge would pull the whole TUI into the extracted command crate. |
| 62 | graph = valid_graph() |
| 63 | graph["codewhale-paths"].add("codewhale-tui") |
| 64 | violations = mod.check_dependency_graph(graph) |
| 65 | self.assertEqual(len(violations), 1) |
| 66 | self.assertIn("codewhale-secrets", str(violations[0])) |
| 67 | |
| 68 | def test_both_packages_reaching_tui_fails_twice(self) -> None: |
| 69 | graph = valid_graph() |
| 70 | graph["codewhale-core"].add("codewhale-tui") |
| 71 | graph["codewhale-paths"].add("codewhale-tui") |
| 72 | self.assertEqual(len(mod.check_dependency_graph(graph)), 2) |
| 73 | |
| 74 | def test_dev_dependency_is_not_a_normal_edge(self) -> None: |
| 75 | metadata = {"packages": [ |
| 76 | {"name": "codewhale-command-contract", "dependencies": [ |
| 77 | {"name": "codewhale-tui", "kind": "dev"}, |
| 78 | {"name": "codewhale-core", "kind": None}, |
| 79 | ]}, |
| 80 | {"name": "codewhale-core", "dependencies": []}, |
| 81 | {"name": "codewhale-secrets", "dependencies": []}, |
| 82 | {"name": "codewhale-tui", "dependencies": []}, |
| 83 | ]} |
| 84 | graph = mod.dependency_graph(metadata) |
| 85 | self.assertEqual(graph["codewhale-command-contract"], {"codewhale-core"}) |
| 86 | self.assertEqual(mod.check_dependency_graph(graph), []) |
| 87 | |
| 88 | |
| 89 | class SourceTests(unittest.TestCase): |
| 90 | def test_clean_shapes_pass(self) -> None: |
| 91 | source = "pub struct CommandContexts<'a> {}\npub trait CommandModelContext {}\n" |
| 92 | self.assertEqual(mod.check_contract_source_text(source, "clean.rs"), []) |
| 93 | |
| 94 | def test_forbidden_edges_fail(self) -> None: |
| 95 | cases = [ |
| 96 | "use codewhale_tui::tui::app::App;", |
| 97 | "use ratatui::widgets::Paragraph;", |
| 98 | "use crate::tui::App;", |
| 99 | "pub struct CommandContext {}", |
| 100 | "let handler: Box<dyn Fn()> = value;", |
| 101 | ] |
| 102 | for source in cases: |
| 103 | with self.subTest(source=source): |
| 104 | self.assertTrue(mod.check_contract_source_text(source, "sample.rs")) |
| 105 | |
| 106 | def test_comments_and_plural_envelope_pass(self) -> None: |
| 107 | source = ( |
| 108 | "// Never import codewhale_tui or define CommandContext here.\n" |
| 109 | "pub struct CommandContexts<'a> { marker: &'a str }\n" |
| 110 | ) |
| 111 | self.assertEqual(mod.check_contract_source_text(source, "safe.rs"), []) |
| 112 | |
| 113 | |
| 114 | class TreeModeTests(unittest.TestCase): |
| 115 | RULE = mod.BoundaryRule( |
| 116 | "codewhale-runtime", |
| 117 | "tree", |
| 118 | ("codewhale-tui", "ratatui", "crossterm"), |
| 119 | "the runtime must stay UI-free", |
| 120 | ) |
| 121 | |
| 122 | def test_parse_cargo_tree_keeps_names(self) -> None: |
| 123 | text = "codewhale-runtime v0.10.0 (/x)\nanyhow v1.0.100\nratatui v0.30.2 (*)\n" |
| 124 | self.assertEqual(mod.parse_cargo_tree(text), {"codewhale-runtime", "anyhow", "ratatui"}) |
| 125 | |
| 126 | def test_clean_tree_passes(self) -> None: |
| 127 | self.assertEqual(mod.check_tree_packages(self.RULE, {"anyhow", "serde"}), []) |
| 128 | |
| 129 | def test_runtime_rule_is_tree_mode(self) -> None: |
| 130 | rule = next(r for r in mod.BOUNDARY_RULES if r.package == "codewhale-runtime") |
| 131 | self.assertEqual(rule.mode, "tree") |
| 132 | self.assertIn("ratatui", rule.forbidden_packages) |
| 133 | self.assertIn("crossterm", rule.forbidden_packages) |
| 134 | |
| 135 | def test_runtime_source_scan(self) -> None: |
| 136 | self.assertEqual( |
| 137 | mod.check_runtime_source_text("// ratatui::Frame is not used here\nfn f() {}\n", "a.rs"), |
| 138 | [], |
| 139 | ) |
| 140 | for source in ( |
| 141 | "use crossterm::terminal;", |
| 142 | "let _ = ratatui::style::Color::Reset;", |
| 143 | 'const X: &str = include_str!("../../tui/assets/x.json");', |
| 144 | ): |
| 145 | with self.subTest(source=source): |
| 146 | self.assertTrue(mod.check_runtime_source_text(source, "a.rs")) |
| 147 | |
| 148 | def test_ui_library_in_tree_fails(self) -> None: |
| 149 | violations = mod.check_tree_packages(self.RULE, {"anyhow", "ratatui", "crossterm"}) |
| 150 | self.assertEqual(len(violations), 2) |
| 151 | self.assertIn("ratatui", str(violations[1]) + str(violations[0])) |
| 152 | |
| 153 | |
| 154 | def write_tree(root: Path, files: dict[str, str]) -> None: |
| 155 | for rel, text in files.items(): |
| 156 | path = root / rel |
| 157 | path.parent.mkdir(parents=True, exist_ok=True) |
| 158 | path.write_text(text, encoding="utf-8") |
| 159 | |
| 160 | |
| 161 | class RatchetTests(unittest.TestCase): |
| 162 | def setUp(self) -> None: |
| 163 | self.graph = mod.load_runtime_ratchet() |
| 164 | |
| 165 | def report(self, tui: dict[str, str], runtime: dict[str, str] | None = None): |
| 166 | with tempfile.TemporaryDirectory() as tmp: |
| 167 | write_tree(Path(tmp, "tui"), tui) |
| 168 | write_tree(Path(tmp, "runtime"), runtime or {}) |
| 169 | return self.graph.build_report(Path(tmp, "tui"), Path(tmp, "runtime")) |
| 170 | |
| 171 | def test_counts_grouped_imports_and_masks_comments_and_strings(self) -> None: |
| 172 | report = self.report({ |
| 173 | "lib.rs": "mod core; mod tui;\n", |
| 174 | "core.rs": ( |
| 175 | "use crate::{tui::App, tui::views::{A, B}};\n" |
| 176 | "// crate::tui::ignored\n" |
| 177 | "const S: &str = \"crate::tui::ignored\";\n" |
| 178 | "fn f() { crate::tui::draw(); }\n" |
| 179 | "#[cfg(test)]\nmod tests { fn t() { crate::tui::fixture(); } }\n" |
| 180 | ), |
| 181 | "tui.rs": "", |
| 182 | }) |
| 183 | self.assertEqual(report.counts["prod"], {"core|tui": 4}) |
| 184 | self.assertEqual(report.counts["test"], {"core|tui": 1}) |
| 185 | |
| 186 | def test_ui_library_and_late_edges_are_counted(self) -> None: |
| 187 | report = self.report({ |
| 188 | "lib.rs": "mod core; mod exec_agent;\n", |
| 189 | "core.rs": "fn f() { crossterm::terminal::enable_raw_mode(); }\n" |
| 190 | "#[cfg(test)]\nmod tests { fn t() { crate::exec_agent::run(); } }\n", |
| 191 | "exec_agent.rs": "", |
| 192 | }) |
| 193 | self.assertEqual(report.counts["uilib"], {"core|crossterm": 1}) |
| 194 | self.assertEqual(report.counts["late"], {"core|exec_agent": 1}) |
| 195 | |
| 196 | def test_runtime_crate_modules_join_the_closure(self) -> None: |
| 197 | report = self.report( |
| 198 | {"lib.rs": "mod core;\nuse codewhale_runtime::{elapsed};\n", "core.rs": ""}, |
| 199 | {"lib.rs": "pub mod elapsed;\n", "elapsed.rs": "fn f() { ratatui::x(); }\n"}, |
| 200 | ) |
| 201 | self.assertIn("elapsed", report.closure) |
| 202 | self.assertEqual(report.counts["uilib"], {"elapsed|ratatui": 1}) |
| 203 | |
| 204 | def test_rise_and_unrecorded_drop_both_fail(self) -> None: |
| 205 | report = self.report({ |
| 206 | "lib.rs": "mod core; mod tui;\n", |
| 207 | "core.rs": "fn f() { crate::tui::a(); crate::tui::b(); }\n", |
| 208 | "tui.rs": "", |
| 209 | }) |
| 210 | rises, drops = self.graph.compare({"counts": {"prod": {"core|tui": 1}}}, report) |
| 211 | self.assertTrue(rises and rises[0].startswith("prod core|tui: 1 -> 2")) |
| 212 | self.assertEqual(drops, []) |
| 213 | rises, drops = self.graph.compare({"counts": {"prod": {"core|tui": 3}}}, report) |
| 214 | self.assertEqual((rises, drops), ([], ["prod core|tui: 3 -> 2"])) |
| 215 | |
| 216 | def test_super_chains_that_reach_the_root_count(self) -> None: |
| 217 | report = self.report({ |
| 218 | "lib.rs": "mod core; mod tui;\n", |
| 219 | "core/mod.rs": ( |
| 220 | "mod inner;\n" |
| 221 | "fn f() { super::tui::a(); }\n" |
| 222 | "pub(in super::super) fn g() {}\n" |
| 223 | "#[cfg(test)]\nmod tests { use super::super::{tui::B, x}; use super::*; }\n" |
| 224 | ), |
| 225 | # Depth 2: two `super`s reach the root, one stays inside `core`. |
| 226 | "core/inner.rs": "use super::super::tui as ui;\nfn f() { super::tui::local(); }\n", |
| 227 | "tui.rs": "", |
| 228 | }) |
| 229 | self.assertEqual(report.counts["prod"], {"core|tui": 2}) |
| 230 | self.assertEqual(report.counts["test"], {"core|tui": 1}) |
| 231 | |
| 232 | def test_hand_raised_baseline_fails_against_the_base(self) -> None: |
| 233 | previous = {"counts": {"prod": {"tools|tui": 8}}} |
| 234 | self.assertEqual(self.graph.baseline_raises(previous, previous), []) |
| 235 | self.assertEqual( |
| 236 | self.graph.baseline_raises(previous, {"counts": {"prod": {"tools|tui": 7}}}), [] |
| 237 | ) |
| 238 | self.assertEqual( |
| 239 | self.graph.baseline_raises(previous, {"counts": {"prod": {"tools|tui": 9}}}), |
| 240 | ["prod tools|tui: 8 -> 9"], |
| 241 | ) |
| 242 | self.assertEqual( |
| 243 | self.graph.baseline_raises(previous, {"counts": {"test": {"core|tui": 1}}}), |
| 244 | ["test core|tui: 0 -> 1 (new pair)"], |
| 245 | ) |
| 246 | |
| 247 | def test_same_scope_split_preserves_original_test_and_production_counts(self) -> None: |
| 248 | original = self.report({ |
| 249 | "lib.rs": 'mod client; mod tui;', |
| 250 | "client.rs": 'fn run() { crate::tui::real(); } #[cfg(test)] mod suite { fn probe() { crate::tui::one(); crate::tui::two(); } }', |
| 251 | "tui.rs": '', |
| 252 | }) |
| 253 | split = self.report({ |
| 254 | "lib.rs": 'mod client; mod tui;', |
| 255 | "client.rs": 'fn run() { crate::tui::real(); } #[cfg(test)] mod suite { include!("client/first.rs"); }', |
| 256 | "client/first.rs": 'fn probe() { crate::tui::one(); crate::tui::two(); }', |
| 257 | "tui.rs": '', |
| 258 | }) |
| 259 | self.assertEqual(split.counts, original.counts) |
| 260 | self.assertEqual(split.closure, original.closure) |
| 261 | self.assertEqual(split.counts["prod"], {"client|tui": 1}) |
| 262 | self.assertEqual(split.counts["test"], {"client|tui": 2}) |
| 263 | |
| 264 | def test_literal_path_and_recursive_raw_includes_reuse_test_scope(self) -> None: |
| 265 | report = self.report({ |
| 266 | "lib.rs": 'mod session_manager; mod tui;', |
| 267 | "session_manager.rs": '#[cfg(test)] #[path="pieces/suite.rs"] mod verification;', |
| 268 | "pieces/suite.rs": 'include!(r"first.rs");', |
| 269 | "pieces/first.rs": 'include!("second.rs");', |
| 270 | "pieces/second.rs": 'fn probe() { crate::tui::one(); }', |
| 271 | "tui.rs": '', |
| 272 | }) |
| 273 | # The graph knows these are test-only even though none has a test name. |
| 274 | crate = self.graph.load_crate("fixture", Path("/not-a-crate")) |
| 275 | self.assertEqual(report.counts["prod"], {}) |
| 276 | # Independent top-level pieces are outside the runtime closure; inspect |
| 277 | # the actual graph for scope below rather than manufacturing a seed. |
| 278 | with tempfile.TemporaryDirectory() as tmp: |
| 279 | source = Path(tmp) / "src" |
| 280 | write_tree(source, { |
| 281 | "lib.rs": 'mod client;', |
| 282 | "client.rs": '#[cfg(test)] #[path="pieces/suite.rs"] mod verification;', |
| 283 | "pieces/suite.rs": 'include!(r"first.rs");', |
| 284 | "pieces/first.rs": 'include!("second.rs");', |
| 285 | "pieces/second.rs": 'fn probe() {}', |
| 286 | }) |
| 287 | crate = self.graph.load_crate("fixture", source) |
| 288 | exact, prefixes = self.graph.test_file_set(crate) |
| 289 | self.assertEqual(exact, {"pieces/suite.rs", "pieces/first.rs", "pieces/second.rs"}) |
| 290 | self.assertEqual(prefixes, set()) |
| 291 | |
| 292 | def test_shared_production_includes_override_test_filename_and_descendants(self) -> None: |
| 293 | report = self.report({ |
| 294 | "lib.rs": 'mod client; mod tui;', |
| 295 | "client.rs": 'include!("client/shared_tests.rs"); #[cfg(test)] mod suite { include!("client/shared_tests.rs"); }', |
| 296 | "client/shared_tests.rs": 'fn real() { crate::tui::one(); } include!("child_test.rs");', |
| 297 | "client/child_test.rs": 'fn real_child() { crate::tui::two(); }', |
| 298 | "tui.rs": '', |
| 299 | }) |
| 300 | self.assertEqual(report.counts["prod"], {"client|tui": 2}) |
| 301 | self.assertEqual(report.counts["test"], {}) |
| 302 | |
| 303 | def test_cfg_test_module_does_not_exempt_a_guessed_directory_prefix(self) -> None: |
| 304 | report = self.report({ |
| 305 | "lib.rs": 'mod client; mod tui;', |
| 306 | "client.rs": '#[cfg(test)] mod suite; #[path="client/suite/shipping.rs"] mod shipping;', |
| 307 | "client/suite.rs": 'fn test_only() {}', |
| 308 | "client/suite/shipping.rs": 'fn real() { crate::tui::one(); }', |
| 309 | "tui.rs": '', |
| 310 | }) |
| 311 | self.assertEqual(report.counts["prod"], {"client|tui": 1}) |
| 312 | self.assertEqual(report.counts["test"], {}) |
| 313 | |
| 314 | def test_comment_dynamic_and_mixed_cfg_cannot_exempt_production_fragments(self) -> None: |
| 315 | report = self.report({ |
| 316 | "lib.rs": 'mod client; mod tui;', |
| 317 | "client.rs": '// #[cfg(test)] mod fake { include!("client/part.rs"); }\n' |
| 318 | '#[cfg(test)] mod suite { include!(concat!("client/", "part.rs")); }' |
| 319 | '#[cfg(any(test, feature="shipping"))] mod shipped { include!("client/part.rs"); }', |
| 320 | "client/part.rs": 'fn real() { crate::tui::one(); }', |
| 321 | "tui.rs": '', |
| 322 | }) |
| 323 | self.assertEqual(report.counts["prod"], {"client|tui": 1}) |
| 324 | self.assertEqual(report.counts["test"], {}) |
| 325 | |
| 326 | def test_real_custom_cargo_root_outside_src_keeps_test_named_child_production(self) -> None: |
| 327 | with tempfile.TemporaryDirectory() as tmp: |
| 328 | crate_root = Path(tmp) / "demo" |
| 329 | source = crate_root / "src" |
| 330 | write_tree(crate_root, { |
| 331 | "Cargo.toml": '[package]\nname="demo"\nversion="0.1.0"\n[lib]\npath="front/entry.rs"\n', |
| 332 | "src/lib.rs": '#[cfg(test)] mod suite { include!("../front/entry.rs"); }', |
| 333 | "front/entry.rs": 'include!("../src/entry_tests.rs");', |
| 334 | "src/entry_tests.rs": 'fn real() { crate::tui::one(); }', |
| 335 | }) |
| 336 | crate = self.graph.load_crate("fixture", source) |
| 337 | exact, prefixes = self.graph.test_file_set(crate) |
| 338 | self.assertFalse(self.graph.file_is_test("entry_tests.rs", exact, prefixes, crate.production_files)) |
| 339 | self.assertIn("entry_tests.rs", crate.production_files) |
| 340 | |
| 341 | def test_scan_roots_do_not_consult_a_neighboring_crates_test_edges(self) -> None: |
| 342 | with tempfile.TemporaryDirectory() as tmp: |
| 343 | left = Path(tmp) / "left" |
| 344 | right = Path(tmp) / "right" |
| 345 | write_tree(left, {"lib.rs": 'mod client;', "client.rs": 'fn real() {}'}) |
| 346 | write_tree(right, {"lib.rs": '#[cfg(test)] mod suite { include!("../left/client.rs"); }'}) |
| 347 | crate = self.graph.load_crate("fixture", left) |
| 348 | exact, _prefixes = self.graph.test_file_set(crate) |
| 349 | self.assertNotIn("client.rs", exact) |
| 350 | |
| 351 | def test_checked_in_baseline_holds(self) -> None: |
| 352 | self.assertEqual(self.graph.check(), []) |
| 353 | |
| 354 | |
| 355 | if __name__ == "__main__": |
| 356 | unittest.main() |
| 357 |