Add workspace configuration support
This commit is contained in:
@@ -44,7 +44,7 @@ Flags:
|
|||||||
- `--output-dir path`: output root. The run writes to `<path>/<run-id>/`.
|
- `--output-dir path`: output root. The run writes to `<path>/<run-id>/`.
|
||||||
Defaults to `./notarius-output`.
|
Defaults to `./notarius-output`.
|
||||||
- `--diagnostics-dir path`: diagnostics work directory override for this
|
- `--diagnostics-dir path`: diagnostics work directory override for this
|
||||||
invocation.
|
invocation. It does not change the workspace directory.
|
||||||
- `--llm-profile id`: override every effective LLM-capable pipeline module
|
- `--llm-profile id`: override every effective LLM-capable pipeline module
|
||||||
binding to use one Scriptorium profile ID. Validator-specific profiles are
|
binding to use one Scriptorium profile ID. Validator-specific profiles are
|
||||||
not overridden. Configured LLM-backed validators with explicit profiles are
|
not overridden. Configured LLM-backed validators with explicit profiles are
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ The maintained fixture is [examples/dnd-spells.config.yml](../examples/dnd-spell
|
|||||||
- `scriptorium`: optional Scriptorium profile source settings.
|
- `scriptorium`: optional Scriptorium profile source settings.
|
||||||
- `pipelines`: optional map of pipeline IDs to pipeline definitions.
|
- `pipelines`: optional map of pipeline IDs to pipeline definitions.
|
||||||
- `concurrency`: optional global concurrency settings.
|
- `concurrency`: optional global concurrency settings.
|
||||||
|
- `workspace`: optional workspace settings for Notarius-owned local state.
|
||||||
- `diagnostics`: optional diagnostics settings.
|
- `diagnostics`: optional diagnostics settings.
|
||||||
|
|
||||||
Unknown YAML fields are rejected. The removed top-level `llm_profiles` field is
|
Unknown YAML fields are rejected. The removed top-level `llm_profiles` field is
|
||||||
@@ -57,6 +58,13 @@ concurrency:
|
|||||||
diagnostics:
|
diagnostics:
|
||||||
work_dir: /tmp/notarius
|
work_dir: /tmp/notarius
|
||||||
retention: auto
|
retention: auto
|
||||||
|
workspace:
|
||||||
|
diagnostics:
|
||||||
|
enabled: true
|
||||||
|
resume:
|
||||||
|
enabled: false
|
||||||
|
debug:
|
||||||
|
enabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
No pipelines are built in. A run requires a configured pipeline.
|
No pipelines are built in. A run requires a configured pipeline.
|
||||||
@@ -98,10 +106,20 @@ These environment variables are applied after the config file:
|
|||||||
|
|
||||||
- `NOTARIUS_CONFIG`: config discovery path.
|
- `NOTARIUS_CONFIG`: config discovery path.
|
||||||
- `NOTARIUS_TOTAL_LLM_CONCURRENCY`: integer global LLM concurrency.
|
- `NOTARIUS_TOTAL_LLM_CONCURRENCY`: integer global LLM concurrency.
|
||||||
- `NOTARIUS_WORK_DIR`: diagnostics work directory.
|
- `NOTARIUS_WORKSPACE_DIR`: workspace directory.
|
||||||
- `NOTARIUS_DIAGNOSTICS_RETENTION`: diagnostics retention mode.
|
- `NOTARIUS_WORKSPACE_DIAGNOSTICS_ENABLED`: boolean diagnostics enablement.
|
||||||
|
- `NOTARIUS_WORKSPACE_DIAGNOSTICS_RETENTION`: workspace diagnostics retention
|
||||||
|
mode.
|
||||||
|
- `NOTARIUS_WORKSPACE_RESUME_ENABLED`: boolean resume checkpointing
|
||||||
|
enablement.
|
||||||
|
- `NOTARIUS_WORKSPACE_DEBUG_ENABLED`: boolean debug artifact enablement.
|
||||||
|
- `NOTARIUS_WORK_DIR`: deprecated diagnostics work directory compatibility
|
||||||
|
override.
|
||||||
|
- `NOTARIUS_DIAGNOSTICS_RETENTION`: deprecated diagnostics retention
|
||||||
|
compatibility override.
|
||||||
|
|
||||||
Integer environment values must parse as base-10 integers.
|
Integer environment values must parse as base-10 integers. Boolean environment
|
||||||
|
values must parse as Go booleans such as `true`, `false`, `1`, or `0`.
|
||||||
|
|
||||||
The removed `NOTARIUS_LLM_DEFAULT_*` variables are not read. Configure provider
|
The removed `NOTARIUS_LLM_DEFAULT_*` variables are not read. Configure provider
|
||||||
endpoint, model, and credential environment variable names through Scriptorium
|
endpoint, model, and credential environment variable names through Scriptorium
|
||||||
@@ -323,19 +341,48 @@ Both modules accept UTF-8 plain text, Markdown, YAML, or JSON reference files.
|
|||||||
The extractor uses references only as supporting disambiguation material; spell
|
The extractor uses references only as supporting disambiguation material; spell
|
||||||
casts still must be present in the source transcript.
|
casts still must be present in the source transcript.
|
||||||
|
|
||||||
|
## Workspace
|
||||||
|
|
||||||
|
`workspace` fields:
|
||||||
|
|
||||||
|
- `directory`: optional workspace root for Notarius-owned local state.
|
||||||
|
- `diagnostics.enabled`: set to `false` to skip diagnostics run directories and
|
||||||
|
diagnostics artifact writes. Default: `true`.
|
||||||
|
- `diagnostics.retention`: `auto`, `always`, or `never`.
|
||||||
|
- `resume.enabled`: boolean resume checkpointing setting. Default: `false`.
|
||||||
|
- `debug.enabled`: boolean debug artifact setting. Default: `false`.
|
||||||
|
|
||||||
|
The current run workflow uses workspace settings for diagnostics configuration.
|
||||||
|
Checkpoint and debug artifact writers are not part of the current workflow.
|
||||||
|
|
||||||
## Diagnostics
|
## Diagnostics
|
||||||
|
|
||||||
|
Preferred workspace diagnostics fields:
|
||||||
|
|
||||||
|
- `workspace.directory`: workspace root for Notarius-owned local state.
|
||||||
|
- `workspace.diagnostics.enabled`: set to `false` to skip creating diagnostics
|
||||||
|
run directories and diagnostics artifacts. Default: `true`.
|
||||||
|
- `workspace.diagnostics.retention`: `auto`, `always`, or `never`.
|
||||||
|
|
||||||
|
When `workspace.directory` is set, diagnostics use
|
||||||
|
`<workspace.directory>/diagnostics` as their work directory.
|
||||||
|
`workspace.diagnostics.retention` overrides legacy diagnostics retention when
|
||||||
|
set.
|
||||||
|
|
||||||
`diagnostics` fields:
|
`diagnostics` fields:
|
||||||
|
|
||||||
- `work_dir`: directory for per-run diagnostics. Default: `/tmp/notarius`.
|
- `work_dir`: deprecated compatibility directory for per-run diagnostics.
|
||||||
- `retention`: `auto`, `always`, or `never`. Empty uses `auto`.
|
Default: `/tmp/notarius`.
|
||||||
|
- `retention`: deprecated compatibility retention mode. `auto`, `always`, or
|
||||||
|
`never`. Empty uses `auto`.
|
||||||
|
|
||||||
`auto` retains diagnostics for failed runs and successful runs with warnings.
|
`auto` retains diagnostics for failed runs and successful runs with warnings.
|
||||||
`always` retains diagnostics for every run. `never` removes diagnostics for
|
`always` retains diagnostics for every run. `never` removes diagnostics for
|
||||||
successful runs without regard to warnings; failed runs are retained.
|
successful runs without regard to warnings; failed runs are retained.
|
||||||
|
|
||||||
The `--diagnostics-dir` run flag overrides `diagnostics.work_dir` for that
|
The `--diagnostics-dir` run flag overrides the effective diagnostics work
|
||||||
invocation.
|
directory for that invocation. It affects diagnostics only and does not change
|
||||||
|
the workspace directory.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,13 @@ Retention is decided by `ShouldRetainRunDirectory`.
|
|||||||
|
|
||||||
## CLI Failure Behavior
|
## CLI Failure Behavior
|
||||||
|
|
||||||
The CLI creates the diagnostics run directory after config loading and before
|
When diagnostics are enabled, the CLI creates the diagnostics run directory
|
||||||
pipeline resolution. Failures before that point do not have diagnostics.
|
after config loading and before pipeline resolution. Failures before that point
|
||||||
|
do not have diagnostics.
|
||||||
|
|
||||||
|
When workspace diagnostics are explicitly disabled, the CLI does not create a
|
||||||
|
diagnostics run directory and skips diagnostics artifact writes. Failures are
|
||||||
|
still printed to stderr.
|
||||||
|
|
||||||
After diagnostics creation, run failures call `WriteErrorLog` and apply
|
After diagnostics creation, run failures call `WriteErrorLog` and apply
|
||||||
retention with `RunSucceeded: false`, so the run directory remains available.
|
retention with `RunSucceeded: false`, so the run directory remains available.
|
||||||
|
|||||||
@@ -54,7 +54,15 @@ Diagnostics are written under:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The default diagnostics work directory is `/tmp/notarius`. It can be set with
|
The default diagnostics work directory is `/tmp/notarius`. It can be set with
|
||||||
`diagnostics.work_dir`, `NOTARIUS_WORK_DIR`, or `--diagnostics-dir`.
|
`workspace.directory`, `NOTARIUS_WORKSPACE_DIR`, legacy
|
||||||
|
`diagnostics.work_dir`, legacy `NOTARIUS_WORK_DIR`, or `--diagnostics-dir`.
|
||||||
|
When a workspace directory is set, diagnostics are written under
|
||||||
|
`<workspace.directory>/diagnostics/<run-id>/`.
|
||||||
|
|
||||||
|
Set `workspace.diagnostics.enabled: false` or
|
||||||
|
`NOTARIUS_WORKSPACE_DIAGNOSTICS_ENABLED=false` to skip diagnostics directory
|
||||||
|
creation and diagnostics artifact writes. Concise failures are still printed to
|
||||||
|
stderr.
|
||||||
|
|
||||||
Implemented diagnostics artifacts:
|
Implemented diagnostics artifacts:
|
||||||
|
|
||||||
@@ -77,8 +85,9 @@ by the current CLI run workflow.
|
|||||||
|
|
||||||
## Retention
|
## Retention
|
||||||
|
|
||||||
Diagnostics retention is configured with `diagnostics.retention`,
|
Diagnostics retention is configured with `workspace.diagnostics.retention`,
|
||||||
`NOTARIUS_DIAGNOSTICS_RETENTION`, or the default `auto`.
|
`NOTARIUS_WORKSPACE_DIAGNOSTICS_RETENTION`, legacy `diagnostics.retention`,
|
||||||
|
legacy `NOTARIUS_DIAGNOSTICS_RETENTION`, or the default `auto`.
|
||||||
|
|
||||||
- `auto`: keep failed runs and successful runs with warnings; remove successful
|
- `auto`: keep failed runs and successful runs with warnings; remove successful
|
||||||
warning-free runs.
|
warning-free runs.
|
||||||
|
|||||||
@@ -156,10 +156,16 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
}
|
}
|
||||||
|
|
||||||
startedAt := opts.Now().UTC()
|
startedAt := opts.Now().UTC()
|
||||||
runDir, err := diagnostics.NewRunDirectory(cfg.Diagnostics.WorkDir, cfg.Diagnostics.Retention)
|
runID := fmt.Sprintf("run-%d", startedAt.UnixNano())
|
||||||
if err != nil {
|
var runDir *diagnostics.RunDirectory
|
||||||
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
if cfg.DiagnosticsEnabled() {
|
||||||
return 1
|
var err error
|
||||||
|
runDir, err = diagnostics.NewRunDirectory(cfg.Diagnostics.WorkDir, cfg.Diagnostics.Retention)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
runID = runDir.RunID()
|
||||||
}
|
}
|
||||||
invocation := diagnostics.InvocationMetadata{
|
invocation := diagnostics.InvocationMetadata{
|
||||||
Operation: "run",
|
Operation: "run",
|
||||||
@@ -168,10 +174,10 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
ConfigPath: loadedConfigPath,
|
ConfigPath: loadedConfigPath,
|
||||||
ConfigSource: configSource(*configPath),
|
ConfigSource: configSource(*configPath),
|
||||||
OnlyLanes: append([]string(nil), only...),
|
OnlyLanes: append([]string(nil), only...),
|
||||||
RunID: runDir.RunID(),
|
RunID: runID,
|
||||||
StartedAt: startedAt,
|
StartedAt: startedAt,
|
||||||
}
|
}
|
||||||
if err := runDir.WriteInvocationMetadata(invocation); err != nil {
|
if err := writeDiagnostics(runDir, func() error { return runDir.WriteInvocationMetadata(invocation) }); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics invocation metadata: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics invocation metadata: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,16 +217,18 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
}
|
}
|
||||||
effective.ResolvedPipeline = materialized
|
effective.ResolvedPipeline = materialized
|
||||||
invocation.PipelineDigest = effective.ResolvedPipeline.Digest
|
invocation.PipelineDigest = effective.ResolvedPipeline.Digest
|
||||||
if err := runDir.WriteInvocationMetadata(invocation); err != nil {
|
if err := writeDiagnostics(runDir, func() error { return runDir.WriteInvocationMetadata(invocation) }); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics invocation metadata: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics invocation metadata: %w", err))
|
||||||
}
|
}
|
||||||
if err := runDir.WriteRedactedEffectiveConfig(effective); err != nil {
|
if err := writeDiagnostics(runDir, func() error { return runDir.WriteRedactedEffectiveConfig(effective) }); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics effective config: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics effective config: %w", err))
|
||||||
}
|
}
|
||||||
if err := runDir.WriteResolvedPipeline(effective.ResolvedPipeline); err != nil {
|
if err := writeDiagnostics(runDir, func() error { return runDir.WriteResolvedPipeline(effective.ResolvedPipeline) }); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics resolved pipeline: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics resolved pipeline: %w", err))
|
||||||
}
|
}
|
||||||
if err := runDir.WriteResolvedReferences(pipeline.ReferenceProvenance(effective.ResolvedPipeline)); err != nil {
|
if err := writeDiagnostics(runDir, func() error {
|
||||||
|
return runDir.WriteResolvedReferences(pipeline.ReferenceProvenance(effective.ResolvedPipeline))
|
||||||
|
}); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics resolved references: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics resolved references: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,45 +258,49 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
RawInput: rawInput,
|
RawInput: rawInput,
|
||||||
LLMClient: llmClient,
|
LLMClient: llmClient,
|
||||||
SessionID: strings.TrimSpace(sessionID.value),
|
SessionID: strings.TrimSpace(sessionID.value),
|
||||||
RunID: runDir.RunID(),
|
RunID: runID,
|
||||||
StartedAt: startedAt,
|
StartedAt: startedAt,
|
||||||
LLMProfiles: llmProfiles,
|
LLMProfiles: llmProfiles,
|
||||||
Metadata: runMetadata(*outputDir, *diagnosticsDir),
|
Metadata: runMetadata(*outputDir, *diagnosticsDir),
|
||||||
Warnings: referenceWarnings,
|
Warnings: referenceWarnings,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if output.Manifest.PipelineID != "" {
|
if output.Manifest.PipelineID != "" && runDir != nil {
|
||||||
_ = runDir.WriteRunManifest(output.Manifest)
|
_ = runDir.WriteRunManifest(output.Manifest)
|
||||||
}
|
}
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("run pipeline %q: %w", pipelineID, err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("run pipeline %q: %w", pipelineID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
runOutputDir := filepath.Join(outputRoot(*outputDir), runDir.RunID())
|
runOutputDir := filepath.Join(outputRoot(*outputDir), runID)
|
||||||
if err := runDir.WriteRunManifest(output.Manifest); err != nil {
|
if err := writeDiagnostics(runDir, func() error { return runDir.WriteRunManifest(output.Manifest) }); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics run manifest: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics run manifest: %w", err))
|
||||||
}
|
}
|
||||||
if err := runDir.WriteWarnings(output.Warnings); err != nil {
|
if err := writeDiagnostics(runDir, func() error { return runDir.WriteWarnings(output.Warnings) }); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics warnings: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics warnings: %w", err))
|
||||||
}
|
}
|
||||||
if err := runDir.WriteRunReport(runReport{
|
if err := writeDiagnostics(runDir, func() error {
|
||||||
RunID: runDir.RunID(),
|
return runDir.WriteRunReport(runReport{
|
||||||
PipelineID: effective.PipelineID,
|
RunID: runDir.RunID(),
|
||||||
OutputPath: runOutputDir,
|
PipelineID: effective.PipelineID,
|
||||||
DiagnosticsPath: runDir.Path(),
|
OutputPath: runOutputDir,
|
||||||
OutputCount: len(output.NormalizeOutputs),
|
DiagnosticsPath: runDir.Path(),
|
||||||
RejectedCount: len(output.Rejected),
|
OutputCount: len(output.NormalizeOutputs),
|
||||||
WarningCount: len(output.Warnings),
|
RejectedCount: len(output.Rejected),
|
||||||
ValidationStatus: output.Manifest.ValidationStatus,
|
WarningCount: len(output.Warnings),
|
||||||
|
ValidationStatus: output.Manifest.ValidationStatus,
|
||||||
|
})
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics run report: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics run report: %w", err))
|
||||||
}
|
}
|
||||||
if err := writeOutputFiles(runOutputDir, output.OutputFiles); err != nil {
|
if err := writeOutputFiles(runOutputDir, output.OutputFiles); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, err)
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, err)
|
||||||
}
|
}
|
||||||
if err := runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
|
if err := writeDiagnostics(runDir, func() error {
|
||||||
RetentionMode: cfg.Diagnostics.Retention,
|
return runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
|
||||||
RunSucceeded: true,
|
RetentionMode: cfg.Diagnostics.Retention,
|
||||||
HasWarnings: len(output.Warnings) > 0,
|
RunSucceeded: true,
|
||||||
|
HasWarnings: len(output.Warnings) > 0,
|
||||||
|
})
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("apply diagnostics retention: %w", err))
|
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("apply diagnostics retention: %w", err))
|
||||||
}
|
}
|
||||||
@@ -327,6 +339,13 @@ func failPipelineCommand(stderr io.Writer, runDir *diagnostics.RunDirectory, ret
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeDiagnostics(runDir *diagnostics.RunDirectory, write func() error) error {
|
||||||
|
if runDir == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return write()
|
||||||
|
}
|
||||||
|
|
||||||
func configSource(configPath string) string {
|
func configSource(configPath string) string {
|
||||||
if strings.TrimSpace(configPath) != "" {
|
if strings.TrimSpace(configPath) != "" {
|
||||||
return "flag"
|
return "flag"
|
||||||
|
|||||||
@@ -2148,6 +2148,29 @@ func TestRunPipelineWritesDiagnosticsArtifactsOnSuccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunPipelineSkipsDiagnosticsWhenWorkspaceDiagnosticsDisabled(t *testing.T) {
|
||||||
|
workspaceDir := filepath.Join(t.TempDir(), "workspace")
|
||||||
|
outputDir := t.TempDir()
|
||||||
|
configPath := writeTestConfig(t, mvpConfigYAMLWithWorkspaceDiagnosticsDisabled("dnd-session", workspaceDir))
|
||||||
|
inputPath := writeSeriatimInput(t)
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
code := RunWithOptions([]string{"run", "dnd-session", "--config", configPath, "--input", inputPath, "--output-dir", outputDir}, &stdout, &stderr, Options{
|
||||||
|
LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil),
|
||||||
|
})
|
||||||
|
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String())
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(workspaceDir, "diagnostics")); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("workspace diagnostics dir stat err = %v, want not exist", err)
|
||||||
|
}
|
||||||
|
if entries := childDirs(t, outputDir); len(entries) != 1 {
|
||||||
|
t.Fatalf("output run dirs = %v, want one run dir", entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunPipelineWritesErrorLogAfterDiagnosticsCreation(t *testing.T) {
|
func TestRunPipelineWritesErrorLogAfterDiagnosticsCreation(t *testing.T) {
|
||||||
diagnosticsDir := t.TempDir()
|
diagnosticsDir := t.TempDir()
|
||||||
configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "always"))
|
configPath := writeTestConfig(t, mvpConfigYAMLWithDiagnostics("dnd-session", diagnosticsDir, "always"))
|
||||||
@@ -2732,6 +2755,21 @@ pipelines:
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mvpConfigYAMLWithWorkspaceDiagnosticsDisabled(pipelineID, workspaceDir string) string {
|
||||||
|
return `version: 2
|
||||||
|
workspace:
|
||||||
|
directory: ` + workspaceDir + `
|
||||||
|
diagnostics:
|
||||||
|
enabled: false
|
||||||
|
pipelines:
|
||||||
|
` + pipelineID + `:
|
||||||
|
input: seriatim
|
||||||
|
artifacts:
|
||||||
|
spells:
|
||||||
|
extract: dnd/spells
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
func writeSeriatimInput(t *testing.T) string {
|
func writeSeriatimInput(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return writeFile(t, "source.json", `{
|
return writeFile(t, "source.json", `{
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/diagnostics"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/diagnostics"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||||
)
|
)
|
||||||
@@ -12,6 +15,7 @@ type Config struct {
|
|||||||
Pipelines map[string]pipeline.PipelineProfile `json:"pipelines"`
|
Pipelines map[string]pipeline.PipelineProfile `json:"pipelines"`
|
||||||
Concurrency ConcurrencyConfig `json:"concurrency"`
|
Concurrency ConcurrencyConfig `json:"concurrency"`
|
||||||
Diagnostics DiagnosticsConfig `json:"diagnostics"`
|
Diagnostics DiagnosticsConfig `json:"diagnostics"`
|
||||||
|
Workspace WorkspaceConfig `json:"workspace"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScriptoriumConfig struct {
|
type ScriptoriumConfig struct {
|
||||||
@@ -28,6 +32,28 @@ type DiagnosticsConfig struct {
|
|||||||
Retention diagnostics.RetentionMode `json:"retention"`
|
Retention diagnostics.RetentionMode `json:"retention"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkspaceConfig struct {
|
||||||
|
Directory string `json:"directory,omitempty"`
|
||||||
|
Diagnostics WorkspaceDiagnosticsConfig `json:"diagnostics"`
|
||||||
|
Resume WorkspaceResumeConfig `json:"resume"`
|
||||||
|
Debug WorkspaceDebugConfig `json:"debug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkspaceDiagnosticsConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Retention diagnostics.RetentionMode `json:"retention,omitempty"`
|
||||||
|
enabledSet bool
|
||||||
|
retentionSet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkspaceResumeConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkspaceDebugConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
func Default() Config {
|
func Default() Config {
|
||||||
return Config{
|
return Config{
|
||||||
Pipelines: map[string]pipeline.PipelineProfile{},
|
Pipelines: map[string]pipeline.PipelineProfile{},
|
||||||
@@ -38,9 +64,41 @@ func Default() Config {
|
|||||||
WorkDir: "/tmp/notarius",
|
WorkDir: "/tmp/notarius",
|
||||||
Retention: diagnostics.RetentionAuto,
|
Retention: diagnostics.RetentionAuto,
|
||||||
},
|
},
|
||||||
|
Workspace: WorkspaceConfig{
|
||||||
|
Diagnostics: WorkspaceDiagnosticsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) RecomputeEffectiveDiagnostics() {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if dir := c.workspaceDirectory(); dir != "" {
|
||||||
|
c.Diagnostics.WorkDir = filepath.Join(dir, "diagnostics")
|
||||||
|
}
|
||||||
|
if c.Workspace.Diagnostics.retentionSet {
|
||||||
|
c.Diagnostics.Retention = c.Workspace.Diagnostics.Retention
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) DiagnosticsEnabled() bool {
|
||||||
|
if !c.Workspace.Diagnostics.enabledSet {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return c.Workspace.Diagnostics.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) workspaceDirectory() string {
|
||||||
|
dir := strings.TrimSpace(c.Workspace.Directory)
|
||||||
|
if dir == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Clean(dir)
|
||||||
|
}
|
||||||
|
|
||||||
func cloneConfig(in Config) Config {
|
func cloneConfig(in Config) Config {
|
||||||
out := in
|
out := in
|
||||||
out.Pipelines = make(map[string]pipeline.PipelineProfile, len(in.Pipelines))
|
out.Pipelines = make(map[string]pipeline.PipelineProfile, len(in.Pipelines))
|
||||||
|
|||||||
@@ -24,6 +24,21 @@ func TestDefaultValues(t *testing.T) {
|
|||||||
if cfg.Diagnostics.Retention != diagnostics.RetentionAuto {
|
if cfg.Diagnostics.Retention != diagnostics.RetentionAuto {
|
||||||
t.Fatalf("unexpected diagnostics retention: %q", cfg.Diagnostics.Retention)
|
t.Fatalf("unexpected diagnostics retention: %q", cfg.Diagnostics.Retention)
|
||||||
}
|
}
|
||||||
|
if cfg.Workspace.Directory != "" {
|
||||||
|
t.Fatalf("unexpected workspace directory: %q", cfg.Workspace.Directory)
|
||||||
|
}
|
||||||
|
if !cfg.Workspace.Diagnostics.Enabled || !cfg.DiagnosticsEnabled() {
|
||||||
|
t.Fatalf("expected workspace diagnostics enabled by default: %+v", cfg.Workspace.Diagnostics)
|
||||||
|
}
|
||||||
|
if cfg.Workspace.Diagnostics.Retention != "" {
|
||||||
|
t.Fatalf("unexpected workspace diagnostics retention: %q", cfg.Workspace.Diagnostics.Retention)
|
||||||
|
}
|
||||||
|
if cfg.Workspace.Resume.Enabled {
|
||||||
|
t.Fatalf("workspace resume should be disabled by default")
|
||||||
|
}
|
||||||
|
if cfg.Workspace.Debug.Enabled {
|
||||||
|
t.Fatalf("workspace debug should be disabled by default")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyFileConfigMergesWithDefaults(t *testing.T) {
|
func TestApplyFileConfigMergesWithDefaults(t *testing.T) {
|
||||||
|
|||||||
@@ -42,6 +42,36 @@ func (c *Config) applyEnvOverridesWithLookup(lookup func(string) (string, bool))
|
|||||||
if raw, ok := lookup("NOTARIUS_DIAGNOSTICS_RETENTION"); ok {
|
if raw, ok := lookup("NOTARIUS_DIAGNOSTICS_RETENTION"); ok {
|
||||||
c.Diagnostics.Retention = diagnostics.RetentionMode(strings.TrimSpace(raw))
|
c.Diagnostics.Retention = diagnostics.RetentionMode(strings.TrimSpace(raw))
|
||||||
}
|
}
|
||||||
|
if raw, ok := lookup("NOTARIUS_WORKSPACE_DIR"); ok {
|
||||||
|
c.Workspace.Directory = strings.TrimSpace(raw)
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("NOTARIUS_WORKSPACE_DIAGNOSTICS_ENABLED"); ok {
|
||||||
|
value, err := parseBoolEnv("NOTARIUS_WORKSPACE_DIAGNOSTICS_ENABLED", raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Workspace.Diagnostics.Enabled = value
|
||||||
|
c.Workspace.Diagnostics.enabledSet = true
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("NOTARIUS_WORKSPACE_DIAGNOSTICS_RETENTION"); ok {
|
||||||
|
c.Workspace.Diagnostics.Retention = diagnostics.RetentionMode(strings.TrimSpace(raw))
|
||||||
|
c.Workspace.Diagnostics.retentionSet = true
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("NOTARIUS_WORKSPACE_RESUME_ENABLED"); ok {
|
||||||
|
value, err := parseBoolEnv("NOTARIUS_WORKSPACE_RESUME_ENABLED", raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Workspace.Resume.Enabled = value
|
||||||
|
}
|
||||||
|
if raw, ok := lookup("NOTARIUS_WORKSPACE_DEBUG_ENABLED"); ok {
|
||||||
|
value, err := parseBoolEnv("NOTARIUS_WORKSPACE_DEBUG_ENABLED", raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Workspace.Debug.Enabled = value
|
||||||
|
}
|
||||||
|
c.RecomputeEffectiveDiagnostics()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,3 +82,11 @@ func parseIntEnv(name string, raw string) (int, error) {
|
|||||||
}
|
}
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseBoolEnv(name string, raw string) (bool, error) {
|
||||||
|
value, err := strconv.ParseBool(strings.TrimSpace(raw))
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("%s: must be a boolean", name)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,10 +13,15 @@ func TestApplyEnvOverridesOperationalValues(t *testing.T) {
|
|||||||
cfg.Pipelines["example"] = pipeline.PipelineProfile{ID: "example", Input: pipeline.Binding("before")}
|
cfg.Pipelines["example"] = pipeline.PipelineProfile{ID: "example", Input: pipeline.Binding("before")}
|
||||||
|
|
||||||
err := cfg.applyEnvOverridesWithLookup(mapLookup(map[string]string{
|
err := cfg.applyEnvOverridesWithLookup(mapLookup(map[string]string{
|
||||||
"NOTARIUS_TOTAL_LLM_CONCURRENCY": "3",
|
"NOTARIUS_TOTAL_LLM_CONCURRENCY": "3",
|
||||||
"NOTARIUS_WORK_DIR": "/tmp/notarius-env",
|
"NOTARIUS_WORK_DIR": "/tmp/notarius-env",
|
||||||
"NOTARIUS_DIAGNOSTICS_RETENTION": "never",
|
"NOTARIUS_DIAGNOSTICS_RETENTION": "never",
|
||||||
"NOTARIUS_PIPELINE_INPUT": "after",
|
"NOTARIUS_WORKSPACE_DIR": "/var/lib/notarius-env",
|
||||||
|
"NOTARIUS_WORKSPACE_DIAGNOSTICS_ENABLED": "false",
|
||||||
|
"NOTARIUS_WORKSPACE_DIAGNOSTICS_RETENTION": "always",
|
||||||
|
"NOTARIUS_WORKSPACE_RESUME_ENABLED": "true",
|
||||||
|
"NOTARIUS_WORKSPACE_DEBUG_ENABLED": "true",
|
||||||
|
"NOTARIUS_PIPELINE_INPUT": "after",
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ApplyEnvOverrides: %v", err)
|
t.Fatalf("ApplyEnvOverrides: %v", err)
|
||||||
@@ -28,9 +33,21 @@ func TestApplyEnvOverridesOperationalValues(t *testing.T) {
|
|||||||
if cfg.Concurrency.TotalLLM != 3 {
|
if cfg.Concurrency.TotalLLM != 3 {
|
||||||
t.Fatalf("unexpected total concurrency: %d", cfg.Concurrency.TotalLLM)
|
t.Fatalf("unexpected total concurrency: %d", cfg.Concurrency.TotalLLM)
|
||||||
}
|
}
|
||||||
if cfg.Diagnostics.WorkDir != "/tmp/notarius-env" || cfg.Diagnostics.Retention != diagnostics.RetentionNever {
|
if cfg.Workspace.Directory != "/var/lib/notarius-env" {
|
||||||
|
t.Fatalf("unexpected workspace directory: %q", cfg.Workspace.Directory)
|
||||||
|
}
|
||||||
|
if cfg.DiagnosticsEnabled() {
|
||||||
|
t.Fatalf("expected workspace diagnostics disabled")
|
||||||
|
}
|
||||||
|
if cfg.Diagnostics.WorkDir != "/var/lib/notarius-env/diagnostics" || cfg.Diagnostics.Retention != diagnostics.RetentionAlways {
|
||||||
t.Fatalf("unexpected diagnostics config: %+v", cfg.Diagnostics)
|
t.Fatalf("unexpected diagnostics config: %+v", cfg.Diagnostics)
|
||||||
}
|
}
|
||||||
|
if !cfg.Workspace.Resume.Enabled {
|
||||||
|
t.Fatalf("expected workspace resume enabled")
|
||||||
|
}
|
||||||
|
if !cfg.Workspace.Debug.Enabled {
|
||||||
|
t.Fatalf("expected workspace debug enabled")
|
||||||
|
}
|
||||||
if cfg.Pipelines["example"].Input.Module != "before" {
|
if cfg.Pipelines["example"].Input.Module != "before" {
|
||||||
t.Fatalf("environment overrides must not change pipeline wiring: %+v", cfg.Pipelines["example"])
|
t.Fatalf("environment overrides must not change pipeline wiring: %+v", cfg.Pipelines["example"])
|
||||||
}
|
}
|
||||||
@@ -46,6 +63,41 @@ func TestApplyEnvOverridesRejectsInvalidIntegers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyEnvOverridesRejectsInvalidBooleans(t *testing.T) {
|
||||||
|
for _, name := range []string{
|
||||||
|
"NOTARIUS_WORKSPACE_DIAGNOSTICS_ENABLED",
|
||||||
|
"NOTARIUS_WORKSPACE_RESUME_ENABLED",
|
||||||
|
"NOTARIUS_WORKSPACE_DEBUG_ENABLED",
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
cfg := Default()
|
||||||
|
err := cfg.applyEnvOverridesWithLookup(mapLookup(map[string]string{name: "maybe"}))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), name) {
|
||||||
|
t.Fatalf("expected named boolean error, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyEnvOverridesLegacyDiagnosticsRemainCompatibleWithoutWorkspace(t *testing.T) {
|
||||||
|
cfg := Default()
|
||||||
|
|
||||||
|
err := cfg.applyEnvOverridesWithLookup(mapLookup(map[string]string{
|
||||||
|
"NOTARIUS_WORK_DIR": "/tmp/notarius-env",
|
||||||
|
"NOTARIUS_DIAGNOSTICS_RETENTION": "never",
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ApplyEnvOverrides: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Diagnostics.WorkDir != "/tmp/notarius-env" {
|
||||||
|
t.Fatalf("diagnostics work dir = %q, want legacy env", cfg.Diagnostics.WorkDir)
|
||||||
|
}
|
||||||
|
if cfg.Diagnostics.Retention != diagnostics.RetentionNever {
|
||||||
|
t.Fatalf("diagnostics retention = %q, want legacy env", cfg.Diagnostics.Retention)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadFromEnvUsesDefaultConfig(t *testing.T) {
|
func TestLoadFromEnvUsesDefaultConfig(t *testing.T) {
|
||||||
t.Setenv("NOTARIUS_TOTAL_LLM_CONCURRENCY", "2")
|
t.Setenv("NOTARIUS_TOTAL_LLM_CONCURRENCY", "2")
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type FileConfig struct {
|
|||||||
Pipelines map[string]FilePipelineProfile `yaml:"pipelines,omitempty"`
|
Pipelines map[string]FilePipelineProfile `yaml:"pipelines,omitempty"`
|
||||||
Concurrency *FileConcurrencyConfig `yaml:"concurrency,omitempty"`
|
Concurrency *FileConcurrencyConfig `yaml:"concurrency,omitempty"`
|
||||||
Diagnostics *FileDiagnosticsConfig `yaml:"diagnostics,omitempty"`
|
Diagnostics *FileDiagnosticsConfig `yaml:"diagnostics,omitempty"`
|
||||||
|
Workspace *FileWorkspaceConfig `yaml:"workspace,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileScriptoriumConfig struct {
|
type FileScriptoriumConfig struct {
|
||||||
@@ -50,6 +51,22 @@ type FileDiagnosticsConfig struct {
|
|||||||
Retention *string `yaml:"retention,omitempty"`
|
Retention *string `yaml:"retention,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileWorkspaceConfig struct {
|
||||||
|
Directory *string `yaml:"directory,omitempty"`
|
||||||
|
Diagnostics *FileWorkspaceDiagnosticsConfig `yaml:"diagnostics,omitempty"`
|
||||||
|
Resume *FileWorkspaceEnabledConfig `yaml:"resume,omitempty"`
|
||||||
|
Debug *FileWorkspaceEnabledConfig `yaml:"debug,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileWorkspaceDiagnosticsConfig struct {
|
||||||
|
Enabled *bool `yaml:"enabled,omitempty"`
|
||||||
|
Retention *string `yaml:"retention,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileWorkspaceEnabledConfig struct {
|
||||||
|
Enabled *bool `yaml:"enabled,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type fileModuleBinding struct {
|
type fileModuleBinding struct {
|
||||||
Module string
|
Module string
|
||||||
LLMProfile string
|
LLMProfile string
|
||||||
@@ -307,6 +324,28 @@ func (c *Config) applyFileConfigWithLookup(fileCfg FileConfig, lookup func(strin
|
|||||||
c.Diagnostics.Retention = diagnostics.RetentionMode(strings.TrimSpace(*fileCfg.Diagnostics.Retention))
|
c.Diagnostics.Retention = diagnostics.RetentionMode(strings.TrimSpace(*fileCfg.Diagnostics.Retention))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if fileCfg.Workspace != nil {
|
||||||
|
if fileCfg.Workspace.Directory != nil {
|
||||||
|
c.Workspace.Directory = strings.TrimSpace(*fileCfg.Workspace.Directory)
|
||||||
|
}
|
||||||
|
if fileCfg.Workspace.Diagnostics != nil {
|
||||||
|
if fileCfg.Workspace.Diagnostics.Enabled != nil {
|
||||||
|
c.Workspace.Diagnostics.Enabled = *fileCfg.Workspace.Diagnostics.Enabled
|
||||||
|
c.Workspace.Diagnostics.enabledSet = true
|
||||||
|
}
|
||||||
|
if fileCfg.Workspace.Diagnostics.Retention != nil {
|
||||||
|
c.Workspace.Diagnostics.Retention = diagnostics.RetentionMode(strings.TrimSpace(*fileCfg.Workspace.Diagnostics.Retention))
|
||||||
|
c.Workspace.Diagnostics.retentionSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileCfg.Workspace.Resume != nil && fileCfg.Workspace.Resume.Enabled != nil {
|
||||||
|
c.Workspace.Resume.Enabled = *fileCfg.Workspace.Resume.Enabled
|
||||||
|
}
|
||||||
|
if fileCfg.Workspace.Debug != nil && fileCfg.Workspace.Debug.Enabled != nil {
|
||||||
|
c.Workspace.Debug.Enabled = *fileCfg.Workspace.Debug.Enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.RecomputeEffectiveDiagnostics()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -554,6 +554,95 @@ diagnostics:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyFileConfigWorkspaceSection(t *testing.T) {
|
||||||
|
cfg := parseAndApplyConfig(t, `
|
||||||
|
version: 2
|
||||||
|
workspace:
|
||||||
|
directory: /var/lib/notarius
|
||||||
|
diagnostics:
|
||||||
|
enabled: false
|
||||||
|
retention: never
|
||||||
|
resume:
|
||||||
|
enabled: true
|
||||||
|
debug:
|
||||||
|
enabled: true
|
||||||
|
diagnostics:
|
||||||
|
work_dir: /tmp/legacy
|
||||||
|
retention: always
|
||||||
|
`)
|
||||||
|
|
||||||
|
if cfg.Workspace.Directory != "/var/lib/notarius" {
|
||||||
|
t.Fatalf("workspace directory = %q, want /var/lib/notarius", cfg.Workspace.Directory)
|
||||||
|
}
|
||||||
|
if cfg.DiagnosticsEnabled() {
|
||||||
|
t.Fatalf("expected diagnostics disabled")
|
||||||
|
}
|
||||||
|
if cfg.Diagnostics.WorkDir != "/var/lib/notarius/diagnostics" {
|
||||||
|
t.Fatalf("effective diagnostics work dir = %q, want workspace diagnostics root", cfg.Diagnostics.WorkDir)
|
||||||
|
}
|
||||||
|
if cfg.Diagnostics.Retention != diagnostics.RetentionNever {
|
||||||
|
t.Fatalf("effective diagnostics retention = %q, want workspace override", cfg.Diagnostics.Retention)
|
||||||
|
}
|
||||||
|
if !cfg.Workspace.Resume.Enabled {
|
||||||
|
t.Fatalf("expected resume enabled")
|
||||||
|
}
|
||||||
|
if !cfg.Workspace.Debug.Enabled {
|
||||||
|
t.Fatalf("expected debug enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyFileConfigLegacyDiagnosticsRemainCompatible(t *testing.T) {
|
||||||
|
cfg := parseAndApplyConfig(t, `
|
||||||
|
version: 2
|
||||||
|
diagnostics:
|
||||||
|
work_dir: /tmp/legacy
|
||||||
|
retention: always
|
||||||
|
`)
|
||||||
|
|
||||||
|
if cfg.Workspace.Directory != "" {
|
||||||
|
t.Fatalf("workspace directory = %q, want unset", cfg.Workspace.Directory)
|
||||||
|
}
|
||||||
|
if !cfg.DiagnosticsEnabled() {
|
||||||
|
t.Fatalf("expected diagnostics enabled")
|
||||||
|
}
|
||||||
|
if cfg.Diagnostics.WorkDir != "/tmp/legacy" {
|
||||||
|
t.Fatalf("effective diagnostics work dir = %q, want legacy", cfg.Diagnostics.WorkDir)
|
||||||
|
}
|
||||||
|
if cfg.Diagnostics.Retention != diagnostics.RetentionAlways {
|
||||||
|
t.Fatalf("effective diagnostics retention = %q, want legacy", cfg.Diagnostics.Retention)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyFileConfigWorkspaceRetentionOverridesLegacyRetentionOnlyWhenSet(t *testing.T) {
|
||||||
|
t.Run("legacy retained", func(t *testing.T) {
|
||||||
|
cfg := parseAndApplyConfig(t, `
|
||||||
|
version: 2
|
||||||
|
workspace:
|
||||||
|
directory: /var/lib/notarius
|
||||||
|
diagnostics:
|
||||||
|
retention: never
|
||||||
|
`)
|
||||||
|
if cfg.Diagnostics.Retention != diagnostics.RetentionNever {
|
||||||
|
t.Fatalf("effective diagnostics retention = %q, want legacy", cfg.Diagnostics.Retention)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("workspace overrides", func(t *testing.T) {
|
||||||
|
cfg := parseAndApplyConfig(t, `
|
||||||
|
version: 2
|
||||||
|
workspace:
|
||||||
|
directory: /var/lib/notarius
|
||||||
|
diagnostics:
|
||||||
|
retention: always
|
||||||
|
diagnostics:
|
||||||
|
retention: never
|
||||||
|
`)
|
||||||
|
if cfg.Diagnostics.Retention != diagnostics.RetentionAlways {
|
||||||
|
t.Fatalf("effective diagnostics retention = %q, want workspace", cfg.Diagnostics.Retention)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func parseAndApplyConfig(t *testing.T, raw string) Config {
|
func parseAndApplyConfig(t *testing.T, raw string) Config {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
fileCfg, err := ParseFileConfigYAML([]byte(raw))
|
fileCfg, err := ParseFileConfigYAML([]byte(raw))
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
func TestRedactedConfigCopiesScriptoriumConfig(t *testing.T) {
|
func TestRedactedConfigCopiesScriptoriumConfig(t *testing.T) {
|
||||||
cfg := Default()
|
cfg := Default()
|
||||||
cfg.Scriptorium.ProfileDir = "./profiles"
|
cfg.Scriptorium.ProfileDir = "./profiles"
|
||||||
|
cfg.Workspace.Directory = "/var/lib/notarius"
|
||||||
|
cfg.Workspace.Resume.Enabled = true
|
||||||
|
|
||||||
redacted := cfg.Redacted()
|
redacted := cfg.Redacted()
|
||||||
|
|
||||||
@@ -20,6 +22,13 @@ func TestRedactedConfigCopiesScriptoriumConfig(t *testing.T) {
|
|||||||
if cfg.Scriptorium.ProfileDir != "./profiles" {
|
if cfg.Scriptorium.ProfileDir != "./profiles" {
|
||||||
t.Fatalf("redaction mutated original config")
|
t.Fatalf("redaction mutated original config")
|
||||||
}
|
}
|
||||||
|
if redacted.Workspace.Directory != "/var/lib/notarius" || !redacted.Workspace.Resume.Enabled {
|
||||||
|
t.Fatalf("expected workspace config preserved, got %+v", redacted.Workspace)
|
||||||
|
}
|
||||||
|
redacted.Workspace.Directory = "/changed"
|
||||||
|
if cfg.Workspace.Directory != "/var/lib/notarius" {
|
||||||
|
t.Fatalf("redaction mutated original workspace config")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigRedactedDiagnosticsPayloadCopiesConfig(t *testing.T) {
|
func TestConfigRedactedDiagnosticsPayloadCopiesConfig(t *testing.T) {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ func (c Config) Validate() error {
|
|||||||
if err := validateScriptorium(c.Scriptorium); err != nil {
|
if err := validateScriptorium(c.Scriptorium); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateWorkspace(c.Workspace); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := validateDiagnostics(c.Diagnostics); err != nil {
|
if err := validateDiagnostics(c.Diagnostics); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -28,6 +31,17 @@ func validateScriptorium(cfg ScriptoriumConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateWorkspace(cfg WorkspaceConfig) error {
|
||||||
|
if cfg.Diagnostics.retentionSet {
|
||||||
|
switch cfg.Diagnostics.Retention {
|
||||||
|
case "", diagnostics.RetentionAuto, diagnostics.RetentionAlways, diagnostics.RetentionNever:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("workspace diagnostics retention %q is not supported", cfg.Diagnostics.Retention)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateDiagnostics(cfg DiagnosticsConfig) error {
|
func validateDiagnostics(cfg DiagnosticsConfig) error {
|
||||||
if strings.TrimSpace(cfg.WorkDir) == "" {
|
if strings.TrimSpace(cfg.WorkDir) == "" {
|
||||||
return fmt.Errorf("diagnostics work dir must not be empty")
|
return fmt.Errorf("diagnostics work dir must not be empty")
|
||||||
|
|||||||
@@ -99,6 +99,17 @@ func TestValidateRejectsInvalidDiagnosticsRetention(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateRejectsInvalidWorkspaceDiagnosticsRetention(t *testing.T) {
|
||||||
|
cfg := validConfig()
|
||||||
|
cfg.Workspace.Diagnostics.Retention = diagnostics.RetentionMode("sometimes")
|
||||||
|
cfg.Workspace.Diagnostics.retentionSet = true
|
||||||
|
|
||||||
|
err := cfg.Validate()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "workspace diagnostics retention") {
|
||||||
|
t.Fatalf("expected workspace retention error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateRejectsInvalidReferenceMaps(t *testing.T) {
|
func TestValidateRejectsInvalidReferenceMaps(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user