"""Exercise CI selection against real Git trees, manifests, and workflow gates.""" from __future__ import annotations import itertools import json from pathlib import Path import re import subprocess import tempfile import tomllib import unittest from unittest.mock import patch from scripts.ci import select_rust_jobs as selector from scripts.tests.test_release_workflow import gate ROOT = Path(__file__).resolve().parents[2] ALL = set(selector.OUTPUTS) def workflow_jobs(filename: str) -> dict[str, str]: text = (ROOT / ".github/workflows" / filename).read_text(encoding="utf-8") parts = re.split(r"^ ([a-z][a-z0-9-]*):\s*$", text.split("\njobs:\n", 1)[1], flags=re.M) return dict(zip(parts[1::2], parts[2::2])) class PathSelectionTests(unittest.TestCase): def test_paths_enable_only_their_required_jobs(self): cases = ( ("docs-site/docs/guide/tmux.md", set()), ("docs-site/i18n/en/code.json", set()), ("README.zh-CN.md", set()), ("AGENTS.md", set()), ("LICENSE", set()), (".github/ISSUE_TEMPLATE/bug_report.yml", set()), ("crates/nyaterm-desktop/locales/ja.json", {"run_rust"}), ("crates/nyaterm-desktop/src/features/settings.rs", {"run_rust"}), ("crates/nyaterm-desktop/src/features/tmux.rs", {"run_rust"}), ("crates/nyaterm-transport/src/tmux/control.rs", {"run_rust", "run_tmux"}), ("crates/nyaterm-transport/src/ssh/worker.rs", {"run_rust", "run_tmux"}), ("crates/nyaterm-terminal/src/lib.rs", {"run_rust", "run_tmux"}), ("crates/nyaterm-core/src/lib.rs", {"run_rust", "run_tmux", "run_plugin_sdk"}), ("crates/nyaterm-plugin-api/wit/plugin.wit", {"run_rust", "run_tmux", "run_plugin_sdk"}), ("crates/nyaterm-mcp-protocol/src/lib.rs", {"run_rust", "run_tmux", "run_plugin_sdk"}), ("crates/nyaterm-store/src/lib.rs", {"run_rust", "run_plugin_sdk"}), ("crates/nyaterm-sync/src/lib.rs", {"run_rust", "run_plugin_sdk"}), ("crates/nyaterm-plugin-host/tests/fixtures/text-tools.wasm", {"run_rust", "run_plugin_sdk"}), ("crates/nyaterm-core/tests/fixtures/README.md", {"run_rust", "run_tmux", "run_plugin_sdk"}), ("crates/nyaterm-app/assets/icons/terminal.svg", {"run_rust"}), ("crates/nyaterm-app/build.rs", {"run_rust", "run_python"}), ("crates/nyaterm-app/resources/icons/icon.ico", {"run_rust", "run_python"}), ("scripts/release/package_native.py", {"run_python"}), ("scripts/tests/test_release_workflow.py", {"run_python"}), ("scripts/tests/test_windows_resources.py", {"run_rust", "run_python"}), ("scripts/plugins/build.py", {"run_python", "run_plugin_sdk"}), ("examples/plugins/guests/text-tools/src/lib.rs", {"run_plugin_sdk"}), ("examples/plugins/templates/templates/inspect.txt", {"run_plugin_sdk"}), (".github/workflows/release.yml", {"run_python", "run_workflows"}), (".github/workflows/docs.yml", {"run_python", "run_workflows"}), (".github/actionlint.yaml", {"run_workflows"}), ) for path, expected in cases: with self.subTest(path=path): self.assertEqual(selector.select_paths([path]).enabled, expected) def test_shared_configuration_enables_every_job(self): paths = ( "Cargo.toml", "Cargo.lock", "crates/nyaterm-desktop/Cargo.toml", "examples/plugins/guests/Cargo.lock", ".cargo/config.toml", "examples/plugins/guests/.cargo/config.toml", "rust-toolchain", "rust-toolchain.toml", "rustfmt.toml", ".rustfmt.toml", "clippy.toml", ".clippy.toml", ".github/workflows/rust.yml", ".github/actions/setup-rust/action.yml", "scripts/ci/select_rust_jobs.py", "scripts/ci/install-linux-deps.sh", "scripts/ci/architecture_allowlist.json", ) for path in paths: with self.subTest(path=path): self.assertEqual(selector.select_paths([path]).enabled, ALL) def test_unknown_paths_and_new_crates_enable_every_job(self): for path in ("crates/nyaterm-new/src/lib.rs", "fixtures/example.md", ".gitattributes", "new-script.sh"): with self.subTest(path=path): self.assertEqual(selector.select_paths([path]).enabled, ALL) def test_mixed_changes_union_all_jobs_and_preserve_reasons(self): selection = selector.select_paths([ "README.md", "crates/nyaterm-transport/src/lib.rs", "scripts/plugins/verify.py", ".github/actionlint.yaml", ]) self.assertEqual(selection.enabled, ALL) self.assertEqual(len(selection.reasons), 4) def test_empty_diff_skips_optional_jobs(self): selection = selector.select_paths([]) self.assertEqual(selection.enabled, set()) self.assertIn("No changed paths", selection.summary()) def test_output_values_are_literal_lowercase_boolean_strings(self): self.assertEqual(selector.select_paths(["README.md"]).outputs(), dict.fromkeys(selector.OUTPUTS, "false")) self.assertEqual(selector.full_selection("test").outputs(), dict.fromkeys(selector.OUTPUTS, "true")) def test_untrusted_filenames_cannot_inject_summary_markup_or_commands(self): summary = selector.select_paths(['docs-site/