299 lines
9.7 KiB
Go
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")
|
|
}
|
|
}
|