Session configuration templates are now proceeded by narratio session init; all other commands require concrete configuration

This commit is contained in:
2026-05-22 17:38:23 -05:00
parent d0936fb022
commit 7324c5a686
20 changed files with 550 additions and 299 deletions

View File

@@ -214,6 +214,209 @@ func TestExecuteSessionInitRemoteLoadsSecretsBeforeObjectStoreInit(t *testing.T)
}
}
func TestExecuteSessionInitLocalRendersCampaignTemplate(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}"
previous_session_id: "{{ previous_session_id }}"
date: "{{ date }}"
title: "{{ title }}"
inputs:
audio_s3:
prefix: "{{ audio_s3_prefix }}"
`)
outputPath := 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",
"--previous-session-id", "2026-05-31",
"--date", "2026-06-07",
"--title", "The Black Cabin",
"--audio-s3-prefix", "audio/",
"--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)
}
got := string(data)
for _, want := range []string{
`session_id: "2026-06-07"`,
`previous_session_id: "2026-05-31"`,
`date: "2026-06-07"`,
`title: "The Black Cabin"`,
`prefix: "audio/"`,
} {
if !strings.Contains(got, want) {
t.Fatalf("generated session = %q, want %q", got, want)
}
}
if strings.Contains(got, "{{") {
t.Fatalf("generated session still contains template placeholder: %q", got)
}
}
func TestExecuteSessionInitRemoteRendersCampaignTemplate(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}"
inputs:
audio_s3:
prefix: audio/
`)
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",
"--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), "{{") || !strings.Contains(string(obj.Data), `session_id: "2026-06-07"`) {
t.Fatalf("remote session data = %q, want rendered concrete session", string(obj.Data))
}
}
func TestExecuteSessionInitTemplatePathIsCampaignRelative(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
templateDir := filepath.Join(filepath.Dir(campaignPath), "templates")
if err := os.MkdirAll(templateDir, 0o755); err != nil {
t.Fatalf("mkdir template dir: %v", err)
}
templatePath := filepath.Join(templateDir, "session.template.yml")
if err := os.WriteFile(templatePath, []byte(`session_id: "{{ session_id }}"
inputs:
audio_dir: ./audio
`), 0o644); err != nil {
t.Fatalf("write session template: %v", err)
}
addSessionTemplateToCampaign(t, campaignPath, "./templates/session.template.yml")
outputPath := 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",
"--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 campaign-relative template output", string(data))
}
}
func TestExecuteSessionInitTemplateMissingVariableFails(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}"
date: "{{ date }}"
inputs:
audio_s3:
prefix: audio/
`)
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute([]string{
"session", "init",
"--config", pipelinePath,
"--campaign", campaignPath,
"--session-id", "2026-06-07",
"--remote",
}, &stdout, &stderr)
if code == 0 {
t.Fatal("exit code = 0, want non-zero")
}
if !strings.Contains(stderr.String(), "missing required template variable value(s): date") {
t.Fatalf("stderr = %q, want missing date variable", stderr.String())
}
}
func TestExecuteSessionInitTemplateUnusedFlagFails(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}"
inputs:
audio_s3:
prefix: audio/
`)
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute([]string{
"session", "init",
"--config", pipelinePath,
"--campaign", campaignPath,
"--session-id", "2026-06-07",
"--title", "Unused Title",
"--remote",
}, &stdout, &stderr)
if code == 0 {
t.Fatal("exit code = 0, want non-zero")
}
if !strings.Contains(stderr.String(), "unused template variable value(s): title") {
t.Fatalf("stderr = %q, want unused title variable", stderr.String())
}
}
func TestExecuteSessionInitTemplateStrictDecodeFailure(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
writeSessionInitTemplate(t, campaignPath, `session_id: "{{ session_id }}"
unknown: true
inputs:
audio_s3:
prefix: audio/
`)
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute([]string{
"session", "init",
"--config", pipelinePath,
"--campaign", campaignPath,
"--session-id", "2026-06-07",
"--remote",
}, &stdout, &stderr)
if code == 0 {
t.Fatal("exit code = 0, want non-zero")
}
if !strings.Contains(stderr.String(), "strict decode failed") {
t.Fatalf("stderr = %q, want strict decode error", stderr.String())
}
}
func TestExecuteSessionValidateLoadsSecretsBeforeObjectStoreInit(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
@@ -486,6 +689,30 @@ func withDefaultPipelineCampaignConfigs(t *testing.T, pipelinePath, campaignPath
})
}
func writeSessionInitTemplate(t *testing.T, campaignPath, templateYAML string) {
t.Helper()
templatePath := filepath.Join(filepath.Dir(campaignPath), "session.template.yml")
if err := os.WriteFile(templatePath, []byte(templateYAML), 0o644); err != nil {
t.Fatalf("write session template: %v", err)
}
addSessionTemplateToCampaign(t, campaignPath, "./session.template.yml")
}
func addSessionTemplateToCampaign(t *testing.T, campaignPath, templateFile string) {
t.Helper()
data, err := os.ReadFile(campaignPath)
if err != nil {
t.Fatalf("read campaign config: %v", err)
}
if strings.Contains(string(data), "session_template_file:") {
t.Fatalf("campaign config already has session_template_file: %q", string(data))
}
updated := "session_template_file: " + templateFile + "\n" + string(data)
if err := os.WriteFile(campaignPath, []byte(updated), 0o644); err != nil {
t.Fatalf("write campaign config: %v", err)
}
}
func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)