Reject ambiguous configuration and reference bindings
This commit is contained in:
@@ -147,7 +147,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
||||
machineOutput := fs.Bool("json", false, "write the successful run result as JSON")
|
||||
debug := fs.Bool("debug", false, "write a debug bundle")
|
||||
debugDir := fs.String("debug-dir", "", "debug bundle directory")
|
||||
llmProfile := fs.String("llm-profile", "", "LLM profile override")
|
||||
llmProfile := singleValueFlag{name: "--llm-profile"}
|
||||
reasoningEffort := singleValueFlag{name: "--reasoning-effort"}
|
||||
clearReasoningEffort := fs.Bool("clear-reasoning-effort", false, "clear the LLM profile reasoning effort")
|
||||
resume := fs.Bool("resume", false, "reuse compatible recorded checkpoints")
|
||||
@@ -157,6 +157,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
||||
referenceFlags := stringListFlag{}
|
||||
withoutReferenceFlags := stringListFlag{}
|
||||
fs.Var(&requestedSessionID, "session-id", "prompt session identifier")
|
||||
fs.Var(&llmProfile, "llm-profile", "LLM profile override")
|
||||
fs.Var(&reasoningEffort, "reasoning-effort", "reasoning effort override")
|
||||
fs.Var(&chunkCache, "chunk_cache", "chunk plan cache mode: auto, bypass, or refresh")
|
||||
fs.Var(&referenceFlags, "reference", "reference binding, as slot=path, chunk.slot=path, merge.slot=path, lane.slot=path, lane.extract.slot=path, lane.merge.slot=path, or lane.normalize.slot=path")
|
||||
@@ -203,6 +204,10 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
||||
fmt.Fprintln(stderr, "notarius: --session-id must not be empty")
|
||||
return 2
|
||||
}
|
||||
if llmProfile.set && strings.TrimSpace(llmProfile.value) == "" {
|
||||
fmt.Fprintln(stderr, "notarius: --llm-profile must not be empty")
|
||||
return 2
|
||||
}
|
||||
if reasoningEffort.set && *clearReasoningEffort {
|
||||
fmt.Fprintln(stderr, "notarius: --reasoning-effort cannot be combined with --clear-reasoning-effort")
|
||||
return 2
|
||||
@@ -338,7 +343,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
||||
PipelineID: pipelineID,
|
||||
Only: only,
|
||||
Catalog: catalog,
|
||||
LLMProfileOverride: *llmProfile,
|
||||
LLMProfileOverride: strings.TrimSpace(llmProfile.value),
|
||||
ReferenceOverrides: referenceOverrides,
|
||||
ReferenceUnbinds: referenceUnbinds,
|
||||
})
|
||||
@@ -426,7 +431,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
||||
if err != nil {
|
||||
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
||||
}
|
||||
checkpointRecorder, checkpointLoader, err := checkpointHandlersForRun(effective.Config.Cache.Checkpoints, opts, effective.ResolvedPipeline, prepared.CheckpointFingerprints(), llmFingerprints, rawInput, only, llmProfiles, strings.TrimSpace(*llmProfile), effectiveSessionID, runtimeOverrides, *resume)
|
||||
checkpointRecorder, checkpointLoader, err := checkpointHandlersForRun(effective.Config.Cache.Checkpoints, opts, effective.ResolvedPipeline, prepared.CheckpointFingerprints(), llmFingerprints, rawInput, only, llmProfiles, strings.TrimSpace(llmProfile.value), effectiveSessionID, runtimeOverrides, *resume)
|
||||
if err != nil {
|
||||
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,12 @@ func TestRunControlsRejectSyntaxWithoutAllocatingState(t *testing.T) {
|
||||
{name: "blank session ID", args: func(roots stateTestRoots) []string {
|
||||
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--session-id", ""}
|
||||
}},
|
||||
{name: "blank LLM profile", args: func(roots stateTestRoots) []string {
|
||||
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--llm-profile", ""}
|
||||
}},
|
||||
{name: "whitespace LLM profile", args: func(roots stateTestRoots) []string {
|
||||
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--llm-profile", " \t "}
|
||||
}},
|
||||
{name: "multiple pipeline IDs", args: func(roots stateTestRoots) []string {
|
||||
return []string{"run", "sample", "extra", "--config", roots.config, "--input", roots.input}
|
||||
}},
|
||||
@@ -253,7 +259,7 @@ func TestRunLLMProfileOverrideAndValidationUseInjectedBoundaries(t *testing.T) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := RunWithOptions([]string{"run", "sample", "--config", roots.config, "--input", roots.input, "--chunk_cache", "bypass", "--llm-profile", "override-profile"}, &stdout, &stderr, opts)
|
||||
code := RunWithOptions([]string{"run", "sample", "--config", roots.config, "--input", roots.input, "--chunk_cache", "bypass", "--llm-profile", " override-profile "}, &stdout, &stderr, opts)
|
||||
if code != 0 || stderr.Len() != 0 {
|
||||
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -349,6 +350,12 @@ func ParseFileConfigYAML(data []byte) (FileConfig, error) {
|
||||
if err := decoder.Decode(&fileCfg); err != nil {
|
||||
return FileConfig{}, fmt.Errorf("decode yaml: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); err == nil {
|
||||
return FileConfig{}, fmt.Errorf("config must contain exactly one YAML document")
|
||||
} else if err != io.EOF {
|
||||
return FileConfig{}, fmt.Errorf("decode trailing yaml document: %w", err)
|
||||
}
|
||||
return fileCfg, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,28 @@ func TestFileConfigMinimalVersion4AppliesOverDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileConfigYAMLRejectsAdditionalDocuments(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "trailing whitespace", source: "version: 4\n\n \t", wantErr: false},
|
||||
{name: "trailing comment", source: "version: 4\n# trailing comment\n", wantErr: false},
|
||||
{name: "second valid document", source: "version: 4\n---\nversion: 4\n", wantErr: true},
|
||||
{name: "second empty document", source: "version: 4\n---\n", wantErr: true},
|
||||
{name: "second malformed document", source: "version: 4\n---\nversion: [\n", wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseFileConfigYAML([]byte(tt.source))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("ParseFileConfigYAML() error = %v, want error=%t", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilePipelineLLMProfileIsPresenceAwareAndDetached(t *testing.T) {
|
||||
const pipelineYAML = `version: 4
|
||||
pipelines:
|
||||
|
||||
@@ -305,7 +305,10 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
|
||||
return ResolvedPipeline{}, err
|
||||
}
|
||||
|
||||
input := resolveBinding(profile.Input, "")
|
||||
input, err := resolveBinding(profile.Input, "", fmt.Sprintf("pipeline %q input reference slot", pipelineID))
|
||||
if err != nil {
|
||||
return ResolvedPipeline{}, err
|
||||
}
|
||||
if input.Module == "" {
|
||||
return ResolvedPipeline{}, fmt.Errorf("pipeline %q input module must not be empty", pipelineID)
|
||||
}
|
||||
@@ -320,7 +323,10 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
|
||||
}
|
||||
capabilities.add(inputModuleSpec.Provides...)
|
||||
|
||||
chunk := resolveBinding(profile.Chunk, DefaultChunkModule)
|
||||
chunk, err := resolveBinding(profile.Chunk, DefaultChunkModule, fmt.Sprintf("pipeline %q chunk reference slot", pipelineID))
|
||||
if err != nil {
|
||||
return ResolvedPipeline{}, err
|
||||
}
|
||||
chunkSpec, err := chunkerSpec(catalog, chunk.Module)
|
||||
if err != nil {
|
||||
return ResolvedPipeline{}, moduleLookupError(pipelineID, "", StageChunk, chunk.Module, err)
|
||||
@@ -363,6 +369,10 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
|
||||
if err != nil {
|
||||
return ResolvedPipeline{}, err
|
||||
}
|
||||
output, err := resolveBinding(profile.Output, DefaultOutputModule, fmt.Sprintf("pipeline %q output reference slot", pipelineID))
|
||||
if err != nil {
|
||||
return ResolvedPipeline{}, err
|
||||
}
|
||||
resolved := ResolvedPipeline{
|
||||
ID: pipelineID,
|
||||
Input: input,
|
||||
@@ -370,7 +380,7 @@ func ResolvePipeline(profile PipelineProfile, options ResolveOptions, catalog Mo
|
||||
Chunk: chunk,
|
||||
ChunkExecutionClass: chunkSpec.ExecutionClass,
|
||||
ChunkReferences: referenceTarget(StageChunk, "", chunk.Module, chunkReferences),
|
||||
Output: resolveBinding(profile.Output, DefaultOutputModule),
|
||||
Output: output,
|
||||
}
|
||||
chunkValidatorChain, err := resolveValidatorChain(pipelineID, "", StageChunk, chunk.Module, chunk.Validators, "", nil, catalog)
|
||||
if err != nil {
|
||||
@@ -485,13 +495,29 @@ func resolveArtifactLane(
|
||||
inherited capabilitySet,
|
||||
catalog ModuleCatalog,
|
||||
) (ResolvedArtifactLane, []ResolvedValidatorChain, capabilitySet, error) {
|
||||
extract, err := resolveBinding(profile.Extract, "", fmt.Sprintf("pipeline %q step %q lane %q extract reference slot", pipelineID, stepID, laneID))
|
||||
if err != nil {
|
||||
return ResolvedArtifactLane{}, nil, nil, err
|
||||
}
|
||||
merge, err := resolveBinding(profile.Merge, DefaultMergeModule, fmt.Sprintf("pipeline %q step %q lane %q merge reference slot", pipelineID, stepID, laneID))
|
||||
if err != nil {
|
||||
return ResolvedArtifactLane{}, nil, nil, err
|
||||
}
|
||||
normalize, err := resolveBinding(profile.Normalize, DefaultNormalizeModule, fmt.Sprintf("pipeline %q step %q lane %q normalize reference slot", pipelineID, stepID, laneID))
|
||||
if err != nil {
|
||||
return ResolvedArtifactLane{}, nil, nil, err
|
||||
}
|
||||
validators, err := resolveBindings(profile.Validators, "", fmt.Sprintf("pipeline %q step %q lane %q validator reference slot", pipelineID, stepID, laneID))
|
||||
if err != nil {
|
||||
return ResolvedArtifactLane{}, nil, nil, err
|
||||
}
|
||||
lane := ResolvedArtifactLane{
|
||||
StepID: strings.TrimSpace(stepID),
|
||||
ID: laneID,
|
||||
Extract: resolveBinding(profile.Extract, ""),
|
||||
Merge: resolveBinding(profile.Merge, DefaultMergeModule),
|
||||
Normalize: resolveBinding(profile.Normalize, DefaultNormalizeModule),
|
||||
Validators: resolveBindings(profile.Validators, ""),
|
||||
Extract: extract,
|
||||
Merge: merge,
|
||||
Normalize: normalize,
|
||||
Validators: validators,
|
||||
}
|
||||
if lane.Extract.Module == "" {
|
||||
return ResolvedArtifactLane{}, nil, nil, fmt.Errorf("pipeline %q lane %q extract module must not be empty", pipelineID, laneID)
|
||||
@@ -516,6 +542,7 @@ func resolveArtifactLane(
|
||||
extractReferences := mergeReferenceMaps(profile.References, lane.Extract.References)
|
||||
references, err := resolveReferenceTargetBindings(referenceResolutionTarget{
|
||||
PipelineID: pipelineID,
|
||||
StepID: strings.TrimSpace(stepID),
|
||||
LaneID: laneID,
|
||||
Stage: StageExtract,
|
||||
Module: lane.Extract.Module,
|
||||
@@ -541,6 +568,7 @@ func resolveArtifactLane(
|
||||
}
|
||||
mergeReferences, err := resolveReferenceTargetBindings(referenceResolutionTarget{
|
||||
PipelineID: pipelineID,
|
||||
StepID: strings.TrimSpace(stepID),
|
||||
LaneID: laneID,
|
||||
Stage: StageMerge,
|
||||
Module: lane.Merge.Module,
|
||||
@@ -566,6 +594,7 @@ func resolveArtifactLane(
|
||||
}
|
||||
normalizeReferences, err := resolveReferenceTargetBindings(referenceResolutionTarget{
|
||||
PipelineID: pipelineID,
|
||||
StepID: strings.TrimSpace(stepID),
|
||||
LaneID: laneID,
|
||||
Stage: StageNormalize,
|
||||
Module: lane.Normalize.Module,
|
||||
@@ -925,7 +954,7 @@ func validatePipelineReferenceDefaults(
|
||||
lanesByID map[string]ArtifactLaneProfile,
|
||||
catalog ModuleCatalog,
|
||||
) error {
|
||||
normalizedPipelineReferences, err := normalizedReferenceMap(pipelineReferences, fmt.Sprintf("pipeline %q reference slot", pipelineID))
|
||||
normalizedPipelineReferences, err := normalizeReferenceMap(pipelineReferences, fmt.Sprintf("pipeline %q reference slot", pipelineID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -939,38 +968,38 @@ func validatePipelineReferenceDefaults(
|
||||
}
|
||||
for _, laneID := range sortedArtifactLaneProfileKeys(lanesByID) {
|
||||
laneProfile := lanesByID[laneID]
|
||||
extract := resolveBinding(laneProfile.Extract, "")
|
||||
if extract.Module == "" {
|
||||
extractModule := resolveModuleKey(laneProfile.Extract.Module, "")
|
||||
if extractModule == "" {
|
||||
return fmt.Errorf("pipeline %q lane %q extract module must not be empty", pipelineID, laneID)
|
||||
}
|
||||
extractSpec, err := extractorSpec(catalog, extract.Module)
|
||||
extractSpec, err := extractorSpec(catalog, extractModule)
|
||||
if err != nil {
|
||||
return moduleLookupError(pipelineID, laneID, StageExtract, extract.Module, err)
|
||||
return moduleLookupError(pipelineID, laneID, StageExtract, extractModule, err)
|
||||
}
|
||||
for _, slot := range extractSpec.ReferenceSlots {
|
||||
declaredByAnyTarget[slot.Name] = struct{}{}
|
||||
}
|
||||
|
||||
merge := resolveBinding(laneProfile.Merge, DefaultMergeModule)
|
||||
mergeModule := resolveModuleKey(laneProfile.Merge.Module, DefaultMergeModule)
|
||||
var artifactType reflect.Type
|
||||
artifactKind := extractSpec.ArtifactKind
|
||||
if artifactKind != "" && catalog.Extractors != nil {
|
||||
if entry, ok := catalog.Extractors.typedEntry(extract.Module); ok {
|
||||
if entry, ok := catalog.Extractors.typedEntry(extractModule); ok {
|
||||
artifactType = entry.valueType
|
||||
}
|
||||
}
|
||||
mergeSpec, err := mergerSpecForArtifact(catalog, merge.Module, artifactKind, artifactType)
|
||||
mergeSpec, err := mergerSpecForArtifact(catalog, mergeModule, artifactKind, artifactType)
|
||||
if err != nil {
|
||||
return moduleLookupError(pipelineID, laneID, StageMerge, merge.Module, err)
|
||||
return moduleLookupError(pipelineID, laneID, StageMerge, mergeModule, err)
|
||||
}
|
||||
for _, slot := range mergeSpec.ReferenceSlots {
|
||||
declaredByAnyTarget[slot.Name] = struct{}{}
|
||||
}
|
||||
|
||||
normalize := resolveBinding(laneProfile.Normalize, DefaultNormalizeModule)
|
||||
normalizeSpec, err := normalizerSpecForArtifact(catalog, normalize.Module, artifactKind, artifactType)
|
||||
normalizeModule := resolveModuleKey(laneProfile.Normalize.Module, DefaultNormalizeModule)
|
||||
normalizeSpec, err := normalizerSpecForArtifact(catalog, normalizeModule, artifactKind, artifactType)
|
||||
if err != nil {
|
||||
return moduleLookupError(pipelineID, laneID, StageNormalize, normalize.Module, err)
|
||||
return moduleLookupError(pipelineID, laneID, StageNormalize, normalizeModule, err)
|
||||
}
|
||||
for _, slot := range normalizeSpec.ReferenceSlots {
|
||||
declaredByAnyTarget[slot.Name] = struct{}{}
|
||||
@@ -987,6 +1016,7 @@ func validatePipelineReferenceDefaults(
|
||||
|
||||
type referenceResolutionTarget struct {
|
||||
PipelineID string
|
||||
StepID string
|
||||
LaneID string
|
||||
Stage ModuleStage
|
||||
Module string
|
||||
@@ -1046,7 +1076,7 @@ func resolveReferenceTargetBindings(target referenceResolutionTarget) ([]Referen
|
||||
return nil
|
||||
}
|
||||
|
||||
normalizedPipelineReferences, err := normalizedReferenceMap(target.PipelineReferences, fmt.Sprintf("pipeline %q reference slot", target.PipelineID))
|
||||
normalizedPipelineReferences, err := normalizeReferenceMap(target.PipelineReferences, fmt.Sprintf("pipeline %q reference slot", target.PipelineID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1059,7 +1089,7 @@ func resolveReferenceTargetBindings(target referenceResolutionTarget) ([]Referen
|
||||
}
|
||||
}
|
||||
|
||||
normalizedLocalReferences, err := normalizedReferenceMap(target.LocalReferences, referenceTargetSlotLabel(target))
|
||||
normalizedLocalReferences, err := normalizeReferenceMap(target.LocalReferences, referenceTargetSlotLabel(target))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1166,6 +1196,9 @@ func sortedReferenceBindings(bindings map[string]ReferenceBinding) []ReferenceBi
|
||||
|
||||
func referenceTargetErrorContext(target referenceResolutionTarget) string {
|
||||
if target.LaneID != "" {
|
||||
if target.StepID != "" {
|
||||
return fmt.Sprintf("pipeline %q step %q lane %q %s module %q", target.PipelineID, target.StepID, target.LaneID, target.Stage, target.Module)
|
||||
}
|
||||
return fmt.Sprintf("pipeline %q lane %q %s module %q", target.PipelineID, target.LaneID, target.Stage, target.Module)
|
||||
}
|
||||
return fmt.Sprintf("pipeline %q %s module %q", target.PipelineID, target.Stage, target.Module)
|
||||
@@ -1173,17 +1206,22 @@ func referenceTargetErrorContext(target referenceResolutionTarget) string {
|
||||
|
||||
func referenceTargetSlotLabel(target referenceResolutionTarget) string {
|
||||
if target.LaneID != "" {
|
||||
if target.StepID != "" {
|
||||
return fmt.Sprintf("pipeline %q step %q lane %q %s reference slot", target.PipelineID, target.StepID, target.LaneID, target.Stage)
|
||||
}
|
||||
return fmt.Sprintf("pipeline %q lane %q %s reference slot", target.PipelineID, target.LaneID, target.Stage)
|
||||
}
|
||||
return fmt.Sprintf("pipeline %q %s reference slot", target.PipelineID, target.Stage)
|
||||
}
|
||||
|
||||
func normalizedReferenceMap(values map[string]ReferenceSource, keyName string) (map[string]ReferenceSource, error) {
|
||||
func normalizeReferenceMap(values map[string]ReferenceSource, keyName string) (map[string]ReferenceSource, error) {
|
||||
if len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make(map[string]ReferenceSource, len(values))
|
||||
for rawSlotName, rawSource := range values {
|
||||
rawSlotNames := sortedStringMapKeys(values)
|
||||
for _, rawSlotName := range rawSlotNames {
|
||||
rawSource := values[rawSlotName]
|
||||
slotName := strings.TrimSpace(rawSlotName)
|
||||
if slotName == "" {
|
||||
return nil, fmt.Errorf("%s must not be empty", keyName)
|
||||
@@ -1236,20 +1274,29 @@ func sortedReferenceBindingKeys(values map[string]ReferenceBinding) []string {
|
||||
return keys
|
||||
}
|
||||
|
||||
func resolveBinding(binding ModuleBinding, defaultModule string) ModuleBinding {
|
||||
module := strings.TrimSpace(binding.Module)
|
||||
if module == "" {
|
||||
module = defaultModule
|
||||
}
|
||||
func resolveBinding(binding ModuleBinding, defaultModule string, referenceSlotLabel string) (ModuleBinding, error) {
|
||||
module := resolveModuleKey(binding.Module, defaultModule)
|
||||
llmProfile := strings.TrimSpace(binding.LLMProfile)
|
||||
references, err := normalizeReferenceMap(binding.References, referenceSlotLabel)
|
||||
if err != nil {
|
||||
return ModuleBinding{}, err
|
||||
}
|
||||
return ModuleBinding{
|
||||
Module: module,
|
||||
LLMProfile: llmProfile,
|
||||
Retries: binding.Retries,
|
||||
Options: cloneOptions(binding.Options),
|
||||
References: normalizeReferenceMap(binding.References),
|
||||
References: references,
|
||||
Validators: cloneValidatorOverride(binding.Validators),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveModuleKey(module string, defaultModule string) string {
|
||||
module = strings.TrimSpace(module)
|
||||
if module == "" {
|
||||
return defaultModule
|
||||
}
|
||||
return module
|
||||
}
|
||||
|
||||
func applyEffectiveLLMProfiles(resolved *ResolvedPipeline, pipelineProfile, overrideProfile string) error {
|
||||
@@ -1312,17 +1359,20 @@ func applyEffectiveLLMProfiles(resolved *ResolvedPipeline, pipelineProfile, over
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveBindings(bindings []ModuleBinding, defaultModule string) []ModuleBinding {
|
||||
func resolveBindings(bindings []ModuleBinding, defaultModule string, referenceSlotLabel string) ([]ModuleBinding, error) {
|
||||
if len(bindings) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
resolved := make([]ModuleBinding, 0, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
resolvedBinding := resolveBinding(binding, defaultModule)
|
||||
for index, binding := range bindings {
|
||||
resolvedBinding, err := resolveBinding(binding, defaultModule, fmt.Sprintf("%s %d", referenceSlotLabel, index))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved = append(resolved, resolvedBinding)
|
||||
}
|
||||
return resolved
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func cloneOptions(options map[string]any) map[string]any {
|
||||
@@ -1358,25 +1408,6 @@ func cloneOptionValue(value any) any {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeReferenceMap(values map[string]ReferenceSource) map[string]ReferenceSource {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]ReferenceSource, len(values))
|
||||
keys := make([]string, 0, len(values))
|
||||
rawByNormalized := make(map[string]string, len(values))
|
||||
for rawKey := range values {
|
||||
key := strings.TrimSpace(rawKey)
|
||||
rawByNormalized[key] = rawKey
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
out[key] = cloneReferenceSource(values[rawByNormalized[key]])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func selectedArtifactLanes(pipelineID string, artifacts map[string]ArtifactLaneProfile, options ResolveOptions) (map[string]ArtifactLaneProfile, []string, error) {
|
||||
lanesByID := make(map[string]ArtifactLaneProfile, len(artifacts))
|
||||
for rawLaneID, lane := range artifacts {
|
||||
|
||||
@@ -159,6 +159,85 @@ func TestResolvePipelineAppliesDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePipelineRejectsReferenceSlotCollisionsAfterTrimming(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
configure func(*PipelineProfile)
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "chunk",
|
||||
configure: func(profile *PipelineProfile) {
|
||||
profile.Chunk = ModuleBinding{Module: DefaultChunkModule, References: map[string]ReferenceSource{" rules ": ExternalReference("rules.md"), "rules": ExternalReference("other.md")}}
|
||||
},
|
||||
want: []string{`pipeline "references"`, "chunk", `reference slot "rules" is duplicated after trimming`},
|
||||
},
|
||||
{
|
||||
name: "extract",
|
||||
configure: func(profile *PipelineProfile) {
|
||||
lane := profile.Artifacts["lane"]
|
||||
lane.Extract.References = map[string]ReferenceSource{" rules ": ExternalReference("rules.md"), "rules": ExternalReference("other.md")}
|
||||
profile.Artifacts["lane"] = lane
|
||||
},
|
||||
want: []string{`pipeline "references"`, `step "default"`, `lane "lane"`, "extract", `reference slot "rules" is duplicated after trimming`},
|
||||
},
|
||||
{
|
||||
name: "merge",
|
||||
configure: func(profile *PipelineProfile) {
|
||||
lane := profile.Artifacts["lane"]
|
||||
lane.Merge = ModuleBinding{Module: DefaultMergeModule, References: map[string]ReferenceSource{" rules ": ExternalReference("rules.md"), "rules": ExternalReference("other.md")}}
|
||||
profile.Artifacts["lane"] = lane
|
||||
},
|
||||
want: []string{`pipeline "references"`, `step "default"`, `lane "lane"`, "merge", `reference slot "rules" is duplicated after trimming`},
|
||||
},
|
||||
{
|
||||
name: "normalize",
|
||||
configure: func(profile *PipelineProfile) {
|
||||
lane := profile.Artifacts["lane"]
|
||||
lane.Normalize = ModuleBinding{Module: DefaultNormalizeModule, References: map[string]ReferenceSource{" rules ": ExternalReference("rules.md"), "rules": ExternalReference("other.md")}}
|
||||
profile.Artifacts["lane"] = lane
|
||||
},
|
||||
want: []string{`pipeline "references"`, `step "default"`, `lane "lane"`, "normalize", `reference slot "rules" is duplicated after trimming`},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
profile := PipelineProfile{
|
||||
ID: "references",
|
||||
Input: Binding("text"),
|
||||
Artifacts: map[string]ArtifactLaneProfile{
|
||||
"lane": {Extract: Binding("event-extractor")},
|
||||
},
|
||||
}
|
||||
tt.configure(&profile)
|
||||
_, err := ResolvePipeline(profile, ResolveOptions{}, newProfileCatalog(t))
|
||||
if err == nil {
|
||||
t.Fatal("ResolvePipeline() error = nil, want normalized reference collision")
|
||||
}
|
||||
for _, fragment := range tt.want {
|
||||
if !strings.Contains(err.Error(), fragment) {
|
||||
t.Fatalf("ResolvePipeline() error = %q, want context %q", err, fragment)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePipelineRejectsEmptyReferenceSlotAfterTrimming(t *testing.T) {
|
||||
_, err := ResolvePipeline(PipelineProfile{
|
||||
ID: "references",
|
||||
Input: Binding("text"),
|
||||
Chunk: ModuleBinding{Module: DefaultChunkModule, References: map[string]ReferenceSource{
|
||||
" \t ": ExternalReference("rules.md"),
|
||||
}},
|
||||
Artifacts: map[string]ArtifactLaneProfile{
|
||||
"lane": {Extract: Binding("event-extractor")},
|
||||
},
|
||||
}, ResolveOptions{}, newProfileCatalog(t))
|
||||
if err == nil || !strings.Contains(err.Error(), `pipeline "references" chunk reference slot must not be empty`) {
|
||||
t.Fatalf("ResolvePipeline() error = %v, want contextual empty reference-slot rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePipelineAppliesEffectiveLLMProfiles(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
|
||||
@@ -71,7 +71,11 @@ func normalizeValidatorChainMapping(mapping ValidatorChainMapping) (ValidatorCha
|
||||
if strings.TrimSpace(validator.Module) == "" {
|
||||
return ValidatorChainMapping{}, fmt.Errorf("validator chain for %q %q has empty validator key at index %d", normalized.Stage, normalized.Module, i)
|
||||
}
|
||||
normalized.Validators[i] = resolveBinding(validator, "")
|
||||
resolved, err := resolveBinding(validator, "", fmt.Sprintf("validator chain for %q %q validator %d reference slot", normalized.Stage, normalized.Module, i))
|
||||
if err != nil {
|
||||
return ValidatorChainMapping{}, err
|
||||
}
|
||||
normalized.Validators[i] = resolved
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user