Add versioned Audita config support
This commit is contained in:
@@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -50,6 +51,8 @@ type processInvocation struct {
|
||||
OutputPath string
|
||||
ReportJSONPath string
|
||||
Config config.Config
|
||||
ConfigPath string
|
||||
ConfigSource string
|
||||
ExplicitModules bool
|
||||
}
|
||||
|
||||
@@ -79,6 +82,8 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
|
||||
GlossaryPath: inv.GlossaryPath,
|
||||
OutputPath: inv.OutputPath,
|
||||
ReportJSONPath: inv.ReportJSONPath,
|
||||
ConfigPath: inv.ConfigPath,
|
||||
ConfigSource: inv.ConfigSource,
|
||||
TranscriptDescription: inv.Config.TranscriptDescription,
|
||||
Modules: append([]string(nil), inv.Config.Modules...),
|
||||
}); err != nil {
|
||||
@@ -340,6 +345,9 @@ func Run(args []string, stdout, stderr io.Writer) int {
|
||||
if args[0] == "process" {
|
||||
return runProcess(args[1:], stdout, stderr)
|
||||
}
|
||||
if args[0] == "config" {
|
||||
return runConfig(args[1:], stdout, stderr)
|
||||
}
|
||||
|
||||
fmt.Fprintf(stderr, "audita: unknown command %q\n\n", args[0])
|
||||
writeRootUsage(stderr)
|
||||
@@ -349,8 +357,30 @@ func Run(args []string, stdout, stderr io.Writer) int {
|
||||
func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
startedAt := time.Now().UTC()
|
||||
|
||||
cfg, err := config.LoadFromEnv()
|
||||
configPathOverride, configPathOverrideSet, err := findConfigPathOverride(args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: invalid CLI configuration: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
configPath, configSource, err := resolveConfigPath(configPathOverride, configPathOverrideSet, os.LookupEnv)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
|
||||
cfg := config.Default()
|
||||
if configPath != "" {
|
||||
fileCfg, fileErr := config.LoadFileConfig(configPath)
|
||||
if fileErr != nil {
|
||||
fmt.Fprintf(stderr, "audita process: invalid config file: %v\n", fileErr)
|
||||
return 2
|
||||
}
|
||||
if applyErr := cfg.ApplyFileConfig(fileCfg); applyErr != nil {
|
||||
fmt.Fprintf(stderr, "audita process: invalid config file: %v\n", applyErr)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
if err := cfg.ApplyEnvOverrides(); err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: invalid environment configuration: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
@@ -472,6 +502,8 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
OutputPath: *pFlags.outputPath,
|
||||
ReportJSONPath: *pFlags.reportJSONPath,
|
||||
Config: cfg,
|
||||
ConfigPath: configPath,
|
||||
ConfigSource: configSource,
|
||||
ExplicitModules: explicitModules,
|
||||
}
|
||||
|
||||
@@ -541,6 +573,114 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func runConfig(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 || isHelpCommand(args) || hasHelpFlag(args) {
|
||||
writeConfigUsage(stdout)
|
||||
return 0
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "validate":
|
||||
return runConfigValidate(args[1:], stdout, stderr)
|
||||
case "print-effective":
|
||||
return runConfigPrintEffective(args[1:], stdout, stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "audita config: unknown command %q\n\n", args[0])
|
||||
writeConfigUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
func runConfigValidate(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("config validate", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
configPath := fs.String("config", "", "Path to versioned YAML config file")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
writeConfigValidateUsage(stdout)
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if strings.TrimSpace(*configPath) == "" {
|
||||
fmt.Fprintln(stderr, "audita config validate: --config is required")
|
||||
return 2
|
||||
}
|
||||
if len(fs.Args()) != 0 {
|
||||
fmt.Fprintln(stderr, "audita config validate: unexpected positional arguments")
|
||||
return 2
|
||||
}
|
||||
|
||||
fileCfg, err := config.LoadFileConfig(strings.TrimSpace(*configPath))
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita config validate: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
cfg := config.Default()
|
||||
if err := cfg.ApplyFileConfig(fileCfg); err != nil {
|
||||
fmt.Fprintf(stderr, "audita config validate: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
fmt.Fprintln(stdout, "config is valid")
|
||||
return 0
|
||||
}
|
||||
|
||||
func runConfigPrintEffective(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("config print-effective", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
configPath := fs.String("config", "", "Path to versioned YAML config file")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
writeConfigPrintEffectiveUsage(stdout)
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if len(fs.Args()) != 0 {
|
||||
fmt.Fprintln(stderr, "audita config print-effective: unexpected positional arguments")
|
||||
return 2
|
||||
}
|
||||
|
||||
configPathValue := strings.TrimSpace(*configPath)
|
||||
configPathSet := configPathValue != ""
|
||||
path, _, err := resolveConfigPath(configPathValue, configPathSet, os.LookupEnv)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
|
||||
cfg := config.Default()
|
||||
if path != "" {
|
||||
fileCfg, fileErr := config.LoadFileConfig(path)
|
||||
if fileErr != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %v\n", fileErr)
|
||||
return 2
|
||||
}
|
||||
if applyErr := cfg.ApplyFileConfig(fileCfg); applyErr != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %v\n", applyErr)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
if err := cfg.ApplyEnvOverrides(); err != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
|
||||
redacted := cfg.Redacted()
|
||||
out, err := json.MarshalIndent(redacted, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
out = append(out, '\n')
|
||||
if _, err := stdout.Write(out); err != nil {
|
||||
fmt.Fprintf(stderr, "audita config print-effective: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func extractErrorPhase(err error) (phase string, message string) {
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, ": ") {
|
||||
@@ -683,6 +823,7 @@ func mapValidatorRejected(in []runner.ValidatorRejectedChange) []reporting.Valid
|
||||
}
|
||||
|
||||
type processFlags struct {
|
||||
configPath *string
|
||||
glossaryPath *string
|
||||
outputPath *string
|
||||
reportJSONPath *string
|
||||
@@ -743,6 +884,7 @@ func newProcessFlagSet(cfg config.Config, stderr io.Writer) (*flag.FlagSet, proc
|
||||
}
|
||||
|
||||
pFlags := processFlags{
|
||||
configPath: fs.String("config", "", "Path to versioned YAML config file"),
|
||||
glossaryPath: fs.String("glossary", "", "Path to glossary YAML file"),
|
||||
outputPath: fs.String("output", "", "Path to corrected transcript JSON output file"),
|
||||
reportJSONPath: fs.String("report-json", "", "Path to machine-readable report JSON output file"),
|
||||
@@ -781,6 +923,63 @@ func newProcessFlagSet(cfg config.Config, stderr io.Writer) (*flag.FlagSet, proc
|
||||
return fs, pFlags
|
||||
}
|
||||
|
||||
func findConfigPathOverride(args []string) (path string, set bool, err error) {
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := strings.TrimSpace(args[i])
|
||||
if arg == "" {
|
||||
continue
|
||||
}
|
||||
if arg == "--config" {
|
||||
if i+1 >= len(args) {
|
||||
return "", false, fmt.Errorf("--config requires a path")
|
||||
}
|
||||
return strings.TrimSpace(args[i+1]), true, nil
|
||||
}
|
||||
if strings.HasPrefix(arg, "--config=") {
|
||||
return strings.TrimSpace(strings.TrimPrefix(arg, "--config=")), true, nil
|
||||
}
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func resolveConfigPath(cliConfigPath string, cliConfigPathSet bool, lookup func(string) (string, bool)) (path string, source string, err error) {
|
||||
if cliConfigPathSet {
|
||||
path = strings.TrimSpace(cliConfigPath)
|
||||
if path == "" {
|
||||
return "", "", fmt.Errorf("--config requires a non-empty path")
|
||||
}
|
||||
if _, statErr := os.Stat(path); statErr != nil {
|
||||
if os.IsNotExist(statErr) {
|
||||
return "", "", fmt.Errorf("config file not found: %s", path)
|
||||
}
|
||||
return "", "", fmt.Errorf("cannot access config file %s: %w", path, statErr)
|
||||
}
|
||||
return path, "flag", nil
|
||||
}
|
||||
|
||||
if raw, ok := lookup("AUDITA_CONFIG"); ok {
|
||||
path = strings.TrimSpace(raw)
|
||||
if path == "" {
|
||||
return "", "", fmt.Errorf("AUDITA_CONFIG must not be empty")
|
||||
}
|
||||
if _, statErr := os.Stat(path); statErr != nil {
|
||||
if os.IsNotExist(statErr) {
|
||||
return "", "", fmt.Errorf("config file not found: %s", path)
|
||||
}
|
||||
return "", "", fmt.Errorf("cannot access config file %s: %w", path, statErr)
|
||||
}
|
||||
return path, "env", nil
|
||||
}
|
||||
|
||||
defaultPath := config.DefaultConfigPath
|
||||
if _, statErr := os.Stat(defaultPath); statErr == nil {
|
||||
return defaultPath, "default", nil
|
||||
} else if !os.IsNotExist(statErr) {
|
||||
return "", "", fmt.Errorf("cannot access config file %s: %w", defaultPath, statErr)
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
func isHelpCommand(args []string) bool {
|
||||
if len(args) == 0 {
|
||||
return false
|
||||
@@ -817,11 +1016,37 @@ func writeRootUsage(w io.Writer) {
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Commands:")
|
||||
fmt.Fprintln(w, " process Process a transcript JSON file")
|
||||
fmt.Fprintln(w, " config Validate and inspect config")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Example:")
|
||||
fmt.Fprintln(w, " audita process transcript.json --glossary glossary.yaml --output corrected.json")
|
||||
}
|
||||
|
||||
func writeConfigUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "Validate and inspect Audita config.")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Usage:")
|
||||
fmt.Fprintln(w, " audita config <command> [flags]")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Commands:")
|
||||
fmt.Fprintln(w, " validate Validate a versioned YAML config file")
|
||||
fmt.Fprintln(w, " print-effective Print redacted effective config JSON (defaults + config file + env)")
|
||||
}
|
||||
|
||||
func writeConfigValidateUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "Validate a versioned YAML config file.")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Usage:")
|
||||
fmt.Fprintln(w, " audita config validate --config <path>")
|
||||
}
|
||||
|
||||
func writeConfigPrintEffectiveUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "Print redacted effective config JSON.")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "Usage:")
|
||||
fmt.Fprintln(w, " audita config print-effective [--config <path>]")
|
||||
}
|
||||
|
||||
func writeProcessUsage(w io.Writer, fs *flag.FlagSet) {
|
||||
fmt.Fprintln(w, "Process a transcript JSON file.")
|
||||
fmt.Fprintln(w)
|
||||
|
||||
@@ -54,6 +54,7 @@ func TestRunProcessHelpListsExpectedFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, expectedFlag := range []string{
|
||||
"--config",
|
||||
"--glossary",
|
||||
"--output",
|
||||
"--report-json",
|
||||
@@ -97,6 +98,394 @@ func TestRunProcessHelpListsExpectedFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveConfigPathDefaultIgnoredWhenMissing(t *testing.T) {
|
||||
lookup := func(string) (string, bool) { return "", false }
|
||||
path, source, err := resolveConfigPath("", false, lookup)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if path != "" || source != "" {
|
||||
t.Fatalf("expected no config path/source, got path=%q source=%q", path, source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigValidateSuccess(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cfgPath := writeFile(t, "config.yml", "version: 1\n")
|
||||
|
||||
exitCode := Run([]string{"config", "validate", "--config", cfgPath}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "config is valid") {
|
||||
t.Fatalf("expected success message, got %q", stdout.String())
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
t.Fatalf("expected empty stderr on success, got %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigValidateInvalidVersion(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cfgPath := writeFile(t, "config.yml", "version: 999\n")
|
||||
|
||||
exitCode := Run([]string{"config", "validate", "--config", cfgPath}, &stdout, &stderr)
|
||||
if exitCode == 0 {
|
||||
t.Fatalf("expected failure for unsupported config version")
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "unsupported config version") {
|
||||
t.Fatalf("expected version error, got %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigValidateUnknownField(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cfgPath := writeFile(t, "config.yml", "version: 1\nunknown_field: true\n")
|
||||
|
||||
exitCode := Run([]string{"config", "validate", "--config", cfgPath}, &stdout, &stderr)
|
||||
if exitCode == 0 {
|
||||
t.Fatalf("expected failure for unknown field")
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "field unknown_field not found") {
|
||||
t.Fatalf("expected unknown-field error, got %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrintEffectiveOutputsRedactedJSON(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
secret := "super-secret-api-key"
|
||||
t.Setenv("AUDITA_LLM_API_KEY", secret)
|
||||
cfgPath := writeFile(t, "config.yml", "version: 1\n")
|
||||
|
||||
exitCode := Run([]string{"config", "print-effective", "--config", cfgPath}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
t.Fatalf("expected empty stderr on success, got %q", stderr.String())
|
||||
}
|
||||
if strings.Contains(stdout.String(), secret) {
|
||||
t.Fatalf("print-effective leaked secret")
|
||||
}
|
||||
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
|
||||
t.Fatalf("expected valid JSON output, got error: %v output=%q", err, stdout.String())
|
||||
}
|
||||
primary, ok := out["PrimaryLLM"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected PrimaryLLM object, got %#v", out["PrimaryLLM"])
|
||||
}
|
||||
if got := primary["APIKey"]; got != "[REDACTED]" {
|
||||
t.Fatalf("expected redacted API key, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigCommandDoesNotRequireTranscriptOrGlossary(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cfgPath := writeFile(t, "config.yml", "version: 1\n")
|
||||
|
||||
exitCode := Run([]string{"config", "validate", "--config", cfgPath}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success without transcript/glossary args, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessConfigFlagMissingFileFails(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
exitCode := Run([]string{
|
||||
"process",
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary",
|
||||
fixturePath("tiny_glossary.yaml"),
|
||||
"--config",
|
||||
filepath.Join(t.TempDir(), "missing.yaml"),
|
||||
}, &stdout, &stderr)
|
||||
if exitCode == 0 {
|
||||
t.Fatalf("expected failure for missing --config file")
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "config file not found") {
|
||||
t.Fatalf("expected missing config file error, got %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessAUDITAConfigMissingFileFails(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
t.Setenv("AUDITA_CONFIG", filepath.Join(t.TempDir(), "missing-env.yaml"))
|
||||
|
||||
exitCode := Run([]string{
|
||||
"process",
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary",
|
||||
fixturePath("tiny_glossary.yaml"),
|
||||
}, &stdout, &stderr)
|
||||
if exitCode == 0 {
|
||||
t.Fatalf("expected failure for missing AUDITA_CONFIG file")
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "config file not found") {
|
||||
t.Fatalf("expected missing config file error, got %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessConfigFileAffectsRuntimeAndInvocationMetadata(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
workDir := t.TempDir()
|
||||
reportPath := filepath.Join(t.TempDir(), "report.json")
|
||||
outputPath := filepath.Join(t.TempDir(), "out.json")
|
||||
cfgPath := writeFile(t, "config.yml", `
|
||||
version: 1
|
||||
pipeline:
|
||||
modules: [grammar]
|
||||
context:
|
||||
description: "config file transcript context"
|
||||
diagnostics:
|
||||
work_dir: `+workDir+`
|
||||
retention: always
|
||||
`)
|
||||
|
||||
exitCode := Run([]string{
|
||||
"process",
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary",
|
||||
fixturePath("tiny_glossary.yaml"),
|
||||
"--config",
|
||||
cfgPath,
|
||||
"--output",
|
||||
outputPath,
|
||||
"--report-json",
|
||||
reportPath,
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout with --output, got %q", stdout.String())
|
||||
}
|
||||
|
||||
runPath := onlyRunDir(t, workDir)
|
||||
configBytes := readFile(t, filepath.Join(runPath, "effective-config.json"))
|
||||
var effectiveConfig struct {
|
||||
Modules []string `json:"Modules"`
|
||||
TranscriptDescription string `json:"TranscriptDescription"`
|
||||
}
|
||||
if err := json.Unmarshal(configBytes, &effectiveConfig); err != nil {
|
||||
t.Fatalf("failed to parse effective config metadata: %v", err)
|
||||
}
|
||||
if strings.Join(effectiveConfig.Modules, ",") != "grammar" {
|
||||
t.Fatalf("expected grammar module from config file, got %#v", effectiveConfig.Modules)
|
||||
}
|
||||
if effectiveConfig.TranscriptDescription != "config file transcript context" {
|
||||
t.Fatalf("unexpected transcript description from config file: %q", effectiveConfig.TranscriptDescription)
|
||||
}
|
||||
|
||||
invocationBytes := readFile(t, filepath.Join(runPath, "invocation.json"))
|
||||
var invocation struct {
|
||||
ConfigPath string `json:"config_path"`
|
||||
ConfigSource string `json:"config_source"`
|
||||
}
|
||||
if err := json.Unmarshal(invocationBytes, &invocation); err != nil {
|
||||
t.Fatalf("failed to parse invocation metadata: %v", err)
|
||||
}
|
||||
if invocation.ConfigPath != cfgPath {
|
||||
t.Fatalf("unexpected invocation config_path: %q", invocation.ConfigPath)
|
||||
}
|
||||
if invocation.ConfigSource != "flag" {
|
||||
t.Fatalf("unexpected invocation config_source: %q", invocation.ConfigSource)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessEnvOverridesConfigFile(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
if req.Config == nil {
|
||||
t.Fatal("expected config in validation request")
|
||||
}
|
||||
if req.Config.PrimaryLLM.Model != "env-model" {
|
||||
t.Fatalf("expected env model override, got %q", req.Config.PrimaryLLM.Model)
|
||||
}
|
||||
return validators.Result{ValidatorName: "capture-config", Decisions: nil}, nil
|
||||
}},
|
||||
},
|
||||
proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) { return nil, nil },
|
||||
},
|
||||
}}
|
||||
t.Cleanup(func() { processModuleFactory = nil })
|
||||
|
||||
t.Setenv("AUDITA_MODEL", "env-model")
|
||||
cfgPath := writeFile(t, "config.yml", `
|
||||
version: 1
|
||||
pipeline:
|
||||
modules: [m]
|
||||
llm:
|
||||
proposal:
|
||||
model: file-model
|
||||
`)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := Run([]string{
|
||||
"process",
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--config", cfgPath,
|
||||
"--modules", "m",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessCLIOverridesEnvAndConfigFile(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
if req.Config == nil {
|
||||
t.Fatal("expected config in validation request")
|
||||
}
|
||||
if req.Config.PrimaryLLM.Model != "cli-model" {
|
||||
t.Fatalf("expected CLI model override, got %q", req.Config.PrimaryLLM.Model)
|
||||
}
|
||||
return validators.Result{ValidatorName: "capture-config", Decisions: nil}, nil
|
||||
}},
|
||||
},
|
||||
proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) { return nil, nil },
|
||||
},
|
||||
}}
|
||||
t.Cleanup(func() { processModuleFactory = nil })
|
||||
|
||||
t.Setenv("AUDITA_MODEL", "env-model")
|
||||
cfgPath := writeFile(t, "config.yml", `
|
||||
version: 1
|
||||
pipeline:
|
||||
modules: [m]
|
||||
llm:
|
||||
proposal:
|
||||
model: file-model
|
||||
`)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := Run([]string{
|
||||
"process",
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--config", cfgPath,
|
||||
"--modules", "m",
|
||||
"--model", "cli-model",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessConfigAPIKeyEnvResolvesAndRedactsEffectiveConfig(t *testing.T) {
|
||||
secret := "secret-from-config-env"
|
||||
t.Setenv("PROPOSAL_KEY_FROM_CONFIG", secret)
|
||||
|
||||
workDir := t.TempDir()
|
||||
outputPath := filepath.Join(t.TempDir(), "out.json")
|
||||
cfgPath := writeFile(t, "config.yml", `
|
||||
version: 1
|
||||
pipeline:
|
||||
modules: [grammar]
|
||||
llm:
|
||||
proposal:
|
||||
api_key_env: PROPOSAL_KEY_FROM_CONFIG
|
||||
diagnostics:
|
||||
work_dir: `+workDir+`
|
||||
retention: always
|
||||
`)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := Run([]string{
|
||||
"process",
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--config", cfgPath,
|
||||
"--output", outputPath,
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
|
||||
runPath := onlyRunDir(t, workDir)
|
||||
configBytes := readFile(t, filepath.Join(runPath, "effective-config.json"))
|
||||
if strings.Contains(string(configBytes), secret) {
|
||||
t.Fatalf("effective config artifact leaked API key from api_key_env")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessTranscriptDescriptionCLIOverridesConfigFileContextDescription(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
if req.Config == nil {
|
||||
t.Fatal("expected config in validation request")
|
||||
}
|
||||
if req.Config.TranscriptDescription != "cli transcript description" {
|
||||
t.Fatalf("expected CLI transcript description to override config file, got %q", req.Config.TranscriptDescription)
|
||||
}
|
||||
return validators.Result{ValidatorName: "capture-config", Decisions: nil}, nil
|
||||
}},
|
||||
},
|
||||
proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) { return nil, nil },
|
||||
},
|
||||
}}
|
||||
t.Cleanup(func() { processModuleFactory = nil })
|
||||
|
||||
cfgPath := writeFile(t, "config.yml", `
|
||||
version: 1
|
||||
pipeline:
|
||||
modules: [m]
|
||||
context:
|
||||
description: "file transcript description"
|
||||
`)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := Run([]string{
|
||||
"process",
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--config", cfgPath,
|
||||
"--modules", "m",
|
||||
"--transcript-description", "cli transcript description",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessMissingTranscriptPath(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
Reference in New Issue
Block a user