package opencode import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" ) // fakeAPIServer — минимальный фейк opencode serve HTTP API v1.17. type fakeAPIServer struct { messages []sessionMessage failCreate bool failVerify bool } func (f *fakeAPIServer) handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/api/session", func(w http.ResponseWriter, r *http.Request) { if f.failCreate { http.Error(w, "boom", http.StatusInternalServerError) return } writeJSON(w, map[string]any{"data": map[string]any{"id": "sess-fake"}}) }) mux.HandleFunc("/api/session/{id}/prompt", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, map[string]any{"data": map[string]any{}}) }) mux.HandleFunc("/api/session/{id}/wait", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, map[string]any{}) }) mux.HandleFunc("/api/session/{id}/interrupt", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, map[string]any{}) }) mux.HandleFunc("/api/session/{id}/message", func(w http.ResponseWriter, _ *http.Request) { if len(f.messages) == 0 { writeJSON(w, map[string]any{"data": []sessionMessage{}}) return } writeJSON(w, map[string]any{"data": f.messages}) }) return mux } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(v) } // textPart — анонимная text-часть assistant-сообщения. func textPart(s string) struct { Type string `json:"type"` Text string `json:"text"` } { return struct { Type string `json:"type"` Text string `json:"text"` }{Type: "text", Text: s} } func fakeClient(t *testing.T, f *fakeAPIServer) *Client { t.Helper() ts := httptest.NewServer(f.handler()) t.Cleanup(ts.Close) return &Client{BaseURL: ts.URL} } func TestClient_CreateSession(t *testing.T) { c := fakeClient(t, &fakeAPIServer{}) id, err := c.CreateSession(context.Background(), "ratatoskr-analyst") if err != nil { t.Fatalf("CreateSession err: %v", err) } if id != "sess-fake" { t.Errorf("id = %q, want sess-fake", id) } } func TestClient_CreateSessionFail(t *testing.T) { c := fakeClient(t, &fakeAPIServer{failCreate: true}) if _, err := c.CreateSession(context.Background(), "x"); err == nil { t.Fatal("CreateSession должен упасть при 500, а не nil") } } func TestClient_Verdict(t *testing.T) { c := fakeClient(t, &fakeAPIServer{ messages: []sessionMessage{{Type: "assistant", Content: []struct { Type string `json:"type"` Text string `json:"text"` }{textPart(`{"phase":"ready"}`)}}}, }) vd, err := c.Verdict(context.Background(), "sess-fake") if err != nil { t.Fatalf("Verdict err: %v", err) } if vd != `{"phase":"ready"}` { t.Errorf("verdict = %q, want вердикт модели", vd) } } func TestClient_VerdictNoText(t *testing.T) { c := fakeClient(t, &fakeAPIServer{}) // нет assistant-сообщения с text if _, err := c.Verdict(context.Background(), "sess-fake"); err == nil { t.Fatal("Verdict должен упасть, когда нет text-части") } else { var ce *ClientErr if !errors.As(err, &ce) { t.Errorf("ожидался *ClientErr, got %T", err) } } }