Files
ImaGen/internal/output/output_test.go
mAi 237270b204 mAi: #211 - bootstrap ImaGen framework skeleton
First step of the model-agnostic image-generation framework. Lands the
plumbing other components (skill, ComfyUI/Replicate adapters, agents)
will plug into:

- internal/backend: Backend interface (Request/Result), thread-safe
  Registry with init-time Register, plus a Mock reference adapter that
  emits a deterministic gradient PNG for smoke tests.
- internal/config: YAML loader for ~/.config/imagen.yaml. Framework owns
  default_backend + output settings + a per-backend block; each adapter
  owns the schema below its own block via BackendSpec.Raw.
- internal/output: filename templating ({date}/{time}/{slug}/{seed}/
  {backend}/{ext}), JSON metadata sidecar, --output override path.
- internal/prompt: embedded styles.yaml, style-preset suffix application.
- internal/server: 501 stub — HTTP surface lands in a follow-up issue.
- cmd/imagen: generate / backends / config (init|validate|path) / serve
  / version subcommands. Stdlib-only flag parsing with a small helper to
  honour positional prompt args ahead of flags (matches the issue spec).
- Tests for output (slug, naming template, sidecar), backend (mock PNG
  validity + determinism, registry build + duplicate panic), config
  (round-trip + validation), prompt (style apply + unknown-style error).
- CLAUDE.md, README.md, docs/architecture.md, docs/usage.md, Makefile.

Acceptance criteria from #211:
1. go build ./... — clean
2. imagen backends — lists registered backends, exits 0
3. imagen generate "test prompt" --backend mock --output /tmp/x.png —
   writes a 1024x1024 PNG plus an x.png.json sidecar
4. imagen config init | imagen config validate — round-trips cleanly
5. CLAUDE.md "Adding a new adapter" — six-step recipe
2026-05-08 14:37:05 +02:00

128 lines
3.4 KiB
Go

package output
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestSlug(t *testing.T) {
cases := map[string]string{
"": "image",
" ": "image",
"A Cat in a Fishbowl": "a-cat-in-a-fishbowl",
"!!! weird---input": "weird-input",
"über-cool prompt": "ber-cool-prompt", // ASCII-only by design
strings.Repeat("a", 80): strings.Repeat("a", 40),
}
for in, want := range cases {
if got := Slug(in); got != want {
t.Errorf("Slug(%q) = %q, want %q", in, got, want)
}
}
}
func TestRenderTemplateAndWrite(t *testing.T) {
dir := t.TempDir()
w := &Writer{
Directory: dir,
NameTemplate: "{date}-{slug}-{seed}.{ext}",
WriteSidecar: true,
Now: func() time.Time {
return time.Date(2026, 5, 8, 14, 30, 15, 0, time.UTC)
},
}
body := []byte("PNGbytes")
out, err := w.Write(bytes.NewReader(body), Inputs{
Prompt: "A cat in a fishbowl",
Backend: "mock",
Seed: 42,
Ext: "png",
Metadata: map[string]any{"foo": "bar"},
})
if err != nil {
t.Fatalf("Write: %v", err)
}
want := filepath.Join(dir, "2026-05-08-a-cat-in-a-fishbowl-42.png")
if out.ImagePath != want {
t.Errorf("image path = %q, want %q", out.ImagePath, want)
}
gotBody, err := os.ReadFile(out.ImagePath)
if err != nil {
t.Fatalf("read image: %v", err)
}
if !bytes.Equal(gotBody, body) {
t.Errorf("image body mismatch")
}
if out.SidecarPath == "" {
t.Fatal("sidecar path empty")
}
sc, err := os.ReadFile(out.SidecarPath)
if err != nil {
t.Fatalf("read sidecar: %v", err)
}
var parsed map[string]any
if err := json.Unmarshal(sc, &parsed); err != nil {
t.Fatalf("sidecar json: %v\n%s", err, sc)
}
if parsed["prompt"] != "A cat in a fishbowl" {
t.Errorf("sidecar prompt = %v", parsed["prompt"])
}
if parsed["backend"] != "mock" {
t.Errorf("sidecar backend = %v", parsed["backend"])
}
if parsed["timestamp"] != "2026-05-08T14:30:15Z" {
t.Errorf("sidecar timestamp = %v", parsed["timestamp"])
}
meta, ok := parsed["metadata"].(map[string]any)
if !ok || meta["foo"] != "bar" {
t.Errorf("sidecar metadata = %v", parsed["metadata"])
}
}
func TestWriteSkipSidecarWhenDisabled(t *testing.T) {
dir := t.TempDir()
w := &Writer{Directory: dir, WriteSidecar: false}
out, err := w.Write(bytes.NewReader([]byte("x")), Inputs{Prompt: "p", Backend: "b", Seed: 1, Ext: "png"})
if err != nil {
t.Fatalf("Write: %v", err)
}
if out.SidecarPath != "" {
t.Errorf("sidecar path = %q, want empty", out.SidecarPath)
}
}
func TestUnknownPlaceholderPassesThrough(t *testing.T) {
got := renderTemplate("{date}-{nonsense}-{seed}", map[string]string{
"date": "2026-05-08", "seed": "1",
})
if got != "2026-05-08-{nonsense}-1" {
t.Errorf("got %q", got)
}
}
func TestWriteToPath(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "explicit.png")
w := &Writer{WriteSidecar: true}
out, err := w.WriteToPath(bytes.NewReader([]byte("z")), target, Inputs{
Prompt: "p", Backend: "b", Seed: 7, Ext: "png",
})
if err != nil {
t.Fatalf("WriteToPath: %v", err)
}
if out.ImagePath != target {
t.Errorf("image path = %q, want %q", out.ImagePath, target)
}
if _, err := os.Stat(target); err != nil {
t.Errorf("expected %s to exist: %v", target, err)
}
if _, err := os.Stat(target + ".json"); err != nil {
t.Errorf("expected sidecar to exist: %v", err)
}
}