* ci: pin black and flake8, let the bandit hook fail, cache npm in the JS job (#352) The lint job installs black==26.10.1 and flake8==7.4.1 from requirements-dev.txt (the versions CI resolved on main today), so a new release cannot turn CI red with no code change. The local bandit hook ran 'bash -c ... "$@"; true': it could never fail, and bash bound the first staged file to $0 so it was never scanned. It now runs 'bandit -lll -q' directly, failing on high severity exactly like code-quality.yml. The frontend modules job's setup-node caches npm keyed on tools/vendor/package-lock.json. * test(history): wait on the writer's state, not short wall-clock pauses (#352) Four 0.08 s pauses before asserting that the worker had not probed yet now wait until the idle worker has run a whole loop pass at the current manual clock value (three clock reads), so a slow runner cannot end the pause before the worker decides. A mutation that probes 20 ms early now fails two tests deterministically. The real-clock bounded close no longer asserts close(0.02) returned in under 0.2 s wall time; it asserts the in-flight write is still blocked and the worker alive, like the output writer test. The manual-clock bound stays exact. The last 0.1 s non-blocking submit check uses the 1 s NON_BLOCKING_CEILING from #351. * test(api): give each API fixture database its own temp folder (#346) Every PromptManagerAPI sweeps its database's folder at startup for the stages, uploads and safety-backup copies an interrupted restore left. The API test fixtures created their database directly in the system temp folder, so any other test process starting an API there (another worktree's suite, a pre-push hook, an agent's targeted run) deleted this process's in-flight restore files. The restore, epoch and retirement tests then failed with 'staged restore changed', 'unable to open database file' or 503 instead of 409. Busy machines fail because they are running other suites, not because they are slow: the same CPU load with a private TMPDIR passes. private_temp_db() puts the fixture database in a new folder removed at cleanup (with any safety backups a restore left beside it). A regression test sweeps a stand-in shared folder in the middle of a multipart restore, as another suite's startup would. * test(history): count worker parks, hold saturation blocks until released (#352) wait_for_worker_pass now counts the idle worker's parks on its wake event (two parks from now = one whole pass begun after the call) instead of assuming a fixed number of clock reads per pass, which a future clock read would have made vacuous. The real-clock bounded close keeps a loose upper bound (0.02 s + NON_BLOCKING_CEILING). The two saturation tests (history and output writer) blocked the worker with release.wait(3) while they queued 1023 more items. On a loaded runner the fill took longer than 3 s, the worker unblocked on its own and drained part of the queue (pending 868..1017 != 1024), and cleanup then reported a leaked worker. The block now holds until the test releases it; 60 s is only a hang guard, and cleanup always sets the release. Under one core with 8 busy loops: 8/8 runs failed before, 6/6 pass after. * test(maintenance): wait on duplicate jobs with a disk-bound hang guard (#409) On Windows py3.13 CI (PR #405) a one-file duplicate cleanup missed its 10 s WAIT. The test already waits on the job's own task; the 10 s bound acted as a deadline on disk-bound work. In the same 50 s window the following tests' setups took up to 9 s against 0.1-0.6 s around it while the test bodies stayed fast, which is consistent with a process-wide disk I/O stall on the runner rather than a lease, handle, executor or job stall (the log has no thread dump to show more). tests/disk_waits.py holds the one disk-bound hang guard (90 s per wait, under the 120 s per-test watchdog), shared with RESTORE_SECONDS (#382). Waits on find and cleanup job tasks in the duplicate-files tests use it; gates and server-state polls keep WAIT. The keeper-validation test turns the safety backup off, since it does not test backups.
256 lines
10 KiB
YAML
256 lines
10 KiB
YAML
name: Tests
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
on:
|
|
merge_group:
|
|
pull_request:
|
|
branches: [main]
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
# Superseded PR pushes cancel their older run. Every main push gets its own
|
|
# group (run_id), so no main commit's run is ever cancelled or dropped as pending.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
env:
|
|
PYTHONFAULTHANDLER: "1"
|
|
|
|
# Every job has a time limit: ceil(1.5 x the slowest of three recent green runs on main) + 3
|
|
# minutes, never under 5. The 3 minutes leave room for the Python runner's watchdog (120 s, see
|
|
# tests/run_suite.py) to print its stack dump before GitHub cancels the job. A limit changes only
|
|
# with new measurements written here (TESTING.md "Time limits and hangs").
|
|
# Measured 2026-10-26 on runs 40257217681 (3be0ada), 40240706568 (fb9134f), 40228167328 (f9835aa):
|
|
# py matrix, slowest py3.10 on windows-latest: 12m43s / 15m54s / 14m53s -> 27 min
|
|
# (ubuntu 5m27s-6m27s, macos 6m14s-8m30s, windows 11m42s-15m54s)
|
|
# 3.10 and 3.11 left the matrix and 3.14 joined it in #356; re-measure after three green main runs.
|
|
# real ComfyUI compatibility: 13m32s / 8m55s / 11m46s -> 24 min
|
|
# coverage gate (85%): 7m26s / 7m22s / 7m54s -> 15 min
|
|
# perf budget 30s / 29s / 27s, black and flake8 39s / 37s / 39s,
|
|
# frontend modules 23s / 19s / 29s -> 5 min (the floor)
|
|
jobs:
|
|
test:
|
|
name: py${{ matrix.python-version }} on ${{ matrix.os }}
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 27
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
python-version: ["3.12", "3.13", "3.14"]
|
|
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Set up Python ${{ matrix.python-version }}
|
|
uses: actions/setup-python@v7
|
|
with:
|
|
python-version: ${{ matrix.python-version }}
|
|
cache: pip
|
|
cache-dependency-path: requirements-dev.txt
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install -r requirements-dev.txt
|
|
|
|
- name: Run tests
|
|
run: python -m tests.run_suite -v
|
|
|
|
comfyui:
|
|
name: real ComfyUI compatibility
|
|
runs-on: ubuntu-latest
|
|
# Chromium and its system libraries come from the pinned Playwright image (tag = @playwright/test in
|
|
# tools/vendor/package-lock.json; tests/vendor/browser-image.test.mjs checks it). The runner install
|
|
# pulled them from Ubuntu's apt mirror on every run, and three runs (#349, #378, #382) crawled at
|
|
# ~26 kB/s for 11-12 minutes until the job hit its limit after every test had passed.
|
|
container:
|
|
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
|
options: --user 1001
|
|
timeout-minutes: 24
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
persist-credentials: false
|
|
- name: Read the reviewed runtime pins
|
|
id: runtime
|
|
run: |
|
|
node --input-type=module <<'JS' >> "$GITHUB_OUTPUT"
|
|
import runtime from './tools/vendor/comfyui-runtime.json' with { type: 'json' };
|
|
for (const [key, value] of Object.entries(runtime)) console.log(`${key}=${value}`);
|
|
import crossPack from './tools/vendor/cross-pack-runtime.json' with { type: 'json' };
|
|
console.log(`kikoRepository=${crossPack.repository}`);
|
|
console.log(`kikoRevision=${crossPack.revision}`);
|
|
JS
|
|
- name: Check out pinned ComfyUI
|
|
uses: actions/checkout@v7
|
|
with:
|
|
repository: ${{ steps.runtime.outputs.repository }}
|
|
ref: ${{ steps.runtime.outputs.revision }}
|
|
path: .ci-comfyui
|
|
persist-credentials: false
|
|
- name: Check out pinned KikoTools for compatibility
|
|
uses: actions/checkout@v7
|
|
with:
|
|
repository: ${{ steps.runtime.outputs.kikoRepository }}
|
|
ref: ${{ steps.runtime.outputs.kikoRevision }}
|
|
path: .ci-kikotools
|
|
persist-credentials: false
|
|
# The image's own Python and Node 24: setup-python/setup-node install the runner's toolcache builds,
|
|
# whose entry points name host paths that do not exist in the container. The image has no ensurepip
|
|
# and the job has no root, so a checksum-pinned pip wheel bootstraps a user-space venv. The image
|
|
# Python must be the reviewed host Python, so a pin or image bump that disagrees fails here.
|
|
- name: Python environment on the reviewed host Python
|
|
env:
|
|
PIP_WHEEL: https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl
|
|
PIP_SHA256: 71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e
|
|
PINNED_PYTHON: ${{ steps.runtime.outputs.pythonVersion }}
|
|
run: |
|
|
image="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')"
|
|
[ "$image" = "$PINNED_PYTHON" ] || { echo "::error::image Python $image is not the pinned $PINNED_PYTHON"; exit 1; }
|
|
venv="$RUNNER_TEMP/venv" wheel="$RUNNER_TEMP/pip-26.2.1-py3-none-any.whl"
|
|
python3 -m venv --without-pip "$venv"
|
|
curl -fsSL --retry 3 -o "$wheel" "$PIP_WHEEL"
|
|
echo "$PIP_SHA256 $wheel" | sha256sum -c -
|
|
"$venv/bin/python" "$wheel/pip" install --no-index --no-cache-dir "$wheel"
|
|
echo "$venv/bin" >> "$GITHUB_PATH"
|
|
- run: python --version && node --version && node --version | grep -q '^v24\.'
|
|
# pip's download cache (torch alone is ~200 MB); inside the container ~ is the job's HOME (/github/home).
|
|
- name: Cache pip downloads
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: ~/.cache/pip
|
|
key: pip-${{ runner.os }}-playwright-v1.64.1-${{ hashFiles('requirements-dev.txt', '.ci-comfyui/requirements.txt', 'tools/vendor/comfyui-runtime.json') }}
|
|
- name: Install CPU runtime and test dependencies
|
|
run: |
|
|
pip install "torch==${{ steps.runtime.outputs.torchVersion }}" "torchvision==${{ steps.runtime.outputs.torchvisionVersion }}" --index-url https://download.pytorch.org/whl/cpu
|
|
pip install -r .ci-comfyui/requirements.txt -r requirements-dev.txt "torch==${{ steps.runtime.outputs.torchVersion }}" "torchvision==${{ steps.runtime.outputs.torchvisionVersion }}"
|
|
# npm's download cache, keyed on the lockfile. node_modules itself is still built fresh by npm ci.
|
|
- name: Cache npm downloads
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: ~/.npm
|
|
key: npm-${{ runner.os }}-playwright-v1.64.1-${{ hashFiles('tools/vendor/package-lock.json') }}
|
|
- run: npm ci --prefix tools/vendor --ignore-scripts
|
|
- name: Classic and Nodes 2.0 startup and compatibility
|
|
env:
|
|
PM_COMFY_ROOT: ${{ github.workspace }}/.ci-comfyui
|
|
PM_TEST_PYTHON: python
|
|
run: npm run --prefix tools/vendor test:comfyui
|
|
- name: Real cached and headless execution history
|
|
env:
|
|
PM_COMFY_ROOT: ${{ github.workspace }}/.ci-comfyui
|
|
PM_TEST_PYTHON: python
|
|
run: npm run --prefix tools/vendor test:executions
|
|
- name: Independent and combined pack compatibility
|
|
env:
|
|
PM_COMFY_ROOT: ${{ github.workspace }}/.ci-comfyui
|
|
PM_KIKO_ROOT: ${{ github.workspace }}/.ci-kikotools
|
|
PM_TEST_PYTHON: python
|
|
run: npm run --prefix tools/vendor test:compat
|
|
- name: Upload real-host reports and startup logs
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: real-comfyui-reports
|
|
path: tools/vendor/test-results/
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|
|
|
|
coverage:
|
|
name: coverage gate (85%)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- uses: actions/setup-python@v7
|
|
with:
|
|
python-version: "3.12"
|
|
cache: pip
|
|
cache-dependency-path: requirements-dev.txt
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install -r requirements-dev.txt
|
|
|
|
- name: Measure coverage
|
|
run: |
|
|
python -m coverage run -m tests.run_suite
|
|
python -m coverage combine
|
|
python -m coverage report --fail-under=85
|
|
|
|
perf-budget:
|
|
# The reference wall-clock measurement (TESTING.md "Performance budgets").
|
|
# Shared-runner timing varies, so it never gates a pull request: it runs on
|
|
# each push to main and on demand. A red run on main means fix the code.
|
|
name: perf budget
|
|
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- uses: actions/setup-python@v7
|
|
with:
|
|
python-version: "3.13"
|
|
cache: pip
|
|
cache-dependency-path: requirements-dev.txt
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install -r requirements-dev.txt
|
|
|
|
- name: Workspace filter budget
|
|
env:
|
|
PROMPTMANAGER_PERF_BUDGET: "1"
|
|
run: python -m unittest -v tests.test_api_workspace_images.TestWorkspaceImages.test_filter_budget
|
|
|
|
lint:
|
|
name: black and flake8
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: actions/setup-python@v7
|
|
with:
|
|
python-version: "3.12"
|
|
cache: pip
|
|
cache-dependency-path: requirements-dev.txt
|
|
# The exact black and flake8 pins from requirements-dev.txt, so a new release cannot turn CI red.
|
|
- run: pip install $(grep -E '^(black|flake8)==' requirements-dev.txt)
|
|
- run: black --check .
|
|
- run: flake8 .
|
|
|
|
js-test:
|
|
name: frontend modules
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- uses: actions/setup-node@v7
|
|
with:
|
|
node-version: "24"
|
|
cache: npm
|
|
cache-dependency-path: tools/vendor/package-lock.json
|
|
|
|
- name: Check shipped extension and page modules parse
|
|
run: |
|
|
shopt -s globstar nullglob
|
|
for f in web/comfy/**/*.js web/comfy/**/*.mjs web/js/**/*.js web/js/**/*.mjs; do
|
|
node --input-type=module --check < "$f" || exit 1
|
|
done
|
|
|
|
- name: Install pinned shared test contracts
|
|
run: npm ci --prefix tools/vendor --ignore-scripts
|
|
|
|
- name: Run JS tests
|
|
run: node --test tests/js/*.test.js
|