iop/apps/edge/internal/openai/responses_handler_test.go
toki 01dc2ef78b refactor: readability baseline 및 테스트 구조 개선
- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동
- 새 테스트 파일 추가 (edge, node, client, config, readability)
- readability_audit 스크립트 및 baseline 추가
- roadmap/SDD 문서 갱신
- agent-client/pi/extensions/openai-sampling-parameters 추가
2026-07-17 16:02:12 +09:00

273 lines
9.8 KiB
Go

package openai
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"iop/packages/go/config"
iop "iop/proto/gen/iop"
)
func TestResponsesDispatchesNonStreamingRequest(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: " world"}
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}
srv := NewServer(config.EdgeOpenAIConf{
Adapter: "ollama",
Target: "llama-fixed",
SessionID: "configured-session",
TimeoutSec: 42,
}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"say hello",
"instructions":"Use short answers.",
"max_output_tokens":16,
"temperature":0.2,
"top_p":0.9,
"background":false
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Target != "llama-fixed" {
t.Fatalf("target: got %q, want llama-fixed", fake.req.Target)
}
if fake.req.Prompt != "Use short answers.\n\nsay hello" {
t.Fatalf("prompt: got %q", fake.req.Prompt)
}
options, ok := fake.req.Input["options"].(map[string]any)
if !ok {
t.Fatalf("options not passed: %+v", fake.req.Input)
}
if options["max_tokens"].(int) != 16 || options["temperature"].(float64) != 0.2 || options["top_p"].(float64) != 0.9 {
t.Fatalf("unexpected response options: %+v", options)
}
if fake.req.TimeoutSec != 42 {
t.Fatalf("timeout: got %d, want 42", fake.req.TimeoutSec)
}
if fake.req.SessionID != "configured-session" {
t.Fatalf("session_id: got %q, want configured-session", fake.req.SessionID)
}
if fake.req.Background {
t.Fatal("background should remain false for responses")
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp.OutputText != "hello world" {
t.Fatalf("output_text: got %q", resp.OutputText)
}
if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != "hello world" {
t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
}
}
func TestResponsesRejectsUnsupportedRequests(t *testing.T) {
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, &fakeRunService{}, nil)
cases := []struct {
name string
method string
body string
want int
}{
{"wrong method", http.MethodGet, "", http.StatusMethodNotAllowed},
{"stream=true", http.MethodPost, `{"model":"m","input":"hi","stream":true}`, http.StatusBadRequest},
{"empty input", http.MethodPost, `{"model":"m","input":""}`, http.StatusBadRequest},
{"non-string input", http.MethodPost, `{"model":"m","input":["a","b"]}`, http.StatusBadRequest},
{"options wrapper unsupported", http.MethodPost, `{"model":"m","input":"hi","options":{"max_output_tokens":16}}`, http.StatusBadRequest},
{"background unsupported", http.MethodPost, `{"model":"m","input":"hi","background":true}`, http.StatusBadRequest},
{"bad max_output_tokens", http.MethodPost, `{"model":"m","input":"hi","max_output_tokens":0}`, http.StatusBadRequest},
{"bad top_p", http.MethodPost, `{"model":"m","input":"hi","top_p":1.5}`, http.StatusBadRequest},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.method, "/v1/responses", strings.NewReader(tc.body))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != tc.want {
t.Fatalf("got %d want %d body=%s", w.Code, tc.want, w.Body.String())
}
})
}
}
func TestResponsesConfiguredTargetWinsOverRequestModel(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "config-target"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"test"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Target != "config-target" {
t.Fatalf("target: got %q, want config-target", fake.req.Target)
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if resp.Model != "client-model" {
t.Fatalf("model: got %q, want client-model", resp.Model)
}
}
func TestResponsesReturnsOpenAICompatibleShape(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer"}
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"say hello"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if !strings.HasPrefix(resp.ID, "resp-") {
t.Fatalf("id: got %q, want resp- prefix", resp.ID)
}
if resp.Model != "client-model" {
t.Fatalf("model: got %q", resp.Model)
}
if resp.OutputText != "answer" {
t.Fatalf("output_text: got %q", resp.OutputText)
}
if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != "answer" {
t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
}
if resp.Usage.TotalTokens != 2 {
t.Fatalf("usage.total_tokens: got %d", resp.Usage.TotalTokens)
}
}
func TestResponsesSanitizesKnownSentinelToken(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden <|mask_end|>"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer <|mask_end|>"}
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 1, OutputTokens: 1}}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"say hello"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if strings.Contains(resp.OutputText, "<|mask_end|>") || strings.Contains(w.Body.String(), "<|mask_end|>") {
t.Fatalf("sentinel leaked in responses output: %s", w.Body.String())
}
if resp.OutputText != "answer" {
t.Fatalf("output_text: got %q, want answer", resp.OutputText)
}
}
func TestResponsesUsageDefaultsToZeroObject(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama"}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"m",
"input":"hi"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
var body map[string]json.RawMessage
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("decode: %v", err)
}
if _, ok := body["usage"]; !ok {
t.Fatal("usage field missing from response")
}
var usage openAIUsage
if err := json.Unmarshal(body["usage"], &usage); err != nil {
t.Fatalf("usage is not an object: %s", body["usage"])
}
}
func TestResponsesStrictOutputNormalizesAgentResponse(t *testing.T) {
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
fake.events <- &iop.RunEvent{Type: "delta", Delta: "plain answer"}
fake.events <- &iop.RunEvent{Type: "complete"}
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "llama", StrictOutput: true}, fake, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{
"model":"client-model",
"input":"Once you've completed the user's task, you must use the attempt_completion tool to present the result.\n\n<attempt_completion>\n<result>done</result>\n</attempt_completion>\n\nfinish"
}`))
w := httptest.NewRecorder()
srv.routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
}
if fake.req.Metadata["strict_output"] != "true" {
t.Fatalf("strict_output metadata: got %q", fake.req.Metadata["strict_output"])
}
if fake.req.Input["think"] != false {
t.Fatalf("strict output should disable thinking by default: %+v", fake.req.Input)
}
if !strings.Contains(fake.req.Prompt, "output exactly one <attempt_completion> block") {
t.Fatalf("strict contract instruction not injected:\n%s", fake.req.Prompt)
}
var resp responsesResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
want := "<attempt_completion>\n<result>plain answer</result>\n</attempt_completion>"
if resp.OutputText != want {
t.Fatalf("output_text:\ngot %q\nwant %q", resp.OutputText, want)
}
if len(resp.Output) == 0 || len(resp.Output[0].Content) == 0 || resp.Output[0].Content[0].Text != want {
t.Fatalf("output[0].content[0].text wrong: %+v", resp.Output)
}
}