Add typed Notarius cache controls

This commit is contained in:
2026-09-16 17:42:16 +00:00
parent f7fc64db10
commit 5086596f94
7 changed files with 165 additions and 2 deletions

View File

@@ -310,6 +310,8 @@ type NotariusConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"`
Binary string `yaml:"binary" json:"binary"`
LLMAPIKey *configkit.Secret `yaml:"llm_api_key,omitempty" json:"llm_api_key,omitempty"`
Resume bool `yaml:"resume,omitempty" json:"resume,omitempty"`
ChunkCache string `yaml:"chunk_cache,omitempty" json:"chunk_cache,omitempty"`
ConfigPath string `yaml:"config_path" json:"config_path"`
PipelineID string `yaml:"pipeline_id" json:"pipeline_id"`
Timeout string `yaml:"timeout" json:"timeout"`

View File

@@ -56,6 +56,8 @@ func (cfg NotariusConfig) MarshalYAML() (any, error) {
Enabled bool `yaml:"enabled"`
Binary string `yaml:"binary"`
LLMAPIKey *string `yaml:"llm_api_key,omitempty"`
Resume bool `yaml:"resume,omitempty"`
ChunkCache string `yaml:"chunk_cache,omitempty"`
ConfigPath string `yaml:"config_path"`
PipelineID string `yaml:"pipeline_id"`
Timeout string `yaml:"timeout"`
@@ -65,6 +67,7 @@ func (cfg NotariusConfig) MarshalYAML() (any, error) {
}
return safeNotarius{
Enabled: cfg.Enabled, Binary: cfg.Binary, LLMAPIKey: secretReferenceName(cfg.LLMAPIKey),
Resume: cfg.Resume, ChunkCache: cfg.ChunkCache,
ConfigPath: cfg.ConfigPath, PipelineID: cfg.PipelineID, Timeout: cfg.Timeout,
WorkingDirectory: cfg.WorkingDirectory, References: cfg.References, Outputs: cfg.Outputs,
}, nil

View File

@@ -1,12 +1,15 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"gopkg.in/yaml.v3"
)
func TestNotariusOmittedAndDisabledBehavior(t *testing.T) {
@@ -45,6 +48,9 @@ notarius:
if disabled.Notarius.Binary != DefaultNotariusBinary || disabled.Notarius.Timeout != DefaultNotariusTimeout {
t.Fatalf("disabled defaults = %#v", disabled.Notarius)
}
if disabled.Notarius.Resume || disabled.Notarius.ChunkCache != "" {
t.Fatalf("disabled cache-control defaults = %#v", disabled.Notarius)
}
if disabled.Notarius.ConfigPath != "relative/notarius.yml" || disabled.Notarius.WorkingDirectory != "" {
t.Fatalf("disabled paths were resolved unexpectedly: %#v", disabled.Notarius)
}
@@ -187,6 +193,89 @@ notarius:
}
}
func TestNotariusCacheControlNormalizationAndValidation(t *testing.T) {
for _, mode := range []string{"", "auto", "bypass", "refresh"} {
t.Run("mode "+mode, func(t *testing.T) {
cfg := validNotariusConfig()
cfg.Resume = true
if mode != "" {
cfg.ChunkCache = " " + mode + " "
}
if err := NormalizePipelineConfig(&PipelineConfig{Notarius: cfg}); err != nil {
t.Fatalf("NormalizePipelineConfig() error = %v", err)
}
if cfg.ChunkCache != mode {
t.Fatalf("ChunkCache = %q, want %q", cfg.ChunkCache, mode)
}
if !cfg.Resume {
t.Fatal("Resume = false, want true")
}
if err := validateNotarius(cfg, nil); err != nil {
t.Fatalf("validateNotarius() error = %v", err)
}
})
}
for _, mode := range []string{"AUTO", "invalid"} {
t.Run("invalid "+mode, func(t *testing.T) {
cfg := validNotariusConfig()
cfg.ChunkCache = mode
err := validateNotarius(cfg, nil)
if err == nil || !strings.Contains(err.Error(), "pipeline.notarius.chunk_cache") {
t.Fatalf("validateNotarius() error = %v, want chunk_cache error", err)
}
})
}
disabled := &NotariusConfig{Enabled: false, Resume: true, ChunkCache: " INVALID "}
if err := NormalizePipelineConfig(&PipelineConfig{Notarius: disabled}); err != nil {
t.Fatalf("NormalizePipelineConfig(disabled) error = %v", err)
}
if disabled.ChunkCache != " INVALID " {
t.Fatalf("disabled ChunkCache = %q, want unchanged", disabled.ChunkCache)
}
if err := validateNotarius(disabled, nil); err != nil {
t.Fatalf("validateNotarius(disabled) error = %v", err)
}
}
func TestNotariusCacheControlsSafeSerialization(t *testing.T) {
cfg := validNotariusConfig()
cfg.Resume = true
cfg.ChunkCache = "refresh"
yamlData, err := yaml.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
jsonData, err := json.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(yamlData), "resume: true") || !strings.Contains(string(yamlData), "chunk_cache: refresh") {
t.Fatalf("YAML omitted cache controls: %s", yamlData)
}
if !strings.Contains(string(jsonData), `"resume":true`) || !strings.Contains(string(jsonData), `"chunk_cache":"refresh"`) {
t.Fatalf("JSON omitted cache controls: %s", jsonData)
}
cfg.Resume = false
cfg.ChunkCache = ""
yamlData, err = yaml.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
jsonData, err = json.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
for _, serialized := range []string{string(yamlData), string(jsonData)} {
if strings.Contains(serialized, "resume") || strings.Contains(serialized, "chunk_cache") {
t.Fatalf("zero-value cache controls were serialized: %s", serialized)
}
}
}
func TestNotariusRelativePathsDoNotMakeEffectiveDigestLocationDependent(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + `
notarius:

View File

@@ -182,6 +182,57 @@ func TestNotariusCredentialRootProvenance(t *testing.T) {
t.Fatal("Notarius credential provenance did not identify the root pipeline")
}
func TestNotariusCacheControlProfileReplacementAndNullReset(t *testing.T) {
tests := []struct {
name string
overlay string
wantResume bool
wantMode string
wantSource bool
wantNil bool
}{
{name: "replacement", overlay: "notarius:\n resume: true\n chunk_cache: refresh\n", wantResume: true, wantMode: "refresh", wantSource: true},
{name: "null reset", overlay: "notarius: null\n", wantNil: true},
}
for _, test := range tests {
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\nnotarius:\n enabled: false\n resume: false\n chunk_cache: bypass\n")
overlay := writePipelineFixture(t, dir, "profiles/testing.yml", test.overlay)
profile := "testing"
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Profile: &profile})
if err != nil {
t.Fatal(err)
}
if test.wantNil {
if cfg.Notarius != nil {
t.Fatalf("Notarius = %#v, want null reset", cfg.Notarius)
}
return
}
if cfg.Notarius == nil || cfg.Notarius.Resume != test.wantResume || cfg.Notarius.ChunkCache != test.wantMode {
t.Fatalf("cache controls = resume %v, mode %q; want resume %v, mode %q", cfg.Notarius.Resume, cfg.Notarius.ChunkCache, test.wantResume, test.wantMode)
}
if !test.wantSource {
return
}
sources, err := EffectivePipelineSources(cfg, ResolvedParty{})
if err != nil {
t.Fatal(err)
}
found := map[string]bool{}
for _, source := range sources {
if source.Path == "notarius.resume" || source.Path == "notarius.chunk_cache" {
found[source.Path] = source.Source == overlay
}
}
if !found["notarius.resume"] || !found["notarius.chunk_cache"] {
t.Fatalf("cache-control provenance did not identify profile: %#v", found)
}
})
}
}
func TestPipelineConfigKitAllowsOneBaseDeclarationWithProfileOverride(t *testing.T) {
dir := t.TempDir()
writePipelineFixture(t, dir, "pipeline.yml", `configkit:

View File

@@ -14,6 +14,7 @@ func NormalizePipelineConfig(cfg *PipelineConfig) error {
}
notarius := *cfg.Notarius
notarius.Binary = strings.TrimSpace(notarius.Binary)
notarius.ChunkCache = strings.TrimSpace(notarius.ChunkCache)
notarius.ConfigPath = cleanNotariusPath(notarius.ConfigPath)
notarius.PipelineID = strings.TrimSpace(notarius.PipelineID)
notarius.Timeout = strings.TrimSpace(notarius.Timeout)

View File

@@ -39,10 +39,12 @@ func validateNotarius(cfg *NotariusConfig, scriptorium *ScriptoriumConfig) error
if len(cfg.References) > MaxNotariusReferenceBindings {
return fmt.Errorf("pipeline.notarius.references must contain at most %d bindings", MaxNotariusReferenceBindings)
}
if err := validateNotariusCanonicalScalars(cfg); err != nil {
return err
}
if !validNotariusChunkCacheMode(cfg.ChunkCache) {
return fmt.Errorf("pipeline.notarius.chunk_cache must be empty, auto, bypass, or refresh")
}
if err := validateNotariusReferences(cfg.References); err != nil {
return err
}
@@ -56,6 +58,7 @@ func validateNotariusCanonicalScalars(cfg *NotariusConfig) error {
want string
}{
{name: "binary", got: cfg.Binary, want: strings.TrimSpace(cfg.Binary)},
{name: "chunk_cache", got: cfg.ChunkCache, want: strings.TrimSpace(cfg.ChunkCache)},
{name: "config_path", got: cfg.ConfigPath, want: cleanNotariusPath(cfg.ConfigPath)},
{name: "pipeline_id", got: cfg.PipelineID, want: strings.TrimSpace(cfg.PipelineID)},
{name: "timeout", got: cfg.Timeout, want: strings.TrimSpace(cfg.Timeout)},
@@ -69,6 +72,15 @@ func validateNotariusCanonicalScalars(cfg *NotariusConfig) error {
return nil
}
func validNotariusChunkCacheMode(mode string) bool {
switch mode {
case "", "auto", "bypass", "refresh":
return true
default:
return false
}
}
func normalizeNotariusReferences(references map[string]string) (map[string]string, error) {
selectors := make([]string, 0, len(references))
for selector := range references {