testutil: export harness + refactor integration tests #98
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Refactors AIBridge integration tests to use a shared exported
testutilharness and typed.txtarfixtures.testutilpackage:TXTARFixture,LLMFixture,UpstreamServer,MCPServer,BridgeServer,RecorderSpy,InspectorFixes #73.
📋 Implementation Plan
Refactor AIBridge tests: exported
testutilharness + fixture structsGoals
.txtarfixtures but make them typed (fixture structs with methods), so tests don’t manuallytxtar.Parse+filesMap+ magic filenames.github.com/coder/aibridge/testutilpackage.Non-goals
.txtar).Proposed
testutilpackage (exported)Key design principles
Harness/Serverstruct that exposes small methods.Package layout (suggested)
testutil/fixture_txtar.go: parse.txtar, expose a safe API.testutil/fixture_llm.go: provider/upstream fixture (request + streaming/non-streaming responses + optional tool-call responses).testutil/upstream_server.go: mock upstream server driven byLLMFixture.testutil/http_reflector.go: “raw HTTP response fixture” server (currentmockHTTPReflectorpattern).testutil/mcp_server.go: mock MCP server + call tracking + proxier creation.testutil/recorder_spy.go:aibridge.Recorderimplementation + query helpers.testutil/bridge_server.go: start an AIBridgehttptest.Serverwith actor context and configurable deps.testutil/inspector.go: one place to access assertion data (recorder + MCP calls + upstream call count).testutil/doc.go: package docs + “test-only / not API-stable” disclaimer + examples.Core types & responsibilities
1) Fixtures
TXTARFixture.Commentand amap[string][]byte.File(name)/MustFile(name)/Has(name).LLMFixtureTXTARFixture.RequestBody(streaming bool) ([]byte, error)(e.g. setsstream=truesafely)Response(call int, streaming bool) ([]byte, error)(supports 2-turn injected tool flows)2) Mock upstream server
type UpstreamServer struct { URL string; Calls atomic; ... }LLMFixtureand options:WithResponseMutator(func(call int, resp []byte) []byte)(keeps the escape hatch used today)WithStatusCode(int)LLMFixture.Response(call, streaming)to select the correct response.RequireCallCount(t, n)/EventuallyCallCount(t, n, timeout).3) Mock MCP server (fix PR72/63-style accretion)
Replace:
callAccumulatorcreateMockMCPSrvsetupMCPServerProxiesForTest(map + accumulator return)With:
type MCPServer struct { Server *httptest.Server; calls map[...]... }CallsByTool(name string) []anyProxiers(t, tracer, logger) map[string]mcp.ServerProxier(orProxier()+Name())Close()viat.Cleanupin constructor4) Recorder + Inspector
RecorderSpy(exported) implementsaibridge.RecorderTokenUsages() []*aibridge.TokenUsageRecordToolUsages() []*aibridge.ToolUsageRecordRequireAllInterceptionsEnded(t)InspectorRecorderSpyMCPServer(optional)UpstreamServer(optional)RequireToolCalled(t, tool string, wantArgs any)(checks both recorded tool usage and MCP receipt)UpstreamCalls() int5) AIBridge test server / harness
BridgeServer(orHarness)aibridge.NewRequestBridge(...)and wraps it inhttptest.Server.BaseContextpattern).*http.Clientused by tests.Do(req *http.Request) (*http.Response, error)NewProviderRequest(provider string, body []byte) (*http.Request, error)(replacescreateOpenAIChatCompletionsReq/createAnthropicMessagesReq).📎 API sketch (example usage)
Migration plan (mixed: refactor big file early, but keep incremental safety)
Phase 0 — Baseline + safety rails
testutilpackage skeleton with docs (doc.go) and a clear “test-only / API may change” disclaimer.testutilto ensure it doesn’t importaibridge_testor rely on_test.gosymbols.Phase 1 — Move the lowest-risk helpers first
mockRecorderClientintotestutil.RecorderSpy(keep method names close to today to simplify migration)..txtarparsing helpers:filesMap→testutil.MustParseTXTARreturningTXTARFixture.setJSONwrapper (if currently local) →testutil.SetJSON/WithJSONField.createAnthropicMessagesReq/createOpenAIChatCompletionsReq→BridgeServer.NewProviderRequest(...).Phase 2 — Fix the tech-debt hotspots (PR72/PR63 patterns)
testutil.MCPServerand movecallAccumulatorinside it.setupMCPServerProxiesForTestusages withmcpSrv.Proxiers(...).map[string]mcp.ServerProxierand*callAccumulator.Phase 3 — Rewrite
bridge_integration_test.goearly (reduce the 1900+ line “god file”)bridge_integration_anthropic_test.gobridge_integration_openai_test.gobridge_integration_injected_tools_test.gobridge_integration_errors_test.gobridge_integration_passthrough_test.gotestutil.UpstreamServerand migrate the “standard flow” tests (simple/builtin-tool/injected-tool) to the new harness.testutil.HTTPReflectorServer.Phase 4 — Migrate metrics + trace integration tests
newTestSrvwithtestutil.NewBridgeServer(or reuse the sameBridgeServertype).Phase 5 — Cleanup + enforce the new pattern
setupInjectedToolTestsetupMCPServerProxiesForTestcallAccumulatortestutil/doc.goorREADME.md), showing the new preferred pattern.Acceptance criteria (what “done” looks like)
txtar.Parse(...)+filesMap(...).callAccumulatoror rawproxiesmaps.bridge_integration_test.gois split and substantially smaller.coder): avoid embedding local fixture paths intestutil; accept[]bytefixtures orfs.FS.testutil(discourages production use).Generated with
mux• Model:openai:gpt-5.2• Thinking:xhigh