H-337: add local Captain acceptance fixture (#63)

* H-337: add local Captain acceptance fixture

* fix(H-337): close acceptance fixture review gaps

* fix(H-337): reject non-string skill metadata

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-20 20:05:40 +08:00
committed by GitHub
co-authored by rogee
parent 60ad320e8d
commit abb1bed424
12 changed files with 818 additions and 19 deletions
@@ -0,0 +1 @@
/captain-fixture
@@ -0,0 +1,12 @@
FROM golang:1.25.13-alpine AS build
WORKDIR /src
COPY go.mod main.go ./
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /captain-fixture .
FROM alpine:3.21
RUN addgroup -S fixture && adduser -S fixture -G fixture
COPY --from=build /captain-fixture /usr/local/bin/captain-fixture
USER fixture
EXPOSE 8080
ENTRYPOINT ["captain-fixture"]
+3
View File
@@ -0,0 +1,3 @@
module gochat.local/captain-fixture
go 1.24.0
+291
View File
@@ -0,0 +1,291 @@
package main
import (
"crypto/sha256"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
)
const (
listenAddr = ":8080"
maxRequestBody = 1 << 20
knowledgeHTML = `<!doctype html><html><head><title>GoChat Local Acceptance Knowledge</title></head><body><main><h1>Local acceptance policy</h1><p id="fixture-fact">The local fixture warranty window is 37 days. Reference marker: KBASE-LOCAL-2026.</p></main></body></html>`
)
type fixtureServer struct {
mu sync.Mutex
attempts map[[32]byte]int
}
type chatRequest struct {
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
Tools []toolDefinition `json:"tools"`
Stream bool `json:"stream"`
}
type chatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
Name string `json:"name"`
ToolCallID string `json:"tool_call_id"`
ToolCalls []toolCall `json:"tool_calls"`
}
type toolDefinition struct {
Function struct {
Name string `json:"name"`
} `json:"function"`
}
type toolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
type embeddingRequest struct {
Model string `json:"model"`
Input []string `json:"input"`
}
func main() {
logger := log.New(os.Stdout, "captain-fixture ", log.LstdFlags)
server := &http.Server{Addr: listenAddr, Handler: newFixtureServer(logger), ReadHeaderTimeout: 5 * time.Second}
logger.Printf("listening addr=%s", listenAddr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal(err)
}
}
func newFixtureServer(logger *log.Logger) http.Handler {
s := &fixtureServer{attempts: make(map[[32]byte]int)}
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
mux.HandleFunc("GET /knowledge", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = io.WriteString(w, knowledgeHTML)
})
mux.HandleFunc("POST /v1/chat/completions", s.chat)
mux.HandleFunc("POST /v1/embeddings", s.embeddings)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
status := &statusWriter{ResponseWriter: w, status: http.StatusOK}
mux.ServeHTTP(status, r)
logger.Printf("method=%s path=%s status=%d", r.Method, r.URL.Path, status.status)
})
}
func (s *fixtureServer) chat(w http.ResponseWriter, r *http.Request) {
body, ok := readBody(w, r)
if !ok {
return
}
var req chatRequest
if err := json.Unmarshal(body, &req); err != nil || len(req.Messages) == 0 {
writeError(w, http.StatusBadRequest, "invalid_request")
return
}
latest := latestUserContent(req.Messages)
if strings.Contains(latest, "[fixture:fail]") {
writeError(w, http.StatusServiceUnavailable, "fixture_failure")
return
}
if strings.Contains(latest, "[fixture:retry]") && s.firstAttempt(body) {
writeError(w, http.StatusServiceUnavailable, "fixture_retry_once")
return
}
message, finishReason := fixtureChatMessage(req)
if req.Stream {
writeStream(w, req.Model, message.Content)
return
}
writeJSON(w, http.StatusOK, map[string]any{
"id": "chatcmpl-fixture", "object": "chat.completion", "created": 1, "model": req.Model,
"choices": []map[string]any{{"index": 0, "message": message, "finish_reason": finishReason}},
"usage": map[string]int{"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
})
}
func (s *fixtureServer) embeddings(w http.ResponseWriter, r *http.Request) {
body, ok := readBody(w, r)
if !ok {
return
}
var req embeddingRequest
if err := json.Unmarshal(body, &req); err != nil || len(req.Input) == 0 {
writeError(w, http.StatusBadRequest, "invalid_request")
return
}
content := strings.Join(req.Input, "\n")
if strings.Contains(content, "[fixture:embedding-fail]") {
writeError(w, http.StatusServiceUnavailable, "fixture_embedding_failure")
return
}
if strings.Contains(content, "[fixture:embedding-retry]") && s.firstAttempt(body) {
writeError(w, http.StatusServiceUnavailable, "fixture_embedding_retry_once")
return
}
vector := make([]float64, 1536)
for i := range vector {
vector[i] = 0.01
}
data := make([]map[string]any, len(req.Input))
for i := range req.Input {
data[i] = map[string]any{"object": "embedding", "index": i, "embedding": vector}
}
writeJSON(w, http.StatusOK, map[string]any{
"object": "list", "data": data, "model": req.Model,
"usage": map[string]int{"prompt_tokens": len(req.Input), "total_tokens": len(req.Input)},
})
}
func (s *fixtureServer) firstAttempt(body []byte) bool {
key := sha256.Sum256(body)
s.mu.Lock()
defer s.mu.Unlock()
s.attempts[key]++
return s.attempts[key] == 1
}
func fixtureChatMessage(req chatRequest) (chatMessage, string) {
all := allMessageContent(req.Messages)
if strings.Contains(all, "Extract factual, self-contained FAQs") {
return chatMessage{Role: "assistant", Content: `{"faqs":[{"question":"What is the local fixture warranty window?","answer":"The local fixture warranty window is 37 days."}]}`}, "stop"
}
if last := req.Messages[len(req.Messages)-1]; last.Role == "tool" {
if last.Name == "activate_skill" {
if key := firstJSONListValue(last.Content, "reference_keys"); key != "" {
return toolMessage("fixture-read", "read_skill_reference", map[string]string{"skill_name": firstCatalogSkill(all), "reference_key": key}), "tool_calls"
}
}
return chatMessage{Role: "assistant", Content: "Fixture skill path completed."}, "stop"
}
if strings.Contains(latestUserContent(req.Messages), "[fixture:skill]") && hasTool(req.Tools, "activate_skill") {
if name := firstCatalogSkill(all); name != "" {
return toolMessage("fixture-activate", "activate_skill", map[string]string{"skill_name": name}), "tool_calls"
}
}
if strings.Contains(all, "Knowledge Base Context:") {
return chatMessage{Role: "assistant", Content: "The local fixture warranty window is 37 days [FAQ 1]."}, "stop"
}
return chatMessage{Role: "assistant", Content: "Fixture chat response."}, "stop"
}
func toolMessage(id, name string, args map[string]string) chatMessage {
raw, _ := json.Marshal(args)
call := toolCall{ID: id, Type: "function"}
call.Function.Name, call.Function.Arguments = name, string(raw)
return chatMessage{Role: "assistant", ToolCalls: []toolCall{call}}
}
func firstCatalogSkill(content string) string {
const start, end = "<available_skills_json>", "</available_skills_json>"
i := strings.Index(content, start)
if i < 0 {
return ""
}
j := strings.Index(content[i+len(start):], end)
if j < 0 {
return ""
}
var catalog []struct {
Name string `json:"name"`
}
if json.Unmarshal([]byte(content[i+len(start):i+len(start)+j]), &catalog) != nil || len(catalog) == 0 {
return ""
}
return catalog[0].Name
}
func firstJSONListValue(content, key string) string {
var payload map[string]any
start, end := strings.Index(content, "{"), strings.LastIndex(content, "}")
if start < 0 || end < start || json.Unmarshal([]byte(content[start:end+1]), &payload) != nil {
return ""
}
values, _ := payload[key].([]any)
if len(values) == 0 {
return ""
}
value, _ := values[0].(string)
return value
}
func hasTool(tools []toolDefinition, name string) bool {
for _, tool := range tools {
if tool.Function.Name == name {
return true
}
}
return false
}
func allMessageContent(messages []chatMessage) string {
var b strings.Builder
for _, message := range messages {
b.WriteString(message.Content)
b.WriteByte('\n')
}
return b.String()
}
func latestUserContent(messages []chatMessage) string {
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Role == "user" {
return messages[i].Content
}
}
return ""
}
func readBody(w http.ResponseWriter, r *http.Request) ([]byte, bool) {
body, err := io.ReadAll(io.LimitReader(r.Body, maxRequestBody+1))
if err != nil || len(body) == 0 || len(body) > maxRequestBody {
writeError(w, http.StatusBadRequest, "invalid_request")
return nil, false
}
return body, true
}
func writeStream(w http.ResponseWriter, model, content string) {
w.Header().Set("Content-Type", "text/event-stream")
chunk := map[string]any{
"id": "chatcmpl-fixture", "object": "chat.completion.chunk", "created": 1, "model": model,
"choices": []map[string]any{{"index": 0, "delta": map[string]string{"role": "assistant", "content": content}, "finish_reason": "stop"}},
}
raw, _ := json.Marshal(chunk)
_, _ = fmt.Fprintf(w, "data: %s\n\ndata: [DONE]\n\n", raw)
}
func writeError(w http.ResponseWriter, status int, code string) {
writeJSON(w, status, map[string]any{"error": map[string]string{"message": "local acceptance fixture error", "type": "fixture_error", "code": code}})
}
func writeJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}
@@ -0,0 +1,96 @@
package main
import (
"bytes"
"encoding/json"
"io"
"log"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestAcceptanceFixtureContract(t *testing.T) {
server := httptest.NewServer(newFixtureServer(log.New(io.Discard, "", 0)))
t.Cleanup(server.Close)
t.Run("knowledge", func(t *testing.T) {
resp, err := http.Get(server.URL + "/knowledge")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK || !bytes.Contains(body, []byte("KBASE-LOCAL-2026")) {
t.Fatalf("knowledge status=%d marker=%t", resp.StatusCode, bytes.Contains(body, []byte("KBASE-LOCAL-2026")))
}
})
t.Run("chat retry and faq", func(t *testing.T) {
fail := `{"model":"gpt-5.6-luna","messages":[{"role":"user","content":"[fixture:fail]"}]}`
if status := post(t, server.URL+"/v1/chat/completions", fail, nil); status != http.StatusServiceUnavailable {
t.Fatalf("failure status=%d", status)
}
recovered := `{"model":"gpt-5.6-luna","messages":[{"role":"user","content":"[fixture:fail]"},{"role":"user","content":"recovered"}]}`
if status := post(t, server.URL+"/v1/chat/completions", recovered, nil); status != http.StatusOK {
t.Fatalf("recovery status=%d", status)
}
retry := `{"model":"gpt-5.6-luna","messages":[{"role":"user","content":"[fixture:retry] one"}]}`
if status := post(t, server.URL+"/v1/chat/completions", retry, nil); status != http.StatusServiceUnavailable {
t.Fatalf("first retry status=%d", status)
}
if status := post(t, server.URL+"/v1/chat/completions", retry, nil); status != http.StatusOK {
t.Fatalf("second retry status=%d", status)
}
faq := `{"model":"gpt-5.6-luna","messages":[{"role":"system","content":"Extract factual, self-contained FAQs"}]}`
var response struct {
Choices []struct {
Message chatMessage `json:"message"`
} `json:"choices"`
}
if status := post(t, server.URL+"/v1/chat/completions", faq, &response); status != http.StatusOK || len(response.Choices) != 1 || !strings.Contains(response.Choices[0].Message.Content, `"faqs"`) {
t.Fatalf("faq status=%d choices=%d", status, len(response.Choices))
}
})
t.Run("skill activation", func(t *testing.T) {
request := `{"model":"gpt-5.6-luna","messages":[{"role":"system","content":"<available_skills_json>[{\"name\":\"local-skill\"}]</available_skills_json>"},{"role":"user","content":"[fixture:skill]"}],"tools":[{"type":"function","function":{"name":"activate_skill"}}]}`
var response struct {
Choices []struct {
Message chatMessage `json:"message"`
} `json:"choices"`
}
status := post(t, server.URL+"/v1/chat/completions", request, &response)
if status != http.StatusOK || len(response.Choices) != 1 || len(response.Choices[0].Message.ToolCalls) != 1 || response.Choices[0].Message.ToolCalls[0].Function.Name != "activate_skill" {
t.Fatalf("status=%d choices=%d", status, len(response.Choices))
}
})
t.Run("embedding dimensions", func(t *testing.T) {
var response struct {
Data []struct {
Embedding []float64 `json:"embedding"`
} `json:"data"`
}
status := post(t, server.URL+"/v1/embeddings", `{"model":"fixture-embedding","input":["hello"]}`, &response)
if status != http.StatusOK || len(response.Data) != 1 || len(response.Data[0].Embedding) != 1536 {
t.Fatalf("status=%d vectors=%d dimensions=%d", status, len(response.Data), len(response.Data[0].Embedding))
}
})
}
func post(t *testing.T, url, body string, dst any) int {
t.Helper()
resp, err := http.Post(url, "application/json", strings.NewReader(body))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if dst != nil {
if err := json.NewDecoder(resp.Body).Decode(dst); err != nil {
t.Fatal(err)
}
}
return resp.StatusCode
}