354 lines
12 KiB
Go
354 lines
12 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/stage"
|
|
)
|
|
|
|
func TestExecuteRunAcceptsPositionalSessionID(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
|
|
var capturedSessionID string
|
|
origExecuteStagesFn := executeStagesFn
|
|
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
|
|
executeStagesFn = func(_ context.Context, cfg *config.Config, _ []stage.Stage, _ RunOptions) (*RunSummary, error) {
|
|
capturedSessionID = cfg.Session.SessionID
|
|
return &RunSummary{
|
|
SessionID: cfg.Session.SessionID,
|
|
ManifestPath: filepath.Join(workspaceRoot, "manifest.json"),
|
|
Executed: []string{"prepare"},
|
|
}, nil
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{
|
|
"run",
|
|
"2026-05-03",
|
|
"--config", pipelinePath,
|
|
"--campaign-file", campaignPath,
|
|
"--session", sessionPath,
|
|
}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "narratio run: session 2026-05-03") {
|
|
t.Fatalf("stdout = %q, want run summary", stdout.String())
|
|
}
|
|
if capturedSessionID != "2026-05-03" {
|
|
t.Fatalf("captured session = %q, want positional session id", capturedSessionID)
|
|
}
|
|
}
|
|
|
|
func TestExecutePositionalSessionIDMismatchFails(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{
|
|
"run",
|
|
"2026-05-04",
|
|
"--config", pipelinePath,
|
|
"--campaign-file", campaignPath,
|
|
"--session", sessionPath,
|
|
}, &stdout, &stderr)
|
|
if code == 0 {
|
|
t.Fatal("exit code = 0, want non-zero")
|
|
}
|
|
if !strings.Contains(stderr.String(), "session_id mismatch") {
|
|
t.Fatalf("stderr = %q, want session mismatch", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteSessionIDFlagFails(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"session", "status", "2026-05-03", "--session-id", "2026-05-04"}, &stdout, &stderr)
|
|
if code == 0 {
|
|
t.Fatal("exit code = 0, want non-zero")
|
|
}
|
|
if !strings.Contains(stderr.String(), "flag provided but not defined: -session-id") {
|
|
t.Fatalf("stderr = %q, want invalid --session-id flag", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestExecuteRemoteSessionFallbackUsesPositionalSessionID(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")})
|
|
remoteKey := seedRemoteSessionConfig(t, fake, "2026-06-07", `session_id: 2026-06-07
|
|
inputs:
|
|
audio_s3:
|
|
prefix: audio/
|
|
`)
|
|
origExecuteStagesFn := executeStagesFn
|
|
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
|
|
executeStagesFn = func(_ context.Context, cfg *config.Config, _ []stage.Stage, _ RunOptions) (*RunSummary, error) {
|
|
return &RunSummary{
|
|
SessionID: cfg.Session.SessionID,
|
|
ManifestPath: filepath.Join(workspaceRoot, "manifest.json"),
|
|
Executed: []string{"prepare"},
|
|
}, nil
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{
|
|
"run",
|
|
"2026-06-07",
|
|
"--config", pipelinePath,
|
|
"--campaign-file", campaignPath,
|
|
}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
downloaded := false
|
|
for _, call := range fake.Downloads {
|
|
if call.Key == remoteKey {
|
|
downloaded = true
|
|
break
|
|
}
|
|
}
|
|
if !downloaded {
|
|
t.Fatalf("remote session %q was not downloaded; downloads=%v", remoteKey, fake.Downloads)
|
|
}
|
|
if storeInitCalls == 0 {
|
|
t.Fatal("object store was not initialized")
|
|
}
|
|
}
|
|
|
|
func TestExecuteWorkflowCommandsAcceptPositionalSessionID(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
wantStage string
|
|
wantForce bool
|
|
}{
|
|
{
|
|
name: "resume",
|
|
args: []string{"resume", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
|
|
wantStage: "prepare",
|
|
wantForce: false,
|
|
},
|
|
{
|
|
name: "analyze",
|
|
args: []string{"analyze", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--artifacts", "session_recap"},
|
|
wantStage: "analyze",
|
|
wantForce: true,
|
|
},
|
|
{
|
|
name: "publish",
|
|
args: []string{"publish", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--artifacts", "session_recap"},
|
|
wantStage: "publish",
|
|
wantForce: true,
|
|
},
|
|
{
|
|
name: "run-stage",
|
|
args: []string{"run-stage", "publish", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--artifacts", "session_recap"},
|
|
wantStage: "publish",
|
|
wantForce: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var capturedStages []string
|
|
var capturedForce bool
|
|
var capturedArtifacts []string
|
|
origExecuteStagesFn := executeStagesFn
|
|
t.Cleanup(func() { executeStagesFn = origExecuteStagesFn })
|
|
executeStagesFn = func(_ context.Context, _ *config.Config, stages []stage.Stage, opts RunOptions) (*RunSummary, error) {
|
|
for _, s := range stages {
|
|
capturedStages = append(capturedStages, s.Name())
|
|
}
|
|
capturedForce = opts.Force
|
|
capturedArtifacts = append([]string(nil), opts.SelectedArtifacts...)
|
|
return &RunSummary{
|
|
SessionID: "2026-05-03",
|
|
ManifestPath: filepath.Join(workspaceRoot, "manifest.json"),
|
|
Executed: []string{tt.wantStage},
|
|
}, nil
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute(tt.args, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
if len(capturedStages) == 0 || capturedStages[0] != tt.wantStage {
|
|
t.Fatalf("captured stages = %#v, want first %q", capturedStages, tt.wantStage)
|
|
}
|
|
if capturedForce != tt.wantForce {
|
|
t.Fatalf("captured force = %t, want %t", capturedForce, tt.wantForce)
|
|
}
|
|
if tt.name == "analyze" || tt.name == "publish" || tt.name == "run-stage" {
|
|
if strings.Join(capturedArtifacts, ",") != "session_recap" {
|
|
t.Fatalf("captured artifacts = %#v, want [session_recap]", capturedArtifacts)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteSessionSubcommandsAcceptPositionalSessionID(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
fake := &storage.FakeBackend{}
|
|
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
|
|
manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix)
|
|
fake.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
|
fake.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "2026-05-03", "sample-campaign")})
|
|
var storeInitCalls int
|
|
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "validate",
|
|
args: []string{"session", "validate", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
|
|
want: "OK config",
|
|
},
|
|
{
|
|
name: "status",
|
|
args: []string{"session", "status", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
|
|
want: "Session: 2026-05-03",
|
|
},
|
|
{
|
|
name: "plan",
|
|
args: []string{"session", "plan", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
|
|
want: "narratio session plan: workdir prepared",
|
|
},
|
|
{
|
|
name: "artifacts",
|
|
args: []string{"session", "artifacts", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
|
|
want: "Built-in:",
|
|
},
|
|
{
|
|
name: "locks",
|
|
args: []string{"session", "locks", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
|
|
want: "Publish locks:",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute(tt.args, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), tt.want) {
|
|
t.Fatalf("stdout = %q, want %q", stdout.String(), tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExecuteSessionInitAcceptsPositionalSessionID(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
|
|
outputPath := filepath.Join(t.TempDir(), "session.yml")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{
|
|
"session", "init", "2026-06-07",
|
|
"--config", pipelinePath,
|
|
"--campaign-file", campaignPath,
|
|
"--output", outputPath,
|
|
}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
data, err := os.ReadFile(outputPath)
|
|
if err != nil {
|
|
t.Fatalf("read generated session: %v", err)
|
|
}
|
|
if !strings.Contains(string(data), `session_id: "2026-06-07"`) {
|
|
t.Fatalf("generated session = %q, want positional session id", string(data))
|
|
}
|
|
}
|
|
|
|
func TestExecuteSessionLocksMutationAcceptsPositionalSessionID(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{
|
|
"session", "locks", "add", "2026-05-03", "narratio.transcript.final_trimmed",
|
|
"--config", pipelinePath,
|
|
"--campaign-file", campaignPath,
|
|
"--session", sessionPath,
|
|
"--reason", "review",
|
|
}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("locks add exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
key := artifacts.S3SessionLocksKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"))
|
|
if !strings.Contains(string(fake.Objects[key].Data), "reason: review") {
|
|
t.Fatalf("lock store data = %q, want reason", string(fake.Objects[key].Data))
|
|
}
|
|
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
code = Execute([]string{
|
|
"session", "locks", "remove", "2026-05-03", "narratio.transcript.final_trimmed",
|
|
"--config", pipelinePath,
|
|
"--campaign-file", campaignPath,
|
|
"--session", sessionPath,
|
|
}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
store, err := config.LoadPublishLockStoreBytes("locks.yml", fake.Objects[key].Data, nil)
|
|
if err != nil {
|
|
t.Fatalf("LoadPublishLockStoreBytes() error = %v", err)
|
|
}
|
|
if len(store.Locks) != 0 {
|
|
t.Fatalf("locks after remove = %#v, want empty", store.Locks)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCleanAcceptsPositionalSessionID(t *testing.T) {
|
|
workspaceRoot := t.TempDir()
|
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
|
workDir := artifacts.SessionWorkDirForCampaign(workspaceRoot, "sample-campaign", "2026-05-03")
|
|
spoolDir := artifacts.SessionSpoolDir(filepath.Join(workspaceRoot, "spool"), "sample-campaign", "2026-05-03")
|
|
mustWriteTestFile(t, filepath.Join(workDir, "manifest.json"), "{}")
|
|
mustWriteTestFile(t, filepath.Join(spoolDir, "run-1", "audio", "alice.flac"), "audio")
|
|
|
|
var stdout bytes.Buffer
|
|
var stderr bytes.Buffer
|
|
code := Execute([]string{"clean", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
|
}
|
|
cleanAssertMissing(t, workDir)
|
|
cleanAssertMissing(t, spoolDir)
|
|
}
|