package app import ( "bytes" "context" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func TestExecuteRunStageArtifactsNonAnalyzeFails(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute( []string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "--artifacts", "session_recap", "polish"}, &stdout, &stderr, ) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), `run-stage: --artifacts is only supported for stage "analyze"`) { t.Fatalf("stderr = %q, want stage-gating error", stderr.String()) } } func TestExecuteUnknownArtifactsFailValidation(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute( []string{"run", "--config", pipelinePath, "--session", sessionPath, "--artifacts", "unknown_artifact"}, &stdout, &stderr, ) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), `run: --artifacts includes unknown artifact "unknown_artifact"`) { t.Fatalf("stderr = %q, want unknown-artifact validation error", stderr.String()) } } func TestRunStageArtifactsDoesNotImplyForce(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") store := &manifest.LocalStore{} seed := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) seed.MarkStageSucceeded("analyze", time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) if err := store.Save(context.Background(), manifestPath, seed); err != nil { t.Fatalf("save manifest: %v", err) } var out bytes.Buffer err := RunStage( context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "--artifacts", "session_recap,session_recap", "analyze"}, &out, ) if err != nil { t.Fatalf("RunStage() error = %v", err) } if !strings.Contains(out.String(), "stage=analyze executed=0 skipped=1 force=false") { t.Fatalf("output = %q, want analyze skip without force", out.String()) } } func TestResumeArtifactsWithSucceededAnalyzeSkipsUnlessForced(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") store := &manifest.LocalStore{} seed := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) for _, stageName := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "analyze", "archive", "notify"} { seed.MarkStageSucceeded(stageName, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) } if err := store.Save(context.Background(), manifestPath, seed); err != nil { t.Fatalf("save manifest: %v", err) } var out bytes.Buffer err := Resume( context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "--artifacts", "session_recap"}, &out, ) if err != nil { t.Fatalf("Resume() error = %v", err) } if !strings.Contains(out.String(), "has no remaining stages") { t.Fatalf("output = %q, want no remaining stages", out.String()) } } func writeValidConfigFilesWithScriptoriumArtifacts(t *testing.T, workspaceRoot string) (string, string) { t.Helper() pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot) f, err := os.OpenFile(pipelinePath, os.O_APPEND|os.O_WRONLY, 0) if err != nil { t.Fatalf("open pipeline config for append: %v", err) } defer f.Close() extra := ` scriptorium: binary: scriptorium artifacts: session_recap: enabled: true prompt_id: dnd.session_recap output_path: artifacts/session_recap.md player_handout: enabled: true prompt_id: dnd.player_handout output_path: artifacts/player_handout.md depends_on: - session_recap inputs: recap: source: narratio.artifact.session_recap required: true ` if _, err := f.WriteString(extra); err != nil { t.Fatalf("append scriptorium config: %v", err) } return pipelinePath, sessionPath }