Include validator policy in pipeline identity
This commit is contained in:
@@ -1010,6 +1010,7 @@ func resolvedPipelineDigest(resolved ResolvedPipeline) (string, error) {
|
||||
Chunk ModuleBinding
|
||||
ChunkReferences ResolvedReferenceTarget
|
||||
ArtifactLanes []ResolvedArtifactLane
|
||||
ValidatorChains []ResolvedValidatorChain
|
||||
Output ModuleBinding
|
||||
}{
|
||||
ID: resolved.ID,
|
||||
@@ -1017,6 +1018,7 @@ func resolvedPipelineDigest(resolved ResolvedPipeline) (string, error) {
|
||||
Chunk: resolved.Chunk,
|
||||
ChunkReferences: resolved.ChunkReferences,
|
||||
ArtifactLanes: resolved.ArtifactLanes,
|
||||
ValidatorChains: resolved.ValidatorChains,
|
||||
Output: resolved.Output,
|
||||
}
|
||||
encoded, err := json.Marshal(withoutDigest)
|
||||
|
||||
@@ -1062,6 +1062,186 @@ func TestResolvePipelineDigestChangesWhenBindingChanges(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePipelineDigestIncludesEffectiveValidatorChain(t *testing.T) {
|
||||
resolve := func(t *testing.T, validators []ModuleBinding, explicitEmpty bool) ResolvedPipeline {
|
||||
t.Helper()
|
||||
catalog := newProfileCatalog(t)
|
||||
registerProfileValidatorSpec(t, catalog, ValidatorSpec{Key: "second-validator", ExecutionClass: contracts.ExecutionClassDeterministic})
|
||||
if len(validators) > 0 {
|
||||
if err := catalog.ValidatorChains.Register(ValidatorChainMapping{
|
||||
Stage: StageExtract,
|
||||
Module: "event-extractor",
|
||||
Validators: validators,
|
||||
}); err != nil {
|
||||
t.Fatalf("register validator chain: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
profile := baselineProfile()
|
||||
if explicitEmpty {
|
||||
lane := profile.Artifacts["events"]
|
||||
lane.Extract.Validators = ValidatorOverride{Set: true}
|
||||
profile.Artifacts["events"] = lane
|
||||
}
|
||||
resolved, err := ResolvePipeline(profile, ResolveOptions{}, catalog)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolvePipeline() error = %v, want nil", err)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
inherited := resolve(t, []ModuleBinding{Binding("grounded"), Binding("second-validator")}, false)
|
||||
repeated := resolve(t, []ModuleBinding{Binding("grounded"), Binding("second-validator")}, false)
|
||||
if inherited.Digest != repeated.Digest {
|
||||
t.Fatalf("same resolved validator policy produced digests %q and %q", inherited.Digest, repeated.Digest)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
left ResolvedPipeline
|
||||
right ResolvedPipeline
|
||||
}{
|
||||
{
|
||||
name: "different default",
|
||||
left: resolve(t, []ModuleBinding{Binding("grounded")}, false),
|
||||
right: resolve(t, []ModuleBinding{Binding("second-validator")}, false),
|
||||
},
|
||||
{
|
||||
name: "added validator",
|
||||
left: resolve(t, nil, false),
|
||||
right: resolve(t, []ModuleBinding{Binding("grounded")}, false),
|
||||
},
|
||||
{
|
||||
name: "removed validator",
|
||||
left: resolve(t, []ModuleBinding{Binding("grounded")}, false),
|
||||
right: resolve(t, nil, false),
|
||||
},
|
||||
{
|
||||
name: "reordered chain",
|
||||
left: inherited,
|
||||
right: resolve(t, []ModuleBinding{Binding("second-validator"), Binding("grounded")}, false),
|
||||
},
|
||||
{
|
||||
name: "explicit empty",
|
||||
left: inherited,
|
||||
right: resolve(t, []ModuleBinding{Binding("grounded"), Binding("second-validator")}, true),
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.left.Digest == tc.right.Digest {
|
||||
t.Fatalf("digest = %q for both validator policies, want changed digest", tc.left.Digest)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedPipelineDigestIncludesCompleteValidatorPolicy(t *testing.T) {
|
||||
base := validatorDigestFixture()
|
||||
baseDigest, err := resolvedPipelineDigest(base)
|
||||
if err != nil {
|
||||
t.Fatalf("resolvedPipelineDigest(base) error = %v, want nil", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*ResolvedPipeline)
|
||||
}{
|
||||
{name: "stage", mutate: func(value *ResolvedPipeline) { value.ValidatorChains[0].Stage = StageMerge }},
|
||||
{name: "lane", mutate: func(value *ResolvedPipeline) { value.ValidatorChains[0].LaneID = "other" }},
|
||||
{name: "owner module", mutate: func(value *ResolvedPipeline) { value.ValidatorChains[0].ModuleKey = "other-extractor" }},
|
||||
{name: "validator order", mutate: func(value *ResolvedPipeline) {
|
||||
value.ValidatorChains[0].Validators[0], value.ValidatorChains[0].Validators[1] = value.ValidatorChains[0].Validators[1], value.ValidatorChains[0].Validators[0]
|
||||
}},
|
||||
{name: "validator module", mutate: func(value *ResolvedPipeline) {
|
||||
value.ValidatorChains[0].Validators[0].Binding.Module = "other-validator"
|
||||
}},
|
||||
{name: "llm profile", mutate: func(value *ResolvedPipeline) { value.ValidatorChains[0].Validators[0].Binding.LLMProfile = "fast" }},
|
||||
{name: "retries", mutate: func(value *ResolvedPipeline) { value.ValidatorChains[0].Validators[0].Binding.Retries++ }},
|
||||
{name: "options", mutate: func(value *ResolvedPipeline) {
|
||||
value.ValidatorChains[0].Validators[0].Binding.Options["threshold"] = 0.75
|
||||
}},
|
||||
{name: "references", mutate: func(value *ResolvedPipeline) {
|
||||
value.ValidatorChains[0].Validators[0].Binding.References["rules"] = "other.md"
|
||||
}},
|
||||
{name: "execution class", mutate: func(value *ResolvedPipeline) {
|
||||
value.ValidatorChains[0].Validators[0].ExecutionClass = contracts.ExecutionClassDeterministic
|
||||
}},
|
||||
{name: "target", mutate: func(value *ResolvedPipeline) {
|
||||
value.ValidatorChains[0].Validators[0].Target = ValidatorTargetSerialized
|
||||
}},
|
||||
{name: "artifact kind", mutate: func(value *ResolvedPipeline) { value.ValidatorChains[0].Validators[0].ArtifactKind = "test/other" }},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
changed := validatorDigestFixture()
|
||||
tc.mutate(&changed)
|
||||
digest, err := resolvedPipelineDigest(changed)
|
||||
if err != nil {
|
||||
t.Fatalf("resolvedPipelineDigest(changed) error = %v, want nil", err)
|
||||
}
|
||||
if digest == baseDigest {
|
||||
t.Fatalf("digest = %q after %s change, want different digest", digest, tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedPipelineDigestCanonicalizesValidatorBindingMaps(t *testing.T) {
|
||||
left := validatorDigestFixture()
|
||||
right := validatorDigestFixture()
|
||||
right.ValidatorChains[0].Validators[0].Binding.Options = map[string]any{}
|
||||
right.ValidatorChains[0].Validators[0].Binding.Options["threshold"] = 0.5
|
||||
right.ValidatorChains[0].Validators[0].Binding.Options["mode"] = "strict"
|
||||
right.ValidatorChains[0].Validators[0].Binding.References = map[string]string{}
|
||||
right.ValidatorChains[0].Validators[0].Binding.References["examples"] = "examples.md"
|
||||
right.ValidatorChains[0].Validators[0].Binding.References["rules"] = "rules.md"
|
||||
|
||||
leftDigest, err := resolvedPipelineDigest(left)
|
||||
if err != nil {
|
||||
t.Fatalf("resolvedPipelineDigest(left) error = %v, want nil", err)
|
||||
}
|
||||
rightDigest, err := resolvedPipelineDigest(right)
|
||||
if err != nil {
|
||||
t.Fatalf("resolvedPipelineDigest(right) error = %v, want nil", err)
|
||||
}
|
||||
if leftDigest != rightDigest {
|
||||
t.Fatalf("equivalent validator maps produced digests %q and %q", leftDigest, rightDigest)
|
||||
}
|
||||
}
|
||||
|
||||
func validatorDigestFixture() ResolvedPipeline {
|
||||
return ResolvedPipeline{
|
||||
ID: "validator-policy",
|
||||
ValidatorChains: []ResolvedValidatorChain{{
|
||||
Stage: StageExtract,
|
||||
LaneID: "events",
|
||||
ModuleKey: "event-extractor",
|
||||
Validators: []ResolvedValidator{
|
||||
{
|
||||
Binding: ModuleBinding{
|
||||
Module: "semantic-validator",
|
||||
LLMProfile: "careful",
|
||||
Retries: 2,
|
||||
Options: map[string]any{"mode": "strict", "threshold": 0.5},
|
||||
References: map[string]string{"rules": "rules.md", "examples": "examples.md"},
|
||||
},
|
||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||
Target: ValidatorTargetTyped,
|
||||
ArtifactKind: "test/notes",
|
||||
},
|
||||
{
|
||||
Binding: Binding("grounded"),
|
||||
ExecutionClass: contracts.ExecutionClassDeterministic,
|
||||
Target: ValidatorTargetTyped,
|
||||
ArtifactKind: "test/notes",
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingTrimsModuleAndLeavesResolutionFieldsEmpty(t *testing.T) {
|
||||
binding := Binding(" module ")
|
||||
if binding.Module != "module" {
|
||||
|
||||
Reference in New Issue
Block a user