Adopt Audita v2 configuration and subprocess contract
This commit is contained in:
@@ -334,19 +334,14 @@ Rules:
|
|||||||
| `pipeline.seriatim.env.filler_max_duration` | float | No | unset |
|
| `pipeline.seriatim.env.filler_max_duration` | float | No | unset |
|
||||||
| `pipeline.audita.binary` | string | No | `audita` |
|
| `pipeline.audita.binary` | string | No | `audita` |
|
||||||
| `pipeline.audita.timeout` | duration | No | `3h` |
|
| `pipeline.audita.timeout` | duration | No | `3h` |
|
||||||
| `pipeline.audita.llm_api_key` | secret reference | No | unset |
|
| `pipeline.audita.credentials[]` | list[secret reference] | No | empty; exact environment-variable names forwarded to Audita |
|
||||||
| `pipeline.audita.modules[]` | list[string] | No | empty |
|
| `pipeline.audita.modules[]` | list[string] | No | unset; omit to use Audita defaults, set `[]` for no modules |
|
||||||
| `pipeline.audita.base_url` | string | No | empty |
|
| `pipeline.audita.overlay` | string | No | empty; no downstream overlay override |
|
||||||
| `pipeline.audita.model` | string | No | empty |
|
| `pipeline.audita.proposal_profile` | string | No | empty; no downstream proposal-profile override |
|
||||||
| `pipeline.audita.total_llm_concurrency` | int | No | unset |
|
| `pipeline.audita.validation_profile` | string | No | empty; no downstream validation-profile override |
|
||||||
| `pipeline.audita.proposal_llm_concurrency` | int | No | unset |
|
|
||||||
| `pipeline.audita.validation_model` | string | No | empty |
|
|
||||||
| `pipeline.audita.validation_llm_concurrency` | int | No | unset |
|
|
||||||
| `pipeline.audita.transcript_description` | string | No | empty |
|
| `pipeline.audita.transcript_description` | string | No | empty |
|
||||||
| `pipeline.audita.config_path` | string | No | empty |
|
| `pipeline.audita.config_path` | string | No | empty |
|
||||||
| `pipeline.audita.output_schema` | string | No | empty |
|
| `pipeline.audita.output_schema` | string | No | empty |
|
||||||
| `pipeline.audita.work_dir_retention` | string | No | empty |
|
|
||||||
| `pipeline.audita.report` | bool | No | `true` |
|
|
||||||
| `pipeline.normalize.output_path` | string | No | `transcripts/final.json` |
|
| `pipeline.normalize.output_path` | string | No | `transcripts/final.json` |
|
||||||
| `pipeline.normalize.output_schema` | string | No | `seriatim-intermediate` |
|
| `pipeline.normalize.output_schema` | string | No | `seriatim-intermediate` |
|
||||||
| `pipeline.normalize.report` | bool | No | `true` |
|
| `pipeline.normalize.report` | bool | No | `true` |
|
||||||
|
|||||||
@@ -274,7 +274,11 @@ credential path remains.
|
|||||||
|
|
||||||
## Stage 4: Cut Audita configuration and adapter over to v2
|
## Stage 4: Cut Audita configuration and adapter over to v2
|
||||||
|
|
||||||
Status: Not started.
|
Status: Complete (2026-09-25).
|
||||||
|
|
||||||
|
Verification: `go test ./internal/config ./internal/adapters/audita ./internal/stage ./internal/app`,
|
||||||
|
`go test -race ./internal/adapters/audita`, `go test ./...`, and
|
||||||
|
`git diff --check` passed.
|
||||||
|
|
||||||
Preparation: Audita v2.0.0 `docs/migrations/v2.md`, `docs/cli.md`,
|
Preparation: Audita v2.0.0 `docs/migrations/v2.md`, `docs/cli.md`,
|
||||||
`docs/integrations/subprocess.md`, and `docs/integrations/output-files.md` in
|
`docs/integrations/subprocess.md`, and `docs/integrations/output-files.md` in
|
||||||
|
|||||||
@@ -93,19 +93,14 @@ audita:
|
|||||||
# Optional overrides; defaults shown explicitly where applicable.
|
# Optional overrides; defaults shown explicitly where applicable.
|
||||||
binary: audita
|
binary: audita
|
||||||
timeout: 3h
|
timeout: 3h
|
||||||
llm_api_key: AUDITA_LLM_API_KEY
|
credentials: [AUDITA_LLM_API_KEY]
|
||||||
modules: [glossary, homophones, spoken_word, grammar]
|
modules: [glossary, homophones, spoken_word, grammar]
|
||||||
base_url: ""
|
overlay: ""
|
||||||
model: ""
|
proposal_profile: ""
|
||||||
total_llm_concurrency: 2
|
validation_profile: ""
|
||||||
proposal_llm_concurrency: 1
|
|
||||||
validation_model: ""
|
|
||||||
validation_llm_concurrency: 1
|
|
||||||
transcript_description: ""
|
transcript_description: ""
|
||||||
config_path: /usr/local/etc/audita/config.yml
|
config_path: /usr/local/etc/audita/config.yml
|
||||||
output_schema: audita-v1
|
output_schema: audita-v1
|
||||||
work_dir_retention: auto
|
|
||||||
report: true
|
|
||||||
|
|
||||||
normalize:
|
normalize:
|
||||||
# Optional; defaults shown explicitly.
|
# Optional; defaults shown explicitly.
|
||||||
|
|||||||
@@ -57,14 +57,9 @@ seriatim:
|
|||||||
audita:
|
audita:
|
||||||
binary: audita
|
binary: audita
|
||||||
timeout: 3h
|
timeout: 3h
|
||||||
llm_api_key: AUDITA_LLM_API_KEY
|
credentials: [AUDITA_LLM_API_KEY]
|
||||||
modules: [glossary, homophones, spoken_word, grammar]
|
modules: [glossary, homophones, spoken_word, grammar]
|
||||||
output_schema: audita-v1
|
output_schema: audita-v1
|
||||||
work_dir_retention: auto
|
|
||||||
total_llm_concurrency: 2
|
|
||||||
proposal_llm_concurrency: 1
|
|
||||||
validation_llm_concurrency: 1
|
|
||||||
report: true
|
|
||||||
|
|
||||||
normalize:
|
normalize:
|
||||||
output_path: transcripts/final.json
|
output_path: transcripts/final.json
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ audita:
|
|||||||
binary: audita
|
binary: audita
|
||||||
modules: [glossary, grammar]
|
modules: [glossary, grammar]
|
||||||
output_schema: audita-v1
|
output_schema: audita-v1
|
||||||
model: narratio-production-model-placeholder
|
proposal_profile: narratio-production-model-placeholder
|
||||||
validation_model: narratio-production-validator-placeholder
|
validation_profile: narratio-production-validator-placeholder
|
||||||
|
|
||||||
normalize:
|
normalize:
|
||||||
output_path: transcripts/final.json
|
output_path: transcripts/final.json
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
audita:
|
audita:
|
||||||
model: narratio-testing-model-placeholder
|
proposal_profile: narratio-testing-model-placeholder
|
||||||
validation_model: narratio-testing-validator-placeholder
|
validation_profile: narratio-testing-validator-placeholder
|
||||||
|
|
||||||
scriptorium:
|
scriptorium:
|
||||||
artifacts:
|
artifacts:
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ func (n *NoopRunner) Run(ctx context.Context, req PolishRequest) (PolishResult,
|
|||||||
}
|
}
|
||||||
return PolishResult{
|
return PolishResult{
|
||||||
ProcessedTranscriptPath: req.OutputProcessedPath,
|
ProcessedTranscriptPath: req.OutputProcessedPath,
|
||||||
ReportPath: req.ReportPath,
|
|
||||||
WorkDir: req.WorkDir,
|
|
||||||
StdoutLogPath: req.StdoutLogPath,
|
StdoutLogPath: req.StdoutLogPath,
|
||||||
StderrLogPath: req.StderrLogPath,
|
StderrLogPath: req.StderrLogPath,
|
||||||
GeneratedConfigPath: req.GeneratedConfigPath,
|
GeneratedConfigPath: req.GeneratedConfigPath,
|
||||||
@@ -54,12 +52,6 @@ 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 == "" {
|
if res.StdoutLogPath == "" {
|
||||||
res.StdoutLogPath = req.StdoutLogPath
|
res.StdoutLogPath = req.StdoutLogPath
|
||||||
}
|
}
|
||||||
@@ -78,7 +70,7 @@ func (f *FakeRunner) Run(ctx context.Context, req PolishRequest) (PolishResult,
|
|||||||
func materializePlaceholders(req PolishRequest) error {
|
func materializePlaceholders(req PolishRequest) error {
|
||||||
if req.GeneratedConfigPath != "" {
|
if req.GeneratedConfigPath != "" {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"schema": "audita.generated.v1",
|
"schema": "audita.generated.v2",
|
||||||
"placeholder": true,
|
"placeholder": true,
|
||||||
"merged_transcript_path": req.MergedTranscriptPath,
|
"merged_transcript_path": req.MergedTranscriptPath,
|
||||||
"output_path": req.OutputProcessedPath,
|
"output_path": req.OutputProcessedPath,
|
||||||
@@ -103,12 +95,6 @@ func materializePlaceholders(req PolishRequest) error {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := writeJSONIfRequested(req.ReportPath, map[string]any{
|
|
||||||
"schema": "audita.report.v1",
|
|
||||||
"steps": []any{},
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,24 +11,29 @@ type Runner interface {
|
|||||||
Run(ctx context.Context, req PolishRequest) (PolishResult, error)
|
Run(ctx context.Context, req PolishRequest) (PolishResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PolishRequest describes an audita invocation.
|
// PolishRequest describes an Audita invocation. Nil Modules uses the runner's
|
||||||
|
// configured selection; a non-nil empty slice selects no modules.
|
||||||
type PolishRequest struct {
|
type PolishRequest struct {
|
||||||
GeneratedConfigPath string
|
GeneratedConfigPath string
|
||||||
MergedTranscriptPath string
|
MergedTranscriptPath string
|
||||||
OutputProcessedPath string
|
OutputProcessedPath string
|
||||||
GlossaryPath string
|
GlossaryPath string
|
||||||
ReportPath string
|
|
||||||
WorkDir string
|
|
||||||
Modules []string
|
Modules []string
|
||||||
StdoutLogPath string
|
StdoutLogPath string
|
||||||
StderrLogPath string
|
StderrLogPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// PolishResult describes a polish output.
|
// ReportBundle contains optional reports adjacent to the requested output.
|
||||||
|
type ReportBundle struct {
|
||||||
|
ErrorsPath string
|
||||||
|
RecoveriesPath string
|
||||||
|
OverviewPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolishResult describes a polish output and available diagnostics.
|
||||||
type PolishResult struct {
|
type PolishResult struct {
|
||||||
ProcessedTranscriptPath string
|
ProcessedTranscriptPath string
|
||||||
ReportPath string
|
Reports ReportBundle
|
||||||
WorkDir string
|
|
||||||
StdoutLogPath string
|
StdoutLogPath string
|
||||||
StderrLogPath string
|
StderrLogPath string
|
||||||
GeneratedConfigPath string
|
GeneratedConfigPath string
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package audita
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"os"
|
||||||
"strconv"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||||
)
|
)
|
||||||
@@ -16,93 +18,27 @@ import (
|
|||||||
// MaxProcessedOutputBytes bounds Audita's processed-transcript JSON result.
|
// MaxProcessedOutputBytes bounds Audita's processed-transcript JSON result.
|
||||||
const MaxProcessedOutputBytes int64 = 64 * 1024 * 1024
|
const MaxProcessedOutputBytes int64 = 64 * 1024 * 1024
|
||||||
|
|
||||||
// MaxReportOutputBytes bounds Audita's optional report JSON result.
|
// MaxReportOutputBytes bounds each optional Audita report.
|
||||||
const MaxReportOutputBytes int64 = 16 * 1024 * 1024
|
const MaxReportOutputBytes int64 = 16 * 1024 * 1024
|
||||||
|
|
||||||
// SubprocessRunnerConfig defines deterministic settings for Audita CLI execution.
|
// SubprocessRunnerConfig defines deterministic settings for Audita CLI execution.
|
||||||
type SubprocessRunnerConfig struct {
|
type SubprocessRunnerConfig struct {
|
||||||
Binary string
|
Binary string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
LLMAPIKey string
|
Credentials []configkit.Secret
|
||||||
LLMAPIKeyReference string
|
|
||||||
Modules []string
|
Modules []string
|
||||||
BaseURL string
|
Overlay string
|
||||||
Model string
|
ProposalProfile string
|
||||||
|
ValidationProfile string
|
||||||
TranscriptDescription string
|
TranscriptDescription string
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
OutputSchema string
|
OutputSchema string
|
||||||
WorkDirRetention string
|
|
||||||
TotalLLMConcurrency *int
|
|
||||||
ProposalLLMConcurrency *int
|
|
||||||
ValidationModel string
|
|
||||||
ValidationLLMConcurrency *int
|
|
||||||
Report bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubprocessRunner invokes Audita via subprocess.
|
// SubprocessRunner invokes Audita via subprocess.
|
||||||
type SubprocessRunner struct {
|
type SubprocessRunner struct {
|
||||||
binary string
|
cfg SubprocessRunnerConfig
|
||||||
timeout time.Duration
|
run func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error)
|
||||||
llmAPIKey string
|
|
||||||
llmAPIKeyReference string
|
|
||||||
modules []string
|
|
||||||
baseURL string
|
|
||||||
model string
|
|
||||||
transcriptDescription string
|
|
||||||
configPath string
|
|
||||||
outputSchema string
|
|
||||||
workDirRetention string
|
|
||||||
totalLLMConcurrency *int
|
|
||||||
proposalLLMConcurrency *int
|
|
||||||
validationModel string
|
|
||||||
validationLLMConcurrency *int
|
|
||||||
report bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSubprocessRunnerFromConfigValues parses config-derived values once.
|
|
||||||
func NewSubprocessRunnerFromConfigValues(
|
|
||||||
binary string,
|
|
||||||
timeout string,
|
|
||||||
llmAPIKey string,
|
|
||||||
llmAPIKeyReference string,
|
|
||||||
modules []string,
|
|
||||||
baseURL string,
|
|
||||||
model string,
|
|
||||||
transcriptDescription string,
|
|
||||||
configPath string,
|
|
||||||
outputSchema string,
|
|
||||||
workDirRetention string,
|
|
||||||
totalLLMConcurrency *int,
|
|
||||||
proposalLLMConcurrency *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,
|
|
||||||
LLMAPIKey: llmAPIKey,
|
|
||||||
LLMAPIKeyReference: llmAPIKeyReference,
|
|
||||||
Modules: modules,
|
|
||||||
BaseURL: baseURL,
|
|
||||||
Model: model,
|
|
||||||
TranscriptDescription: transcriptDescription,
|
|
||||||
ConfigPath: configPath,
|
|
||||||
OutputSchema: outputSchema,
|
|
||||||
WorkDirRetention: workDirRetention,
|
|
||||||
TotalLLMConcurrency: totalLLMConcurrency,
|
|
||||||
ProposalLLMConcurrency: proposalLLMConcurrency,
|
|
||||||
ValidationModel: validationModel,
|
|
||||||
ValidationLLMConcurrency: validationLLMConcurrency,
|
|
||||||
Report: report,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSubprocessRunner constructs a validated Audita subprocess runner.
|
// NewSubprocessRunner constructs a validated Audita subprocess runner.
|
||||||
@@ -113,281 +49,177 @@ func NewSubprocessRunner(cfg SubprocessRunnerConfig) (*SubprocessRunner, error)
|
|||||||
if cfg.Timeout <= 0 {
|
if cfg.Timeout <= 0 {
|
||||||
return nil, fmt.Errorf("audita timeout must be > 0")
|
return nil, fmt.Errorf("audita timeout must be > 0")
|
||||||
}
|
}
|
||||||
if (cfg.LLMAPIKey == "") != (strings.TrimSpace(cfg.LLMAPIKeyReference) == "") {
|
|
||||||
return nil, fmt.Errorf("audita credential value and reference must be provided together")
|
|
||||||
}
|
|
||||||
for i, module := range cfg.Modules {
|
for i, module := range cfg.Modules {
|
||||||
if strings.TrimSpace(module) == "" {
|
if strings.TrimSpace(module) == "" {
|
||||||
return nil, fmt.Errorf("audita module at index %d is empty", i)
|
return nil, fmt.Errorf("audita module at index %d is empty", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.BaseURL) != "" {
|
|
||||||
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 cfg.TotalLLMConcurrency != nil && *cfg.TotalLLMConcurrency <= 0 {
|
|
||||||
return nil, fmt.Errorf("audita total llm concurrency must be > 0 when provided")
|
|
||||||
}
|
|
||||||
if cfg.ProposalLLMConcurrency != nil && *cfg.ProposalLLMConcurrency <= 0 {
|
|
||||||
return nil, fmt.Errorf("audita proposal 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")
|
|
||||||
}
|
|
||||||
switch strings.TrimSpace(cfg.OutputSchema) {
|
switch strings.TrimSpace(cfg.OutputSchema) {
|
||||||
case "", "bare-segments", "audita-v1":
|
case "", "bare-segments", "audita-v1":
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("audita output schema must be one of: bare-segments, audita-v1")
|
return nil, fmt.Errorf("audita output schema must be one of: bare-segments, audita-v1")
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(cfg.WorkDirRetention) {
|
cfg.Binary = strings.TrimSpace(cfg.Binary)
|
||||||
case "", "always", "auto", "never":
|
cfg.Credentials = append([]configkit.Secret(nil), cfg.Credentials...)
|
||||||
default:
|
cfg.Modules = cloneModules(cfg.Modules)
|
||||||
return nil, fmt.Errorf("audita work dir retention must be one of: always, auto, never")
|
for i := range cfg.Modules {
|
||||||
|
cfg.Modules[i] = strings.TrimSpace(cfg.Modules[i])
|
||||||
|
}
|
||||||
|
return &SubprocessRunner{cfg: cfg, run: subprocess.Run}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
modules := make([]string, len(cfg.Modules))
|
func cloneModules(modules []string) []string {
|
||||||
for i, m := range cfg.Modules {
|
if modules == nil {
|
||||||
modules[i] = strings.TrimSpace(m)
|
return nil
|
||||||
|
}
|
||||||
|
return append([]string{}, modules...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SubprocessRunner{
|
// Run executes Audita process and validates its corrected transcript.
|
||||||
binary: strings.TrimSpace(cfg.Binary),
|
|
||||||
timeout: cfg.Timeout,
|
|
||||||
llmAPIKey: cfg.LLMAPIKey,
|
|
||||||
llmAPIKeyReference: strings.TrimSpace(cfg.LLMAPIKeyReference),
|
|
||||||
modules: modules,
|
|
||||||
baseURL: strings.TrimSpace(cfg.BaseURL),
|
|
||||||
model: strings.TrimSpace(cfg.Model),
|
|
||||||
transcriptDescription: strings.TrimSpace(cfg.TranscriptDescription),
|
|
||||||
configPath: strings.TrimSpace(cfg.ConfigPath),
|
|
||||||
outputSchema: strings.TrimSpace(cfg.OutputSchema),
|
|
||||||
workDirRetention: strings.TrimSpace(cfg.WorkDirRetention),
|
|
||||||
totalLLMConcurrency: cfg.TotalLLMConcurrency,
|
|
||||||
proposalLLMConcurrency: cfg.ProposalLLMConcurrency,
|
|
||||||
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) {
|
func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishResult, error) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return PolishResult{}, fmt.Errorf("audita subprocess runner is nil")
|
return PolishResult{}, fmt.Errorf("audita subprocess runner is nil")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.MergedTranscriptPath) == "" {
|
for _, field := range []struct{ name, value string }{
|
||||||
return PolishResult{}, fmt.Errorf("audita merged transcript path is required")
|
{"merged transcript path", req.MergedTranscriptPath},
|
||||||
|
{"glossary path", req.GlossaryPath},
|
||||||
|
{"output processed path", req.OutputProcessedPath},
|
||||||
|
} {
|
||||||
|
if strings.TrimSpace(field.value) == "" {
|
||||||
|
return PolishResult{}, fmt.Errorf("audita %s is required", field.name)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.GlossaryPath) == "" {
|
|
||||||
return PolishResult{}, fmt.Errorf("audita glossary path is required")
|
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.OutputProcessedPath) == "" {
|
modules := req.Modules
|
||||||
return PolishResult{}, fmt.Errorf("audita output processed path is required")
|
if modules == nil {
|
||||||
|
modules = r.cfg.Modules
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.WorkDir) == "" {
|
for i, module := range modules {
|
||||||
return PolishResult{}, fmt.Errorf("audita work dir is required")
|
if strings.TrimSpace(module) == "" {
|
||||||
|
return PolishResult{}, fmt.Errorf("audita module at index %d is empty", i)
|
||||||
}
|
}
|
||||||
if r.report && strings.TrimSpace(req.ReportPath) == "" {
|
|
||||||
return PolishResult{}, fmt.Errorf("audita report is enabled but report path is missing")
|
|
||||||
}
|
}
|
||||||
|
modules = cloneModules(modules)
|
||||||
reqModules := req.Modules
|
for i := range modules {
|
||||||
if reqModules == nil {
|
modules[i] = strings.TrimSpace(modules[i])
|
||||||
reqModules = append([]string(nil), r.modules...)
|
|
||||||
}
|
}
|
||||||
args := r.buildArgs(req, reqModules)
|
args := r.buildArgs(req, modules)
|
||||||
|
env, names, err := subprocess.CredentialEnvironment(r.cfg.Credentials)
|
||||||
credentialPresent := r.llmAPIKeyReference != ""
|
if err != nil {
|
||||||
env := map[string]string{}
|
return PolishResult{}, fmt.Errorf("audita credentials: %w", err)
|
||||||
if credentialPresent {
|
|
||||||
env["AUDITA_LLM_API_KEY"] = r.llmAPIKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.GeneratedConfigPath != "" {
|
if req.GeneratedConfigPath != "" {
|
||||||
if err := r.writeInvocationConfig(req, args, reqModules, credentialPresent); err != nil {
|
if err := r.writeInvocationConfig(req, args, modules, names); err != nil {
|
||||||
return PolishResult{}, fmt.Errorf("write audita invocation config %q: %w", req.GeneratedConfigPath, err)
|
return PolishResult{}, fmt.Errorf("write audita invocation config %q: %w", req.GeneratedConfigPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run := r.run
|
||||||
runRes, err := subprocess.Run(ctx, subprocess.RunRequest{
|
if run == nil {
|
||||||
Executable: r.binary,
|
run = subprocess.Run
|
||||||
Args: args,
|
}
|
||||||
Timeout: r.timeout,
|
runRes, runErr := run(ctx, subprocess.RunRequest{
|
||||||
EnvOverrides: env,
|
Executable: r.cfg.Binary, Args: args, Timeout: r.cfg.Timeout,
|
||||||
DiagnosticOwner: "audita",
|
EnvOverrides: env, SensitiveEnvNames: names, DiagnosticOwner: "audita",
|
||||||
StdoutLogPath: req.StdoutLogPath,
|
StdoutLogPath: req.StdoutLogPath, StderrLogPath: req.StderrLogPath,
|
||||||
StderrLogPath: req.StderrLogPath,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
result := r.result(req, modules, names, runRes)
|
||||||
wrappedMessage := fmt.Sprintf(
|
if runErr != nil {
|
||||||
"run audita process (binary=%q, stdout_log=%q, stderr_log=%q)",
|
result.Reports, _ = discoverReports(req.OutputProcessedPath)
|
||||||
r.binary,
|
return result, fmt.Errorf("run audita process (binary=%q, output_dir=%q, stdout_log=%q, stderr_log=%q): %w",
|
||||||
req.StdoutLogPath,
|
r.cfg.Binary, filepath.Dir(req.OutputProcessedPath), req.StdoutLogPath, req.StderrLogPath, runErr)
|
||||||
req.StderrLogPath,
|
|
||||||
)
|
|
||||||
wrappedMessage = addSubprocessStreamHint(wrappedMessage, err)
|
|
||||||
return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf(
|
|
||||||
"%s: %w",
|
|
||||||
wrappedMessage,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateProcessedOutput(req.OutputProcessedPath); err != nil {
|
if err := validateProcessedOutput(req.OutputProcessedPath); err != nil {
|
||||||
return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf("validate audita processed output %q: %w", req.OutputProcessedPath, err)
|
result.Reports, _ = discoverReports(req.OutputProcessedPath)
|
||||||
|
return result, fmt.Errorf("validate audita processed output %q: %w", req.OutputProcessedPath, err)
|
||||||
}
|
}
|
||||||
if r.report {
|
reports, err := discoverReports(req.OutputProcessedPath)
|
||||||
if err := validateJSONFile(req.ReportPath); err != nil {
|
if err != nil {
|
||||||
return r.failureResult(req, reqModules, runRes, credentialPresent), fmt.Errorf("validate audita report output %q: %w", req.ReportPath, err)
|
return result, err
|
||||||
}
|
}
|
||||||
|
result.Reports = reports
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *SubprocessRunner) result(req PolishRequest, modules, names []string, runRes subprocess.RunResult) PolishResult {
|
||||||
return PolishResult{
|
return PolishResult{
|
||||||
ProcessedTranscriptPath: req.OutputProcessedPath,
|
ProcessedTranscriptPath: req.OutputProcessedPath,
|
||||||
ReportPath: req.ReportPath,
|
StdoutLogPath: req.StdoutLogPath, StderrLogPath: req.StderrLogPath,
|
||||||
WorkDir: req.WorkDir,
|
GeneratedConfigPath: req.GeneratedConfigPath, ExitCode: runRes.ExitCode,
|
||||||
StdoutLogPath: req.StdoutLogPath,
|
Duration: runRes.Duration, InvokedBinary: r.cfg.Binary,
|
||||||
StderrLogPath: req.StderrLogPath,
|
|
||||||
GeneratedConfigPath: req.GeneratedConfigPath,
|
|
||||||
ExitCode: runRes.ExitCode,
|
|
||||||
Duration: runRes.Duration,
|
|
||||||
InvokedBinary: r.binary,
|
|
||||||
Metadata: map[string]any{
|
Metadata: map[string]any{
|
||||||
"adapter": "audita_subprocess",
|
"adapter": "audita_subprocess", "modules": modules,
|
||||||
"modules": reqModules,
|
"overlay": r.cfg.Overlay, "proposal_profile": r.cfg.ProposalProfile,
|
||||||
"base_url": r.baseURL,
|
"validation_profile": r.cfg.ValidationProfile,
|
||||||
"model": r.model,
|
"transcript_description": r.cfg.TranscriptDescription,
|
||||||
"transcript_description": r.transcriptDescription,
|
"config_path": r.cfg.ConfigPath, "output_schema": r.cfg.OutputSchema,
|
||||||
"config_path": r.configPath,
|
"credential_names": names,
|
||||||
"output_schema": r.outputSchema,
|
|
||||||
"work_dir_retention": r.workDirRetention,
|
|
||||||
"validation_model": r.validationModel,
|
|
||||||
"total_llm_concurrency": r.totalLLMConcurrency,
|
|
||||||
"proposal_llm_concurrency": r.proposalLLMConcurrency,
|
|
||||||
"validation_llm_concurrency": r.validationLLMConcurrency,
|
|
||||||
"credential_env_var": r.llmAPIKeyReference,
|
|
||||||
"credential_present": credentialPresent,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SubprocessRunner) failureResult(req PolishRequest, modules []string, runRes subprocess.RunResult, credentialPresent 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,
|
|
||||||
"transcript_description": r.transcriptDescription,
|
|
||||||
"config_path": r.configPath,
|
|
||||||
"output_schema": r.outputSchema,
|
|
||||||
"work_dir_retention": r.workDirRetention,
|
|
||||||
"validation_model": r.validationModel,
|
|
||||||
"total_llm_concurrency": r.totalLLMConcurrency,
|
|
||||||
"proposal_llm_concurrency": r.proposalLLMConcurrency,
|
|
||||||
"validation_llm_concurrency": r.validationLLMConcurrency,
|
|
||||||
"credential_env_var": r.llmAPIKeyReference,
|
|
||||||
"credential_present": credentialPresent,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SubprocessRunner) buildArgs(req PolishRequest, modules []string) []string {
|
func (r *SubprocessRunner) buildArgs(req PolishRequest, modules []string) []string {
|
||||||
args := []string{
|
args := []string{"process", req.MergedTranscriptPath, "--glossary", req.GlossaryPath, "--output", req.OutputProcessedPath}
|
||||||
"process",
|
if modules != nil {
|
||||||
req.MergedTranscriptPath,
|
|
||||||
"--glossary", req.GlossaryPath,
|
|
||||||
"--output", req.OutputProcessedPath,
|
|
||||||
"--work-dir", req.WorkDir,
|
|
||||||
}
|
|
||||||
if r.baseURL != "" {
|
|
||||||
args = append(args, "--base-url", r.baseURL)
|
|
||||||
}
|
|
||||||
if r.model != "" {
|
|
||||||
args = append(args, "--model", r.model)
|
|
||||||
}
|
|
||||||
if len(modules) > 0 {
|
|
||||||
args = append(args, "--modules", strings.Join(modules, ","))
|
args = append(args, "--modules", strings.Join(modules, ","))
|
||||||
}
|
}
|
||||||
if r.report {
|
for _, setting := range []struct{ flag, value string }{
|
||||||
args = append(args, "--report-json", req.ReportPath)
|
{"--config", r.cfg.ConfigPath}, {"--overlay", r.cfg.Overlay},
|
||||||
|
{"--proposal-profile", r.cfg.ProposalProfile}, {"--validation-profile", r.cfg.ValidationProfile},
|
||||||
|
{"--transcript-description", r.cfg.TranscriptDescription}, {"--output-schema", r.cfg.OutputSchema},
|
||||||
|
} {
|
||||||
|
if setting.value != "" {
|
||||||
|
args = append(args, setting.flag, setting.value)
|
||||||
}
|
}
|
||||||
if r.transcriptDescription != "" {
|
|
||||||
args = append(args, "--transcript-description", r.transcriptDescription)
|
|
||||||
}
|
|
||||||
if r.configPath != "" {
|
|
||||||
args = append(args, "--config", r.configPath)
|
|
||||||
}
|
|
||||||
if r.outputSchema != "" {
|
|
||||||
args = append(args, "--output-schema", r.outputSchema)
|
|
||||||
}
|
|
||||||
if r.workDirRetention != "" {
|
|
||||||
args = append(args, "--work-dir-retention", r.workDirRetention)
|
|
||||||
}
|
|
||||||
if r.totalLLMConcurrency != nil {
|
|
||||||
args = append(args, "--total-llm-concurrency", strconv.Itoa(*r.totalLLMConcurrency))
|
|
||||||
}
|
|
||||||
if r.proposalLLMConcurrency != nil {
|
|
||||||
args = append(args, "--proposal-llm-concurrency", strconv.Itoa(*r.proposalLLMConcurrency))
|
|
||||||
}
|
|
||||||
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
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SubprocessRunner) writeInvocationConfig(req PolishRequest, args []string, modules []string, credentialPresent bool) error {
|
func (r *SubprocessRunner) writeInvocationConfig(req PolishRequest, args, modules, names []string) error {
|
||||||
payload := map[string]any{
|
return writeGeneratedYAML(req.GeneratedConfigPath, map[string]any{
|
||||||
"schema": "audita.generated.v1",
|
"schema": "audita.generated.v2", "binary": r.cfg.Binary, "args": args,
|
||||||
"binary": r.binary,
|
"timeout": r.cfg.Timeout.String(), "modules": modules,
|
||||||
"args": args,
|
"overlay": r.cfg.Overlay, "proposal_profile": r.cfg.ProposalProfile,
|
||||||
"timeout": r.timeout.String(),
|
"validation_profile": r.cfg.ValidationProfile,
|
||||||
"modules": modules,
|
"transcript_description": r.cfg.TranscriptDescription,
|
||||||
"base_url": r.baseURL,
|
"config_path": r.cfg.ConfigPath, "output_schema": r.cfg.OutputSchema,
|
||||||
"model": r.model,
|
"merged_transcript_path": req.MergedTranscriptPath, "glossary_path": req.GlossaryPath,
|
||||||
"transcript_description": r.transcriptDescription,
|
"output_path": req.OutputProcessedPath, "credential_names": names,
|
||||||
"config_path": r.configPath,
|
})
|
||||||
"output_schema": r.outputSchema,
|
|
||||||
"work_dir_retention": r.workDirRetention,
|
|
||||||
"validation_model": r.validationModel,
|
|
||||||
"total_llm_concurrency": r.totalLLMConcurrency,
|
|
||||||
"proposal_llm_concurrency": r.proposalLLMConcurrency,
|
|
||||||
"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.llmAPIKeyReference,
|
|
||||||
"credential_present": credentialPresent,
|
|
||||||
}
|
}
|
||||||
return writeGeneratedYAML(req.GeneratedConfigPath, payload)
|
|
||||||
|
func discoverReports(outputPath string) (ReportBundle, error) {
|
||||||
|
var reports ReportBundle
|
||||||
|
for _, report := range []struct {
|
||||||
|
name string
|
||||||
|
dest *string
|
||||||
|
}{
|
||||||
|
{"errors.yml", &reports.ErrorsPath},
|
||||||
|
{"recoveries.yml", &reports.RecoveriesPath},
|
||||||
|
{"report.md", &reports.OverviewPath},
|
||||||
|
} {
|
||||||
|
path := filepath.Join(filepath.Dir(outputPath), report.name)
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return reports, fmt.Errorf("inspect audita report %q: %w", path, err)
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return reports, fmt.Errorf("audita report %q is not a regular file", path)
|
||||||
|
}
|
||||||
|
if _, err := fileops.ReadFileBounded(path, MaxReportOutputBytes); err != nil {
|
||||||
|
return reports, fmt.Errorf("audita report %q exceeds or cannot be read within %d-byte limit: %w", path, MaxReportOutputBytes, err)
|
||||||
|
}
|
||||||
|
*report.dest = path
|
||||||
|
}
|
||||||
|
return reports, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateProcessedOutput(path string) error {
|
func validateProcessedOutput(path string) error {
|
||||||
data, err := readAuditaResult(path, MaxProcessedOutputBytes, "processed transcript")
|
data, err := fileops.ReadFileBounded(path, MaxProcessedOutputBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("audita processed transcript result exceeds or cannot be read within %d-byte limit: %w", MaxProcessedOutputBytes, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload map[string]any
|
var payload map[string]any
|
||||||
if err := json.Unmarshal(data, &payload); err != nil {
|
if err := json.Unmarshal(data, &payload); err != nil {
|
||||||
return fmt.Errorf("parse json: %w", err)
|
return fmt.Errorf("parse json: %w", err)
|
||||||
@@ -401,34 +233,3 @@ func validateProcessedOutput(path string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSubprocessStreamHint(message string, runErr error) string {
|
|
||||||
if runErr == nil {
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
lower := strings.ToLower(runErr.Error())
|
|
||||||
if strings.Contains(lower, "bad file descriptor") || strings.Contains(lower, "exit code 120") {
|
|
||||||
return message + "; hint=audita child process may have started with invalid stderr/stdout descriptors"
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateJSONFile(path string) error {
|
|
||||||
data, err := readAuditaResult(path, MaxReportOutputBytes, "report")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var v any
|
|
||||||
if err := json.Unmarshal(data, &v); err != nil {
|
|
||||||
return fmt.Errorf("parse json: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readAuditaResult(path string, limit int64, category string) ([]byte, error) {
|
|
||||||
data, err := fileops.ReadFileBounded(path, limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("audita %s result exceeds or cannot be read within %d-byte limit: %w", category, limit, err)
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,630 +2,262 @@ package audita
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
|
func auditaTestRequest(t *testing.T) PolishRequest {
|
||||||
|
t.Helper()
|
||||||
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)
|
|
||||||
totalLLMConcurrency := 3
|
|
||||||
proposalLLMConcurrency := 2
|
|
||||||
validationLLMConcurrency := 2
|
|
||||||
runner, err := NewSubprocessRunner(SubprocessRunnerConfig{
|
|
||||||
Binary: wrapper,
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "super-secret",
|
|
||||||
LLMAPIKeyReference: "OPENAI_KEY_SOURCE",
|
|
||||||
Modules: []string{"glossary", "homophones", "glossary"},
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
TranscriptDescription: "Campaign Session 42",
|
|
||||||
ConfigPath: "/etc/audita/config.yml",
|
|
||||||
OutputSchema: "audita-v1",
|
|
||||||
WorkDirRetention: "auto",
|
|
||||||
TotalLLMConcurrency: &totalLLMConcurrency,
|
|
||||||
ProposalLLMConcurrency: &proposalLLMConcurrency,
|
|
||||||
ValidationModel: "openrouter/google/gemma-4-31b-it",
|
|
||||||
ValidationLLMConcurrency: &validationLLMConcurrency,
|
|
||||||
Report: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewSubprocessRunner() error = %v", err)
|
|
||||||
}
|
|
||||||
t.Setenv("OPENAI_KEY_SOURCE", "changed-after-resolution")
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
req := PolishRequest{
|
return PolishRequest{
|
||||||
GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"),
|
MergedTranscriptPath: filepath.Join(dir, "merged.json"),
|
||||||
MergedTranscriptPath: filepath.Join(dir, "base.json"),
|
|
||||||
GlossaryPath: filepath.Join(dir, "glossary.yml"),
|
GlossaryPath: filepath.Join(dir, "glossary.yml"),
|
||||||
OutputProcessedPath: filepath.Join(dir, "polished.json"),
|
OutputProcessedPath: filepath.Join(dir, "polished.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,
|
|
||||||
"--work-dir", req.WorkDir,
|
|
||||||
"--base-url", "https://openrouter.ai/api/v1",
|
|
||||||
"--model", "openrouter/google/gemma-4-31b-it",
|
|
||||||
"--modules", "glossary,homophones,glossary",
|
|
||||||
"--report-json", req.ReportPath,
|
|
||||||
"--transcript-description", "Campaign Session 42",
|
|
||||||
"--config", "/etc/audita/config.yml",
|
|
||||||
"--output-schema", "audita-v1",
|
|
||||||
"--work-dir-retention", "auto",
|
|
||||||
"--total-llm-concurrency", "3",
|
|
||||||
"--proposal-llm-concurrency", "2",
|
|
||||||
"--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"] != "" {
|
|
||||||
t.Fatalf("AUDITA_LLM_CONCURRENCY = %q, want empty/omitted", 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 TestSubprocessRunnerRejectsIncompleteResolvedCredential(t *testing.T) {
|
|
||||||
_, err := NewSubprocessRunner(SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKeyReference: "MISSING_AUDITA_KEY",
|
|
||||||
})
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "credential value and reference must be provided together") {
|
|
||||||
t.Fatalf("NewSubprocessRunner() error = %v, want credential pair error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubprocessRunnerUnconfiguredCredentialEnvOmitsCredential(t *testing.T) {
|
|
||||||
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
|
||||||
t.Setenv("AUDITA_HELPER_MODE", "success")
|
|
||||||
recordPath := filepath.Join(t.TempDir(), "record.json")
|
|
||||||
t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath)
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "",
|
|
||||||
Modules: []string{"glossary"},
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
Report: false,
|
|
||||||
})
|
|
||||||
req := auditaReqForTest(t, false)
|
|
||||||
|
|
||||||
res, err := runner.Run(context.Background(), req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Run() error = %v", err)
|
|
||||||
}
|
|
||||||
if res.Metadata["credential_present"] != false {
|
|
||||||
t.Fatalf("credential_present = %#v, want false", res.Metadata["credential_present"])
|
|
||||||
}
|
|
||||||
rec := readAuditaHelperRecord(t, recordPath)
|
|
||||||
if rec.Env["AUDITA_LLM_API_KEY"] != "" {
|
|
||||||
t.Fatalf("AUDITA_LLM_API_KEY = %q, want omitted/empty", rec.Env["AUDITA_LLM_API_KEY"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubprocessRunnerOmitsUnspecifiedParentEnvironment(t *testing.T) {
|
|
||||||
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
|
||||||
t.Setenv("AUDITA_HELPER_MODE", "success")
|
|
||||||
t.Setenv("AUDITA_INHERITED_MARKER", "inherited-from-parent")
|
|
||||||
recordPath := filepath.Join(t.TempDir(), "record.json")
|
|
||||||
t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath)
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
Modules: []string{"glossary"},
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
Report: false,
|
|
||||||
LLMAPIKey: "",
|
|
||||||
})
|
|
||||||
req := auditaReqForTest(t, false)
|
|
||||||
if _, err := runner.Run(context.Background(), req); err != nil {
|
|
||||||
t.Fatalf("Run() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rec := readAuditaHelperRecord(t, recordPath)
|
|
||||||
if rec.Env["AUDITA_INHERITED_MARKER"] != "" {
|
|
||||||
t.Fatalf("AUDITA_INHERITED_MARKER = %q, want omitted from the child environment", rec.Env["AUDITA_INHERITED_MARKER"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubprocessRunnerOmitsModulesFlagWhenNotConfigured(t *testing.T) {
|
|
||||||
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
|
||||||
t.Setenv("AUDITA_HELPER_MODE", "success")
|
|
||||||
recordPath := filepath.Join(t.TempDir(), "record.json")
|
|
||||||
t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath)
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "",
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
Report: false,
|
|
||||||
})
|
|
||||||
req := auditaReqForTest(t, false)
|
|
||||||
if _, err := runner.Run(context.Background(), req); err != nil {
|
|
||||||
t.Fatalf("Run() error = %v", err)
|
|
||||||
}
|
|
||||||
rec := readAuditaHelperRecord(t, recordPath)
|
|
||||||
for i := 0; i < len(rec.Args); i++ {
|
|
||||||
if rec.Args[i] == "--modules" {
|
|
||||||
t.Fatalf("args contained --modules unexpectedly: %#v", rec.Args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubprocessRunnerOmitsBaseURLAndModelFlagsWhenNotConfigured(t *testing.T) {
|
|
||||||
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
|
||||||
t.Setenv("AUDITA_HELPER_MODE", "success")
|
|
||||||
recordPath := filepath.Join(t.TempDir(), "record.json")
|
|
||||||
t.Setenv("AUDITA_HELPER_RECORD_PATH", recordPath)
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "",
|
|
||||||
Report: false,
|
|
||||||
})
|
|
||||||
req := auditaReqForTest(t, false)
|
|
||||||
if _, err := runner.Run(context.Background(), req); err != nil {
|
|
||||||
t.Fatalf("Run() error = %v", err)
|
|
||||||
}
|
|
||||||
rec := readAuditaHelperRecord(t, recordPath)
|
|
||||||
for i := 0; i < len(rec.Args); i++ {
|
|
||||||
if rec.Args[i] == "--base-url" {
|
|
||||||
t.Fatalf("args contained --base-url unexpectedly: %#v", rec.Args)
|
|
||||||
}
|
|
||||||
if rec.Args[i] == "--model" {
|
|
||||||
t.Fatalf("args contained --model unexpectedly: %#v", rec.Args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
|
|
||||||
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"))
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "super-secret",
|
|
||||||
LLMAPIKeyReference: "OPENAI_KEY_SOURCE",
|
|
||||||
Modules: []string{"glossary"},
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), req.StdoutLogPath) || !strings.Contains(err.Error(), req.StderrLogPath) {
|
|
||||||
t.Fatalf("error = %q, want stdout/stderr log paths", err.Error())
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "super-secret") {
|
|
||||||
t.Fatalf("failure diagnostic exposed credential value: %q", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubprocessRunnerSubprocessFailureAddsStderrDescriptorHint(t *testing.T) {
|
|
||||||
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
|
||||||
t.Setenv("AUDITA_HELPER_MODE", "fail_bad_fd")
|
|
||||||
t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
|
|
||||||
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "super-secret",
|
|
||||||
LLMAPIKeyReference: "OPENAI_KEY_SOURCE",
|
|
||||||
Modules: []string{"glossary"},
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
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(), "hint=audita child process may have started with invalid stderr/stdout descriptors") {
|
|
||||||
t.Fatalf("error = %q, want stderr descriptor hint", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubprocessRunnerMissingOutputFails(t *testing.T) {
|
|
||||||
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"))
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "super-secret",
|
|
||||||
LLMAPIKeyReference: "OPENAI_KEY_SOURCE",
|
|
||||||
Modules: []string{"glossary"},
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
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) {
|
|
||||||
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"))
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "super-secret",
|
|
||||||
LLMAPIKeyReference: "OPENAI_KEY_SOURCE",
|
|
||||||
Modules: []string{"glossary"},
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
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) {
|
|
||||||
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"))
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "super-secret",
|
|
||||||
LLMAPIKeyReference: "OPENAI_KEY_SOURCE",
|
|
||||||
Modules: []string{"glossary"},
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
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) {
|
|
||||||
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"))
|
|
||||||
|
|
||||||
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
|
||||||
Binary: writeAuditaHelperWrapper(t),
|
|
||||||
Timeout: mustParseAuditaDuration(t, "2s"),
|
|
||||||
LLMAPIKey: "super-secret",
|
|
||||||
LLMAPIKeyReference: "OPENAI_KEY_SOURCE",
|
|
||||||
Modules: []string{"glossary"},
|
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
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", "synthetic-secret", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", "", "", "", "", nil, nil, "", nil, true)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected binary validation error")
|
|
||||||
}
|
|
||||||
_, err = NewSubprocessRunnerFromConfigValues("audita", "bad", "synthetic-secret", "AUDITA_LLM_API_KEY", []string{"glossary"}, "https://openrouter.ai/api/v1", "openrouter/google/gemma-4-31b-it", "", "", "", "", nil, nil, "", nil, true)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected timeout parse error")
|
|
||||||
}
|
|
||||||
_, err = NewSubprocessRunner(SubprocessRunnerConfig{
|
|
||||||
Binary: "audita",
|
|
||||||
Timeout: mustParseAuditaDuration(t, "1s"),
|
|
||||||
LLMAPIKey: "synthetic-secret",
|
|
||||||
LLMAPIKeyReference: "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"),
|
|
||||||
"AUDITA_INHERITED_MARKER": os.Getenv("AUDITA_INHERITED_MARKER"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
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 "fail_bad_fd":
|
|
||||||
_, _ = os.Stderr.WriteString("OSError: [Errno 9] Bad file descriptor\n")
|
|
||||||
os.Exit(120)
|
|
||||||
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, "base.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"),
|
GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"),
|
||||||
MergedTranscriptPath: merged,
|
StdoutLogPath: filepath.Join(dir, "stdout.log"),
|
||||||
GlossaryPath: glossary,
|
StderrLogPath: filepath.Join(dir, "stderr.log"),
|
||||||
OutputProcessedPath: filepath.Join(dir, "polished.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 {
|
func auditaTestRunner(t *testing.T, cfg SubprocessRunnerConfig) *SubprocessRunner {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
exe, err := os.Executable()
|
if cfg.Binary == "" {
|
||||||
|
cfg.Binary = "audita"
|
||||||
|
}
|
||||||
|
if cfg.Timeout == 0 {
|
||||||
|
cfg.Timeout = time.Second
|
||||||
|
}
|
||||||
|
runner, err := NewSubprocessRunner(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("os.Executable() error = %v", err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
path := filepath.Join(t.TempDir(), "audita-helper-wrapper.sh")
|
return runner
|
||||||
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) {
|
func auditaTestSecret(t *testing.T, name, value string) configkit.Secret {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
t.Setenv(name, value)
|
||||||
t.Fatalf("MkdirAll(%q): %v", path, err)
|
type loaded struct {
|
||||||
|
Credential configkit.Secret `yaml:"credential"`
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
result, err := configkit.Load(configkit.Bytes("credential.yml", []byte("credential: "+name+"\n")), configkit.Options[loaded]{})
|
||||||
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 {
|
if err != nil {
|
||||||
t.Fatalf("ReadFile(%q): %v", path, err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var rec auditaHelperRecord
|
return result.Config.Credential
|
||||||
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) {
|
func writeAuditaTestFile(t *testing.T, path, value string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
data, err := os.ReadFile(path)
|
if err := os.WriteFile(path, []byte(value), 0o600); err != nil {
|
||||||
if err != nil {
|
t.Fatal(err)
|
||||||
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 {
|
func TestSubprocessRunnerUsesV2InvocationAndCapturedCredentials(t *testing.T) {
|
||||||
t.Helper()
|
const first = "NARRATIO_AUDITA_CUSTOM_ONE"
|
||||||
d, err := time.ParseDuration(value)
|
const second = "NARRATIO_AUDITA_CUSTOM_TWO"
|
||||||
|
firstSecret := auditaTestSecret(t, first, "loaded-one")
|
||||||
|
secondSecret := auditaTestSecret(t, second, "loaded-two")
|
||||||
|
t.Setenv(first, "parent-changed")
|
||||||
|
t.Setenv(second, "parent-changed-too")
|
||||||
|
t.Setenv("NARRATIO_AUDITA_UNSELECTED", "not-forwarded")
|
||||||
|
runner := auditaTestRunner(t, SubprocessRunnerConfig{
|
||||||
|
Credentials: []configkit.Secret{firstSecret, secondSecret},
|
||||||
|
Modules: []string{"grammar", "glossary", "grammar"},
|
||||||
|
Overlay: "testing", ProposalProfile: "proposal", ValidationProfile: "validation",
|
||||||
|
TranscriptDescription: "campaign", ConfigPath: "/etc/audita/config.yml", OutputSchema: "audita-v1",
|
||||||
|
})
|
||||||
|
req := auditaTestRequest(t)
|
||||||
|
var captured subprocess.RunRequest
|
||||||
|
runner.run = func(_ context.Context, processReq subprocess.RunRequest) (subprocess.RunResult, error) {
|
||||||
|
captured = processReq
|
||||||
|
writeAuditaTestFile(t, req.OutputProcessedPath, `{"segments":[]}`)
|
||||||
|
for _, name := range []string{"errors.yml", "recoveries.yml", "report.md"} {
|
||||||
|
writeAuditaTestFile(t, filepath.Join(filepath.Dir(req.OutputProcessedPath), name), "recoverable diagnostic\n")
|
||||||
|
}
|
||||||
|
return subprocess.RunResult{}, nil
|
||||||
|
}
|
||||||
|
result, err := runner.Run(context.Background(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("time.ParseDuration(%q) error = %v", value, err)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []string{"process", req.MergedTranscriptPath, "--glossary", req.GlossaryPath, "--output", req.OutputProcessedPath,
|
||||||
|
"--modules", "grammar,glossary,grammar", "--config", "/etc/audita/config.yml", "--overlay", "testing",
|
||||||
|
"--proposal-profile", "proposal", "--validation-profile", "validation", "--transcript-description", "campaign", "--output-schema", "audita-v1"}
|
||||||
|
if !reflect.DeepEqual(captured.Args, want) {
|
||||||
|
t.Fatalf("args = %#v, want %#v", captured.Args, want)
|
||||||
|
}
|
||||||
|
if len(captured.EnvOverrides) != 2 || captured.EnvOverrides[first] != "loaded-one" || captured.EnvOverrides[second] != "loaded-two" || captured.EnvOverrides["NARRATIO_AUDITA_UNSELECTED"] != "" {
|
||||||
|
t.Fatalf("credential forwarding = %#v", captured.EnvOverrides)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(captured.SensitiveEnvNames, []string{first, second}) {
|
||||||
|
t.Fatalf("sensitive names = %#v", captured.SensitiveEnvNames)
|
||||||
|
}
|
||||||
|
if result.Reports.ErrorsPath == "" || result.Reports.RecoveriesPath == "" || result.Reports.OverviewPath == "" {
|
||||||
|
t.Fatalf("reports = %#v", result.Reports)
|
||||||
|
}
|
||||||
|
generated, err := os.ReadFile(req.GeneratedConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(generated), "audita.generated.v2") || !strings.Contains(string(generated), first) || strings.Contains(string(generated), "loaded-one") {
|
||||||
|
t.Fatalf("unsafe or incomplete generated invocation: %s", generated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerModulePresenceAndReportAbsence(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
configured []string
|
||||||
|
requested []string
|
||||||
|
wantFlag bool
|
||||||
|
wantValue string
|
||||||
|
}{
|
||||||
|
{"omitted", nil, nil, false, ""},
|
||||||
|
{"configured", []string{"grammar", "grammar"}, nil, true, "grammar,grammar"},
|
||||||
|
{"empty override", []string{"grammar"}, []string{}, true, ""},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
runner := auditaTestRunner(t, SubprocessRunnerConfig{Modules: test.configured})
|
||||||
|
req := auditaTestRequest(t)
|
||||||
|
req.Modules = test.requested
|
||||||
|
runner.run = func(_ context.Context, processReq subprocess.RunRequest) (subprocess.RunResult, error) {
|
||||||
|
index := -1
|
||||||
|
for i, arg := range processReq.Args {
|
||||||
|
if arg == "--modules" {
|
||||||
|
index = i
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(arg, "--report") || arg == "--debug" || arg == "--work-dir" {
|
||||||
|
t.Fatalf("retired or unwanted flag %q", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index >= 0) != test.wantFlag || (index >= 0 && processReq.Args[index+1] != test.wantValue) {
|
||||||
|
t.Fatalf("args = %#v", processReq.Args)
|
||||||
|
}
|
||||||
|
writeAuditaTestFile(t, req.OutputProcessedPath, `{"segments":[]}`)
|
||||||
|
return subprocess.RunResult{}, nil
|
||||||
|
}
|
||||||
|
result, err := runner.Run(context.Background(), req)
|
||||||
|
if err != nil || result.Reports != (ReportBundle{}) {
|
||||||
|
t.Fatalf("result = %#v, error = %v", result, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerRejectsMissingOrMalformedTranscript(t *testing.T) {
|
||||||
|
for _, output := range []string{"", `{"segments":"wrong"}`} {
|
||||||
|
t.Run(output, func(t *testing.T) {
|
||||||
|
runner := auditaTestRunner(t, SubprocessRunnerConfig{})
|
||||||
|
req := auditaTestRequest(t)
|
||||||
|
runner.run = func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error) {
|
||||||
|
if output != "" {
|
||||||
|
writeAuditaTestFile(t, req.OutputProcessedPath, output)
|
||||||
|
}
|
||||||
|
return subprocess.RunResult{}, nil
|
||||||
|
}
|
||||||
|
if _, err := runner.Run(context.Background(), req); err == nil {
|
||||||
|
t.Fatal("missing or malformed transcript was accepted")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerPreservesFailureAndAvailableReports(t *testing.T) {
|
||||||
|
runner := auditaTestRunner(t, SubprocessRunnerConfig{})
|
||||||
|
req := auditaTestRequest(t)
|
||||||
|
processFailure := errors.New("audita exited after writing output")
|
||||||
|
runner.run = func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error) {
|
||||||
|
writeAuditaTestFile(t, req.OutputProcessedPath, `{"segments":[]}`)
|
||||||
|
writeAuditaTestFile(t, filepath.Join(filepath.Dir(req.OutputProcessedPath), "errors.yml"), "status: failed\n")
|
||||||
|
return subprocess.RunResult{ExitCode: 7}, processFailure
|
||||||
|
}
|
||||||
|
result, err := runner.Run(context.Background(), req)
|
||||||
|
if !errors.Is(err, processFailure) || !strings.Contains(err.Error(), filepath.Dir(req.OutputProcessedPath)) {
|
||||||
|
t.Fatalf("error = %v", err)
|
||||||
|
}
|
||||||
|
if result.Reports.ErrorsPath == "" || result.Reports.RecoveriesPath != "" || result.Reports.OverviewPath != "" {
|
||||||
|
t.Fatalf("reports = %#v", result.Reports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerDoesNotMaskProcessFailureWithReportError(t *testing.T) {
|
||||||
|
runner := auditaTestRunner(t, SubprocessRunnerConfig{})
|
||||||
|
req := auditaTestRequest(t)
|
||||||
|
processFailure := errors.New("audita process failed")
|
||||||
|
runner.run = func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error) {
|
||||||
|
path := filepath.Join(filepath.Dir(req.OutputProcessedPath), "errors.yml")
|
||||||
|
if err := os.WriteFile(path, make([]byte, MaxReportOutputBytes+1), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return subprocess.RunResult{ExitCode: 7}, processFailure
|
||||||
|
}
|
||||||
|
if _, err := runner.Run(context.Background(), req); !errors.Is(err, processFailure) {
|
||||||
|
t.Fatalf("error = %v, want original process error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerBoundsAdjacentReports(t *testing.T) {
|
||||||
|
runner := auditaTestRunner(t, SubprocessRunnerConfig{})
|
||||||
|
req := auditaTestRequest(t)
|
||||||
|
runner.run = func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error) {
|
||||||
|
writeAuditaTestFile(t, req.OutputProcessedPath, `{"segments":[]}`)
|
||||||
|
path := filepath.Join(filepath.Dir(req.OutputProcessedPath), "errors.yml")
|
||||||
|
if err := os.WriteFile(path, make([]byte, MaxReportOutputBytes+1), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return subprocess.RunResult{}, nil
|
||||||
|
}
|
||||||
|
if _, err := runner.Run(context.Background(), req); err == nil || !strings.Contains(err.Error(), "errors.yml") || !strings.Contains(err.Error(), "16777216") {
|
||||||
|
t.Fatalf("error = %v, want named report limit", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerRedactsCustomCredentialStreams(t *testing.T) {
|
||||||
|
const first = "NARRATIO_AUDITA_STREAM_ONE"
|
||||||
|
const second = "NARRATIO_AUDITA_STREAM_TWO"
|
||||||
|
for _, fail := range []bool{false, true} {
|
||||||
|
t.Run(map[bool]string{false: "success", true: "failure"}[fail], func(t *testing.T) {
|
||||||
|
firstSecret := auditaTestSecret(t, first, "audita-first-secret")
|
||||||
|
secondSecret := auditaTestSecret(t, second, "audita-second-secret")
|
||||||
|
req := auditaTestRequest(t)
|
||||||
|
body := "#!/bin/sh\nprintf '%s\\n' \"$" + first + "\"\nprintf '%s\\n' \"$" + second + "\" >&2\n"
|
||||||
|
if fail {
|
||||||
|
body += "exit 7\n"
|
||||||
|
} else {
|
||||||
|
body += "printf '{\"segments\":[]}' > \"" + req.OutputProcessedPath + "\"\n"
|
||||||
|
}
|
||||||
|
binary := filepath.Join(t.TempDir(), "audita-helper")
|
||||||
|
writeAuditaTestFile(t, binary, body)
|
||||||
|
if err := os.Chmod(binary, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
runner := auditaTestRunner(t, SubprocessRunnerConfig{Binary: binary, Credentials: []configkit.Secret{firstSecret, secondSecret}})
|
||||||
|
_, err := runner.Run(context.Background(), req)
|
||||||
|
if (err != nil) != fail {
|
||||||
|
t.Fatalf("error = %v, fail = %v", err, fail)
|
||||||
|
}
|
||||||
|
for _, path := range []string{req.StdoutLogPath, req.StderrLogPath} {
|
||||||
|
data, readErr := os.ReadFile(path)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Fatal(readErr)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(data), "audita-first-secret") || strings.Contains(string(data), "audita-second-secret") || !strings.Contains(string(data), "<redacted>") {
|
||||||
|
t.Fatalf("unredacted stream %q: %s", path, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fail && (strings.Contains(err.Error(), "audita-first-secret") || strings.Contains(err.Error(), "audita-second-secret")) {
|
||||||
|
t.Fatalf("unredacted failure: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return d
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,8 +272,8 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
|
|||||||
}, &diff); err != nil {
|
}, &diff); err != nil {
|
||||||
t.Fatalf("compare profiles: %v", err)
|
t.Fatalf("compare profiles: %v", err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(diff.String(), "audita.model") || !strings.Contains(diff.String(), "character_items_arannis.profile_id") {
|
if !strings.Contains(diff.String(), "audita.proposal_profile") || !strings.Contains(diff.String(), "character_items_arannis.profile_id") {
|
||||||
t.Fatalf("profile diff = %q, want model and expanded family changes", diff.String())
|
t.Fatalf("profile diff = %q, want proposal profile and expanded family changes", diff.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
common := []string{
|
common := []string{
|
||||||
@@ -373,7 +373,7 @@ func TestAssembledSplitBundleProfileSwitchRecordsProvenanceAndLimitsReuse(t *tes
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("switch to testing profile: %v", err)
|
t.Fatalf("switch to testing profile: %v", err)
|
||||||
}
|
}
|
||||||
if got, want := testingRuns, []int{0, 0, 0, 1, 1, 1, 1}; !reflect.DeepEqual(got, want) {
|
if got, want := testingRuns, []int{0, 0, 0, 0, 0, 0, 0}; !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("testing profile runs = %v, want %v", got, want)
|
t.Fatalf("testing profile runs = %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
assertAssembledProvenance(t, store, testingSummary, "testing", config.EffectivePipelineDigest(testingCfg.Pipeline))
|
assertAssembledProvenance(t, store, testingSummary, "testing", config.EffectivePipelineDigest(testingCfg.Pipeline))
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ seriatim:
|
|||||||
binary: ` + seriatimBinary + `
|
binary: ` + seriatimBinary + `
|
||||||
audita:
|
audita:
|
||||||
binary: ` + auditaBinary + `
|
binary: ` + auditaBinary + `
|
||||||
llm_api_key: OPENROUTER_API_KEY
|
credentials: [OPENROUTER_API_KEY]
|
||||||
notification:
|
notification:
|
||||||
mode: noop
|
mode: noop
|
||||||
`
|
`
|
||||||
@@ -778,10 +778,18 @@ func TestAuditaAppHelper(t *testing.T) {
|
|||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
processArgs := args[start:]
|
processArgs := args[start:]
|
||||||
|
if len(processArgs) < 2 || processArgs[0] != "process" {
|
||||||
|
_, _ = os.Stderr.WriteString("expected Audita v2 process invocation\n")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
for _, arg := range processArgs {
|
||||||
|
if arg == "--report-json" || arg == "--work-dir" || arg == "--debug" || arg == "--reporting=false" {
|
||||||
|
_, _ = os.Stderr.WriteString("retired or unwanted Audita flag\n")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
outputPath := appSeriatimFlagValue(processArgs, "--output")
|
outputPath := appSeriatimFlagValue(processArgs, "--output")
|
||||||
reportPath := appSeriatimFlagValue(processArgs, "--report-json")
|
|
||||||
workDir := appSeriatimFlagValue(processArgs, "--work-dir")
|
|
||||||
if strings.TrimSpace(outputPath) == "" {
|
if strings.TrimSpace(outputPath) == "" {
|
||||||
_, _ = os.Stderr.WriteString("missing --output\n")
|
_, _ = os.Stderr.WriteString("missing --output\n")
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
@@ -794,19 +802,9 @@ func TestAuditaAppHelper(t *testing.T) {
|
|||||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("write output: %v\n", err))
|
_, _ = os.Stderr.WriteString(fmt.Sprintf("write output: %v\n", err))
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(reportPath) != "" {
|
for _, name := range []string{"errors.yml", "recoveries.yml", "report.md"} {
|
||||||
if err := os.MkdirAll(filepath.Dir(reportPath), 0o755); err != nil {
|
if err := os.WriteFile(filepath.Join(filepath.Dir(outputPath), name), []byte("status: completed\n"), 0o644); err != nil {
|
||||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir report dir: %v\n", err))
|
_, _ = os.Stderr.WriteString(fmt.Sprintf("write %s: %v\n", name, err))
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(reportPath, []byte(`{"report":true}`), 0o644); err != nil {
|
|
||||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("write report: %v\n", err))
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(workDir) != "" {
|
|
||||||
if err := os.MkdirAll(workDir, 0o755); err != nil {
|
|
||||||
_, _ = os.Stderr.WriteString(fmt.Sprintf("mkdir work dir: %v\n", err))
|
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,8 +395,8 @@ func TestConfigInspectionOutputsNeverExposeResolvedCredentialValues(t *testing.T
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
writeInspectionOverlays(t, pipelinePath,
|
writeInspectionOverlays(t, pipelinePath,
|
||||||
"audita:\n model: production\n llm_api_key: NARRATIO_INSPECT_LLM_ONE\n",
|
"audita:\n proposal_profile: production\n credentials: [NARRATIO_INSPECT_LLM_ONE]\n",
|
||||||
"audita:\n model: testing\n llm_api_key: NARRATIO_INSPECT_LLM_TWO\nnotarius:\n credentials: [NARRATIO_INSPECT_NOTARIUS_TWO]\n",
|
"audita:\n proposal_profile: testing\n credentials: [NARRATIO_INSPECT_LLM_TWO]\nnotarius:\n credentials: [NARRATIO_INSPECT_NOTARIUS_TWO]\n",
|
||||||
)
|
)
|
||||||
mustWriteTestFile(t, filepath.Join(filepath.Dir(pipelinePath), "overlays", "without-notarius-key.yml"), "notarius:\n credentials: []\n")
|
mustWriteTestFile(t, filepath.Join(filepath.Dir(pipelinePath), "overlays", "without-notarius-key.yml"), "notarius:\n credentials: []\n")
|
||||||
|
|
||||||
@@ -476,8 +476,8 @@ func TestConfigCommandsInspectMaintainedSplitBundleWithoutRuntimeState(t *testin
|
|||||||
if err := ConfigDiff(context.Background(), []string{"default", "testing", "--config", pipelinePath, "--campaign-file", campaignPath}, &diff); err != nil {
|
if err := ConfigDiff(context.Background(), []string{"default", "testing", "--config", pipelinePath, "--campaign-file", campaignPath}, &diff); err != nil {
|
||||||
t.Fatalf("ConfigDiff() error = %v", err)
|
t.Fatalf("ConfigDiff() error = %v", err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(diff.String(), "audita.model") {
|
if !strings.Contains(diff.String(), "audita.proposal_profile") {
|
||||||
t.Fatalf("split bundle overlay diff = %q, want model change", diff.String())
|
t.Fatalf("split bundle overlay diff = %q, want proposal profile change", diff.String())
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(workspacePath); !os.IsNotExist(err) {
|
if _, err := os.Stat(workspacePath); !os.IsNotExist(err) {
|
||||||
t.Fatalf("inspection created example workspace: %v", err)
|
t.Fatalf("inspection created example workspace: %v", err)
|
||||||
|
|||||||
@@ -207,35 +207,18 @@ func buildDefaultAuditaRunner(cfg *config.Config) (audita.Runner, error) {
|
|||||||
return &audita.NoopRunner{}, nil
|
return &audita.NoopRunner{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
report := false
|
timeout, err := time.ParseDuration(a.Timeout)
|
||||||
if a.Report != nil {
|
if err != nil {
|
||||||
report = *a.Report
|
return nil, fmt.Errorf("parse pipeline.audita.timeout %q: %w", a.Timeout, err)
|
||||||
}
|
}
|
||||||
llmAPIKey := ""
|
runner, err := audita.NewSubprocessRunner(audita.SubprocessRunnerConfig{
|
||||||
llmAPIKeyReference := ""
|
Binary: a.Binary, Timeout: timeout, Credentials: a.Credentials,
|
||||||
if a.LLMAPIKey != nil {
|
Modules: append(a.Modules[:0:0], a.Modules...),
|
||||||
llmAPIKey = a.LLMAPIKey.Value()
|
Overlay: a.Overlay, ProposalProfile: a.ProposalProfile,
|
||||||
llmAPIKeyReference = a.LLMAPIKey.EnvName()
|
ValidationProfile: a.ValidationProfile,
|
||||||
}
|
TranscriptDescription: a.TranscriptDescription, ConfigPath: a.ConfigPath,
|
||||||
|
OutputSchema: a.OutputSchema,
|
||||||
runner, err := audita.NewSubprocessRunnerFromConfigValues(
|
})
|
||||||
a.Binary,
|
|
||||||
a.Timeout,
|
|
||||||
llmAPIKey,
|
|
||||||
llmAPIKeyReference,
|
|
||||||
append([]string(nil), a.Modules...),
|
|
||||||
a.BaseURL,
|
|
||||||
a.Model,
|
|
||||||
a.TranscriptDescription,
|
|
||||||
a.ConfigPath,
|
|
||||||
a.OutputSchema,
|
|
||||||
a.WorkDirRetention,
|
|
||||||
a.TotalLLMConcurrency,
|
|
||||||
a.ProposalLLMConcurrency,
|
|
||||||
a.ValidationModel,
|
|
||||||
a.ValidationLLMConcurrency,
|
|
||||||
report,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("from pipeline.audita: %w", err)
|
return nil, fmt.Errorf("from pipeline.audita: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,8 +364,8 @@ func TestRefinementSemanticChangesRespectDependencyBranches(t *testing.T) {
|
|||||||
wantRuns [7]int
|
wantRuns [7]int
|
||||||
wantExtractStatus manifest.StageStatus
|
wantExtractStatus manifest.StageStatus
|
||||||
}{
|
}{
|
||||||
{name: "polish model", mutate: func(cfg *config.Config) {
|
{name: "polish description", mutate: func(cfg *config.Config) {
|
||||||
cfg.Pipeline.Audita.Model = "production"
|
cfg.Pipeline.Audita.TranscriptDescription = "production"
|
||||||
}, wantRuns: [7]int{0, 0, 0, 1, 1, 1, 1}, wantExtractStatus: manifest.StatusStale},
|
}, wantRuns: [7]int{0, 0, 0, 1, 1, 1, 1}, wantExtractStatus: manifest.StatusStale},
|
||||||
{name: "normalize schema", mutate: func(cfg *config.Config) {
|
{name: "normalize schema", mutate: func(cfg *config.Config) {
|
||||||
cfg.Pipeline.Normalize = &config.NormalizeConfig{OutputSchema: "seriatim.transcript.v2"}
|
cfg.Pipeline.Normalize = &config.NormalizeConfig{OutputSchema: "seriatim.transcript.v2"}
|
||||||
|
|||||||
@@ -167,19 +167,14 @@ type SeriatimEnvConfig struct {
|
|||||||
type AuditaConfig struct {
|
type AuditaConfig struct {
|
||||||
Binary string `yaml:"binary" json:"binary"`
|
Binary string `yaml:"binary" json:"binary"`
|
||||||
Timeout string `yaml:"timeout" json:"timeout"`
|
Timeout string `yaml:"timeout" json:"timeout"`
|
||||||
LLMAPIKey *configkit.Secret `yaml:"llm_api_key,omitempty" json:"llm_api_key,omitempty"`
|
Credentials []configkit.Secret `yaml:"credentials,omitempty" json:"credentials,omitempty"`
|
||||||
Modules []string `yaml:"modules" json:"modules"`
|
Modules []string `yaml:"modules" json:"modules"`
|
||||||
BaseURL string `yaml:"base_url" json:"base_url"`
|
Overlay string `yaml:"overlay" json:"overlay"`
|
||||||
Model string `yaml:"model" json:"model"`
|
ProposalProfile string `yaml:"proposal_profile" json:"proposal_profile"`
|
||||||
TotalLLMConcurrency *int `yaml:"total_llm_concurrency" json:"total_llm_concurrency"`
|
ValidationProfile string `yaml:"validation_profile" json:"validation_profile"`
|
||||||
ProposalLLMConcurrency *int `yaml:"proposal_llm_concurrency" json:"proposal_llm_concurrency"`
|
|
||||||
ValidationModel string `yaml:"validation_model" json:"validation_model"`
|
|
||||||
ValidationLLMConcurrency *int `yaml:"validation_llm_concurrency" json:"validation_llm_concurrency"`
|
|
||||||
TranscriptDescription string `yaml:"transcript_description" json:"transcript_description"`
|
TranscriptDescription string `yaml:"transcript_description" json:"transcript_description"`
|
||||||
ConfigPath string `yaml:"config_path" json:"config_path"`
|
ConfigPath string `yaml:"config_path" json:"config_path"`
|
||||||
OutputSchema string `yaml:"output_schema" json:"output_schema"`
|
OutputSchema string `yaml:"output_schema" json:"output_schema"`
|
||||||
WorkDirRetention string `yaml:"work_dir_retention" json:"work_dir_retention"`
|
|
||||||
Report *bool `yaml:"report" json:"report"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NormalizeConfig configures normalize-stage transcript schema/output behavior.
|
// NormalizeConfig configures normalize-stage transcript schema/output behavior.
|
||||||
|
|||||||
@@ -21,32 +21,25 @@ func (cfg StorageS3Config) MarshalYAML() (any, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalYAML exposes only the public Audita credential reference name.
|
// MarshalYAML exposes only public Audita credential reference names.
|
||||||
func (cfg AuditaConfig) MarshalYAML() (any, error) {
|
func (cfg AuditaConfig) MarshalYAML() (any, error) {
|
||||||
type safeAudita struct {
|
type safeAudita struct {
|
||||||
Binary string `yaml:"binary"`
|
Binary string `yaml:"binary"`
|
||||||
Timeout string `yaml:"timeout"`
|
Timeout string `yaml:"timeout"`
|
||||||
LLMAPIKey *string `yaml:"llm_api_key,omitempty"`
|
Credentials []string `yaml:"credentials,omitempty"`
|
||||||
Modules []string `yaml:"modules"`
|
Modules []string `yaml:"modules"`
|
||||||
BaseURL string `yaml:"base_url"`
|
Overlay string `yaml:"overlay"`
|
||||||
Model string `yaml:"model"`
|
ProposalProfile string `yaml:"proposal_profile"`
|
||||||
TotalLLMConcurrency *int `yaml:"total_llm_concurrency"`
|
ValidationProfile string `yaml:"validation_profile"`
|
||||||
ProposalLLMConcurrency *int `yaml:"proposal_llm_concurrency"`
|
|
||||||
ValidationModel string `yaml:"validation_model"`
|
|
||||||
ValidationLLMConcurrency *int `yaml:"validation_llm_concurrency"`
|
|
||||||
TranscriptDescription string `yaml:"transcript_description"`
|
TranscriptDescription string `yaml:"transcript_description"`
|
||||||
ConfigPath string `yaml:"config_path"`
|
ConfigPath string `yaml:"config_path"`
|
||||||
OutputSchema string `yaml:"output_schema"`
|
OutputSchema string `yaml:"output_schema"`
|
||||||
WorkDirRetention string `yaml:"work_dir_retention"`
|
|
||||||
Report *bool `yaml:"report"`
|
|
||||||
}
|
}
|
||||||
return safeAudita{
|
return safeAudita{
|
||||||
Binary: cfg.Binary, Timeout: cfg.Timeout, LLMAPIKey: secretReferenceName(cfg.LLMAPIKey),
|
Binary: cfg.Binary, Timeout: cfg.Timeout, Credentials: secretReferenceNames(cfg.Credentials),
|
||||||
Modules: cfg.Modules, BaseURL: cfg.BaseURL, Model: cfg.Model,
|
Modules: cfg.Modules, Overlay: cfg.Overlay, ProposalProfile: cfg.ProposalProfile,
|
||||||
TotalLLMConcurrency: cfg.TotalLLMConcurrency, ProposalLLMConcurrency: cfg.ProposalLLMConcurrency,
|
ValidationProfile: cfg.ValidationProfile, TranscriptDescription: cfg.TranscriptDescription,
|
||||||
ValidationModel: cfg.ValidationModel, ValidationLLMConcurrency: cfg.ValidationLLMConcurrency,
|
ConfigPath: cfg.ConfigPath, OutputSchema: cfg.OutputSchema,
|
||||||
TranscriptDescription: cfg.TranscriptDescription, ConfigPath: cfg.ConfigPath,
|
|
||||||
OutputSchema: cfg.OutputSchema, WorkDirRetention: cfg.WorkDirRetention, Report: cfg.Report,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const (
|
|||||||
|
|
||||||
DefaultAuditaBinary = "audita"
|
DefaultAuditaBinary = "audita"
|
||||||
DefaultAuditaTimeout = "3h"
|
DefaultAuditaTimeout = "3h"
|
||||||
DefaultAuditaReport = true
|
|
||||||
DefaultNotariusBinary = "notarius"
|
DefaultNotariusBinary = "notarius"
|
||||||
DefaultNotariusTimeout = "3h"
|
DefaultNotariusTimeout = "3h"
|
||||||
// MaxNotariusReferenceBindings is the maximum number of CLI reference
|
// MaxNotariusReferenceBindings is the maximum number of CLI reference
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func TestCredentialValuesNeverEnterEffectiveProjectionOrDigest(t *testing.T) {
|
|||||||
t.Setenv("NARRATIO_TEST_SECRET_KEY", firstCanary)
|
t.Setenv("NARRATIO_TEST_SECRET_KEY", firstCanary)
|
||||||
t.Setenv("NARRATIO_TEST_AUDITA_KEY", firstCanary)
|
t.Setenv("NARRATIO_TEST_AUDITA_KEY", firstCanary)
|
||||||
t.Setenv("NARRATIO_TEST_NOTARIUS_KEY", firstCanary)
|
t.Setenv("NARRATIO_TEST_NOTARIUS_KEY", firstCanary)
|
||||||
pipelineYAML := strings.Replace(testPipelineBaseYAML, "audita:\n binary: audita\n", "audita:\n binary: audita\n llm_api_key: NARRATIO_TEST_AUDITA_KEY\n", 1) + `
|
pipelineYAML := strings.Replace(testPipelineBaseYAML, "audita:\n binary: audita\n", "audita:\n binary: audita\n credentials: [NARRATIO_TEST_AUDITA_KEY]\n", 1) + `
|
||||||
storage:
|
storage:
|
||||||
backend: s3
|
backend: s3
|
||||||
s3:
|
s3:
|
||||||
@@ -375,10 +375,10 @@ func TestExamplesLoadAndValidate(t *testing.T) {
|
|||||||
if err := Validate(cfg); err != nil {
|
if err := Validate(cfg); err != nil {
|
||||||
t.Fatalf("validate split bundle: %v", err)
|
t.Fatalf("validate split bundle: %v", err)
|
||||||
}
|
}
|
||||||
if cfg.Pipeline.Audita.Model != profile.model {
|
if cfg.Pipeline.Audita.ProposalProfile != profile.model {
|
||||||
t.Fatalf("audita model = %q, want %q", cfg.Pipeline.Audita.Model, profile.model)
|
t.Fatalf("audita proposal profile = %q, want %q", cfg.Pipeline.Audita.ProposalProfile, profile.model)
|
||||||
}
|
}
|
||||||
if cfg.Pipeline.Audita.LLMAPIKey != nil || cfg.Pipeline.Storage.Backend != StorageBackendLocal || cfg.Pipeline.Storage.S3 != nil {
|
if len(cfg.Pipeline.Audita.Credentials) != 0 || cfg.Pipeline.Storage.Backend != StorageBackendLocal || cfg.Pipeline.Storage.S3 != nil {
|
||||||
t.Fatalf("split bundle must remain offline and secret-free: audita=%#v storage=%#v", cfg.Pipeline.Audita, cfg.Pipeline.Storage)
|
t.Fatalf("split bundle must remain offline and secret-free: audita=%#v storage=%#v", cfg.Pipeline.Audita, cfg.Pipeline.Storage)
|
||||||
}
|
}
|
||||||
selectedOverlay, selected := SelectedPipelineOverlay(cfg.Pipeline)
|
selectedOverlay, selected := SelectedPipelineOverlay(cfg.Pipeline)
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ func pipelineConfigDefaults() PipelineConfig {
|
|||||||
OutputSchema: DefaultSeriatimOutputSchema, CoalesceGap: float64Ptr(DefaultSeriatimCoalesceGap),
|
OutputSchema: DefaultSeriatimOutputSchema, CoalesceGap: float64Ptr(DefaultSeriatimCoalesceGap),
|
||||||
Report: boolPtr(DefaultSeriatimReport),
|
Report: boolPtr(DefaultSeriatimReport),
|
||||||
},
|
},
|
||||||
Audita: AuditaConfig{Binary: DefaultAuditaBinary, Timeout: DefaultAuditaTimeout, Report: boolPtr(DefaultAuditaReport)},
|
Audita: AuditaConfig{Binary: DefaultAuditaBinary, Timeout: DefaultAuditaTimeout},
|
||||||
Normalize: &NormalizeConfig{OutputPath: DefaultNormalizeOutputPath, OutputSchema: DefaultNormalizeOutputSchema, Report: boolPtr(DefaultNormalizeReport)},
|
Normalize: &NormalizeConfig{OutputPath: DefaultNormalizeOutputPath, OutputSchema: DefaultNormalizeOutputSchema, Report: boolPtr(DefaultNormalizeReport)},
|
||||||
Trim: &TrimConfig{
|
Trim: &TrimConfig{
|
||||||
Enabled: boolPtr(DefaultTrimEnabled), OutputPath: DefaultTrimOutputPath,
|
Enabled: boolPtr(DefaultTrimEnabled), OutputPath: DefaultTrimOutputPath,
|
||||||
|
|||||||
@@ -129,24 +129,71 @@ func TestPipelineOverlayDoesNotRequireUnusedBaseCredentials(t *testing.T) {
|
|||||||
for _, credential := range []string{testingKey, "null"} {
|
for _, credential := range []string{testingKey, "null"} {
|
||||||
t.Run(credential, func(t *testing.T) {
|
t.Run(credential, func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\naudita:\n llm_api_key: "+production+"\n")
|
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\naudita:\n credentials: ["+production+"]\n")
|
||||||
writePipelineFixture(t, dir, "overlays/testing.yml", "audita:\n llm_api_key: "+credential+"\n")
|
selected := "[" + credential + "]"
|
||||||
|
if credential == "null" {
|
||||||
|
selected = credential
|
||||||
|
}
|
||||||
|
writePipelineFixture(t, dir, "overlays/testing.yml", "audita:\n credentials: "+selected+"\n")
|
||||||
profile := "testing"
|
profile := "testing"
|
||||||
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &profile})
|
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &profile})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if credential == "null" {
|
if credential == "null" {
|
||||||
if cfg.Audita.LLMAPIKey != nil {
|
if len(cfg.Audita.Credentials) != 0 {
|
||||||
t.Fatal("profile did not remove the base credential")
|
t.Fatal("profile did not remove the base credential")
|
||||||
}
|
}
|
||||||
} else if cfg.Audita.LLMAPIKey == nil || cfg.Audita.LLMAPIKey.EnvName() != testingKey || !cfg.Audita.LLMAPIKey.IsResolved() {
|
} else if len(cfg.Audita.Credentials) != 1 || cfg.Audita.Credentials[0].EnvName() != testingKey || !cfg.Audita.Credentials[0].IsResolved() {
|
||||||
t.Fatal("profile credential was not resolved")
|
t.Fatal("profile credential was not resolved")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuditaModulesPreserveOmittedEmptyAndReplacementSelections(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name, base, overlay string
|
||||||
|
wantNil bool
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{name: "omitted", wantNil: true},
|
||||||
|
{name: "null", base: " modules: null\n", wantNil: true},
|
||||||
|
{name: "empty", base: " modules: []\n", want: []string{}},
|
||||||
|
{name: "overlay empty", base: " modules: [glossary]\n", overlay: "audita:\n modules: []\n", want: []string{}},
|
||||||
|
{name: "overlay order", base: " modules: []\n", overlay: "audita:\n modules: [grammar, glossary, grammar]\n", want: []string{"grammar", "glossary", "grammar"}},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\naudita:\n binary: audita\n"+test.base)
|
||||||
|
options := PipelineLoadOptions{}
|
||||||
|
if test.overlay != "" {
|
||||||
|
writePipelineFixture(t, dir, "overlays/testing.yml", test.overlay)
|
||||||
|
selection := "testing"
|
||||||
|
options.Overlay = &selection
|
||||||
|
}
|
||||||
|
cfg, err := LoadPipelineWithOptions(root, options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if (cfg.Audita.Modules == nil) != test.wantNil || !reflect.DeepEqual(cfg.Audita.Modules, test.want) {
|
||||||
|
t.Fatalf("modules = %#v, want %#v (nil=%v)", cfg.Audita.Modules, test.want, test.wantNil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuditaRejectsRetiredConfigurationFields(t *testing.T) {
|
||||||
|
for _, field := range []string{"llm_api_key", "base_url", "model", "validation_model", "total_llm_concurrency", "proposal_llm_concurrency", "validation_llm_concurrency", "work_dir_retention", "report"} {
|
||||||
|
t.Run(field, func(t *testing.T) {
|
||||||
|
root := writePipelineFixture(t, t.TempDir(), "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\naudita:\n "+field+": retired\n")
|
||||||
|
if _, err := LoadPipeline(root); err == nil || !strings.Contains(err.Error(), "decode.unknown_field") {
|
||||||
|
t.Fatalf("error = %v, want unknown field", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNotariusCredentialOverlayReplacementAndProvenance(t *testing.T) {
|
func TestNotariusCredentialOverlayReplacementAndProvenance(t *testing.T) {
|
||||||
const baseKey = "NARRATIO_NOTARIUS_UNUSED_BASE_KEY"
|
const baseKey = "NARRATIO_NOTARIUS_UNUSED_BASE_KEY"
|
||||||
const profileKey = "NARRATIO_NOTARIUS_PROFILE_KEY"
|
const profileKey = "NARRATIO_NOTARIUS_PROFILE_KEY"
|
||||||
|
|||||||
@@ -176,24 +176,6 @@ func validateAudita(cfg AuditaConfig) error {
|
|||||||
return fmt.Errorf("pipeline.audita.modules[%d] must be one of: glossary, homophones, spoken_word, grammar", i)
|
return fmt.Errorf("pipeline.audita.modules[%d] must be one of: glossary, homophones, spoken_word, grammar", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.BaseURL) != "" {
|
|
||||||
u, err := url.Parse(cfg.BaseURL)
|
|
||||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("pipeline.audita.base_url must be a valid URL: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("pipeline.audita.base_url must be a valid URL")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cfg.TotalLLMConcurrency != nil && *cfg.TotalLLMConcurrency <= 0 {
|
|
||||||
return fmt.Errorf("pipeline.audita.total_llm_concurrency must be > 0")
|
|
||||||
}
|
|
||||||
if cfg.ProposalLLMConcurrency != nil && *cfg.ProposalLLMConcurrency <= 0 {
|
|
||||||
return fmt.Errorf("pipeline.audita.proposal_llm_concurrency must be > 0")
|
|
||||||
}
|
|
||||||
if cfg.ValidationLLMConcurrency != nil && *cfg.ValidationLLMConcurrency <= 0 {
|
|
||||||
return fmt.Errorf("pipeline.audita.validation_llm_concurrency must be > 0")
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(cfg.TranscriptDescription) == "" && cfg.TranscriptDescription != "" {
|
if strings.TrimSpace(cfg.TranscriptDescription) == "" && cfg.TranscriptDescription != "" {
|
||||||
return fmt.Errorf("pipeline.audita.transcript_description must be non-empty when provided")
|
return fmt.Errorf("pipeline.audita.transcript_description must be non-empty when provided")
|
||||||
}
|
}
|
||||||
@@ -205,10 +187,5 @@ func validateAudita(cfg AuditaConfig) error {
|
|||||||
default:
|
default:
|
||||||
return fmt.Errorf("pipeline.audita.output_schema must be one of: bare-segments, audita-v1")
|
return fmt.Errorf("pipeline.audita.output_schema must be one of: bare-segments, audita-v1")
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(cfg.WorkDirRetention) {
|
|
||||||
case "", "always", "auto", "never":
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("pipeline.audita.work_dir_retention must be one of: always, auto, never")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,39 +67,23 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("polish: resolve run-local processed transcript path: %w", err)
|
return nil, fmt.Errorf("polish: resolve run-local processed transcript path: %w", err)
|
||||||
}
|
}
|
||||||
canonicalReportPath := filepath.Join(paths.ArtifactsDir, "audita.report.json")
|
|
||||||
reportPath := canonicalReportPath
|
|
||||||
if runLayout.Enabled {
|
|
||||||
reportPath, err = runLocalPathForCanonical(runLayout, paths, canonicalReportPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("polish: resolve run-local report path: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
workDir := filepath.Join(paths.ArtifactsDir, "audita-work")
|
|
||||||
stdoutPath := filepath.Join(paths.LogsDir, "audita.stdout.log")
|
stdoutPath := filepath.Join(paths.LogsDir, "audita.stdout.log")
|
||||||
stderrPath := filepath.Join(paths.LogsDir, "audita.stderr.log")
|
stderrPath := filepath.Join(paths.LogsDir, "audita.stderr.log")
|
||||||
generatedConfigPath := filepath.Join(paths.ConfigDir, "audita.generated.yml")
|
generatedConfigPath := filepath.Join(paths.ConfigDir, "audita.generated.yml")
|
||||||
if runLayout.Enabled {
|
if runLayout.Enabled {
|
||||||
workDir = filepath.Join(runLayout.ScratchDir, "audita-work")
|
|
||||||
stdoutPath = filepath.Join(runLayout.LogsDir, "audita.stdout.log")
|
stdoutPath = filepath.Join(runLayout.LogsDir, "audita.stdout.log")
|
||||||
stderrPath = filepath.Join(runLayout.LogsDir, "audita.stderr.log")
|
stderrPath = filepath.Join(runLayout.LogsDir, "audita.stderr.log")
|
||||||
generatedConfigPath = filepath.Join(runLayout.ConfigDir, "audita.generated.yml")
|
generatedConfigPath = filepath.Join(runLayout.ConfigDir, "audita.generated.yml")
|
||||||
}
|
}
|
||||||
|
|
||||||
reportEnabled := env.Config.Pipeline.Audita.Report != nil && *env.Config.Pipeline.Audita.Report
|
|
||||||
req := audita.PolishRequest{
|
req := audita.PolishRequest{
|
||||||
GeneratedConfigPath: generatedConfigPath,
|
GeneratedConfigPath: generatedConfigPath,
|
||||||
MergedTranscriptPath: mergedPath,
|
MergedTranscriptPath: mergedPath,
|
||||||
OutputProcessedPath: processedPath,
|
OutputProcessedPath: processedPath,
|
||||||
GlossaryPath: glossaryPath,
|
GlossaryPath: glossaryPath,
|
||||||
ReportPath: "",
|
|
||||||
WorkDir: workDir,
|
|
||||||
StdoutLogPath: stdoutPath,
|
StdoutLogPath: stdoutPath,
|
||||||
StderrLogPath: stderrPath,
|
StderrLogPath: stderrPath,
|
||||||
}
|
}
|
||||||
if reportEnabled {
|
|
||||||
req.ReportPath = reportPath
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := env.Audita.Run(ctx, req)
|
res, err := env.Audita.Run(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,16 +98,6 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
|||||||
return nil, fmt.Errorf("polish: processed transcript %q invalid: %w", finalProcessedPath, err)
|
return nil, fmt.Errorf("polish: processed transcript %q invalid: %w", finalProcessedPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalReportPath, err := authoritativeOutputPath(req.ReportPath, res.ReportPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("polish: %w", err)
|
|
||||||
}
|
|
||||||
if reportEnabled {
|
|
||||||
if err := validateTranscriptJSONFile(finalReportPath); err != nil {
|
|
||||||
return nil, fmt.Errorf("polish: report %q invalid: %w", finalReportPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
materializedProcessed, err := materializeRunLocalOutput(env.ArtifactStore, finalProcessedPath, canonicalProcessedPath, artifacts.Ref{
|
materializedProcessed, err := materializeRunLocalOutput(env.ArtifactStore, finalProcessedPath, canonicalProcessedPath, artifacts.Ref{
|
||||||
Kind: "transcript_polished",
|
Kind: "transcript_polished",
|
||||||
Category: "transcripts",
|
Category: "transcripts",
|
||||||
@@ -133,39 +107,6 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
|||||||
return nil, fmt.Errorf("polish: materialize canonical polished transcript: %w", err)
|
return nil, fmt.Errorf("polish: materialize canonical polished transcript: %w", err)
|
||||||
}
|
}
|
||||||
outputs := []artifacts.Ref{materializedProcessed}
|
outputs := []artifacts.Ref{materializedProcessed}
|
||||||
if reportEnabled {
|
|
||||||
materializedReport, err := materializeRunLocalOutput(env.ArtifactStore, finalReportPath, canonicalReportPath, artifacts.Ref{
|
|
||||||
Kind: "audita_report",
|
|
||||||
Category: "artifacts",
|
|
||||||
SessionID: sessionID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("polish: materialize canonical report: %w", err)
|
|
||||||
}
|
|
||||||
outputs = append(outputs, materializedReport)
|
|
||||||
}
|
|
||||||
|
|
||||||
var validationConcurrency any
|
|
||||||
if env.Config.Pipeline.Audita.ValidationLLMConcurrency != nil {
|
|
||||||
validationConcurrency = *env.Config.Pipeline.Audita.ValidationLLMConcurrency
|
|
||||||
}
|
|
||||||
var totalLLMConcurrency any
|
|
||||||
if env.Config.Pipeline.Audita.TotalLLMConcurrency != nil {
|
|
||||||
totalLLMConcurrency = *env.Config.Pipeline.Audita.TotalLLMConcurrency
|
|
||||||
}
|
|
||||||
var proposalLLMConcurrency any
|
|
||||||
if env.Config.Pipeline.Audita.ProposalLLMConcurrency != nil {
|
|
||||||
proposalLLMConcurrency = *env.Config.Pipeline.Audita.ProposalLLMConcurrency
|
|
||||||
}
|
|
||||||
reportCanonicalPath := ""
|
|
||||||
if reportEnabled {
|
|
||||||
reportCanonicalPath = canonicalReportPath
|
|
||||||
}
|
|
||||||
credentialReference := ""
|
|
||||||
if env.Config.Pipeline.Audita.LLMAPIKey != nil {
|
|
||||||
credentialReference = env.Config.Pipeline.Audita.LLMAPIKey.EnvName()
|
|
||||||
}
|
|
||||||
|
|
||||||
meta := map[string]any{
|
meta := map[string]any{
|
||||||
"stage": "polish",
|
"stage": "polish",
|
||||||
"merged_transcript_path": mergedPath,
|
"merged_transcript_path": mergedPath,
|
||||||
@@ -173,22 +114,13 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
|||||||
"glossary_path": glossaryPath,
|
"glossary_path": glossaryPath,
|
||||||
"run_output_path": finalProcessedPath,
|
"run_output_path": finalProcessedPath,
|
||||||
"output_path": canonicalProcessedPath,
|
"output_path": canonicalProcessedPath,
|
||||||
"run_report_path": finalReportPath,
|
"modules": append(env.Config.Pipeline.Audita.Modules[:0:0], env.Config.Pipeline.Audita.Modules...),
|
||||||
"report_path": reportCanonicalPath,
|
"overlay": env.Config.Pipeline.Audita.Overlay,
|
||||||
"audita_work_dir": workDir,
|
"proposal_profile": env.Config.Pipeline.Audita.ProposalProfile,
|
||||||
"report_enabled": reportEnabled,
|
"validation_profile": env.Config.Pipeline.Audita.ValidationProfile,
|
||||||
"modules": append([]string(nil), env.Config.Pipeline.Audita.Modules...),
|
|
||||||
"base_url": env.Config.Pipeline.Audita.BaseURL,
|
|
||||||
"model": env.Config.Pipeline.Audita.Model,
|
|
||||||
"transcript_description": env.Config.Pipeline.Audita.TranscriptDescription,
|
"transcript_description": env.Config.Pipeline.Audita.TranscriptDescription,
|
||||||
"config_path": env.Config.Pipeline.Audita.ConfigPath,
|
"config_path": env.Config.Pipeline.Audita.ConfigPath,
|
||||||
"output_schema": env.Config.Pipeline.Audita.OutputSchema,
|
"output_schema": env.Config.Pipeline.Audita.OutputSchema,
|
||||||
"work_dir_retention": env.Config.Pipeline.Audita.WorkDirRetention,
|
|
||||||
"validation_model": env.Config.Pipeline.Audita.ValidationModel,
|
|
||||||
"total_llm_concurrency": totalLLMConcurrency,
|
|
||||||
"proposal_llm_concurrency": proposalLLMConcurrency,
|
|
||||||
"validation_llm_concurrency": validationConcurrency,
|
|
||||||
"llm_api_key": credentialReference,
|
|
||||||
"timeout": env.Config.Pipeline.Audita.Timeout,
|
"timeout": env.Config.Pipeline.Audita.Timeout,
|
||||||
"binary": env.Config.Pipeline.Audita.Binary,
|
"binary": env.Config.Pipeline.Audita.Binary,
|
||||||
"generated_config_path": generatedConfigPath,
|
"generated_config_path": generatedConfigPath,
|
||||||
@@ -198,22 +130,12 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
|||||||
"adapter_exit_code": res.ExitCode,
|
"adapter_exit_code": res.ExitCode,
|
||||||
"adapter_invoked_binary": res.InvokedBinary,
|
"adapter_invoked_binary": res.InvokedBinary,
|
||||||
"adapter_processed_output_path": res.ProcessedTranscriptPath,
|
"adapter_processed_output_path": res.ProcessedTranscriptPath,
|
||||||
"adapter_report_path": res.ReportPath,
|
|
||||||
"adapter_generated_config_path": res.GeneratedConfigPath,
|
"adapter_generated_config_path": res.GeneratedConfigPath,
|
||||||
"adapter_work_dir": res.WorkDir,
|
|
||||||
"adapter_stdout_log_path": res.StdoutLogPath,
|
"adapter_stdout_log_path": res.StdoutLogPath,
|
||||||
"adapter_stderr_log_path": res.StderrLogPath,
|
"adapter_stderr_log_path": res.StderrLogPath,
|
||||||
"credential_env_var": credentialReference,
|
|
||||||
"credential_present": false,
|
|
||||||
}
|
}
|
||||||
if res.Metadata != nil {
|
if res.Metadata != nil {
|
||||||
meta["adapter_metadata"] = res.Metadata
|
meta["adapter_metadata"] = res.Metadata
|
||||||
if value, ok := res.Metadata["credential_present"]; ok {
|
|
||||||
meta["credential_present"] = value
|
|
||||||
}
|
|
||||||
if value, ok := res.Metadata["credential_env_var"]; ok {
|
|
||||||
meta["credential_env_var"] = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &StageResult{
|
return &StageResult{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/audita"
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/audita"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
@@ -43,18 +44,12 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
|
|||||||
if req.GlossaryPath != filepath.Join(paths.InputsDir, "glossary.yml") {
|
if req.GlossaryPath != filepath.Join(paths.InputsDir, "glossary.yml") {
|
||||||
t.Fatalf("glossary path = %q", req.GlossaryPath)
|
t.Fatalf("glossary path = %q", req.GlossaryPath)
|
||||||
}
|
}
|
||||||
if req.WorkDir != filepath.Join(paths.ArtifactsDir, "audita-work") {
|
if len(result.Outputs) != 1 {
|
||||||
t.Fatalf("work dir = %q", req.WorkDir)
|
t.Fatalf("outputs len = %d, want 1", len(result.Outputs))
|
||||||
}
|
|
||||||
if len(result.Outputs) != 2 {
|
|
||||||
t.Fatalf("outputs len = %d, want 2", len(result.Outputs))
|
|
||||||
}
|
}
|
||||||
if result.Outputs[0].Kind != "transcript_polished" {
|
if result.Outputs[0].Kind != "transcript_polished" {
|
||||||
t.Fatalf("output[0] kind = %q, want transcript_polished", result.Outputs[0].Kind)
|
t.Fatalf("output[0] kind = %q, want transcript_polished", result.Outputs[0].Kind)
|
||||||
}
|
}
|
||||||
if result.Outputs[1].Kind != "audita_report" {
|
|
||||||
t.Fatalf("output[1] kind = %q, want audita_report", result.Outputs[1].Kind)
|
|
||||||
}
|
|
||||||
if len(result.Logs) != 2 {
|
if len(result.Logs) != 2 {
|
||||||
t.Fatalf("logs = %#v, want 2", result.Logs)
|
t.Fatalf("logs = %#v, want 2", result.Logs)
|
||||||
}
|
}
|
||||||
@@ -65,18 +60,6 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
|
|||||||
if result.Metadata["stage"] != "polish" {
|
if result.Metadata["stage"] != "polish" {
|
||||||
t.Fatalf("metadata stage = %#v, want polish", result.Metadata["stage"])
|
t.Fatalf("metadata stage = %#v, want polish", result.Metadata["stage"])
|
||||||
}
|
}
|
||||||
if result.Metadata["report_enabled"] != true {
|
|
||||||
t.Fatalf("metadata report_enabled = %#v, want true", result.Metadata["report_enabled"])
|
|
||||||
}
|
|
||||||
if result.Metadata["audita_work_dir"] != filepath.Join(paths.ArtifactsDir, "audita-work") {
|
|
||||||
t.Fatalf("metadata audita_work_dir = %#v", result.Metadata["audita_work_dir"])
|
|
||||||
}
|
|
||||||
if result.Metadata["total_llm_concurrency"] != 3 {
|
|
||||||
t.Fatalf("metadata total_llm_concurrency = %#v, want 3", result.Metadata["total_llm_concurrency"])
|
|
||||||
}
|
|
||||||
if result.Metadata["proposal_llm_concurrency"] != 2 {
|
|
||||||
t.Fatalf("metadata proposal_llm_concurrency = %#v, want 2", result.Metadata["proposal_llm_concurrency"])
|
|
||||||
}
|
|
||||||
if result.Metadata["output_schema"] != "audita-v1" {
|
if result.Metadata["output_schema"] != "audita-v1" {
|
||||||
t.Fatalf("metadata output_schema = %#v, want audita-v1", result.Metadata["output_schema"])
|
t.Fatalf("metadata output_schema = %#v, want audita-v1", result.Metadata["output_schema"])
|
||||||
}
|
}
|
||||||
@@ -185,25 +168,6 @@ func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolishStageFailsWhenReportInvalid(t *testing.T) {
|
|
||||||
env, m := setupPolishEnv(t)
|
|
||||||
paths := sessionPathsForEnv(env, m.SessionID)
|
|
||||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
|
||||||
setCurrentBuiltInEvidence(t, m, "merge", artifacts.TranscriptOutputKindBase, artifacts.ArtifactTranscriptBase, filepath.Join(paths.TranscriptsDir, "base.json"))
|
|
||||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
|
||||||
badReport := filepath.Join(paths.ArtifactsDir, "bad.report.json")
|
|
||||||
writeFile(t, badReport, "not-json")
|
|
||||||
env.Audita = &audita.FakeRunner{Result: audita.PolishResult{ReportPath: badReport}}
|
|
||||||
|
|
||||||
_, err := (polishStage{}).Run(context.Background(), env, m)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error, got nil")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "report") {
|
|
||||||
t.Fatalf("error = %q", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPolishStageUsesRunLocalPathsAndMaterializesCanonical(t *testing.T) {
|
func TestPolishStageUsesRunLocalPathsAndMaterializesCanonical(t *testing.T) {
|
||||||
env, m := setupPolishEnv(t)
|
env, m := setupPolishEnv(t)
|
||||||
env.Config.Session.Campaign = "sample-campaign"
|
env.Config.Session.Campaign = "sample-campaign"
|
||||||
@@ -244,11 +208,6 @@ func setupPolishEnv(t *testing.T) (*Env, *manifest.Manifest) {
|
|||||||
writeFile(t, sessionPath, "session_id: 2026-05-03\n")
|
writeFile(t, sessionPath, "session_id: 2026-05-03\n")
|
||||||
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
||||||
|
|
||||||
report := true
|
|
||||||
totalLLMConcurrency := 3
|
|
||||||
proposalLLMConcurrency := 2
|
|
||||||
validationLLMConcurrency := 2
|
|
||||||
|
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
PipelinePath: pipelinePath,
|
PipelinePath: pipelinePath,
|
||||||
SessionPath: sessionPath,
|
SessionPath: sessionPath,
|
||||||
@@ -257,19 +216,11 @@ func setupPolishEnv(t *testing.T) (*Env, *manifest.Manifest) {
|
|||||||
Audita: config.AuditaConfig{
|
Audita: config.AuditaConfig{
|
||||||
Binary: "audita",
|
Binary: "audita",
|
||||||
Timeout: "3h",
|
Timeout: "3h",
|
||||||
LLMAPIKey: testSecretReference("AUDITA_LLM_API_KEY"),
|
Credentials: []configkit.Secret{*testSecretReference("AUDITA_LLM_API_KEY")},
|
||||||
Modules: []string{"glossary", "homophones", "grammar"},
|
Modules: []string{"glossary", "homophones", "grammar"},
|
||||||
BaseURL: "https://openrouter.ai/api/v1",
|
|
||||||
Model: "openrouter/google/gemma-4-31b-it",
|
|
||||||
TranscriptDescription: "Campaign Session 42",
|
TranscriptDescription: "Campaign Session 42",
|
||||||
ConfigPath: "/etc/audita/config.yml",
|
ConfigPath: "/etc/audita/config.yml",
|
||||||
OutputSchema: "audita-v1",
|
OutputSchema: "audita-v1",
|
||||||
WorkDirRetention: "auto",
|
|
||||||
ValidationModel: "openrouter/google/gemma-4-31b-it",
|
|
||||||
TotalLLMConcurrency: &totalLLMConcurrency,
|
|
||||||
ProposalLLMConcurrency: &proposalLLMConcurrency,
|
|
||||||
ValidationLLMConcurrency: &validationLLMConcurrency,
|
|
||||||
Report: &report,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Session: &config.SessionConfig{
|
Session: &config.SessionConfig{
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type polishSemanticConfig struct {
|
type polishSemanticConfig struct {
|
||||||
ServiceURL string `json:"service_url"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
ValidationModel string `json:"validation_model"`
|
|
||||||
Modules []string `json:"modules"`
|
Modules []string `json:"modules"`
|
||||||
TranscriptDescription string `json:"transcript_description"`
|
TranscriptDescription string `json:"transcript_description"`
|
||||||
OutputSchema string `json:"output_schema"`
|
OutputSchema string `json:"output_schema"`
|
||||||
@@ -74,9 +71,6 @@ func (polishStage) SemanticConfigFingerprint(env *Env) (manifest.SemanticConfigF
|
|||||||
}
|
}
|
||||||
sort.Strings(modules)
|
sort.Strings(modules)
|
||||||
return FingerprintSemanticConfig(polishSemanticConfigVersion, polishSemanticConfig{
|
return FingerprintSemanticConfig(polishSemanticConfigVersion, polishSemanticConfig{
|
||||||
ServiceURL: strings.TrimSpace(audita.BaseURL),
|
|
||||||
Model: strings.TrimSpace(audita.Model),
|
|
||||||
ValidationModel: strings.TrimSpace(audita.ValidationModel),
|
|
||||||
Modules: modules,
|
Modules: modules,
|
||||||
TranscriptDescription: strings.TrimSpace(audita.TranscriptDescription),
|
TranscriptDescription: strings.TrimSpace(audita.TranscriptDescription),
|
||||||
OutputSchema: strings.TrimSpace(audita.OutputSchema),
|
OutputSchema: strings.TrimSpace(audita.OutputSchema),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package stage
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,13 +12,10 @@ func TestPolishSemanticConfigSensitivity(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
mutate func(*Env)
|
mutate func(*Env)
|
||||||
}{
|
}{
|
||||||
{name: "model", mutate: func(env *Env) { env.Config.Pipeline.Audita.Model = "production" }},
|
|
||||||
{name: "validation model", mutate: func(env *Env) { env.Config.Pipeline.Audita.ValidationModel = "validator-v2" }},
|
|
||||||
{name: "modules", mutate: func(env *Env) { env.Config.Pipeline.Audita.Modules = []string{"terms", "speakers", "style"} }},
|
{name: "modules", mutate: func(env *Env) { env.Config.Pipeline.Audita.Modules = []string{"terms", "speakers", "style"} }},
|
||||||
{name: "description", mutate: func(env *Env) { env.Config.Pipeline.Audita.TranscriptDescription = "live campaign" }},
|
{name: "description", mutate: func(env *Env) { env.Config.Pipeline.Audita.TranscriptDescription = "live campaign" }},
|
||||||
{name: "schema", mutate: func(env *Env) { env.Config.Pipeline.Audita.OutputSchema = "audita.transcript.v2" }},
|
{name: "schema", mutate: func(env *Env) { env.Config.Pipeline.Audita.OutputSchema = "audita.transcript.v2" }},
|
||||||
{name: "external config", mutate: func(env *Env) { env.Config.Pipeline.Audita.ConfigPath = "configs/other.yml" }},
|
{name: "external config", mutate: func(env *Env) { env.Config.Pipeline.Audita.ConfigPath = "configs/other.yml" }},
|
||||||
{name: "service", mutate: func(env *Env) { env.Config.Pipeline.Audita.BaseURL = "https://other.example" }},
|
|
||||||
}
|
}
|
||||||
assertRefinementChanges(t, polishStage{}, semantic)
|
assertRefinementChanges(t, polishStage{}, semantic)
|
||||||
assertRefinementUnchanged(t, polishStage{}, []struct {
|
assertRefinementUnchanged(t, polishStage{}, []struct {
|
||||||
@@ -28,12 +26,7 @@ func TestPolishSemanticConfigSensitivity(t *testing.T) {
|
|||||||
{name: "runner settings", mutate: func(env *Env) {
|
{name: "runner settings", mutate: func(env *Env) {
|
||||||
env.Config.Pipeline.Audita.Binary = "/opt/audita"
|
env.Config.Pipeline.Audita.Binary = "/opt/audita"
|
||||||
env.Config.Pipeline.Audita.Timeout = "1h"
|
env.Config.Pipeline.Audita.Timeout = "1h"
|
||||||
env.Config.Pipeline.Audita.TotalLLMConcurrency = intPointer(20)
|
env.Config.Pipeline.Audita.Credentials = []configkit.Secret{*testSecretReference("OTHER_KEY")}
|
||||||
env.Config.Pipeline.Audita.ProposalLLMConcurrency = intPointer(10)
|
|
||||||
env.Config.Pipeline.Audita.ValidationLLMConcurrency = intPointer(10)
|
|
||||||
env.Config.Pipeline.Audita.LLMAPIKey = testSecretReference("OTHER_KEY")
|
|
||||||
env.Config.Pipeline.Audita.WorkDirRetention = "always"
|
|
||||||
env.Config.Pipeline.Audita.Report = boolPointer(false)
|
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -119,12 +112,9 @@ func refinementSemanticEnv() *Env {
|
|||||||
env := initialSemanticEnv()
|
env := initialSemanticEnv()
|
||||||
env.Config.Session.Title = "Session title"
|
env.Config.Session.Title = "Session title"
|
||||||
env.Config.Pipeline.Audita = config.AuditaConfig{
|
env.Config.Pipeline.Audita = config.AuditaConfig{
|
||||||
Binary: "audita", Timeout: "10m", LLMAPIKey: testSecretReference("AUDITA_KEY"),
|
Binary: "audita", Timeout: "10m", Credentials: []configkit.Secret{*testSecretReference("AUDITA_KEY")},
|
||||||
Modules: []string{"speakers", "terms"}, BaseURL: "https://audita.example",
|
Modules: []string{"speakers", "terms"}, TranscriptDescription: "tabletop session",
|
||||||
Model: "testing", ValidationModel: "validator", TranscriptDescription: "tabletop session",
|
|
||||||
ConfigPath: "configs/audita.yml", OutputSchema: "audita.transcript.v1",
|
ConfigPath: "configs/audita.yml", OutputSchema: "audita.transcript.v1",
|
||||||
TotalLLMConcurrency: intPointer(4), ProposalLLMConcurrency: intPointer(2),
|
|
||||||
ValidationLLMConcurrency: intPointer(2), WorkDirRetention: "failure", Report: boolPointer(true),
|
|
||||||
}
|
}
|
||||||
env.Config.Pipeline.Normalize = &config.NormalizeConfig{
|
env.Config.Pipeline.Normalize = &config.NormalizeConfig{
|
||||||
OutputPath: "transcripts/final.json", OutputSchema: "seriatim.transcript.v1", Report: boolPointer(true),
|
OutputPath: "transcripts/final.json", OutputSchema: "seriatim.transcript.v1", Report: boolPointer(true),
|
||||||
|
|||||||
Reference in New Issue
Block a user