Add a fake Basecamp for testing basecamp connect end to end
PR 0 of a stack of 8 that tests basecamp connect end to end. This one adds the fake Basecamp the others run against. It changes no behaviour of the CLI.
Why
basecamp connect has good unit tests, but nothing runs the real binary against a Basecamp that speaks both lanes of the event feed. Each command test brings its own small mock (connectSetupServer, connectAS, the feed servers in the connector tests), and none of them has a cable. So a connector that starts, connects, admits and prints a request line has never been tested as one process.
The later PRs need one fake that can:
- serve a real
basecamp connectsubprocess: the agent connection ceremony, client_credentials tokens, the account reads, the stream ticket, the poll lane and the Action Cable socket, and the recording reads admission makes; - say exactly what each lane serves, so a catch-up, a gap or a duplicate is a deterministic scenario and not a race;
- break on purpose: token 429 / 5xx / invalid_client, a dropped cable, silent pings, reads that fail N times, reads held open;
- express every knob of
connectSetupServer, so PR 1 can move those tests onto it and delete the mock.
What
internal/connector/fakebasecamp, a normal package (not a test file) so command tests, connector tests and a subprocess harness can all share it. It takes a small Reporter interface (the part of testing.TB it needs), so it imports no test library.
- One world, one lock. A
Worldholds the account, people, projects, recordings, the agents' OAuth clients and the bearer tokens. Every request is answered from it under one mutex, and tests change it withUpdate. Access follows Basecamp: a token names a person, and a person sees what their projects hold. Anything else answers 404, on the reads and on both lanes. - Lanes are published explicitly.
Emit(ev, Live|Poll|Both)puts an event on the lanes named, andPublishadds an emitted event to another lane later. There is no clock and no lag. A live event is queued to each matching subscription under the world's lock, so it is ordered against subscription confirms. Socket writes happen outside the lock, one writer per connection. - The poll lane follows the contract eventfeed checks:
eventsalways present, ascending ids, an opaque position, and anextURL that repeats the request's own filters. A page walks a fixed number of rows, so a page can be empty whilenextsays there is more. - The cable follows eventfeed's own loopback server: the
actioncable-v1-jsonsubprotocol, welcome, confirm or reject, a ping every three seconds, and aws://URL on the same loopback port. - The connection ceremony hands over the approved client under a freshly rotated secret, as Basecamp does, and spends the code. Minting checks the secret, so a secret rotated away is
invalid_clientwith no fault needed. - Faults and waits.
Injectanswers a route with any status or body, for N requests or until removed, optionally only for some callers (ByAgents).Gateholds a route's requests open untilRelease.Beforeruns a function as a route is reached.DropCableandStopPingsact on the live connections. Every request is logged, andAwaitwaits for a condition without sleeping: it re-checks after every change. - Strict about routes. A request no route serves is answered 404 and reported through the
Reporter, so a client that starts calling something the fake does not know fails where it is seen.
go run ./e2e/fakebasecamp serves the default world on a loopback port. It prints the environment and the three commands that connect, set up and run the agent, and prints each request as it is answered. Typing mention posts a comment that mentions the agent on both lanes, and drop severs the cable. I used it to run auth agent connect, connect setup and connect by hand. A mention came out as a request line on stdout.
Dependencies. github.com/coder/websocket moves from an indirect entry in go.sum to a direct requirement. The SDK already uses it for this same purpose in its tests. The Nix vendorHash is updated with make update-nix-hash, and that build was verified.
Tests
The fake is held to the real clients, not to itself:
- eventfeed in process.
eventfeed.NewLivewith the connector's own filters, over its real seams and WebSocket transport:- a catch-up walk across three pages, one of them emptied by the filters, with the exact
since/position/ filter queries asserted; - an event on both lanes, emitted while the walk is held at a gate, is delivered once;
- a cable dropped while the reconnect is held at its mint delivers the missed event through the catch-up.
- a catch-up walk across three pages, one of them emptied by the filters, with the exact
- The cable by hand. welcome, confirm, reject of a second subscription, the push frame's exact members, filters and project visibility on the live lane, pings, and silence after
StopPings. - The connection ceremony through
internal/auth.ConnectAgentwith the production HTTP clients, the secret rotation, a second connection invalidating the first secret, a spent code refused, and a read-only approval. - The reads through the SDK. profile (with boss), people (client, readable only by some, unknown), projects, project people,
authorization.json, andRecordings().Summarizefor message, comment, to-do, card and chat line, each finding the agent's mention. Also subscriptions and the recording history that admission reads. - Faults. token 429 with
Retry-After: 1, 503 andinvalid_client, faults that lapse after N, faults only for agents, a 200 override, gates, hooks, the poll lane's 400s, and unrouted requests being reported.
Every wait is an Await or a channel with a deadline, never a sleep. The package passes go test -race -count=20 and -race -count=10 -cpu=1,2. bin/ci is green.
Next in the stack
- Move
connect_setup_test.goonto the fake and deleteconnectSetupServer. - The subprocess harness and the happy path.
- Failure scenarios that need no new knobs.
- Queue callbacks in
runConnect, with the held-reads backlog test. - The handoff grace knob, from the
devbuild tag. - A dev BC3 target running the same scenarios.
- The lock message, docs, skill and evals.
Summary by cubic
Adds a fake Basecamp server for end-to-end testing of basecamp connect: the agent connection ceremony, the account reads, and both lanes of the event feed. It changes no CLI behavior.
Faking
- One world under one lock answers every request; tests change it through
Update. - Events are published to the live, poll, or both lanes explicitly, so catch-ups, gaps, and duplicates are deterministic scenarios.
Injectfaults a route with any status or body, for N requests, optionally scoped to agents;Gateholds requests open, andDropCable/StopPingsact on live connections.- Faults are also readable:
Awaitre-checks after every change and returnsErrClosedonce the fake closes.
Real clients
- Held to eventfeed's live connector,
internal/auth's connection ceremony, and the SDK's reads. - Stream tickets expire after their 120-second lifetime and stay replayable within it; the client_credentials mint refuses a scope wider than the client's approval.
go run ./e2e/fakebasecampserves it interactively;coder/websocketbecomes a direct dependency and the NixvendorHashis updated.