package app import ( "bytes" "context" "os" "strings" "testing" "gitea.maximumdirect.net/eric/narratio/internal/config" ) func TestPlanRejectsDiscoveredSessionTemplate(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) sessionTemplate := `session_id: "{{ session_id }}" previous_session_id: "{{ previous_session_id }}" campaign: sample-campaign inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml ` if err := os.WriteFile(sessionPath, []byte(sessionTemplate), 0o644); err != nil { t.Fatalf("write session template: %v", err) } origSessionDefaults := append([]string(nil), config.DefaultSessionConfigSearchPaths...) config.DefaultSessionConfigSearchPaths = []string{sessionPath} t.Cleanup(func() { config.DefaultSessionConfigSearchPaths = origSessionDefaults }) var out bytes.Buffer err := Plan(context.Background(), []string{ "2026-04-04", "--config", pipelinePath, "--campaign-file", campaignPath, "--previous-session-id", "2026-03-28", }, &out) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "session.yml must be concrete") { t.Fatalf("error = %q, want concrete session guidance", err.Error()) } if !strings.Contains(err.Error(), "run narratio session init") { t.Fatalf("error = %q, want session init guidance", err.Error()) } } func TestPlanFailsWhenSessionIDMismatchesConcreteSession(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) var out bytes.Buffer err := Plan(context.Background(), []string{"2026-04-04", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "session_id mismatch") { t.Fatalf("error = %q, want mismatch context", err.Error()) } } func TestPlanFailsWhenPreviousSessionIDMismatchesConcreteSession(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) sessionYAML := `session_id: 2026-05-03 previous_session_id: 2026-04-26 campaign: sample-campaign inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml ` if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } var out bytes.Buffer err := Plan(context.Background(), []string{ "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--previous-session-id", "2026-04-25", }, &out) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "previous_session_id mismatch") { t.Fatalf("error = %q, want mismatch context", err.Error()) } } func TestRunStageAcceptsPositionalSessionIDAndParsesStageName(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) var out bytes.Buffer err := RunStage(context.Background(), []string{"prepare", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out) if err != nil { t.Fatalf("RunStage() error = %v", err) } if !strings.Contains(out.String(), "stage=prepare") { t.Fatalf("output = %q, want stage output", out.String()) } } func TestResolveSessionConfigPathErrorIncludesSearchedPaths(t *testing.T) { _, err := resolveSessionConfigPathWithCandidates("", []string{"/usr/local/etc/narratio/session.yml", "/etc/narratio/session.yml"}) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "searched") { t.Fatalf("error = %q, want searched paths", err.Error()) } if !strings.Contains(err.Error(), "pass --session") { t.Fatalf("error = %q, want explicit-session guidance", err.Error()) } }