196 lines
7.3 KiB
Go
196 lines
7.3 KiB
Go
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
|
|
}
|