Промпт отправляется в /api/session/:id/message (v1.18) с новым форматом
тела parts:[{type:"text"}], вместо {prompt:{text}}. Причина перехода:
/prompt сам подставляет не ту модель, что в конфиге; /message использует
модель из конфига.
- client.go: Send → POST /message + тело parts; шапка-док v1.18
- тесты: fakeAPIServer + e2eFakeAPI отвечают на POST /message (GET уже был)
116 lines
3.3 KiB
Go
116 lines
3.3 KiB
Go
package opencode
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"testing"
|
||
)
|
||
|
||
// fakeAPIServer — минимальный фейк opencode serve HTTP API v1.18.
|
||
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}/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, r *http.Request) {
|
||
// GET — чтение сообщений; POST — отправка промпта (v1.18).
|
||
if r.Method == http.MethodGet {
|
||
if len(f.messages) == 0 {
|
||
writeJSON(w, map[string]any{"data": []sessionMessage{}})
|
||
return
|
||
}
|
||
writeJSON(w, map[string]any{"data": f.messages})
|
||
return
|
||
}
|
||
writeJSON(w, map[string]any{"data": map[string]any{}})
|
||
})
|
||
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)
|
||
}
|
||
}
|
||
} |