package app import ( "bytes" "context" "os" "path/filepath" "strings" "testing" "gitea.maximumdirect.net/eric/narratio/internal/adapters/storage" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func TestExecuteSessionInitRemoteWritesCanonicalSessionConfig(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} var storeInitCalls int restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{filepath.Join(t.TempDir(), "session.yml")}) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{ "session", "init", "--config", pipelinePath, "--campaign", campaignPath, "--session-id", "2026-06-07", "--title", "The Black Cabin", "--remote", }, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } key := artifacts.S3SessionConfigKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-06-07")) obj, ok := fake.Objects[key] if !ok { t.Fatalf("remote session key %q not uploaded; objects=%v", key, fake.Objects) } if !strings.Contains(string(obj.Data), `session_id: "2026-06-07"`) || !strings.Contains(string(obj.Data), "prefix: audio/") { t.Fatalf("remote session data = %q", string(obj.Data)) } if storeInitCalls != 1 { t.Fatalf("object store init calls = %d, want 1", storeInitCalls) } } func TestExecuteLockAndUnlockUseRemoteLockStore(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} var storeInitCalls int restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{ "lock", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--reason", "manual edit", "narratio.transcript.trimmed", }, &stdout, &stderr) if code != 0 { t.Fatalf("lock exit code = %d, want 0; stderr=%q", code, stderr.String()) } key := artifacts.S3SessionLocksKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")) obj, ok := fake.Objects[key] if !ok { t.Fatalf("remote locks key %q not uploaded", key) } if !strings.Contains(string(obj.Data), "source: narratio.transcript.trimmed") || !strings.Contains(string(obj.Data), "reason: manual edit") { t.Fatalf("lock store data = %q", string(obj.Data)) } stdout.Reset() stderr.Reset() code = Execute([]string{ "unlock", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "narratio.transcript.trimmed", }, &stdout, &stderr) if code != 0 { t.Fatalf("unlock exit code = %d, want 0; stderr=%q", code, stderr.String()) } store, err := config.LoadArchiveLockStoreBytes("locks.yml", fake.Objects[key].Data, nil) if err != nil { t.Fatalf("LoadArchiveLockStoreBytes() error = %v", err) } if len(store.Locks) != 0 { t.Fatalf("locks after unlock = %#v, want empty", store.Locks) } if storeInitCalls != 2 { t.Fatalf("object store init calls = %d, want 2", storeInitCalls) } } func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} trimmedKey := artifacts.S3PromotedArtifactKey( artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/trimmed.json", ) fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)}) var storeInitCalls int restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{ "artifacts", "list", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--remote", }, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "narratio.transcript.trimmed remote=promoted") { t.Fatalf("stdout = %q, want promoted remote availability", stdout.String()) } } func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidArchiveConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} lockKey := artifacts.S3SessionLocksKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")) fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.trimmed\n reason: remote review\n")}) var storeInitCalls int restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") for _, stageName := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "analyze"} { // The archive stage only checks the manifest statuses and source files. _ = stageName } mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "trimmed.json"), `{"segments":[]}`) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"run-stage", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force", "archive"}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } promotedKey := artifacts.S3PromotedArtifactKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/trimmed.json") if _, ok := fake.Objects[promotedKey]; ok { t.Fatalf("locked promoted key %q was uploaded", promotedKey) } } func writeValidArchiveConfigFiles(t *testing.T, workspaceRoot string) (string, string, string) { t.Helper() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) data, err := os.ReadFile(pipelinePath) if err != nil { t.Fatalf("read pipeline: %v", err) } updated := strings.Replace(string(data), "upload_run: false", "upload_run: true", 1) if err := os.WriteFile(pipelinePath, []byte(updated), 0o644); err != nil { t.Fatalf("write pipeline: %v", err) } ctx := context.Background() cfg, err := config.LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, config.SessionLoadOptions{}) if err != nil { t.Fatalf("LoadWithSessionOptions() error = %v", err) } store := &manifest.LocalStore{} m := manifest.New("2026-05-03", nowUTC()) m.Campaign = "sample-campaign" m.RunID = "20260521T160000Z-test" for _, name := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "analyze"} { m.MarkStageSucceeded(name, nowUTC(), nil) } path := artifacts.SessionManifestPathForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID) if err := store.Save(ctx, path, m); err != nil { t.Fatalf("save manifest: %v", err) } runManifestPath := artifacts.SessionRunManifestPathForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, m.RunID) if err := os.MkdirAll(filepath.Dir(runManifestPath), 0o755); err != nil { t.Fatalf("mkdir run manifest: %v", err) } if err := os.WriteFile(runManifestPath, []byte("{}\n"), 0o644); err != nil { t.Fatalf("write run manifest: %v", err) } return pipelinePath, campaignPath, sessionPath }