Every successful imagen generate now (a) uploads the PNG to the private imagen-generated bucket and (b) inserts a row into imagen.images, the data plane the flexsiebels owner-mode viewer reads from. Schema, RLS, indexes, bucket and PostgREST exposure landed via four applied migrations on msupabase: imagen_schema_init, imagen_schema_grants, imagen_storage_policies, imagen_pgrst_expose (authenticator role-level ALTER + reload). Owner UUID for m: ac6c9501-3757-4a6d-8b97-2cff4288382b — documented in the config sample. Code: new internal/cloud/ package mirroring the internal/usage/ shape. PostgREST POST against the imagen schema (Accept-Profile + Content- Profile headers), Storage upload via PUT with x-upsert, retry on 5xx / transport but not 4xx, owner_user_id required (the column is NOT NULL and the read-side RLS policy needs it). Wiring in cmd/imagen/generate.go: --no-cloud flag, output.cloud_sync config knob (auto|on|off mirroring --preview), $IMAGEN_CLOUD_SYNC env override. The hook reads the just-written PNG + sidecar from disk and calls cloud.Sync; failures emit "imagen: cloud sync: <err>" to stderr without changing exit code, so a Supabase blip never loses the artefact. output.Outputs grew Date/Slug/Seed fields so storage_path mirrors the local filename's prefix exactly (no UTC-vs-local drift). Config: owner_user_id field added; sample comment points at the auth.users lookup. imagen config validate warns on stderr when cloud_sync is on/auto but owner_user_id is empty. Tests: cloud_test.go covers happy path, retry-on-5xx, no-retry-on-4xx, missing-owner-uuid, missing-date-or-slug, signed URL, and the partial- success case where the upload landed but the DB insert failed. generate_test.go covers the precedence chain for cloud-sync mode resolution. Build + tests clean across the tree. Real smoke against mRock: generation through flux-schnell-local writes the local PNG + sidecar AND uploads to imagen-generated/2026-05-11/... AND inserts into imagen.images. Signed URL round-trips the same bytes. --no-cloud verified to skip both Storage and DB.
138 lines
3.6 KiB
Go
138 lines
3.6 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadAndValidate(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "imagen.yaml")
|
|
if err := os.WriteFile(path, []byte(Sample), 0o644); err != nil {
|
|
t.Fatalf("write sample: %v", err)
|
|
}
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.DefaultBackend != "flux-schnell-local" {
|
|
t.Errorf("default = %q", cfg.DefaultBackend)
|
|
}
|
|
mock, ok := cfg.Backends["mock"]
|
|
if !ok {
|
|
t.Fatalf("mock backend missing")
|
|
}
|
|
if mock.Type != "mock" {
|
|
t.Errorf("mock type = %q", mock.Type)
|
|
}
|
|
flux, ok := cfg.Backends["flux-schnell-local"]
|
|
if !ok {
|
|
t.Fatalf("flux backend missing")
|
|
}
|
|
if flux.Type != "comfyui" {
|
|
t.Errorf("flux type = %q", flux.Type)
|
|
}
|
|
if flux.Raw["base_url"] != "http://mrock:8188" {
|
|
t.Errorf("flux base_url = %v", flux.Raw["base_url"])
|
|
}
|
|
if flux.Raw["model"] != "flux1-schnell.safetensors" {
|
|
t.Errorf("flux model = %v", flux.Raw["model"])
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsUnknownDefault(t *testing.T) {
|
|
c := &Config{
|
|
DefaultBackend: "ghost",
|
|
Backends: map[string]BackendSpec{"real": {Type: "mock"}},
|
|
}
|
|
if err := c.Validate(); err == nil {
|
|
t.Errorf("expected error for unknown default_backend")
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsMissingType(t *testing.T) {
|
|
c := &Config{
|
|
Backends: map[string]BackendSpec{"x": {}},
|
|
}
|
|
if err := c.Validate(); err == nil {
|
|
t.Errorf("expected error for missing type")
|
|
}
|
|
}
|
|
|
|
func TestValidatePreviewMode(t *testing.T) {
|
|
for _, mode := range []string{"", "auto", "on", "off"} {
|
|
c := &Config{Output: OutputConfig{Preview: mode}}
|
|
if err := c.Validate(); err != nil {
|
|
t.Errorf("preview=%q: unexpected error %v", mode, err)
|
|
}
|
|
}
|
|
bad := &Config{Output: OutputConfig{Preview: "yes"}}
|
|
if err := bad.Validate(); err == nil {
|
|
t.Errorf("expected error for invalid preview value")
|
|
}
|
|
}
|
|
|
|
func TestValidateCloudSyncMode(t *testing.T) {
|
|
for _, mode := range []string{"", "auto", "on", "off"} {
|
|
c := &Config{Output: OutputConfig{CloudSync: mode}}
|
|
if err := c.Validate(); err != nil {
|
|
t.Errorf("cloud_sync=%q: unexpected error %v", mode, err)
|
|
}
|
|
}
|
|
bad := &Config{Output: OutputConfig{CloudSync: "yes"}}
|
|
if err := bad.Validate(); err == nil {
|
|
t.Errorf("expected error for invalid cloud_sync value")
|
|
}
|
|
}
|
|
|
|
func TestSampleParsesCloudSyncAuto(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "imagen.yaml")
|
|
if err := os.WriteFile(path, []byte(Sample), 0o644); err != nil {
|
|
t.Fatalf("write sample: %v", err)
|
|
}
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.Output.CloudSync != "auto" {
|
|
t.Errorf("Output.CloudSync = %q, want auto", cfg.Output.CloudSync)
|
|
}
|
|
// owner_user_id is intentionally empty in the sample — operators fill
|
|
// it in after looking up their auth.users.id.
|
|
if cfg.OwnerUserID != "" {
|
|
t.Errorf("Sample OwnerUserID should be empty, got %q", cfg.OwnerUserID)
|
|
}
|
|
}
|
|
|
|
func TestSampleParsesPreviewAuto(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "imagen.yaml")
|
|
if err := os.WriteFile(path, []byte(Sample), 0o644); err != nil {
|
|
t.Fatalf("write sample: %v", err)
|
|
}
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.Output.Preview != "auto" {
|
|
t.Errorf("Output.Preview = %q, want auto", cfg.Output.Preview)
|
|
}
|
|
}
|
|
|
|
func TestExpandPath(t *testing.T) {
|
|
home, _ := os.UserHomeDir()
|
|
cases := map[string]string{
|
|
"": "",
|
|
"/abs/path": "/abs/path",
|
|
"~": home,
|
|
"~/foo/bar": filepath.Join(home, "foo/bar"),
|
|
}
|
|
for in, want := range cases {
|
|
if got := ExpandPath(in); got != want {
|
|
t.Errorf("ExpandPath(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|