Add Notarius configuration profile support
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Implementation Plan: Notarius Configuration Profile Selection
|
# Implementation Plan: Notarius Configuration Profile Selection
|
||||||
|
|
||||||
Status: Ready for implementation; all stages pending.
|
Status: Implementation in progress; Stage 1 complete.
|
||||||
|
|
||||||
## Scope and Working Instructions
|
## Scope and Working Instructions
|
||||||
|
|
||||||
@@ -56,7 +56,12 @@ the feature request:
|
|||||||
|
|
||||||
## Stage 1: Add Configuration and Adapter Support
|
## Stage 1: Add Configuration and Adapter Support
|
||||||
|
|
||||||
Status: Pending.
|
Status: Complete.
|
||||||
|
|
||||||
|
Completion marker (2026-09-21): Added composed optional configuration and safe
|
||||||
|
inspection support, adapter request/argument forwarding, copy auditing, and
|
||||||
|
focused configuration and subprocess coverage. Verified with
|
||||||
|
`go test ./internal/config ./internal/adapters/notarius`.
|
||||||
|
|
||||||
Preparation:
|
Preparation:
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ func (f *FakeRunner) Run(ctx context.Context, req RunRequest) (RunResult, error)
|
|||||||
return RunResult{}, err
|
return RunResult{}, err
|
||||||
}
|
}
|
||||||
copyRequest := req
|
copyRequest := req
|
||||||
|
if req.ConfigProfile != nil {
|
||||||
|
profile := *req.ConfigProfile
|
||||||
|
copyRequest.ConfigProfile = &profile
|
||||||
|
}
|
||||||
copyRequest.References = append([]ReferenceBinding(nil), req.References...)
|
copyRequest.References = append([]ReferenceBinding(nil), req.References...)
|
||||||
f.Requests = append(f.Requests, copyRequest)
|
f.Requests = append(f.Requests, copyRequest)
|
||||||
if f.Err != nil {
|
if f.Err != nil {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type RunRequest struct {
|
|||||||
Binary string
|
Binary string
|
||||||
// LLMAPIKey is an optional already-resolved credential reference.
|
// LLMAPIKey is an optional already-resolved credential reference.
|
||||||
LLMAPIKey *configkit.Secret
|
LLMAPIKey *configkit.Secret
|
||||||
|
ConfigProfile *string
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
PipelineID string
|
PipelineID string
|
||||||
InputPath string
|
InputPath string
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ func (r *SubprocessRunner) Run(ctx context.Context, req RunRequest) (RunResult,
|
|||||||
for _, reference := range references {
|
for _, reference := range references {
|
||||||
args = append(args, "--reference", reference.Selector+"="+reference.Path)
|
args = append(args, "--reference", reference.Selector+"="+reference.Path)
|
||||||
}
|
}
|
||||||
|
if req.ConfigProfile != nil {
|
||||||
|
args = append(args, "--config-profile", *req.ConfigProfile)
|
||||||
|
}
|
||||||
args = append(args, "--json")
|
args = append(args, "--json")
|
||||||
processResult, err := r.run(ctx, subprocess.RunRequest{
|
processResult, err := r.run(ctx, subprocess.RunRequest{
|
||||||
Executable: req.Binary,
|
Executable: req.Binary,
|
||||||
|
|||||||
@@ -118,6 +118,53 @@ func TestSubprocessRunnerMapsCacheControlsToArguments(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerMapsConfigProfileToArgumentPair(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
supplied bool
|
||||||
|
value string
|
||||||
|
}{
|
||||||
|
{name: "omitted"},
|
||||||
|
{name: "empty", supplied: true},
|
||||||
|
{name: "default", supplied: true, value: "default"},
|
||||||
|
{name: "named", supplied: true, value: "inexpensive"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
req := validRunRequest(t)
|
||||||
|
if test.supplied {
|
||||||
|
req.ConfigProfile = &test.value
|
||||||
|
}
|
||||||
|
var captured sharedsubprocess.RunRequest
|
||||||
|
runner := &SubprocessRunner{run: func(_ context.Context, processReq sharedsubprocess.RunRequest) (sharedsubprocess.RunResult, error) {
|
||||||
|
captured = processReq
|
||||||
|
writeValidBundleAndReceipt(t, req, false)
|
||||||
|
return sharedsubprocess.RunResult{ExitCode: 0}, nil
|
||||||
|
}}
|
||||||
|
|
||||||
|
if _, err := runner.Run(context.Background(), req); err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
flagIndex := -1
|
||||||
|
for index, arg := range captured.Args {
|
||||||
|
if arg == "--config-profile" {
|
||||||
|
flagIndex = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !test.supplied {
|
||||||
|
if flagIndex >= 0 {
|
||||||
|
t.Fatalf("subprocess args unexpectedly contain config profile: %#v", captured.Args)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if flagIndex < 0 || flagIndex+2 >= len(captured.Args) || captured.Args[flagIndex+1] != test.value || captured.Args[flagIndex+2] != "--json" {
|
||||||
|
t.Fatalf("config profile argument boundary = %#v, want flag, exact value, then --json", captured.Args)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSubprocessRunnerRejectsInvalidChunkCacheBeforeLaunch(t *testing.T) {
|
func TestSubprocessRunnerRejectsInvalidChunkCacheBeforeLaunch(t *testing.T) {
|
||||||
for _, mode := range []string{"AUTO", "invalid", " auto "} {
|
for _, mode := range []string{"AUTO", "invalid", " auto "} {
|
||||||
t.Run(mode, func(t *testing.T) {
|
t.Run(mode, func(t *testing.T) {
|
||||||
@@ -388,6 +435,10 @@ func TestSubprocessRunnerReturnsProcessFailuresWithoutParsingStdout(t *testing.T
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
req := validRunRequest(t)
|
req := validRunRequest(t)
|
||||||
|
if test.name == "nonzero" {
|
||||||
|
profile := "missing-profile"
|
||||||
|
req.ConfigProfile = &profile
|
||||||
|
}
|
||||||
req.Binary = writeShellScript(t, "#!/bin/sh\n"+test.scriptBody)
|
req.Binary = writeShellScript(t, "#!/bin/sh\n"+test.scriptBody)
|
||||||
req.Timeout = test.timeout
|
req.Timeout = test.timeout
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -684,7 +735,8 @@ func TestLoadDiagnosticSummariesValidateBoundsAndTolerateUnknownFields(t *testin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFakeRunnerCapturesRequestsAndHonorsContextAndError(t *testing.T) {
|
func TestFakeRunnerCapturesRequestsAndHonorsContextAndError(t *testing.T) {
|
||||||
req := RunRequest{PipelineID: "pipeline", References: []ReferenceBinding{{Selector: "party", Path: "/references/party.json"}}}
|
profile := "testing"
|
||||||
|
req := RunRequest{PipelineID: "pipeline", ConfigProfile: &profile, References: []ReferenceBinding{{Selector: "party", Path: "/references/party.json"}}}
|
||||||
want := RunResult{BundleRoot: "/bundle"}
|
want := RunResult{BundleRoot: "/bundle"}
|
||||||
fake := &FakeRunner{Result: want}
|
fake := &FakeRunner{Result: want}
|
||||||
got, err := fake.Run(context.Background(), req)
|
got, err := fake.Run(context.Background(), req)
|
||||||
@@ -695,6 +747,10 @@ func TestFakeRunnerCapturesRequestsAndHonorsContextAndError(t *testing.T) {
|
|||||||
if fake.Requests[0].References[0].Path != "/references/party.json" {
|
if fake.Requests[0].References[0].Path != "/references/party.json" {
|
||||||
t.Fatalf("fake retained aliased request references: %#v", fake.Requests[0].References)
|
t.Fatalf("fake retained aliased request references: %#v", fake.Requests[0].References)
|
||||||
}
|
}
|
||||||
|
profile = "changed"
|
||||||
|
if *fake.Requests[0].ConfigProfile != "testing" {
|
||||||
|
t.Fatalf("fake retained aliased config profile: %q", *fake.Requests[0].ConfigProfile)
|
||||||
|
}
|
||||||
|
|
||||||
wantErr := errors.New("configured failure")
|
wantErr := errors.New("configured failure")
|
||||||
fake.Err = wantErr
|
fake.Err = wantErr
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ type NotariusConfig struct {
|
|||||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||||
Binary string `yaml:"binary" json:"binary"`
|
Binary string `yaml:"binary" json:"binary"`
|
||||||
LLMAPIKey *configkit.Secret `yaml:"llm_api_key,omitempty" json:"llm_api_key,omitempty"`
|
LLMAPIKey *configkit.Secret `yaml:"llm_api_key,omitempty" json:"llm_api_key,omitempty"`
|
||||||
|
ConfigProfile *string `yaml:"config_profile,omitempty" json:"config_profile,omitempty"`
|
||||||
Resume bool `yaml:"resume,omitempty" json:"resume,omitempty"`
|
Resume bool `yaml:"resume,omitempty" json:"resume,omitempty"`
|
||||||
ChunkCache string `yaml:"chunk_cache,omitempty" json:"chunk_cache,omitempty"`
|
ChunkCache string `yaml:"chunk_cache,omitempty" json:"chunk_cache,omitempty"`
|
||||||
ConfigPath string `yaml:"config_path" json:"config_path"`
|
ConfigPath string `yaml:"config_path" json:"config_path"`
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ func (cfg NotariusConfig) MarshalYAML() (any, error) {
|
|||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
Binary string `yaml:"binary"`
|
Binary string `yaml:"binary"`
|
||||||
LLMAPIKey *string `yaml:"llm_api_key,omitempty"`
|
LLMAPIKey *string `yaml:"llm_api_key,omitempty"`
|
||||||
|
ConfigProfile *string `yaml:"config_profile,omitempty"`
|
||||||
Resume bool `yaml:"resume,omitempty"`
|
Resume bool `yaml:"resume,omitempty"`
|
||||||
ChunkCache string `yaml:"chunk_cache,omitempty"`
|
ChunkCache string `yaml:"chunk_cache,omitempty"`
|
||||||
ConfigPath string `yaml:"config_path"`
|
ConfigPath string `yaml:"config_path"`
|
||||||
@@ -67,7 +68,8 @@ func (cfg NotariusConfig) MarshalYAML() (any, error) {
|
|||||||
}
|
}
|
||||||
return safeNotarius{
|
return safeNotarius{
|
||||||
Enabled: cfg.Enabled, Binary: cfg.Binary, LLMAPIKey: secretReferenceName(cfg.LLMAPIKey),
|
Enabled: cfg.Enabled, Binary: cfg.Binary, LLMAPIKey: secretReferenceName(cfg.LLMAPIKey),
|
||||||
Resume: cfg.Resume, ChunkCache: cfg.ChunkCache,
|
ConfigProfile: cfg.ConfigProfile,
|
||||||
|
Resume: cfg.Resume, ChunkCache: cfg.ChunkCache,
|
||||||
ConfigPath: cfg.ConfigPath, PipelineID: cfg.PipelineID, Timeout: cfg.Timeout,
|
ConfigPath: cfg.ConfigPath, PipelineID: cfg.PipelineID, Timeout: cfg.Timeout,
|
||||||
WorkingDirectory: cfg.WorkingDirectory, References: cfg.References, Outputs: cfg.Outputs,
|
WorkingDirectory: cfg.WorkingDirectory, References: cfg.References, Outputs: cfg.Outputs,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ storage:
|
|||||||
notarius:
|
notarius:
|
||||||
enabled: false
|
enabled: false
|
||||||
llm_api_key: NARRATIO_TEST_NOTARIUS_KEY
|
llm_api_key: NARRATIO_TEST_NOTARIUS_KEY
|
||||||
|
config_profile: ""
|
||||||
`
|
`
|
||||||
pipelinePath, _ := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
pipelinePath, _ := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
||||||
|
|
||||||
@@ -140,6 +141,24 @@ notarius:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(string(firstYAML), "config_profile: \"\"\n") {
|
||||||
|
t.Fatalf("effective YAML omitted explicit empty Notarius config profile: %q", firstYAML)
|
||||||
|
}
|
||||||
|
var roundTrip struct {
|
||||||
|
Notarius *struct {
|
||||||
|
LLMAPIKey *string `yaml:"llm_api_key"`
|
||||||
|
ConfigProfile *string `yaml:"config_profile"`
|
||||||
|
} `yaml:"notarius"`
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(firstYAML, &roundTrip); err != nil {
|
||||||
|
t.Fatalf("round-trip effective YAML: %v", err)
|
||||||
|
}
|
||||||
|
if roundTrip.Notarius == nil || roundTrip.Notarius.ConfigProfile == nil || *roundTrip.Notarius.ConfigProfile != "" {
|
||||||
|
t.Fatalf("round-trip config profile = %#v, want explicit empty", roundTrip.Notarius)
|
||||||
|
}
|
||||||
|
if roundTrip.Notarius.LLMAPIKey == nil || *roundTrip.Notarius.LLMAPIKey != "NARRATIO_TEST_NOTARIUS_KEY" {
|
||||||
|
t.Fatalf("round-trip credential reference = %#v, want safe public name", roundTrip.Notarius.LLMAPIKey)
|
||||||
|
}
|
||||||
|
|
||||||
t.Setenv("NARRATIO_TEST_ACCESS_KEY", secondCanary)
|
t.Setenv("NARRATIO_TEST_ACCESS_KEY", secondCanary)
|
||||||
t.Setenv("NARRATIO_TEST_SECRET_KEY", secondCanary)
|
t.Setenv("NARRATIO_TEST_SECRET_KEY", secondCanary)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -161,6 +162,45 @@ func TestNotariusCredentialProfileReplacementAndProvenance(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNotariusConfigProfileComposition(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
baseValue string
|
||||||
|
overlayName string
|
||||||
|
overlay string
|
||||||
|
want *string
|
||||||
|
}{
|
||||||
|
{name: "omitted"},
|
||||||
|
{name: "explicit empty", baseValue: `""`, want: stringPointer("")},
|
||||||
|
{name: "explicit default", baseValue: "default", want: stringPointer("default")},
|
||||||
|
{name: "base named selection", baseValue: "production", want: stringPointer("production")},
|
||||||
|
{name: "profile replaces named selection", baseValue: "production", overlayName: "testing", overlay: "notarius:\n config_profile: inexpensive\n", want: stringPointer("inexpensive")},
|
||||||
|
{name: "profile clears named selection", baseValue: "production", overlayName: "testing", overlay: "notarius:\n config_profile: null\n"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
rootYAML := "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n"
|
||||||
|
if test.baseValue != "" {
|
||||||
|
rootYAML += " config_profile: " + test.baseValue + "\n"
|
||||||
|
}
|
||||||
|
root := writePipelineFixture(t, dir, "pipeline.yml", rootYAML)
|
||||||
|
options := PipelineLoadOptions{}
|
||||||
|
if test.overlayName != "" {
|
||||||
|
writePipelineFixture(t, dir, "profiles/"+test.overlayName+".yml", test.overlay)
|
||||||
|
options.Profile = &test.overlayName
|
||||||
|
}
|
||||||
|
cfg, err := LoadPipelineWithOptions(root, options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cfg.Notarius == nil || !reflect.DeepEqual(cfg.Notarius.ConfigProfile, test.want) {
|
||||||
|
t.Fatalf("config profile = %#v, want %#v", cfg.Notarius.ConfigProfile, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNotariusCredentialRootProvenance(t *testing.T) {
|
func TestNotariusCredentialRootProvenance(t *testing.T) {
|
||||||
const envName = "NARRATIO_NOTARIUS_ROOT_KEY"
|
const envName = "NARRATIO_NOTARIUS_ROOT_KEY"
|
||||||
t.Setenv(envName, "synthetic-notarius-root-key")
|
t.Setenv(envName, "synthetic-notarius-root-key")
|
||||||
|
|||||||
Reference in New Issue
Block a user