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
94 lines
2.1 KiB
Go
94 lines
2.1 KiB
Go
package backend
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"image/png"
|
|
"io"
|
|
"testing"
|
|
)
|
|
|
|
func TestMockGeneratesValidPNG(t *testing.T) {
|
|
be, err := NewMock("mock", nil)
|
|
if err != nil {
|
|
t.Fatalf("NewMock: %v", err)
|
|
}
|
|
if be.Name() != "mock" {
|
|
t.Errorf("Name() = %q", be.Name())
|
|
}
|
|
res, err := be.Generate(context.Background(), Request{
|
|
Prompt: "test prompt",
|
|
Width: 64,
|
|
Height: 64,
|
|
Seed: 1234,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
defer res.ImageReader.Close()
|
|
if res.MimeType != "image/png" {
|
|
t.Errorf("mime = %q", res.MimeType)
|
|
}
|
|
body, err := io.ReadAll(res.ImageReader)
|
|
if err != nil {
|
|
t.Fatalf("read body: %v", err)
|
|
}
|
|
img, err := png.Decode(bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("decode png: %v", err)
|
|
}
|
|
if img.Bounds().Dx() != 64 || img.Bounds().Dy() != 64 {
|
|
t.Errorf("dims = %v", img.Bounds())
|
|
}
|
|
if seed, ok := res.Metadata["seed"].(int64); !ok || seed != 1234 {
|
|
t.Errorf("metadata seed = %v", res.Metadata["seed"])
|
|
}
|
|
}
|
|
|
|
func TestMockDeterministicBySeed(t *testing.T) {
|
|
be, _ := NewMock("mock", nil)
|
|
gen := func() []byte {
|
|
res, err := be.Generate(context.Background(), Request{Prompt: "p", Width: 32, Height: 32, Seed: 99})
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
defer res.ImageReader.Close()
|
|
b, _ := io.ReadAll(res.ImageReader)
|
|
return b
|
|
}
|
|
a := gen()
|
|
b := gen()
|
|
if !bytes.Equal(a, b) {
|
|
t.Errorf("same seed produced different images: %d vs %d bytes", len(a), len(b))
|
|
}
|
|
}
|
|
|
|
func TestRegistryBuildAndUnknown(t *testing.T) {
|
|
r := NewRegistry()
|
|
r.Register("mock", NewMock)
|
|
if !r.Has("mock") {
|
|
t.Errorf("Has(mock) = false")
|
|
}
|
|
be, err := r.Build("mock", "instance-1", nil)
|
|
if err != nil {
|
|
t.Fatalf("Build: %v", err)
|
|
}
|
|
if be.Name() != "instance-1" {
|
|
t.Errorf("Name() = %q", be.Name())
|
|
}
|
|
if _, err := r.Build("nope", "x", nil); err == nil {
|
|
t.Errorf("expected error for unknown type")
|
|
}
|
|
}
|
|
|
|
func TestRegistryDuplicatePanic(t *testing.T) {
|
|
r := NewRegistry()
|
|
r.Register("dup", NewMock)
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Errorf("expected panic on duplicate Register")
|
|
}
|
|
}()
|
|
r.Register("dup", NewMock)
|
|
}
|