diff --git a/.github/scripts/linux_ci_cargo_test_partition.py b/.github/scripts/linux_ci_cargo_test_partition.py index 3d5342b7..93bcbd0c 100644 --- a/.github/scripts/linux_ci_cargo_test_partition.py +++ b/.github/scripts/linux_ci_cargo_test_partition.py @@ -15,6 +15,8 @@ TESTABLE_KINDS = frozenset({"lib", "bin", "test", "bench", "example"}) SKIPPED_ONLY_KINDS = frozenset({"custom-build"}) TARGET_NAME_RE = re.compile(r"^[A-Za-z0-9_-]+$") +NON_LIB_SHARDS = ("merge", "other") +MERGE_TEST_TARGETS = frozenset({"merge_cli", "pr_issue_cli"}) @dataclass(frozen=True) @@ -117,7 +119,11 @@ def load_root_package(manifest_dir: Path) -> Mapping[str, object]: raise SystemExit(f"no metadata package for {manifest_path}") -def partition_argv(targets: Sequence[TestableTarget], partition: str) -> list[str]: +def partition_argv( + targets: Sequence[TestableTarget], partition: str, shard: str | None = None +) -> list[str]: + if shard is not None and (partition != "non-lib" or shard not in NON_LIB_SHARDS): + raise ValueError(f"invalid {partition} shard: {shard}") if partition == "lib": lib_targets = [target for target in targets if target.kind == "lib"] if len(lib_targets) != 1: @@ -128,6 +134,9 @@ def partition_argv(targets: Sequence[TestableTarget], partition: str) -> list[st for target in targets: if target.kind == "lib": continue + owner = "merge" if target.kind == "test" and target.name in MERGE_TEST_TARGETS else "other" + if shard is not None and owner != shard: + continue argv.extend(target.argv_tokens()) return argv raise ValueError(f"unknown partition: {partition}") @@ -183,6 +192,12 @@ def verify_partition(targets: Sequence[TestableTarget]) -> None: "partition argv does not cover metadata targets " f"(missing={missing!r}, extra={extra!r})" ) + sharded_keys = [ + key for shard in NON_LIB_SHARDS + for key in argv_to_keys(partition_argv(targets, "non-lib", shard)) + ] + if len(sharded_keys) != len(set(sharded_keys)) or set(sharded_keys) != non_lib_set: + raise ValueError("non-lib shards do not form a disjoint complete partition") def emit_argv(argv: Sequence[str]) -> None: @@ -203,6 +218,10 @@ def build_parser() -> argparse.ArgumentParser: choices=("lib", "non-lib"), help="required for argv", ) + parser.add_argument( + "--shard", choices=NON_LIB_SHARDS, + help="optional non-lib target shard; omission retains the complete non-lib partition", + ) parser.add_argument( "--manifest-dir", type=Path, @@ -225,7 +244,7 @@ def main(argv: Sequence[str] | None = None) -> int: if args.partition is None: parser.error("argv requires a partition") - partition_argv_tokens = partition_argv(targets, args.partition) + partition_argv_tokens = partition_argv(targets, args.partition, args.shard) if args.partition == "non-lib" and not partition_argv_tokens: sys.stderr.write("non-lib partition is empty\n") return 1 diff --git a/.github/scripts/linux_ci_library_shards.py b/.github/scripts/linux_ci_library_shards.py index 387a5373..bd37b8be 100644 --- a/.github/scripts/linux_ci_library_shards.py +++ b/.github/scripts/linux_ci_library_shards.py @@ -14,7 +14,10 @@ from pathlib import Path from typing import Sequence -SHARDS = ("autopilot", "supervise-core", "supervise-fixtures", "rest") +SHARDS = ( + "autopilot", "supervise-core", "supervise-bound", "supervise-decomposition", "supervise-parent", + "supervise-nested", "supervise-fixtures", "external-agent", "worktree", "rest", +) REST_SAFETY_PREFLIGHTS = ( "process_runner::tests::gemini_online_profile_projects_only_the_trusted_resolver_file", "runtime_adapter::cursor_managed_parent::tests::offline_check_failure_diagnostic_keeps_untrusted_output_private", @@ -95,6 +98,10 @@ def partition_inventory(names: Sequence[str]) -> dict[str, list[str]]: if not names or len(names) != len(set(names)): raise ValueError("library inventory must be nonempty and unique") partitions: dict[str, list[str]] = {shard: [] for shard in SHARDS} + standalone = { + *SAFETY_PREFLIGHTS["supervise-core"], + *SUPERVISE_DIAGNOSTICS, *SUPERVISE_CAUSAL_DIAGNOSTICS, + } for name in sorted(names): # Match the root module, not an arbitrary substring in a test name. if name.startswith("autopilot::"): @@ -103,6 +110,23 @@ def partition_inventory(names: Sequence[str]) -> dict[str, list[str]]: owner = "supervise-fixtures" elif name.startswith("supervise::"): owner = "supervise-core" + # Keep the existing standalone preflights/diagnostics with their + # original owners; split the remaining slow assignment bodies. + if name not in standalone: + if name.startswith("supervise::assignment_execution::bound_parent_turn"): + owner = "supervise-bound" + elif name.startswith("supervise::assignment_execution::decomposition_tests::"): + owner = "supervise-decomposition" + elif name.startswith("supervise::assignment_execution::managed_parent_controller_tests::"): + owner = "supervise-parent" + elif name.startswith("supervise::assignment_execution::") and not name.startswith( + "supervise::assignment_execution::attached_parent_capture_tests::" + ): + owner = "supervise-nested" + elif name.startswith("external_agent::"): + owner = "external-agent" + elif name.startswith("worktree::"): + owner = "worktree" else: owner = "rest" partitions[owner].append(name) diff --git a/.github/scripts/run-linux-ci-cargo-test-partition.sh b/.github/scripts/run-linux-ci-cargo-test-partition.sh index c01a2c30..173167de 100644 --- a/.github/scripts/run-linux-ci-cargo-test-partition.sh +++ b/.github/scripts/run-linux-ci-cargo-test-partition.sh @@ -3,13 +3,15 @@ set -euo pipefail if [[ "$#" -lt 1 || "$#" -gt 2 ]]; then - printf '%s\n' "usage: $0 lib | non-lib" >&2 + printf '%s\n' "usage: $0 lib | non-lib [merge|other]" >&2 exit 1 fi partition="$1" case "${partition}:${2:-}" in - lib:autopilot | lib:supervise-core | lib:supervise-fixtures | lib:rest | non-lib:) ;; + lib:autopilot | lib:supervise-core | lib:supervise-bound | lib:supervise-decomposition | lib:supervise-parent | \ + lib:supervise-nested | lib:supervise-fixtures | lib:external-agent | lib:worktree | lib:rest | \ + non-lib: | non-lib:merge | non-lib:other) ;; *) printf '%s\n' "invalid partition: ${partition}" >&2 exit 1 @@ -25,8 +27,12 @@ python3 "${partition_py}" verify --manifest-dir "${repo_root}" partition_argv_output="" argv_status=0 +shard_args=() +if [[ "${partition}" == non-lib && -n "${2:-}" ]]; then + shard_args=(--shard "$2") +fi partition_argv_output="$( - python3 "${partition_py}" argv "${partition}" --manifest-dir "${repo_root}" + python3 "${partition_py}" argv "${partition}" "${shard_args[@]}" --manifest-dir "${repo_root}" )" || argv_status=$? if [[ "${argv_status}" -ne 0 ]]; then exit "${argv_status}" diff --git a/.github/scripts/test_linux_ci_library_shards.py b/.github/scripts/test_linux_ci_library_shards.py index bdaf6327..7191aa2c 100644 --- a/.github/scripts/test_linux_ci_library_shards.py +++ b/.github/scripts/test_linux_ci_library_shards.py @@ -22,10 +22,11 @@ class LinuxCiLibraryShardTests(unittest.TestCase): def setUpClass(cls) -> None: cls.shards = load_library_shards() - def test_four_shard_names(self) -> None: + def test_shard_names(self) -> None: self.assertEqual( self.shards.SHARDS, - ("autopilot", "supervise-core", "supervise-fixtures", "rest"), + ("autopilot", "supervise-core", "supervise-bound", "supervise-decomposition", "supervise-parent", + "supervise-nested", "supervise-fixtures", "external-agent", "worktree", "rest"), ) def test_mixed_inventory_disjoint_complete_union(self) -> None: @@ -38,7 +39,7 @@ def test_mixed_inventory_disjoint_complete_union(self) -> None: parts = self.shards.partition_inventory(inventory) self.assertEqual(parts["autopilot"], ["autopilot::alpha"]) self.assertEqual( - parts["supervise-core"], ["supervise::assignment_execution::beta"] + parts["supervise-nested"], ["supervise::assignment_execution::beta"] ) self.assertEqual(parts["supervise-fixtures"], ["supervise::tests::gamma"]) self.assertEqual(parts["rest"], ["runtime_adapter::delta"]) @@ -46,6 +47,39 @@ def test_mixed_inventory_disjoint_complete_union(self) -> None: self.assertEqual(sorted(flattened), sorted(inventory)) self.assertEqual(len(flattened), len(set(flattened))) + def test_new_shards_refine_the_old_partition_without_loss_or_overlap(self) -> None: + inventory = [ + *self.shards.SUPERVISE_DIAGNOSTICS, + *self.shards.SUPERVISE_CAUSAL_DIAGNOSTICS, + *self.shards.SAFETY_PREFLIGHTS["supervise-core"], + *self.shards.REST_SAFETY_PREFLIGHTS, + "autopilot::new", "supervise::tests::new", "supervise::future::new", + "supervise::assignment_execution::decomposition_tests::new", + "supervise::assignment_execution::managed_parent_controller_tests::new", + "supervise::assignment_execution::nested_worker::new", + "supervise::assignment_execution::attached_parent_capture_tests::new", + "supervise::assignment_execution::bound_parent_turn_tests::new", + "external_agent::new", "worktree::new", "future_module::new", + "future::external_agent::lookalike", "worktree_extra::lookalike", + ] + inventory = sorted(set(inventory)) + parts = self.shards.partition_inventory(inventory) + self.assertEqual(parts, self.shards.partition_inventory(list(reversed(inventory)))) + old_core = sorted(n for n in inventory if n.startswith("supervise::") + and not n.startswith("supervise::tests::")) + old_rest = sorted(n for n in inventory if not n.startswith(("autopilot::", "supervise::"))) + self.assertEqual(sorted(n for key in ("supervise-core", "supervise-bound", "supervise-decomposition", + "supervise-parent", "supervise-nested") + for n in parts[key]), old_core) + self.assertEqual(sorted(n for key in ("external-agent", "worktree", "rest") + for n in parts[key]), old_rest) + flattened = [n for group in parts.values() for n in group] + self.assertEqual(sorted(flattened), inventory) + self.assertEqual(len(flattened), len(set(flattened))) + for shard, names in self.shards.SAFETY_PREFLIGHTS.items(): + self.assertTrue(set(names) <= set(parts[shard])) + self.shards.validate_mandatory_supervise_ownership(inventory, parts) + def test_lookalike_substrings_go_to_rest(self) -> None: inventory = [ "host::autopilot::lookalike", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 094ac85d..80e4e5b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,14 +70,12 @@ jobs: linux-library: name: Linux library tests (${{ matrix.shard }}) needs: nix-package - # Ubuntu 26.04 shards exceeded the 45-minute job timeout mid-test; keep - # 24.04 (last within budget) until 26.04 test runtime fits the timeout. - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest timeout-minutes: 45 strategy: fail-fast: false matrix: - shard: [autopilot, supervise-core, supervise-fixtures, rest] + shard: [autopilot, supervise-core, supervise-bound, supervise-decomposition, supervise-parent, supervise-nested, supervise-fixtures, external-agent, worktree, rest] steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -258,6 +256,18 @@ jobs: run: | sudo apt-get update sudo apt-get install -y pkg-config libdbus-1-dev + # Package installation can replace systemd and its sandbox executor. + # Re-exec the existing user manager before starting confined tests. + if systemctl is-active --quiet "user@$(id -u).service"; then + export XDG_RUNTIME_DIR="/run/user/$(id -u)" + export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" + systemctl --version + echo 'systemd user manager before re-exec:' + systemctl --user show --property=Version + systemctl --user daemon-reexec + echo 'systemd user manager after re-exec:' + systemctl --user show --property=Version + fi - name: Verify delegated Linux process startup run: bash .github/scripts/run-linux-all-tests-in-delegated-user-scope.sh /usr/bin/true @@ -319,12 +329,14 @@ jobs: run: bash .github/scripts/run-linux-ci-cargo-test-partition.sh lib "${{ matrix.shard }}" linux-integration: - name: Linux integration tests + name: Linux integration tests (${{ matrix.shard }}) needs: nix-package - # Ubuntu 26.04 hits the candidate-validation readiness timeout; keep the - # last verified image until its sandbox startup behavior is qualified. - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + shard: [merge, other] steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -505,6 +517,18 @@ jobs: run: | sudo apt-get update sudo apt-get install -y pkg-config libdbus-1-dev + # Package installation can replace systemd and its sandbox executor. + # Re-exec the existing user manager before starting confined tests. + if systemctl is-active --quiet "user@$(id -u).service"; then + export XDG_RUNTIME_DIR="/run/user/$(id -u)" + export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" + systemctl --version + echo 'systemd user manager before re-exec:' + systemctl --user show --property=Version + systemctl --user daemon-reexec + echo 'systemd user manager after re-exec:' + systemctl --user show --property=Version + fi - name: Verify delegated Linux process startup run: bash .github/scripts/run-linux-all-tests-in-delegated-user-scope.sh /usr/bin/true @@ -520,7 +544,7 @@ jobs: run: bash .github/scripts/install-github-hosted-ci-codex.sh - name: Run integration and binary tests - run: bash .github/scripts/run-linux-ci-cargo-test-partition.sh non-lib + run: bash .github/scripts/run-linux-ci-cargo-test-partition.sh non-lib "${{ matrix.shard }}" linux-gate: name: Linux fmt, check, clippy, and test diff --git a/scripts/tests/test_linux_ci_cargo_test_partition.py b/scripts/tests/test_linux_ci_cargo_test_partition.py index 1e157934..92dcc918 100644 --- a/scripts/tests/test_linux_ci_cargo_test_partition.py +++ b/scripts/tests/test_linux_ci_cargo_test_partition.py @@ -57,6 +57,42 @@ def test_custom_build_target_is_excluded(self) -> None: targets = partition.collect_testable_targets(package) self.assertEqual([target.kind for target in targets], ["lib"]) + def test_non_lib_shards_cover_original_targets_once_including_future_targets(self) -> None: + package = sample_package() + package["targets"].extend([ + {"kind": ["test"], "name": "pr_issue_cli"}, + {"kind": ["test"], "name": "future_integration"}, + {"kind": ["example"], "name": "merge_cli"}, + ]) + targets = partition.collect_testable_targets(package) + partition.verify_partition(targets) + old = partition.argv_to_keys(partition.partition_argv(targets, "non-lib")) + groups = { + shard: partition.argv_to_keys(partition.partition_argv(targets, "non-lib", shard)) + for shard in partition.NON_LIB_SHARDS + } + self.assertEqual(groups["merge"], [("test", "merge_cli"), ("test", "pr_issue_cli")]) + self.assertIn(("example", "merge_cli"), groups["other"]) + self.assertIn(("test", "future_integration"), groups["other"]) + flattened = [key for group in groups.values() for key in group] + self.assertCountEqual(flattened, old) + self.assertEqual(len(flattened), len(set(flattened))) + + def test_invalid_non_lib_shard_refused(self) -> None: + targets = partition.collect_testable_targets(sample_package()) + for group, shard in (("non-lib", "unknown"), ("lib", "merge")): + with self.subTest(group=group, shard=shard), self.assertRaises(ValueError): + partition.partition_argv(targets, group, shard) + + def test_integration_matrix_requires_every_target_shard(self) -> None: + source = WORKFLOW.read_text(encoding="utf-8") + job = source.split(" linux-integration:", 1)[1].split(" linux-gate:", 1)[0] + self.assertIn("shard: [merge, other]", job) + self.assertIn("fail-fast: false", job) + self.assertIn("timeout-minutes: 45", job) + self.assertNotIn("continue-on-error", job) + self.assertIn('non-lib "${{ matrix.shard }}"', job) + def test_unsupported_target_kind_fails_closed(self) -> None: package = { "targets": [ diff --git a/scripts/tests/test_linux_ci_library_shards.py b/scripts/tests/test_linux_ci_library_shards.py index 76dc4563..0b956a39 100644 --- a/scripts/tests/test_linux_ci_library_shards.py +++ b/scripts/tests/test_linux_ci_library_shards.py @@ -684,7 +684,11 @@ def test_wrapper_keeps_metadata_verification_and_delegation(self): " marker.touch()\n" "else:\n" " assert marker.exists()\n" - " print('--lib' if sys.argv[2] == 'lib' else '--bin\\nmaco')\n", + " if '--shard' in sys.argv:\n" + " shard = sys.argv[sys.argv.index('--shard') + 1]\n" + " print('--test\\nmerge_cli' if shard == 'merge' else '--bench\\ncoordination')\n" + " else:\n" + " print('--lib' if sys.argv[2] == 'lib' else '--bin\\nmaco')\n", encoding="utf-8", ) (scripts / "run-linux-all-tests-in-delegated-user-scope.sh").write_text( @@ -703,6 +707,12 @@ def test_wrapper_keeps_metadata_verification_and_delegation(self): ) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.splitlines(), ["cargo", "test", "--locked", "--bin", "maco"]) + for shard, flags in (("merge", ["--test", "merge_cli"]), ("other", ["--bench", "coordination"])): + result = subprocess.run( + ["bash", str(wrapper), "non-lib", shard], capture_output=True, text=True + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.splitlines(), ["cargo", "test", "--locked", *flags]) for args in [["lib"], ["lib", "unknown"], ["non-lib", "rest"]]: result = subprocess.run(["bash", str(wrapper), *args], capture_output=True) self.assertNotEqual(result.returncode, 0) @@ -748,15 +758,19 @@ def test_exact_shards_cover_real_harness_and_preserve_ignored_tests(self): mod assignment_execution { mod decomposition_tests { #[test] fn auditor_runtime_override_preserves_active_heterogeneous_lens_contract() {} + #[test] fn new_decomposition_body() {} } mod bound_parent_turn_tests { // Empty body verifies wrapper selection only, not evidence semantics. #[test] fn bound_nested_evidence_rejects_forged_widened_failed_and_cancelled_results() {} + #[test] fn new_bound_parent_body() {} } mod managed_parent_controller_tests { // Empty body verifies wrapper selection only, not worker cleanup semantics. #[test] fn managed_cycle_isolated_workers_overlap_and_import_exact_union() {} + #[test] fn new_managed_parent_body() {} } + mod nested_driver_tests { #[test] fn new_nested_body() {} } mod attached_parent_capture_tests { // Empty body verifies wrapper selection only, not real ownership semantics. #[test] fn attached_capture_bookkeeping_and_shutdown_errors_retain_owners() {} @@ -767,6 +781,8 @@ def test_exact_shards_cover_real_harness_and_preserve_ignored_tests(self): mod autopilot { #[test] fn nested() {} } #[test] fn supervise_lookalike() {} } +mod external_agent { #[test] fn new_external_body() {} } +mod worktree { #[test] fn new_worktree_body() {} } mod process_runner { mod tests { #[test] fn gemini_online_profile_projects_only_the_trusted_resolver_file() {} @@ -787,7 +803,7 @@ def test_exact_shards_cover_real_harness_and_preserve_ignored_tests(self): subprocess.run(["rustc", "--test", str(fixture), "-o", str(binary)], check=True) runner = [str(binary)] inventory = shards.enumerate_tests(runner) - self.assertEqual(len(inventory), 20) + self.assertEqual(len(inventory), 26) partitions = shards.partition_inventory(inventory) observed = [] for shard, names in partitions.items(): @@ -813,7 +829,7 @@ def test_exact_shards_cover_real_harness_and_preserve_ignored_tests(self): ) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn(f'"selected": "{shard}"', result.stdout) - self.assertIn('"total": 20', result.stdout) + self.assertIn('"total": 26', result.stdout) if __name__ == "__main__":