Files
Agent-Native/runtime/llm/route_contract_test.go
T
agent-exec-transport 899fd70d27
ci / docker-checkout-hostgateway (pull_request) Successful in 12s
governance / governance (pull_request) Successful in 35s
ci / vet-and-test (pull_request) Successful in 1m24s
agent-exec transport e38bcf1cbe415c5d6a3e91773d811b0a/445c377b4484
2026-10-20 04:03:18 +00:00

631 lines
26 KiB
Go

package llm
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"reflect"
"strconv"
"strings"
"testing"
)
const (
rcModel = "fixture-model"
rcEOFReason = "tool call stream ended before [DONE]"
)
var rcTools = []ToolDecl{
{Name: "read_PRIVATE_I1", Parameters: json.RawMessage(`{"type":"object","required":["path"],"properties":{"path":{"type":"string"}}}`)},
{Name: "list_PRIVATE_I1", Parameters: json.RawMessage(`{"type":"object","properties":{"dir":{"type":"string"}}}`)},
}
func rcFrame(v any) string {
raw, err := json.Marshal(v)
if err != nil {
panic(err)
}
return "data: " + string(raw) + "\n\n"
}
func rcChunk(choices ...map[string]any) string {
return rcFrame(map[string]any{"choices": choices})
}
func rcFrag(index int, id, name, args string) map[string]any {
fn := map[string]any{}
if name != "" {
fn["name"] = name
}
if args != "" {
fn["arguments"] = args
}
frag := map[string]any{"index": index, "function": fn}
if id != "" {
frag["id"] = id
}
return frag
}
func rcToolChoice(finish string, frags ...map[string]any) map[string]any {
choice := map[string]any{"delta": map[string]any{"tool_calls": frags}}
if finish != "" {
choice["finish_reason"] = finish
}
return choice
}
func rcFinishChunk(reason string) string {
return rcChunk(map[string]any{"delta": map[string]any{}, "finish_reason": reason})
}
var (
rcText = rcChunk(map[string]any{"delta": map[string]any{"content": "PRIVATE_TEXT_I1", "reasoning_content": "PRIVATE_THINK_I1"}})
rcCall1 = rcChunk(rcToolChoice("", rcFrag(0, "call_PRIVATE_I1", "read_PRIVATE_I1", `{"path":`)))
rcCall2 = rcChunk(rcToolChoice("", rcFrag(0, "", "", `"PRIVATE_I1.txt"}`)))
rcFinish = rcFinishChunk("tool_calls")
rcUsage = rcFrame(map[string]any{"choices": []any{}, "usage": map[string]any{"prompt_tokens": 5, "completion_tokens": 2, "total_tokens": 7}})
rcDone = "data: [DONE]\n\n"
rcStream = rcText + rcCall1 + rcCall2 + rcFinish + rcUsage
rcCall = ToolCall{ID: "call_PRIVATE_I1", Name: "read_PRIVATE_I1", Arguments: `{"path":"PRIVATE_I1.txt"}`}
rcErrorFrame = rcFrame(map[string]any{"error": map[string]any{"message": "PRIVATE_PROVIDER_I1"}})
)
// rcSingleCall streams one complete call in one fragment with the given parts.
func rcSingleCall(id, name, args string) string {
return rcText + rcChunk(rcToolChoice("", rcFrag(0, id, name, args))) + rcFinish + rcUsage
}
func rcWrite(stream string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(w, stream)
w.(http.Flusher).Flush()
}
}
func rcHTTP2(t *testing.T, h http.Handler) *httptest.Server {
t.Helper()
server := httptest.NewUnstartedServer(h)
server.EnableHTTP2 = true
server.StartTLS()
t.Cleanup(server.Close)
return server
}
func rcTLS1(t *testing.T, h http.Handler) *httptest.Server {
t.Helper()
server := httptest.NewTLSServer(h)
t.Cleanup(server.Close)
return server
}
func rcPlain(t *testing.T, h http.Handler) *httptest.Server {
t.Helper()
server := httptest.NewServer(h)
t.Cleanup(server.Close)
return server
}
// rcHTTPClient trusts server like server.Client() but owns its connection
// pool, so each run starts on a fresh connection and HTTP/2 stream ids match.
func rcHTTPClient(t *testing.T, server *httptest.Server) *http.Client {
t.Helper()
base := server.Client().Transport.(*http.Transport)
transport := &http.Transport{TLSClientConfig: base.TLSClientConfig.Clone(), ForceAttemptHTTP2: true}
t.Cleanup(transport.CloseIdleConnections)
return &http.Client{Transport: transport}
}
func rcClientFor(t *testing.T, server *httptest.Server) *Client {
return &Client{BaseURL: server.URL, APIKey: "fixture", HTTPClient: rcHTTPClient(t, server)}
}
func rcContract(model, base string) *RouteContractEOF {
return &RouteContractEOF{Model: model, BaseURL: base, IdentitySHA256: RouteContractIdentity(model, base)}
}
func rcFixtureClient(resp func() *http.Response) *Client {
return &Client{BaseURL: "https://fixture.invalid", APIKey: "fixture", HTTPClient: &http.Client{
Transport: terminalRoundTripFunc(func(r *http.Request) (*http.Response, error) {
res := resp()
res.Request = r
return res, nil
}),
}}
}
type rcSetup struct {
client *Client
contract *RouteContractEOF
opts StreamOptions
tools []ToolDecl
}
type rcResult struct {
end Event
releasedMessages int
releasedCalls int
}
func rcRun(t *testing.T, ctx context.Context, base *Client, contract *RouteContractEOF, tools []ToolDecl, opts StreamOptions, onEvent func(Event)) rcResult {
t.Helper()
client := *base
client.RouteContract = contract
var res rcResult
terminals := 0
for ev := range client.Stream(ctx, rcModel, []Message{{Role: RoleUser, Content: "hello"}}, tools, opts) {
if onEvent != nil {
onEvent(ev)
}
if ev.Kind != KindMessageEnd && ev.Kind != KindError {
continue
}
terminals++
res.end = ev
if ev.Kind == KindMessageEnd && ev.Err == nil && len(ev.Message.ToolCalls) > 0 {
res.releasedMessages++
res.releasedCalls += len(ev.Message.ToolCalls)
}
}
if terminals != 1 {
t.Fatalf("terminal events = %d, want 1", terminals)
}
rcRequireSafeLabels(t, res.end)
return res
}
// rcRequireSafeLabels is the sentinel scan: the new labels hold only their
// fixed enums and the observation never carries content, tool identity,
// arguments or error text.
func rcRequireSafeLabels(t *testing.T, end Event) {
t.Helper()
obs := end.TerminalObservation
if obs == nil {
t.Fatalf("terminal without observation: %#v", end)
}
switch end.CompletionBasis {
case "", CompletionDoneMarker, CompletionRouteContractEOF:
default:
t.Fatalf("completion basis outside enum: %q", end.CompletionBasis)
}
if obs.CompletionBasis != end.CompletionBasis {
t.Fatalf("observation basis %q != event basis %q", obs.CompletionBasis, end.CompletionBasis)
}
if c := obs.ContractCheck; c != "" && (len(c) != 2 || c[0] != 'C' || c[1] < '1' || c[1] > '9') {
t.Fatalf("contract check outside enum: %q", c)
}
raw, err := json.Marshal(obs)
if err != nil {
t.Fatal(err)
}
for _, secret := range []string{"PRIVATE", "[DONE]", "tool call", "fixture-model"} {
if strings.Contains(string(raw), secret) {
t.Fatalf("observation leaked %q: %s", secret, raw)
}
}
for _, label := range []string{end.CompletionBasis, obs.CompletionBasis, obs.ContractCheck} {
if strings.Contains(label, "PRIVATE") || strings.Contains(label, " ") {
t.Fatalf("label carries content: %q", label)
}
}
}
func rcStreamCase(server func(*testing.T, http.Handler) *httptest.Server, stream string) func(*testing.T) rcSetup {
return func(t *testing.T) rcSetup {
s := server(t, rcWrite(stream))
return rcSetup{client: rcClientFor(t, s), contract: rcContract(rcModel, s.URL)}
}
}
func rcH2(stream string) func(*testing.T) rcSetup { return rcStreamCase(rcHTTP2, stream) }
func rcRedirectCase(target func(t *testing.T) string) func(*testing.T) rcSetup {
return func(t *testing.T) rcSetup {
location := target(t)
mux := http.NewServeMux()
mux.HandleFunc("/chat/completions", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, location, http.StatusTemporaryRedirect)
})
mux.HandleFunc("/final/chat/completions", rcWrite(rcStream))
s := rcHTTP2(t, mux)
return rcSetup{client: rcClientFor(t, s), contract: rcContract(rcModel, s.URL)}
}
}
type rcCase struct {
name string
setup func(*testing.T) rcSetup
// want is done_marker, route_contract_eof, off, or a check id C1..C9.
want string
// reason is the contract-off failure text; empty skips the check.
reason string
calls []ToolCall
noUsage bool
}
func rcCases() []rcCase {
twoCalls := rcText + rcCall1 + rcCall2 +
rcChunk(rcToolChoice("", rcFrag(1, "call_PRIVATE_I1_b", "list_PRIVATE_I1", `{}`))) + rcFinish + rcUsage
return []rcCase{
// [DONE] keeps today's path whether or not the contract is configured.
{name: "done/contract-on", setup: rcH2(rcStream + rcDone), want: CompletionDoneMarker},
{name: "done/contract-c1-failing", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream + rcDone)(t)
s.contract = rcContract("other-model", s.client.BaseURL)
return s
}, want: CompletionDoneMarker},
{name: "done/known-length", setup: rcKnownLength(rcStream + rcDone), want: CompletionDoneMarker},
// Released only when every condition holds.
{name: "release/http2", setup: rcH2(rcStream), want: CompletionRouteContractEOF},
{name: "release/known-length-match", setup: rcKnownLength(rcStream), want: CompletionRouteContractEOF},
{name: "release/usage-before-finish", setup: rcH2(rcText + rcUsage + rcCall1 + rcCall2 + rcFinish), want: CompletionRouteContractEOF},
{name: "release/usage-after-finish", setup: rcH2(rcText + rcCall1 + rcCall2 + rcFinish + rcUsage), want: CompletionRouteContractEOF},
{name: "release/fragment-in-finish-chunk", setup: rcH2(rcText + rcCall1 +
rcChunk(rcToolChoice("tool_calls", rcFrag(0, "", "", `"PRIVATE_I1.txt"}`))) + rcUsage), want: CompletionRouteContractEOF},
{name: "release/two-calls", setup: rcH2(twoCalls), want: CompletionRouteContractEOF,
calls: []ToolCall{rcCall, {ID: "call_PRIVATE_I1_b", Name: "list_PRIVATE_I1", Arguments: `{}`}}},
{name: "release/api-base-override-to-contract", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream)(t)
s.opts.APIBase = s.client.BaseURL + "/"
s.client.BaseURL = "https://unused.invalid"
return s
}, want: CompletionRouteContractEOF},
// The zero value is off.
{name: "off/zero-contract", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream)(t)
s.contract = &RouteContractEOF{}
return s
}, want: "off", reason: rcEOFReason},
// C1 route identity.
{name: "c1/model-mismatch", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream)(t)
s.contract = rcContract("other-model", s.client.BaseURL)
return s
}, want: "C1", reason: rcEOFReason},
{name: "c1/base-http", setup: rcStreamCase(rcPlain, rcStream), want: "C1", reason: rcEOFReason},
{name: "c1/base-userinfo", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream)(t)
s.client.BaseURL = strings.Replace(s.client.BaseURL, "https://", "https://user@", 1)
return s
}, want: "C1", reason: rcEOFReason},
{name: "c1/base-query", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream)(t)
s.client.BaseURL += "?x=1"
return s
}, want: "C1", reason: rcEOFReason},
{name: "c1/digest-mismatch", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream)(t)
s.contract.IdentitySHA256 = strings.Repeat("0", 64)
return s
}, want: "C1", reason: rcEOFReason},
{name: "c1/digest-uppercase", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream)(t)
s.contract.IdentitySHA256 = strings.ToUpper(s.contract.IdentitySHA256)
return s
}, want: "C1", reason: rcEOFReason},
{name: "c1/protocol-not-openai-chat", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream)(t)
s.client.Protocol = ProtocolGoogle
return s
}, want: "C1", reason: rcEOFReason},
{name: "c1/api-base-override-elsewhere", setup: func(t *testing.T) rcSetup {
other := rcHTTP2(t, rcWrite(rcStream))
s := rcH2(rcStream)(t)
s.opts.APIBase = other.URL
s.client.HTTPClient = rcHTTPClient(t, other)
return s
}, want: "C1", reason: rcEOFReason},
{name: "c1/redirect-other-host", setup: rcRedirectCase(func(t *testing.T) string {
return rcHTTP2(t, rcWrite(rcStream)).URL + "/chat/completions"
}), want: "C1", reason: rcEOFReason},
{name: "c1/redirect-same-host-other-path", setup: rcRedirectCase(func(t *testing.T) string {
return "/final/chat/completions"
}), want: "C1", reason: rcEOFReason},
{name: "c1/redirect-https-downgrade", setup: rcRedirectCase(func(t *testing.T) string {
return rcPlain(t, rcWrite(rcStream)).URL + "/chat/completions"
}), want: "C1", reason: rcEOFReason},
// C2 body boundary.
{name: "c2/known-length-mismatch", setup: func(t *testing.T) rcSetup {
client := rcFixtureClient(func() *http.Response {
return &http.Response{StatusCode: http.StatusOK, ProtoMajor: 1, ProtoMinor: 1,
ContentLength: int64(len(rcStream) + 64), Header: http.Header{},
Body: io.NopCloser(strings.NewReader(rcStream))}
})
return rcSetup{client: client, contract: rcContract(rcModel, client.BaseURL)}
}, want: "C2", reason: rcEOFReason},
{name: "c2/unknown-framing", setup: func(t *testing.T) rcSetup {
client := rcFixtureClient(func() *http.Response {
return &http.Response{StatusCode: http.StatusOK, ContentLength: -1, Header: http.Header{},
Body: io.NopCloser(strings.NewReader(rcStream))}
})
return rcSetup{client: client, contract: rcContract(rcModel, client.BaseURL)}
}, want: "C2", reason: rcEOFReason},
{name: "c2/chunked", setup: rcStreamCase(rcTLS1, rcStream), want: "C2", reason: rcEOFReason},
{name: "c2/close-delimited", setup: func(t *testing.T) rcSetup {
s := rcTLS1(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Transfer-Encoding", "identity")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, rcStream)
w.(http.Flusher).Flush()
}))
return rcSetup{client: rcClientFor(t, s), contract: rcContract(rcModel, s.URL)}
}, want: "C2", reason: rcEOFReason},
// A reset or dropped stream never reaches body EOF, so the boundary
// condition fails before the read-outcome condition is evaluated.
{name: "c2/http2-rst-stream", setup: func(t *testing.T) rcSetup {
s := rcHTTP2(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rcWrite(rcStream)(w, r)
panic(http.ErrAbortHandler)
}))
return rcSetup{client: rcClientFor(t, s), contract: rcContract(rcModel, s.URL)}
}, want: "C2", reason: "stream read failed after reported usage"},
{name: "c2/mid-stream-disconnect", setup: func(t *testing.T) rcSetup {
s := rcTLS1(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rcWrite(rcStream)(w, r)
conn, _, err := w.(http.Hijacker).Hijack()
if err == nil {
_ = conn.Close()
}
}))
return rcSetup{client: rcClientFor(t, s), contract: rcContract(rcModel, s.URL)}
}, want: "C2", reason: "stream read failed after reported usage"},
// The parser stops at a bad frame; with a trailing delimiter it never
// reads to body EOF, so the boundary condition fails first.
{name: "c2/invalid-frame-before-eof", setup: rcH2(rcStream + "data: PRIVATE_not_json\n\n"), want: "C2",
reason: "malformed stream chunk after reported usage"},
{name: "c2/provider-error-before-eof", setup: rcH2(rcStream + rcErrorFrame), want: "C2",
reason: "provider error after reported usage: PRIVATE_PROVIDER_I1"},
// C3 read outcome: the bad frame is the last bytes, so body EOF is seen.
{name: "c3/half-frame", setup: rcH2(rcStream + `data: {"choices":[{"delta":{"content":"PRIVATE_HALF`), want: "C3",
reason: "malformed stream chunk after reported usage"},
{name: "c3/invalid-frame", setup: rcH2(rcStream + "data: PRIVATE_not_json"), want: "C3",
reason: "malformed stream chunk after reported usage"},
{name: "c3/provider-error-after-usage", setup: rcH2(rcStream + strings.TrimRight(rcErrorFrame, "\n")),
want: "C3", reason: "provider error after reported usage: PRIVATE_PROVIDER_I1"},
{name: "c3/provider-error-before-usage", setup: rcH2(rcText + rcCall1 + rcCall2 + rcFinish + strings.TrimRight(rcErrorFrame, "\n")),
want: "C3", reason: "PRIVATE_PROVIDER_I1", noUsage: true},
// C4 finish reason and choices.
{name: "c4/two-finish-chunks", setup: rcH2(rcText + rcCall1 + rcCall2 + rcFinish + rcFinish + rcUsage), want: "C4", reason: rcEOFReason},
{name: "c4/finish-stop-with-tools", setup: rcH2(rcText + rcCall1 + rcCall2 + rcFinishChunk("stop") + rcUsage), want: "C4", reason: rcEOFReason},
{name: "c4/finish-length-with-tools", setup: rcH2(rcText + rcCall1 + rcCall2 + rcFinishChunk("length") + rcUsage), want: "C4", reason: rcEOFReason},
{name: "c4/multi-choice-chunk", setup: rcH2(rcText +
rcChunk(map[string]any{"delta": map[string]any{"content": "a"}}, map[string]any{"delta": map[string]any{"content": "b"}}) +
rcCall1 + rcCall2 + rcFinish + rcUsage), want: "C4", reason: rcEOFReason},
{name: "c4/truncated-at-frame-boundary-without-finish", setup: rcH2(rcText + rcCall1 + rcCall2 + rcUsage), want: "C4", reason: rcEOFReason},
// C5 usage.
{name: "c5/missing-usage", setup: rcH2(rcText + rcCall1 + rcCall2 + rcFinish), want: "C5", reason: rcEOFReason, noUsage: true},
// C6 fragments after finish.
{name: "c6/fragment-after-finish-chunk", setup: rcH2(rcText + rcCall1 + rcFinish + rcCall2 + rcUsage), want: "C6", reason: rcEOFReason},
// C7 call identity and argument shape.
{name: "c7/missing-id", setup: rcH2(rcSingleCall("", "read_PRIVATE_I1", `{"path":"PRIVATE_I1.txt"}`)), want: "C7", reason: rcEOFReason},
{name: "c7/missing-name", setup: rcH2(rcSingleCall("call_PRIVATE_I1", "", `{"path":"PRIVATE_I1.txt"}`)), want: "C7", reason: rcEOFReason},
{name: "c7/array-arguments", setup: rcH2(rcSingleCall("call_PRIVATE_I1", "read_PRIVATE_I1", `["PRIVATE_I1.txt"]`)), want: "C7", reason: rcEOFReason},
{name: "c7/broken-arguments", setup: rcH2(rcSingleCall("call_PRIVATE_I1", "read_PRIVATE_I1", `{"path":"PRIVATE_I1.txt"`)), want: "C7", reason: rcEOFReason},
{name: "c7/finish-without-call", setup: rcH2(rcText + rcFinish + rcUsage), want: "C7", reason: "tool call finish reason without a tool call"},
// C8 declared tools and argument schema.
{name: "c8/unknown-tool", setup: rcH2(rcSingleCall("call_PRIVATE_I1", "write_PRIVATE_I1", `{"path":"PRIVATE_I1.txt"}`)), want: "C8", reason: rcEOFReason},
{name: "c8/no-tools-declared", setup: func(t *testing.T) rcSetup {
s := rcH2(rcStream)(t)
s.tools = []ToolDecl{}
return s
}, want: "C8", reason: rcEOFReason},
{name: "c8/missing-required", setup: rcH2(rcSingleCall("call_PRIVATE_I1", "read_PRIVATE_I1", `{}`)), want: "C8", reason: rcEOFReason},
{name: "c8/blank-required", setup: rcH2(rcSingleCall("call_PRIVATE_I1", "read_PRIVATE_I1", `{"path":" "}`)), want: "C8", reason: rcEOFReason},
{name: "c8/wrong-property-type", setup: rcH2(rcSingleCall("call_PRIVATE_I1", "read_PRIVATE_I1", `{"path":7}`)), want: "C8", reason: rcEOFReason},
{name: "c8/unknown-argument", setup: rcH2(rcSingleCall("call_PRIVATE_I1", "read_PRIVATE_I1", `{"path":"a","PRIVATE":1}`)), want: "C8", reason: rcEOFReason},
// C9 tail event.
{name: "c9/comment-tail", setup: rcH2(rcStream + ": PRIVATE_keepalive\n\n"), want: "C9", reason: rcEOFReason},
}
}
func rcKnownLength(stream string) func(*testing.T) rcSetup {
return func(t *testing.T) rcSetup {
s := rcTLS1(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Content-Length", strconv.Itoa(len(stream)))
_, _ = io.WriteString(w, stream)
}))
return rcSetup{client: rcClientFor(t, s), contract: rcContract(rcModel, s.URL)}
}
}
func TestRouteContractEOFMatrix(t *testing.T) {
for _, tc := range rcCases() {
t.Run(tc.name, func(t *testing.T) {
s := tc.setup(t)
tools := s.tools
if tools == nil {
tools = rcTools
}
wantCalls := tc.calls
if wantCalls == nil {
wantCalls = []ToolCall{rcCall}
}
wantUsage := 7
if tc.noUsage {
wantUsage = 0
}
off := rcRun(t, context.Background(), s.client, nil, tools, s.opts, nil)
on := rcRun(t, context.Background(), s.client, s.contract, tools, s.opts, nil)
for _, r := range []rcResult{off, on} {
if r.end.Usage.TotalTokens != wantUsage {
t.Fatalf("usage = %+v, want total %d", r.end.Usage, wantUsage)
}
}
if off.end.TerminalObservation.ContractCheck != "" {
t.Fatalf("contract-off stream carried a contract check: %+v", off.end.TerminalObservation)
}
if tc.reason != "" && off.end.ErrorMessage != tc.reason {
t.Fatalf("contract-off failure = %q, want %q", off.end.ErrorMessage, tc.reason)
}
switch tc.want {
case CompletionDoneMarker:
if !reflect.DeepEqual(on.end, off.end) {
t.Fatalf("contract changed a [DONE] terminal:\n on=%#v\noff=%#v", on.end, off.end)
}
if on.end.Err != nil || !on.end.StreamComplete || on.end.CompletionBasis != CompletionDoneMarker ||
on.end.TerminalObservation.ContractCheck != "" || on.end.TerminalObservation.ReadOutcome != "done_marker" {
t.Fatalf("[DONE] terminal not marked done_marker: %#v", on.end)
}
rcRequireReleased(t, on, wantCalls)
case CompletionRouteContractEOF:
rcRequireRejected(t, off)
if off.end.CompletionBasis != "" || off.end.ErrorMessage != rcEOFReason {
t.Fatalf("contract-off stream was not rejected unchanged: %#v", off.end)
}
end := on.end
if end.Err != nil || end.StreamComplete || end.CompletionBasis != CompletionRouteContractEOF ||
end.TerminalObservation.ContractCheck != "" || end.TerminalObservation.ReadOutcome != "clean_eof" ||
end.Message.StopReason != StopToolCalls || end.FinishReason != "tool_calls" || end.HTTPStatus != http.StatusOK {
t.Fatalf("contract stream was not released: %#v obs=%+v", end, end.TerminalObservation)
}
if end.Message.Content != off.end.Message.Content || !reflect.DeepEqual(end.Message.Thinking, off.end.Message.Thinking) {
t.Fatalf("released message changed content: %#v vs %#v", end.Message, off.end.Message)
}
rcRequireReleased(t, on, wantCalls)
case "off":
if !reflect.DeepEqual(on.end, off.end) {
t.Fatalf("zero contract changed the terminal:\n on=%#v\noff=%#v", on.end, off.end)
}
rcRequireRejected(t, on)
default:
rcRequireRejected(t, off)
rcRequireRejected(t, on)
if got := on.end.TerminalObservation.ContractCheck; got != tc.want {
t.Fatalf("contract check = %q, want %q (obs=%+v)", got, tc.want, on.end.TerminalObservation)
}
stripped := on.end
obs := *on.end.TerminalObservation
obs.ContractCheck = ""
stripped.TerminalObservation = &obs
if !reflect.DeepEqual(stripped, off.end) {
t.Fatalf("rejection differs from contract-off beyond contractCheck:\n on=%#v %+v\noff=%#v %+v",
stripped, stripped.TerminalObservation, off.end, off.end.TerminalObservation)
}
}
})
}
}
func rcRequireReleased(t *testing.T, r rcResult, calls []ToolCall) {
t.Helper()
if r.releasedMessages != 1 || r.releasedCalls != len(calls) || !reflect.DeepEqual(r.end.Message.ToolCalls, calls) {
t.Fatalf("released %d messages / %d calls %#v, want 1 / %#v", r.releasedMessages, r.releasedCalls, r.end.Message.ToolCalls, calls)
}
}
func rcRequireRejected(t *testing.T, r rcResult) {
t.Helper()
end := r.end
if r.releasedMessages != 0 || r.releasedCalls != 0 || len(end.Message.ToolCalls) != 0 || end.Err == nil ||
end.StreamComplete || end.CompletionBasis != "" || end.TerminalObservation.CompletionBasis != "" {
t.Fatalf("stream was released: %#v", end)
}
}
func TestRouteContractEOFCancelDuringReadAborts(t *testing.T) {
server := rcHTTP2(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rcWrite(rcText+rcCall1+rcCall2+rcFinish+rcUsage)(w, r)
<-r.Context().Done()
}))
client := rcClientFor(t, server)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
res := rcRun(t, ctx, client, rcContract(rcModel, server.URL), rcTools, StreamOptions{}, func(ev Event) {
if ev.Kind == KindToolCallDelta {
cancel()
}
})
end := res.end
if end.Kind != KindError || end.Message.StopReason != StopAborted || end.TerminalObservation.ReadOutcome != "cancelled" ||
end.TerminalObservation.ContractCheck != "" || end.CompletionBasis != "" || res.releasedMessages != 0 ||
res.releasedCalls != 0 || len(end.Message.ToolCalls) != 0 {
t.Fatalf("cancelled contract stream was not aborted: %#v obs=%+v", end, end.TerminalObservation)
}
}
func TestParseSSEUnchangedWithoutDone(t *testing.T) {
var end Event
for ev := range ParseSSE(context.Background(), strings.NewReader(rcStream)) {
end = ev
}
if end.Kind != KindMessageEnd || end.ErrorMessage != rcEOFReason || len(end.Message.ToolCalls) != 0 ||
end.StreamComplete || end.CompletionBasis != "" || end.Usage.TotalTokens != 7 ||
end.TerminalObservation == nil || end.TerminalObservation.CompletionBasis != "" ||
end.TerminalObservation.ContractCheck != "" || end.TerminalObservation.BodyBoundary != nil {
t.Fatalf("ParseSSE changed without [DONE]: %#v", end)
}
raw, err := json.Marshal(end.TerminalObservation)
if err != nil || strings.Contains(string(raw), "completionBasis") || strings.Contains(string(raw), "contractCheck") {
t.Fatalf("ParseSSE observation JSON changed: %s err=%v", raw, err)
}
var done Event
for ev := range ParseSSE(context.Background(), strings.NewReader(rcStream+rcDone)) {
done = ev
}
if done.Err != nil || !done.StreamComplete || done.CompletionBasis != "" || done.TerminalObservation.CompletionBasis != "" ||
!reflect.DeepEqual(done.Message.ToolCalls, []ToolCall{rcCall}) {
t.Fatalf("ParseSSE changed with [DONE]: %#v", done)
}
}
func TestRouteContractIdentity(t *testing.T) {
sum := sha256.Sum256([]byte("openai-chat\nm\nhttps://api.example.com/v1"))
if got, want := RouteContractIdentity("m", "https://api.example.com/v1"), hex.EncodeToString(sum[:]); got != want {
t.Fatalf("identity = %s, want %s", got, want)
}
if got := RouteContractIdentity("m", "https://api.example.com/v1"); got != strings.ToLower(got) || len(got) != 64 {
t.Fatalf("identity is not lowercase hex: %s", got)
}
}
func TestNormalizeRouteBaseURL(t *testing.T) {
tests := []struct {
raw, want string
ok bool
}{
{"https://api.example.com/v1", "https://api.example.com/v1", true},
{"HTTPS://API.Example.COM/v1/", "https://api.example.com/v1", true},
{"https://api.example.com:8443/v1//", "https://api.example.com:8443/v1", true},
{"https://api.example.com", "https://api.example.com", true},
{"https://api.example.com/V1", "https://api.example.com/V1", true},
{"http://api.example.com/v1", "", false},
{"https://user@api.example.com/v1", "", false},
{"https://user:pw@api.example.com/v1", "", false},
{"https://api.example.com/v1?x=1", "", false},
{"https://api.example.com/v1?", "", false},
{"https://api.example.com/v1#frag", "", false},
{"https://api.example.com/v1#", "", false},
{"https:api.example.com", "", false},
{"https:///v1", "", false},
{"", "", false},
{"://bad", "", false},
}
for _, tc := range tests {
got, ok := NormalizeRouteBaseURL(tc.raw)
if got != tc.want || ok != tc.ok {
t.Fatalf("NormalizeRouteBaseURL(%q) = %q, %v; want %q, %v", tc.raw, got, ok, tc.want, tc.ok)
}
}
}