Skip to content

test(forge): pin the one-shared-Linear-TokenSource invariant (RIG-3135) - #1073

Merged
trunk-io[bot] merged 2 commits into
mainfrom
compass-forge/rig-3135-shared-tokensource
Sep 11, 2026
Merged

test(forge): pin the one-shared-Linear-TokenSource invariant (RIG-3135)#1073
trunk-io[bot] merged 2 commits into
mainfrom
compass-forge/rig-3135-shared-tokensource

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Closes RIG-3135. Test-only; no production behaviour changes.

What was unguarded

The forge Linear notify lane and the Linear write coordinate must ride exactly
ONE linearagent.TokenSource (DEC-4's one-instance rule). Linear revokes a
client-credentials app's tokens when its scope set changes, and the mint
singleflight coalesces only WITHIN an instance — so two sources are two mints
racing one credential, each revoking the other's live token.

That was held by code inspection only. The gating branches (nil / error /
partial-warn) and each consumer's nil-or-non-nil acceptance were tested, but
nothing asserted the same instance reaches both sinks, so a refactor building
a source per call site passed CI.

Why this needed a seam at all

The sibling GitHub invariant is easy: both lanes share a whole *forge.GitHub,
so TestForgeLanesShareOneBudgetGate compares client pointers. Linear is not
that shape — buildLinearNotifyLane and buildForgeWriteService each build
their OWN *forge.Linear, so client identity proves nothing and the only
shared object is the token field, which is unexported.

Adds Linear.TokenSourceForTest, a read-only accessor. It mirrors the existing
forgeNotifyLane.reader recorded-for-tests pattern (same comment shape, same
"production reads it never" note). RIG-3135 assumed this would need an
injectable OAuth endpoint or a pgtest e2e with an httptest OAuth server;
neither turned out to be necessary, because forge.TokenSource is already an
interface and both builders accept an already-built source.

The test

TestForgeLinearLanesShareOneTokenSource reads the source each BUILDER
threaded into the client it produced — notifyLane.reader, and the registry's
resolved author — never a handle the test holds. A builder that accepted the
shared source and then minted its own would satisfy every existing test and
fail only this one. It also asserts the two clients are distinct objects, so
the two arms cannot be one object read twice.

Mutation-proved, both arms independently:

mutation result
write coordinate mints its own source fails the write arm only
notify lane mints its own source fails the notify arm only

Verification

go build -tags unix ./... OK · go vet -tags unix ./internal/forge/... ./server/... OK ·
go test -tags unix ./internal/forge/... ./server/... both ok · gofmt clean ·
moon ci 32 actions, 0 failed.

Review

0 high, 1 medium, 4 low. The medium was mine and it is the same defect I
shipped one PR ago: I asserted that two TokenSource instances each revoke the
other's live token. The documented trigger is a scope-set CHANGE, and
tokenScope is a pinned const, so both sources mint identical scope and that
trigger cannot fire. The frozen record says something weaker and truer —
"whether concurrent same-scope mints from independent instances coexist is
unverified — one shared instance removes the question entirely"
(compass-forge-app-credentials, T4). Reworded to match, because a reader who
discovers same-scope mints do not cross-revoke would conclude the sharing is
unnecessary and "fix" it.

Also corrected the citation (the directive is T4's text, not DEC-4), dropped a
dead nil-guard, and documented why the nil store/hub/board args are safe.

Took the optional acceptance bullet too. A fourth arm now asserts the GitHub
author role IS the primaryClient the builder was passed, mutation-proved like
the others: registering a freshly-built client as author fails that arm alone.
All four arms are individually mutation-proved.

Verified after the fixes: go build OK, go vet OK, both packages ok,
gofmt clean.

The forge Linear notify lane and write coordinate MUST ride exactly one
linearagent.TokenSource (DEC-4). Linear revokes a client-credentials app's
tokens when its scope set changes, and the mint singleflight coalesces only
WITHIN an instance, so a second source is two mints racing one credential --
each revoking the other's live token. That invariant was held by code
inspection only: a refactor building a source per site passed CI.

Unlike the GitHub lanes, which share a whole *forge.GitHub and are pinned by
TestForgeLanesShareOneBudgetGate, the two Linear sinks each build their OWN
*forge.Linear. Only the source inside is shared, so client identity proves
nothing and forge.Linear.token is unexported. Adds a read-only
TokenSourceForTest accessor -- the smallest seam that makes it observable,
matching the existing forgeNotifyLane.reader recorded-for-tests pattern, and
no injectable OAuth endpoint or pgtest e2e as the issue had assumed.

The test reads the source each BUILDER threaded into the client it produced,
never a handle the test holds, and asserts the two clients are distinct
objects so the pair is not trivially true. Mutation-proved against the exact
regression, twice: minting a separate source at the write coordinate fails the
write arm, and at the notify lane fails the notify arm.

Refs RIG-3090.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@trunk-io

trunk-io Bot commented Sep 11, 2026

Copy link
Copy Markdown

😎 Merged successfully - details.

@linear-code

linear-code Bot commented Sep 11, 2026

Copy link
Copy Markdown

RIG-3135

RIG-3090

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-forge-rig-3135-share.compass-eng-docs.pages.dev

Deployed from compass-forge/rig-3135-shared-tokensource at 031e67e.

…uthor hop (RIG-3135)

Review found the comments asserting a mechanism the frozen record marks
unverified, which is the same defect I shipped one PR ago.

I wrote that two TokenSource instances each revoke the other's live token.
The documented trigger is a scope-set CHANGE (tokenScope const comment in
internal/linearagent/client.go), and tokenScope is pinned, so both sources
mint IDENTICAL scope and that trigger cannot fire here. What the record
actually says is weaker and is the real reason: 'whether concurrent
same-scope mints from independent instances coexist is unverified -- one
shared instance removes the question entirely'
(docs/designs/server/compass-forge-app-credentials/design.md, T4). Stating
an unverified hazard as certain invites a future reader to 'fix' the sharing
once they discover same-scope mints do not in fact cross-revoke.

Also corrects the citation: the one-instance directive is T4's task text,
not DEC-4 (which is the clean-cutover/no-PAT-fallback decision).

Drops a dead guard -- NewTokenSource cannot return nil -- and records why the
nil store/hub/board args are safe, mirroring the sibling budget test's note.

Closes RIG-3135's optional secondary bullet with a fourth arm: the GitHub
author role IS the primaryClient the builder was passed. Mutation-proved like
the others -- registering a freshly-built client as author fails that arm
alone.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka marked this pull request as ready for review September 11, 2026 15:41
@trunk-io
trunk-io Bot merged commit cdedf15 into main Sep 11, 2026
17 checks passed
@trunk-io
trunk-io Bot deleted the compass-forge/rig-3135-shared-tokensource branch September 11, 2026 18:35
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