Improve error handling and diagnostics for the audita subprocess

This commit is contained in:
2026-05-04 11:46:57 -05:00
parent 89d7bc75c3
commit 4550987bc0
7 changed files with 124 additions and 18 deletions

View File

@@ -157,14 +157,16 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe
}
args := r.buildArgs(req, reqModules)
credentialEnvVar := strings.TrimSpace(r.llmAPIKeyEnv)
credentialPresent := false
env := map[string]string{}
if strings.TrimSpace(r.llmAPIKeyEnv) != "" {
credential, ok := os.LookupEnv(r.llmAPIKeyEnv)
if ok && strings.TrimSpace(credential) != "" {
env["AUDITA_LLM_API_KEY"] = credential
credentialPresent = true
if credentialEnvVar != "" {
credential, ok := os.LookupEnv(credentialEnvVar)
if !ok || strings.TrimSpace(credential) == "" {
return PolishResult{}, fmt.Errorf("audita: required credential environment variable %s is not set", credentialEnvVar)
}
env["AUDITA_LLM_API_KEY"] = credential
credentialPresent = true
}
primaryConcurrencyViaEnv := false
if r.llmConcurrency != nil {
@@ -187,7 +189,13 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe
StderrLogPath: req.StderrLogPath,
})
if err != nil {
return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf("run audita process (binary=%q): %w", r.binary, err)
return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf(
"run audita process (binary=%q, stdout_log=%q, stderr_log=%q): %w",
r.binary,
req.StdoutLogPath,
req.StderrLogPath,
err,
)
}
if err := validateProcessedOutput(req.OutputProcessedPath); err != nil {

View File

@@ -120,7 +120,33 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
}
}
func TestSubprocessRunnerMissingCredentialEnvOmitsCredential(t *testing.T) {
func TestSubprocessRunnerMissingConfiguredCredentialFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
llmConcurrency := 1
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "MISSING_AUDITA_KEY",
Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it",
LLMConcurrency: &llmConcurrency,
Report: false,
})
req := auditaReqForTest(t, false)
_, err := runner.Run(context.Background(), req)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "required credential environment variable MISSING_AUDITA_KEY is not set") {
t.Fatalf("error = %q, want missing credential guidance", err.Error())
}
}
func TestSubprocessRunnerUnconfiguredCredentialEnvOmitsCredential(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
@@ -133,7 +159,7 @@ func TestSubprocessRunnerMissingCredentialEnvOmitsCredential(t *testing.T) {
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"),
LLMAPIKeyEnv: "MISSING_AUDITA_KEY",
LLMAPIKeyEnv: "",
Modules: []string{"glossary"},
BaseURL: "https://openrouter.ai/api/v1",
Model: "openrouter/google/gemma-4-31b-it",
@@ -186,6 +212,9 @@ func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
if !strings.Contains(err.Error(), "exit code") {
t.Fatalf("error = %q, want exit code context", err.Error())
}
if !strings.Contains(err.Error(), req.StdoutLogPath) || !strings.Contains(err.Error(), req.StderrLogPath) {
t.Fatalf("error = %q, want stdout/stderr log paths", err.Error())
}
}
func TestSubprocessRunnerMissingOutputFails(t *testing.T) {

View File

@@ -109,6 +109,67 @@ func TestRunTimeout(t *testing.T) {
}
}
func TestRunInheritsParentEnvironmentByDefault(t *testing.T) {
exe, err := os.Executable()
if err != nil {
t.Fatalf("os.Executable() error = %v", err)
}
t.Setenv("GO_WANT_SUBPROCESS_HELPER", "1")
t.Setenv("SUBPROCESS_HELPER_ENV_KEY", "SUBPROCESS_PARENT_VALUE")
t.Setenv("SUBPROCESS_PARENT_VALUE", "inherited-value")
dir := t.TempDir()
stdoutPath := filepath.Join(dir, "stdout.log")
req := RunRequest{
Executable: exe,
Args: []string{"-test.run=TestSubprocessHelper", "--", "printenv"},
StdoutLogPath: stdoutPath,
}
if _, err := Run(context.Background(), req); err != nil {
t.Fatalf("Run() error = %v", err)
}
data, err := os.ReadFile(stdoutPath)
if err != nil {
t.Fatalf("read stdout log: %v", err)
}
if strings.TrimSpace(string(data)) != "inherited-value" {
t.Fatalf("stdout = %q, want inherited-value", strings.TrimSpace(string(data)))
}
}
func TestRunEnvOverridesWinOverInheritedValues(t *testing.T) {
exe, err := os.Executable()
if err != nil {
t.Fatalf("os.Executable() error = %v", err)
}
t.Setenv("GO_WANT_SUBPROCESS_HELPER", "1")
t.Setenv("SUBPROCESS_HELPER_ENV_KEY", "SUBPROCESS_PARENT_VALUE")
t.Setenv("SUBPROCESS_PARENT_VALUE", "parent-value")
dir := t.TempDir()
stdoutPath := filepath.Join(dir, "stdout.log")
req := RunRequest{
Executable: exe,
Args: []string{"-test.run=TestSubprocessHelper", "--", "printenv"},
EnvOverrides: map[string]string{"SUBPROCESS_PARENT_VALUE": "override-value"},
StdoutLogPath: stdoutPath,
}
if _, err := Run(context.Background(), req); err != nil {
t.Fatalf("Run() error = %v", err)
}
data, err := os.ReadFile(stdoutPath)
if err != nil {
t.Fatalf("read stdout log: %v", err)
}
if strings.TrimSpace(string(data)) != "override-value" {
t.Fatalf("stdout = %q, want override-value", strings.TrimSpace(string(data)))
}
}
func TestWriteYAMLAtomic(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.generated.yml")
@@ -190,6 +251,10 @@ func TestSubprocessHelper(t *testing.T) {
case "sleep":
time.Sleep(500 * time.Millisecond)
os.Exit(0)
case "printenv":
key := os.Getenv("SUBPROCESS_HELPER_ENV_KEY")
_, _ = os.Stdout.WriteString(os.Getenv(key) + "\n")
os.Exit(0)
default:
os.Exit(2)
}

View File

@@ -137,9 +137,6 @@ func applyAuditaDefaults(cfg *AuditaConfig) {
if cfg.Timeout == "" {
cfg.Timeout = "3h"
}
if cfg.LLMAPIKeyEnv == "" {
cfg.LLMAPIKeyEnv = "AUDITA_LLM_API_KEY"
}
if cfg.Modules == nil {
cfg.Modules = []string{
"glossary",

View File

@@ -602,8 +602,8 @@ inputs:
if cfg.Pipeline.Audita.Timeout != "3h" {
t.Fatalf("audita.timeout = %q, want %q", cfg.Pipeline.Audita.Timeout, "3h")
}
if cfg.Pipeline.Audita.LLMAPIKeyEnv != "AUDITA_LLM_API_KEY" {
t.Fatalf("audita.llm_api_key_env = %q, want %q", cfg.Pipeline.Audita.LLMAPIKeyEnv, "AUDITA_LLM_API_KEY")
if cfg.Pipeline.Audita.LLMAPIKeyEnv != "" {
t.Fatalf("audita.llm_api_key_env = %q, want empty by default", cfg.Pipeline.Audita.LLMAPIKeyEnv)
}
if got := strings.Join(cfg.Pipeline.Audita.Modules, ","); got != "glossary,homophones,glossary,spoken_word,grammar,homophones,glossary" {
t.Fatalf("audita.modules = %q, want default sequence", got)