package suggestions import ( "net/http" "path/filepath" "strings" "testing" "github.com/go-chi/chi/v5" "gitea.parodia.dev/drwily/petal/internal/auth" "gitea.parodia.dev/drwily/petal/internal/db" "gitea.parodia.dev/drwily/petal/internal/llm" ) // The langpack decides what Petal says in the browser; users.pair_lang has to // decide what the *model* says too, or a pt-PT writer gets a Mandarin gloss on // an otherwise Portuguese screen. These tests follow the value from the column // to the system prompt for each pass that names a language. // // This is the same failure mode the standing isolation rule guards against: the // column is read in a query the handler already ran, so nothing fails loudly if // the join is dropped — the prompt just quietly reverts to Mandarin. // newPairServer seeds one writer on the given pair with a document of her own. func newPairServer(t *testing.T, client llm.LLMClient, pairLang string) (http.Handler, string, *db.DB) { t.Helper() database, err := db.Open(filepath.Join(t.TempDir(), "pair.db")) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { database.Close() }) const userID = "writer-pt" if _, err := database.Exec( `INSERT INTO users (id, email, display_name, pair_lang) VALUES (?, ?, ?, ?)`, userID, "w@example.com", "Writer", pairLang, ); err != nil { t.Fatalf("seed user: %v", err) } var docID string if err := database.QueryRow( `INSERT INTO documents (user_id, content_text) VALUES (?, ?) RETURNING id`, userID, "The rain was strong yesterday.", ).Scan(&docID); err != nil { t.Fatalf("seed doc: %v", err) } h := New(database, client) r := chi.NewRouter() r.Route("/docs", func(dr chi.Router) { h.RegisterDocRoutes(dr) }) r.Mount("/suggestions", h.Routes()) return auth.Middleware(auth.StaticResolver(userID))(r), docID, database } func TestCollocationPromptUsesTheWritersPair(t *testing.T) { client := &recordingClient{response: `{"suggestions":[]}`} srv, docID, _ := newPairServer(t, client, "pt-PT") rec := do(t, srv, http.MethodPost, "/docs/"+docID+"/collocation", "") if rec.Code != http.StatusOK { t.Fatalf("collocation: code=%d body=%s", rec.Code, rec.Body) } system := client.last.Messages[0].Content if !strings.Contains(system, "European Portuguese") { t.Fatalf("collocation prompt ignored pair_lang:\n%s", system) } if strings.Contains(system, "Simplified Chinese") { t.Fatalf("collocation prompt fell back to Mandarin:\n%s", system) } } func TestTranslatePromptUsesTheWritersPair(t *testing.T) { client := &recordingClient{response: "Chove muito."} srv, docID, database := newPairServer(t, client, "pt-PT") var sugID string if err := database.QueryRow( `INSERT INTO suggestions (doc_id, original, replacement, explanation, type, from_pos, to_pos) VALUES (?, ?, ?, ?, ?, 0, 5) RETURNING id`, docID, "strong rain", "heavy rain", "Natives usually say heavy rain.", "collocation", ).Scan(&sugID); err != nil { t.Fatalf("seed suggestion: %v", err) } rec := do(t, srv, http.MethodPost, "/suggestions/"+sugID+"/translate", "") if rec.Code != http.StatusOK { t.Fatalf("translate: code=%d body=%s", rec.Code, rec.Body) } system := client.last.Messages[0].Content if !strings.Contains(system, "European Portuguese") || strings.Contains(system, "Chinese") { t.Fatalf("translate prompt ignored pair_lang:\n%s", system) } } // A writer whose column still holds the default — every account today — must be // answered exactly as before. func TestDefaultPairIsUnchanged(t *testing.T) { client := &recordingClient{response: `{"suggestions":[]}`} srv, docID, _ := newPairServer(t, client, "zh") if rec := do(t, srv, http.MethodPost, "/docs/"+docID+"/collocation", ""); rec.Code != http.StatusOK { t.Fatalf("collocation: code=%d body=%s", rec.Code, rec.Body) } if system := client.last.Messages[0].Content; !strings.Contains(system, "Simplified Chinese (Mandarin) gloss") { t.Fatalf("zh writer no longer gets a Mandarin gloss:\n%s", system) } }