package config import ( "os" "path/filepath" "strings" "testing" ) func TestLoadSessionWithOptionsRejectsCompactPlaceholder(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.yml") sessionYAML := `session_id: "{{session_id}}" campaign: sample-campaign inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml players_file: ./players.yml party_file: ./party.yml ` if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } _, err := LoadSessionWithOptions(sessionPath, SessionLoadOptions{SessionID: "2026-04-04"}) if err == nil { t.Fatal("expected error, got nil") } assertConcreteSessionTemplateError(t, err, "session_id") } func TestLoadSessionWithOptionsRejectsSpacedPlaceholder(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.yml") sessionYAML := `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 players_file: ./players.yml party_file: ./party.yml ` if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } _, err := LoadSessionWithOptions(sessionPath, SessionLoadOptions{ SessionID: "2026-04-04", PreviousSessionID: "2026-03-28", }) if err == nil { t.Fatal("expected error, got nil") } assertConcreteSessionTemplateError(t, err, "session_id", "previous_session_id") } func TestLoadSessionWithOptionsMismatchFails(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.yml") sessionYAML := `session_id: 2026-05-03 campaign: sample-campaign inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml players_file: ./players.yml party_file: ./party.yml ` if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } _, err := LoadSessionWithOptions(sessionPath, SessionLoadOptions{SessionID: "2026-04-04"}) 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()) } if strings.Contains(err.Error(), "rendered") { t.Fatalf("error = %q, should not mention rendered session", err.Error()) } } func TestLoadSessionWithOptionsPreviousSessionMismatchFails(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.yml") 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 players_file: ./players.yml party_file: ./party.yml ` if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } _, err := LoadSessionWithOptions(sessionPath, SessionLoadOptions{ SessionID: "2026-05-03", PreviousSessionID: "2026-04-25", }) 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()) } if strings.Contains(err.Error(), "rendered") { t.Fatalf("error = %q, should not mention rendered session", err.Error()) } } func TestLoadSessionWithOptionsRequiresExpectedPreviousSession(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.yml") sessionYAML := `session_id: 2026-05-03 campaign: sample-campaign inputs: audio_dir: ./audio ` if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } _, err := LoadSessionWithOptions(sessionPath, SessionLoadOptions{PreviousSessionID: "2026-04-26"}) if err == nil || !strings.Contains(err.Error(), "previous_session_id is required") { t.Fatalf("LoadSessionWithOptions() error = %v, want missing previous-session expectation error", err) } } func TestLoadSessionWithOptionsUnknownFieldStillRejected(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.yml") sessionYAML := `session_id: 2026-04-04 campaign: sample-campaign unknown_field: true inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml players_file: ./players.yml party_file: ./party.yml ` if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } _, err := LoadSessionWithOptions(sessionPath, SessionLoadOptions{SessionID: "2026-04-04"}) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "strict decode failed") { t.Fatalf("error = %q, want strict-decode context", err.Error()) } } func TestLoadSessionWithOptionsConcreteSessionStillLoads(t *testing.T) { dir := t.TempDir() sessionPath := filepath.Join(dir, "session.yml") sessionYAML := `session_id: 2026-05-03 campaign: sample-campaign inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml players_file: ./players.yml party_file: ./party.yml ` if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } cfg, err := LoadSessionWithOptions(sessionPath, SessionLoadOptions{}) if err != nil { t.Fatalf("LoadSessionWithOptions() error = %v", err) } if cfg.SessionID != "2026-05-03" { t.Fatalf("SessionID = %q, want 2026-05-03", cfg.SessionID) } } func TestLoadSessionBytesWithOptionsRejectsPlaceholder(t *testing.T) { sessionYAML := []byte(`session_id: "{{ session_id }}" campaign: sample-campaign inputs: audio_s3: prefix: audio/ `) _, err := LoadSessionBytesWithOptions("s3://bucket/session.yml", sessionYAML, SessionLoadOptions{SessionID: "2026-05-03"}) if err == nil { t.Fatal("expected error, got nil") } assertConcreteSessionTemplateError(t, err, "session_id") } func TestLoadSessionBytesWithOptionsStrictDecode(t *testing.T) { cfg, err := LoadSessionBytesWithOptions("s3://bucket/session.yml", []byte("session_id: 2026-05-03\n"), SessionLoadOptions{}) if err != nil { t.Fatalf("LoadSessionBytesWithOptions() error = %v", err) } if cfg.SessionID != "2026-05-03" { t.Fatalf("SessionID = %q, want 2026-05-03", cfg.SessionID) } _, err = LoadSessionBytesWithOptions("s3://bucket/session.yml", []byte("session_id: 2026-05-03\nunknown: true\n"), SessionLoadOptions{}) if err == nil { t.Fatal("expected strict decode error, got nil") } if !strings.Contains(err.Error(), "strict decode failed") { t.Fatalf("error = %q, want strict decode context", err.Error()) } } func TestLoadSessionBytesWithOptionsMismatchFails(t *testing.T) { _, err := LoadSessionBytesWithOptions("s3://bucket/session.yml", []byte("session_id: 2026-05-03\n"), SessionLoadOptions{SessionID: "2026-04-04"}) if err == nil { t.Fatal("expected mismatch error, got nil") } if !strings.Contains(err.Error(), "session_id mismatch") { t.Fatalf("error = %q, want mismatch context", err.Error()) } } func TestLoadSessionBytesWithOptionsRequiresExpectedPreviousSession(t *testing.T) { _, err := LoadSessionBytesWithOptions( "s3://bucket/session.yml", []byte("session_id: 2026-05-03\n"), SessionLoadOptions{PreviousSessionID: "2026-04-26"}, ) if err == nil || !strings.Contains(err.Error(), "previous_session_id is required") { t.Fatalf("LoadSessionBytesWithOptions() error = %v, want missing previous-session expectation error", err) } } func assertConcreteSessionTemplateError(t *testing.T, err error, vars ...string) { t.Helper() 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()) } for _, name := range vars { if !strings.Contains(err.Error(), name) { t.Fatalf("error = %q, want variable %q", err.Error(), name) } } }