From ad03473337ca4b994f820093a4bf6114d581c618 Mon Sep 17 00:00:00 2001 From: Meta-Develop <116134763+Meta-Develop@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:56:52 +0900 Subject: [PATCH 1/2] Re-exec the CI user manager after package upgrades and restore ubuntu-latest GitHub ubuntu-26.04 images upgrade systemd 259.5 to 259.9 while installing the CAM build dependencies, leaving the running user manager on the old binary. Confined candidate-validation sandboxes then missed their readiness window and library shards hit the 45-minute limit, so the Linux jobs were pinned to ubuntu-24.04. Re-exec the active user manager after apt and log its version so the jobs can return to ubuntu-latest. --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce298aad..9163b4dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,9 +48,7 @@ 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 @@ -236,6 +234,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 @@ -299,9 +309,7 @@ jobs: linux-integration: name: Linux integration tests 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 steps: - name: Check out repository @@ -483,6 +491,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 From 24aaf3babf8a6535f8a2400ee7f0b6e4d5c60110 Mon Sep 17 00:00:00 2001 From: Meta-Develop <116134763+Meta-Develop@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:52:06 +0900 Subject: [PATCH 2/2] Split the slow Linux test shards so every job fits on ubuntu-26.04 On ubuntu-26.04 the supervise-core library shard exceeded the 45-minute job limit and the rest shard needed 44.5 minutes. Refine supervise-core and rest by module into smaller library shards and split the integration targets into merge and other groups, keeping the job limits and requiring every shard. The partition helpers verify at runtime that the shards cover exactly the original test set. --- .../scripts/linux_ci_cargo_test_partition.py | 23 ++++++++++- .github/scripts/linux_ci_library_shards.py | 26 +++++++++++- .../run-linux-ci-cargo-test-partition.sh | 12 ++++-- .../scripts/test_linux_ci_library_shards.py | 40 +++++++++++++++++-- .github/workflows/ci.yml | 10 +++-- .../test_linux_ci_cargo_test_partition.py | 36 +++++++++++++++++ scripts/tests/test_linux_ci_library_shards.py | 22 ++++++++-- 7 files changed, 154 insertions(+), 15 deletions(-) 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 9163b4dc..9afb7028 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: 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 @@ -307,10 +307,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 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 @@ -518,7 +522,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__":