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) } }