Skip to content

fix(tests): the journey model gate asks the instance, not the pytest host (#2812) - #2837

Merged
vybe merged 2 commits into
devfrom
fix/2812-journey-model-gate
Sep 16, 2026
Merged

vybe merged 2 commits into
devfrom
fix/2812-journey-model-gate

Conversation

@trinity-ability

@trinity-ability trinity-ability commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Summary

J03's first turn and J10's "I can read what they said" / "every fan-out subtask completes" gated on os.getenv("ANTHROPIC_API_KEY") in the pytest process — the harness host, not the instance under test. A stack whose agents authenticate by subscription (SUB-003) has no such variable on the host yet answers normally, so those journeys skipped permanently and invisibly (the reason is allowlisted in tests/harness/audit_skips.py).

A gate that cannot fail is worse than no gate: #2336's per-PR journey-smoke and #2350's merge-enforced Journey Impact declaration were both green while asserting nothing about a real model answer.

The gate is a composite of two instance-side reads

# Read Answers
1 auth_mode from GET /api/subscriptions/agents/{name}/auth which credential the callee is routed to
2 claude_auth_configured from GET /api/settings/feature-flags whether the instance actually holds one (non-empty platform key OR a registered subscription)

Both must hold. Neither reads the pytest host's environment; neither discloses a credential value. J10 passes B — the agent that has to produce the words — not the caller.

Three shapes, three different failures — all now recorded

The first push of this PR failed journey-smoke, and the failure was this fix's own assumption. Keeping the history visible because each shape fails in a different direction:

Gate shape Failure it produces
ANTHROPIC_API_KEY on the pytest host skips forever on a subscription stack — the original #2812 defect
GET /api/subscriptions non-empty false failure when a fresh ephemeral agent never gets a subscription assigned (#2812 rejects this by name)
auth_mode alone false failure on the credential-free CI stack

That third one is why the composite exists. get_agent_auth_mode derives purely from DB state, and use_platform_api_key is a per-agent routing flag — not evidence a key exists. .env.example ships ANTHROPIC_API_KEY= empty and CI copies it verbatim, so a fresh agent reports auth_mode="api_key": routed to a platform key that isn't there. Read 2 is what separates "configured to use a credential" from "a credential exists".

Residual, stated rather than hidden

A credential that is present but invalid (a revoked key, or the literal ANTHROPIC_API_KEY=placeholder, which the backend does not special-case) passes both reads, so the journey runs and fails rather than skipping. The old host-side gate vetoed that sentinel by string comparison; an instance-side gate cannot see the value and should not. This tier's doctrine is that a stack which cannot deliver the promise is a finding, not a silent pass. Only a real turn could close it — #2812's other candidate signal (a session-scoped probe), a heavier change than this gate warrants.

Acceptance criteria

  • AC 1 — one shared helper in tests/journeys/conftest.py, no per-file os.getenv gate
  • AC 2 — decision reflects the instance, per callee plus instance credential state
  • AC 3 — still skips with the allowlisted reason where agents cannot answer; runs on a subscription-only stack
  • AC 4docs/testing/STRATEGY.md item 11 names all three rejected shapes and the failure each produces

Test Plan

Not done

tests/registry.json entries for the new test file. The registry is ~32% complete (255 of 789 unit files) and unenforced, and it is a hot file (40 commits this cycle) — two release-day branches editing it would conflict by construction. Worth a /update-tests sweep after the cut.

Release context

MUST-LAND item 2 of 7 in the 0.9.5 freeze work order. Fixes #2812

🤖 Generated with Claude Code

sim and others added 2 commits September 16, 2026 09:43
…host (#2812)

J03's first turn and J10's "I can read what they said" / "every fan-out subtask
completes" gated on `os.getenv("ANTHROPIC_API_KEY")` in the pytest process.
That is the harness host, not the instance under test. A stack whose agents
authenticate by subscription (SUB-003) has no such variable on the host yet
answers normally, so those journeys skipped PERMANENTLY — and invisibly,
because the reason is allowlisted in `tests/harness/audit_skips.py`.

A gate that cannot fail is worse than no gate: #2336's per-PR journey-smoke and
#2350's merge-enforced Journey Impact declaration were both green while
asserting nothing about a real model answer.

One helper now decides for both files (AC 1):
`conftest.skip_unless_agent_can_answer` reads the CALLEE's own `auth_mode` from
`GET /api/subscriptions/agents/{name}/auth` — "what the callee will actually
have" (AC 2) rather than what the instance has somewhere. J10 passes B, the
agent that has to produce the words, not the caller.

`GET /api/subscriptions` being non-empty was rejected for the reason #2812
gives: it describes the instance, not the callee, and would turn today's
permanent skip into a false FAILURE when a freshly created ephemeral agent
never gets a subscription assigned. Asking the agent cannot make that mistake,
and the endpoint reports the MODE, never the value.

The skip reason keeps the verbatim substring `journey needs a real provider
key`, so it stays allowlisted (AC 3), and now names the agent and its mode — so
a permanent skip is diagnosable from the run output, the property that was
missing. A collection-time `skipif` could not do this: it cannot ask a live
stack about an agent that does not exist yet, so the gate moved into the tests.

Deliberate behaviour change, called out for review rather than discovery: a
stack advertising a credential that cannot actually answer (the classic case
being a literal `ANTHROPIC_API_KEY=placeholder`, which the backend does not
special-case) now FAILS instead of skipping. The old host-side gate vetoed that
sentinel by string comparison; an instance-side gate cannot see the value and
should not. This tier's doctrine is that a stack which cannot deliver the
promise is a finding, not a silent pass.

`docs/testing/STRATEGY.md` gains acceptance item 11 stating how the gate
decides and naming both rejected shapes (AC 4).

Tests: `tests/unit/test_2812_journey_model_gate.py` — 10 tests driving the
helper with a fake client, so the gate's logic is covered in the unit tier with
no live stack. Covers both working modes, the not-configured skip, the real
`audit_skips` matcher run over the real message, three unreadable-endpoint
degradations, that the named agent is the one asked about, and per-agent
caching. The regression pin matches over the AST, not source text: the prose in
these files necessarily names the variable to explain why it is no longer
consulted — once inside a docstring, which no comment-prefix check can see.
Mutation-verified: re-adding a host read turns it red, removing it green.

Not done: registry.json entries for the two new test files this work adds.
The registry is ~32% complete and unenforced, and it is a hot file (40 commits
this cycle); two release-day branches editing it would conflict by
construction. Worth a `/update-tests` sweep after the cut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t the route (#2812)

journey-smoke went red on the first push and the failure was the fix's own
assumption. `get_agent_auth_mode` derives PURELY from DB state, and
`use_platform_api_key` is a per-agent ROUTING FLAG — not evidence that a key
exists. `.env.example` ships `ANTHROPIC_API_KEY=` empty and CI copies it
verbatim, so on the credential-free stack a fresh agent still reports
`auth_mode="api_key"`: it is configured to USE the platform key, there just
isn't one. Gating on that alone ran the three keyed journeys against a keyless
stack and failed them — the #2812 defect pointing the other way.

`auth_mode` answers "which credential source is this agent routed to", not
"can it answer". The gate is now a composite of two instance-side reads:

  1. the callee's own `auth_mode` (`subscription` / `api_key` / `not_configured`)
  2. `claude_auth_configured` from `GET /api/settings/feature-flags` — a
     non-empty platform Anthropic key OR any registered subscription, the one
     definition behind the flag and ent#582's first-credential check

Both must hold. Neither reads the pytest host's environment; neither discloses
a credential value. The #2812 case still works: a subscription-authenticated
stack has no `ANTHROPIC_API_KEY` anywhere yet reports
`claude_auth_configured=True`, so its journeys run.

Residual unchanged and still stated: a credential that is present but INVALID
(revoked key, or the literal `placeholder`, which the backend does not
special-case) passes both reads, so the journey runs and fails rather than
skipping. Only a real turn could close that, which is #2812's other candidate
signal and a heavier change than this gate warrants.

Tests: 13 now (up from 10). Two new pins carry the lesson — the exact
journey-smoke combination (routed to `api_key`, instance holds nothing) must
SKIP, and the subscription-stack case #2812 exists to fix must still RUN.
Mutation-verified: deleting the instance half turns the regression pin red.
`docs/testing/STRATEGY.md` item 11 now names all three rejected shapes with the
failure each one produces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: batch validated on train/20260916-0919 (train PR #2839)

@vybe
vybe merged commit daf6540 into dev Sep 16, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants