Compass forge integration testing
Status: Active Follow-on to: forge write path (RIG-2170, frozen) — this record ADDS the live-contract test tiers; it does not re-open that record.
Problem / Intent
Section titled “Problem / Intent”Every forge test in the tree is hermetic — go/internal/forge/github_test.go
drives the client through a stub http.RoundTripper (“scriptedRoundTripper
serves a queue of scripted responses (or a per-call func), recording each
request for assertions”, github_test.go:26-27), and the write-path record’s
frozen strategy is “differential-oracle pyramid per DL-174 (hermetic in-memory
reference — FakeProvider + httptest-backed clients — in the default gate”
(compass-forge-write-path/design.md:136-139). Hermetic tests prove our code
matches the contract we believe GitHub has; nothing proves we modeled that
contract correctly, and nothing detects GitHub drifting out from under our
model (field shapes, 422 bodies, the F1 reviewer≠author assumption). This
record adds the two missing tiers — a committed golden-fixture replay leg and
a live-credentials oracle leg — and settles where the oracle’s credentials
live in CI.
Approach
Section titled “Approach”Matt ruled the two-part decision (2026-08-18); this record captures and specifies it — it does not re-decide it.
Leg 1 — hermetic golden-fixture replay (proves OUR code matches the captured contract)
Section titled “Leg 1 — hermetic golden-fixture replay (proves OUR code matches the captured contract)”A plain, untagged Go test in go/internal/forge replaying committed GitHub
response fixtures from a new go/internal/forge/testdata/ directory (the
directory does not exist today — the package holds only ten .go files, no
testdata/) through the existing scriptedRoundTripper stub. Zero network,
zero credentials. Because it is untagged, it rides the compass-go battery:
moon’s affected detection schedules it on every forge-affected PR AND the
full main + nightly sweep (.github/workflows/ci.yml:25-26 “A pull request runs
moon ci :ci, which runs only the projects the PR affects”; the nightly
full-sweep trigger at ci.yml:62-66). No .github/workflows/ci.yml edit
and no YAML enumeration — the one-job rationale explicitly rejects “a
project list in a workflow is a second source of truth for something
.moon/workspace.yml already owns. That list goes stale silently”
(ci.yml:9-10), and leg 1 needs none: the new _test.go file is covered by
the test task’s **/*.go input glob (go/moon.yml:51-54 &go_sources —
'**/*.go', 'go.mod', 'go.sum'; consumed by test at
go/moon.yml:145-155).
One grounded correction to the “no moon.yml edit” assumption: the fixture
FILES are JSON, and **/*.go does not match them — a fixture-only change
(a -update regeneration commit) would schedule nothing on the affected-PR
path. T1 therefore adds '**/testdata/**' to the &go_sources anchor
(go/moon.yml:51-54). That is an inputs-glob edit in the project’s own
moon file — the sanctioned single source of truth the one-job rationale
defends — not the banned workflow-YAML project enumeration.
Fixtures regenerate via the standard Go golden pattern, but the capturing
run needs the live credentials — which live ONLY in Actions secrets, so a
local -update run is impossible. Regeneration therefore runs as a
workflow_dispatch lane that executes
go test -tags livegithub -run <update-test> -update IN CI (where the
secrets live) and opens a BOT PR with the rewritten testdata/ diff for
review. No human holds the PATs locally. This adds one bot-opens-a-PR
credential path (the same bot, scoped to opening PRs). The committed
fixtures are the reviewed contract snapshot.
Leg 2 — live-credentials oracle (proves GitHub’s contract has not drifted)
Section titled “Leg 2 — live-credentials oracle (proves GitHub’s contract has not drifted)”A //go:build livegithub-tagged Go test suite re-running the SAME scenarios
against a REAL throwaway GitHub repo under repo-scoped CI secrets (dedicated
test-only bot PATs), asserting the committed fixtures still match the live
responses. It mirrors the existing //go:build podman dogfood-e2e shape
exactly: the tag keeps it invisible to the moon battery’s go test ./...
(“It is build-tagged podman, so the moon go test ./... battery never
compiles it — this step is the ONLY thing that runs it”, ci.yml:704-705;
go/e2e/harness_test.go:1 //go:build podman), and a dedicated CI step runs
it with the capture-replay-exit discipline plus an assert-it-ran-not-skipped
guard (ci.yml:719-748 derives the skip string from the harness source so
“rewording the skip cannot leave this grep matching nothing and reporting
success”, ci.yml:729-730).
Cadence (Matt’s ruling): a REQUIRED check on forge-affected PRs AND nightly. Not nightly-only, not advisory.
The headline scenario is the F1 reviewer≠author assumption — the single most
load-bearing thing the oracle proves: the reviewer credential
(GITHUB_FORGE_REVIEWER_TOKEN) can APPROVE a PR authored by the author
credential (GITHUB_FORGE_TOKEN) on the throwaway repo, because “GitHub
rejects APPROVE and REQUEST_CHANGES from the PR’s own author with a 422 (only
COMMENT is allowed from the author …), and every Compass-authored PR has the
author-credential account as its author”
(compass-forge-write-path/design.md:277-280; the two-credential ruling at
:126-135: “GitHub holds TWO: the AUTHOR credential (GITHUB_FORGE_TOKEN,
serve.go:761-762) … and a DISTINCT REVIEWER identity
(GITHUB_FORGE_REVIEWER_TOKEN) for the submit_review arm”; ledgered as
DL-201, DECISIONS.md:145).
Credential separation. The oracle’s CI secrets are SEPARATE, test-only
bot PATs scoped to the throwaway repo — never the production server_only
declared secrets. Production forge credentials resolve through the declared-
secret path (go/server/serve.go:122-124 “SecretName is the declared
server_only secret NAME holding the forge token (default
GITHUB_FORGE_TOKEN; the VALUE never crosses config or a flag)”;
validateForgeSecret, serve.go:779-790; DL-052, DECISIONS.md:75). The
test PATs live only in GitHub Actions repo secrets, belong to a throwaway bot
account (plus one reviewer identity), and can be revoked with zero production
blast radius.
How the dedicated CI step avoids the stale-enumeration failure
Section titled “How the dedicated CI step avoids the stale-enumeration failure”The one-job design’s warning is about per-project lists in workflow YAML
(ci.yml:6-18). The live-oracle step is a single FIXED step, not a list: it
is keyed on one path filter (go/internal/forge/** + the workflow file
itself) computed in-workflow, it names one Go package
(./internal/forge/... with -tags livegithub), and there is nothing
per-project to enumerate or forget. Adding a second provider’s oracle
(Linear) extends the same single step’s tag/package, not a matrix. The
build-tag boundary is the same one the dogfood step and the pgtest step
already ride (“The suites are build-tagged pgtest, so the moon battery’s
go test ./... never compiles them — the folded step below is the only thing
that runs them”, ci.yml:42-44).
One honest caveat: the hardcoded go/internal/forge/** path glob is itself
a mini second source of truth — forge behavior affected from OUTSIDE that
path (e.g. go/server/serve.go wiring or a shared HTTP/token package) would
skip the oracle on that PR silently. That filter is a PR-COST optimization
only: push-to-main and the nightly run the step UNCONDITIONALLY, so a stale
filter delays detection by at most a day, never hides it — the same backstop
structure ci.yml:28-36 articulates for moon’s inputs globs.
A new secret-bearing required check (D2’s dogfood tier is unchanged)
Section titled “A new secret-bearing required check (D2’s dogfood tier is unchanged)”This is a STANDALONE new decision, not an amendment to D2. The CI gate
GAINS one secret-bearing live-oracle step; D2’s full-stack deterministic
tier stays exactly as decided — secret-free and per-PR. D2 lives in the
platform dogfood-e2e record, not a DECISIONS.md row: “D2 — Per-PR gate
cadence (Decision, Matt, 2026-08-05). The full-stack deterministic tier is
the per-PR gate” (compass-dogfood-e2e/design.md:815-830). That tier is
UNTOUCHED, and the ci.yml:702-704 no-secrets comment describes the DOGFOOD
step specifically and stays true of it.
What actually changes is the implicit repo-wide posture that D2 merely
instantiated — “the PR gate carries no secrets.” The forge oracle is a
SECOND, distinct per-PR step that DOES carry secrets; it neither modifies
D2’s tier nor rewrites its no-secrets comment. It lands as a NEW DL row for
this record’s decision (the frozen-record convention: a later record adds by
citation, never rewrites a frozen record). The ci.yml:702-704 comment may
gain a clause clarifying that its no-secrets scope is the dogfood tier (T3’s
diff touches that file anyway), but D2 itself is not re-opened.
The decided tradeoff (recorded, not re-opened). D2’s no-secrets clause was protecting two things: fork-PR secret safety and gate determinism. Matt accepted the costs for the strongest correctness guarantee:
- Determinism: a live-GitHub flake, outage, or rate-limit exhaustion can now red an otherwise-good forge PR. Mitigations (retry-on-rerun, a concurrency cap, tight scenario budget) reduce but cannot eliminate this; it is the accepted price of a standing drift oracle.
- Fork safety: compass is PUBLIC with 0 forks (verified via the GitHub
API, 2026-08-18:
"private": false, "forks_count": 0). Name the hole honestly: because the same-repo-head guard skips the oracle on a fork-head PR — and GitHub withholds repo secrets from fork-headpull_requestruns, so it could not run there anyway — a fork PR touching forge code SKIPS the oracle step and the single rolled-up CI check goes VACUOUSLY GREEN. The required-on-forge-affected-PRs guarantee structurally cannot hold for a fork PR. Matt’s accepted compensating control (ruling): accept vacuous-green on forks; the push-to-main run (which HAS secrets) catches any drift within one merge; and by CONVENTION a maintainer re-pushes a fork contribution to a same-repo branch before merge, where the oracle runs with secrets. 0 forks today makes this dormant, not absent.
Alternatives considered
Section titled “Alternatives considered”Options Matt rejected, recorded so the record is self-explaining:
(a) Manual-dogfood-only — no new test infrastructure
Section titled “(a) Manual-dogfood-only — no new test infrastructure”Rely on agents dogfooding the write path to surface contract mismatches. Rejected: discovery is unbounded-latency and lands as production incidents (a 422 in an agent’s write, not a red check); nothing pins the contract a fix was validated against.
(b) Live oracle nightly + workflow_dispatch only (PR gate stays hermetic)
Section titled “(b) Live oracle nightly + workflow_dispatch only (PR gate stays hermetic)”Preserves D2’s per-PR determinism intact; drift is caught within a day. Rejected: a forge PR can merge green against stale fixtures and be reverted tomorrow — the strongest guarantee (no forge change merges unvalidated against the live contract) requires the oracle on the PR gate.
(c) Live oracle on PRs but NON-blocking (advisory)
Section titled “(c) Live oracle on PRs but NON-blocking (advisory)”Same signal, no red. Rejected: an advisory check is a check people learn to ignore; the correctness guarantee only exists if the gate can refuse the merge.
(d) Record/replay fixtures with no standing live oracle
Section titled “(d) Record/replay fixtures with no standing live oracle”Fixtures regenerate only when a human runs -update. Rejected: the fixtures
decay silently between regenerations — exactly the drift class the oracle
exists to catch; (d) is leg 1 without leg 2.
(e) go-vcr (gopkg.in/dnaeon/go-vcr) record/replay cassettes
Section titled “(e) go-vcr (gopkg.in/dnaeon/go-vcr) record/replay cassettes”Leg 1’s hand-rolled fixture harness (schema + loader + -update +
request-match) reimplements what go-vcr ships: cassette record/replay via an
http.RoundTripper drop-in, request matchers, and re-record modes. Rejected
because we want the fixture to be a REVIEWED, diffable contract document with
an explicit request-assertion half, not an opaque cassette — but the
rejection is recorded so the reinvention is defended, not silent.
(f) GitHub’s official OpenAPI description (github/rest-api-description) as a hermetic shape oracle
Section titled “(f) GitHub’s official OpenAPI description (github/rest-api-description) as a hermetic shape oracle”Schema-validating our fixtures and request bodies against GitHub’s published versioned spec would catch field-shape drift with zero network/secrets/flake, covering a large fraction of leg 2’s drift class while leaving live-only semantics (the F1 422 rule, auth-failure mapping, rate-limit headers) to a smaller live suite — it would shrink the per-PR live surface. DEFERRED as a possible future MIDDLE tier, not adopted in v1: this record’s scope stays the two decided legs.
(g) Pact / consumer-driven contract testing
Section titled “(g) Pact / consumer-driven contract testing”The obvious named tool for “integration testing an external API”; a poor fit because contract testing presumes a provider who verifies the pact, and GitHub/Linear will not.
Global Constraints
Section titled “Global Constraints”- Go floor:
go 1.25.0(go/go.mod:15); the module path isgithub.com/sealedsecurity/compass/go(go.mod:13). - No YAML project enumeration in CI. The workflow may gain the single
fixed live-oracle step, never a per-project/per-task list (
ci.yml:4-18). Leg 1 requires zeroci.ymlchanges. - Fork-PR posture for leg 2 (accepted vacuous-green + push-to-main
catch). The secret-bearing step guards on a same-repo head
(
github.event.pull_request.head.repo.full_name == github.repository), and GitHub withholds repo secrets from fork-headpull_requestruns — so a fork PR can never run the oracle. The honest consequence: a fork-head PR touching forge code skips the oracle and the rolled-up CI check goes VACUOUSLY GREEN; the required-on-forge-affected-PRs guarantee cannot hold for a fork PR. Matt’s accepted compensating control: accept vacuous-green on forks, rely on the push-to-main run (which has secrets) to catch drift within one merge, and by convention re-push a fork contribution to a same-repo branch before merge. Repo Actions settings also require approval for ALL outside-collaborator workflow runs (defense in depth). Verified starting posture: public, 0 forks — dormant, not absent. - Test PATs ≠ production secrets. The oracle uses dedicated bot PATs
(author + reviewer identities) scoped to the throwaway repo, held only as
GitHub Actions repo secrets — never the
server_onlydeclared secrets the server resolves (DL-052,DECISIONS.md:75;serve.go:122-124). - Fixtures live in
go/internal/forge/testdata/(new directory); regeneration is a-updateflag gated on live credentials; committed fixtures are the reviewed contract snapshot. - The live suite is
//go:build livegithub, mirroring thepodmanprecedent (go/e2e/harness_test.go:1) so the moon battery never compiles it; its CI step is the only runner. - Honor the assert-it-ran-not-skipped guard idiom: the skip string is
derived from the suite source, and the
okline is required for the package, per the pgtest and dogfood guards (ci.yml:272-314,:719-748). - Capture-replay-exit step shape: redirect (never
| tee), replay the log, exit ongo test’s own status (ci.yml:257-270). - Sequencing: leg 2’s write scenarios depend on the write-path
implementation tasks (GitHub write methods +
SubmitReview,compass-forge-write-path/design.mdT2/T3) being merged;github.goat main tip7396fcffstill implements only the read half (compass-forge-write-path/design.md:49-51; grep forfunc (g *GitHub) Createreturns nothing). Leg 1’s read-path fixtures can land first. - Ledger coupling: this record’s PR must touch
docs/designs/product/DECISIONS.mdor declareLedger-impact:(tools/design-ledger-gate/index.ts:20-23: “a PR whose changed set touches a product design record MUST also touch DECISIONS.md, unless it declaresLedger-impact:in the PR body”).
T1 — Golden-fixture harness + replay test + seed fixtures (leg 1)
Section titled “T1 — Golden-fixture harness + replay test + seed fixtures (leg 1)”Add the fixture harness and the first fixture set for the existing read methods, extending to the write methods as write-path T2/T3 merge.
Fixtures cover BOTH providers co-equally — GitHub and Linear issue/comment
create+read — under testdata/<provider>/; the harness is provider-generic.
The one GitHub-only concept is the PR/review family, which Linear has no
analogue for.
go/internal/forge/golden_test.go(untagged): loads each fixture pair (request expectation + scripted response) fromgo/internal/forge/testdata/<provider>/<scenario>.json, replays it throughscriptedRoundTripper(github_test.go:28-32) against the real client built bynewTestGitHub(github_test.go:70-76), and asserts both halves: the request the client emitted (method, URL, headers, body) matches the captured request, and the decoded domain value matches the captured expectation.go/internal/forge/golden.go(test-support, or_test.go-only if no non-test consumer emerges): the fixture schema + loader +-updateregeneration path. Regeneration hits the live throwaway repo with the leg-2 credentials and rewrites the fixture files.go/moon.yml: extend the&go_sourcesanchor (go/moon.yml:51-54) with'**/testdata/**'so a fixture-only diff schedules the battery (see Approach — grounded correction).
Interfaces:
// fixture is one captured GitHub exchange: the request our client is// expected to emit and the scripted response to replay.type fixture struct { Name string `json:"name"` Request fixtureRequest `json:"request"` // method, path, query, headers (auth redacted), body Response fixtureResponse `json:"response"` // status, headers, body (verbatim GitHub JSON)}
var update = flag.Bool("update", false, "regenerate testdata fixtures from the live throwaway repo (requires LIVEGITHUB_* env)")
func loadFixtures(t *testing.T, dir string) []fixturefunc writeFixture(t *testing.T, dir string, f fixture)Test cycle: red-green (rule://red-green-testing) — commit a deliberately
mis-shaped fixture first to watch the replay assert fail, then the real
capture; moon run compass-go:test green; a fixture-only commit demonstrably
schedules compass-go under moon ci :ci.
T2 — //go:build livegithub oracle suite + -update capture + F1 headline scenario (leg 2)
Section titled “T2 — //go:build livegithub oracle suite + -update capture + F1 headline scenario (leg 2)”go/internal/forge/livegithub_test.go(//go:build livegithub): the same scenarios as T1’s fixtures, executed against the real throwaway repo, each asserting the live response matches the committed fixture (shape-level comparison with an explicit volatile-field allowlist: ids, timestamps, URLs, rate-limit headers).- Skip-when-unavailable, loud-in-CI: the suite
t.Skips when the credential env is absent (the local-sandbox posture, mirroringgo/e2e/harness_test.go:28-30’spodmanUsable()skip), and the T3 CI guard turns that skip into a failure in CI (theCOMPASS_REQUIRE_LIVEpattern,ci.yml:252-256, is the harness-level analogue if preferred). - Headline scenario (F1): author PAT opens a PR on the throwaway repo → author PAT attempting APPROVE asserts the 422 rejection body matches the fixture → reviewer PAT APPROVEs successfully → teardown closes the PR and deletes the branch. Additional scenarios: create issue, comment on issue/PR, create PR, submit REQUEST_CHANGES/COMMENT reviews, each asserting the response fixture still matches; auth-failure mapping (revoked-token 401 → the client’s TokenSource invalidation path).
- Linear scenarios (co-equal, landing as the RIG-2209 Linear provider
lands): create issue, comment on issue, get/list issues against the Linear
test team under the
LINEAR_FORGEtoken, each asserting the live response still matches the committed fixture. The GitHub PR/review family has no Linear analogue — those ops returnErrUnsupportedand are NOT Linear scenarios. Mirrors the GitHub leg’s shape under its own build tag. - Scenario hygiene: every scenario creates uniquely-named artifacts (run-id-suffixed) and tears them down; a leaked artifact must not fail the next run.
Interfaces:
//go:build livegithub
// Env contract (CI secrets → env in the T3 step):// LIVEGITHUB_REPO — "owner/name" of the throwaway repo// LIVEGITHUB_AUTHOR_TOKEN — test-only author bot PAT// LIVEGITHUB_REVIEWER_TOKEN — test-only reviewer bot PAT// LINEAR_FORGE — test-only Linear token (dedicated test team)func requireLive(t *testing.T) (repo string, author, reviewer forge.TokenSource) // t.Skip when unsetTest cycle: go test -tags livegithub ./internal/forge/... green locally
against the provisioned throwaway repo; a deliberately corrupted fixture
turns the oracle red (proving it compares, not just executes).
T3 — CI leg wiring + repo-settings prerequisites
Section titled “T3 — CI leg wiring + repo-settings prerequisites”The one ci.yml edit this record authorizes: a single fixed step (plus its
guard step) in the existing gates job, after the pgtest step
(ci.yml:244-314 is the shape to mirror).
- Step condition — the explicit boolean, NOT a single same-repo guard ANDed
across all three events (that form nulls out the oracle on
pushandschedule, wheregithub.event.pull_requestis absent): run when(event == pull_request AND same-repo-head AND path-affected) OR event == push OR event == schedule. Path-affected is the filter overgo/internal/forge/**and the workflow file; the nightly trigger isschedule(ci.yml:62-66). The same-repo-head guard (github.event.pull_request.head.repo.full_name == github.repository, Global Constraints) applies ONLY to thepull_requestarm —pushto main andschedulerun the oracle UNCONDITIONALLY with secrets, and are the backstop. - Env: the
LIVEGITHUB_*secrets and theLINEAR_FORGEtoken; capture- replay-exit run shape; a source-derived skip-string guard step asserting the suite ran rather than skipped (mirrorci.yml:719-748). - Update the D2 comment at
ci.yml:702-704to scope its no-secrets clause to the dogfood tier and cite this record (D2’s tier is unchanged; the oracle is a separate secret-bearing step — see “A new secret-bearing required check”). - Regeneration lane: a
workflow_dispatchjob that runsgo test -tags livegithub -run <update-test> -updatein CI and opens a BOT PR with the rewrittentestdata/diff for review (the PATs live only in Actions secrets, so no local-updateis possible). One bot-opens-a-PR credential path, same bot scoped to opening PRs. - Required-blocking activation ordering: the required-blocking flip activates AFTER the write scenarios exist (leg-1 read fixtures + the provider write methods land first); nightly-only/advisory in the interim is a STAGING order for the decided end state, not a re-litigation. The interim is short — the providers are landing now.
- Repo/infra prerequisites, each classified per
rule://no-human-clicks(prefer IaC; genuine console-only steps become ahuman-actionhandoff issue perskill://human-action-handoff):- GitHub testbed: create
RigelBuild/compass-forge-testbed(private) (API-scriptable:gh repo create/ Pulumi GitHub provider — IaC). - Create the two GitHub bot accounts (author + reviewer) and mint their fine-grained PATs scoped to that repo (account creation + PAT minting is console-only — human-action handoff to Matt, with a copy-pasteable runbook).
- Invite the reviewer bot as a collaborator on the private testbed AND accept the invite (author-PAT invites via the invitations API, reviewer-PAT accepts — both scriptable); without the accepted invite the reviewer’s APPROVE 404s, and it is on the F1 headline critical path.
- Linear testbed: provision a dedicated Linear test team and mint a test
LINEAR_FORGEtoken (team creation is console-only — human-action handoff; token minting per the sanctioned declared-secret path where API-exposed). - Add the repo secrets — the
LIVEGITHUB_*trio andLINEAR_FORGE(API-scriptable:gh secret set— IaC/CLI). - Actions settings: require approval for all outside-collaborator runs (API-scriptable via the Actions permissions API where exposed; otherwise handoff).
- Branch-protection/required-check registration for the new check name is
NOT needed: the step lives inside the one existing
CIjob, so the existing single required check already gates it (ci.yml:20-21“a single check name to require”).
- GitHub testbed: create
Interfaces: the step’s env contract is T2’s LIVEGITHUB_* trio plus the
Linear LINEAR_FORGE token; the run line is
go test -tags livegithub -race -v -timeout 10m ./internal/forge/... under
working-directory: go, CGO_ENABLED: '1' (the race-lane convention,
go/moon.yml:150-152), with the Linear live leg run under its own build tag
by the same step.
T4 — Ledger delta
Section titled “T4 — Ledger delta”- Append the new DL row (next free id — DL-207 at drafting time; renumber to
the ledger head at merge): the two-leg integration-testing decision, its
required-on-affected-PRs + nightly cadence, the fork-PR vacuous-green
posture, and the standalone secret-bearing-step decision recorded by
citation (D2 is a platform dogfood-e2e record decision, not a ledger row —
see Approach — and its dogfood tier is unchanged; this record’s row records
a NEW secret-bearing per-PR step, and the
ci.yml:702-704comment update (T3) scopes D2’s no-secrets clause to the dogfood tier). - Extend the write-path record’s pyramid BY CITATION only (its record is
frozen; DL-174/DL-175,
DECISIONS.md:264-265, stay Active — the two new legs sit above the pyramid, they do not replace any tier). - The record PR itself carries this DECISIONS.md delta (the touch-coupling
gate,
tools/design-ledger-gate/index.ts:20-23).
Interfaces: one DECISIONS.md row under the forge topic heading; Status:
header of this record set per the record-Status grammar at merge.
T5 — Frozen-record forward pointer (dogfood-e2e)
Section titled “T5 — Frozen-record forward pointer (dogfood-e2e)”Add a one-line bidirectional pointer next to D2 in the frozen
compass-dogfood-e2e/design.md (§815-830) authorizing this record’s
live-oracle secret-bearing carve-out — the frozen-record convention (add by
citation, never rewrite the frozen prose). This record points at D2; D2
points back here, so the carve-out is discoverable from both sides.
- T1 (compass-server) — golden-fixture harness
(
golden.go/golden_test.go),testdata/<provider>/seed fixtures for BOTH providers’ read + issue/comment create+read (GitHub and Linear co-equally; write methods as the provider write halves land),-updateregeneration flag,go/moon.yml&go_sources+'**/testdata/**'; red-green + affected-detection smoke. - T2 (compass-server) —
//go:build livegithuboracle suite (and the mirrored Linear live leg as RIG-2209 lands): fixture-match assertions with a volatile-field allowlist, the GitHub-only F1 reviewer≠author headline scenario (author 422 → reviewer APPROVE), Linear issue/comment scenarios (the PR/review family isErrUnsupportedon Linear, not scenarios), unique-name + teardown hygiene, skip-when-unavailable. - T3 (compass-server + infra) — the single fixed CI step + skip
guard in the
gatesjob (explicit(pull_request AND same-repo-head AND path-affected) OR push OR schedulecondition, secrets env, capture-replay-exit),ci.yml:702-704D2 comment update, theworkflow_dispatch-updatebot-PR regen lane; provision the GitHub testbed repo + secrets via IaC/CLI, the reviewer-bot collaborator invite+accept, the Linear test team +LINEAR_FORGEtoken; bot accounts + PATs + (if API-unreachable) Actions approval setting as ahuman-actionhandoff. - T4 (design ledger) — new DL row (two-leg decision + cadence + fork-PR vacuous-green posture + the standalone secret-bearing-step decision recorded by citation, D2’s tier unchanged); no Status flips (DL-174/175/201 stay Active); rides this record’s PR.
- T5 (design docs) — the frozen-record forward pointer next to D2
in
compass-dogfood-e2e/design.md(§815-830), added by citation.
Resolved questions
Section titled “Resolved questions”Matt has ruled on all four; recorded here as decided (not open forks).
- Testbed naming, org, and identities (RESOLVED). GitHub:
RigelBuild/compass-forge-testbed(private — no public visibility needed, and privacy shrinks the abuse surface) with two fresh bot accounts (author + reviewer), each holding a fine-grained PAT scoped to that repo. Linear: a dedicated test team plus a testLINEAR_FORGEtoken. New scoped credentials for each provider; console-only creation steps (account/team creation, PAT minting) become ahuman-actionhandoff with a copy-pasteable runbook (T3). - Fork-PR gating on a public repo (RESOLVED). Accept vacuous-green on fork PRs and rely on the push-to-main run (which has secrets) to catch any drift within one merge; by convention a maintainer re-pushes a fork contribution to a same-repo branch before merge, where the oracle runs with secrets. Not merge_group, not a hard gate. The same-repo-head step condition + GitHub’s fork-secret withholding + require-approval-for- outside-collaborators are the defense-in-depth guards. See “A new secret-bearing required check” and Global Constraints. 0 forks today makes this dormant, not absent.
- Rate-limit / flake budget (RESOLVED). Accepted as decided; mitigate
with the per-ref concurrency group (
ci.yml:86-88), a tight scenario set (single-digit API calls per scenario), and re-run-clears-transient (the pgtest precedent,ci.yml:40-42). Additionally guard GitHub’s SECONDARY rate limit (abuse detection on rapid content creation, independent of the 5000/hr primary budget): two concurrent forge PRs share one author bot against one testbed repo, and the per-ref concurrency group does NOT serialize across different PRs — so add a repo-level concurrency group on the oracle step, or per-run backoff on 403-secondary responses. - Linear scope (RESOLVED). Linear is CO-EQUAL and in-scope now, not
deferred: Compass writes to GitHub AND Linear, and both providers are
first-class in both legs (golden-replay and live oracle). Linear’s live
oracle uses the Linear test team +
LINEAR_FORGEtoken (question 1). The Linear provider is RIG-2209 (write-path T6, in flight now); its live write scenarios land as that provider lands. The GitHub-only F1 reviewer≠author scenario stays GitHub-only (Linear has no PR/review concept — theErrUnsupportedarm); the issue/comment create+read scenarios cover both.