Files
neural-hunt/internal/artifact/collection_test.go
groot 47c523dd98
All checks were successful
release-tag / release-image (push) Successful in 3m51s
RC-14
2026-08-14 06:17:30 +02:00

299 lines
9.7 KiB
Go

package artifact
import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"neuralhunt/internal/settings"
)
func sampleWin() win {
return win{
ID: "task_1234567890abcdef",
Seed: "public-seed-42",
Winner: "client_abcdef1234567890",
Signature: "signature",
Guess: "777",
DisplayName: "Aurora Vault",
RangeBits: 28,
Completed: time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC),
PromptInstructions: "Add a subtle aurora motif to the environmental lighting.",
NegativePrompt: "no giant hat",
}
}
func TestCollectionTraitsDeterministic(t *testing.T) {
x := sampleWin()
a := deriveCollectionTraits(x)
b := deriveCollectionTraits(x)
if a != b {
t.Fatalf("traits are not deterministic: %#v != %#v", a, b)
}
if a.ThemeName == "" || a.Outfit == "" || a.EditionCode == "" || a.AccentA == "" {
t.Fatalf("missing dynamic traits: %#v", a)
}
}
func TestBuildCollectionPromptUsesIdentityLockAndVariables(t *testing.T) {
x := sampleWin()
traits := deriveCollectionTraits(x)
p := buildCollectionPrompt(x, traits)
for _, want := range []string{
"Image 1 is the canonical RIFT CHARACTER reference",
"Image 2 is the TASK STYLE reference",
"identity comes from Image 1; visual style comes from Image 2",
"VISUAL LANGUAGE — TASK STYLE HAS AUTHORITY",
"Do NOT force RIFT back into the neutral 3D look of Image 1",
traits.ThemeName,
traits.Outfit,
x.PromptInstructions,
x.NegativePrompt,
"exactly five dark rings",
"No card frame or UI inside the generated artwork",
} {
if !strings.Contains(p, want) {
t.Fatalf("prompt missing %q", want)
}
}
}
func TestRenderCardSVG(t *testing.T) {
x := sampleWin()
traits := deriveCollectionTraits(x)
art := []byte("fake-png-bytes")
card := string(renderCardSVG(art, "png", x, traits))
for _, want := range []string{
`width="1024" height="1536"`,
"NEURAL HUNT",
strings.ToUpper(traits.ThemeName),
"WINNING EDITION",
base64.StdEncoding.EncodeToString(art),
"data:image/png;base64,",
} {
if !strings.Contains(card, want) {
t.Fatalf("card missing %q", want)
}
}
}
func TestGPTImage2PortraitSize(t *testing.T) {
if err := validateOpenAIImageSize("gpt-image-2", 1024, 1536); err != nil {
t.Fatalf("1024x1536 should be valid: %v", err)
}
if err := validateOpenAIImageSize("gpt-image-2", 1000, 1536); err == nil {
t.Fatal("non-multiple-of-16 width should be rejected")
}
}
func TestRIFTDefaultQualityIsMedium(t *testing.T) {
t.Setenv("ARTIFACT_QUALITY", "")
if got := settings.Defaults().ArtifactQuality; got != "medium" {
t.Fatalf("default artifact quality = %q, want medium", got)
}
}
func TestOpenAIRequestSendsReferenceAsImageEdit(t *testing.T) {
var sawImage bool
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/images/edits" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
if got := r.Header.Get("Authorization"); got != "Bearer test-key" {
t.Errorf("bad auth: %q", got)
}
if err := r.ParseMultipartForm(2 << 20); err != nil {
t.Fatalf("parse multipart: %v", err)
}
if r.FormValue("model") != "gpt-image-2" || r.FormValue("size") != "1024x1536" || r.FormValue("quality") != "medium" {
t.Errorf("unexpected fields: model=%q size=%q quality=%q", r.FormValue("model"), r.FormValue("size"), r.FormValue("quality"))
}
files := r.MultipartForm.File["image[]"]
if len(files) != 1 {
t.Fatalf("expected one reference image, got %d", len(files))
}
if got := files[0].Header.Get("Content-Type"); got != "image/png" {
t.Fatalf("reference content type = %q, want image/png", got)
}
f, err := files[0].Open()
if err != nil {
t.Fatal(err)
}
b, _ := io.ReadAll(f)
_ = f.Close()
sawImage = string(b) == "reference-bytes"
w.Header().Set("Content-Type", "application/json")
w.Header().Set("x-request-id", "req_test")
fmt.Fprintf(w, `{"data":[{"b64_json":%q}],"usage":{"total_tokens":6000,"input_tokens":3000,"output_tokens":3000,"input_tokens_details":{"text_tokens":1000,"image_tokens":2000}}}`, base64.StdEncoding.EncodeToString([]byte("generated-png")))
}))
defer ts.Close()
t.Setenv("OPENAI_API_KEY", "test-key")
t.Setenv("OPENAI_BASE_URL", ts.URL)
w := &Worker{http: ts.Client()}
cfg := settings.Defaults()
cfg.ArtifactModel = "gpt-image-2"
cfg.ArtifactWidth = 1024
cfg.ArtifactHeight = 1536
cfg.ArtifactQuality = "medium"
res, err := w.openAIRequest(context.Background(), cfg, "test prompt", []referenceImage{{Name: "reference.png", ContentType: "image/png", Bytes: []byte("reference-bytes")}})
if err != nil {
t.Fatal(err)
}
if !sawImage {
t.Fatal("reference image was not transmitted")
}
if string(res.Bytes) != "generated-png" || res.Meta["endpoint"] != "images/edits" {
t.Fatalf("unexpected response: %#v", res)
}
if res.Usage == nil || res.Usage.InputTokens != 3000 || res.Usage.TextInputTokens != 1000 || res.Usage.ImageInputTokens != 2000 || res.Usage.OutputTokens != 3000 {
t.Fatalf("unexpected usage: %#v", res.Usage)
}
if res.EstimatedCostUSD == nil || *res.EstimatedCostUSD < 0.110999 || *res.EstimatedCostUSD > 0.111001 {
t.Fatalf("unexpected cost estimate: %#v", res.EstimatedCostUSD)
}
}
func TestEstimateOpenAIImageCost(t *testing.T) {
u := imageUsage{InputTokens: 3000, TextInputTokens: 1000, ImageInputTokens: 2000, OutputTokens: 3000, TotalTokens: 6000}
cost, basis, ok := estimateOpenAIImageCost("gpt-image-2", u)
if !ok || basis == "" {
t.Fatalf("expected priced usage, got ok=%v basis=%q", ok, basis)
}
if cost < 0.110999 || cost > 0.111001 {
t.Fatalf("unexpected gpt-image-2 cost: %.9f", cost)
}
if _, _, ok := estimateOpenAIImageCost("future-image-model", u); ok {
t.Fatal("unknown model must not get an invented price")
}
}
func TestTaskStyleReferenceDefaultsAndCustom(t *testing.T) {
dir := t.TempDir()
w := &Worker{dir: dir}
def, err := w.loadStyleReference("")
if err != nil {
t.Fatal(err)
}
if def.Custom || def.Name != "default_style_reference.jpg" || len(def.Bytes) < 1000 || def.SHA256 == "" {
t.Fatalf("unexpected default style: %#v", def)
}
if err := os.MkdirAll(filepath.Join(dir, "_styles"), 0o750); err != nil {
t.Fatal(err)
}
customBytes := append([]byte(nil), styleReferenceJPEG...)
customName := "custom.jpg"
if err := os.WriteFile(filepath.Join(dir, "_styles", customName), customBytes, 0o640); err != nil {
t.Fatal(err)
}
custom, err := w.loadStyleReference(customName)
if err != nil {
t.Fatal(err)
}
if !custom.Custom || custom.Name != customName || custom.SHA256 != def.SHA256 {
t.Fatalf("unexpected custom style: %#v", custom)
}
if _, err := w.loadStyleReference("../escape.jpg"); err == nil {
t.Fatal("path traversal style reference should be rejected")
}
}
func TestOpenAIRequestSendsCharacterAndStyleReferences(t *testing.T) {
var names []string
var contentTypes []string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(4 << 20); err != nil {
t.Fatalf("parse multipart: %v", err)
}
files := r.MultipartForm.File["image[]"]
if len(files) != 2 {
t.Fatalf("expected two references, got %d", len(files))
}
for _, f := range files {
names = append(names, f.Filename)
contentTypes = append(contentTypes, f.Header.Get("Content-Type"))
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"data":[{"b64_json":%q}]}`, base64.StdEncoding.EncodeToString([]byte("generated")))
}))
defer ts.Close()
t.Setenv("OPENAI_API_KEY", "test-key")
t.Setenv("OPENAI_BASE_URL", ts.URL)
w := &Worker{http: ts.Client()}
cfg := settings.Defaults()
_, err := w.openAIRequest(context.Background(), cfg, "prompt", []referenceImage{
{Name: "character_anchor.png", ContentType: "image/png", Bytes: []byte("anchor")},
{Name: "task_style.jpg", ContentType: "image/jpeg", Bytes: []byte("style")},
})
if err != nil {
t.Fatal(err)
}
if strings.Join(names, ",") != "character_anchor.png,task_style.jpg" {
t.Fatalf("unexpected reference order: %v", names)
}
if strings.Join(contentTypes, ",") != "image/png,image/jpeg" {
t.Fatalf("unexpected reference content types: %v", contentTypes)
}
}
func TestRarityDistributionAndOverride(t *testing.T) {
d := rarityDistribution{
Common: 5000,
Uncommon: 2500,
Rare: 1500,
UltraRare: 900,
SpecialIllustrationRare: 100,
}
cases := []struct {
roll int
want string
}{
{0, "SPECIAL ILLUSTRATION RARE"},
{99, "SPECIAL ILLUSTRATION RARE"},
{100, "ULTRA RARE"},
{999, "ULTRA RARE"},
{1000, "RARE"},
{2499, "RARE"},
{2500, "UNCOMMON"},
{4999, "UNCOMMON"},
{5000, "COMMON"},
{9999, "COMMON"},
}
for _, tc := range cases {
if got := chooseRarity(tc.roll, d, ""); got != tc.want {
t.Fatalf("roll %d = %q, want %q", tc.roll, got, tc.want)
}
}
if got := chooseRarity(9999, d, "SIR"); got != "SPECIAL ILLUSTRATION RARE" {
t.Fatalf("forced rarity = %q", got)
}
if got := chooseRarity(0, d, "ULTRA RARE"); got != "ULTRA RARE" {
t.Fatalf("forced ultra rarity = %q", got)
}
}
func TestRarityCardStylesDiffer(t *testing.T) {
x := sampleWin()
base := deriveCollectionTraits(x)
seen := map[string]string{}
for _, rarity := range []string{"COMMON", "UNCOMMON", "RARE", "ULTRA RARE", "SPECIAL ILLUSTRATION RARE"} {
traits := base
traits.Rarity = rarity
card := string(renderCardSVG([]byte("art"), "png", x, traits))
if !strings.Contains(card, rarity) && rarity != "SPECIAL ILLUSTRATION RARE" {
t.Fatalf("card for %s does not visibly identify rarity", rarity)
}
seen[rarity] = card
}
if seen["COMMON"] == seen["RARE"] || seen["RARE"] == seen["ULTRA RARE"] || seen["ULTRA RARE"] == seen["SPECIAL ILLUSTRATION RARE"] {
t.Fatal("rarity card renders should differ")
}
}