Make configuration truthful and clean remote session files

This commit is contained in:
2026-08-10 21:41:00 +00:00
parent 72a200968a
commit 32653f54f9
32 changed files with 395 additions and 72 deletions

View File

@@ -11,7 +11,7 @@ func TestCacheDefaults(t *testing.T) {
whisperx:
transcribe_url: https://example.com/transcribe
notification:
timeout: 10s
mode: noop
`, `session_id: 2026-05-03
inputs:
audio_dir: ./audio

View File

@@ -18,7 +18,7 @@ campaigns:
whisperx:
transcribe_url: https://example.com/transcribe
notification:
timeout: 10s
mode: noop
`
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
t.Fatalf("write pipeline.yml: %v", err)
@@ -196,7 +196,7 @@ func writeCampaignConfigTestFiles(t *testing.T, campaignYAML, sessionYAML string
campaignPath := filepath.Join(dir, "campaign.yml")
sessionPath := filepath.Join(dir, "session.yml")
pipelineYAML := "workspace:\n root: " + filepath.ToSlash(filepath.Join(dir, "work")) + "\nwhisperx:\n transcribe_url: https://example.com/transcribe\nnotification:\n timeout: 10s\n"
pipelineYAML := "workspace:\n root: " + filepath.ToSlash(filepath.Join(dir, "work")) + "\nwhisperx:\n transcribe_url: https://example.com/transcribe\nnotification:\n mode: noop\n"
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
t.Fatalf("write pipeline.yml: %v", err)
}

View File

@@ -255,8 +255,6 @@ type ScriptoriumArtifactConfig struct {
// ScriptoriumInputConfig configures one named prompt input source.
type ScriptoriumInputConfig struct {
Source string `yaml:"source"`
Artifact string `yaml:"artifact"`
Path string `yaml:"path"`
Required bool `yaml:"required"`
}
@@ -280,11 +278,9 @@ type NotariusOutputConfig struct {
ModuleKey string `yaml:"module_key"`
}
// NotificationConfig configures notification backend settings.
// NotificationConfig configures the supported notification behavior.
type NotificationConfig struct {
Backend string `yaml:"backend"`
Recipient string `yaml:"recipient"`
Timeout string `yaml:"timeout"`
Mode string `yaml:"mode"`
}
// SessionInputsConfig contains per-session input references.
@@ -329,5 +325,4 @@ type SessionSource struct {
S3Key string
S3Size int64
S3ETag string
SpoolPath string
}

View File

@@ -62,6 +62,7 @@ const (
DefaultArchiveEnabled = true
DefaultArchiveUploadRun = true
DefaultNotificationMode = "noop"
PathWorkDirSegment = "work"
PathInputsDirSegment = "inputs"

View File

@@ -378,6 +378,16 @@ func applyPipelineDefaults(cfg *PipelineConfig) {
applyRenderDefaults(&cfg.Render)
applyScriptoriumDefaults(cfg.Scriptorium)
applyNotariusDefaults(cfg.Notarius)
applyNotificationDefaults(&cfg.Notification)
}
func applyNotificationDefaults(cfg *NotificationConfig) {
if cfg == nil {
return
}
if strings.TrimSpace(cfg.Mode) == "" {
cfg.Mode = DefaultNotificationMode
}
}
func applyCampaignsDefaults(cfg *CampaignsConfig) {

View File

@@ -0,0 +1,68 @@
package config
import (
"strings"
"testing"
)
func TestNotificationConfigSupportsOnlyNoopMode(t *testing.T) {
tests := []struct {
name string
section string
wantLoad string
wantValidate string
}{
{
name: "default noop mode",
section: "",
},
{
name: "explicit noop mode",
section: "notification:\n mode: noop\n",
},
{
name: "backend is rejected",
section: "notification:\n backend: email\n",
wantLoad: "field backend not found",
},
{
name: "recipient is rejected",
section: "notification:\n recipient: party@example.com\n",
wantLoad: "field recipient not found",
},
{
name: "provider mode is rejected",
section: "notification:\n mode: email\n",
wantValidate: "pipeline.notification.mode must be \"noop\"",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pipelinePath, sessionPath := writeConfigFiles(t, testPipelineBaseYAML+"\n"+tt.section, testSessionBaseYAML)
cfg, err := Load(pipelinePath, sessionPath)
if tt.wantLoad != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantLoad) {
t.Fatalf("Load() error = %v, want %q", err, tt.wantLoad)
}
return
}
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if got := cfg.Pipeline.Notification.Mode; tt.wantValidate == "" && got != DefaultNotificationMode {
t.Fatalf("notification.mode = %q, want %q", got, DefaultNotificationMode)
}
err = Validate(cfg)
if tt.wantValidate != "" {
if err == nil || !strings.Contains(err.Error(), tt.wantValidate) {
t.Fatalf("Validate() error = %v, want %q", err, tt.wantValidate)
}
return
}
if err != nil {
t.Fatalf("Validate() error = %v", err)
}
})
}
}

View File

@@ -95,7 +95,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
wantValidateErr: "pipeline.scriptorium.timeout must be a valid duration",
},
{
name: "legacy previous session artifact source fails validation",
name: "legacy previous session source fails validation",
scriptoriumYAML: `scriptorium:
binary: scriptorium
artifacts:
@@ -109,8 +109,6 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
required: true
previous_recap:
source: ` + legacyPreviousSource + `
artifact: session_recap
path: ""
required: false
vars:
session_id: true
@@ -118,6 +116,21 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
`,
wantValidateErr: `pipeline.scriptorium.artifacts.session_recap.inputs.previous_recap.source "` + legacyPreviousSource + `" is unsupported`,
},
{
name: "obsolete input passthrough fields fail strict decoding",
scriptoriumYAML: `scriptorium:
artifacts:
session_recap:
enabled: true
prompt_id: dnd.session_recap
output_path: artifacts/session_recap.md
inputs:
transcript:
source: narratio.transcript.polished
artifact: session_recap
`,
wantLoadErr: "field artifact not found",
},
{
name: "canonical previous-session source is accepted",
scriptoriumYAML: `scriptorium:

View File

@@ -122,13 +122,20 @@ func validatePipeline(cfg *PipelineConfig) error {
if err := validateScriptorium(cfg.Scriptorium, cfg.Notarius); err != nil {
return err
}
if err := validateDuration("pipeline.notification.timeout", cfg.Notification.Timeout); err != nil {
if err := validateNotification(cfg.Notification); err != nil {
return err
}
return nil
}
func validateNotification(cfg NotificationConfig) error {
if strings.EqualFold(strings.TrimSpace(cfg.Mode), DefaultNotificationMode) {
return nil
}
return fmt.Errorf("pipeline.notification.mode must be %q until a notification provider is configured", DefaultNotificationMode)
}
func validateStorage(cfg StorageConfig) error {
backend := strings.ToLower(strings.TrimSpace(cfg.Backend))
switch backend {

View File

@@ -23,7 +23,6 @@ func TestValidateDurationsRequirePositiveValues(t *testing.T) {
p.Scriptorium.Artifacts["session_recap"] = ScriptoriumArtifactConfig{Timeout: value}
}, want: "pipeline.scriptorium.artifacts.session_recap.timeout"},
{name: "trim bounds timeout", set: func(p *PipelineConfig, value string) { p.Trim.Bounds.Timeout = value }, want: "pipeline.trim.bounds.timeout"},
{name: "notification timeout", set: func(p *PipelineConfig, value string) { p.Notification.Timeout = value }, want: "pipeline.notification.timeout"},
}
for _, value := range []string{"0s", "-1ms"} {
@@ -82,7 +81,6 @@ func TestValidateDurationRejectsOverflowAndAcceptsPositiveSubsecondValues(t *tes
cfg.Pipeline.Audita.Timeout = "1ms"
cfg.Pipeline.Scriptorium.Timeout = "1ms"
cfg.Pipeline.Trim.Bounds.Timeout = "1ms"
cfg.Pipeline.Notification.Timeout = "1ms"
if cfg.Pipeline.Scriptorium.Artifacts == nil {
cfg.Pipeline.Scriptorium.Artifacts = map[string]ScriptoriumArtifactConfig{}
}