package cli import ( "bytes" "encoding/json" "errors" "io" "os" "path/filepath" "strings" "testing" appconfig "gitea.maximumdirect.net/eric/scriptorium/internal/config" ) func TestConfigCommandsInspectApplicationWithoutEngine(t *testing.T) { path := writeAppConfigFile(t, "{}\n") for _, mode := range []string{"validate", "show", "sources"} { code, stdout, stderr := runCLICommand(t, configCommand, []string{mode, "--config", path}) if code != ExitOK || stderr != "" { t.Fatalf("%s failed: code=%d stdout=%q stderr=%q", mode, code, stdout, stderr) } switch mode { case "validate": if stdout != "" { t.Fatalf("validation wrote stdout: %q", stdout) } case "show": if !strings.Contains(stdout, "render_format: text") || !strings.Contains(stdout, "max_request_bytes: 16777216") { t.Fatalf("effective defaults missing: %q", stdout) } case "sources": var sources []configurationSource if err := json.Unmarshal([]byte(stdout), &sources); err != nil || len(sources) != 1 || sources[0].Layer != "base" { t.Fatalf("unexpected source records: %q (%v)", stdout, err) } } } } func TestConfigCommandsExposeComposedSourcesAndResolvedValues(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, "overlays"), 0o755); err != nil { t.Fatal(err) } base := filepath.Join(dir, "config.yml") for path, body := range map[string]string{ base: "configkit:\n imports:\n - path: fragment.yml\nprompt_dir: ' ./before/../prompts '\nprofile_dir: ./profiles\n", filepath.Join(dir, "fragment.yml"): "server:\n addr: :8081\n", filepath.Join(dir, "overlays", "local.yml"): "server:\n addr: :9090\n", } { if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } } args := []string{"--config", base, "--config-overlay", "local", "--profile-dir=", "--addr", ":7070"} code, stdout, stderr := runCLICommand(t, configCommand, append([]string{"show"}, args...)) if code != ExitOK || stderr != "" || !strings.Contains(stdout, "prompt_dir: prompts") || !strings.Contains(stdout, "profile_dir: ''") || !strings.Contains(stdout, "addr: :7070") { t.Fatalf("resolved effective output changed: code=%d out=%q err=%q", code, stdout, stderr) } code, stdout, stderr = runCLICommand(t, configCommand, append([]string{"sources"}, args...)) if code != ExitOK || stderr != "" || !strings.HasSuffix(stdout, "\n") { t.Fatalf("source command failed: code=%d out=%q err=%q", code, stdout, stderr) } var sources []configurationSource if err := json.Unmarshal([]byte(stdout), &sources); err != nil || len(sources) != 5 { t.Fatalf("unexpected composed source records: %+v (%v)", sources, err) } for i, layer := range []string{"base", "base", "overlay", "cli", "cli"} { if string(sources[i].Layer) != layer { t.Fatalf("source %d layer = %q, want %q", i, sources[i].Layer, layer) } } } func TestConfigInspectionKeepsReferencesAndOrdinaryParameters(t *testing.T) { const name = "SCRIPTORIUM_CONFIG_TEST_API_KEY" const secret = "synthetic-secret-value-should-not-appear" path := writeAppConfigFile(t, `backends: synthetic: endpoint: http://127.0.0.1:1/v1 api_key_env: SCRIPTORIUM_CONFIG_TEST_API_KEY extra_params: visible_parameter: synthetic-ordinary-value `) t.Setenv(name, secret) for _, present := range []bool{true, false} { if !present { if err := os.Unsetenv(name); err != nil { t.Fatal(err) } } code, stdout, stderr := runCLICommand(t, configCommand, []string{"show", "--config", path}) if code != ExitOK || stderr != "" || !strings.Contains(stdout, name) || !strings.Contains(stdout, "synthetic-ordinary-value") || strings.Contains(stdout, secret) { t.Fatalf("credential boundary changed with present=%t: code=%d out=%q err=%q", present, code, stdout, stderr) } } result, err := appconfig.Load(appconfig.LoadOptions{Path: path, Explicit: true}) if err != nil { t.Fatal(err) } settings := appconfig.Settings(result.Config) if len(settings.Backends) != 1 || settings.Backends[0].ExtraParams["visible_parameter"] != "synthetic-ordinary-value" { t.Fatalf("runtime backend parameters changed: %+v", settings.Backends) } } type errorOutputWriter struct{} func (errorOutputWriter) Write([]byte) (int, error) { return 0, errors.New("output unavailable") } func TestConfigCommandFailuresDoNotProduceSuccessfulOutput(t *testing.T) { path := writeAppConfigFile(t, "server:\n max_request_bytes: -1\n") for _, mode := range []string{"validate", "show", "sources"} { code, stdout, stderr := runCLICommand(t, configCommand, []string{mode, "--config", path}) if code != ExitRuntimeError || stdout != "" || !strings.Contains(stderr, "validate.error") { t.Fatalf("%s misclassified invalid config: code=%d out=%q err=%q", mode, code, stdout, stderr) } } path = writeAppConfigFile(t, "{}\n") var stderr bytes.Buffer if code := configCommand([]string{"show", "--config", path}, errorOutputWriter{}, &stderr); code != ExitRuntimeError || !strings.Contains(stderr.String(), "output error") { t.Fatalf("writer error not reported: code=%d stderr=%q", code, stderr.String()) } if code := configCommand([]string{"sources", "--config", path}, errorOutputWriter{}, io.Discard); code != ExitRuntimeError { t.Fatalf("source writer error returned %d", code) } } func TestConfigExplainUsesResolvedHistoryAndKeepsParametersVisible(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, "overlays"), 0o755); err != nil { t.Fatal(err) } base := filepath.Join(dir, "config.yml") for path, body := range map[string]string{ base: `prompt_dir: " ./prompts/../prompts " server: addr: :8080 backends: "team/blue~1": endpoint: http://127.0.0.1:1/v1 api_key_env: SCRIPTORIUM_EXPLAIN_KEY extra_params: "0": base-parameter `, filepath.Join(dir, "overlays", "local.yml"): `server: addr: :9090 backends: "team/blue~1": extra_params: "0": overlay-parameter `, } { if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } } const keyValue = "synthetic-key-bytes-must-not-appear" t.Setenv("SCRIPTORIUM_EXPLAIN_KEY", keyValue) args := []string{"--config", base, "--config-overlay", "local", "--addr", ":9090"} for _, tc := range []struct { pointer string want []string }{ {pointer: "/server/addr", want: []string{":8080", ":9090", "--addr"}}, {pointer: "/prompt_dir", want: []string{"normalized application path", "prompts"}}, {pointer: "/backends/team~1blue~01/extra_params/0", want: []string{"base-parameter", "overlay-parameter"}}, {pointer: "/backends/team~1blue~01/api_key_env", want: []string{"SCRIPTORIUM_EXPLAIN_KEY"}}, {pointer: "", want: []string{"base-parameter", "overlay-parameter", "SCRIPTORIUM_EXPLAIN_KEY"}}, } { code, stdout, stderr := runCLICommand(t, configCommand, append([]string{"explain", "--path", tc.pointer}, args...)) if code != ExitOK || stderr != "" || strings.Contains(stdout, keyValue) { t.Fatalf("explain %q failed or disclosed key: code=%d out=%q err=%q", tc.pointer, code, stdout, stderr) } for _, want := range tc.want { if !strings.Contains(stdout, want) { t.Fatalf("explain %q lacks %q: %q", tc.pointer, want, stdout) } } } if err := os.Unsetenv("SCRIPTORIUM_EXPLAIN_KEY"); err != nil { t.Fatal(err) } code, stdout, stderr := runCLICommand(t, configCommand, append([]string{"explain", "--path", "/backends/team~1blue~01/api_key_env"}, args...)) if code != ExitOK || stderr != "" || !strings.Contains(stdout, "SCRIPTORIUM_EXPLAIN_KEY") || strings.Contains(stdout, keyValue) { t.Fatalf("absent credential changed reference explanation: code=%d out=%q err=%q", code, stdout, stderr) } } func TestConfigExplainRejectsUnknownOrMalformedPaths(t *testing.T) { path := writeAppConfigFile(t, "{}\n") for _, args := range [][]string{ {"explain", "--config", path}, {"explain", "--config", path, "--path", "not-a-pointer"}, {"explain", "--config", path, "--path", "/missing"}, } { code, stdout, stderr := runCLICommand(t, configCommand, args) if code != ExitRuntimeError || stdout != "" || stderr == "" { t.Fatalf("invalid explanation path accepted: args=%q code=%d out=%q err=%q", args, code, stdout, stderr) } } var stderr bytes.Buffer if code := configCommand([]string{"explain", "--config", path, "--path", ""}, errorOutputWriter{}, &stderr); code != ExitRuntimeError || !strings.Contains(stderr.String(), "output error") { t.Fatalf("explanation writer failure not reported: code=%d err=%q", code, stderr.String()) } }