Add configuration path explanations

This commit is contained in:
2026-09-26 18:17:57 +00:00
parent 3d7c9aa324
commit 41304e4eea
7 changed files with 131 additions and 6 deletions

View File

@@ -23,16 +23,19 @@ func configCommand(args []string, stdout, stderr io.Writer) int {
return ExitRuntimeError
}
mode := args[0]
if mode != "validate" && mode != "show" && mode != "sources" {
if mode != "validate" && mode != "show" && mode != "sources" && mode != "explain" {
fmt.Fprintln(stderr, "config parse error: unknown configuration mode")
return ExitRuntimeError
}
fs := flag.NewFlagSet("config "+mode, flag.ContinueOnError)
fs.SetOutput(io.Discard)
var path, overlay string
var path, overlay, pointer string
registerConfigSelectionFlags(fs, &path, &overlay)
registerConfigurationOverrides(fs)
if mode == "explain" {
fs.StringVar(&pointer, "path", "", "configuration path as a JSON Pointer")
}
if err := fs.Parse(args[1:]); err != nil {
writeApplicationError(stderr, "config parse", err)
return ExitRuntimeError
@@ -41,6 +44,19 @@ func configCommand(args []string, stdout, stderr io.Writer) int {
fmt.Fprintln(stderr, "config parse error: positional arguments are not accepted")
return ExitRuntimeError
}
var explanationPath configkit.Path
if mode == "explain" {
if !flagWasSet(fs, "path") {
fmt.Fprintln(stderr, "config parse error: --path is required")
return ExitRuntimeError
}
var err error
explanationPath, err = configkit.ParsePath(pointer)
if err != nil {
fmt.Fprintln(stderr, "config parse error: --path must be a JSON Pointer")
return ExitRuntimeError
}
}
result, err := appconfig.Load(appconfig.LoadOptions{
Path: path, Explicit: flagWasSet(fs, "config"), Overlay: overlay,
@@ -66,6 +82,8 @@ func configCommand(args []string, stdout, stderr io.Writer) int {
})
}
err = json.NewEncoder(stdout).Encode(sources)
case "explain":
err = result.WriteExplanation(stdout, explanationPath)
}
if err != nil {
fmt.Fprintf(stderr, "config output error: %v\n", err)

View File

@@ -126,3 +126,82 @@ func TestConfigCommandFailuresDoNotProduceSuccessfulOutput(t *testing.T) {
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())
}
}

View File

@@ -760,5 +760,5 @@ func printUsage(w io.Writer) {
fmt.Fprintf(w, " serve: scriptorium serve [--config PATH] [--config-overlay NAME] [--addr %s] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR] [--artifact-root DIR] [--max-request-bytes N] [--max-artifact-bytes N] [--max-response-bytes N]\n", defaults.HTTPAddrDefault)
fmt.Fprintln(w, " inspect prompt: scriptorium inspect prompt --prompt ID [--prompt-version VERSION] [--config PATH] [--config-overlay NAME] [--prompt-dir DIR] [--format text|json] [--out path]")
fmt.Fprintln(w, " inspect profile: scriptorium inspect profile --profile ID [--config PATH] [--config-overlay NAME] [--profile-dir DIR] [--format text|json] [--out path]")
fmt.Fprintln(w, " config: scriptorium config <validate|show|sources> [--config PATH] [--config-overlay NAME] [application overrides]")
fmt.Fprintln(w, " config: scriptorium config <validate|show|sources|explain> [--config PATH] [--config-overlay NAME] [application overrides] [--path /pointer for explain]")
}