283 lines
10 KiB
Go
283 lines
10 KiB
Go
package api
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestNormalizeAIMode(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
in string
|
||
want string
|
||
}{
|
||
{name: "blank defaults to chat", in: "", want: aiModeChat},
|
||
{name: "case-insensitive codegen", in: " CODEGEN ", want: aiModeCodegen},
|
||
{name: "case-insensitive explain", in: "Explain", want: aiModeExplain},
|
||
{name: "unknown defaults to chat", in: "boom", want: aiModeChat},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
if got := normalizeAIMode(tt.in); got != tt.want {
|
||
t.Fatalf("normalizeAIMode(%q) = %q, want %q", tt.in, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestDefaultPromptFunctionName(t *testing.T) {
|
||
tests := []struct {
|
||
lang string
|
||
want string
|
||
}{
|
||
{lang: "python", want: "main"},
|
||
{lang: "nodejs", want: "main"},
|
||
{lang: "go", want: "Handler"},
|
||
{lang: "php", want: "handler"},
|
||
{lang: "ruby", want: "handler"},
|
||
{lang: "unknown", want: "main"},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.lang, func(t *testing.T) {
|
||
if got := defaultPromptFunctionName(tt.lang); got != tt.want {
|
||
t.Fatalf("defaultPromptFunctionName(%q) = %q, want %q", tt.lang, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestBuildLLMChatRequestCodegenPython(t *testing.T) {
|
||
req, err := buildLLMChatRequest(aiPromptRequest{
|
||
Mode: aiModeCodegen,
|
||
Language: "python",
|
||
Name: " ",
|
||
Description: " сделать простой echo endpoint ",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||
}
|
||
if req.Model != "gpt-oss-120b" {
|
||
t.Fatalf("unexpected model %q", req.Model)
|
||
}
|
||
if got, want := len(req.Messages), 2; got != want {
|
||
t.Fatalf("messages len = %d, want %d", got, want)
|
||
}
|
||
sys := req.Messages[0].Content
|
||
usr := req.Messages[1].Content
|
||
if !strings.Contains(sys, "Fission") || !strings.Contains(sys, "main.main") || !strings.Contains(sys, "def main():") {
|
||
t.Fatalf("python system prompt is missing Fission rules: %s", sys)
|
||
}
|
||
if !strings.Contains(sys, "не генерируй def main(ctx)") && !strings.Contains(strings.ToLower(sys), "def main(ctx)") {
|
||
t.Fatalf("python system prompt must forbid ctx main: %s", sys)
|
||
}
|
||
if !strings.Contains(usr, "Language: python") {
|
||
t.Fatalf("python user prompt missing language: %s", usr)
|
||
}
|
||
if !strings.Contains(usr, "Function name: main") {
|
||
t.Fatalf("blank name should fall back to main: %s", usr)
|
||
}
|
||
if !strings.Contains(usr, "echo endpoint") {
|
||
t.Fatalf("description must be present: %s", usr)
|
||
}
|
||
if !strings.Contains(usr, "Верни только код") || !strings.Contains(usr, "entrypoint") {
|
||
t.Fatalf("python user prompt missing constraints: %s", usr)
|
||
}
|
||
}
|
||
|
||
func TestBuildLLMChatRequestCodegenNodeJS(t *testing.T) {
|
||
req, err := buildLLMChatRequest(aiPromptRequest{
|
||
Mode: aiModeCodegen,
|
||
Language: "nodejs",
|
||
Name: "handler",
|
||
Description: "возвращает status/body и не падает на пустом input",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||
}
|
||
sys := req.Messages[0].Content
|
||
usr := req.Messages[1].Content
|
||
for _, want := range []string{"CommonJS", "module.exports", "Fission node wrapper", "status, body, headers"} {
|
||
if !strings.Contains(sys, want) {
|
||
t.Fatalf("node system prompt missing %q: %s", want, sys)
|
||
}
|
||
}
|
||
if !strings.Contains(usr, "Language: nodejs") || !strings.Contains(usr, "Function name: handler") {
|
||
t.Fatalf("node user prompt missing fields: %s", usr)
|
||
}
|
||
}
|
||
|
||
func TestBuildLLMChatRequestCodegenGo(t *testing.T) {
|
||
req, err := buildLLMChatRequest(aiPromptRequest{
|
||
Mode: aiModeCodegen,
|
||
Language: "go",
|
||
Description: "печатает ответ и ставит content-type",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||
}
|
||
sys := req.Messages[0].Content
|
||
usr := req.Messages[1].Content
|
||
for _, want := range []string{"package main", "func Handler(w http.ResponseWriter, r *http.Request)", "net/http"} {
|
||
if !strings.Contains(strings.ToLower(sys), strings.ToLower(want)) {
|
||
t.Fatalf("go system prompt missing %q: %s", want, sys)
|
||
}
|
||
}
|
||
if !strings.Contains(usr, "Function name: Handler") {
|
||
t.Fatalf("go user prompt must default to Handler: %s", usr)
|
||
}
|
||
}
|
||
|
||
func TestBuildLLMChatRequestCodegenPHP(t *testing.T) {
|
||
req, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeCodegen, Language: "php", Description: "simple greeting"})
|
||
if err != nil {
|
||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||
}
|
||
sys := req.Messages[0].Content
|
||
for _, want := range []string{"main.php", "main.php::handler", "function handler($context)", "$context[\"response\"]"} {
|
||
if !strings.Contains(sys, want) {
|
||
t.Fatalf("php system prompt missing %q: %s", want, sys)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBuildLLMChatRequestCodegenRuby(t *testing.T) {
|
||
req, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeCodegen, Language: "ruby", Description: "return hello"})
|
||
if err != nil {
|
||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||
}
|
||
sys := req.Messages[0].Content
|
||
for _, want := range []string{"handler.rb", "entrypoint handler", "def handler"} {
|
||
if !strings.Contains(sys, want) {
|
||
t.Fatalf("ruby system prompt missing %q: %s", want, sys)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBuildLLMChatRequestExplain(t *testing.T) {
|
||
req, err := buildLLMChatRequest(aiPromptRequest{
|
||
Mode: aiModeExplain,
|
||
Language: "python",
|
||
Code: "def main():\n return 'ok'\n",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||
}
|
||
sys := req.Messages[0].Content
|
||
usr := req.Messages[1].Content
|
||
for _, want := range []string{"объясни", "совместим ли он с Fission", "Если entrypoint", "не переписывай код"} {
|
||
if !strings.Contains(strings.ToLower(sys+"\n"+usr), strings.ToLower(want)) {
|
||
t.Fatalf("explain prompt missing %q: sys=%s usr=%s", want, sys, usr)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestBuildLLMChatRequestChat(t *testing.T) {
|
||
req, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeChat, Question: "что делает слой 2?"})
|
||
if err != nil {
|
||
t.Fatalf("buildLLMChatRequest error = %v", err)
|
||
}
|
||
if len(req.Messages) != 2 {
|
||
t.Fatalf("expected 2 messages, got %d", len(req.Messages))
|
||
}
|
||
if req.Messages[1].Content != "что делает слой 2?" {
|
||
t.Fatalf("chat question mismatch: %q", req.Messages[1].Content)
|
||
}
|
||
}
|
||
|
||
func TestBuildLLMChatRequestRejectsMissingFields(t *testing.T) {
|
||
if _, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeCodegen, Description: "x"}); err == nil {
|
||
t.Fatal("expected codegen without language to fail")
|
||
}
|
||
if _, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeExplain, Language: "python"}); err == nil {
|
||
t.Fatal("expected explain without code to fail")
|
||
}
|
||
if _, err := buildLLMChatRequest(aiPromptRequest{Mode: aiModeChat}); err == nil {
|
||
t.Fatal("expected chat without question to fail")
|
||
}
|
||
}
|
||
|
||
func TestHandleAIAskUsesCodegenPrompt(t *testing.T) {
|
||
var gotReq llmChatRequest
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path != "/chat/completions" {
|
||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&gotReq); err != nil {
|
||
t.Fatalf("decode upstream request: %v", err)
|
||
}
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"module.exports = async function () { return { status: 200, body: \"ok\" }; }"}}]}`))
|
||
}))
|
||
defer server.Close()
|
||
|
||
s := &Server{http: server.Client(), llmURL: server.URL, llmKey: "secret"}
|
||
req := httptest.NewRequest(http.MethodPost, "/console/api/ai/ask", bytes.NewBufferString(`{"mode":"codegen","language":"python","description":"сделай echo"}`))
|
||
rec := httptest.NewRecorder()
|
||
s.handleAIAsk(rec, req)
|
||
|
||
if rec.Code != http.StatusOK {
|
||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||
}
|
||
if gotReq.Model != "gpt-oss-120b" {
|
||
t.Fatalf("model = %q", gotReq.Model)
|
||
}
|
||
if len(gotReq.Messages) != 2 {
|
||
t.Fatalf("messages len = %d", len(gotReq.Messages))
|
||
}
|
||
if !strings.Contains(gotReq.Messages[0].Content, "Python") || !strings.Contains(gotReq.Messages[1].Content, "echo") {
|
||
t.Fatalf("unexpected upstream prompts: %#v", gotReq.Messages)
|
||
}
|
||
}
|
||
|
||
func TestHandleAIAskUsesExplainPrompt(t *testing.T) {
|
||
var gotReq llmChatRequest
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if err := json.NewDecoder(r.Body).Decode(&gotReq); err != nil {
|
||
t.Fatalf("decode upstream request: %v", err)
|
||
}
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"код делает hello"}}]}`))
|
||
}))
|
||
defer server.Close()
|
||
|
||
s := &Server{http: server.Client(), llmURL: server.URL, llmKey: "secret"}
|
||
req := httptest.NewRequest(http.MethodPost, "/console/api/ai/ask", bytes.NewBufferString(`{"mode":"explain","language":"go","code":"package main\nfunc Handler(w http.ResponseWriter, r *http.Request) {}"}`))
|
||
rec := httptest.NewRecorder()
|
||
s.handleAIAsk(rec, req)
|
||
|
||
if rec.Code != http.StatusOK {
|
||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||
}
|
||
if !strings.Contains(gotReq.Messages[0].Content, "Go") || !strings.Contains(gotReq.Messages[1].Content, "совместим") {
|
||
t.Fatalf("unexpected explain prompts: %#v", gotReq.Messages)
|
||
}
|
||
}
|
||
|
||
func TestHandleAIAskKeepsChatModeSimple(t *testing.T) {
|
||
var gotReq llmChatRequest
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if err := json.NewDecoder(r.Body).Decode(&gotReq); err != nil {
|
||
t.Fatalf("decode upstream request: %v", err)
|
||
}
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`))
|
||
}))
|
||
defer server.Close()
|
||
|
||
s := &Server{http: server.Client(), llmURL: server.URL, llmKey: "secret"}
|
||
req := httptest.NewRequest(http.MethodPost, "/console/api/ai/ask", bytes.NewBufferString(`{"question":"что такое слой 1?"}`))
|
||
rec := httptest.NewRecorder()
|
||
s.handleAIAsk(rec, req)
|
||
|
||
if rec.Code != http.StatusOK {
|
||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||
}
|
||
if gotReq.Messages[1].Content != "что такое слой 1?" {
|
||
t.Fatalf("chat question mismatch: %#v", gotReq.Messages)
|
||
}
|
||
}
|