iop/apps/edge/internal/openai/hot_path_anthropic_gate_test.go
toki 495996fee4 feat(openai): 핫패스 에이전트 실행 경로를 확장한다
Anthropic·Chat 게이트와 관찰·종료 제어를 통합하고 관련 계약·검증 산출물을 반영한다.
2026-08-06 00:09:24 +09:00

629 lines
30 KiB
Go

package openai
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
iop "iop/proto/gen/iop"
)
type hotPathAnthropicSSEEvent struct {
name string
payload map[string]any
}
func TestHotPathAnthropicDirectStreamCodec(t *testing.T) {
tests := []struct {
name, profile, responseID, providerToolID, providerBody string
wantSignature string
}{
{
name: "native provider", profile: "anthropic", responseID: "msg-anthropic-gate", providerToolID: "provider-native-tool",
providerBody: strings.Join([]string{
`data: {"type":"message_start","message":{"id":"msg-anthropic-gate","type":"message","role":"assistant","content":[],"usage":{"input_tokens":9,"output_tokens":0,"cache_read_input_tokens":2}}}`,
`data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}}`,
`data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"plan "}}`,
`data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"now"}}`,
`data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"sig-native"}}`,
`data: {"type":"content_block_stop","index":0}`,
`data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}`,
`data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"alpha "}}`,
`data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"omega"}}`,
`data: {"type":"content_block_stop","index":1}`,
`data: {"type":"content_block_start","index":2,"content_block":{"type":"tool_use","id":"provider-native-tool","name":"read_file","input":{}}}`,
`data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"{\"path\":"}}`,
`data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"\"README.md\"}"}}`,
`data: {"type":"content_block_stop","index":2}`,
`data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":7}}`,
`data: {"type":"message_stop"}`, "",
}, "\n\n"),
wantSignature: "sig-native",
},
{
name: "OpenAI provider", profile: "openai", responseID: "chatcmpl-anthropic-gate", providerToolID: "provider-openai-tool",
providerBody: strings.Join([]string{
`data: {"id":"chatcmpl-anthropic-gate","created":1777002001,"choices":[{"index":0,"delta":{"reasoning_content":"plan "},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-anthropic-gate","created":1777002001,"choices":[{"index":0,"delta":{"reasoning_content":"now"},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-anthropic-gate","created":1777002001,"choices":[{"index":0,"delta":{"content":"alpha "},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-anthropic-gate","created":1777002001,"choices":[{"index":0,"delta":{"content":"omega"},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-anthropic-gate","created":1777002001,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"provider-openai-tool","type":"function","function":{"name":"read_file","arguments":"{\"path\":"}}]},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-anthropic-gate","created":1777002001,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"README.md\"}"}}]},"finish_reason":null}]}`,
`data: {"id":"chatcmpl-anthropic-gate","created":1777002001,"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":9,"completion_tokens":7,"total_tokens":16}}`,
`data: [DONE]`, "",
}, "\n\n"),
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
var decoded normalizedStageOutput
var decodeErr error
if test.profile == "anthropic" {
decoded, decodeErr = decodeAnthropicPresetSSE([]byte(test.providerBody))
} else {
decoded, decodeErr = decodeOpenAIPresetSSE([]byte(test.providerBody))
}
if decodeErr != nil || len(decoded.ToolCalls) != 1 || len(decoded.Deltas) != 6 {
t.Fatalf("provider fixture decode: output=%+v err=%v", decoded, decodeErr)
}
candidate := anthropicTestCandidate(t, test.profile)
fragments := splitAnthropicFixture([]byte(test.providerBody), 13, 79, 211, len(test.providerBody)-17)
contentType := "text/event-stream"
srv, fake := newHotPathHandlerServer(t, candidate, anthropicTunnelFrames(http.StatusOK, contentType, fragments...))
response := serveHotPathAnthropicBody(t, srv, `{"model":"virtual-model","max_tokens":64,"messages":[{"role":"user","content":"hello"}],"tools":[{"name":"read_file","description":"read","input_schema":{"type":"object"}}],"stream":true}`)
if response.Code != http.StatusOK || response.Header().Get("Content-Type") != "text/event-stream" {
t.Fatalf("response mismatch: status=%d headers=%v body=%s", response.Code, response.Header(), response.Body.String())
}
events := decodeHotPathAnthropicSSE(t, response.Body.String())
assertHotPathAnthropicDirectEvents(t, events, test.responseID, test.wantSignature)
if fake.poolSubmitCountSnapshot() != 1 {
t.Fatalf("selector submissions=%d, want 1", fake.poolSubmitCountSnapshot())
}
assertHotPathWaiting(t, srv, test.responseID+"-tool-1", test.providerToolID)
})
}
}
func TestHotPathAnthropicDirectStreamPreservesEmptyToolInput(t *testing.T) {
providerBody := strings.Join([]string{
`data: {"type":"message_start","message":{"id":"msg-empty-tool","type":"message","role":"assistant","content":[],"usage":{"input_tokens":5,"output_tokens":0}}}`,
`data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"provider-zero-arg-tool","name":"list_dir","input":{}}}`,
`data: {"type":"content_block_stop","index":0}`,
`data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":4}}`,
`data: {"type":"message_stop"}`, "",
}, "\n\n")
candidate := anthropicTestCandidate(t, "anthropic")
contentType := "text/event-stream"
srv, fake := newHotPathHandlerServer(t, candidate, anthropicTunnelFrames(http.StatusOK, contentType, []byte(providerBody)))
response := serveHotPathAnthropicBody(t, srv, `{"model":"virtual-model","max_tokens":64,"messages":[{"role":"user","content":"list files"}],"tools":[{"name":"list_dir","description":"list","input_schema":{"type":"object"}}],"stream":true}`)
if response.Code != http.StatusOK || response.Header().Get("Content-Type") != "text/event-stream" {
t.Fatalf("response mismatch: status=%d headers=%v body=%s", response.Code, response.Header(), response.Body.String())
}
events := decodeHotPathAnthropicSSE(t, response.Body.String())
wantNames := []string{
"message_start",
"content_block_start", "content_block_delta", "content_block_stop",
"message_delta", "message_stop",
}
if got := hotPathAnthropicEventNames(events); strings.Join(got, ",") != strings.Join(wantNames, ",") {
t.Fatalf("event order=%v, want %v; body=%s", got, wantNames, response.Body.String())
}
var toolID, toolName, partialJSON string
var deltaCount int
for _, event := range events {
switch event.name {
case "content_block_start":
block := hotPathAnthropicMap(t, event.payload["content_block"])
if block["type"] == "tool_use" {
toolID, _ = block["id"].(string)
toolName, _ = block["name"].(string)
}
case "content_block_delta":
delta := hotPathAnthropicMap(t, event.payload["delta"])
if delta["type"] == "input_json_delta" {
deltaCount++
partialJSON, _ = delta["partial_json"].(string)
}
}
}
if toolID != "msg-empty-tool-tool-1" || toolName != "list_dir" || deltaCount != 1 || partialJSON != "{}" {
t.Fatalf("empty tool preservation mismatch: toolID=%q toolName=%q deltaCount=%d partialJSON=%q", toolID, toolName, deltaCount, partialJSON)
}
if fake.poolSubmitCountSnapshot() != 1 {
t.Fatalf("selector submissions=%d, want 1", fake.poolSubmitCountSnapshot())
}
assertHotPathWaiting(t, srv, "msg-empty-tool-tool-1", "provider-zero-arg-tool")
}
func TestHotPathAnthropicLightStreamAggregatesStages(t *testing.T) {
fixture := newScriptedLightFixture(t, "anthropic", false)
fixture.service.responses[3] = func(string) string {
return scriptedLightCompletionWithUsage("anthropic", "local-visible", "local-reason", 5, 3)
}
fixture.service.responses[4] = func(requestID string) string {
return scriptedReviewWriteWithUsage("anthropic", requestID, 7, 4)
}
prepare := fixture.request()
fixture.consumeToolResponse(prepare, []string{`{"written":true}`})
pair := fixture.request()
fixture.consumeToolResponse(pair, []string{`{"written":true}`, `{"written":true}`})
localRead := fixture.request()
fixture.consumeToolResponse(localRead, []string{`{"written":true}`})
before := len(fixture.service.snapshots())
response := fixture.requestWithOptions(64, true)
if response.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
if got := len(fixture.service.snapshots()) - before; got != 2 {
t.Fatalf("same-turn provider stages=%d, want 2", got)
}
requests := fixture.service.snapshots()
assertCapturedHotPathBudget(t, requests[len(requests)-2], fixture.service.candidate, 64)
assertCapturedHotPathBudget(t, requests[len(requests)-1], fixture.service.candidate, 61)
events := decodeHotPathAnthropicSSE(t, response.Body.String())
assertHotPathAnthropicBlockIndexes(t, events, 5)
wantNames := []string{
"message_start",
"content_block_start", "content_block_delta", "content_block_delta", "content_block_stop",
"content_block_start", "content_block_delta", "content_block_stop",
"content_block_start", "content_block_delta", "content_block_delta", "content_block_stop",
"content_block_start", "content_block_delta", "content_block_stop",
"content_block_start", "content_block_delta", "content_block_stop",
"message_delta", "message_stop",
}
if got := hotPathAnthropicEventNames(events); strings.Join(got, ",") != strings.Join(wantNames, ",") {
t.Fatalf("event order=%v, want %v; body=%s", got, wantNames, response.Body.String())
}
startMessage := hotPathAnthropicMap(t, events[0].payload["message"])
requestID, snapshot := soleHotPathSnapshot(t, fixture.server)
if startMessage["id"] != "msg-light-complete" || startMessage["id"] == requestID || startMessage["model"] != "virtual-model" {
t.Fatalf("outer identity mismatch: message=%+v logical_request=%s", startMessage, requestID)
}
wantKinds := []string{"thinking", "text", "thinking", "text", "tool_use"}
var gotKinds, thinking, text []string
var toolID, toolName, toolArgs, stopReason string
for _, event := range events {
switch event.name {
case "content_block_start":
block := hotPathAnthropicMap(t, event.payload["content_block"])
gotKinds = append(gotKinds, fmt.Sprint(block["type"]))
if block["type"] == "tool_use" {
toolID, _ = block["id"].(string)
toolName, _ = block["name"].(string)
}
case "content_block_delta":
delta := hotPathAnthropicMap(t, event.payload["delta"])
switch delta["type"] {
case "thinking_delta":
thinking = append(thinking, fmt.Sprint(delta["thinking"]))
case "text_delta":
text = append(text, fmt.Sprint(delta["text"]))
case "input_json_delta":
toolArgs += fmt.Sprint(delta["partial_json"])
}
case "message_delta":
delta := hotPathAnthropicMap(t, event.payload["delta"])
stopReason, _ = delta["stop_reason"].(string)
usage := hotPathAnthropicMap(t, event.payload["usage"])
if usage["input_tokens"] != float64(12) || usage["output_tokens"] != float64(7) {
t.Fatalf("aggregate usage=%+v, want input=12 output=7", usage)
}
}
}
if strings.Join(gotKinds, ",") != strings.Join(wantKinds, ",") ||
strings.Join(thinking, "") != "local-reasonreview-reason" || strings.Join(text, "") != "local-visiblereview-visible" ||
toolName != "write_file" || !json.Valid([]byte(toolArgs)) || stopReason != "tool_use" {
t.Fatalf("multi-stage output mismatch: kinds=%v thinking=%v text=%v tool=%q/%q/%q stop=%q body=%s",
gotKinds, thinking, text, toolID, toolName, toolArgs, stopReason, response.Body.String())
}
if len(snapshot.ExpectedCallIDs) != 1 || snapshot.ExpectedCallIDs[0] != toolID {
t.Fatalf("tool correlation mismatch: tool=%q snapshot=%+v", toolID, snapshot)
}
if toolID != "msg-light-complete-tool-1" || strings.Contains(response.Body.String(), "msg-review-write") {
t.Fatalf("public identity/tool namespace leaked a later provider id: tool=%q body=%s", toolID, response.Body.String())
}
}
func TestHotPathAnthropicToolIDsAreMonotonic(t *testing.T) {
candidate := anthropicTestCandidate(t, "anthropic")
providerBody := []byte(`{"id":"msg-anthropic-tools","type":"message","role":"assistant","content":[{"type":"tool_use","id":"provider-tool-a","name":"read_file","input":{"path":"a"}},{"type":"tool_use","id":"provider-tool-b","name":"read_file","input":{"path":"b"}}],"stop_reason":"tool_use","usage":{"input_tokens":4,"output_tokens":3}}`)
srv, _ := newHotPathHandlerServer(t, candidate, anthropicTunnelFrames(http.StatusOK, "application/json", providerBody))
response := serveHotPathAnthropic(t, srv, true)
if response.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
events := decodeHotPathAnthropicSSE(t, response.Body.String())
assertHotPathAnthropicBlockIndexes(t, events, 2)
var toolIDs []string
for _, event := range events {
if event.name != "content_block_start" {
continue
}
block := hotPathAnthropicMap(t, event.payload["content_block"])
if block["type"] == "tool_use" {
toolIDs = append(toolIDs, fmt.Sprint(block["id"]))
}
}
wantIDs := []string{"msg-anthropic-tools-tool-1", "msg-anthropic-tools-tool-2"}
if fmt.Sprint(toolIDs) != fmt.Sprint(wantIDs) {
t.Fatalf("tool ids=%v, want %v; body=%s", toolIDs, wantIDs, response.Body.String())
}
requestID, snapshot := soleHotPathSnapshot(t, srv)
expectedSet := make(map[string]bool, len(snapshot.ExpectedCallIDs))
for _, id := range snapshot.ExpectedCallIDs {
expectedSet[id] = true
}
if len(snapshot.ExpectedCallIDs) != len(wantIDs) || !expectedSet[wantIDs[0]] || !expectedSet[wantIDs[1]] {
t.Fatalf("expected caller ids=%v, want %v", snapshot.ExpectedCallIDs, wantIDs)
}
srv.requestCoordinator.mu.Lock()
record := srv.requestCoordinator.requests[requestID]
mapping := map[string]string{}
if record != nil {
for _, id := range wantIDs {
mapping[id] = record.publicToProvider[id]
}
}
srv.requestCoordinator.mu.Unlock()
if mapping[wantIDs[0]] != "provider-tool-a" || mapping[wantIDs[1]] != "provider-tool-b" {
t.Fatalf("provider tool mapping=%v", mapping)
}
}
func TestHotPathAnthropicCallerCapAndNonStream(t *testing.T) {
candidate := anthropicTestCandidate(t, "anthropic")
providerBody := []byte(`{"id":"msg-anthropic-cap","type":"message","role":"assistant","content":[{"type":"text","text":"abcdefghij"}],"stop_reason":"end_turn","usage":{"input_tokens":3,"output_tokens":2}}`)
srv, fake := newHotPathHandlerServer(t, candidate, anthropicTunnelFrames(http.StatusOK, "application/json", providerBody[:31], providerBody[31:]))
response := serveHotPathAnthropicBody(t, srv, `{"model":"virtual-model","max_tokens":2,"messages":[{"role":"user","content":"cap"}],"stream":false}`)
if response.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
var decoded struct {
ID string `json:"id"`
Model string `json:"model"`
Content []json.RawMessage `json:"content"`
StopReason string `json:"stop_reason"`
Usage anthropicUsage `json:"usage"`
}
if err := json.Unmarshal(response.Body.Bytes(), &decoded); err != nil {
t.Fatal(err)
}
if decoded.ID != "msg-anthropic-cap" || decoded.Model != "virtual-model" || decoded.StopReason != "end_turn" ||
decoded.Usage.InputTokens != 3 || decoded.Usage.OutputTokens != 2 || len(decoded.Content) != 1 {
t.Fatalf("non-stream envelope mismatch: %+v body=%s", decoded, response.Body.String())
}
var textBlock struct {
Type string `json:"type"`
Text string `json:"text"`
}
if err := json.Unmarshal(decoded.Content[0], &textBlock); err != nil || textBlock.Type != "text" || textBlock.Text != "abcdefghij" {
t.Fatalf("provider-token content=%+v err=%v", textBlock, err)
}
if fake.poolSubmitCountSnapshot() != 1 {
t.Fatalf("selector submissions=%d, want 1", fake.poolSubmitCountSnapshot())
}
var upstream map[string]any
if bodies := fake.tunnelBodiesSnapshot(); len(bodies) != 1 {
t.Fatalf("upstream body count=%d, want 1", len(bodies))
} else if err := json.Unmarshal(bodies[0], &upstream); err != nil || upstream["max_tokens"] != float64(2) {
t.Fatalf("upstream max_tokens was not retained: body=%s decoded=%+v err=%v", bodies[0], upstream, err)
}
assertHotPathTerminal(t, srv)
}
func TestHotPathAnthropicErrorBoundaries(t *testing.T) {
t.Run("required max tokens fails before dispatch", func(t *testing.T) {
candidate := anthropicTestCandidate(t, "anthropic")
srv, fake := newHotPathHandlerServer(t, candidate, nil)
response := serveHotPathAnthropicBody(t, srv, `{"model":"virtual-model","messages":[{"role":"user","content":"missing cap"}],"stream":true}`)
if response.Code != http.StatusBadRequest || !strings.Contains(response.Body.String(), `"type":"invalid_request_error"`) ||
strings.Contains(response.Body.String(), "message_start") {
t.Fatalf("pre-dispatch validation mismatch: status=%d body=%s", response.Code, response.Body.String())
}
if fake.poolSubmitCountSnapshot() != 0 {
t.Fatalf("selector submissions=%d, want 0", fake.poolSubmitCountSnapshot())
}
})
t.Run("provider error before commit is JSON", func(t *testing.T) {
candidate := anthropicTestCandidate(t, "anthropic")
frames := make(chan *iop.ProviderTunnelFrame, 2)
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: http.StatusBadGateway}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
srv, fake := newHotPathHandlerServer(t, candidate, frames)
response := serveHotPathAnthropic(t, srv, true)
if response.Code != http.StatusBadGateway || !strings.Contains(response.Body.String(), `"type":"api_error"`) ||
strings.Contains(response.Body.String(), "message_start") || strings.Contains(response.Body.String(), "message_stop") {
t.Fatalf("pre-commit error mismatch: status=%d body=%s", response.Code, response.Body.String())
}
if fake.poolSubmitCountSnapshot() != 1 {
t.Fatalf("selector submissions=%d, want 1", fake.poolSubmitCountSnapshot())
}
assertHotPathTerminal(t, srv)
})
t.Run("missing provider identity fails before commit", func(t *testing.T) {
candidate := anthropicTestCandidate(t, "anthropic")
body := []byte("data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"unsafe\"}}\n\n")
srv, fake := newHotPathHandlerServer(t, candidate, anthropicTunnelFrames(http.StatusOK, "text/event-stream", body))
response := serveHotPathAnthropic(t, srv, true)
if response.Code != http.StatusBadGateway || !strings.Contains(response.Body.String(), `"type":"api_error"`) ||
strings.Contains(response.Body.String(), "message_start") {
t.Fatalf("missing-identity failure mismatch: status=%d body=%s", response.Code, response.Body.String())
}
if fake.poolSubmitCountSnapshot() != 1 {
t.Fatalf("selector submissions=%d, want 1", fake.poolSubmitCountSnapshot())
}
})
t.Run("conflicting provider identity fails after commit", func(t *testing.T) {
candidate := anthropicTestCandidate(t, "anthropic")
frames := make(chan *iop.ProviderTunnelFrame, 4)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: http.StatusOK,
Headers: map[string]string{"Content-Type": "text/event-stream"},
}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(strings.Join([]string{
`data: {"type":"message_start","message":{"id":"msg-first","usage":{"input_tokens":1}}}`,
`data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":"visible"}}`, "",
}, "\n\n"))}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
Body: []byte("data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg-conflict\",\"usage\":{\"input_tokens\":1}}}\n\n")}
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
close(frames)
srv, _ := newHotPathHandlerServer(t, candidate, frames)
response := serveHotPathAnthropic(t, srv, true)
events := decodeHotPathAnthropicSSE(t, response.Body.String())
if got := hotPathAnthropicEventNames(events); strings.Join(got, ",") != "message_start,content_block_start,content_block_delta,error" ||
strings.Contains(response.Body.String(), "msg-conflict") || strings.Contains(response.Body.String(), "message_stop") {
t.Fatalf("conflicting-identity terminal mismatch: events=%v body=%s", got, response.Body.String())
}
})
}
func TestHotPathAnthropicFlushesBeforeEndAndErrorsAfterCommit(t *testing.T) {
candidate := anthropicTestCandidate(t, "anthropic")
frames := make(chan *iop.ProviderTunnelFrame, 4)
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: http.StatusOK,
Headers: map[string]string{"Content-Type": "text/event-stream"}, RunId: "run-anthropic-live",
}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, RunId: "run-anthropic-live",
Body: []byte(strings.Join([]string{
`data: {"type":"message_start","message":{"id":"msg-anthropic-live","usage":{"input_tokens":3,"output_tokens":0}}}`,
`data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`,
`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"early-visible"}}`, "",
}, "\n\n")),
}
srv, fake := newHotPathHandlerServer(t, candidate, frames)
httpServer := httptest.NewServer(srv.routes())
defer httpServer.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
body := `{"model":"virtual-model","max_tokens":64,"messages":[{"role":"user","content":"flush"}],"stream":true}`
request, err := http.NewRequestWithContext(ctx, http.MethodPost, httpServer.URL+"/v1/messages", strings.NewReader(body))
if err != nil {
t.Fatal(err)
}
request.Header.Set(anthropicVersionHeader, anthropicSupportedVersion)
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatalf("stream request did not flush before provider END: %v", err)
}
defer response.Body.Close()
reader := bufio.NewReader(response.Body)
var early strings.Builder
for range 3 {
frame, err := readHotPathSSEFrame(reader)
if err != nil {
t.Fatalf("read pre-END Anthropic frame: %v", err)
}
early.WriteString(frame)
}
if response.StatusCode != http.StatusOK || !strings.Contains(early.String(), `"id":"msg-anthropic-live"`) ||
!strings.Contains(early.String(), `"text":"early-visible"`) || strings.Contains(early.String(), "message_stop") {
t.Fatalf("pre-END flush mismatch: status=%d body=%s", response.StatusCode, early.String())
}
frames <- &iop.ProviderTunnelFrame{
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_ERROR, Error: "provider failed", RunId: "run-anthropic-live",
}
close(frames)
rest, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("read post-commit error: %v", err)
}
wire := early.String() + string(rest)
events := decodeHotPathAnthropicSSE(t, wire)
if got := hotPathAnthropicEventNames(events); strings.Join(got, ",") != "message_start,content_block_start,content_block_delta,error" ||
strings.Count(wire, "event: error") != 1 || strings.Contains(wire, "message_delta") || strings.Contains(wire, "message_stop") {
t.Fatalf("post-commit provider error mismatch: events=%v body=%s", got, wire)
}
if fake.poolSubmitCountSnapshot() != 1 {
t.Fatalf("selector submissions=%d, want 1", fake.poolSubmitCountSnapshot())
}
}
func serveHotPathAnthropicBody(t *testing.T, srv *Server, body string) *httptest.ResponseRecorder {
t.Helper()
request := httptest.NewRequest(http.MethodPost, "/v1/messages", strings.NewReader(body))
request.Header.Set(anthropicVersionHeader, anthropicSupportedVersion)
response := httptest.NewRecorder()
srv.routes().ServeHTTP(response, request)
return response
}
func decodeHotPathAnthropicSSE(t *testing.T, body string) []hotPathAnthropicSSEEvent {
t.Helper()
body = strings.ReplaceAll(body, "\r\n", "\n")
var events []hotPathAnthropicSSEEvent
for _, frame := range strings.Split(body, "\n\n") {
frame = strings.TrimSpace(frame)
if frame == "" {
continue
}
var name string
var data []string
for _, line := range strings.Split(frame, "\n") {
switch {
case strings.HasPrefix(line, "event:"):
name = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
case strings.HasPrefix(line, "data:"):
data = append(data, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
}
}
if name == "" || len(data) == 0 {
t.Fatalf("malformed Anthropic SSE frame %q", frame)
}
var payload map[string]any
if err := json.Unmarshal([]byte(strings.Join(data, "\n")), &payload); err != nil {
t.Fatalf("decode Anthropic SSE %q: %v", frame, err)
}
if payload["type"] != name {
t.Fatalf("event/type mismatch: event=%q payload=%+v", name, payload)
}
events = append(events, hotPathAnthropicSSEEvent{name: name, payload: payload})
}
return events
}
func hotPathAnthropicEventNames(events []hotPathAnthropicSSEEvent) []string {
names := make([]string, 0, len(events))
for _, event := range events {
names = append(names, event.name)
}
return names
}
func hotPathAnthropicMap(t *testing.T, value any) map[string]any {
t.Helper()
mapped, ok := value.(map[string]any)
if !ok {
t.Fatalf("value is not an object: %#v", value)
}
return mapped
}
func assertHotPathAnthropicDirectEvents(t *testing.T, events []hotPathAnthropicSSEEvent, responseID, signature string) {
t.Helper()
assertHotPathAnthropicBlockIndexes(t, events, 3)
wantNames := []string{
"message_start",
"content_block_start", "content_block_delta", "content_block_delta",
}
if signature != "" {
wantNames = append(wantNames, "content_block_delta")
}
wantNames = append(wantNames,
"content_block_stop",
"content_block_start", "content_block_delta", "content_block_delta", "content_block_stop",
"content_block_start", "content_block_delta", "content_block_delta", "content_block_stop",
"message_delta", "message_stop",
)
if got := hotPathAnthropicEventNames(events); strings.Join(got, ",") != strings.Join(wantNames, ",") {
t.Fatalf("event order=%v, want %v", got, wantNames)
}
message := hotPathAnthropicMap(t, events[0].payload["message"])
if message["id"] != responseID || message["model"] != "virtual-model" {
t.Fatalf("message_start mismatch: %+v", message)
}
if signature != "" {
startUsage := hotPathAnthropicMap(t, message["usage"])
if startUsage["input_tokens"] != float64(9) {
t.Fatalf("message_start usage mismatch: %+v", message)
}
}
wantKinds := []string{"thinking", "text", "tool_use"}
var kinds, thinking, text, toolFragments []string
var toolID, toolName, stopReason, gotSignature string
for _, event := range events {
switch event.name {
case "content_block_start":
block := hotPathAnthropicMap(t, event.payload["content_block"])
kinds = append(kinds, fmt.Sprint(block["type"]))
if block["type"] == "tool_use" {
toolID, _ = block["id"].(string)
toolName, _ = block["name"].(string)
}
case "content_block_delta":
delta := hotPathAnthropicMap(t, event.payload["delta"])
switch delta["type"] {
case "thinking_delta":
thinking = append(thinking, fmt.Sprint(delta["thinking"]))
case "text_delta":
text = append(text, fmt.Sprint(delta["text"]))
case "input_json_delta":
toolFragments = append(toolFragments, fmt.Sprint(delta["partial_json"]))
case "signature_delta":
gotSignature, _ = delta["signature"].(string)
}
case "message_delta":
delta := hotPathAnthropicMap(t, event.payload["delta"])
stopReason, _ = delta["stop_reason"].(string)
usage := hotPathAnthropicMap(t, event.payload["usage"])
if usage["input_tokens"] != float64(9) || usage["output_tokens"] != float64(7) {
t.Fatalf("terminal usage=%+v, want input=9 output=7", usage)
}
}
}
if strings.Join(kinds, ",") != strings.Join(wantKinds, ",") || strings.Join(thinking, "") != "plan now" ||
strings.Join(text, "") != "alpha omega" || strings.Join(toolFragments, "") != `{"path":"README.md"}` ||
len(toolFragments) != 2 || toolID != responseID+"-tool-1" || toolName != "read_file" ||
stopReason != "tool_use" || gotSignature != signature {
t.Fatalf("stream aggregate mismatch: kinds=%v thinking=%v text=%v tool=%q/%q/%v stop=%q signature=%q",
kinds, thinking, text, toolID, toolName, toolFragments, stopReason, gotSignature)
}
}
func assertHotPathAnthropicBlockIndexes(t *testing.T, events []hotPathAnthropicSSEEvent, wantBlocks int) {
t.Helper()
nextStart := 0
active := -1
for _, event := range events {
switch event.name {
case "content_block_start":
index := int(event.payload["index"].(float64))
if active != -1 || index != nextStart {
t.Fatalf("non-monotonic block start: active=%d index=%d next=%d", active, index, nextStart)
}
active = index
nextStart++
case "content_block_delta":
index := int(event.payload["index"].(float64))
if index != active {
t.Fatalf("block delta index=%d, active=%d", index, active)
}
case "content_block_stop":
index := int(event.payload["index"].(float64))
if index != active {
t.Fatalf("block stop index=%d, active=%d", index, active)
}
active = -1
}
}
if active != -1 || nextStart != wantBlocks {
t.Fatalf("block boundary mismatch: active=%d starts=%d want=%d", active, nextStart, wantBlocks)
}
}