Unify inspection configuration handling
This commit is contained in:
@@ -6,23 +6,36 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit"
|
||||||
appconfig "gitea.maximumdirect.net/eric/scriptorium/internal/config"
|
appconfig "gitea.maximumdirect.net/eric/scriptorium/internal/config"
|
||||||
appformat "gitea.maximumdirect.net/eric/scriptorium/internal/format"
|
appformat "gitea.maximumdirect.net/eric/scriptorium/internal/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
type promptInspectionConfig struct {
|
type promptInspectionConfig struct {
|
||||||
configPath, promptDir, promptID, promptVersion, outputPath string
|
configPath, promptDir, promptID, promptVersion, outputPath string
|
||||||
|
configExplicit bool
|
||||||
outputFormat appformat.OutputFormat
|
outputFormat appformat.OutputFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
type profileInspectionConfig struct {
|
type profileInspectionConfig struct {
|
||||||
configPath, profileDir, profileID, outputPath string
|
configPath, profileDir, profileID, outputPath string
|
||||||
|
configExplicit bool
|
||||||
outputFormat appformat.OutputFormat
|
outputFormat appformat.OutputFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// emptyPromptDefinitionFS satisfies Promptkit's engine-level prompt-source
|
||||||
|
// requirement without exposing the caller's working directory. Profile
|
||||||
|
// inspection never reads this source.
|
||||||
|
type emptyPromptDefinitionFS struct{}
|
||||||
|
|
||||||
|
func (emptyPromptDefinitionFS) Open(name string) (fs.File, error) {
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||||
|
}
|
||||||
|
|
||||||
func inspectCommand(args []string, stdout, stderr io.Writer) int {
|
func inspectCommand(args []string, stdout, stderr io.Writer) int {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
fmt.Fprintln(stderr, "inspect parse error: inspection mode is required")
|
fmt.Fprintln(stderr, "inspect parse error: inspection mode is required")
|
||||||
@@ -78,7 +91,7 @@ func inspectProfileCommand(args []string, stdout, stderr io.Writer) int {
|
|||||||
fmt.Fprintf(stderr, "inspect error: %v\n", err)
|
fmt.Fprintf(stderr, "inspect error: %v\n", err)
|
||||||
return ExitRuntimeError
|
return ExitRuntimeError
|
||||||
}
|
}
|
||||||
engine, err := newEngine(settings)
|
engine, err := newEngine(settings, promptkit.WithPromptFS(emptyPromptDefinitionFS{}, "."))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "engine error: %v\n", err)
|
fmt.Fprintf(stderr, "engine error: %v\n", err)
|
||||||
return ExitRuntimeError
|
return ExitRuntimeError
|
||||||
@@ -113,6 +126,7 @@ func parseProfileInspectionArgs(args []string) (*profileInspectionConfig, error)
|
|||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cfg.configExplicit = flagWasSet(fs, "config")
|
||||||
if fs.NArg() > 0 {
|
if fs.NArg() > 0 {
|
||||||
return nil, fmt.Errorf("unexpected positional args: %v", fs.Args())
|
return nil, fmt.Errorf("unexpected positional args: %v", fs.Args())
|
||||||
}
|
}
|
||||||
@@ -131,14 +145,13 @@ func parseProfileInspectionArgs(args []string) (*profileInspectionConfig, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveAppSettingsForProfileInspection(cfg *profileInspectionConfig) (engineSettings, error) {
|
func resolveAppSettingsForProfileInspection(cfg *profileInspectionConfig) (engineSettings, error) {
|
||||||
settings, err := appconfig.LoadConfig(cfg.configPath, cfg.configPath != "")
|
settings, err := resolveAppSettingsWithConfigPresence(cfg.configPath, cfg.configExplicit, appconfig.CLIOverrides{
|
||||||
|
ProfileDir: cfg.profileDir,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return engineSettings{}, fmt.Errorf("application config error: %w", err)
|
return engineSettings{}, err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.profileDir) != "" {
|
return engineSettings{profileDir: settings.ProfileDir, backends: settings.Backends}, nil
|
||||||
settings.ProfileDir = filepath.Clean(cfg.profileDir)
|
|
||||||
}
|
|
||||||
return engineSettings{promptDir: ".", profileDir: settings.ProfileDir, backends: settings.Backends}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) {
|
func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) {
|
||||||
@@ -155,6 +168,7 @@ func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) {
|
|||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cfg.configExplicit = flagWasSet(fs, "config")
|
||||||
if fs.NArg() > 0 {
|
if fs.NArg() > 0 {
|
||||||
return nil, fmt.Errorf("unexpected positional args: %v", fs.Args())
|
return nil, fmt.Errorf("unexpected positional args: %v", fs.Args())
|
||||||
}
|
}
|
||||||
@@ -173,12 +187,11 @@ func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveAppSettingsForPromptInspection(cfg *promptInspectionConfig) (engineSettings, error) {
|
func resolveAppSettingsForPromptInspection(cfg *promptInspectionConfig) (engineSettings, error) {
|
||||||
settings, err := appconfig.LoadConfig(cfg.configPath, cfg.configPath != "")
|
settings, err := resolveAppSettingsWithConfigPresence(cfg.configPath, cfg.configExplicit, appconfig.CLIOverrides{
|
||||||
|
PromptDir: cfg.promptDir,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return engineSettings{}, fmt.Errorf("application config error: %w", err)
|
return engineSettings{}, err
|
||||||
}
|
|
||||||
if strings.TrimSpace(cfg.promptDir) != "" {
|
|
||||||
settings.PromptDir = filepath.Clean(cfg.promptDir)
|
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(settings.PromptDir) == "" {
|
if strings.TrimSpace(settings.PromptDir) == "" {
|
||||||
return engineSettings{}, errors.New(errPromptDirRequired)
|
return engineSettings{}, errors.New(errPromptDirRequired)
|
||||||
|
|||||||
@@ -508,7 +508,11 @@ func registerConfigPathFlag(fs *flag.FlagSet, target *string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveAppSettings(fs *flag.FlagSet, configPath string, overrides appconfig.CLIOverrides) (appconfig.AppSettings, error) {
|
func resolveAppSettings(fs *flag.FlagSet, configPath string, overrides appconfig.CLIOverrides) (appconfig.AppSettings, error) {
|
||||||
settings, err := appconfig.LoadConfig(configPath, flagWasSet(fs, "config"))
|
return resolveAppSettingsWithConfigPresence(configPath, flagWasSet(fs, "config"), overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveAppSettingsWithConfigPresence(configPath string, configExplicit bool, overrides appconfig.CLIOverrides) (appconfig.AppSettings, error) {
|
||||||
|
settings, err := appconfig.LoadConfig(configPath, configExplicit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return appconfig.AppSettings{}, fmt.Errorf("application config error: %w", err)
|
return appconfig.AppSettings{}, fmt.Errorf("application config error: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1183,6 +1183,24 @@ output:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInspectionParsersPreserveExplicitEmptyConfigPath(t *testing.T) {
|
||||||
|
promptConfig, err := parsePromptInspectionArgs([]string{"--config=", "--prompt", "fixture"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse prompt inspection: %v", err)
|
||||||
|
}
|
||||||
|
if promptConfig.configPath != "" || !promptConfig.configExplicit {
|
||||||
|
t.Fatalf("expected explicit empty prompt config path, got %+v", promptConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
profileConfig, err := parseProfileInspectionArgs([]string{"--config=", "--profile", "fixture"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse profile inspection: %v", err)
|
||||||
|
}
|
||||||
|
if profileConfig.configPath != "" || !profileConfig.configExplicit {
|
||||||
|
t.Fatalf("expected explicit empty profile config path, got %+v", profileConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPromptkitV09DefinitionsRenderThroughCLI(t *testing.T) {
|
func TestPromptkitV09DefinitionsRenderThroughCLI(t *testing.T) {
|
||||||
fixtureRoot := promptkitV09FixtureRoot(t)
|
fixtureRoot := promptkitV09FixtureRoot(t)
|
||||||
configPath := writePromptkitV09Config(t, fixtureRoot, true)
|
configPath := writePromptkitV09Config(t, fixtureRoot, true)
|
||||||
@@ -1283,27 +1301,22 @@ func TestPromptkitV09ProfileInspectionResolvesSupportedTargets(t *testing.T) {
|
|||||||
t.Setenv("FIXTURE_PROFILE_API_KEY", secret)
|
t.Setenv("FIXTURE_PROFILE_API_KEY", secret)
|
||||||
|
|
||||||
fixtureRoot := promptkitV09FixtureRoot(t)
|
fixtureRoot := promptkitV09FixtureRoot(t)
|
||||||
configPath := writePromptkitV09Config(t, fixtureRoot, false)
|
configPath := writePromptkitV09Config(t, fixtureRoot, true)
|
||||||
profileDir := filepath.Join(fixtureRoot, "profiles")
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
profileID string
|
profileID string
|
||||||
profileDir string
|
|
||||||
wantBackend string
|
wantBackend string
|
||||||
wantModel string
|
wantModel string
|
||||||
wantAPIKeyEnv string
|
wantAPIKeyEnv string
|
||||||
}{
|
}{
|
||||||
{name: "inherited custom backend", profileID: "custom-derived", profileDir: profileDir, wantBackend: "fixture-custom", wantModel: "fixture-derived-model", wantAPIKeyEnv: "FIXTURE_PROFILE_API_KEY"},
|
{name: "inherited custom backend", profileID: "custom-derived", wantBackend: "fixture-custom", wantModel: "fixture-derived-model", wantAPIKeyEnv: "FIXTURE_PROFILE_API_KEY"},
|
||||||
{name: "endpoint only", profileID: "endpoint-only", profileDir: profileDir, wantModel: "fixture-endpoint-model"},
|
{name: "endpoint only", profileID: "endpoint-only", wantModel: "fixture-endpoint-model"},
|
||||||
{name: "built in", profileID: "deepseek-4-flash", wantBackend: "openrouter", wantModel: "deepseek/deepseek-v4-flash", wantAPIKeyEnv: "OPENROUTER_API_KEY"},
|
{name: "built in", profileID: "deepseek-4-flash", wantBackend: "openrouter", wantModel: "deepseek/deepseek-v4-flash", wantAPIKeyEnv: "OPENROUTER_API_KEY"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
args := []string{"profile", "--config", configPath, "--profile", tc.profileID, "--format", "json"}
|
args := []string{"profile", "--config", configPath, "--profile", tc.profileID, "--format", "json"}
|
||||||
if tc.profileDir != "" {
|
|
||||||
args = append(args, "--profile-dir", tc.profileDir)
|
|
||||||
}
|
|
||||||
code, stdout, stderr := runCLICommand(t, inspectCommand, args)
|
code, stdout, stderr := runCLICommand(t, inspectCommand, args)
|
||||||
if code != ExitOK {
|
if code != ExitOK {
|
||||||
t.Fatalf("expected ExitOK, got %d stderr=%q", code, stderr)
|
t.Fatalf("expected ExitOK, got %d stderr=%q", code, stderr)
|
||||||
@@ -1326,11 +1339,22 @@ func TestPromptkitV09ProfileInspectionResolvesSupportedTargets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configWithoutPromptDir := writePromptkitV09Config(t, fixtureRoot, false)
|
||||||
|
code, _, stderr := runCLICommand(t, inspectCommand, []string{
|
||||||
|
"profile",
|
||||||
|
"--config", configWithoutPromptDir,
|
||||||
|
"--profile-dir", filepath.Join(fixtureRoot, "profiles"),
|
||||||
|
"--profile", "custom-derived",
|
||||||
|
})
|
||||||
|
if code != ExitOK {
|
||||||
|
t.Fatalf("profile inspection unexpectedly required a prompt directory: %q", stderr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProfileInspectionHonorsDirectoryPrecedenceOutputAndFailures(t *testing.T) {
|
func TestProfileInspectionHonorsDirectoryPrecedenceOutputAndFailures(t *testing.T) {
|
||||||
fixtureRoot := promptkitV09FixtureRoot(t)
|
fixtureRoot := promptkitV09FixtureRoot(t)
|
||||||
configPath := writePromptkitV09Config(t, fixtureRoot, false)
|
configPath := writePromptkitV09Config(t, fixtureRoot, true)
|
||||||
overrideDir := t.TempDir()
|
overrideDir := t.TempDir()
|
||||||
writeProfileFile(t, overrideDir, "custom-derived", "http://127.0.0.1:9000/v1", "override-model")
|
writeProfileFile(t, overrideDir, "custom-derived", "http://127.0.0.1:9000/v1", "override-model")
|
||||||
outPath := filepath.Join(t.TempDir(), "inspection.json")
|
outPath := filepath.Join(t.TempDir(), "inspection.json")
|
||||||
|
|||||||
Reference in New Issue
Block a user