Make artifact and media metadata first-class server records

Artifact and media metadata were previously reconstructed from job result
refs and JSON sidecar files. This change regularizes metadata into SQLite,
keeps filesystem blobs in place, and preserves backward compatibility via
lazy fallback and backfill from existing job refs and JSON metadata.

Constraint: Blob storage remains on the local filesystem in this phase
Rejected: Migrate blobs into SQLite | larger scope and worse operational profile for current media sizes
Rejected: Hard cutover without fallback | unsafe for historical data already on the test server
Confidence: medium
Scope-risk: moderate
Directive: Treat SQLite as the metadata source of truth; JSON sidecars are compatibility fallback only
Tested: go test ./...; deployed to test server 101.42.99.35; verified /v1/artifacts, /v1/artifacts/:id, signed media URL 200, unsigned content 401
Not-tested: Full historical backfill sweep over all existing artifact rows under production-sized data volume
This commit is contained in:
wtgoku
2026-04-18 23:06:34 +08:00
parent 07fd1ad0a7
commit d0318292c1
12 changed files with 1303 additions and 75 deletions
+130
View File
@@ -1,10 +1,12 @@
package server
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
@@ -271,6 +273,134 @@ func TestResolveVideoReferencesSupportsImagesArray(t *testing.T) {
}
}
func TestResolveVideoReferencesAcceptsCanonicalImageURL(t *testing.T) {
cfg := Config{
SQLitePath: filepath.Join(t.TempDir(), "popiart.db"),
SkillhubDir: makeEmptySkillhub(t),
SessionSecret: "test-secret",
}
server, err := NewWithConfig(cfg)
if err != nil {
t.Fatalf("NewWithConfig: %v", err)
}
imageBytes := tinyPNG(t)
refSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write(imageBytes)
}))
defer refSrv.Close()
refs, err := server.resolveVideoReferences(context.Background(), &job{
UserID: "user_test",
UpstreamKey: "sk-upstream",
}, map[string]any{
"image_url": refSrv.URL + "/single.png",
})
if err != nil {
t.Fatalf("resolveVideoReferences canonical image_url: %v", err)
}
if len(refs) != 1 {
t.Fatalf("expected 1 ref, got %#v", refs)
}
if refs[0].URL != refSrv.URL+"/single.png" {
t.Fatalf("expected URL to round-trip, got %q", refs[0].URL)
}
if refs[0].ContentType != "image/png" {
t.Fatalf("expected image/png, got %q", refs[0].ContentType)
}
if !bytes.Equal(refs[0].Content, imageBytes) {
t.Fatal("expected canonical image_url bytes to match")
}
}
func TestResolveVideoReferencesAcceptsSignedSameOriginMediaURL(t *testing.T) {
cfg := Config{
SQLitePath: filepath.Join(t.TempDir(), "popiart.db"),
SkillhubDir: makeEmptySkillhub(t),
SessionSecret: "test-secret",
}
server, err := NewWithConfig(cfg)
if err != nil {
t.Fatalf("NewWithConfig: %v", err)
}
srv := httptest.NewServer(server.Handler())
defer srv.Close()
server.cfg.PublicBaseURL = srv.URL
sessionToken, _, ok, err := server.store.createSession("sk-video-same-origin-user")
if err != nil {
t.Fatalf("createSession: %v", err)
}
if !ok {
t.Fatal("expected session creation to succeed")
}
current, exists, err := server.store.session(sessionToken)
if err != nil {
t.Fatalf("load session: %v", err)
}
if !exists {
t.Fatal("expected stored session")
}
imageBytes := tinyPNG(t)
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "video-reference.png")
if err != nil {
t.Fatalf("create form file: %v", err)
}
if _, err := part.Write(imageBytes); err != nil {
t.Fatalf("write form file: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("close multipart writer: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/v1/media/upload", &body)
req.Header.Set("Authorization", "Bearer "+sessionToken)
req.Header.Set("Content-Type", writer.FormDataContentType())
rec := httptest.NewRecorder()
server.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("expected media upload 201, got %d body=%s", rec.Code, rec.Body.String())
}
var envelope struct {
OK bool `json:"ok"`
Data media `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode media upload response: %v body=%s", err, rec.Body.String())
}
if !envelope.OK || envelope.Data.URL == "" {
t.Fatalf("expected signed media url, got %s", rec.Body.String())
}
refs, err := server.resolveVideoReferences(context.Background(), &job{
UserID: current.User.ID,
SessionID: current.Token,
UpstreamKey: current.UpstreamKey,
}, map[string]any{
"image_url": envelope.Data.URL,
})
if err != nil {
t.Fatalf("resolveVideoReferences same-origin signed media url: %v", err)
}
if len(refs) != 1 {
t.Fatalf("expected 1 ref, got %#v", refs)
}
if refs[0].URL != envelope.Data.URL {
t.Fatalf("expected signed URL to round-trip, got %q", refs[0].URL)
}
if refs[0].ContentType != "image/png" {
t.Fatalf("expected image/png, got %q", refs[0].ContentType)
}
if !bytes.Equal(refs[0].Content, imageBytes) {
t.Fatal("expected same-origin signed media bytes to match")
}
}
func TestFetchVideoTaskFallsBackToGenericTaskEnvelope(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {