Skip to content

Fix the Review Lens plugin's wire contract, and 20 defects behind it #655

Fix the Review Lens plugin's wire contract, and 20 defects behind it

Fix the Review Lens plugin's wire contract, and 20 defects behind it #655

Workflow file for this run

name: CI
# Minimal CI: re-verifies on every PR exactly what has been run manually
# before every merge - pytest, npm run check. python-tests' PRIMARY cell
# (windows-latest, Python 3.12) exists because backend/tests/
# test_backend_secrets_at_rest.py exercises real Windows DPAPI
# (CryptProtectData/CryptUnprotectData) - the secrets-at-rest encryption
# this app actually ships - which only exists on Windows; that's still the
# one cell every other job effectively depends on for real DPAPI coverage.
# This used to also be pinned for PySide6/QtWebEngine's offscreen mode, but
# Qt was fully removed at the R7.6b cutover.
#
# ADR-015 stage 15.6's OS/Python MATRIX IS NOT DONE, and this job is
# deliberately still a single windows-latest/3.12 cell. It was attempted
# (4 cells: windows 3.10/3.12 + ubuntu 3.11/3.12) and reverted after CI
# proved two hard blockers, both of which are real work rather than a
# config tweak:
#
# 1. requirements.txt cannot install on 3.10/3.11 at all. It is a
# hash-locked pip-compile output resolved against 3.12 - numpy==2.5.1
# requires >=3.12, contourpy==1.3.3 requires >=3.11 - so those cells
# die at `pip install -r requirements.txt` before running a thing.
# (This file's build-check note below already said the lock is
# 3.12-specific; that warning was correct.) Fixing it means either
# per-version lockfiles or an unpinned install path for non-primary
# cells - a real dependency-management decision.
# 2. ~18 tests genuinely assume Windows and have no platform guard: the
# DPAPI secrets-at-rest suite (which asserts encryption actually
# happened, only true on Windows), subprocess.CREATE_NO_WINDOW in
# test_code_sandbox_domain.py, and os.startfile in
# test_diagnostic_bundle.py. They fail on ubuntu even at 3.12 where
# the lock installs fine. Guarding them touches a security-boundary
# test file and deserves its own reviewed change.
#
# The one skipif this stage DID land (the unguarded real-DPAPI probe in
# test_backend_secrets_at_rest.py) is kept - it is correct on its own
# merits regardless of when the matrix returns.
#
# The pytest/compileall steps run from the repo root, never a subdirectory:
# pinning working-directory scopes collection to that directory's tests and
# silently skips the rest. Collection today (2026-08-25) is backend/tests/,
# contracts/tests/, tests/ (the permanent Qt-removal gate), and mutation_tests/
# (property-based tests added after this comment was first written) - the
# exact set `python -m pytest -q` from the repo root runs locally, so a green
# local run and a green CI run mean the same thing. Verify with that command,
# not a narrowed path - and re-check this directory list occasionally, since
# it has drifted before.
#
# R7.6b: graphlink_app/tests/ is gone from this list because graphlink_app/
# itself was deleted at the Qt-removal cutover.
#
# ADR-015 stage 15.1: build-check is a third, independent job. It exists
# because `pip install -r requirements.txt` (the python-tests job, above)
# never actually builds this project's own wheel - so a broken py-modules
# list (a real incident: graphlink_note_agent and graphlink_process_env were
# both missing, the latter silently disabling the subprocess secret-scrubbing
# allowlist) shipped invisibly. This job builds the wheel, twine-checks its
# metadata, and imports backend.agents from a clean venv with the repo
# checkout NOT on sys.path (Push-Location $env:TEMP before the import test) -
# the same failure mode a `pip install .` from the loose repo directory would
# hide. `build`/`twine` are installed directly in this job rather than added
# to requirements.txt: they are CI-only tooling, not an app runtime
# dependency, and requirements.txt's hash-lock is compiled against the
# project's pinned Python 3.12 - regenerating it from a different interpreter
# risks a lock that resolves differently than what 3.12 actually needs.
# ADR-015 stage 15.6: e2e-playwright is a fourth, independent job. Every
# job above exercises the backend or the frontend in isolation (unit/
# integration tests, a mock DOM for React, a wheel import) - none of them
# ever boots the real backend/app.py factory under uvicorn AND drives the
# real BUILT SPA against it with a real browser, the one gap those layers
# structurally cannot close (a wiring break between the two - a renamed
# WS message shape, a selector that silently stopped matching real DOM -
# can pass every unit suite and still leave the shipped app broken).
# tests_e2e/run_backend.py's own module docstring has the full "why
# Chromium is a faithful proxy for pywebview's WebView2, why no auth
# token, why a fresh temp dir every run" reasoning; web_ui/playwright.
# config.ts's own comment has the "why Chromium-only, why single-worker"
# half. ubuntu-latest, not windows: this suite never touches DPAPI/Job
# Objects/ACLs (python-tests' own windows-latest cells already cover
# those), so there is no reason to pay the 2x Windows-runner bill here -
# same economy note build-check/frontend-checks already follow.
# Actions-minutes economy (2026-08-07, ~90% of the month's quota burned by
# 189 runs in one week): the push->main trigger is REMOVED - every merge is a
# squash whose tree is byte-identical to the PR head that CI just validated,
# so the post-merge run was a pure duplicate. The PR run is the gate.
# Re-evaluate next cycle if non-squash merges ever appear.
on:
pull_request:
branches: [main]
# ADR-015 stage 15.2: least-privilege token (this workflow only ever reads
# the checkout - no job pushes, comments, or creates releases), and a
# concurrency group so a rapid string of pushes to the same PR/branch cancels
# the now-stale in-flight run instead of queueing redundant Windows minutes
# behind it.
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
python-tests:
# ADR-015 stage 15.3: was "Python tests (offscreen)" - a Qt-removal-era
# name for a job that no longer runs anything Qt-related (the offscreen
# QT_QPA_PLATFORM setting it referred to is gone, see pyproject.toml's
# own [tool.pytest.ini_options]). Windows-pinned for DPAPI, not Qt - see
# this file's own module comment above.
name: Python checks (Windows, DPAPI)
runs-on: windows-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
# ADR-015 stage 15.7: pytest/pytest-cov/ruff/mypy are dev-only
# tooling, deliberately NOT in requirements.txt (the prod lockfile a
# real install pulls in) - see pyproject.toml's
# [project.optional-dependencies] dev extra, which lists the
# identical set for a contributor's own `pip install -e .[dev]`.
# pip-tools is deliberately NOT installed here: the lock-currency
# check (tests/test_dependency_lock_currency.py) compares the two
# requirements files as text rather than shelling out to
# pip-compile, precisely so CI never depends on pip-tools-vs-pip
# version skew - see that test's own module docstring for the CI
# failure that proved the shell-out approach unusable.
- name: Install dev tooling (pytest, ruff, mypy - CI-only, not in the prod lockfile)
run: pip install --quiet pytest==9.1.1 pytest-cov==7.1.0 pytest-xdist==3.8.0 ruff==0.16.4 mypy==2.3.1 hypothesis==6.165.10
- name: Compile check
run: python -m compileall -q .
- name: ruff (scoped rule set - see pyproject.toml's own [tool.ruff])
run: python -m ruff check .
- name: mypy (scoped rule set - see pyproject.toml's own [tool.mypy])
run: python -m mypy
- name: Run pytest (with coverage floor - see pyproject.toml's own [tool.coverage])
run: python -m pytest -q --cov --cov-report=term-missing --ignore=backend/tests/perf
# ADR-015 stage 15.4 review: coverage.py's line tracer adds real
# per-line overhead - enough to trip test_loop_watchdog.py's own
# 100ms stall ceiling under instrumentation (confirmed: it failed
# under --cov, passed clean without it). Running the perf suite in
# its own uninstrumented invocation is what test_loop_watchdog.py's
# own module docstring already asks for (stable timing, no
# environmental noise) - coverage instrumentation is exactly the
# kind of noise that docstring means.
- name: Run pytest (perf suite, uninstrumented AND serial - coverage tracing and parallel sibling workers would both corrupt its timing assertions)
run: python -m pytest -q -n0 backend/tests/perf
- name: pip-audit (known-vulnerability scan of the locked dependency set)
run: pip install --quiet pip-audit==2.10.1 && python -m pip_audit -r requirements.txt
build-check:
name: Build check (wheel builds, installs, and imports cleanly)
# ubuntu, not windows (2026-08-07 minutes economy): Windows runners bill
# at 2x. Nothing here is Windows-bound - the wheel is pure-Python and
# graphlink_execution_guard's WinDLL load is behind `sys.platform ==
# "win32"`, so `import backend.agents` is clean on Linux. Only
# python-tests genuinely needs Windows (DPAPI, Job Objects, ACLs).
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install build tooling
run: pip install build==1.5.0 twine==7.0.0
- name: Build the wheel
run: python -m build --wheel
- name: Twine check (validates package metadata, incl. the version string)
# python -m twine, not a bare `twine` invocation: pip's script-install
# directory isn't reliably on PATH across runner/pip configurations
# (confirmed locally - `twine` alone was not found even right after
# `pip install twine` succeeded), and the module form has no such
# dependency.
run: python -m twine check dist/*.whl
- name: Install wheel + real runtime deps into a clean venv, import backend.agents
shell: bash
run: |
python -m venv wheel_test_venv
wheel_test_venv/bin/pip install --quiet -r requirements.txt
wheel=$(ls dist/*.whl | head -1)
wheel_test_venv/bin/pip install --quiet --no-deps "$wheel"
cd /tmp
"$GITHUB_WORKSPACE/wheel_test_venv/bin/python" -c "import backend.agents, graphlink_note_agent, graphlink_process_env; print('OK: backend.agents and all py-modules import cleanly from the installed wheel')"
frontend-checks:
name: Frontend checks (npm run check)
# ubuntu, not windows - same 2x-billing economy note as build-check.
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# check:schema shells out to the Python codegen script (stdlib-only,
# no pip install needed) to prove the generated TS/JSON Schema
# artifacts aren't stale relative to the Python dataclasses. No
# cache: pip here (unlike the other two jobs' setup-python) - this
# step never runs a pip install, so the cache dir never gets
# populated and setup-python's own post-job cache-save step fails
# ("Cache folder path is retrieved for pip but doesn't exist on
# disk") - confirmed live on PR #212's first CI run.
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: web_ui/.nvmrc
cache: npm
cache-dependency-path: web_ui/package-lock.json
- name: Install npm dependencies
working-directory: web_ui
run: npm ci
# Wrapped rather than a bare `npm audit --audit-level=high`: that
# conflates "this dependency set has a high-severity advisory" with
# "registry.npmjs.org did not answer" into the same exit code, and the
# second failed two consecutive Python-only pull requests in one
# afternoon. See web_ui/scripts/audit-with-retry.mjs's own header for
# the full reasoning and the trade it makes. A real advisory still
# fails the build on the first attempt.
- name: npm audit (known-vulnerability scan, high+critical only)
working-directory: web_ui
run: node scripts/audit-with-retry.mjs
- name: Run checks (schema drift, typecheck, lint, vitest, build)
working-directory: web_ui
run: npm run check
e2e-playwright:
name: E2E (Playwright boot-smoke)
# ubuntu, not windows - see this file's own module comment above for
# why this suite specifically doesn't need a Windows runner.
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: web_ui/.nvmrc
cache: npm
cache-dependency-path: web_ui/package-lock.json
# The prod lockfile, not the dev extras python-tests installs - this
# job never runs pytest/ruff/mypy, it only needs a real Python
# environment for tests_e2e/run_backend.py's own `from backend.app
# import create_app`, exactly what a real `pip install -r
# requirements.txt` gives a contributor.
- name: Install Python dependencies
run: pip install -r requirements.txt
- name: Install npm dependencies
working-directory: web_ui
run: npm ci
# Playwright's own webServer (playwright.config.ts) serves the SPA
# from web_ui/dist/app - the real production build, the same
# directory graphlink_desktop.py's own real launch serves from - not
# a vite dev server, so this build has to exist before the suite runs
# at all.
- name: Build the SPA
working-directory: web_ui
run: npm run build
# Chromium only (see playwright.config.ts's own comment for why) -
# `--with-deps` also apt-installs the handful of shared libraries a
# headless Chromium needs on a bare ubuntu-latest runner, which this
# job otherwise doesn't have.
- name: Install Playwright's Chromium browser
working-directory: web_ui
run: npx playwright install --with-deps chromium
- name: Run Playwright E2E suite
working-directory: web_ui
run: npm run test:e2e
# A fifth job, and the smallest. python-tests is the only job that runs
# pytest and it is pinned to windows-latest for DPAPI, so the 9 tests that
# assert POSIX file mode bits - on chats.db, the knowledge store, DB
# backups, and session.dat, the secrets-at-rest file - skipped in CI and
# ran nowhere else. Nine assertions about the permissions of a file holding
# API keys, executing on no machine anyone checked.
#
# Scoped to the four files that contain them, NOT all of backend/tests:
# pytest imports every module it collects, and this file's own comment
# above records that ~18 tests elsewhere genuinely assume Windows.
# Narrowing collection is what keeps this job about permissions rather
# than about porting the suite to Linux. tests/test_posix_permission_
# coverage.py ties the file list below to the marked tests, so a tenth
# marked test landing in a fifth file fails the build instead of silently
# not running here.
#
# ubuntu-latest, and deliberately cheap: no coverage, no xdist workers
# (-n0 for 9 tests), no dev tooling beyond pytest itself.
posix-permissions:
name: POSIX file permissions (ubuntu)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: pip install -r requirements.txt
# pytest-xdist is needed even at -n0: [tool.pytest.ini_options].addopts
# names -n, so the plugin has to be importable for the run to start.
# hypothesis for the same class of reason - backend/tests/conftest.py
# imports it at module scope, so pytest cannot load the conftest (and
# therefore collects nothing at all) without it. Versions match the
# python-tests job's own pins.
- name: Install pytest
run: pip install --quiet pytest==9.1.1 pytest-xdist==3.8.0 hypothesis==6.165.10
- name: Run the POSIX permission assertions
run: >
python -m pytest -q -n0 -m posix_permissions
backend/tests/test_chat_library.py
backend/tests/test_db_backup.py
backend/tests/test_knowledge_store.py
backend/tests/test_backend_secrets_at_rest.py