package app import ( "bytes" "context" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func TestExecuteValidCommands(t *testing.T) { workspaceRoot := t.TempDir() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"source":"commands-test","segments":[{"speaker":"alice"}]}`)) })) defer srv.Close() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL) cases := []struct { name string args []string wantOut string }{ {name: "run", args: []string{"run", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "narratio run: session 2026-05-03; executed=11 skipped=1; manifest="}, {name: "session plan", args: []string{"session", "plan", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "prepare: skip\ntranscribe: skip\nmerge: skip\npolish: skip\nnormalize: skip\ntrim: skip\nextract: run\nrender: skip\nanalyze: skip\npublish: skip\nnotify: skip"}, {name: "session status", args: []string{"session", "status", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "Session: 2026-05-03"}, {name: "run-stage", args: []string{"run-stage", "polish", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "narratio run-stage: stage=polish executed=0 skipped=1 force=false; manifest="}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := Execute(tc.args, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0", code) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } if !strings.Contains(stdout.String(), tc.wantOut) { t.Fatalf("stdout = %q, want to contain %q", stdout.String(), tc.wantOut) } }) } } func TestExecuteMissingRequiredFlags(t *testing.T) { cases := []struct { name string args []string want string }{ {name: "run missing session", args: []string{"run"}, want: "run: session_id is required"}, {name: "plan old top-level removed", args: []string{"plan"}, want: `unknown command: "plan"`}, {name: "status old top-level removed", args: []string{"status"}, want: `unknown command: "status"`}, {name: "resume removed", args: []string{"resume"}, want: `unknown command: "resume"`}, {name: "run-stage missing name", args: []string{"run-stage", "--config", "a", "--session", "b"}, want: "run-stage: expected stage name and session_id"}, {name: "run-stage missing session", args: []string{"run-stage", "polish"}, want: "run-stage: expected stage name and session_id"}, {name: "run missing config uses defaults", args: []string{"run", "2026-05-03", "--session", "session.yml"}, want: "run: no pipeline config path provided and no default pipeline config found; searched:"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := Execute(tc.args, &stdout, &stderr) if code == 0 { t.Fatalf("exit code = 0, want non-zero") } if stdout.Len() != 0 { t.Fatalf("stdout = %q, want empty", stdout.String()) } if !strings.Contains(stderr.String(), tc.want) { t.Fatalf("stderr = %q, want to contain %q", stderr.String(), tc.want) } }) } } func TestExecuteRunStageUnknownFails(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe") var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"run-stage", "unknown", "2026-05-03", "--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(), "unknown stage") { t.Fatalf("stderr = %q, want unknown stage error", stderr.String()) } } func TestExecuteRunStageArchiveAliasFails(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe") var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"run-stage", "archive", "2026-05-03", "--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(), `unknown stage "archive"`) { t.Fatalf("stderr = %q, want unknown stage alias error", stderr.String()) } } func TestExecuteRunStageNormalizeIsAccepted(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe") workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "polished.json"), `{"segments":[{"id":1}]}`) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"run-stage", "normalize", "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(), "stage=normalize executed=1 skipped=0") { t.Fatalf("stdout = %q, want normalize stage execution", stdout.String()) } } func TestExecuteRunStageTranscribeUsesConfiguredWhisperXServer(t *testing.T) { workspaceRoot := t.TempDir() var serverCalls int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serverCalls++ w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"source":"run-stage-test","segments":[{"speaker":"alice"}]}`)) })) defer srv.Close() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"run-stage", "prepare", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr) if code != 0 { t.Fatalf("prepare exit code = %d, want 0; stderr=%q", code, stderr.String()) } stdout.Reset() stderr.Reset() code = Execute([]string{"run-stage", "transcribe", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr) if code != 0 { t.Fatalf("transcribe exit code = %d, want 0; stderr=%q", code, stderr.String()) } if serverCalls == 0 { t.Fatal("expected whisperx server to be called at least once") } outPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "transcripts", "raw", "alice.json") data, err := os.ReadFile(outPath) if err != nil { t.Fatalf("ReadFile(%q): %v", outPath, err) } got := strings.TrimSpace(string(data)) if got == `{"schema":"speaker_transcript.v1","segments":[]}` { t.Fatalf("got noop transcript output: %q", got) } if !strings.Contains(got, `"source":"run-stage-test"`) { t.Fatalf("output = %q, want run-stage server json", got) } } func TestExecuteRunStagePolishLoadsCredentialFromSecretsDir(t *testing.T) { workspaceRoot := t.TempDir() configDir := t.TempDir() sessionID := "2026-05-03" secretsDir := filepath.Join(configDir, "secrets") if err := os.MkdirAll(secretsDir, secretDirectoryPrivateMode); err != nil { t.Fatalf("MkdirAll(%q): %v", secretsDir, err) } if err := os.WriteFile(filepath.Join(secretsDir, "OPENROUTER_API_KEY"), []byte("from-secret-file\n"), 0o600); err != nil { t.Fatalf("write OPENROUTER_API_KEY secret file: %v", err) } seriatimBinary := writeSeriatimAppTestWrapper(t) auditaBinary := writeAuditaAppTestWrapper(t) t.Setenv("GO_WANT_APP_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_APP_AUDITA_HELPER", "1") pipelinePath := filepath.Join(configDir, "pipeline.yml") campaignPath := writeAppTestCampaignConfig(t, configDir) sessionPath := filepath.Join(configDir, "session.yml") pipelineYAML := `workspace: root: ` + workspaceRoot + ` storage: backend: local secrets: env_dir: ./secrets whisperx: transcribe_url: https://example.com/transcribe seriatim: binary: ` + seriatimBinary + ` audita: binary: ` + auditaBinary + ` llm_api_key_env: OPENROUTER_API_KEY notification: mode: noop ` sessionYAML := `session_id: ` + sessionID + ` 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(pipelinePath, []byte(pipelineYAML), 0o644); err != nil { t.Fatalf("write pipeline.yml: %v", err) } if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } originalWD, err := os.Getwd() if err != nil { t.Fatalf("Getwd(): %v", err) } if err := os.Chdir(configDir); err != nil { t.Fatalf("Chdir(%q): %v", configDir, err) } t.Cleanup(func() { _ = os.Chdir(originalWD) }) workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", sessionID) mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"schema":"seriatim-intermediate","segments":[]}`) mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "[]\n") var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"run-stage", "polish", sessionID, "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "stage=polish executed=1 skipped=0") { t.Fatalf("stdout = %q, want polish execution", stdout.String()) } } func TestExecuteRunFailsWhenConfiguredSecretsDirMissing(t *testing.T) { workspaceRoot := t.TempDir() configDir := t.TempDir() pipelinePath := filepath.Join(configDir, "pipeline.yml") campaignPath := writeAppTestCampaignConfig(t, configDir) sessionPath := filepath.Join(configDir, "session.yml") pipelineYAML := `workspace: root: ` + workspaceRoot + ` storage: backend: local secrets: env_dir: ./missing-secrets whisperx: transcribe_url: https://example.com/transcribe seriatim: binary: seriatim audita: binary: audita notification: mode: noop ` 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(pipelinePath, []byte(pipelineYAML), 0o644); err != nil { t.Fatalf("write pipeline.yml: %v", err) } if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session.yml: %v", err) } 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.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "validate secrets env_dir") { t.Fatalf("stderr = %q, want secrets validation error context", stderr.String()) } } func TestExecuteUsesDefaultPipelineConfigPathWhenConfigFlagOmitted(t *testing.T) { workspaceRoot := t.TempDir() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"source":"default-config-test","segments":[{"speaker":"alice"}]}`)) })) defer srv.Close() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL) originalDefaults := append([]string(nil), config.DefaultPipelineConfigSearchPaths...) config.DefaultPipelineConfigSearchPaths = []string{pipelinePath} defer func() { config.DefaultPipelineConfigSearchPaths = originalDefaults }() _ = campaignPath var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"run", "2026-05-03", "--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; executed=11 skipped=1; manifest=") { t.Fatalf("stdout = %q, want successful run output", stdout.String()) } } func TestExecuteMissingCampaignConfigReportsRegistryPath(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) if err := os.Remove(campaignPath); err != nil { t.Fatalf("remove campaign config: %v", err) } var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"run", "2026-05-03", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if stdout.Len() != 0 { t.Fatalf("stdout = %q, want empty", stdout.String()) } if !strings.Contains(stderr.String(), "load campaign config") { t.Fatalf("stderr = %q, want campaign discovery failure", stderr.String()) } if !strings.Contains(stderr.String(), filepath.ToSlash(filepath.Join("campaigns", "sample-campaign", "campaign.yml"))) { t.Fatalf("stderr = %q, want campaign registry path", stderr.String()) } } func TestExecuteUsesPipelineDefaultCampaignID(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, _, sessionPath := writeValidConfigFiles(t, workspaceRoot) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "status", "2026-05-03", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "Campaign: sample-campaign") { t.Fatalf("stdout = %q, want default campaign", stdout.String()) } } func TestExecuteCampaignIDSelectsRegistryCampaign(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) campaignRoot := filepath.Dir(filepath.Dir(campaignPath)) otherDir := filepath.Join(campaignRoot, "icewind") mustWriteTestFile(t, filepath.Join(otherDir, "campaign.yml"), `campaign_id: icewind inputs: speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml players_file: ./players.yml party_file: ./party.yml `) mustWriteTestFile(t, filepath.Join(otherDir, "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n") mustWriteTestFile(t, filepath.Join(otherDir, "autocorrect.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(otherDir, "glossary.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(otherDir, "players.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(otherDir, "party.yml"), "[]\n") var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "status", "2026-05-03", "--config", pipelinePath, "--campaign", "icewind", "--session", sessionPath}, &stdout, &stderr) if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), "Campaign: icewind") { t.Fatalf("stdout = %q, want selected campaign", stdout.String()) } } func TestExecuteRejectsCampaignIDAndCampaignFile(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"session", "status", "2026-05-03", "--config", pipelinePath, "--campaign", "sample-campaign", "--campaign-file", campaignPath, "--session", sessionPath}, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } if !strings.Contains(stderr.String(), "mutually exclusive") { t.Fatalf("stderr = %q, want mutually exclusive error", stderr.String()) } } func TestExecuteInvalidCommand(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := Execute([]string{"bogus"}, &stdout, &stderr) if code == 0 { t.Fatalf("exit code = 0, want non-zero") } if stdout.Len() != 0 { t.Fatalf("stdout = %q, want empty", stdout.String()) } out := stderr.String() if !strings.Contains(out, "unknown command") { t.Fatalf("stderr = %q, want unknown command message", out) } if !strings.Contains(out, "Usage: narratio") { t.Fatalf("stderr = %q, want usage message", out) } } func TestExecuteMissingCommand(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer code := Execute(nil, &stdout, &stderr) if code == 0 { t.Fatalf("exit code = 0, want non-zero") } if stdout.Len() != 0 { t.Fatalf("stdout = %q, want empty", stdout.String()) } if !strings.Contains(stderr.String(), "Usage: narratio") { t.Fatalf("stderr = %q, want usage message", stderr.String()) } } func writeValidConfigFiles(t *testing.T, workspaceRoot string, transcribeURL ...string) (string, string, string) { t.Helper() dir := t.TempDir() pipelinePath := filepath.Join(dir, "pipeline.yml") campaignRoot := filepath.Join(dir, "campaigns") campaignDir := filepath.Join(campaignRoot, "sample-campaign") campaignPath := filepath.Join(campaignDir, "campaign.yml") sessionPath := filepath.Join(dir, "session.yml") url := "https://example.com/transcribe" if len(transcribeURL) > 0 && strings.TrimSpace(transcribeURL[0]) != "" { url = transcribeURL[0] } seriatimBinary := writeSeriatimAppTestWrapper(t) scriptoriumBinary := writeScriptoriumAppTestWrapper(t) auditaBinary := writeAuditaAppTestWrapper(t) t.Setenv("AUDITA_LLM_API_KEY", "test-audita-key") t.Setenv("PATH", filepath.Dir(scriptoriumBinary)+string(os.PathListSeparator)+os.Getenv("PATH")) pipelineYAML := `workspace: root: ` + workspaceRoot + ` campaigns: root: ` + campaignRoot + ` default_campaign_id: sample-campaign cache: root: ` + filepath.Join(workspaceRoot, "cache") + ` spool: root: ` + filepath.Join(workspaceRoot, "spool") + ` storage: backend: s3 s3: bucket: test-bucket publish: enabled: true upload_run: false whisperx: transcribe_url: ` + url + ` timeout: 2s retries: 0 retry_delay: 1ms concurrency: 1 seriatim: binary: ` + seriatimBinary + ` timeout: 10m output_schema: seriatim-intermediate coalesce_gap: 3.0 report: true audita: binary: ` + auditaBinary + ` notification: mode: noop ` sessionYAML := `session_id: 2026-05-03 inputs: audio_dir: ./audio ` campaignYAML := `campaign_id: sample-campaign inputs: speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml players_file: ./players.yml party_file: ./party.yml ` if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil { t.Fatalf("write pipeline config: %v", err) } if err := os.MkdirAll(campaignDir, 0o755); err != nil { t.Fatalf("create campaign dir: %v", err) } if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil { t.Fatalf("write campaign config: %v", err) } if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil { t.Fatalf("write session config: %v", err) } mustWriteTestFile(t, filepath.Join(campaignDir, "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n") mustWriteTestFile(t, filepath.Join(campaignDir, "autocorrect.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(campaignDir, "glossary.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(campaignDir, "players.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(campaignDir, "party.yml"), "[]\n") mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes") return pipelinePath, campaignPath, sessionPath } func writeAppTestCampaignConfig(t *testing.T, dir string) string { t.Helper() campaignPath := filepath.Join(dir, "campaign.yml") campaignYAML := `campaign_id: sample-campaign inputs: speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml players_file: ./players.yml party_file: ./party.yml ` if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil { t.Fatalf("write campaign.yml: %v", err) } return campaignPath } func writeManifestPathForExecute(t *testing.T) string { t.Helper() store := &manifest.LocalStore{} m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) m.MarkStageSucceeded("prepare", time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) path := filepath.Join(t.TempDir(), "manifest.json") if err := store.Save(context.Background(), path, m); err != nil { t.Fatalf("save manifest: %v", err) } return path } func mustWriteTestFile(t *testing.T, path, contents string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("mkdir %q: %v", path, err) } if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { t.Fatalf("write %q: %v", path, err) } } func writeSeriatimAppTestWrapper(t *testing.T) string { t.Helper() exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } path := filepath.Join(t.TempDir(), "seriatim-helper-wrapper.sh") content := "#!/bin/sh\nexec \"" + exe + "\" -test.run=TestSeriatimAppHelper -- \"$@\"\n" if err := os.WriteFile(path, []byte(content), 0o755); err != nil { t.Fatalf("WriteFile(%q): %v", path, err) } return path } func writeScriptoriumAppTestWrapper(t *testing.T) string { t.Helper() exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } path := filepath.Join(t.TempDir(), "scriptorium") content := "#!/bin/sh\nexec \"" + exe + "\" -test.run=TestScriptoriumAppHelper -- \"$@\"\n" if err := os.WriteFile(path, []byte(content), 0o755); err != nil { t.Fatalf("WriteFile(%q): %v", path, err) } return path } func TestScriptoriumAppHelper(t *testing.T) { if !appHelperInvocation() { return } args := os.Args start := -1 for i := range args { if args[i] == "--" { start = i + 1 break } } if start < 0 || start >= len(args) { _, _ = os.Stderr.WriteString("missing -- args separator\n") os.Exit(2) } runArgs := args[start:] outputPath := appSeriatimFlagValue(runArgs, "--out") if strings.TrimSpace(outputPath) == "" { outputPath = appSeriatimFlagValue(runArgs, "--output") } if strings.TrimSpace(outputPath) == "" { _, _ = os.Stderr.WriteString("missing output flag\n") os.Exit(2) } if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir output dir: %v\n", err)) os.Exit(2) } if err := os.WriteFile(outputPath, []byte(`{"trim_action":"copy","warnings":[]}`), 0o644); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("write output: %v\n", err)) os.Exit(2) } _, _ = os.Stdout.WriteString("scriptorium helper stdout\n") _, _ = os.Stderr.WriteString("scriptorium helper stderr\n") os.Exit(0) } func TestSeriatimAppHelper(t *testing.T) { if !appHelperInvocation() { return } args := os.Args start := -1 for i := range args { if args[i] == "--" { start = i + 1 break } } if start < 0 || start >= len(args) { _, _ = os.Stderr.WriteString("missing -- args separator\n") os.Exit(2) } mergeArgs := args[start:] outputPath := appSeriatimFlagValue(mergeArgs, "--output-file") reportPath := appSeriatimFlagValue(mergeArgs, "--report-file") if strings.TrimSpace(outputPath) == "" { _, _ = os.Stderr.WriteString("missing --output-file\n") os.Exit(2) } if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir output dir: %v\n", err)) os.Exit(2) } if err := os.WriteFile(outputPath, []byte(`{"schema":"seriatim-intermediate","segments":[]}`), 0o644); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("write output: %v\n", err)) os.Exit(2) } if strings.TrimSpace(reportPath) != "" { if err := os.MkdirAll(filepath.Dir(reportPath), 0o755); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir report dir: %v\n", err)) os.Exit(2) } if err := os.WriteFile(reportPath, []byte(`{"report":true}`), 0o644); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("write report: %v\n", err)) os.Exit(2) } } _, _ = os.Stdout.WriteString("seriatim helper stdout\n") _, _ = os.Stderr.WriteString("seriatim helper stderr\n") os.Exit(0) } func writeAuditaAppTestWrapper(t *testing.T) string { t.Helper() exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable() error = %v", err) } path := filepath.Join(t.TempDir(), "audita-helper-wrapper.sh") content := "#!/bin/sh\nexec \"" + exe + "\" -test.run=TestAuditaAppHelper -- \"$@\"\n" if err := os.WriteFile(path, []byte(content), 0o755); err != nil { t.Fatalf("WriteFile(%q): %v", path, err) } return path } func TestAuditaAppHelper(t *testing.T) { if !appHelperInvocation() { return } args := os.Args start := -1 for i := range args { if args[i] == "--" { start = i + 1 break } } if start < 0 || start >= len(args) { _, _ = os.Stderr.WriteString("missing -- args separator\n") os.Exit(2) } processArgs := args[start:] outputPath := appSeriatimFlagValue(processArgs, "--output") reportPath := appSeriatimFlagValue(processArgs, "--report-json") workDir := appSeriatimFlagValue(processArgs, "--work-dir") if strings.TrimSpace(outputPath) == "" { _, _ = os.Stderr.WriteString("missing --output\n") os.Exit(2) } if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir output dir: %v\n", err)) os.Exit(2) } if err := os.WriteFile(outputPath, []byte(`{"schema":"audita.processed.v1","segments":[]}`), 0o644); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("write output: %v\n", err)) os.Exit(2) } if strings.TrimSpace(reportPath) != "" { if err := os.MkdirAll(filepath.Dir(reportPath), 0o755); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir report dir: %v\n", err)) os.Exit(2) } if err := os.WriteFile(reportPath, []byte(`{"report":true}`), 0o644); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("write report: %v\n", err)) os.Exit(2) } } if strings.TrimSpace(workDir) != "" { if err := os.MkdirAll(workDir, 0o755); err != nil { _, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir work dir: %v\n", err)) os.Exit(2) } } _, _ = os.Stdout.WriteString("audita helper stdout\n") _, _ = os.Stderr.WriteString("audita helper stderr\n") os.Exit(0) } func appHelperInvocation() bool { for _, arg := range os.Args { if arg == "--" { return true } } return false } func appSeriatimFlagValue(args []string, name string) string { for i := 0; i < len(args)-1; i++ { if args[i] == name { return args[i+1] } } return "" }