diff --git a/.github/workflows/agent-notification-tests.yml b/.github/workflows/agent-notification-tests.yml index 6d7322977c19..6adbed73f989 100644 --- a/.github/workflows/agent-notification-tests.yml +++ b/.github/workflows/agent-notification-tests.yml @@ -12,7 +12,28 @@ on: - Sources/TerminalNotification*.swift - Sources/AgentFeedSemanticInput.swift - Sources/Feed/** - - cmuxTests/** + # The files that define a suite in unit_test_suites below. ci.yml's shards + # run all of cmuxTests, so a broader pattern only adds a second run. + - cmuxTests/AgentDeliveryTTYBindingTests.swift + - cmuxTests/AgentJournalLifecycleCenterTests.swift + - cmuxTests/AgentNotificationLiveRetargetTests.swift + - cmuxTests/AgentNotificationMoveRaceTests.swift + - cmuxTests/AgentNotificationMutationBoundaryTests.swift + - cmuxTests/AgentRelayTTYOwnershipTests.swift + - cmuxTests/AgentSemanticNotificationDeliveryTests.swift + - cmuxTests/ClaudeBackgroundWorkNotifyTests.swift + - cmuxTests/FeedWaiterRegistryTests.swift + - cmuxTests/MemoryPressureNotificationTests.swift + - cmuxTests/NativeNotificationFallbackCommandTests.swift + - cmuxTests/OpenCodeHookRegressionTests.swift + # Shared helpers those suite files use. + - cmuxTests/AgentHookTestNotificationPipeline.swift + - cmuxTests/AgentJournalTestSupport.swift + - cmuxTests/BundledCLILinkageTests.swift + - cmuxTests/ClaudeHookLiveDeliveryTargetTestSupport.swift + - cmuxTests/ClaudeHookSurfaceResolutionSwiftTests.swift + - cmuxTests/ClaudeNotificationStatusLifecycleTests.swift + - cmuxTests/TerminalControllingTTYWaiter.swift - Packages/macOS/CMUXAgentLaunch/** - .github/workflows/test-depot.yml - scripts/ci/classify-app-host-test-output.py diff --git a/tests/test_ci_self_hosted_guard.sh b/tests/test_ci_self_hosted_guard.sh index 4831fd9eb927..d7610d9db498 100755 --- a/tests/test_ci_self_hosted_guard.sh +++ b/tests/test_ci_self_hosted_guard.sh @@ -1292,6 +1292,62 @@ check_signing_intermediate_imports check_signing_intermediate_helper_behavior check_sentry_cli_install_portability check_sentry_cli_helper_behavior +check_agent_notification_paths_cover_its_suites() { + # The workflow reruns suites that ci.yml's shards already run, so it should + # start only for changes that can affect them: every file that defines one of + # its suites, and every helper file those name, must match a path trigger, + # and no cmuxTests trigger may match any other file. + ROOT_DIR="$ROOT_DIR" python3 - <<'PY' +import fnmatch, os, re, sys +from pathlib import Path + +root = Path(os.environ["ROOT_DIR"]) +text = (root / ".github/workflows/agent-notification-tests.yml").read_text(encoding="utf-8") +paths = re.findall(r"^\s+- (cmuxTests/\S+)\s*$", text, flags=re.M) +suites = re.search(r"^\s*unit_test_suites:\s*(\S+)", text, flags=re.M).group(1).split(",") +errors = [] +if "cmuxTests/**" in paths: + errors.append("must not trigger on all of cmuxTests/**") +sources = {f: f.read_text(encoding="utf-8", errors="ignore") for f in sorted((root / "cmuxTests").glob("*.swift"))} +suite_files = set() +for suite in suites: + decl = re.compile(rf"^\s*(?:@\w+(?:\([^)]*\))?\s+)*(?:\w+\s+)*(?:class|struct|actor|extension)\s+{re.escape(suite)}\b", re.M) + files = [f for f, source in sources.items() if decl.search(source)] + if not files: + errors.append(f"runs {suite}, which no file in cmuxTests defines") + suite_files.update(files) + for f in files: + rel = f"cmuxTests/{f.name}" + if not any(fnmatch.fnmatchcase(rel, p) for p in paths): + errors.append(f"runs {suite} but {rel} matches no path trigger") +# A helper is a file whose top-level type the suite files name. Nested and +# private types are skipped: another file cannot reach them, and several test +# files declare a private type of the same name. +suite_text = "\n".join(sources[f] for f in suite_files) +helper_files = set() +top_level = re.compile(r"^(?:@\w+(?:\([^)]*\))?\s+)*(?:(?:final|internal|public|open)\s+)*(?:class|struct|enum|actor|protocol)\s+(\w+)", re.M) +for f, source in sources.items(): + if f in suite_files: + continue + used = sorted(n for n in set(top_level.findall(source)) if re.search(rf"\b{re.escape(n)}\b", suite_text)) + rel = f"cmuxTests/{f.name}" + if used: + helper_files.add(f) + if used and not any(fnmatch.fnmatchcase(rel, p) for p in paths): + errors.append(f"suites use {', '.join(used)} from {rel}, which matches no path trigger") +# A trigger that also matches unrelated test files starts a second run of +# suites that ci.yml already ran. +for p in paths: + extra = sorted(f.name for f in sources if fnmatch.fnmatchcase(f"cmuxTests/{f.name}", p) and f not in suite_files | helper_files) + if extra: + errors.append(f"trigger {p} also matches unrelated files: {', '.join(extra[:5])}") +for e in errors: + print(f"FAIL: agent-notification-tests.yml {e}") +sys.exit(1 if errors else 0) +PY + echo "PASS: agent notification paths cover every suite file and helper the workflow runs" +} + pr_workflow_events() { # Prints the pull request events a workflow triggers on, for the mapping, # list and scalar forms of `on:`. @@ -1432,5 +1488,6 @@ check_no_ci_swift_package_skips check_web_db_behavior_tests check_web_test_runner_behavior check_tmux_terminal_nightly_isolation +check_agent_notification_paths_cover_its_suites check_pr_macos_workflows_cancel_superseded_runs check_no_paid_overflow_fallbacks