fix(server): unblock migrations + fix upstream version check - #2797
Merged
Conversation
django-dbbackup v5.0 (pinned in #2768 alongside the Django 5 LTS upgrade) raises RuntimeError on import if the legacy DBBACKUP_STORAGE / DBBACKUP_STORAGE_OPTIONS settings are present: File ".../dbbackup/settings.py", line 13, in <module> raise RuntimeError(msg) RuntimeError: The settings DBBACKUP_STORAGE and DBBACKUP_STORAGE_OPTIONS have been deprecated in favor of using Django Storages configuration. This blocks every manage.py invocation (most visibly migrate during container start), since the dbbackup management commands import the package at registration time. Move the backup storage config into Django 5's STORAGES dict under the 'dbbackup' key. Defining STORAGES replaces the framework defaults entirely, so 'default' and 'staticfiles' are restated even though they match the global default — without them, file uploads and WhiteNoise-served staticfiles would lose their backends. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
remote_branch_available() listed all branches via GET /branches and
scanned for a name match, but that endpoint paginates at 30 results
and returns them alphabetically. Once auto-generated branches
(claude/*, dependabot/*, renovate/*) pushed past 30, master fell off
the first page, the listing scan returned False, and every
fetch_remote_hash() call logged "Remote Git branch not available"
followed by is_up_to_date()'s "Unable to get latest version from
GitHub". The Anthias repo currently has 81 branches.
Switch to the per-branch endpoint
GET /repos/screenly/anthias/branches/{branch}, which returns 200 if
the branch exists and 404 if not, with no pagination. 404 is treated
as a clean negative ("missing") and cached without triggering the
generic error backoff. Other non-200 responses still go through
handle_github_error().
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
4 tasks
vpetersson
added a commit
that referenced
this pull request
May 1, 2026
#2798) * feat(telemetry): move GA emission to Celery beat with refreshed schema The single GA "version" event used to be emitted inline from is_up_to_date() — which is on the synchronous request path for the home-page banner and the v2 info endpoint — and was gated behind the cache-miss branch of fetch_remote_hash(). Combined with the branches-pagination bug fixed in #2797 it had been dead in practice. Even when it worked, it was prone to deadlocking request workers (no timeout) and crashing them on non-ConnectionError exceptions (narrow except). Move the call to a Celery beat task that fires hourly. The task delegates to lib.telemetry.send_telemetry(), which guards itself with a 24h cooldown stored in Redis (the persisted volume, not the tmpfs celerybeat schedule) so devices that reboot frequently still emit at most one event per day. The HTTP POST gets timeout=5 and a broad RequestException catch so a sluggish GA can never drag down worker latency. Refresh the payload to match Anthias's current device matrix and GA4 conventions: branch, commit_short — version identity device_type — pi4-64 / pi5 / x86 (was missing) hardware_model — /proc/cpuinfo model (was Pi_Version) is_balena, is_docker — runtime flavour os_release — /etc/os-release PRETTY_NAME resolution, audio_output — viewer config that informs hardware/perf decisions tls_enabled — HTTPS adoption signal asset_count, asset_<type>_count — playlist usage breakdown Drops the legacy NOOBS bool — NOOBS was retired upstream years ago and the field has been useless. Drops Pi_Version in favour of the neutral hardware_model + device_type pair so x86 readouts make sense. Lib import note: lib/telemetry pulls in anthias_app.models.Asset for the count query, matching the existing celery_tasks.py pattern (this is a server-side helper, never imported by the viewer). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(telemetry): annotate _build_payload return type for mypy Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(telemetry): use secrets.choice for device_id to satisfy Sonar S2245 device_id is a non-cryptographic identifier — it just needs to be unique enough for GA to count distinct devices — but Sonar flags random.choice with a generic "weak crypto" warning that fails the quality gate. secrets.choice is a one-line drop-in with the same output shape, costing nothing at this volume. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(telemetry): drop os_release — reads container OS, not host /etc/os-release inside the anthias-server container is the image's base layer (Debian trixie pinned in the Dockerfile), not the host's OS. The field would be near-constant across every deployment and tells us nothing about user environments, so it's just schema noise. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(telemetry): drop is_docker — always true in our deploy model Anthias only ships as Docker containers; the field would be a constant True across every deployment. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
8 tasks
vpetersson
added a commit
that referenced
this pull request
May 1, 2026
remote_branch_available: happy path (200 → True), 404 → False, network exception triggers backoff, 5xx triggers backoff, cache hits short- circuit, and active backoff suppresses requests. Locks in #2797's direct-branch endpoint behavior via a URL substring assertion. fetch_remote_hash: missing GIT_BRANCH, cache hit, branch-unavailable short-circuit, happy path, and request-exception handling. _get_ghcr_anonymous_token: happy path, RequestException, ValueError on JSON decode, missing token field, non-string token field. _get_ghcr_manifest_digest: happy path, 404 (no backoff), 5xx + 429 (with backoff), RequestException, missing/empty Docker-Content-Digest header (with backoff). is_running_latest_published_image: missing inputs, all three cache verdicts ('1'/'0'/'?'), backoff active, no token, no latest digest, current digest 404 caches '?', match → True, mismatch → False, and cache key scoped per (device_type, short_hash). handle_github_error: with and without exc.response. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
vpetersson
added a commit
that referenced
this pull request
May 2, 2026
* refactor(tests): migrate Python test suite from Django runner to pytest Replaces ./manage.py test with pytest as the single Python test runner for speed (xdist parallelization), readability (function-style + bare assert), and to drop the mock and unittest_parametrize extra deps in favor of stdlib unittest.mock + pytest.mark.parametrize. - pyproject.toml: drop mock and unittest-parametrize from dev (and types-mock from dev-host); add pytest, pytest-django, pytest-mock, pytest-xdist; add [tool.pytest.ini_options] with DJANGO_SETTINGS_MODULE, testpaths covering tests/, api/tests/ and anthias_app/, and the integration marker. - All 13 Python test modules under tests/ and api/tests/ converted to function-based pytest with @pytest.mark.django_db where Django ORM is touched, @pytest.mark.parametrize replacing unittest_parametrize, and @pytest.mark.integration replacing Django @tag('integration'). Mocks now import unittest.mock. - tests/test_app.py: integration tests use @pytest.mark.django_db(transaction=True) since the original unittest.TestCase did not wrap in a Django transaction (the live server uses the same SQLite DB). - .github/workflows/test-runner.yml + CLAUDE.md: invocations switched to `pytest -n auto -m "not integration"` and `pytest -m integration`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * test(coverage): add tests for lib/auth.py Cover NoAuth, BasicAuth, the abstract Auth base, password hashing round-trip, the legacy SHA256 detector, and the @Authorized decorator's three branches (no auth, auth-required redirect, view passthrough). The Authorization header path exercises malformed base64, missing colon, unsupported scheme, and the RFC 7617 colon-in-password case. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * test(coverage): cover GitHub + GHCR helpers in lib/github.py remote_branch_available: happy path (200 → True), 404 → False, network exception triggers backoff, 5xx triggers backoff, cache hits short- circuit, and active backoff suppresses requests. Locks in #2797's direct-branch endpoint behavior via a URL substring assertion. fetch_remote_hash: missing GIT_BRANCH, cache hit, branch-unavailable short-circuit, happy path, and request-exception handling. _get_ghcr_anonymous_token: happy path, RequestException, ValueError on JSON decode, missing token field, non-string token field. _get_ghcr_manifest_digest: happy path, 404 (no backoff), 5xx + 429 (with backoff), RequestException, missing/empty Docker-Content-Digest header (with backoff). is_running_latest_published_image: missing inputs, all three cache verdicts ('1'/'0'/'?'), backoff active, no token, no latest digest, current digest 404 caches '?', match → True, mismatch → False, and cache key scoped per (device_type, short_hash). handle_github_error: with and without exc.response. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * test(coverage): add tests for lib/diagnostics.py Cover the env-var helpers (get_git_branch / get_git_short_hash / get_git_hash), the file-backed helpers (get_uptime, get_debian_version, get_load_avg, get_utc_isodate), the device-helper-backed accessors (get_raspberry_code, get_raspberry_model), the CEC-via-subprocess get_display_power on True/False/CEC error/Unknown/empty stdout/timeout, and try_connectivity in all-OK / all-Error / mixed configurations. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * test(coverage): add tests for viewer ↔ server Redis messaging ViewerPublisher.send_to_viewer publishes the namespaced 'viewer …' payload on VIEWER_CHANNEL; ReplySender pushes JSON onto the per- correlation-ID list and sets the 30s TTL; ReplyCollector.recv_json covers the BLPOP path with second-rounding, the LPOP non-blocking path (timeout_ms <= 0), and ReplyTimeoutError on no reply. Singleton get_instance() caches the first instance and rejects double-init. ViewerSubscriber._consume dispatches commands with/without parameters, skips wrong-topic and non-string payloads, falls back to the 'unknown' handler when registered, and is a no-op when no handler matches. run() signals viewer-subscriber-ready, falls into _consume, and on ConnectionError marks unready before sleeping. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * test(coverage): add pytest tests for anthias_app/views_files.py Cover _client_ip directly (REMOTE_ADDR happy path, IPv6, malformed, missing), the require_client_in decorator (allow/reject/malformed/ multi-CIDR), and both views (anthias_assets, static_with_mime) for the in-CIDR happy path, out-of-CIDR rejection, traversal blocking, missing files, directory requests, symlink escape, and the mime override allowlist. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * test(coverage): cover reboot/shutdown/get_display_power Celery tasks reboot_anthias and shutdown_anthias now have both code paths exercised: on Balena, the supervisor helper is invoked (via tenacity Retrying); off Balena, an 'r.publish('hostcmd', …)' is issued. get_display_power delegates to lib.diagnostics and persists the verdict + 1h TTL on Redis. send_telemetry_task is a thin wrapper. cleanup also gains a test for the early-return path when settings['assetdir'] is missing. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * test(coverage): expand lib/utils.py coverage and add device_helper tests lib/utils: cover string_to_bool truthy/falsy/invalid, validate_url across http/https/rtsp/rtmp + invalid forms, the env-var booleans (is_ci / is_balena_app / is_demo_node / is_docker), the Balena supervisor helpers (get_balena_device_info / reboot / shutdown / version OK + error), JSON datetime serialisation, the perfect-paper- password generator (length and no-symbols variant), and the template-handle-unicode non-string fallback. lib/device_helper: parse_cpu_info on a representative Pi 4 cpuinfo fixture and on a minimal stub, plus get_device_type's full mapping (pi5/Compute Module 5, pi4/Compute Module 4, pi3/Compute Module 3, pi2, pi1) with x86 fallback when /proc/device-tree/model is missing. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * ci(coverage): wire up pytest-cov and enforce 80% line+branch coverage Add pytest-cov==6.0.0 to the dev group, configure [tool.coverage.run] with branch coverage and the focused source list (lib, viewer, api, anthias_app, celery_tasks, settings) plus omits for boilerplate (migrations, asgi/wsgi/urls/routing, image_builder, host_agent, viewer/__main__, etc.), and set fail_under = 80 in [tool.coverage.report] so CI fails when coverage regresses. The test-runner workflow now passes --cov flags to both the unit and integration jobs (--cov-append on the second so the totals merge), and points Codecov at the resulting coverage.xml that lands on the runner workspace via the /usr/src/app bind-mount. CLAUDE.md gets a coverage example next to the existing pytest invocation. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(tests): satisfy mypy on coverage tests Mypy under strict settings flagged 47 errors in the recently-added coverage tests. They fall into a few mechanical buckets, all in test files only — no source changes: - Replace `patch.object(<module>, '<attr>', ...)` with the string form `patch('<dotted.path>', ...)` for `utils.os`, `utils.requests`, `diagnostics.device_helper`, `diagnostics.utils`, and `celery_tasks_module.diagnostics`. mypy refuses to introspect the re-exported attribute even though it exists at runtime. - Switch the `request.session = {}` ignore code from `[attr-defined]` (which mypy reports as unused) to `[assignment]` — the actual error is "dict cannot be assigned to SessionBase". - Drop unused ignore comments that mypy flagged (`tests/test_messaging.py` `_redis = fake_redis`, `tests/test_github.py` `exc.response = None`, `tests/test_auth.py` `view()` / `view() -> str`). - Stop assigning the result of `NoAuth.authenticate()` (declared `-> None`); call it as a statement instead. Verified locally with `uv run mypy .` (Success: no issues found in 105 source files), `uv run ruff check .`, and `uv run ruff format --check .`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * ci(sonar): add sonar-project.properties to silence test-file false positives Sonar's default analysis treated everything under the repo as sonar.sources, including the new coverage tests. That tripped a batch of S5332 / S2068 / S1244 / S5443 false positives on fixtures that intentionally exercise insecure protocols, hard-coded test credentials, exact float comparisons, and tempfile.TemporaryDirectory patterns. Pin sonar.sources to the actual source dirs and mark tests/ + api/tests/ as sonar.tests so the narrower rule set applies. Wire up sonar.python.coverage.reportPaths so the coverage.xml emitted by pytest-cov is consumed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat(tests): make unit suite locally runnable + force-mock externals The unit suite previously assumed Docker: settings.py hard-coded the SQLite test DB to /data/.anthias/test.db, viewer modules required PyGObject from python3-gi, and connect_to_redis() pointed at the hostname `redis`. Developers had no way to run pytest from the host. Changes - anthias_django/settings.py: when ENVIRONMENT=test, default the SQLite path to BASE_DIR/.anthias-test.db. CI containers preserve the /data location via the new ANTHIAS_TEST_DB_PATH env var. - docker-compose.test.yml: set ANTHIAS_TEST_DB_PATH=/data/.anthias/test.db so CI keeps its historic DB layout without churn. - .gitignore: cover .anthias-test.db and its WAL siblings. - conftest.py (new, repo root): force ENVIRONMENT=test, stub gi / gi.repository / pydbus in sys.modules so viewer/__init__.py imports without the distro PyGObject stack, replace lib.utils.connect_to_redis with a dict-backed MagicMock factory at conftest load time AND via an autouse fixture (so module-level `r = connect_to_redis()` bindings in celery_tasks / lib.github / lib.telemetry / api.views.* never hit a real socket), and ensure settings['assetdir'] exists once per session so legacy cleanup fixtures don't FileNotFoundError out. - CLAUDE.md: document the local pytest recipe alongside the Docker one. External-call audit - requests.get/head/post/put/delete: all unit-test references inside patch() / patch.object() — clean. - connect_to_redis() / module-level `r`: every reference is either patched per-test or mocked by the new conftest fixture. - subprocess: only tests/test_migrate_legacy_paths.py runs a real subprocess, which is the migration shell script under test (local, no network) — left as-is per the task spec. - DBus / gi: stubbed at conftest level; no test exercises a live bus. Local run: `uv sync --group test && uv run pytest -m "not integration"` yields 359 passed, 12 deselected (libcec-dev required on host for the cec wheel build). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * style(tests): apply ruff format to conftest.py Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(tests): address Copilot review + drop unused type:ignore in conftest Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * refactor(tests): drop type:ignore / noqa suppressions in tests + conftest Replace bare-dict assignments to ``request.session`` in test_auth.py with ``SessionStore()`` from the signed_cookies backend (typed correctly, no DB write). Use ``cast()`` for the singleton-sentinel assignments in test_messaging.py instead of ``# type: ignore[assignment]``. In conftest.py, swap the ``try: import gi`` probe for ``importlib.util.find_spec``, and use ``setattr()`` for the dynamic attribute writes onto stubbed modules — both stop tripping mypy without suppression. Always stub ``pydbus`` when ``gi`` is missing (real pydbus does ``from gi.repository import GLib, GObject`` and our minimal stub doesn't fake ``GObject``). Drop the dead ``# noqa: E501`` markers from three test signatures (E501 isn't in the active ruff rule selection, so they were suppressing nothing) and rename the one signature that genuinely was over 79 chars (``test__if_git_branch_env_does_not_exist__is_up_to_date_should_return_true`` → ``test_returns_true_when_git_branch_env_missing``). Verified: ``ruff check``, ``ruff format --check``, ``mypy``, and the non-integration ``pytest`` run (359 passed) are all clean with zero suppressions remaining in tests/ + conftest.py. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * ci(sonar): silence test-only rule false positives S2068 (hard-coded credential), S1244 (float equality), S5864 (identity check), and S7492 (comprehension unpack) flag patterns that are intentional in test fixtures. Suppress them under tests/** and api/tests/** via sonar.issue.ignore.multicriteria rather than scattering NOSONAR comments. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * ci(sonar): fix issue-ignore globs to actually match top-level test files `tests/**/*.py` is Ant-style "tests/<dir>/.../<file>.py" — it requires at least one intermediate directory and so does not match `tests/test_auth.py`. Switch all four exclusions to `**/test_*.py`, which matches every test file under the project regardless of nesting and is the only pattern Sonar's PathMatcher reliably honors for top-level test directories. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(tests): unblock integration DB sharing, settings xdist race, sonar Three independent issues surfaced when CI ran fcb8768. 1. Selenium integration tests in tests/test_app.py were asserting on `Asset.objects.all()` and seeing 0 because pytest-django's default in-memory SQLite test DB is invisible to the separate anthias-server container handling the upload. Pin DATABASES['default']['TEST']['NAME'] to the same path the server reads via ANTHIAS_TEST_DB_PATH so both ends share one file. Gated on ENVIRONMENT=test because the conftest.py setdefault races with pytest-django's plugin-time settings load — leaving TEST.NAME pointed at /data/.anthias/anthias.db on local unit runs would make pytest-django try to open the production path. 2. tests/test_settings.py had four tests racing on a single `/tmp/.anthias/anthias.conf`; under `pytest -n auto` workers stomped on each other's writes/cleanups. Scope the temp HOME by PYTEST_XDIST_WORKER so each worker gets its own subtree, and replace mkdir+rmtree with idempotent makedirs / ignore_errors. 3. SonarCloud Automatic Analysis ignores sonar.issue.ignore.multicriteria from sonar-project.properties. Drop the dead config and silence the four genuine test-file false positives inline: - 7× S2068 (auth fixtures need literal passwords) → # NOSONAR - 1× S1244 (float equality on parsed value) → pytest.approx - 1× S5864 (`is sentinel_response`) → `==` - 1× S7492 (`all([... for ...])` style hint) → drop brackets Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(tests): per-line NOSONAR after ruff split dicts onto multiple lines The previous commit placed `# NOSONAR` on the dict closing-brace line, but `ruff format` reflowed the two longer dicts across multiple lines — leaving each `'password': 'newpw'` entry on its own line without a NOSONAR. Sonar's marker is line-scoped, so S2068 still fired on those 4 lines. Move the markers to each password key line directly. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * test(utils): mock url_fails() network calls in url_* tests The three test_url_* tests called url_fails() with literal URLs that hit example.com over real HTTP HEAD/GET, making the suite dependent on outbound DNS+internet from CI runners and prone to flake under xdist parallelism. Replace them with patched-requests equivalents that exercise the same three branches: - test_url_fails_returns_true_on_connection_error - test_url_fails_returns_false_on_2xx_response - test_url_fails_short_circuits_for_invalid_url The third test also asserts that requests.head is never called, locking in the validate_url() short-circuit so a future regression that lets a schemeless path slip through can't pass silently. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(tests): address Copilot's three remaining review threads 1. anthias_django/settings.py — settings.py was loaded by pytest-django's plugin init before the root conftest.py could run `os.environ.setdefault('ENVIRONMENT', 'test')`, so on local pytest runs `db_path` resolved to `/data/.anthias/anthias.db`. Functionally harmless (pytest-django shadows NAME with `:memory:` for SQLite when TEST.NAME is unset) but fragile and confusing. Detect pytest directly via `any('pytest' in a for a in sys.argv)` so the test branch fires regardless of import order. Covers `pytest`, `python -m pytest`, and `uv run pytest`. 2. conftest.py — _install_dbus_stubs() bailed out as soon as `gi` was importable, but a host can have system `gi` without pip-installed `pydbus` (or vice versa). The real pydbus's module-load also imports GLib and GObject from gi.repository, which our minimal gi stub didn't include. Restructure: if `gi` is missing, stub gi and pydbus together (real pydbus can't load against our stub gi); if `gi` is present but `pydbus` is missing, stub only pydbus; add GObject to the stub. 3. anthias_app/tests.py — legacy unittest.TestCase view-file tests are wholly subsumed by tests/test_views_files.py (which adds directory-request and disallowed-mime cases too). Delete the legacy file and drop `anthias_app` from `testpaths`. `python_files` tightens from `["test_*.py", "tests.py"]` to `["test_*.py"]` since no `tests.py` files remain. Local: 347 pass under `pytest -n auto -m "not integration"` (was 359; the 12-test drop matches the 12 deleted duplicates), mypy/ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(tests): gate TEST.NAME on ANTHIAS_INTEGRATION_TEST=1 Setting `DATABASES.default.TEST.NAME` to a single file path on every pytest run forced all `pytest -n auto` xdist workers onto one SQLite file, courting `database is locked` failures and cross-worker leakage. The unit step doesn't actually need a shared DB — pytest-django gives each worker its own `:memory:` SQLite when TEST.NAME is unset, which is exactly what unit tests want. Only the integration step needs the shared file (so the separate anthias-server container's writes are visible to the test process' `Asset.objects.all()` queries). Gate TEST.NAME on a new ANTHIAS_INTEGRATION_TEST=1 env var, set in test-runner.yml only for the `pytest -m integration` step. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(tests): address Copilot review of retry coverage + fake-redis list ops 1. .github/workflows/test-runner.yml — the integration step runs under nick-fields/retry. With `--cov-append`, partial line-hit data from any failed earlier attempt was silently merged into the final coverage.xml on retry success. Snapshot `.coverage` to `.coverage.unit-snapshot` immediately after the unit step, then restore from that snapshot at the start of every retry attempt so only the last (successful) attempt contributes integration data. 2. conftest.py — the conftest fake-Redis got list ops wrong: `lpop(key)` returned the entire stored list and `blpop(...)` always returned None. No current test exercises BLPOP against the shared fake (test_messaging uses its own MagicMock), but the semantics are a latent footgun for any test that drives `ReplyCollector.recv_json`. Implement real list-head pop and a non-blocking BLPOP that yields the first available value. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * docs(claude): note ANTHIAS_INTEGRATION_TEST=1 for local integration runs The docker-compose integration command in CLAUDE.md still showed the pre-`3844fbcf` invocation. Without the env var, pytest-django uses per-worker `:memory:` SQLite, the anthias-server container writes to a different file, and Selenium tests that assert on `Asset.objects` silently see zero rows. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(tests): skip pytest-django's DB destroy/create on integration runs The integration step pins TEST.NAME to /data/.anthias/test.db so the test process and the uvicorn server (started by prepare_test_environment.sh -s) share one SQLite file. pytest-django's default DB setup runs `os.remove(NAME)` followed by re-create + migrate. That works today only because Django's default CONN_MAX_AGE=0 makes uvicorn open a fresh connection per request — change CONN_MAX_AGE or pre-warm a connection at startup and the server's handle would point at an unlinked inode while pytest writes to the new file. Pass --reuse-db so pytest-django opens the existing file in place and never deletes it. The DB is already migrated by prepare_test_environment.sh before the server starts, and transaction=True on integration tests still flushes tables between tests for per-test isolation. Update CLAUDE.md's local-Docker recipe to match. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Two unrelated server-stack regressions surfacing on the latest deploy. One fatal, one silent. Bundled as a hotfix PR since both are small, contained, and would otherwise need separate cycles.
1. dbbackup blocks
manage.py migrate(fatal)django-dbbackupwas bumped to 5.3.0 in #2768 alongside the Django 5 LTS upgrade. v5 dropped the legacyDBBACKUP_STORAGE/DBBACKUP_STORAGE_OPTIONSkeys in favor of Django 5'sSTORAGESdict and now raisesRuntimeErroron import if either legacy key is present:This blocks every
manage.pyinvocation — most visiblymigrateduringanthias-servercontainer startup — because dbbackup's management commands import the package at registration time.Migrate the config into
STORAGES['dbbackup']. DefiningSTORAGESreplaces Django's framework defaults entirely, sodefaultandstaticfilesare restated even though they match the global defaults — without them, file uploads and WhiteNoise-served static files would lose their backends.2. Version check silently broken (non-fatal but wrong)
lib/github.py:remote_branch_available()listed all branches viaGET /repos/screenly/anthias/branchesand scanned the response for a name match. That endpoint paginates at 30 results, sorted alphabetically. Once auto-generated branches (claude/*,dependabot/*,renovate/*) pushed the count past 30,masterfell off the first page and the lookup returnedFalse. Everyfetch_remote_hash()call then logged:…and
is_up_to_date()short-circuited toTrueregardless of actual upstream state. Confirmed: the repo currently has 81 branches, andmasteris not in the first 30 alphabetically:Switch to the per-branch endpoint
GET /repos/.../branches/{branch}: 200 if it exists, 404 if not, no pagination. 404 is a clean negative and is cached without triggering the generic error backoff; other non-200s still flow throughhandle_github_error().Test plan
ruff checkandruff format --checkpassanthias-serverstarts pastRunning migration...without the RuntimeErrorUpdate Availablebanner reflects actual upstream state (no longer always-up-to-date)🤖 Generated with Claude Code