Files
coder/coderd/x/chatd/chatdebug/model_coverage_internal_test.go
T
Michael Suchacz c97f4da3ac chore: sync fantasy fork with upstream v0.40.0 and openai-go with v3.50.0 (#27981)
Our fantasy fork had drifted far behind upstream charmbracelet/fantasy
(base v0.31.0 vs current v0.40.0). This PR updates the pinned forks
after reconciling which fork hacks upstream has fixed and which we still
need, and adapts this repo to the new APIs.

## Fork updates

- `charm.land/fantasy` ->
[coder/fantasy#51](https://github.com/coder/fantasy/pull/51) (merged):
`coder_2_33` synced with upstream v0.40.0, pinned at the merge commit
`bb10946892ef`.
- `github.com/openai/openai-go/v3` ->
[coder/openai-go#10](https://github.com/coder/openai-go/pull/10)
(merged): `coder/pinned` rebased from v3.16.0 onto upstream v3.50.0
(required by upstream fantasy), pinned at the merge commit
`92b5addb22d2`.
- `coder/anthropic-sdk-go` pin unchanged; the fantasy fork now tracks
the same revision this repo ships.

## Hack reconciliation summary

Dropped from our fantasy diff (upstream now has equivalents, often
stricter): truncated-stream fail-closed detection, Anthropic EffortXHigh
/ computer use / thinking effort / thinking display, replay fidelity for
signed reasoning and web_search errors, PDF and text documents with
sanitized filename titles, refusal finish-reason mapping (upstream also
maps Bedrock `content_filtered`/`guardrail_intervened`), gpt-5.5/5.6
Responses routing, the Go 1.25 downgrade, and the openai-go SSE decoder
and appendCompact patches.

Still fork-only and preserved: OpenAI computer use, OpenAI Responses
replay continuity validation, Anthropic pre-4.6 budget-thinking
conversion plus explicit thinking disable for effort none, Anthropic
RefusalMetadata parsing, Bedrock cross-region inference profile region
mirroring, and openai-go deferred body serialization with the
WithJSONSet fix.

Picked up new upstream features: stream transport retry with in-band SSE
error classification, Bedrock expired-credential refresh, per-message
cache markers for OpenAI-compatible models, tool panic recovery, extra
usage fields in provider metadata, and ClientMetadata on tool results.

## Changes in this repo

- `aibridge/intercept/responses`: `ResponseOutputItemUnion.Arguments`
became a union type in openai-go v3.50; read function-call arguments via
`.OfString` (plus test literal updates).
- `coderd/x/chatd/chatdebug`: register the new fantasy `Call.Headers`,
`ObjectCall.Headers`, and `ToolResultPart.ClientMetadata` fields in the
normalization coverage map (all skipped).
- `aibridge/internal/integrationtest`: make the RST test listener drain
the request before resetting the connection. The new SDK's write path
exposed the previous 1-byte-read race as sporadic `use of closed network
connection` failures; the fix holds over 40 consecutive runs.
- `go.mod`: rewrite the fork provenance comments to describe the
post-sync state.

## Validation

- `go build ./...` and `go vet ./...` clean (vet findings identical to
base).
- Fresh (`-count=1`) runs of `./coderd/x/chatd/...`, `./aibridge/...`,
`./coderd/aibridged/...`, `./coderd/database/db2sdk/`: 37 packages pass.
- `TestClientAndConnectionError` stress-tested 40x clean.
- Both fork PRs have green CI.

> Mux acted on Mike's behalf to create this PR.
2026-08-11 11:20:05 +02:00

336 lines
13 KiB
Go

package chatdebug
import (
"reflect"
"testing"
"charm.land/fantasy"
"github.com/stretchr/testify/require"
)
// fieldDisposition documents whether a fantasy struct field is captured
// by the corresponding normalized struct ("normalized") or
// intentionally omitted ("skipped: <reason>"). The test fails when a
// fantasy type gains a field that is not yet classified, forcing the
// developer to decide whether to normalize or skip it.
//
// This mirrors the audit-table exhaustiveness check in
// enterprise/audit/table.go; same idea, different domain.
type fieldDisposition = map[string]string
// TestNormalizationFieldCoverage ensures every exported field on the
// fantasy types that model.go normalizes is explicitly accounted for.
// When the fantasy library adds a field the test fails, surfacing the
// drift at `go test` time rather than silently dropping data.
func TestNormalizationFieldCoverage(t *testing.T) {
t.Parallel()
tests := []struct {
name string
typ reflect.Type
fields fieldDisposition
}{
// ── struct-to-struct mappings ──────────────────────────
{
name: "fantasy.Usage → normalizedUsage",
typ: reflect.TypeFor[fantasy.Usage](),
fields: fieldDisposition{
"InputTokens": "normalized",
"OutputTokens": "normalized",
"TotalTokens": "normalized",
"ReasoningTokens": "normalized",
"CacheCreationTokens": "normalized",
"CacheReadTokens": "normalized",
},
},
{
name: "fantasy.Call → normalizedCallPayload",
typ: reflect.TypeFor[fantasy.Call](),
fields: fieldDisposition{
"Prompt": "normalized",
"MaxOutputTokens": "normalized",
"Temperature": "normalized",
"TopP": "normalized",
"TopK": "normalized",
"PresencePenalty": "normalized",
"FrequencyPenalty": "normalized",
"Tools": "normalized",
"ToolChoice": "normalized",
"UserAgent": "skipped: internal transport header, not useful for debug panel",
"Headers": "skipped: transport headers, may carry credentials",
"ProviderOptions": "skipped: opaque provider data, only count preserved",
},
},
{
name: "fantasy.ObjectCall → normalizedObjectCallPayload",
typ: reflect.TypeFor[fantasy.ObjectCall](),
fields: fieldDisposition{
"Prompt": "normalized",
"Schema": "skipped: full schema too large; SchemaName+SchemaDescription captured instead",
"SchemaName": "normalized",
"SchemaDescription": "normalized",
"MaxOutputTokens": "normalized",
"Temperature": "normalized",
"TopP": "normalized",
"TopK": "normalized",
"PresencePenalty": "normalized",
"FrequencyPenalty": "normalized",
"UserAgent": "skipped: internal transport header, not useful for debug panel",
"Headers": "skipped: transport headers, may carry credentials",
"ProviderOptions": "skipped: opaque provider data, only count preserved",
"RepairText": "skipped: function value, not serializable",
},
},
{
name: "fantasy.Response → normalizedResponsePayload",
typ: reflect.TypeFor[fantasy.Response](),
fields: fieldDisposition{
"Content": "normalized",
"FinishReason": "normalized",
"Usage": "normalized",
"Warnings": "normalized",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
},
},
{
name: "fantasy.ObjectResponse → normalizedObjectResponsePayload",
typ: reflect.TypeFor[fantasy.ObjectResponse](),
fields: fieldDisposition{
"Object": "skipped: arbitrary user type, not serializable generically",
"RawText": "normalized: as RawTextLength (length only, content unbounded)",
"Usage": "normalized",
"FinishReason": "normalized",
"Warnings": "normalized",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
},
},
{
name: "fantasy.CallWarning → normalizedWarning",
typ: reflect.TypeFor[fantasy.CallWarning](),
fields: fieldDisposition{
"Type": "normalized",
"Setting": "normalized",
"Tool": "skipped: interface value, warning message+type sufficient for debug panel",
"Details": "normalized",
"Message": "normalized",
},
},
{
name: "fantasy.StreamPart → appendNormalizedStreamContent",
typ: reflect.TypeFor[fantasy.StreamPart](),
fields: fieldDisposition{
"Type": "normalized",
"ID": "normalized: as ToolCallID in content parts",
"ToolCallName": "normalized: as ToolName in content parts",
"ToolCallInput": "normalized: as Arguments or Result (bounded)",
"Delta": "normalized: accumulated into text/reasoning content parts",
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
"Usage": "normalized: captured in stream finalize",
"FinishReason": "normalized: captured in stream finalize",
"Error": "normalized: captured in stream error handling",
"Warnings": "normalized: captured in stream warning accumulation",
"SourceType": "normalized",
"URL": "normalized",
"Title": "normalized",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
},
},
{
name: "fantasy.ObjectStreamPart → wrapObjectStreamSeq",
typ: reflect.TypeFor[fantasy.ObjectStreamPart](),
fields: fieldDisposition{
"Type": "normalized: drives switch in wrapObjectStreamSeq",
"Object": "skipped: arbitrary user type, only ObjectPartCount tracked",
"Delta": "normalized: accumulated into rawTextLength",
"Error": "normalized: captured in stream error handling",
"Usage": "normalized: captured in stream finalize",
"FinishReason": "normalized: captured in stream finalize",
"Warnings": "normalized: captured in stream warning accumulation",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
},
},
// ── message part types (normalizeMessageParts) ────────
{
name: "fantasy.TextPart → normalizedMessagePart",
typ: reflect.TypeFor[fantasy.TextPart](),
fields: fieldDisposition{
"Text": "normalized: bounded to MaxMessagePartTextLength",
"ProviderOptions": "skipped: opaque provider-specific options",
},
},
{
name: "fantasy.ReasoningPart → normalizedMessagePart",
typ: reflect.TypeFor[fantasy.ReasoningPart](),
fields: fieldDisposition{
"Text": "normalized: bounded to MaxMessagePartTextLength",
"ProviderOptions": "skipped: opaque provider-specific options",
},
},
{
name: "fantasy.FilePart → normalizedMessagePart",
typ: reflect.TypeFor[fantasy.FilePart](),
fields: fieldDisposition{
"Filename": "normalized",
"Data": "skipped: binary data never stored in debug records",
"MediaType": "normalized",
"ProviderOptions": "skipped: opaque provider-specific options",
},
},
{
name: "fantasy.ToolCallPart → normalizedMessagePart",
typ: reflect.TypeFor[fantasy.ToolCallPart](),
fields: fieldDisposition{
"ToolCallID": "normalized",
"ToolName": "normalized",
"Input": "normalized: as Arguments (bounded)",
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
"ProviderOptions": "skipped: opaque provider-specific options",
},
},
{
name: "fantasy.ToolResultPart → normalizedMessagePart",
typ: reflect.TypeFor[fantasy.ToolResultPart](),
fields: fieldDisposition{
"ToolCallID": "normalized",
"Output": "normalized: text extracted via normalizeToolResultOutput",
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
"ProviderOptions": "skipped: opaque provider-specific options",
"ClientMetadata": "skipped: client execution metadata not needed for debug panel",
},
},
// ── response content types (normalizeContentParts) ────
{
name: "fantasy.TextContent → normalizedContentPart",
typ: reflect.TypeFor[fantasy.TextContent](),
fields: fieldDisposition{
"Text": "normalized: bounded to MaxMessagePartTextLength",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
},
},
{
name: "fantasy.ReasoningContent → normalizedContentPart",
typ: reflect.TypeFor[fantasy.ReasoningContent](),
fields: fieldDisposition{
"Text": "normalized: bounded to MaxMessagePartTextLength",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
},
},
{
name: "fantasy.FileContent → normalizedContentPart",
typ: reflect.TypeFor[fantasy.FileContent](),
fields: fieldDisposition{
"MediaType": "normalized",
"Data": "skipped: binary data never stored in debug records",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
},
},
{
name: "fantasy.SourceContent → normalizedContentPart",
typ: reflect.TypeFor[fantasy.SourceContent](),
fields: fieldDisposition{
"SourceType": "normalized",
"ID": "skipped: provider-internal identifier, not actionable in debug panel",
"URL": "normalized",
"Title": "normalized",
"MediaType": "skipped: only relevant for document sources, rarely useful for debugging",
"Filename": "skipped: only relevant for document sources, rarely useful for debugging",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
},
},
{
name: "fantasy.ToolCallContent → normalizedContentPart",
typ: reflect.TypeFor[fantasy.ToolCallContent](),
fields: fieldDisposition{
"ToolCallID": "normalized",
"ToolName": "normalized",
"Input": "normalized: as Arguments (bounded), InputLength tracks original",
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
"Invalid": "skipped: validation state not surfaced in debug panel",
"ValidationError": "skipped: validation state not surfaced in debug panel",
},
},
{
name: "fantasy.ToolResultContent → normalizedContentPart",
typ: reflect.TypeFor[fantasy.ToolResultContent](),
fields: fieldDisposition{
"ToolCallID": "normalized",
"ToolName": "normalized",
"Result": "normalized: text extracted via normalizeToolResultOutput",
"ClientMetadata": "skipped: client execution metadata not needed for debug panel",
"ProviderExecuted": "skipped: provider vs client distinction not needed for debug panel",
"ProviderMetadata": "skipped: opaque provider-specific metadata",
"StopTurn": "skipped: control-flow flag is not rendered in the debug panel",
},
},
// ── tool types (normalizeTools) ───────────────────────
{
name: "fantasy.FunctionTool → normalizedTool",
typ: reflect.TypeFor[fantasy.FunctionTool](),
fields: fieldDisposition{
"Name": "normalized",
"Description": "normalized",
"InputSchema": "normalized: preserved as JSON for debug panel rendering",
"ProviderOptions": "skipped: opaque provider-specific options",
},
},
{
name: "fantasy.ProviderDefinedTool → normalizedTool",
typ: reflect.TypeFor[fantasy.ProviderDefinedTool](),
fields: fieldDisposition{
"ID": "normalized",
"Name": "normalized",
"Args": "skipped: provider-specific configuration not needed for debug panel",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Every exported field on the fantasy type must be
// registered as "normalized" or "skipped: <reason>".
for i := range tt.typ.NumField() {
field := tt.typ.Field(i)
if !field.IsExported() {
continue
}
disposition, ok := tt.fields[field.Name]
if !ok {
require.Failf(t, "unregistered field",
"%s.%s is not in the coverage map: "+
"add it as \"normalized\" or \"skipped: <reason>\"",
tt.typ.Name(), field.Name)
}
require.NotEmptyf(t, disposition,
"%s.%s has an empty disposition: "+
"use \"normalized\" or \"skipped: <reason>\"",
tt.typ.Name(), field.Name)
}
// Catch stale entries that reference removed fields.
for name := range tt.fields {
found := false
for i := range tt.typ.NumField() {
if tt.typ.Field(i).Name == name {
found = true
break
}
}
require.Truef(t, found,
"stale coverage entry %s.%s: "+
"field no longer exists in fantasy, remove it",
tt.typ.Name(), name)
}
})
}
}