Add a fake Basecamp for testing basecamp connect end to end

RO
5d ago
+3597-1

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 connect subprocess: 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 World holds 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 with Update. 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, and Publish adds 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: events always present, ascending ids, an opaque position, and a next URL that repeats the request's own filters. A page walks a fixed number of rows, so a page can be empty while next says there is more.
  • The cable follows eventfeed's own loopback server: the actioncable-v1-json subprotocol, welcome, confirm or reject, a ping every three seconds, and a ws:// 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_client with no fault needed.
  • Faults and waits. Inject answers a route with any status or body, for N requests or until removed, optionally only for some callers (ByAgents). Gate holds a route's requests open until Release. Before runs a function as a route is reached. DropCable and StopPings act on the live connections. Every request is logged, and Await waits 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.NewLive with 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.
  • 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. ConnectAgent with 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, and Recordings().Summarize for 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 and invalid_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

  1. Move connect_setup_test.go onto the fake and delete connectSetupServer.
  2. The subprocess harness and the happy path.
  3. Failure scenarios that need no new knobs.
  4. Queue callbacks in runConnect, with the held-reads backlog test.
  5. The handoff grace knob, from the dev build tag.
  6. A dev BC3 target running the same scenarios.
  7. 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.
  • Inject faults a route with any status or body, for N requests, optionally scoped to agents; Gate holds requests open, and DropCable / StopPings act on live connections.
  • Faults are also readable: Await re-checks after every change and returns ErrClosed once 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/fakebasecamp serves it interactively; coder/websocket becomes a direct dependency and the Nix vendorHash is updated.