from __future__ import annotations import json import os import re import shutil import subprocess from collections.abc import Callable from pathlib import Path import yaml WORKFLOW_DIR = Path(".github/workflows") CLASSIFIER = Path(".github/scripts/classify-ci-changes.sh") PR_TARGET_VALIDATOR = Path(".github/scripts/validate-pr-target-branch.sh") PR_BODY_LINT = Path(".github/scripts/validate_pr_body.py") TEST_PATH_RE = re.compile(r"tests/[A-Za-z0-9_./-]+\.py") def _workflow(name: str) -> dict: path = WORKFLOW_DIR / name assert path.is_file(), f"missing workflow: {path}" return yaml.safe_load(path.read_text(encoding="utf-8")) def _trigger_keys(data: dict) -> set[str]: triggers = data.get("on", {}) if triggers is None: return set() if isinstance(triggers, str): return {triggers} return set(triggers) def _workflow_texts() -> list[str]: return [path.read_text(encoding="utf-8") for path in WORKFLOW_DIR.glob("*.yml")] def _is_windows_wsl_bash(path: str) -> bool: normalized = path.replace("\\", "/").lower() return normalized.endswith("/windows/system32/bash.exe") def _bash_executable( *, os_name: str = os.name, path_lookup: Callable[[str], str | None] = shutil.which, exists: Callable[[Path], bool] = Path.is_file, program_files: str | None = None, ) -> str: found = path_lookup("bash") if os_name != "nt": return found or "bash" candidates: list[Path] = [] if found and not _is_windows_wsl_bash(found): candidates.append(Path(found)) git_root = Path(program_files or os.environ.get("ProgramFiles", r"C:\Program Files")) / "Git" candidates.extend( [ git_root / "bin" / "bash.exe", git_root / "usr" / "bin" / "bash.exe", ] ) for candidate in candidates: if exists(candidate): return str(candidate) raise AssertionError("Git Bash is required to run the CI change classifier on Windows") def _classify_changed_files( tmp_path: Path, paths: list[str], *, line_ending: str = "\n", ) -> dict[str, str]: changed_file = tmp_path / "changed-files.txt" output_file = tmp_path / "github-output.txt" changed_file.write_text( line_ending.join(paths) + line_ending, encoding="utf-8", newline="", ) env = os.environ.copy() env["GITHUB_OUTPUT"] = output_file.as_posix() subprocess.run( [_bash_executable(), CLASSIFIER.as_posix(), changed_file.as_posix()], check=True, env=env, text=True, capture_output=True, ) outputs: dict[str, str] = {} for line in output_file.read_text(encoding="utf-8").splitlines(): key, value = line.split("=", 1) outputs[key] = value return outputs def _expected_classifier_outputs(**overrides: str) -> dict[str, str]: outputs = { "docs_only": "false", "runtime_changed": "false", "test_changed": "false", "ci_changed": "false", "dependency_changed": "false", "release_changed": "false", "windows_full_required": "false", "frontend_changed": "false", "tui_changed": "false", "desktop_changed": "false", "python_changed": "false", "platform_sensitive_changed": "false", "build_wheel_required": "false", "full_required": "false", } outputs.update(overrides) return outputs def _validate_pr_target( tmp_path: Path, *, base: str, head: str = "feature/example", title: str = "Example change", labels: list[str] | None = None, changed_files: list[str] | None = None, ) -> subprocess.CompletedProcess[str]: event_path = tmp_path / "event.json" changed_files_path = tmp_path / "changed-files.txt" if changed_files is not None: changed_files_path.write_text("\n".join(changed_files) + "\n", encoding="utf-8") event_path.write_text( json.dumps( { "pull_request": { "base": {"ref": base}, "head": {"ref": head}, "labels": [{"name": label} for label in labels or []], "title": title, }, } ), encoding="utf-8", ) env = os.environ.copy() env.update( { "GITHUB_EVENT_PATH": event_path.as_posix(), "PR_BASE_REF": base, "PR_HEAD_REF": head, "PR_LABELS": ",".join(labels or []), "PR_TITLE": title, } ) if changed_files is not None: env["PR_CHANGED_FILES_PATH"] = changed_files_path.as_posix() return subprocess.run( [_bash_executable(), PR_TARGET_VALIDATOR.as_posix()], env=env, text=True, capture_output=True, check=False, ) def test_default_ci_blocks_pull_requests_and_main_pushes() -> None: ci_path = WORKFLOW_DIR / "ci.yml" if not ci_path.exists(): return data = _workflow("ci.yml") text = ci_path.read_text(encoding="utf-8") assert {"pull_request", "push", "workflow_dispatch"} <= _trigger_keys(data) assert "branches: [main]" in text assert "PYTHONPATH: ${{ github.workspace }}" in text assert "Configure runtime directories" in text assert 'OPENSQUILLA_STATE_DIR=%s/opensquilla-state\\n' in text assert 'OPENSQUILLA_LOG_DIR=%s/opensquilla-logs\\n' in text assert "OPENSQUILLA_TURN_CALL_LOG: \"0\"" in text assert "actionlint@v1.7.12" in text assert "Classify changed files" in text assert "OpenTUI package tests" in text assert "Lint, test, and build (ubuntu-latest, 3.12)" in text assert "Windows compatibility smoke (3.12)" in text assert "Windows high-risk" in text assert "Release packaging contracts" in text assert "CI result" in text assert 'push)\n before="${{ github.event.before }}"' in text assert 'git diff --name-only "${before}" "${after}" > "${changed_files}"' in text assert 'printf \'.ci/run-all\\n\' > "${changed_files}"' in text assert "runtime_changed" in text assert "test_changed" in text assert "ci_changed" in text assert "dependency_changed" in text assert "release_changed" in text assert "windows_full_required" in text assert "frontend_changed" in text assert "tui_changed" in text assert "desktop_changed" in text assert "python_changed" in text assert "platform_sensitive_changed" in text assert "build_wheel_required" in text assert "full_required" in text assert ".github/scripts/check_ci_results.py" in text assert "code_changed" not in text assert "workflow_changed" not in text def test_default_ci_keeps_main_pushes_targeted_and_manual_runs_full() -> None: ci_path = WORKFLOW_DIR / "ci.yml" if not ci_path.exists(): return text = ci_path.read_text(encoding="utf-8") assert 'before="${{ github.event.before }}"' in text assert 'after="${{ github.event.after }}"' in text assert 'git diff --name-only "${before}" "${after}" > "${changed_files}"' in text assert 'workflow_dispatch' in text assert 'printf \'.ci/run-all\\n\' > "${changed_files}"' in text def test_ci_verifies_committed_frontend_dist_is_fresh() -> None: # The gateway serves the COMMITTED dist, not source, so CI must fail when a # Web UI source change lands without a rebuilt+committed bundle. An # exists-only check would let a stale bundle ship (#413 residue), so the step # must diff the freshly-built dist against the committed one. ci_path = WORKFLOW_DIR / "ci.yml" if not ci_path.exists(): return text = ci_path.read_text(encoding="utf-8") assert "Verify committed dist is fresh" in text assert "git diff --quiet -- src/opensquilla/gateway/static/dist" in text assert "committed Web UI dist is stale" in text def test_desktop_ci_runs_profile_substrate_unit_tests() -> None: data = _workflow("ci.yml") desktop_steps = data["jobs"]["desktop-check"]["steps"] unit_step = next(step for step in desktop_steps if step.get("name") == "Run desktop unit tests") assert "node scripts/test-desktop-profile-substrate.mjs" in unit_step["run"] assert "node scripts/test-desktop-profile-context.mjs" in unit_step["run"] def test_pr_target_validator_allows_main_pull_requests(tmp_path: Path) -> None: result = _validate_pr_target( tmp_path, base="main", changed_files=["src/opensquilla/engine/agent.py"], ) assert result.returncode == 0 assert "Pull request targets main." in result.stdout def test_pr_target_validator_blocks_dev_pull_requests( tmp_path: Path, ) -> None: result = _validate_pr_target(tmp_path, base="dev") assert result.returncode == 1 assert "Ordinary pull requests should target main" in result.stderr def test_pr_target_validator_allows_docs_only_main_pull_requests( tmp_path: Path, ) -> None: result = _validate_pr_target( tmp_path, base="main", head="docs/agent-testing", title="docs: add agent testing framework guide", changed_files=["docs/testing/framework.md"], ) assert result.returncode == 0 assert "Pull request targets main." in result.stdout def test_pr_target_validator_allows_labeled_main_pull_requests_without_exception( tmp_path: Path, ) -> None: labels = [ "allow-main-target", "release", "hotfix", "main-sync", "release-docs", "sync-to-main", "docs-preview", ] for label in labels: result = _validate_pr_target( tmp_path, base="main", head="release/0.3.2", labels=[label], changed_files=["src/opensquilla/engine/agent.py"], ) assert result.returncode == 0 assert "Pull request targets main." in result.stdout def test_pr_target_validator_allows_staging_branch_pull_requests( tmp_path: Path, ) -> None: for base in [ "sandbox-optimization", "integration/sandbox-hardening", "staging/sandbox-hardening", "release/0.3.2", ]: result = _validate_pr_target( tmp_path, base=base, head="pr/sandbox-run-modes-sandbox-optimization", changed_files=["src/opensquilla/sandbox/backend/windows_appcontainer.py"], ) assert result.returncode == 0 assert "staging/collaboration" in result.stdout assert "target main" in result.stdout def test_pr_target_validator_allows_labeled_staging_pull_requests( tmp_path: Path, ) -> None: for label in ["maintainer-staging", "collaboration"]: result = _validate_pr_target( tmp_path, base="sandbox-review", head="feature/shared-sandbox-work", labels=[label], changed_files=["src/opensquilla/sandbox/policy.py"], ) assert result.returncode == 0 assert "staging/collaboration" in result.stdout def test_pr_target_validator_blocks_unknown_target_branches(tmp_path: Path) -> None: result = _validate_pr_target( tmp_path, base="feature/private-target", head="feature/example", changed_files=["src/opensquilla/engine/agent.py"], ) assert result.returncode == 1 assert "Ordinary pull requests should target main" in result.stderr def test_pr_target_validator_handles_missing_event_path() -> None: env = os.environ.copy() env.pop("GITHUB_EVENT_PATH", None) env.pop("PR_LABELS", None) env["PR_BASE_REF"] = "feature/private-target" result = subprocess.run( [_bash_executable(), PR_TARGET_VALIDATOR.as_posix()], env=env, text=True, capture_output=True, check=False, ) assert result.returncode == 1 assert "Ordinary pull requests should target main" in result.stderr assert "Traceback" not in result.stderr def test_pr_target_branch_workflow_runs_trusted_base_validator() -> None: data = _workflow("pr-target-branch.yml") text = (WORKFLOW_DIR / "pr-target-branch.yml").read_text(encoding="utf-8") assert _trigger_keys(data) == {"pull_request"} assert "pull_request_target" not in text assert "Validate target branch" in text assert "github.event.repository.default_branch" in text assert "hashFiles('.github/scripts/validate-pr-target-branch.sh') == ''" in text assert "github.event.pull_request.head.sha" in text assert "pull-requests: read" in text assert "PR_LABELS" in text assert "PR_NUMBER" in text assert ".github/scripts/validate-pr-target-branch.sh" in text def test_pr_body_lint_workflow_warns_from_trusted_base() -> None: data = _workflow("pr-body-lint.yml") text = (WORKFLOW_DIR / "pr-body-lint.yml").read_text(encoding="utf-8") assert _trigger_keys(data) == {"pull_request"} assert "pull_request_target" not in text assert "Validate PR body fields" in text assert "github.event.repository.default_branch" in text assert "hashFiles('.github/scripts/validate_pr_body.py') == ''" in text assert "github.event.pull_request.head.sha" in text assert "pull-requests: read" in text assert PR_BODY_LINT.as_posix() in text assert "PR_BODY_LINT_STRICT: \"0\"" in text def test_issue_link_sync_tracks_open_and_closed_final_prs_from_trusted_base() -> None: data = _workflow("issue-link-sync.yml") text = (WORKFLOW_DIR / "issue-link-sync.yml").read_text(encoding="utf-8") pull_request_target = data["on"]["pull_request_target"] assert set(pull_request_target["types"]) == {"opened", "reopened", "edited", "closed"} assert pull_request_target["branches"] == ["main"] assert "ref: ${{ github.event.pull_request.base.sha }}" in text assert "persist-credentials: false" in text assert "issues: write" in text assert ".github/scripts/issue_link_sync.py" in text def test_ci_change_classifier_allows_root_and_docs_markdown_only(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, [ "README.md", "CHANGELOG.md", "docs/features/skills.md", ".github/pull_request_template.md", ], ) assert outputs == _expected_classifier_outputs(docs_only="true") def test_classifier_helper_prefers_git_bash_over_windows_wsl_bash(tmp_path: Path) -> None: git_bash = tmp_path / "Git" / "bin" / "bash.exe" result = _bash_executable( os_name="nt", path_lookup=lambda _name: r"C:\Windows\System32\bash.exe", exists=lambda path: path == git_bash, program_files=str(tmp_path), ) assert result == str(git_bash) def test_ci_change_classifier_accepts_crlf_changed_files(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, ["README.md", "docs/features/skills.md"], line_ending="\r\n", ) assert outputs["docs_only"] == "true" assert outputs["runtime_changed"] == "false" assert outputs["windows_full_required"] == "false" assert outputs["python_changed"] == "false" assert outputs["full_required"] == "false" def test_ci_change_classifier_treats_runtime_markdown_as_runtime(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, ["src/opensquilla/identity/templates/bootstrap/AGENTS.md"], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", ) def test_ci_change_classifier_fails_closed_for_unclassified_tests(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, ["tests/test_ci/test_workflows.py"], ) assert outputs == _expected_classifier_outputs( test_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", ) def test_ci_change_classifier_keeps_webui_only_changes_off_windows_full( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, ["opensquilla-webui/src/views/ChatView.vue"], ) assert outputs == _expected_classifier_outputs(frontend_changed="true") def test_ci_change_classifier_tracks_ci_dependency_and_release_changes(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, [".github/workflows/ci.yml", ".github/scripts/classify-ci-changes.sh", "uv.lock"], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", test_changed="true", ci_changed="true", dependency_changed="true", release_changed="true", windows_full_required="true", frontend_changed="true", tui_changed="true", desktop_changed="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", full_required="true", ) def test_ci_change_classifier_tracks_release_surface_changes(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, [ ".github/workflows/wheelhouse-release.yml", "scripts/build_wheelhouse_zip.py", "README.release.md", "RELEASES.md", "tests/test_scripts/test_build_wheelhouse_zip.py", ], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", test_changed="true", ci_changed="true", release_changed="true", windows_full_required="true", python_changed="true", build_wheel_required="true", ) def test_ci_change_classifier_tracks_tui_changes_without_windows_full(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, ["src/opensquilla/cli/tui/opentui/package/src/composer.mjs"], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", tui_changed="true", python_changed="true", build_wheel_required="true", ) def test_ci_change_classifier_fails_closed_for_unclassified_runtime_paths( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, ["src/opensquilla/future_profile_store/transaction.py"], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", ) def test_ci_change_classifier_fails_closed_for_unknown_root_paths(tmp_path: Path) -> None: outputs = _classify_changed_files(tmp_path, ["future-runtime-policy.json"]) assert outputs == _expected_classifier_outputs( runtime_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", ) def test_ci_change_classifier_covers_state_and_installation_boundaries( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, [ "src/opensquilla/session/manager.py", "src/opensquilla/scheduler/persistence.py", "src/opensquilla/memory/store.py", "src/opensquilla/uninstall/actions.py", "tests/test_recovery/test_new_contract.py", "tests/test_uninstall/test_actions.py", ], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", test_changed="true", release_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", ) def test_ci_change_classifier_tracks_platform_sensitive_changes(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, ["tests/test_tools/test_shell_process_isolation.py"], ) assert outputs == _expected_classifier_outputs( test_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", ) def test_ci_change_classifier_runs_windows_full_for_native_source_snapshot( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, ["tests/test_migration/test_source_snapshot_windows.py"], ) assert outputs == _expected_classifier_outputs( test_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", ) def test_ci_change_classifier_runs_windows_full_for_native_source_snapshot_implementation( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, ["src/opensquilla/migration/source_snapshot_windows.py"], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", ) def test_ci_change_classifier_runs_full_for_its_own_windows_gate(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, [".github/workflows/ci.yml"], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", test_changed="true", ci_changed="true", dependency_changed="true", release_changed="true", windows_full_required="true", frontend_changed="true", tui_changed="true", desktop_changed="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", full_required="true", ) def test_ci_change_classifier_fails_closed_for_future_ci_surfaces(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, [ ".github/workflows/future-profile-safety.yml", ".github/scripts/future_profile_gate.py", ], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", test_changed="true", ci_changed="true", dependency_changed="true", release_changed="true", windows_full_required="true", frontend_changed="true", tui_changed="true", desktop_changed="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", full_required="true", ) def test_ci_change_classifier_runs_windows_release_gates_for_profile_verifier( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, [".github/scripts/verify-release-profile-preservation.py"], ) assert outputs == _expected_classifier_outputs( ci_changed="true", release_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", ) def test_ci_change_classifier_tracks_packaged_update_policy_probe_as_release_surface( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, ["desktop/electron/scripts/test-packaged-update-policy.mjs"], ) assert outputs == _expected_classifier_outputs( release_changed="true", windows_full_required="true", desktop_changed="true", platform_sensitive_changed="true", ) def test_ci_change_classifier_runs_windows_full_for_persistence_risk( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, [ "src/opensquilla/persistence/migrator.py", "tests/test_persistence/test_migrator.py", "migrations/V999__example.py", ], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", test_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", ) def test_ci_change_classifier_runs_windows_full_for_provider_onboarding_risk( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, [ "src/opensquilla/provider/registry.py", "src/opensquilla/onboarding/provider_specs.py", "tests/test_onboarding/test_mutations.py", "tests/test_provider/test_spec_substrate.py", ], ) assert outputs == _expected_classifier_outputs( runtime_changed="true", test_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", ) def test_ci_change_classifier_runs_windows_full_for_gateway_functional_e2e( tmp_path: Path, ) -> None: outputs = _classify_changed_files( tmp_path, [ "tests/functional/test_gateway_non_image_attachment_materialization_e2e.py", "tests/functional/test_gateway_attachment_history_e2e.py", ], ) assert outputs == _expected_classifier_outputs( test_changed="true", windows_full_required="true", python_changed="true", platform_sensitive_changed="true", ) def test_ci_change_classifier_tracks_desktop_changes(tmp_path: Path) -> None: outputs = _classify_changed_files( tmp_path, ["desktop/electron/src/main.ts"], ) # A desktop change gates the desktop-check Node tests and, as a platform- # sensitive surface, the Windows full suite — but not the Python quality gate. assert outputs == _expected_classifier_outputs( desktop_changed="true", platform_sensitive_changed="true", windows_full_required="true", ) def test_ci_change_classifier_run_all_requires_full_ci(tmp_path: Path) -> None: outputs = _classify_changed_files(tmp_path, [".ci/run-all"]) assert outputs == _expected_classifier_outputs( runtime_changed="true", test_changed="true", ci_changed="true", dependency_changed="true", release_changed="true", windows_full_required="true", frontend_changed="true", tui_changed="true", desktop_changed="true", python_changed="true", platform_sensitive_changed="true", build_wheel_required="true", full_required="true", ) def test_default_ci_uses_layered_job_conditions() -> None: data = _workflow("ci.yml") jobs = data["jobs"] assert "tui-check" in jobs assert "frontend_changed == 'true'" in jobs["frontend-check"]["if"] assert "full_required == 'true'" in jobs["frontend-check"]["if"] assert "tui_changed == 'true'" in jobs["tui-check"]["if"] assert "desktop_changed == 'true'" in jobs["desktop-check"]["if"] assert "python_changed == 'true'" in jobs["ubuntu-quality"]["if"] assert "platform_sensitive_changed == 'true'" in jobs["windows-compat"]["if"] assert "windows_full_required == 'true'" in jobs["windows-full"]["if"] assert "platform_sensitive_changed == 'true'" in jobs["macos-recovery"]["if"] assert "desktop_changed == 'true'" in jobs["macos-recovery"]["if"] assert "platform_sensitive_changed == 'true'" in jobs["desktop-recovery-e2e"]["if"] assert "desktop_changed == 'true'" in jobs["desktop-recovery-e2e"]["if"] assert "release_changed == 'true'" in jobs["release-packaging"]["if"] assert "tui-check" in jobs["ci-result"]["needs"] assert "desktop-check" in jobs["ci-result"]["needs"] assert "macos-recovery" in jobs["ci-result"]["needs"] assert "desktop-recovery-e2e" in jobs["ci-result"]["needs"] def test_ci_result_gate_covers_every_conditional_job_and_classifier_flag() -> None: jobs = _workflow("ci.yml")["jobs"] gate = jobs["ci-result"] gate_step = next( step for step in gate["steps"] if step.get("name") == "Check required CI results" ) assert gate["name"] == "CI result" setup_python = next(step for step in gate["steps"] if step.get("name") == "Set up Python") assert setup_python["with"]["python-version"] == "3.12" assert set(gate["needs"]) == { "classify-changes", "workflow-lint", "readme-locale-check", "frontend-check", "tui-check", "desktop-check", "ubuntu-quality", "windows-compat", "windows-full", "macos-recovery", "desktop-recovery-e2e", "release-packaging", } assert gate_step["run"] == "python .github/scripts/check_ci_results.py" assert gate_step["env"]["RESULT_MACOS_RECOVERY"] == ( "${{ needs.macos-recovery.result }}" ) assert gate_step["env"]["RESULT_DESKTOP_RECOVERY_E2E"] == ( "${{ needs.desktop-recovery-e2e.result }}" ) assert set(key for key in gate_step["env"] if key.startswith("FLAG_")) == { "FLAG_DOCS_ONLY", "FLAG_RUNTIME_CHANGED", "FLAG_TEST_CHANGED", "FLAG_CI_CHANGED", "FLAG_DEPENDENCY_CHANGED", "FLAG_RELEASE_CHANGED", "FLAG_WINDOWS_FULL_REQUIRED", "FLAG_FRONTEND_CHANGED", "FLAG_TUI_CHANGED", "FLAG_DESKTOP_CHANGED", "FLAG_PYTHON_CHANGED", "FLAG_PLATFORM_SENSITIVE_CHANGED", "FLAG_BUILD_WHEEL_REQUIRED", "FLAG_FULL_REQUIRED", } def test_desktop_recovery_e2e_runs_compiled_flows_on_all_release_platforms() -> None: job = _workflow("ci.yml")["jobs"]["desktop-recovery-e2e"] assert job["strategy"]["fail-fast"] is False assert job["strategy"]["matrix"]["os"] == [ "ubuntu-latest", "macos-latest", "windows-latest", ] build = next(step for step in job["steps"] if step.get("name") == "Build Desktop TypeScript") run = next( step for step in job["steps"] if step.get("name") == "Run compiled Desktop recovery flows" ) upload = next( step for step in job["steps"] if step.get("name") == "Upload Desktop recovery report" ) assert build["run"] == "npm run build" assert "xvfb-run -a node" in run["run"] assert "test-profile-recovery-flow.mjs" in run["run"] assert "test-profile-recovery-accessibility.mjs" in run["run"] assert "test-profile-import-flow.mjs" in run["run"] assert "test-unsafe-profile-no-write.mjs" in run["run"] assert "exit 1" in run["run"] assert upload["if"] == "${{ always() }}" assert "github.run_attempt" in upload["with"]["name"] def test_windows_smoke_does_not_install_bun_by_default() -> None: data = _workflow("ci.yml") jobs = data["jobs"] windows_steps = jobs["windows-compat"]["steps"] assert all(step.get("uses") != "oven-sh/setup-bun@v2" for step in windows_steps) assert all("OpenTUI" not in step.get("name", "") for step in windows_steps) assert "lfs" not in windows_steps[0].get("with", {}) tui_steps = jobs["tui-check"]["steps"] assert any(step.get("uses") == "oven-sh/setup-bun@v2" for step in tui_steps) assert any("bun run test:bun" in step.get("run", "") for step in tui_steps) def test_windows_high_risk_job_runs_parallel_reported_shards() -> None: data = _workflow("ci.yml") jobs = data["jobs"] windows_full = jobs["windows-full"] steps = windows_full["steps"] test_step = next(step for step in steps if step.get("name") == "Test Windows shard") upload_step = next( step for step in steps if step.get("name") == "Upload Windows shard report" ) assert windows_full["name"] == "Windows high-risk (${{ matrix.shard }})" assert windows_full["timeout-minutes"] == 45 assert windows_full["strategy"] == { "fail-fast": False, "matrix": { "shard": [ "core", "gateway-sqlite", "recovery-migration", "desktop-installer-contracts", ] }, } checkout = next(step for step in steps if step.get("name") == "Check out repository") assert checkout["with"]["lfs"] == "${{ matrix.shard == 'core' }}" bun_step = next(step for step in steps if step.get("name") == "Set up Bun") assert bun_step["if"] == "${{ matrix.shard == 'core' }}" assert steps[0]["name"] == "Prepare diagnostic report" assert "OPENSQUILLA_STATE_DIR" not in steps[0]["run"] assert "PATH" not in steps[0]["run"] assert "HOME" not in steps[0]["run"] assert ".github/scripts/windows_test_shards.py run" in test_step["run"] assert '"${{ github.event_name }}" == "pull_request"' in test_step["run"] assert "--maxfail=3" in test_step["run"] assert "--maxfail=1" not in test_step["run"] assert "set -euo pipefail" in test_step["run"] assert 'tee "${CI_REPORT_DIR}/pytest.log"' in test_step["run"] assert upload_step["if"] == "${{ always() }}" assert upload_step["uses"] == "actions/upload-artifact@v4" assert upload_step["with"]["if-no-files-found"] == "error" assert upload_step["with"]["retention-days"] == 14 def test_recovery_windows_shard_uses_and_always_cleans_distinct_real_volumes() -> None: windows_full = _workflow("ci.yml")["jobs"]["windows-full"] steps = windows_full["steps"] provision_index = next( index for index, step in enumerate(steps) if step.get("name") == "Provision distinct Windows test volumes" ) test_index = next( index for index, step in enumerate(steps) if step.get("name") == "Test Windows shard" ) cleanup_index = next( index for index, step in enumerate(steps) if step.get("name") == "Clean up Windows test volumes" ) provision = steps[provision_index] cleanup = steps[cleanup_index] provision_script = provision["run"] cleanup_script = cleanup["run"] assert provision_index < test_index < cleanup_index assert provision["if"] == "${{ matrix.shard == 'recovery-migration' }}" assert provision["shell"] == "pwsh" assert "$env:RUNNER_TEMP" in provision_script assert "$volumeB = Join-Path -Path $env:LOCALAPPDATA" in provision_script assert "$env:SystemDrive" in provision_script assert "[guid]::NewGuid()" in provision_script assert "[System.IO.Path]::GetPathRoot($volumeA)" in provision_script assert "[System.IO.Path]::GetPathRoot($volumeB)" in provision_script assert "throw \"Windows test volume roots must use different drives\"" in provision_script assert "OPENSQUILLA_WINDOWS_TEST_VOLUME_A=$volumeA" in provision_script assert "OPENSQUILLA_WINDOWS_TEST_VOLUME_B=$volumeB" in provision_script assert cleanup["if"] == "${{ always() && matrix.shard == 'recovery-migration' }}" assert cleanup["shell"] == "pwsh" assert "$env:OPENSQUILLA_WINDOWS_TEST_VOLUME_A" in cleanup_script assert "$env:OPENSQUILLA_WINDOWS_TEST_VOLUME_B" in cleanup_script assert "Remove-Item -LiteralPath $testRoot -Recurse -Force" in cleanup_script def test_windows_high_risk_job_cannot_wash_test_failures_green() -> None: windows_full = _workflow("ci.yml")["jobs"]["windows-full"] test_step = next( step for step in windows_full["steps"] if step.get("name") == "Test Windows shard" ) serialized = json.dumps(windows_full, sort_keys=True) assert windows_full["strategy"]["fail-fast"] is False assert all("continue-on-error" not in step for step in windows_full["steps"]) assert "--reruns" not in serialized assert "pytest-rerunfailures" not in serialized assert "continue-on-error" not in serialized assert "|| true" not in test_step["run"] assert "set -euo pipefail" in test_step["run"] assert "github.run_attempt" in serialized def test_macos_recovery_runs_native_contracts_and_cannot_wash_failures_green() -> None: job = _workflow("ci.yml")["jobs"]["macos-recovery"] test_step = next( step for step in job["steps"] if step.get("name") == "Test native profile recovery contracts" ) upload_step = next( step for step in job["steps"] if step.get("name") == "Upload macOS recovery report" ) serialized = json.dumps(job, sort_keys=True) assert job["name"] == "macOS profile recovery and native no-replace (3.12)" assert job["runs-on"] == "macos-latest" assert job["timeout-minutes"] == 30 assert "tests/test_recovery" in test_step["run"] assert "tests/test_migration/test_opensquilla_home_migration.py" in test_step["run"] assert "tests/test_desktop/test_electron_startup_contract.py" in test_step["run"] assert "set -euo pipefail" in test_step["run"] assert "--maxfail=3" in test_step["run"] assert '--junitxml="${CI_REPORT_DIR}/junit.xml"' in test_step["run"] assert 'tee "${CI_REPORT_DIR}/pytest.log"' in test_step["run"] assert "status=${PIPESTATUS[0]}" in test_step["run"] assert 'exit "${status}"' in test_step["run"] assert upload_step["if"] == "${{ always() }}" assert upload_step["with"]["if-no-files-found"] == "error" assert "github.run_attempt" in upload_step["with"]["name"] assert "continue-on-error" not in serialized assert "--reruns" not in serialized assert "pytest-rerunfailures" not in serialized assert "|| true" not in test_step["run"] def test_ubuntu_quality_only_fetches_lfs_for_full_ci() -> None: data = _workflow("ci.yml") ubuntu_steps = data["jobs"]["ubuntu-quality"]["steps"] checkout = ubuntu_steps[0] test_step = next(step for step in ubuntu_steps if step.get("name") == "Test") assert checkout["uses"] == "actions/checkout@v4" assert checkout["with"]["lfs"] == ( "${{ needs.classify-changes.outputs.full_required == 'true' }}" ) assert "uv run pytest tests -q" in test_step["run"] assert "tests/test_artifacts.py" not in test_step["run"] assert "--ignore=tests/test_ci/test_router_artifact_manifest.py" in test_step["run"] assert "tests/test_recovery" in test_step["run"] assert "tests/test_migration/test_opensquilla_home_migration.py" in test_step["run"] def test_manual_workflows_reference_existing_test_files() -> None: for text in _workflow_texts(): for raw_path in TEST_PATH_RE.findall(text): assert Path(raw_path).is_file(), f"workflow references missing test: {raw_path}" def test_webui_browser_workflow_is_manual_and_opt_in() -> None: data = _workflow("webui-browser-smoke.yml") text = (WORKFLOW_DIR / "webui-browser-smoke.yml").read_text(encoding="utf-8") assert _trigger_keys(data) == {"workflow_dispatch"} assert 'OPENSQUILLA_WEBUI_BROWSER_E2E: "1"' in text assert "tests/functional/test_webui_browser_e2e.py" in text assert "playwright install chromium" in text def test_llm_workflow_is_single_manual_smoke() -> None: data = _workflow("llm-e2e.yml") text = (WORKFLOW_DIR / "llm-e2e.yml").read_text(encoding="utf-8") assert _trigger_keys(data) == {"workflow_dispatch"} assert "OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}" in text assert "tests/functional/test_llm_smoke.py" in text assert "llm_costly" not in text assert "tests/functional/test_webui_llm_e2e.py" not in text def test_live_release_e2e_workflow_is_manual_and_separates_private_inputs() -> None: data = _workflow("live-release-e2e.yml") text = (WORKFLOW_DIR / "live-release-e2e.yml").read_text(encoding="utf-8") assert _trigger_keys(data) == {"workflow_dispatch"} assert "tests/functional/test_gateway_llm_e2e.py" in text assert "tests/functional/test_webui_browser_chat_e2e.py" in text assert "tests/functional/test_live_channel_telegram_smoke.py" in text assert "OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}" in text assert ( "OPENSQUILLA_LIVE_TELEGRAM_BOT_TOKEN: " "${{ secrets.OPENSQUILLA_LIVE_TELEGRAM_BOT_TOKEN }}" ) in text assert ( "OPENSQUILLA_LIVE_TELEGRAM_CHAT_ID: " "${{ secrets.OPENSQUILLA_LIVE_TELEGRAM_CHAT_ID }}" ) in text assert "tests/private" not in text def test_default_ci_stays_offline_and_does_not_run_live_gates() -> None: text = (WORKFLOW_DIR / "ci.yml").read_text(encoding="utf-8") assert "OPENROUTER_API_KEY" not in text assert "OPENSQUILLA_LIVE_TELEGRAM" not in text assert "OPENSQUILLA_GATEWAY_LLM_E2E" not in text assert "OPENSQUILLA_WEBUI_BROWSER_E2E" not in text assert "OPENSQUILLA_WEBUI_BROWSER_CHAT_E2E" not in text assert "test_gateway_llm_e2e.py" not in text assert "test_live_channel_telegram_smoke.py" not in text def test_live_release_e2e_fails_fast_when_required_provider_secret_is_missing() -> None: text = (WORKFLOW_DIR / "live-release-e2e.yml").read_text(encoding="utf-8") assert "Fail if OpenRouter secret is missing" in text assert 'if [ -z "$OPENROUTER_API_KEY" ]; then' in text assert "OPENROUTER_API_KEY GitHub secret is required" in text assert "Fail if Telegram secrets are missing when channel smoke is enabled" in text assert 'if [ -z "$OPENSQUILLA_LIVE_TELEGRAM_BOT_TOKEN" ]' in text assert 'if [ -z "$OPENSQUILLA_LIVE_TELEGRAM_CHAT_ID" ]' in text def test_wheelhouse_release_publishes_only_recommended_router_profile() -> None: text = (WORKFLOW_DIR / "wheelhouse-release.yml").read_text(encoding="utf-8") assert " profile:\n" not in text assert "RELEASE_PROFILE: recommended" in text assert "opensquilla-release-assets-python-${{ env.RELEASE_PROFILE }}" in text assert "opensquilla-release-assets-${{ env.RELEASE_PROFILE }}" in text assert "--profile \"${RELEASE_PROFILE}\"" not in text assert "- core" not in text def test_wheelhouse_release_hydrates_current_router_bundle() -> None: text = (WORKFLOW_DIR / "wheelhouse-release.yml").read_text(encoding="utf-8") assert "models/v4.2_phase3_inference" in text assert 'root / "bge_onnx" / "model.onnx"' in text assert 'root / "features" / "tfidf.pkl"' in text assert 'root / "lgbm_main.bin"' in text assert 'root / "mlp" / "model.onnx"' in text assert 'root / "router.runtime.yaml"' in text assert "intent_head.joblib" not in text assert "router_model.onnx" not in text