1209 lines
42 KiB
Python
1209 lines
42 KiB
Python
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
|