Files
fission-console/client-console/internal/api/ai_prompts_test.go
T

283 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}