light-heart-labs--dreamserver
9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
148 行
6.1 KiB
Python
148 行
6.1 KiB
Python
"""Tests for the __ODS_RESULT__ sentinel emission on /api/setup/test.
|
|
|
|
The frontend SetupWizard parser treats the sentinel as the source of truth
|
|
for diagnostic success/failure. Absence falls back to scraping log lines
|
|
for "All tests passed!", which is fragile, so the contract is that every
|
|
terminal state of the streaming endpoint MUST yield exactly one sentinel
|
|
line as its last line.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import stat
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
SENTINEL_PREFIX = "__ODS_RESULT__:"
|
|
|
|
|
|
def _last_sentinel_line(lines):
|
|
"""Return the parsed (status, rc) tuple for the last sentinel found.
|
|
|
|
Yields a tuple even when several sentinel lines exist in the stream
|
|
so we can assert the final terminator wins. ``rc`` is returned as a
|
|
string to keep the parse symmetric with the wire format.
|
|
"""
|
|
sentinel = None
|
|
for line in lines:
|
|
if line.startswith(SENTINEL_PREFIX):
|
|
sentinel = line
|
|
assert sentinel is not None, f"no __ODS_RESULT__ line found in stream: {lines!r}"
|
|
payload = sentinel[len(SENTINEL_PREFIX):]
|
|
status, _, rc = payload.partition(":")
|
|
return status, rc
|
|
|
|
|
|
def _write_test_script(install_root: Path, body: str) -> Path:
|
|
"""Write a bash script to install_root/scripts/ods-test-functional.sh."""
|
|
scripts = install_root / "scripts"
|
|
scripts.mkdir(parents=True, exist_ok=True)
|
|
path = scripts / "ods-test-functional.sh"
|
|
path.write_text(body, encoding="utf-8")
|
|
path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
return path
|
|
|
|
|
|
@pytest.fixture()
|
|
def setup_install_dir(tmp_path, monkeypatch):
|
|
"""Provide an isolated INSTALL_DIR for the setup router.
|
|
|
|
The setup endpoint resolves the diagnostic script via
|
|
``Path(INSTALL_DIR) / "scripts" / "ods-test-functional.sh"``; if it's
|
|
not there it falls back to ``Path(os.getcwd()) / "ods-test-functional.sh"``.
|
|
We point INSTALL_DIR at a tmp dir and chdir to another tmp dir so the
|
|
cwd fallback is also empty unless the test opts in.
|
|
"""
|
|
install_root = tmp_path / "ods"
|
|
install_root.mkdir()
|
|
cwd_root = tmp_path / "cwd"
|
|
cwd_root.mkdir()
|
|
|
|
monkeypatch.setattr("routers.setup.INSTALL_DIR", str(install_root))
|
|
monkeypatch.chdir(cwd_root)
|
|
return install_root
|
|
|
|
|
|
def test_setup_test_emits_pass_sentinel_on_success(test_client, setup_install_dir):
|
|
"""A diagnostic script that exits 0 must terminate with PASS:0."""
|
|
_write_test_script(setup_install_dir, "#!/bin/bash\necho 'check 1 ok'\necho 'check 2 ok'\nexit 0\n")
|
|
|
|
with test_client.stream("POST", "/api/setup/test", headers=test_client.auth_headers) as response:
|
|
assert response.status_code == 200
|
|
lines = [line for line in response.iter_lines() if line]
|
|
|
|
status, rc = _last_sentinel_line(lines)
|
|
assert status == "PASS"
|
|
assert rc == "0"
|
|
# Sentinel must be the LAST non-empty line on the wire.
|
|
assert lines[-1].startswith(SENTINEL_PREFIX), (
|
|
f"sentinel was emitted but not last; tail was: {lines[-3:]!r}"
|
|
)
|
|
|
|
|
|
def test_setup_test_emits_fail_sentinel_with_returncode_on_failure(test_client, setup_install_dir):
|
|
"""A diagnostic script that exits non-zero must terminate with FAIL:<rc>."""
|
|
_write_test_script(setup_install_dir, "#!/bin/bash\necho 'check failed'\nexit 3\n")
|
|
|
|
with test_client.stream("POST", "/api/setup/test", headers=test_client.auth_headers) as response:
|
|
assert response.status_code == 200
|
|
lines = [line for line in response.iter_lines() if line]
|
|
|
|
status, rc = _last_sentinel_line(lines)
|
|
assert status == "FAIL"
|
|
assert rc == "3", f"expected the script's literal exit code, got {rc!r}"
|
|
assert lines[-1].startswith(SENTINEL_PREFIX)
|
|
|
|
|
|
def test_setup_test_emits_sentinel_when_script_missing(test_client, setup_install_dir):
|
|
"""When neither INSTALL_DIR/scripts nor cwd has the script, the
|
|
error_stream() fallback runs aiohttp probes against configured
|
|
services and still terminates with a sentinel."""
|
|
# No script written — both lookup paths miss; error_stream() runs.
|
|
# In the test environment the dashboard SERVICES map is populated but
|
|
# nothing is actually listening, so every probe will fail and the
|
|
# sentinel should be FAIL:1.
|
|
|
|
with test_client.stream("POST", "/api/setup/test", headers=test_client.auth_headers) as response:
|
|
assert response.status_code == 200
|
|
lines = [line for line in response.iter_lines() if line]
|
|
|
|
status, rc = _last_sentinel_line(lines)
|
|
assert status in {"PASS", "FAIL"}, f"unknown sentinel status {status!r}"
|
|
# The FAIL path is the realistic outcome — services aren't running in
|
|
# the test harness — but assert only the structural contract here so
|
|
# this test stays robust against fixture changes.
|
|
assert rc.lstrip("-").isdigit(), f"sentinel rc must be numeric, got {rc!r}"
|
|
assert lines[-1].startswith(SENTINEL_PREFIX)
|
|
|
|
|
|
def test_setup_test_sentinel_format_is_machine_parseable(test_client, setup_install_dir):
|
|
"""The on-the-wire format must match the regex the SetupWizard frontend
|
|
pins: ``^__ODS_RESULT__:(PASS|FAIL):(-?\\d+)$``. This test guards
|
|
against accidental whitespace, prefix, or trailing-character drift on
|
|
either side of the contract."""
|
|
import re
|
|
|
|
_write_test_script(setup_install_dir, "#!/bin/bash\nexit 0\n")
|
|
sentinel_re = re.compile(r"^__ODS_RESULT__:(PASS|FAIL):(-?\d+)$")
|
|
|
|
with test_client.stream("POST", "/api/setup/test", headers=test_client.auth_headers) as response:
|
|
assert response.status_code == 200
|
|
lines = [line for line in response.iter_lines() if line]
|
|
|
|
sentinel_lines = [line for line in lines if line.startswith(SENTINEL_PREFIX)]
|
|
assert len(sentinel_lines) == 1, (
|
|
f"expected exactly one sentinel line, got {len(sentinel_lines)}: {sentinel_lines!r}"
|
|
)
|
|
assert sentinel_re.match(sentinel_lines[0]), (
|
|
f"sentinel does not match frontend parser regex: {sentinel_lines[0]!r}"
|
|
)
|
|
|
|
|
|
def test_setup_test_requires_auth(test_client):
|
|
"""Unauthenticated POST must fail before anything is streamed."""
|
|
response = test_client.post("/api/setup/test")
|
|
assert response.status_code == 401
|