ci: give the live-instance integration suite a nightly CI home (#1896) - #1963
ci: give the live-instance integration suite a nightly CI home (#1896)#1963dolho wants to merge 3 commits into
Conversation
ec2faec to
7646a5a
Compare
|
Resolve by running |
~93 of the test files in `tests/` root need a RUNNING Trinity — the fixtures create and delete real agents against `TRINITY_API_URL` — and no CI job ran them, so they gated nothing. `integration-nightly.yml` boots a stack per open PR and runs the suite on both sides, reusing the pieces that already exist rather than inventing any: `scripts/deploy/start.sh` for the stack (as frontend-e2e.yml does), the discover -> test -> comment job split and sticky-comment machinery from backend-unit-nightly.yml, and `diff-pytest-failures.py` for base-vs-head attribution. NOT BLOCKED ON #1895, despite that issue's sequencing note. Membership is directory-based — `--ignore=unit --ignore=process_engine`, exactly as tests/run-core.sh:22 already defines the same suite — so every file #1895 relocates leaves this job's scope automatically and the workflow needs no edit. There is no "~60 files" target to chase and no list to drift. Nightly rather than per-PR (AC 2): the suite MUTATES its target and needs ~3-5 min on top of a ~2-3 min boot, twice. In the merge path that is ~10 min on every backend PR plus a mutating flake surface in front of every merge. Three things measured rather than assumed, each of which would have shipped this broken: 1. `--continue-on-collection-errors` is load-bearing. Three root modules currently fail to IMPORT (two git-sync files wanting `utils.credential_sanitizer`, and tests/integration's conftest). Without the flag pytest aborts the session with "Interrupted: 3 errors during collection" and writes NO JUnit XML — the job would have failed every night for every PR while reporting nothing. With it the errors land on both sides, cancel in the diff, and a NEW collection error still surfaces. 2. `src/cli` must be pip-installed. `test_cli_*.py` imports `trinity_cli`, which is deliberately kept out of requirements-test.txt (an `-e ./src/cli` line there breaks GitHub's dependency-graph updater). tests/setup-env.sh installs it on demand; CI has to do the same or those modules silently never run — the very no-coverage this issue is about. 3. A missing JUnit must fail the job LOUDLY. `diff-pytest-failures.py` is fail-closed on a missing input, so a stack that never booted would have been reported to the author as "this PR introduced regressions" — an infrastructure failure wearing a PR's name, which is #1941's exact class. Also inherited from #1941: full-depth fetch (two shallow sides share no ancestor and every PR reads as conflicting) and a conflict verdict gated on actual unmerged paths. Teardown (AC 4): `docker compose down -v` between the two sides so head never inherits base's agents, volumes or DB, and the runner VM is destroyed afterwards regardless — the instance is disposable by construction. Runtime (AC 5): 45-minute ceiling, expected ~15-20. The escalation if it is hit is written into the workflow header — shard by marker or reduce the PR limit BEFORE raising the ceiling. `tests/run-core.sh` is untouched and remains the supported local path (AC 6); a guard test asserts the two keep defining the same suite. Related to #1896 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The new workflow shipped `python-version: '3.11'` while every image is `FROM python:3.13`. #1891's parity guard caught it before merge — and this is exactly the case its docstring predicts: "a seventh workflow added next month with a stale pin has to fail this test, or the guard only documents today's drift instead of closing the class." It matters more here than in a unit job: this suite boots real containers, so running the client on 3.11 against 3.13 services reintroduces the stdlib-removal blind spot (`crypt`, `audioop`) that #1891 exists to close. Related to #1896
1d9d4c5 to
d7938b6
Compare
|
Rebased onto current Conflicts were in Local verification after the rebase is in the individual runs; no source conflicts, only the registry. Ready for review. |
obasilakis
left a comment
There was a problem hiding this comment.
This is a well-built workflow and the AC coverage is complete — stack via start.sh rather than a bespoke compose path, nightly-gated, base-vs-head JUnit attribution, down -v between sides, a stated budget with a written escalation ladder, run-core.sh untouched. 13 guard tests, all passing. The #1941 lessons are applied pre-emptively (fetch-depth: 0, no --depth on the PR-head fetch, the unmerged-path gate), which is good to see land in a second workflow before the first one has even merged. The #1891 Python pin catching this file at '3.11' is that guard doing exactly its job.
Two blocking items, both small.
Blocking 1 — the false ✅ "clean", filed as #2029
Filed separately because it spans three workflows rather than just this PR: #2029.
Two paths reach it here. The merge infra branch (:177) exits without writing merge_conflict; the missing-XML branch (:278) exits after merge_conflict=false was written but before steps.diff runs. Either way Write status JSON is if: always() (:303), regression defaults to false (:307), and the script's final else (:376) renders:
✅ Live-instance integration suite clean when this PR is merged into
dev
:271-273 states the opposite:
Fail the job loudly instead; the
commentstep then posts nothing for this PR rather than something false.
It posts something false — the comment job is if: always() and never inspects needs.test.result.
This one is the worst of the three instances. backend-unit-nightly.yml:301 guards creation with } else if (status.regression || status.merge_conflict), so a false clean there can only overwrite an existing sticky. Here :396 is a bare } else { → createComment unconditionally, and this is a new workflow where no PR has a sticky yet. The first bad night posts a fresh green tick to up to 20 PRs.
Fix in #2029 — refuse to write the status file when the verdict is unknown, so the comment job skips that PR.
Blocking 2 — a secret in the job that runs untrusted fork code
:196
ANTHROPIC_API_KEY: ${{ secrets.E2E_ANTHROPIC_API_KEY || 'placeholder' }}That env is on the step that runs ./scripts/deploy/start.sh and the pytest suite from the merged PR tree — :234 switches to head-side before boot_stack, so it is arbitrary shell from the PR, not just test files. schedule exposes secrets to fork PRs (unlike pull_request), and discover enumerates them: #2009 is an open fork PR targeting dev right now.
This breaks the invariant the header says is inherited verbatim. backend-unit-nightly.yml's only secret is GITHUB_TOKEN in discover — its test job is genuinely credential-free. This one isn't.
The guard reads as if it covers this but doesn't. test_the_job_running_pr_code_has_no_write_token (:158-161) asserts only on the permissions: block. Token scope is correct; a secrets.* reference in the same job is unguarded — which is precisely how this would survive review. Extending that test to assert no secrets. other than GITHUB_TOKEN appears in the test job would close it.
To be straight about severity: E2E_ANTHROPIC_API_KEY does not exist on the repo today (gh secret list shows seven secrets, not that one), so the expression resolves to 'placeholder' and nothing is currently exposed. The problem is that the workflow is designed around that secret existing, and whoever adds it will not re-review this file. Either make the design fork-safe (have discover skip isCrossRepository PRs) or commit to the placeholder and skip-mark the tests that need a real key — which is the AC below anyway.
Non-blocking
1. The "pre-merge" stash comment names the wrong branch. :122-128 says "Workspace is unmodified dev at this point", but the checkout at :117 has no ref: and this repo's default branch is main, so a scheduled run copies the trusted diff script from main. The security property holds (still trusted, still pre-merge), but the script can be a release behind dev's. backend-unit-nightly.yml pins ref: dev explicitly — worth matching, and it would make the comment true.
2. || 'placeholder' silently degrades coverage. With no key, agent-dependent tests fail on both sides and cancel out in the diff, so the job reports clean while a chunk of the suite is inert. #1896's own AC asks to "confirm nothing requires a real external key … or the affected tests must be skip-marked rather than given credentials in CI" — the skip-marking isn't there. Given this issue is specifically about tests that gate nothing, inheriting a silently-inert slice seems worth closing here rather than later. It also interacts with Blocking 2: skip-marking is the fork-safe answer.
3. Fleet runtime. 20 PRs at max-parallel: 3 and ~15-20 min each is roughly 2 hours wall-clock. Fine for a nightly — noting it only because the escalation ladder at :34-39 is written per-PR and the fleet total is the number that will actually bite.
The directory-based membership (--ignore=unit --ignore=process_engine, matching run-core.sh:22) is the detail that makes this survive #1895, and the note explaining why it therefore didn't need to wait for #1895 is right. --continue-on-collection-errors being justified with the actual count of currently-broken imports, rather than added defensively, is the kind of thing that makes this reviewable.
…r computed (#2029) Same defect as the backend nightly, and this workflow reaches it by design: the missing-JUnit guard added here exits 1 when a stack never booted, `Write status JSON` still runs under `if: always()`, the unset merge output stringifies to a clean verdict, and the sticky comment says ✅ for a suite that never ran. The comment above that guard claimed "the `comment` step then posts nothing for this PR rather than something false". That was not true when I wrote it — it posts something false. Corrected in place rather than quietly deleted, because a comment asserting a property the code does not have is worth more as a recorded mistake than as a tidy line. The status step now refuses to write when the merge verdict is unknown, and only defaults `regression` to false when the merge actually conflicted — the one case where the diff legitimately never ran. The comment job enumerates the status files that exist, so a missing one leaves that PR's sticky untouched. 3 guard tests, each mutation-verified. Refs #2029 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
~93 of the
test_*.pyfiles intests/root need a running Trinity — the fixtures create and delete real agents againstTRINITY_API_URL— and no CI job ran them. They gated nothing.integration-nightly.ymlis that home. It composes what already exists rather than inventing anything:scripts/deploy/start.shfor the stack (asfrontend-e2e.ymldoes), thediscover → test → commentsplit and sticky-comment machinery frombackend-unit-nightly.yml, anddiff-pytest-failures.pyfor base-vs-head attribution.It did not need to wait for #1895
The issue's Technical Notes open with "Sequencing: land #1895 first" — but
tests/run-core.sh:22already defines this suite directory-based:pytest -m "not slow" --ignore=unit --ignore=process_engineThe workflow uses the same formulation, so every file #1895 relocates leaves this job's scope automatically, with no workflow edit. There is no "~60" target to chase and no file list to drift — which is also the AC that keeps this from rotting.
Why nightly (AC 2)
The suite mutates its target and needs ~3-5 min on top of a ~2-3 min boot, twice (base and head). In the merge path that is ~10 min on every backend PR plus a mutating flake surface in front of every merge. Nightly also gets the base-vs-head attribution the ACs require for free — that tooling exists for exactly this shape.
Three things measured, not assumed — each would have shipped this broken
--continue-on-collection-errorsis load-bearing. Three root modules currently fail to import (twogit-syncfiles wantingutils.credential_sanitizer, plustests/integration's conftest). Without the flag pytest aborts withInterrupted: 3 errors during collectionand writes no JUnit XML at all — the job would have failed every night, for every PR, reporting nothing. With it, the errors land on both sides, cancel in the diff, and a new collection error still surfaces. I found this by actually running the collection, not by reading the invocation.src/clihas to be installed.test_cli_*.pyimportstrinity_cli, deliberately kept out ofrequirements-test.txt(an-e ./src/cliline breaks GitHub's dependency-graph updater —tests/setup-env.shdocuments it). Without the on-demand install those modules fail to import and their tests silently never run — precisely the no-coverage this issue exists to end. That accounted for 2 of the original 5 collection errors.A missing JUnit must fail the job loudly.
diff-pytest-failures.pyis fail-closed on a missing input, so a stack that never booted would have been reported to the author as "this PR introduced regressions" — an infrastructure failure wearing a PR's name. That is bug(ci): nightly unit-suite reports a false merge conflict on every PR — shallow fetch on both sides leaves no common ancestor #1941's exact class, so the job now checks both XMLs exist and errors out instead of posting a false verdict.Also inherited from #1941, since this is a second workflow that merges: full-depth fetch (two shallow sides share no ancestor and every PR reads as conflicting), and a conflict verdict gated on actual unmerged paths.
AC coverage
scripts/deploy/start.sh, runs the live root suiteworkflow_dispatch(with a single-PR input), never onpull_requestregression difftooling the unit job usesdocker compose down -vbetween sides so head never inherits base's agents/volumes/DB; the runner VM is destroyed afterwards regardless, which is what makes the instance disposabletests/run-core.shuntouched; a guard asserts the two keep defining the same suiteGuards
13 tests in
test_1896_integration_nightly_workflow.py, in the shape oftest_1941_nightly_merge_depth.py— they pin the properties whose absence is invisible until the night it matters, including the two load-bearing flags above and the security split (the PR-code job holds onlycontents: read; the write-scopedcommentjob never checks out PR code).What I could not verify
The workflow has never executed — it is nightly and its first real run is after merge. YAML parses, every
runblock passesbash -n, the diff script's self-test passes and its CLI contract matches, and the pytest invocation was run locally to the point of collection. The stack boot itself is copied verbatim fromfrontend-e2e.yml, which runs green today. First-run risk is real;workflow_dispatchwith apr_numberinput exists so it can be smoke-tested against one PR on demand rather than waiting for 06:30 UTC.Known caveat, not introduced here: the 3 pre-existing collection errors above are real breakage in the root suite. They are exactly what "no CI job runs these" produces, and they belong to the #1895/#1896 cleanup rather than to whichever PR this job runs against first.
Related to #1896
🤖 Generated with Claude Code