Make configuration truthful and clean remote session files
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ const (
|
||||
|
||||
DefaultArchiveEnabled = true
|
||||
DefaultArchiveUploadRun = true
|
||||
DefaultNotificationMode = "noop"
|
||||
|
||||
PathWorkDirSegment = "work"
|
||||
PathInputsDirSegment = "inputs"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
68
internal/config/notification_test.go
Normal file
68
internal/config/notification_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user