package app import ( "context" "encoding/json" "fmt" "strings" "testing" "time" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" ) func TestDiscoverRemoteCurrentStateSuccess(t *testing.T) { cfg := restoreDiscoveryConfig() store := &storage.FakeBackend{} sessionPrefix, manifestKey, runIDKey := restoreDiscoveryKeys(cfg) store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)}) state, err := discoverRemoteCurrentState(context.Background(), cfg, store) if err != nil { t.Fatalf("discoverRemoteCurrentState() error = %v", err) } if state.RunID != "20260519T010203Z-a1b2c3d4" { t.Fatalf("run id = %q, want 20260519T010203Z-a1b2c3d4", state.RunID) } if state.SessionPrefix != sessionPrefix { t.Fatalf("session prefix = %q, want %q", state.SessionPrefix, sessionPrefix) } if state.CurrentRunIDKey != runIDKey { t.Fatalf("current run id key = %q, want %q", state.CurrentRunIDKey, runIDKey) } if state.CurrentManifestKey != manifestKey { t.Fatalf("current manifest key = %q, want %q", state.CurrentManifestKey, manifestKey) } if state.Manifest == nil { t.Fatal("manifest is nil") } } func TestDiscoverRemoteCurrentStateMissingRunPointerFails(t *testing.T) { cfg := restoreDiscoveryConfig() store := &storage.FakeBackend{} _, err := discoverRemoteCurrentState(context.Background(), cfg, store) if err == nil || !strings.Contains(err.Error(), "remote current run pointer missing") { t.Fatalf("error = %v, want missing run pointer failure", err) } } func TestDiscoverRemoteCurrentStateEmptyRunPointerFails(t *testing.T) { cfg := restoreDiscoveryConfig() store := &storage.FakeBackend{} _, manifestKey, runIDKey := restoreDiscoveryKeys(cfg) store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte(" \n\t")}) store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)}) _, err := discoverRemoteCurrentState(context.Background(), cfg, store) if err == nil || !strings.Contains(err.Error(), "is empty") { t.Fatalf("error = %v, want empty run pointer failure", err) } } func TestDiscoverRemoteCurrentStateMissingManifestFails(t *testing.T) { cfg := restoreDiscoveryConfig() store := &storage.FakeBackend{} _, _, runIDKey := restoreDiscoveryKeys(cfg) store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) _, err := discoverRemoteCurrentState(context.Background(), cfg, store) if err == nil || !strings.Contains(err.Error(), "remote current manifest missing") { t.Fatalf("error = %v, want missing manifest failure", err) } } func TestDiscoverRemoteCurrentStateInvalidManifestFails(t *testing.T) { cfg := restoreDiscoveryConfig() store := &storage.FakeBackend{} _, manifestKey, runIDKey := restoreDiscoveryKeys(cfg) store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) store.SeedObject(storage.FakeObject{Key: manifestKey, Data: []byte("{invalid json")}) _, err := discoverRemoteCurrentState(context.Background(), cfg, store) if err == nil || !strings.Contains(err.Error(), "remote current manifest decode failed") { t.Fatalf("error = %v, want manifest decode failure", err) } } func TestDiscoverRemoteCurrentStateSessionMismatchFails(t *testing.T) { cfg := restoreDiscoveryConfig() store := &storage.FakeBackend{} _, manifestKey, runIDKey := restoreDiscoveryKeys(cfg) store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "wrong-session", cfg.Session.Campaign)}) _, err := discoverRemoteCurrentState(context.Background(), cfg, store) if err == nil || !strings.Contains(err.Error(), "does not match requested session_id") { t.Fatalf("error = %v, want session mismatch failure", err) } } func TestDiscoverRemoteCurrentStateCampaignMismatchFails(t *testing.T) { cfg := restoreDiscoveryConfig() store := &storage.FakeBackend{} _, manifestKey, runIDKey := restoreDiscoveryKeys(cfg) store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, "wrong-campaign")}) _, err := discoverRemoteCurrentState(context.Background(), cfg, store) if err == nil || !strings.Contains(err.Error(), "does not match requested campaign") { t.Fatalf("error = %v, want campaign mismatch failure", err) } } func TestDiscoverRemoteCurrentStateEmptyCampaignFails(t *testing.T) { cfg := restoreDiscoveryConfig() store := &storage.FakeBackend{} _, manifestKey, runIDKey := restoreDiscoveryKeys(cfg) store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) store.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, "")}) _, err := discoverRemoteCurrentState(context.Background(), cfg, store) if err == nil || !strings.Contains(err.Error(), "campaign is required") { t.Fatalf("error = %v, want empty campaign failure", err) } } func TestDiscoverRemoteCurrentStateUsesCurrentKeysUnderSessionPrefix(t *testing.T) { cfg := restoreDiscoveryConfig() sessionPrefix, manifestKey, runIDKey := restoreDiscoveryKeys(cfg) base := &storage.FakeBackend{} store := &captureObjectStore{delegate: base} base.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) base.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)}) _, err := discoverRemoteCurrentState(context.Background(), cfg, store) if err != nil { t.Fatalf("discoverRemoteCurrentState() error = %v", err) } expectedRunKey := fmt.Sprintf("%scurrent/run_id.txt", sessionPrefix) expectedManifestKey := fmt.Sprintf("%scurrent/manifest.json", sessionPrefix) if !containsString(store.existsKeys, expectedRunKey) { t.Fatalf("exists keys = %#v, want run pointer key %q", store.existsKeys, expectedRunKey) } if !containsString(store.existsKeys, expectedManifestKey) { t.Fatalf("exists keys = %#v, want manifest key %q", store.existsKeys, expectedManifestKey) } if !containsString(store.downloadKeys, expectedRunKey) { t.Fatalf("download keys = %#v, want run pointer key %q", store.downloadKeys, expectedRunKey) } if !containsString(store.downloadKeys, expectedManifestKey) { t.Fatalf("download keys = %#v, want manifest key %q", store.downloadKeys, expectedManifestKey) } } type captureObjectStore struct { delegate storage.ObjectStore existsKeys []string downloadKeys []string } func (s *captureObjectStore) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) { return s.delegate.List(ctx, prefix) } func (s *captureObjectStore) Download(ctx context.Context, key, localPath string) error { s.downloadKeys = append(s.downloadKeys, key) return s.delegate.Download(ctx, key, localPath) } func (s *captureObjectStore) Upload(ctx context.Context, localPath, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) { return s.delegate.Upload(ctx, localPath, key, opts) } func (s *captureObjectStore) Exists(ctx context.Context, key string) (bool, error) { s.existsKeys = append(s.existsKeys, key) return s.delegate.Exists(ctx, key) } func restoreDiscoveryConfig() *config.Config { return &config.Config{ Pipeline: &config.PipelineConfig{ Storage: config.StorageConfig{ S3: &config.StorageS3Config{ Bucket: "my-dnd-archive", RootPrefix: "dnd", }, }, }, Session: &config.SessionConfig{ SessionID: "2026-05-03", Campaign: "sample-campaign", }, } } func restoreDiscoveryKeys(cfg *config.Config) (sessionPrefix, manifestKey, runIDKey string) { sessionPrefix = artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID) manifestKey, runIDKey = artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) return sessionPrefix, manifestKey, runIDKey } func restoreManifestJSON(t *testing.T, sessionID, campaign string) []byte { t.Helper() now := time.Date(2026, 5, 19, 23, 0, 0, 0, time.UTC).Format(time.RFC3339Nano) payload := map[string]any{ "session_id": sessionID, "campaign": campaign, "created_at": now, "updated_at": now, "stages": map[string]any{}, } data, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal manifest payload: %v", err) } return append(data, '\n') } func containsString(values []string, target string) bool { for _, value := range values { if value == target { return true } } return false }