Implement Audita subprocess adapter
This commit is contained in:
@@ -2,7 +2,10 @@ package audita
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
||||||
)
|
)
|
||||||
@@ -18,7 +21,15 @@ func (n *NoopRunner) Run(ctx context.Context, req PolishRequest) (PolishResult,
|
|||||||
if err := materializePlaceholders(req); err != nil {
|
if err := materializePlaceholders(req); err != nil {
|
||||||
return PolishResult{}, err
|
return PolishResult{}, err
|
||||||
}
|
}
|
||||||
return PolishResult{ProcessedTranscriptPath: req.OutputProcessedPath, Metadata: map[string]any{"placeholder": true}}, nil
|
return PolishResult{
|
||||||
|
ProcessedTranscriptPath: req.OutputProcessedPath,
|
||||||
|
ReportPath: req.ReportPath,
|
||||||
|
WorkDir: req.WorkDir,
|
||||||
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
|
StderrLogPath: req.StderrLogPath,
|
||||||
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||||
|
Metadata: map[string]any{"placeholder": true},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FakeRunner captures polish requests and returns deterministic responses.
|
// FakeRunner captures polish requests and returns deterministic responses.
|
||||||
@@ -44,6 +55,21 @@ func (f *FakeRunner) Run(ctx context.Context, req PolishRequest) (PolishResult,
|
|||||||
if res.ProcessedTranscriptPath == "" {
|
if res.ProcessedTranscriptPath == "" {
|
||||||
res.ProcessedTranscriptPath = req.OutputProcessedPath
|
res.ProcessedTranscriptPath = req.OutputProcessedPath
|
||||||
}
|
}
|
||||||
|
if res.ReportPath == "" {
|
||||||
|
res.ReportPath = req.ReportPath
|
||||||
|
}
|
||||||
|
if res.WorkDir == "" {
|
||||||
|
res.WorkDir = req.WorkDir
|
||||||
|
}
|
||||||
|
if res.StdoutLogPath == "" {
|
||||||
|
res.StdoutLogPath = req.StdoutLogPath
|
||||||
|
}
|
||||||
|
if res.StderrLogPath == "" {
|
||||||
|
res.StderrLogPath = req.StderrLogPath
|
||||||
|
}
|
||||||
|
if res.GeneratedConfigPath == "" {
|
||||||
|
res.GeneratedConfigPath = req.GeneratedConfigPath
|
||||||
|
}
|
||||||
if res.Metadata == nil {
|
if res.Metadata == nil {
|
||||||
res.Metadata = map[string]any{"fake": true}
|
res.Metadata = map[string]any{"fake": true}
|
||||||
}
|
}
|
||||||
@@ -72,5 +98,34 @@ func materializePlaceholders(req PolishRequest) error {
|
|||||||
return fmt.Errorf("write stderr log %q: %w", req.StderrLogPath, err)
|
return fmt.Errorf("write stderr log %q: %w", req.StderrLogPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := writeJSONIfRequested(req.OutputProcessedPath, map[string]any{
|
||||||
|
"schema": "audita.processed.v1",
|
||||||
|
"segments": []any{},
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := writeJSONIfRequested(req.ReportPath, map[string]any{
|
||||||
|
"schema": "audita.report.v1",
|
||||||
|
"steps": []any{},
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSONIfRequested(path string, payload any) error {
|
||||||
|
if path == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create parent directory %q: %w", filepath.Dir(path), err)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal placeholder json for %q: %w", path, err)
|
||||||
|
}
|
||||||
|
if err := subprocess.WriteFileAtomic(path, data, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write placeholder json %q: %w", path, err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
// Package audita declares the adapter contract for transcript polishing.
|
// Package audita declares the adapter contract for transcript polishing.
|
||||||
package audita
|
package audita
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// TODO: implement a real Audita subprocess/service adapter.
|
// TODO: implement a real Audita subprocess/service adapter.
|
||||||
|
|
||||||
@@ -15,6 +18,14 @@ type PolishRequest struct {
|
|||||||
GeneratedConfigPath string
|
GeneratedConfigPath string
|
||||||
MergedTranscriptPath string
|
MergedTranscriptPath string
|
||||||
OutputProcessedPath string
|
OutputProcessedPath string
|
||||||
|
GlossaryPath string
|
||||||
|
ReportPath string
|
||||||
|
WorkDir string
|
||||||
|
Modules []string
|
||||||
|
BaseURL string
|
||||||
|
Model string
|
||||||
|
ValidationModel string
|
||||||
|
ValidationLLMConcurrency *int
|
||||||
StdoutLogPath string
|
StdoutLogPath string
|
||||||
StderrLogPath string
|
StderrLogPath string
|
||||||
}
|
}
|
||||||
@@ -22,5 +33,13 @@ type PolishRequest struct {
|
|||||||
// PolishResult describes a polish output.
|
// PolishResult describes a polish output.
|
||||||
type PolishResult struct {
|
type PolishResult struct {
|
||||||
ProcessedTranscriptPath string
|
ProcessedTranscriptPath string
|
||||||
|
ReportPath string
|
||||||
|
WorkDir string
|
||||||
|
StdoutLogPath string
|
||||||
|
StderrLogPath string
|
||||||
|
GeneratedConfigPath string
|
||||||
|
ExitCode int
|
||||||
|
Duration time.Duration
|
||||||
|
InvokedBinary string
|
||||||
Metadata map[string]any
|
Metadata map[string]any
|
||||||
}
|
}
|
||||||
|
|||||||
335
internal/adapters/audita/subprocess.go
Normal file
335
internal/adapters/audita/subprocess.go
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
package audita
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SubprocessRunnerConfig defines deterministic settings for Audita CLI execution.
|
||||||
|
type SubprocessRunnerConfig struct {
|
||||||
|
Binary string
|
||||||
|
Timeout time.Duration
|
||||||
|
LLMAPIKeyEnv string
|
||||||
|
Modules []string
|
||||||
|
BaseURL string
|
||||||
|
Model string
|
||||||
|
LLMConcurrency *int
|
||||||
|
ValidationModel string
|
||||||
|
ValidationLLMConcurrency *int
|
||||||
|
Report bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubprocessRunner invokes Audita via subprocess.
|
||||||
|
type SubprocessRunner struct {
|
||||||
|
binary string
|
||||||
|
timeout time.Duration
|
||||||
|
llmAPIKeyEnv string
|
||||||
|
modules []string
|
||||||
|
baseURL string
|
||||||
|
model string
|
||||||
|
llmConcurrency *int
|
||||||
|
validationModel string
|
||||||
|
validationLLMConcurrency *int
|
||||||
|
report bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubprocessRunnerFromConfigValues parses config-derived values once.
|
||||||
|
func NewSubprocessRunnerFromConfigValues(
|
||||||
|
binary string,
|
||||||
|
timeout string,
|
||||||
|
llmAPIKeyEnv string,
|
||||||
|
modules []string,
|
||||||
|
baseURL string,
|
||||||
|
model string,
|
||||||
|
llmConcurrency *int,
|
||||||
|
validationModel string,
|
||||||
|
validationLLMConcurrency *int,
|
||||||
|
report bool,
|
||||||
|
) (*SubprocessRunner, error) {
|
||||||
|
if strings.TrimSpace(timeout) == "" {
|
||||||
|
return nil, fmt.Errorf("audita timeout is required")
|
||||||
|
}
|
||||||
|
parsedTimeout, err := time.ParseDuration(timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse audita timeout %q: %w", timeout, err)
|
||||||
|
}
|
||||||
|
return NewSubprocessRunner(SubprocessRunnerConfig{
|
||||||
|
Binary: binary,
|
||||||
|
Timeout: parsedTimeout,
|
||||||
|
LLMAPIKeyEnv: llmAPIKeyEnv,
|
||||||
|
Modules: modules,
|
||||||
|
BaseURL: baseURL,
|
||||||
|
Model: model,
|
||||||
|
LLMConcurrency: llmConcurrency,
|
||||||
|
ValidationModel: validationModel,
|
||||||
|
ValidationLLMConcurrency: validationLLMConcurrency,
|
||||||
|
Report: report,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSubprocessRunner constructs a validated Audita subprocess runner.
|
||||||
|
func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error) {
|
||||||
|
if strings.TrimSpace(cfg.Binary) == "" {
|
||||||
|
return nil, fmt.Errorf("audita binary is required")
|
||||||
|
}
|
||||||
|
if cfg.Timeout <= 0 {
|
||||||
|
return nil, fmt.Errorf("audita timeout must be > 0")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.LLMAPIKeyEnv) == "" {
|
||||||
|
return nil, fmt.Errorf("audita llm api key env var name is required")
|
||||||
|
}
|
||||||
|
if len(cfg.Modules) == 0 {
|
||||||
|
return nil, fmt.Errorf("audita modules must include at least one module")
|
||||||
|
}
|
||||||
|
for i, module := range cfg.Modules {
|
||||||
|
if strings.TrimSpace(module) == "" {
|
||||||
|
return nil, fmt.Errorf("audita module at index %d is empty", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.BaseURL) == "" {
|
||||||
|
return nil, fmt.Errorf("audita base url is required")
|
||||||
|
}
|
||||||
|
u, err := url.Parse(cfg.BaseURL)
|
||||||
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("audita base url %q is invalid: %w", cfg.BaseURL, err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("audita base url %q is invalid", cfg.BaseURL)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Model) == "" {
|
||||||
|
return nil, fmt.Errorf("audita model is required")
|
||||||
|
}
|
||||||
|
if cfg.LLMConcurrency != nil && *cfg.LLMConcurrency <= 0 {
|
||||||
|
return nil, fmt.Errorf("audita llm concurrency must be > 0 when provided")
|
||||||
|
}
|
||||||
|
if cfg.ValidationLLMConcurrency != nil && *cfg.ValidationLLMConcurrency <= 0 {
|
||||||
|
return nil, fmt.Errorf("audita validation llm concurrency must be > 0 when provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
modules := make([]string, len(cfg.Modules))
|
||||||
|
for i, m := range cfg.Modules {
|
||||||
|
modules[i] = strings.TrimSpace(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SubprocessRunner{
|
||||||
|
binary: strings.TrimSpace(cfg.Binary),
|
||||||
|
timeout: cfg.Timeout,
|
||||||
|
llmAPIKeyEnv: strings.TrimSpace(cfg.LLMAPIKeyEnv),
|
||||||
|
modules: modules,
|
||||||
|
baseURL: strings.TrimSpace(cfg.BaseURL),
|
||||||
|
model: strings.TrimSpace(cfg.Model),
|
||||||
|
llmConcurrency: cfg.LLMConcurrency,
|
||||||
|
validationModel: strings.TrimSpace(cfg.ValidationModel),
|
||||||
|
validationLLMConcurrency: cfg.ValidationLLMConcurrency,
|
||||||
|
report: cfg.Report,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes Audita process with deterministic flags and validates output artifacts.
|
||||||
|
func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishResult, error) {
|
||||||
|
if r == nil {
|
||||||
|
return PolishResult{}, fmt.Errorf("audita subprocess runner is nil")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.MergedTranscriptPath) == "" {
|
||||||
|
return PolishResult{}, fmt.Errorf("audita merged transcript path is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.GlossaryPath) == "" {
|
||||||
|
return PolishResult{}, fmt.Errorf("audita glossary path is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.OutputProcessedPath) == "" {
|
||||||
|
return PolishResult{}, fmt.Errorf("audita output processed path is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.WorkDir) == "" {
|
||||||
|
return PolishResult{}, fmt.Errorf("audita work dir is required")
|
||||||
|
}
|
||||||
|
if r.report && strings.TrimSpace(req.ReportPath) == "" {
|
||||||
|
return PolishResult{}, fmt.Errorf("audita report is enabled but report path is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
reqModules := req.Modules
|
||||||
|
if len(reqModules) == 0 {
|
||||||
|
reqModules = append([]string(nil), r.modules...)
|
||||||
|
}
|
||||||
|
args := r.buildArgs(req, reqModules)
|
||||||
|
|
||||||
|
credential, credentialPresent := os.LookupEnv(r.llmAPIKeyEnv)
|
||||||
|
if !credentialPresent || strings.TrimSpace(credential) == "" {
|
||||||
|
return PolishResult{}, fmt.Errorf("audita credential env var %q is required but not set", r.llmAPIKeyEnv)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := map[string]string{
|
||||||
|
"AUDITA_LLM_API_KEY": credential,
|
||||||
|
}
|
||||||
|
primaryConcurrencyViaEnv := false
|
||||||
|
if r.llmConcurrency != nil {
|
||||||
|
env["AUDITA_LLM_CONCURRENCY"] = strconv.Itoa(*r.llmConcurrency)
|
||||||
|
primaryConcurrencyViaEnv = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GeneratedConfigPath != "" {
|
||||||
|
if err := r.writeInvocationConfig(req, args, reqModules, credentialPresent, primaryConcurrencyViaEnv); err != nil {
|
||||||
|
return PolishResult{}, fmt.Errorf("write audita invocation config %q: %w", req.GeneratedConfigPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runRes, err := subprocess.Run(ctx, subprocess.RunRequest{
|
||||||
|
Executable: r.binary,
|
||||||
|
Args: args,
|
||||||
|
Timeout: r.timeout,
|
||||||
|
EnvOverrides: env,
|
||||||
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateProcessedOutput(req.OutputProcessedPath); err != nil {
|
||||||
|
return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf("validate audita processed output %q: %w", req.OutputProcessedPath, err)
|
||||||
|
}
|
||||||
|
if r.report {
|
||||||
|
if err := validateJSONFile(req.ReportPath); err != nil {
|
||||||
|
return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf("validate audita report output %q: %w", req.ReportPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PolishResult{
|
||||||
|
ProcessedTranscriptPath: req.OutputProcessedPath,
|
||||||
|
ReportPath: req.ReportPath,
|
||||||
|
WorkDir: req.WorkDir,
|
||||||
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
|
StderrLogPath: req.StderrLogPath,
|
||||||
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||||
|
ExitCode: runRes.ExitCode,
|
||||||
|
Duration: runRes.Duration,
|
||||||
|
InvokedBinary: r.binary,
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"adapter": "audita_subprocess",
|
||||||
|
"modules": reqModules,
|
||||||
|
"base_url": r.baseURL,
|
||||||
|
"model": r.model,
|
||||||
|
"validation_model": r.validationModel,
|
||||||
|
"validation_llm_concurrency": r.validationLLMConcurrency,
|
||||||
|
"credential_env_var": r.llmAPIKeyEnv,
|
||||||
|
"credential_present": credentialPresent,
|
||||||
|
"primary_llm_concurrency_via_env": primaryConcurrencyViaEnv,
|
||||||
|
"primary_llm_concurrency_env_name": "AUDITA_LLM_CONCURRENCY",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SubprocessRunner) failureResult(req PolishRequest, modules []string, runRes subprocess.RunResult, credentialPresent bool, primaryConcurrencyViaEnv bool) PolishResult {
|
||||||
|
return PolishResult{
|
||||||
|
ProcessedTranscriptPath: req.OutputProcessedPath,
|
||||||
|
ReportPath: req.ReportPath,
|
||||||
|
WorkDir: req.WorkDir,
|
||||||
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
|
StderrLogPath: req.StderrLogPath,
|
||||||
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||||
|
ExitCode: runRes.ExitCode,
|
||||||
|
Duration: runRes.Duration,
|
||||||
|
InvokedBinary: r.binary,
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"adapter": "audita_subprocess",
|
||||||
|
"modules": modules,
|
||||||
|
"base_url": r.baseURL,
|
||||||
|
"model": r.model,
|
||||||
|
"validation_model": r.validationModel,
|
||||||
|
"validation_llm_concurrency": r.validationLLMConcurrency,
|
||||||
|
"credential_env_var": r.llmAPIKeyEnv,
|
||||||
|
"credential_present": credentialPresent,
|
||||||
|
"primary_llm_concurrency_via_env": primaryConcurrencyViaEnv,
|
||||||
|
"primary_llm_concurrency_env_name": "AUDITA_LLM_CONCURRENCY",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SubprocessRunner) buildArgs(req PolishRequest, modules []string) []string {
|
||||||
|
args := []string{
|
||||||
|
"process",
|
||||||
|
req.MergedTranscriptPath,
|
||||||
|
"--glossary", req.GlossaryPath,
|
||||||
|
"--output", req.OutputProcessedPath,
|
||||||
|
"--modules", strings.Join(modules, ","),
|
||||||
|
"--base-url", r.baseURL,
|
||||||
|
"--model", r.model,
|
||||||
|
"--work-dir", req.WorkDir,
|
||||||
|
}
|
||||||
|
if r.report {
|
||||||
|
args = append(args, "--report-json", req.ReportPath)
|
||||||
|
}
|
||||||
|
if r.validationModel != "" {
|
||||||
|
args = append(args, "--validation-model", r.validationModel)
|
||||||
|
}
|
||||||
|
if r.validationLLMConcurrency != nil {
|
||||||
|
args = append(args, "--validation-llm-concurrency", strconv.Itoa(*r.validationLLMConcurrency))
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SubprocessRunner) writeInvocationConfig(req PolishRequest, args []string, modules []string, credentialPresent bool, primaryConcurrencyViaEnv bool) error {
|
||||||
|
payload := map[string]any{
|
||||||
|
"schema": "audita.generated.v1",
|
||||||
|
"binary": r.binary,
|
||||||
|
"args": args,
|
||||||
|
"timeout": r.timeout.String(),
|
||||||
|
"modules": modules,
|
||||||
|
"base_url": r.baseURL,
|
||||||
|
"model": r.model,
|
||||||
|
"validation_model": r.validationModel,
|
||||||
|
"validation_llm_concurrency": r.validationLLMConcurrency,
|
||||||
|
"report_enabled": r.report,
|
||||||
|
"merged_transcript_path": req.MergedTranscriptPath,
|
||||||
|
"glossary_path": req.GlossaryPath,
|
||||||
|
"output_path": req.OutputProcessedPath,
|
||||||
|
"report_path": req.ReportPath,
|
||||||
|
"work_dir": req.WorkDir,
|
||||||
|
"credential_env_var": r.llmAPIKeyEnv,
|
||||||
|
"credential_present": credentialPresent,
|
||||||
|
"primary_llm_concurrency_via_env": primaryConcurrencyViaEnv,
|
||||||
|
}
|
||||||
|
if r.llmConcurrency != nil {
|
||||||
|
payload["llm_concurrency"] = *r.llmConcurrency
|
||||||
|
}
|
||||||
|
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateProcessedOutput(path string) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload map[string]any
|
||||||
|
if err := json.Unmarshal(data, &payload); err != nil {
|
||||||
|
return fmt.Errorf("parse json: %w", err)
|
||||||
|
}
|
||||||
|
segments, ok := payload["segments"]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("top-level segments is required")
|
||||||
|
}
|
||||||
|
if _, ok := segments.([]any); !ok {
|
||||||
|
return fmt.Errorf("top-level segments must be an array")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateJSONFile(path string) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read file: %w", err)
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal(data, &v); err != nil {
|
||||||
|
return fmt.Errorf("parse json: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
513
internal/adapters/audita/subprocess_test.go
Normal file
513
internal/adapters/audita/subprocess_test.go
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
package audita
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("helper wrapper script uses /bin/sh")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
||||||
|
t.Setenv("AUDITA_HELPER_MODE", "success")
|
||||||
|
t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
|
||||||
|
|
||||||
|
recordPath := filepath.Join(t.TempDir(), "record.json")
|
||||||
|
t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath)
|
||||||
|
|
||||||
|
wrapper := writeAuditaHelperWrapper(t)
|
||||||
|
llmConcurrency := 1
|
||||||
|
validationLLMConcurrency := 2
|
||||||
|
runner, err := NewSubprocessRunner(SubprocessRunnerConfig{
|
||||||
|
Binary: wrapper,
|
||||||
|
Timeout: mustParseAuditaDuration(t, "2s"),
|
||||||
|
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
|
||||||
|
Modules: []string{"glossary", "homophones", "glossary"},
|
||||||
|
BaseURL: "https://openrouter.ai/api/v1",
|
||||||
|
Model: "openrouter/google/gemma-4-31b-it",
|
||||||
|
LLMConcurrency: &llmConcurrency,
|
||||||
|
ValidationModel: "openrouter/google/gemma-4-31b-it",
|
||||||
|
ValidationLLMConcurrency: &validationLLMConcurrency,
|
||||||
|
Report: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSubprocessRunner() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
req := PolishRequest{
|
||||||
|
GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"),
|
||||||
|
MergedTranscriptPath: filepath.Join(dir, "merged.json"),
|
||||||
|
GlossaryPath: filepath.Join(dir, "glossary.yml"),
|
||||||
|
OutputProcessedPath: filepath.Join(dir, "processed.json"),
|
||||||
|
ReportPath: filepath.Join(dir, "audita.report.json"),
|
||||||
|
WorkDir: filepath.Join(dir, "artifacts", "audita-work"),
|
||||||
|
StdoutLogPath: filepath.Join(dir, "audita.stdout.log"),
|
||||||
|
StderrLogPath: filepath.Join(dir, "audita.stderr.log"),
|
||||||
|
}
|
||||||
|
writeAuditaTestFile(t, req.MergedTranscriptPath, `{"segments":[]}`)
|
||||||
|
writeAuditaTestFile(t, req.GlossaryPath, "terms: []\n")
|
||||||
|
|
||||||
|
res, err := runner.Run(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
if res.ProcessedTranscriptPath != req.OutputProcessedPath {
|
||||||
|
t.Fatalf("ProcessedTranscriptPath = %q, want %q", res.ProcessedTranscriptPath, req.OutputProcessedPath)
|
||||||
|
}
|
||||||
|
if res.ReportPath != req.ReportPath {
|
||||||
|
t.Fatalf("ReportPath = %q, want %q", res.ReportPath, req.ReportPath)
|
||||||
|
}
|
||||||
|
if res.WorkDir != req.WorkDir {
|
||||||
|
t.Fatalf("WorkDir = %q, want %q", res.WorkDir, req.WorkDir)
|
||||||
|
}
|
||||||
|
if res.InvokedBinary != wrapper {
|
||||||
|
t.Fatalf("InvokedBinary = %q, want %q", res.InvokedBinary, wrapper)
|
||||||
|
}
|
||||||
|
if res.ExitCode != 0 {
|
||||||
|
t.Fatalf("ExitCode = %d, want 0", res.ExitCode)
|
||||||
|
}
|
||||||
|
if res.Duration <= 0 {
|
||||||
|
t.Fatalf("Duration = %s, want > 0", res.Duration)
|
||||||
|
}
|
||||||
|
assertJSONFileAudita(t, req.OutputProcessedPath)
|
||||||
|
assertJSONFileAudita(t, req.ReportPath)
|
||||||
|
if _, err := os.Stat(req.StdoutLogPath); err != nil {
|
||||||
|
t.Fatalf("stdout log missing: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(req.StderrLogPath); err != nil {
|
||||||
|
t.Fatalf("stderr log missing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec := readAuditaHelperRecord(t, recordPath)
|
||||||
|
wantArgs := []string{
|
||||||
|
"process", req.MergedTranscriptPath,
|
||||||
|
"--glossary", req.GlossaryPath,
|
||||||
|
"--output", req.OutputProcessedPath,
|
||||||
|
"--modules", "glossary,homophones,glossary",
|
||||||
|
"--base-url", "https://openrouter.ai/api/v1",
|
||||||
|
"--model", "openrouter/google/gemma-4-31b-it",
|
||||||
|
"--work-dir", req.WorkDir,
|
||||||
|
"--report-json", req.ReportPath,
|
||||||
|
"--validation-model", "openrouter/google/gemma-4-31b-it",
|
||||||
|
"--validation-llm-concurrency", "2",
|
||||||
|
}
|
||||||
|
if strings.Join(rec.Args, "\n") != strings.Join(wantArgs, "\n") {
|
||||||
|
t.Fatalf("args = %#v, want %#v", rec.Args, wantArgs)
|
||||||
|
}
|
||||||
|
if rec.Env["AUDITA_LLM_API_KEY"] != "super-secret" {
|
||||||
|
t.Fatalf("AUDITA_LLM_API_KEY = %q, want propagated secret", rec.Env["AUDITA_LLM_API_KEY"])
|
||||||
|
}
|
||||||
|
if rec.Env["AUDITA_LLM_CONCURRENCY"] != "1" {
|
||||||
|
t.Fatalf("AUDITA_LLM_CONCURRENCY = %q, want 1", rec.Env["AUDITA_LLM_CONCURRENCY"])
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgData, err := os.ReadFile(req.GeneratedConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read generated config: %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(cfgData), "super-secret") {
|
||||||
|
t.Fatalf("generated config must not contain credential value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerMissingCredentialFails(t *testing.T) {
|
||||||
|
llmConcurrency := 1
|
||||||
|
runner, err := NewSubprocessRunner(SubprocessRunnerConfig{
|
||||||
|
Binary: "audita",
|
||||||
|
Timeout: mustParseAuditaDuration(t, "1s"),
|
||||||
|
LLMAPIKeyEnv: "MISSING_AUDITA_KEY",
|
||||||
|
Modules: []string{"glossary"},
|
||||||
|
BaseURL: "https://openrouter.ai/api/v1",
|
||||||
|
Model: "openrouter/google/gemma-4-31b-it",
|
||||||
|
LLMConcurrency: &llmConcurrency,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSubprocessRunner() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := PolishRequest{
|
||||||
|
MergedTranscriptPath: "/tmp/merged.json",
|
||||||
|
GlossaryPath: "/tmp/glossary.yml",
|
||||||
|
OutputProcessedPath: "/tmp/processed.json",
|
||||||
|
WorkDir: "/tmp/audita-work",
|
||||||
|
}
|
||||||
|
_, err = runner.Run(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "MISSING_AUDITA_KEY") {
|
||||||
|
t.Fatalf("error = %q, want env var name context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("helper wrapper script uses /bin/sh")
|
||||||
|
}
|
||||||
|
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
||||||
|
t.Setenv("AUDITA_HELPER_MODE", "fail")
|
||||||
|
t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
|
||||||
|
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
llmConcurrency := 1
|
||||||
|
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
||||||
|
Binary: writeAuditaHelperWrapper(t),
|
||||||
|
Timeout: mustParseAuditaDuration(t, "2s"),
|
||||||
|
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
|
||||||
|
Modules: []string{"glossary"},
|
||||||
|
BaseURL: "https://openrouter.ai/api/v1",
|
||||||
|
Model: "openrouter/google/gemma-4-31b-it",
|
||||||
|
LLMConcurrency: &llmConcurrency,
|
||||||
|
Report: true,
|
||||||
|
})
|
||||||
|
req := auditaReqForTest(t, true)
|
||||||
|
_, err := runner.Run(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "run audita process") {
|
||||||
|
t.Fatalf("error = %q, want subprocess context", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "exit code") {
|
||||||
|
t.Fatalf("error = %q, want exit code context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerMissingOutputFails(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("helper wrapper script uses /bin/sh")
|
||||||
|
}
|
||||||
|
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
||||||
|
t.Setenv("AUDITA_HELPER_MODE", "missing_output")
|
||||||
|
t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
|
||||||
|
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
llmConcurrency := 1
|
||||||
|
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
||||||
|
Binary: writeAuditaHelperWrapper(t),
|
||||||
|
Timeout: mustParseAuditaDuration(t, "2s"),
|
||||||
|
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
|
||||||
|
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(), "validate audita processed output") {
|
||||||
|
t.Fatalf("error = %q, want output validation context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerInvalidOutputJSONFails(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("helper wrapper script uses /bin/sh")
|
||||||
|
}
|
||||||
|
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
||||||
|
t.Setenv("AUDITA_HELPER_MODE", "invalid_output")
|
||||||
|
t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
|
||||||
|
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
llmConcurrency := 1
|
||||||
|
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
||||||
|
Binary: writeAuditaHelperWrapper(t),
|
||||||
|
Timeout: mustParseAuditaDuration(t, "2s"),
|
||||||
|
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
|
||||||
|
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(), "parse json") {
|
||||||
|
t.Fatalf("error = %q, want parse json context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerSegmentsMissingFails(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("helper wrapper script uses /bin/sh")
|
||||||
|
}
|
||||||
|
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
||||||
|
t.Setenv("AUDITA_HELPER_MODE", "segments_missing")
|
||||||
|
t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
|
||||||
|
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
llmConcurrency := 1
|
||||||
|
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
||||||
|
Binary: writeAuditaHelperWrapper(t),
|
||||||
|
Timeout: mustParseAuditaDuration(t, "2s"),
|
||||||
|
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
|
||||||
|
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(), "segments") {
|
||||||
|
t.Fatalf("error = %q, want segments validation context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("helper wrapper script uses /bin/sh")
|
||||||
|
}
|
||||||
|
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
||||||
|
t.Setenv("AUDITA_HELPER_MODE", "invalid_report")
|
||||||
|
t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
|
||||||
|
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
llmConcurrency := 1
|
||||||
|
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
||||||
|
Binary: writeAuditaHelperWrapper(t),
|
||||||
|
Timeout: mustParseAuditaDuration(t, "2s"),
|
||||||
|
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
|
||||||
|
Modules: []string{"glossary"},
|
||||||
|
BaseURL: "https://openrouter.ai/api/v1",
|
||||||
|
Model: "openrouter/google/gemma-4-31b-it",
|
||||||
|
LLMConcurrency: &llmConcurrency,
|
||||||
|
Report: true,
|
||||||
|
})
|
||||||
|
req := auditaReqForTest(t, true)
|
||||||
|
_, err := runner.Run(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "validate audita report output") {
|
||||||
|
t.Fatalf("error = %q, want report validation context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerConstructorValidation(t *testing.T) {
|
||||||
|
_, err := NewSubprocessRunnerFromConfigValues("", "3h", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", nil, "", nil, true)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected binary validation error")
|
||||||
|
}
|
||||||
|
_, err = NewSubprocessRunnerFromConfigValues("audita", "bad", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", nil, "", nil, true)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected timeout parse error")
|
||||||
|
}
|
||||||
|
_, err = NewSubprocessRunner(SubprocessRunnerConfig{
|
||||||
|
Binary: "audita",
|
||||||
|
Timeout: mustParseAuditaDuration(t, "1s"),
|
||||||
|
LLMAPIKeyEnv: "AUDITA_LLM_API_KEY",
|
||||||
|
Modules: []string{"glossary"},
|
||||||
|
BaseURL: "://",
|
||||||
|
Model: "openrouter/google/gemma-4-31b-it",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected base url validation error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type auditaHelperRecord struct {
|
||||||
|
Args []string `json:"args"`
|
||||||
|
Env map[string]string `json:"env"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuditaSubprocessHelper(t *testing.T) {
|
||||||
|
if os.Getenv("GO_WANT_AUDITA_HELPER") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args := os.Args
|
||||||
|
start := -1
|
||||||
|
for i := range args {
|
||||||
|
if args[i] == "--" {
|
||||||
|
start = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start < 0 || start >= len(args) {
|
||||||
|
_, _ = os.Stderr.WriteString("missing -- args separator\n")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
procArgs := args[start:]
|
||||||
|
outPath := auditaFlagValue(procArgs, "--output")
|
||||||
|
reportPath := auditaFlagValue(procArgs, "--report-json")
|
||||||
|
recordPath := os.Getenv("AUDITA_HELPER_RECORD_PATH")
|
||||||
|
if strings.TrimSpace(recordPath) != "" {
|
||||||
|
rec := auditaHelperRecord{
|
||||||
|
Args: procArgs,
|
||||||
|
Env: map[string]string{
|
||||||
|
"AUDITA_LLM_API_KEY": os.Getenv("AUDITA_LLM_API_KEY"),
|
||||||
|
"AUDITA_LLM_CONCURRENCY": os.Getenv("AUDITA_LLM_CONCURRENCY"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(rec)
|
||||||
|
_ = os.MkdirAll(filepath.Dir(recordPath), 0o755)
|
||||||
|
_ = os.WriteFile(recordPath, data, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := os.Getenv("AUDITA_HELPER_MODE")
|
||||||
|
switch mode {
|
||||||
|
case "success":
|
||||||
|
writeAuditaHelperFile(outPath, `{"schema":"audita.processed.v1","segments":[]}`)
|
||||||
|
if reportPath != "" {
|
||||||
|
writeAuditaHelperFile(reportPath, `{"schema":"audita.report.v1","steps":[]}`)
|
||||||
|
}
|
||||||
|
_, _ = os.Stdout.WriteString("audita helper success stdout\n")
|
||||||
|
_, _ = os.Stderr.WriteString("audita helper success stderr\n")
|
||||||
|
os.Exit(0)
|
||||||
|
case "fail":
|
||||||
|
_, _ = os.Stderr.WriteString("audita helper failure\n")
|
||||||
|
os.Exit(8)
|
||||||
|
case "missing_output":
|
||||||
|
if reportPath != "" {
|
||||||
|
writeAuditaHelperFile(reportPath, `{"schema":"audita.report.v1","steps":[]}`)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
case "invalid_output":
|
||||||
|
writeAuditaHelperFile(outPath, `not-json`)
|
||||||
|
if reportPath != "" {
|
||||||
|
writeAuditaHelperFile(reportPath, `{"schema":"audita.report.v1","steps":[]}`)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
case "segments_missing":
|
||||||
|
writeAuditaHelperFile(outPath, `{"schema":"audita.processed.v1"}`)
|
||||||
|
if reportPath != "" {
|
||||||
|
writeAuditaHelperFile(reportPath, `{"schema":"audita.report.v1","steps":[]}`)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
case "invalid_report":
|
||||||
|
writeAuditaHelperFile(outPath, `{"schema":"audita.processed.v1","segments":[]}`)
|
||||||
|
if reportPath != "" {
|
||||||
|
writeAuditaHelperFile(reportPath, `not-json`)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
default:
|
||||||
|
_, _ = os.Stderr.WriteString(fmt.Sprintf("unknown helper mode %q\n", mode))
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustAuditaRunner(t *testing.T, cfg SubprocessRunnerConfig) *SubprocessRunner {
|
||||||
|
t.Helper()
|
||||||
|
r, err := NewSubprocessRunner(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewSubprocessRunner() error = %v", err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func auditaReqForTest(t *testing.T, withReport bool) PolishRequest {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
merged := filepath.Join(dir, "merged.json")
|
||||||
|
glossary := filepath.Join(dir, "glossary.yml")
|
||||||
|
writeAuditaTestFile(t, merged, `{"segments":[]}`)
|
||||||
|
writeAuditaTestFile(t, glossary, "terms: []\n")
|
||||||
|
req := PolishRequest{
|
||||||
|
GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"),
|
||||||
|
MergedTranscriptPath: merged,
|
||||||
|
GlossaryPath: glossary,
|
||||||
|
OutputProcessedPath: filepath.Join(dir, "processed.json"),
|
||||||
|
WorkDir: filepath.Join(dir, "artifacts", "audita-work"),
|
||||||
|
StdoutLogPath: filepath.Join(dir, "audita.stdout.log"),
|
||||||
|
StderrLogPath: filepath.Join(dir, "audita.stderr.log"),
|
||||||
|
}
|
||||||
|
if withReport {
|
||||||
|
req.ReportPath = filepath.Join(dir, "audita.report.json")
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAuditaHelperWrapper(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("os.Executable() error = %v", err)
|
||||||
|
}
|
||||||
|
path := filepath.Join(t.TempDir(), "audita-helper-wrapper.sh")
|
||||||
|
content := "#!/bin/sh\nexec \"" + exe + "\" -test.run=TestAuditaSubprocessHelper -- \"$@\"\n"
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o755); err != nil {
|
||||||
|
t.Fatalf("WriteFile(%q) error = %v", path, err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAuditaTestFile(t *testing.T, path, contents string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll(%q): %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile(%q): %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAuditaHelperFile(path, contents string) {
|
||||||
|
if strings.TrimSpace(path) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = os.MkdirAll(filepath.Dir(path), 0o755)
|
||||||
|
_ = os.WriteFile(path, []byte(contents), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func auditaFlagValue(args []string, name string) string {
|
||||||
|
for i := 0; i < len(args)-1; i++ {
|
||||||
|
if args[i] == name {
|
||||||
|
return args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAuditaHelperRecord(t *testing.T, path string) auditaHelperRecord {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(%q): %v", path, err)
|
||||||
|
}
|
||||||
|
var rec auditaHelperRecord
|
||||||
|
if err := json.Unmarshal(data, &rec); err != nil {
|
||||||
|
t.Fatalf("json unmarshal helper record: %v", err)
|
||||||
|
}
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertJSONFileAudita(t *testing.T, path string) {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(%q): %v", path, err)
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal(data, &v); err != nil {
|
||||||
|
t.Fatalf("json unmarshal %q: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustParseAuditaDuration(t *testing.T, value string) time.Duration {
|
||||||
|
t.Helper()
|
||||||
|
d, err := time.ParseDuration(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("time.ParseDuration(%q) error = %v", value, err)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user