Forward Notarius cache controls during extraction

This commit is contained in:
2026-09-16 17:44:28 +00:00
parent 5086596f94
commit 5202f16972
7 changed files with 156 additions and 2 deletions

View File

@@ -35,6 +35,8 @@ type RunRequest struct {
ReceiptPath string
LogPath string
Timeout time.Duration
Resume bool
ChunkCache string
References []ReferenceBinding
}

View File

@@ -73,6 +73,12 @@ func (r *SubprocessRunner) Run(ctx context.Context, req RunRequest) (RunResult,
"--input", req.InputPath,
"--output-dir", req.OutputRoot,
}
if req.Resume {
args = append(args, "--resume")
}
if req.ChunkCache != "" {
args = append(args, "--chunk_cache", req.ChunkCache)
}
for _, reference := range references {
args = append(args, "--reference", reference.Selector+"="+reference.Path)
}
@@ -159,6 +165,9 @@ func validateRunRequest(req RunRequest) ([]ReferenceBinding, error) {
if req.Timeout <= 0 {
return nil, fmt.Errorf("notarius timeout must be positive")
}
if !validChunkCacheMode(req.ChunkCache) {
return nil, fmt.Errorf("notarius chunk cache mode must be empty, auto, bypass, or refresh")
}
for label, path := range map[string]string{
"config": req.ConfigPath,
"input": req.InputPath,
@@ -217,6 +226,15 @@ func validateRunRequest(req RunRequest) ([]ReferenceBinding, error) {
return references, nil
}
func validChunkCacheMode(mode string) bool {
switch mode {
case "", "auto", "bypass", "refresh":
return true
default:
return false
}
}
type receiptDocument struct {
SchemaVersion string `json:"schema_version"`
RunID string `json:"run_id"`

View File

@@ -77,6 +77,68 @@ func TestSubprocessRunnerBuildsExactInvocationAndDiscoversBundle(t *testing.T) {
}
}
func TestSubprocessRunnerMapsCacheControlsToArguments(t *testing.T) {
tests := []struct {
name string
resume bool
chunkCache string
want []string
}{
{name: "resume", resume: true, want: []string{"--resume"}},
{name: "auto", chunkCache: "auto", want: []string{"--chunk_cache", "auto"}},
{name: "bypass", chunkCache: "bypass", want: []string{"--chunk_cache", "bypass"}},
{name: "refresh", chunkCache: "refresh", want: []string{"--chunk_cache", "refresh"}},
{name: "resume and mode", resume: true, chunkCache: "auto", want: []string{"--resume", "--chunk_cache", "auto"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := validRunRequest(t)
req.Resume = test.resume
req.ChunkCache = test.chunkCache
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)
}
want := []string{
"run", req.PipelineID, "--config", req.ConfigPath, "--input", req.InputPath,
"--output-dir", req.OutputRoot,
}
want = append(want, test.want...)
want = append(want, "--json")
if !reflect.DeepEqual(captured.Args, want) {
t.Fatalf("subprocess args = %#v, want %#v", captured.Args, want)
}
})
}
}
func TestSubprocessRunnerRejectsInvalidChunkCacheBeforeLaunch(t *testing.T) {
for _, mode := range []string{"AUTO", "invalid", " auto "} {
t.Run(mode, func(t *testing.T) {
req := validRunRequest(t)
req.ChunkCache = mode
started := false
runner := &SubprocessRunner{run: func(context.Context, sharedsubprocess.RunRequest) (sharedsubprocess.RunResult, error) {
started = true
return sharedsubprocess.RunResult{}, nil
}}
_, err := runner.Run(context.Background(), req)
if err == nil || !strings.Contains(err.Error(), "chunk cache mode") {
t.Fatalf("Run() error = %v, want chunk cache validation failure", err)
}
if started {
t.Fatal("subprocess started with invalid chunk cache mode")
}
})
}
}
func TestSubprocessRunnerForwardsLoadedCredentialWithoutRereadingEnvironment(t *testing.T) {
const envName = "NARRATIO_NOTARIUS_CUSTOM_LLM_KEY"
const loadedValue = "loaded-notarius-credential"

View File

@@ -146,10 +146,16 @@ func (extractStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (re
if err != nil {
return nil, fmt.Errorf("extract: build configuration fingerprint: %w", err)
}
resume := notariusConfig.Resume
chunkCache := notariusConfig.ChunkCache
if env.Force {
resume = false
chunkCache = "refresh"
}
request := notarius.RunRequest{
Binary: resolvedBinary, LLMAPIKey: notariusConfig.LLMAPIKey, ConfigPath: configPath, PipelineID: notariusConfig.PipelineID,
InputPath: inputPath, OutputRoot: outputRoot, WorkingDirectory: workingDirectory,
ReceiptPath: receiptPath, LogPath: logPath, Timeout: timeout, References: referenceBindings,
ReceiptPath: receiptPath, LogPath: logPath, Timeout: timeout, Resume: resume, ChunkCache: chunkCache, References: referenceBindings,
}
stageSucceeded := false
bundleMoved := false

View File

@@ -96,6 +96,9 @@ notarius:
if len(req.References) != 0 {
t.Fatalf("references = %#v, want none", req.References)
}
if req.Resume || req.ChunkCache != "" {
t.Fatalf("default cache controls = resume %v, mode %q", req.Resume, req.ChunkCache)
}
if req.LLMAPIKey != loadedPipeline.Notarius.LLMAPIKey || req.LLMAPIKey.EnvName() != credentialName || !req.LLMAPIKey.IsResolved() {
t.Fatal("request did not retain the loaded Notarius credential reference")
}
@@ -114,6 +117,41 @@ notarius:
}
}
func TestExtractStageAppliesNotariusCacheControlsAndForcePrecedence(t *testing.T) {
tests := []struct {
name string
configuredResume bool
configuredMode string
force bool
wantResume bool
wantMode string
}{
{name: "ordinary defaults"},
{name: "ordinary configured", configuredResume: true, configuredMode: "bypass", wantResume: true, wantMode: "bypass"},
{name: "force overrides configured", configuredResume: true, configuredMode: "bypass", force: true, wantMode: "refresh"},
{name: "force overrides defaults", force: true, wantMode: "refresh"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
env, m, fake := setupExtractEnv(t)
env.Config.Pipeline.Notarius.Resume = test.configuredResume
env.Config.Pipeline.Notarius.ChunkCache = test.configuredMode
env.Force = test.force
if _, err := (extractStage{}).Run(context.Background(), env, m); err != nil {
t.Fatalf("Run() error = %v", err)
}
if len(fake.Requests) != 1 {
t.Fatalf("requests = %d, want 1", len(fake.Requests))
}
request := fake.Requests[0]
if request.Resume != test.wantResume || request.ChunkCache != test.wantMode {
t.Fatalf("cache controls = resume %v, mode %q; want resume %v, mode %q", request.Resume, request.ChunkCache, test.wantResume, test.wantMode)
}
})
}
}
func TestExtractStageResolvesAllPreparedReferencesInSelectorOrder(t *testing.T) {
env, m, fake := setupExtractEnv(t)
configured := []struct {

View File

@@ -76,6 +76,26 @@ func TestExtractSemanticConfigSensitivityAndOperationalExclusions(t *testing.T)
if !want.Equal(got) {
t.Fatalf("operational settings changed fingerprint: got %#v want %#v", got, want)
}
for _, test := range []struct {
name string
mutate func(*config.NotariusConfig)
}{
{name: "resume", mutate: func(cfg *config.NotariusConfig) { cfg.Resume = true }},
{name: "chunk cache", mutate: func(cfg *config.NotariusConfig) { cfg.ChunkCache = "refresh" }},
} {
t.Run("operational "+test.name, func(t *testing.T) {
env := downstreamSemanticEnv()
test.mutate(env.Config.Pipeline.Notarius)
got, err := (extractStage{}).SemanticConfigFingerprint(env)
if err != nil {
t.Fatal(err)
}
if !want.Equal(got) {
t.Fatalf("%s changed semantic fingerprint: got %#v want %#v", test.name, got, want)
}
})
}
}
func TestExtractResumeValidationExcludesOperationalSettings(t *testing.T) {
@@ -85,6 +105,8 @@ func TestExtractResumeValidationExcludesOperationalSettings(t *testing.T) {
env.Config.Pipeline.Notarius.ConfigPath = "/not/present/private.yml"
env.Config.Pipeline.Notarius.Timeout = "not-a-duration"
env.Config.Pipeline.Notarius.WorkingDirectory = "/not/present/work"
env.Config.Pipeline.Notarius.Resume = true
env.Config.Pipeline.Notarius.ChunkCache = "bypass"
validation, err := (extractStage{}).ValidateResume(context.Background(), env, m)
if err != nil {