package app import ( "bytes" "context" "fmt" "os" "path/filepath" "strings" "testing" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) const ( sharedProviderName = "NARRATIO_SHARED_PROVIDER_KEY" auditaOnlyName = "NARRATIO_AUDITA_ONLY_KEY" sharedProviderKey = "synthetic-shared-provider-value" auditaOnlyKey = "synthetic-audita-only-value" ) func TestCommandsShareProviderCredentialAcrossAdapters(t *testing.T) { configDir := t.TempDir() workspace := t.TempDir() secretsDir := filepath.Join(configDir, "secrets") for name, value := range map[string]string{ sharedProviderName: sharedProviderKey, auditaOnlyName: auditaOnlyKey, } { restoreEnvAfterTest(t, name) if err := os.Unsetenv(name); err != nil { t.Fatal(err) } mustWriteSecretFile(t, filepath.Join(secretsDir, name), value+"\n") } fixture := writeProviderProcessWrapper(t) seriatim := writeSeriatimAppTestWrapper(t) configPath := filepath.Join(configDir, "pipeline.yml") campaignPath := writeAppTestCampaignConfig(t, configDir) sessionPath := filepath.Join(configDir, "session.yml") notariusConfig := filepath.Join(configDir, "notarius.yml") mustWriteTestFile(t, notariusConfig, "pipelines: {}\n") mustWriteTestFile(t, configPath, `workspace: root: `+workspace+` storage: backend: local whisperx: transcribe_url: https://example.com/transcribe seriatim: binary: `+seriatim+` audita: binary: `+fixture+` credentials: [`+sharedProviderName+`, `+auditaOnlyName+`] scriptorium: binary: `+fixture+` credentials: [`+sharedProviderName+`] render_debug: true artifacts: recap: enabled: true prompt_id: recap output_path: artifacts/recap.md inputs: transcript: source: narratio.transcript.final_trimmed notarius: enabled: true binary: `+fixture+` config_path: `+notariusConfig+` pipeline_id: dnd-session credentials: [`+sharedProviderName+`] outputs: npc_registry: lane_id: npc-registry media_type: application/json schema_id: notarius.dnd.npc_registry schema_version: v1 module_key: dnd/npc-registry notification: mode: noop `) mustWriteTestFile(t, sessionPath, `session_id: 2026-05-03 campaign: sample-campaign inputs: audio_dir: ./audio speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml `) var stdout, stderr bytes.Buffer if code := Execute([]string{"config", "show", "--config", configPath}, &stdout, &stderr); code != 0 { t.Fatalf("config show exit = %d; stderr=%q", code, stderr.String()) } if !strings.Contains(stdout.String(), sharedProviderName) || !strings.Contains(stdout.String(), auditaOnlyName) { t.Fatalf("config show omitted reference names: %q", stdout.String()) } assertNoProviderValues(t, stdout.Bytes(), stderr.Bytes()) workRoot := filepath.Join(workspace, "work", "sample-campaign", "2026-05-03") manifestPath := filepath.Join(workRoot, "manifest.json") basePath := filepath.Join(workRoot, "transcripts", "base.json") mustWriteTestFile(t, basePath, `{"schema":"seriatim-intermediate","segments":[]}`) seedCurrentBuiltInEvidence(t, manifestPath, "2026-05-03", "sample-campaign", "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, basePath) mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "[]\n") args := []string{"--config", configPath, "--campaign-file", campaignPath, "--session", sessionPath} stdout.Reset() stderr.Reset() if code := Execute(append([]string{"run-stage", "polish", "2026-05-03", "--force"}, args...), &stdout, &stderr); code != 0 { t.Fatalf("polish exit = %d; stderr=%q", code, stderr.String()) } assertNoProviderValues(t, stdout.Bytes(), stderr.Bytes()) m, err := (&manifest.LocalStore{}).Load(context.Background(), manifestPath) if err != nil { t.Fatal(err) } if polish := m.Stages["polish"]; polish == nil || len(polish.Outputs) != 1 || len(polish.Logs) != 5 { t.Fatalf("polish did not retain canonical transcript and ordinary reports: %#v", polish) } for _, name := range []string{"errors.yml", "recoveries.yml", "report.md"} { found := false for _, path := range m.Stages["polish"].Logs { found = found || filepath.Base(path) == name } if !found { t.Fatalf("missing polish report %q", name) } } trimmedPath := filepath.Join(workRoot, "transcripts", "final.trimmed.json") mustWriteTestFile(t, trimmedPath, `{"segments":[]}`) setCurrentBuiltInEvidence(t, m, "trim", artifacts.TranscriptOutputKindFinalTrimmed, artifacts.ArtifactTranscriptFinalTrimmed, trimmedPath) if err := (&manifest.LocalStore{}).Save(context.Background(), manifestPath, m); err != nil { t.Fatal(err) } stdout.Reset() stderr.Reset() if code := Execute(append([]string{"run-stage", "extract", "2026-05-03", "--force"}, args...), &stdout, &stderr); code != 0 { t.Fatalf("extract exit = %d; stderr=%q", code, stderr.String()) } assertNoProviderValues(t, stdout.Bytes(), stderr.Bytes()) if !strings.Contains(stdout.String(), "stage=extract executed=1") { t.Fatalf("extract did not execute: %q", stdout.String()) } redactedLogs := 0 stdout.Reset() stderr.Reset() if code := Execute(append([]string{"analyze", "2026-05-03"}, args...), &stdout, &stderr); code != 0 { t.Fatalf("analyze exit = %d; stderr=%q", code, stderr.String()) } assertNoProviderValues(t, stdout.Bytes(), stderr.Bytes()) if err := filepath.WalkDir(workRoot, func(path string, entry os.DirEntry, walkErr error) error { if walkErr != nil || entry.IsDir() { return walkErr } data, err := os.ReadFile(path) if err == nil { assertNoProviderValues(t, data) if strings.HasSuffix(path, ".stderr.log") && bytes.Contains(data, []byte("")) { redactedLogs++ } } return err }); err != nil { t.Fatal(err) } if redactedLogs < 4 { t.Fatalf("redacted subprocess logs = %d, want Audita, Notarius and Scriptorium run/render", redactedLogs) } } func assertNoProviderValues(t *testing.T, content ...[]byte) { t.Helper() for _, data := range content { if bytes.Contains(data, []byte(sharedProviderKey)) || bytes.Contains(data, []byte(auditaOnlyKey)) { t.Fatal("credential value appeared in inspection or workspace output") } } } func writeProviderProcessWrapper(t *testing.T) string { t.Helper() exe, err := os.Executable() if err != nil { t.Fatal(err) } path := filepath.Join(t.TempDir(), "provider-process.sh") if err := os.WriteFile(path, []byte("#!/bin/sh\nexec \""+exe+"\" -test.run=TestProviderCredentialProcessFixture -- \"$@\"\n"), 0o755); err != nil { t.Fatal(err) } return path } func TestProviderCredentialProcessFixture(t *testing.T) { if !appHelperInvocation() { return } start := -1 for i, arg := range os.Args { if arg == "--" { start = i + 1 break } } if start < 0 || start >= len(os.Args) { os.Exit(2) } args := os.Args[start:] if os.Getenv(sharedProviderName) != sharedProviderKey { _, _ = os.Stderr.WriteString("shared credential not forwarded\n") os.Exit(2) } if appSeriatimFlagValue(args, "--prompt") != "" { if _, present := os.LookupEnv(auditaOnlyName); present { os.Exit(2) } if appSeriatimFlagValue(args, "--api-key-env") != "" { os.Exit(2) } output := appSeriatimFlagValue(args, "--out") if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil { os.Exit(2) } if err := os.WriteFile(output, []byte("{}\n"), 0o644); err != nil { os.Exit(2) } _, _ = os.Stdout.WriteString(sharedProviderKey + "\n") _, _ = os.Stderr.WriteString(sharedProviderKey + "\n") os.Exit(0) } switch args[0] { case "process": if os.Getenv(auditaOnlyName) != auditaOnlyKey { _, _ = os.Stderr.WriteString("Audita-only credential not forwarded\n") os.Exit(2) } output := appSeriatimFlagValue(args, "--output") if output == "" { os.Exit(2) } if err := os.MkdirAll(filepath.Dir(output), 0o755); err != nil { os.Exit(2) } if err := os.WriteFile(output, []byte(`{"schema":"audita.processed.v1","segments":[]}`), 0o644); err != nil { os.Exit(2) } for _, name := range []string{"errors.yml", "recoveries.yml", "report.md"} { if err := os.WriteFile(filepath.Join(filepath.Dir(output), name), []byte("status: completed\n"), 0o644); err != nil { os.Exit(2) } } case "run": if _, present := os.LookupEnv(auditaOnlyName); present { _, _ = os.Stderr.WriteString("Audita-only credential reached Notarius\n") os.Exit(2) } root := appSeriatimFlagValue(args, "--output-dir") bundle := filepath.Join(root, "fixture-run") if err := os.MkdirAll(filepath.Join(bundle, "lanes"), 0o755); err != nil { os.Exit(2) } for name, value := range map[string]string{ "manifest.json": "{}", "rejected.json": `{"rejected":[]}`, "warnings.json": `{"schema_version":"notarius.warnings.v2","group_count":0,"occurrence_count":0,"groups":[]}`, "diagnostics.json": `{"schema_version":"notarius.diagnostics.v1","group_count":0,"occurrence_count":0,"truncated":false,"unrepresented_occurrence_count":0,"groups":[]}`, "lanes/npc.json": `{"records":[]}`, "index.json": `{"manifest_file":"manifest.json","output_files":[{"lane_id":"npc-registry","file":"lanes/npc.json","media_type":"application/json","module_key":"dnd/npc-registry","schema_id":"notarius.dnd.npc_registry","schema_version":"v1"}],"rejected_file":"rejected.json","warnings_file":"warnings.json","diagnostics_file":"diagnostics.json"}`, } { if err := os.WriteFile(filepath.Join(bundle, filepath.FromSlash(name)), []byte(value), 0o644); err != nil { os.Exit(2) } } _, _ = os.Stdout.WriteString(fmt.Sprintf(`{"schema_version":"notarius.run-result.v2","run_id":"fixture-run","pipeline_id":"dnd-session","output_directory":%q,"index_file":"index.json","normalized_output_count":1,"rejected_output_count":0,"warning_group_count":0,"warning_occurrence_count":0,"diagnostic_group_count":0,"diagnostic_occurrence_count":0,"diagnostics_truncated":false,"validation_status":"approved"}`, bundle)) default: os.Exit(2) } _, _ = os.Stderr.WriteString(sharedProviderKey + "\n") os.Exit(0) }