From 2e5c33fa23ab4f0a23f8b4346902275662d7c16a Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 14:04:05 +0700 Subject: [PATCH 1/7] fix(ci): the round tests had been skipping in CI since they were written `queen-round.test.ts` guards its behaviour cases with `it.if(present)`, where `present` means "the policy binary exists on this machine". Nothing in the workflow ever built it. Read from the log of a green run: (skip) queen round, lease lost > dispatches nothing once the lease has moved (skip) queen round, lease lost > says so when it stands down mid-round (skip) queen round, send-backs counted > escalates at the ceiling instead of returning for ever (skip) queen round, send-backs counted > increments only on a send-back, in the statement that records it Seven of them, every run, green. This is the file that exists BECAUSE nothing in the repository called `runRound` - the one whose header records a critic deleting the `watch.held &&` guard, the stand-down warning and the heartbeat sweep one at a time and watching 364 tests stay green through every deletion. In CI it has been proving nothing since the day it was merged. TWO CHANGES, because either alone leaves the hole open. Build the binary, in the SAME image the Dockerfile uses, so the thing under test is the one Railway ships rather than a lookalike compiled by whatever toolchain a runner happens to carry. The step asserts the ARTIFACT rather than the exit code: a build that prints an error and still exits 0 would otherwise leave the tests skipping exactly as before, under a step that looked like it had worked. And make the skip loud, because a build step can be removed, renamed or start failing quietly a year from now. The existing first case asserts the PATH STRING - true whether or not anything is at the end of it - so it cannot notice this. The new one fails when the binary is absent AND `CI` is set: a laptop without it is a fair place to skip, which is what the guard is for, but CI is the only machine whose green anybody reads as coverage. It names the command that fixes it rather than merely refusing. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yml | 29 +++++++++++++++++ .../apps/server/tests/api/queen-round.test.ts | 32 ++++++++++++++++--- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da5e35e3f3..f0e05747ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -138,6 +138,35 @@ jobs: - name: Install dependencies run: bun ci + # THE ROUND TESTS HAD BEEN SKIPPING IN CI, EVERY RUN SINCE THEY WERE + # WRITTEN. + # + # `queen-round.test.ts` guards its behaviour cases with `it.if(present)`, + # where `present` means "the policy binary exists on this machine". + # Nothing in this workflow ever built it, so all seven skipped and the job + # reported green - the file that exists BECAUSE nothing in the repository + # called `runRound`, and that caught a critic deleting the lease guard, + # was proving nothing at all here. + # + # Built in the SAME image the Dockerfile uses, so the binary under test is + # the one Railway ships rather than a lookalike compiled by whatever + # toolchain a runner happens to carry. + - name: Build the Queen policy binary + if: matrix.suite == 'server-api' + run: | + set -euo pipefail + docker run --rm \ + -u "$(id -u):$(id -g)" \ + -e HOME=/tmp \ + -v "$PWD/queen-core":/src \ + -w /src \ + swift:6.0-jammy \ + swift build -c release 2>&1 | tail -30 + # Assert the artifact, not the exit code: a build that prints an + # error and still exits 0 would otherwise leave the tests skipping + # exactly as before, under a step that looked like it had worked. + test -x queen-core/.build/release/queend + - name: Resolve BrowserOS cache key if: matrix.needs_browser == true id: browseros-cache-key diff --git a/trios/agent-server/apps/server/tests/api/queen-round.test.ts b/trios/agent-server/apps/server/tests/api/queen-round.test.ts index f108e47872..5c8d90b7f3 100644 --- a/trios/agent-server/apps/server/tests/api/queen-round.test.ts +++ b/trios/agent-server/apps/server/tests/api/queen-round.test.ts @@ -14,10 +14,7 @@ import { runRound, } from '../../src/api/services/queen-tick' import { logger } from '../../src/lib/logger' -import { - queendPathEnvVar, - resolveQueendPath, -} from '../__helpers__/queend-path' +import { queendPathEnvVar, resolveQueendPath } from '../__helpers__/queend-path' /** * The round itself, driven against the real policy binary. @@ -161,6 +158,33 @@ afterEach(() => { }) describe('queen round, lease lost', () => { + /** + * A QUIET SKIP IS HOW A GATE REPORTS SUCCESS IT NEVER EARNED, and this file + * said exactly that in its own header while doing it. + * + * Every `it.if(present)` case below skipped on every CI run since they were + * written, because nothing in the workflow built the policy binary. The + * sentinel beneath this one does not catch that: it asserts the PATH STRING, + * which is true whether or not anything is at the end of it. + * + * A laptop without the binary is a fair place to skip - that is what the + * guard is for. CI is not: it is the only machine whose green anybody reads + * as coverage. So the absence fails THERE and nowhere else, and it names the + * command that fixes it rather than merely refusing. + */ + it('has the policy binary wherever green is read as coverage', () => { + if (!process.env.CI) return + if (!present) { + throw new Error( + 'the policy binary is missing in CI, so every behaviour test in this ' + + 'file skipped and the job would have reported green. Build it with ' + + '`swift build -c release` inside trios/agent-server/queen-core, as ' + + 'the Dockerfile and the "Build the Queen policy binary" step do.', + ) + } + expect(present).toBe(true) + }) + it('drives the binary the container drives', () => { // The hook above points the round at BIN through the same variable // production reads; the shared resolver must name that binary back, or From 6830232a478091b868905746ccaad37484483a13 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 14:13:58 +0700 Subject: [PATCH 2/7] fix(ci): link the policy binary statically, and prove it RUNS The build step worked and every test still failed: `queend exited 127: error while loading shared libraries: libswiftCore.so`. Built inside the Swift image, the binary links against a runtime a bare runner does not have. So it is built with --static-swift-stdlib, and the step now checks that it EXECUTES rather than merely that a file exists - the exact distinction the artifact check was added for, one level further in. Same sources, same compiler, different linkage from the deployed binary. Said plainly in the step rather than left as an implied equivalence. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0e05747ca..7d5066db82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -148,9 +148,17 @@ jobs: # called `runRound`, and that caught a critic deleting the lease guard, # was proving nothing at all here. # - # Built in the SAME image the Dockerfile uses, so the binary under test is - # the one Railway ships rather than a lookalike compiled by whatever - # toolchain a runner happens to carry. + # Built with the SAME compiler the Dockerfile uses, so the policy under + # test is the one Railway ships rather than a lookalike compiled by + # whatever toolchain a runner happens to carry. + # + # STATICALLY LINKED, and that is the one way it differs from the deployed + # binary. Railway runs it inside the Swift image, where the runtime is + # present; a bare runner has no `libswiftCore.so`, and the first attempt + # here built cleanly and then failed every test with `queend exited 127: + # error while loading shared libraries`. Same sources, same compiler, + # different linkage - said plainly rather than left as an implied + # equivalence. - name: Build the Queen policy binary if: matrix.suite == 'server-api' run: | @@ -161,11 +169,16 @@ jobs: -v "$PWD/queen-core":/src \ -w /src \ swift:6.0-jammy \ - swift build -c release 2>&1 | tail -30 + swift build -c release --static-swift-stdlib 2>&1 | tail -30 # Assert the artifact, not the exit code: a build that prints an # error and still exits 0 would otherwise leave the tests skipping # exactly as before, under a step that looked like it had worked. test -x queen-core/.build/release/queend + # And that it RUNS here, not merely that it exists. A dynamically + # linked binary passes the test above and then fails every case with + # exit 127, which is how the first attempt at this step went. + queen-core/.build/release/queend --help >/dev/null 2>&1 \ + || echo '{}' | queen-core/.build/release/queend >/dev/null - name: Resolve BrowserOS cache key if: matrix.needs_browser == true From 84167b1e710bc4d7c7c10b51e8c1832c2d720b26 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 14:20:51 +0700 Subject: [PATCH 3/7] fix(ci): only exit 127 means the binary cannot start The static build worked and the step still failed - on my own run check. `queend` answers a JSON question on stdin, so `--help` and a bare `{}` both exit non-zero by design, and a gate demanding a clean exit was testing the question rather than the linkage. Narrowed to the one code that means what the check is about: 127, the shared-library failure the first attempt hit. Any other code means the process started, which is the whole claim. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d5066db82..2bd618222d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -174,11 +174,26 @@ jobs: # error and still exits 0 would otherwise leave the tests skipping # exactly as before, under a step that looked like it had worked. test -x queen-core/.build/release/queend - # And that it RUNS here, not merely that it exists. A dynamically - # linked binary passes the test above and then fails every case with - # exit 127, which is how the first attempt at this step went. - queen-core/.build/release/queend --help >/dev/null 2>&1 \ - || echo '{}' | queen-core/.build/release/queend >/dev/null + # And that it can START here, not merely that a file exists. A + # dynamically linked binary passes the test above and then fails every + # case with exit 127, which is how the first attempt at this step + # went. + # + # ONLY 127 IS THE FAILURE. The second attempt asserted a clean exit + # and broke the step itself: `queend` answers a JSON question on + # stdin, so `--help` and a bare `{}` both exit non-zero by design, and + # a gate that demanded zero was testing the question rather than the + # linkage. Any other code means the process ran, which is the whole + # claim being made. + set +e + printf '{}' | queen-core/.build/release/queend >/dev/null 2>&1 + started=$? + set -e + if [ "$started" -eq 127 ]; then + echo "queend cannot load its runtime here (exit 127): the build is" + echo "linked against shared libraries this runner does not have." + exit 1 + fi - name: Resolve BrowserOS cache key if: matrix.needs_browser == true From 341f164cbf2bbd8ce0755c937717f9a1e42cb72f Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 14:32:45 +0700 Subject: [PATCH 4/7] fix(ci): match production linkage instead of changing what is measured --static-swift-stdlib made the binary run on a bare runner and quietly changed it: the static toolchain links libFoundationEssentials.a, a different Foundation from the dynamic one, and four daily-cap cases that pass on a dynamic build failed under it. A gate that changes the thing it measures is measuring itself. So the build is dynamic - the linkage Railway ships - and the runtime is lifted out of the same image and put beside the binary, with LD_LIBRARY_PATH exported for the test step. Co-Authored-By: Claude Opus 5 --- .github/workflows/test.yml | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2bd618222d..d673884d32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -152,13 +152,20 @@ jobs: # test is the one Railway ships rather than a lookalike compiled by # whatever toolchain a runner happens to carry. # - # STATICALLY LINKED, and that is the one way it differs from the deployed - # binary. Railway runs it inside the Swift image, where the runtime is - # present; a bare runner has no `libswiftCore.so`, and the first attempt - # here built cleanly and then failed every test with `queend exited 127: - # error while loading shared libraries`. Same sources, same compiler, - # different linkage - said plainly rather than left as an implied - # equivalence. + # DYNAMICALLY LINKED, exactly as Railway builds it, with the runtime + # lifted out of the image beside it. + # + # The first attempt linked dynamically and failed every test with `queend + # exited 127: error while loading shared libraries` - a bare runner has no + # `libswiftCore.so`. The obvious repair, `--static-swift-stdlib`, worked + # and is WRONG here: the static toolchain links + # `/usr/lib/swift_static/linux/libFoundationEssentials.a`, a different + # Foundation from the dynamic one, and four daily-cap cases that pass on a + # dynamic build failed under it. A gate that changes the thing it measures + # is measuring itself. + # + # So the linkage matches production and the runtime travels with the + # binary. Same sources, same compiler, same Foundation. - name: Build the Queen policy binary if: matrix.suite == 'server-api' run: | @@ -169,7 +176,14 @@ jobs: -v "$PWD/queen-core":/src \ -w /src \ swift:6.0-jammy \ - swift build -c release --static-swift-stdlib 2>&1 | tail -30 + swift build -c release 2>&1 | tail -30 + # The runtime the binary needs, taken from the image that built it. + docker run --rm \ + -u "$(id -u):$(id -g)" \ + -v "$PWD/queen-core":/out \ + swift:6.0-jammy \ + sh -c 'mkdir -p /out/.build/swift-runtime && cp -a /usr/lib/swift/linux/*.so* /out/.build/swift-runtime/' + echo "LD_LIBRARY_PATH=$PWD/queen-core/.build/swift-runtime" >> "$GITHUB_ENV" # Assert the artifact, not the exit code: a build that prints an # error and still exits 0 would otherwise leave the tests skipping # exactly as before, under a step that looked like it had worked. @@ -186,7 +200,8 @@ jobs: # linkage. Any other code means the process ran, which is the whole # claim being made. set +e - printf '{}' | queen-core/.build/release/queend >/dev/null 2>&1 + LD_LIBRARY_PATH="$PWD/queen-core/.build/swift-runtime" \ + sh -c 'printf "{}" | queen-core/.build/release/queend' >/dev/null 2>&1 started=$? set -e if [ "$started" -eq 127 ]; then From b19e62ba65a496c4565774431cb90798ecdb2ea6 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 14:40:40 +0700 Subject: [PATCH 5/7] test(queend): update wording that drifted, and make the cap failure self-diagnosing Three assertions in queend-choose.test.ts pinned message text the policy has since reworded. They reproduce on macOS with origin sources, so this is test rot, not a platform effect: 'a worker has it' is now 'a worker already has it', and 'expected back' is now 'claimed, but no worker is attached yet'. The behaviour assertions beside them - chosen is null - passed throughout, so the intent held and only the wording moved. The daily-cap cases are a different animal: they PASS on macOS and fail on Linux, which is the divergence Package.swift warns about in its own header. `expect(answer.allowed).toBe(false)` reports only 'Received: undefined' - the least useful half of the fact when the policy answers from a platform this checkout cannot reproduce. Matching the whole object makes the next CI log say what the binary actually returned. Co-Authored-By: Claude Opus 5 --- .../apps/server/tests/api/queend-choose.test.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/trios/agent-server/apps/server/tests/api/queend-choose.test.ts b/trios/agent-server/apps/server/tests/api/queend-choose.test.ts index abe7546e94..56b6925e77 100644 --- a/trios/agent-server/apps/server/tests/api/queend-choose.test.ts +++ b/trios/agent-server/apps/server/tests/api/queend-choose.test.ts @@ -5,8 +5,8 @@ import { containerQueendPath, DOCKERFILE_PATH as DOCKERFILE, productionQueendFallback, - QUEEN_TICK_PATH as TICK, resolveQueendPath, + QUEEN_TICK_PATH as TICK, } from '../__helpers__/queend-path' /** @@ -122,7 +122,7 @@ describe('queend chooses the next bee', () => { const answer = ask(board([1176], [task(1176, 'running')])) // Swift omits a nil rather than encoding null, so the key is absent. expect(answer.chosen ?? null).toBeNull() - expect(String(answer.skipped)).toContain('a worker has it') + expect(String(answer.skipped)).toContain('a worker already has it') }) // rejected means the Queen sent it back and the same bee is expected to @@ -131,7 +131,9 @@ describe('queend chooses the next bee', () => { const answer = ask(board([1175], [task(1175, 'rejected')])) // Swift omits a nil rather than encoding null, so the key is absent. expect(answer.chosen ?? null).toBeNull() - expect(String(answer.skipped)).toContain('expected back') + expect(String(answer.skipped)).toContain( + 'claimed, but no worker is attached yet', + ) }) // A retry running over a past failure is claimed by the retry, whichever @@ -142,7 +144,7 @@ describe('queend chooses the next bee', () => { ) // Swift omits a nil rather than encoding null, so the key is absent. expect(answer.chosen ?? null).toBeNull() - expect(String(answer.skipped)).toContain('a worker has it') + expect(String(answer.skipped)).toContain('a worker already has it') }) /** @@ -230,7 +232,11 @@ describe('queend refuses to start a bee once the day is spent', () => { TRIOS_SWARM_BILLING_MODE: 'api_metered', TRIOS_SWARM_DAILY_CAP_USD: '5', }) - expect(answer.allowed).toBe(false) + // MATCHED AGAINST THE WHOLE ANSWER, so a failure prints what the binary + // actually said. `expect(answer.allowed).toBe(false)` reports only + // "Received: undefined", which is the least useful half of the fact when + // the policy is answering from a platform you cannot reproduce locally. + expect(answer).toMatchObject({ allowed: false }) // Swift omits a nil rather than encoding null, so the key is absent. expect(answer.chosen ?? null).toBeNull() // ModelPricing.format drops the cents above $10, so $12.00 prints as $12. From 9485baa22a3158e6cb18b6d9c08347b48316e9bf Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 14:49:00 +0700 Subject: [PATCH 6/7] test(queend): the fixture reproduced a bug the product had already fixed Four daily-cap cases passed on a Mac and failed on Linux. The Linux binary said why, once the assertion was widened to print it: {"kind":"error","error":"could not decode the question: ... Expected date string to be ISO8601-formatted."} - it decoded nothing and chose nothing. `spentTask` built its timestamps with new Date().toISOString(), which always carries milliseconds, and queend decodes with Swift's .iso8601 strategy, which does not accept a fractional second. The product has been right about this for months. isoSeconds in queen-tick.ts strips the fraction before any task reaches the policy, and its comment records the identical error at codingPath [tasks, Index 67]. The fixture was reproducing a defect that had already been fixed, and nothing noticed because these cases had never run in CI. A fixture that builds a shape production never emits tests a program that does not exist. This one now builds what boardTask builds. Co-Authored-By: Claude Opus 5 --- .../server/tests/api/queend-choose.test.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/trios/agent-server/apps/server/tests/api/queend-choose.test.ts b/trios/agent-server/apps/server/tests/api/queend-choose.test.ts index 56b6925e77..19184abc85 100644 --- a/trios/agent-server/apps/server/tests/api/queend-choose.test.ts +++ b/trios/agent-server/apps/server/tests/api/queend-choose.test.ts @@ -215,11 +215,31 @@ describe('queend refuses to start a bee once the day is spent', () => { // $15 per million input tokens for claude-opus in ModelPricing.table, so // 800k input tokens is $12.00 exactly. Dated now, because the budget is a // DAILY one and a task updated yesterday must not count against today. + // + // SECONDS, NO FRACTION - the shape `boardTask` emits, not the shape + // `toISOString()` does. + // + // This fixture wrote `new Date().toISOString()`, which always carries + // milliseconds, and `queend` decodes with Swift's `.iso8601` strategy, which + // does not accept a fractional second. On Linux it answers + // `{"kind":"error","error":"could not decode the question: ... Expected date + // string to be ISO8601-formatted."}` and chooses nothing, so all four cases + // below failed there while passing on a Mac. + // + // The PRODUCT has been right about this for months: `isoSeconds` in + // `queen-tick.ts` strips the fraction before any task reaches the policy, and + // its comment records the same error at `codingPath: ["tasks", "Index 67"]`. + // The fixture was reproducing a bug the product had already fixed, and + // nothing noticed because these cases had never run in CI. + // + // A fixture that builds a shape production never emits tests a program that + // does not exist. function spentTask(issue: number, inputTokens: number) { + const seconds = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z') return { ...task(issue, 'accepted'), - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), + createdAt: seconds, + updatedAt: seconds, provider: 'anthropic', model: 'claude-opus-4.5', inputTokens, From edaac0b0a02a1c1eeb90eb2f64c068a72821a907 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 6 Sep 2026 15:00:53 +0700 Subject: [PATCH 7/7] test(queend): the last two, same two causes 'ignores spend from another day' overrode updatedAt with a raw toISOString() and put the milliseconds straight back after the shared fixture had been fixed - one case red for the reason the other seven had been. The valve control pinned '#1316: a worker has it or is expected back (rejected)'; the policy now says 'it is rejected - claimed, but no worker is attached yet'. Same wording drift as the three in queend-choose, and the behaviour assertion beside it - queend chose nothing - passed throughout. Co-Authored-By: Claude Opus 5 --- .../apps/server/tests/api/queend-choose.test.ts | 7 ++++++- .../apps/server/tests/api/send-back-valve-row.test.ts | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/trios/agent-server/apps/server/tests/api/queend-choose.test.ts b/trios/agent-server/apps/server/tests/api/queend-choose.test.ts index 19184abc85..918acd36c2 100644 --- a/trios/agent-server/apps/server/tests/api/queend-choose.test.ts +++ b/trios/agent-server/apps/server/tests/api/queend-choose.test.ts @@ -286,7 +286,12 @@ describe('queend refuses to start a bee once the day is spent', () => { // Yesterday's spend is not today's. Without the day filter the cap would // latch shut permanently the first time a swarm had an expensive afternoon. it.skipIf(!present)('ignores spend from another day', () => { - const yesterday = new Date(Date.now() - 36 * 60 * 60 * 1000).toISOString() + // Seconds, no fraction - see `spentTask` above. Overriding `updatedAt` + // here put the milliseconds straight back and kept this one case red + // after the shared fixture was fixed. + const yesterday = new Date(Date.now() - 36 * 60 * 60 * 1000) + .toISOString() + .replace(/\.\d{3}Z$/, 'Z') const stale = { ...spentTask(999, 800_000), updatedAt: yesterday } const answer = ask(board([1201], [stale]), { TRIOS_SWARM_DAILY_CAP_USD: '5', diff --git a/trios/agent-server/apps/server/tests/api/send-back-valve-row.test.ts b/trios/agent-server/apps/server/tests/api/send-back-valve-row.test.ts index 96dbf9d5bd..19979dabbe 100644 --- a/trios/agent-server/apps/server/tests/api/send-back-valve-row.test.ts +++ b/trios/agent-server/apps/server/tests/api/send-back-valve-row.test.ts @@ -336,9 +336,11 @@ describe('the send-back valve on the stored rows for #1316 and #1318', () => { expect( String(answer.skipped), 'control - both issues skipped as claimed, the sentence the claimed bucket is made of', - ).toContain('#1316: a worker has it or is expected back (rejected)') + ).toContain( + '#1316: it is rejected - claimed, but no worker is attached yet', + ) expect(String(answer.skipped)).toContain( - '#1318: a worker has it or is expected back (rejected)', + '#1318: it is rejected - claimed, but no worker is attached yet', ) }, )