Deep-clone source metadata during chunk materialization
This commit is contained in:
@@ -679,41 +679,11 @@ func validateOutputFileName(name string) error {
|
||||
}
|
||||
|
||||
func cloneMetadata(metadata map[string]any) map[string]any {
|
||||
if len(metadata) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]any, len(metadata))
|
||||
for key, value := range metadata {
|
||||
out[key] = cloneMetadataValue(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneMetadataValue(value any) any {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
return cloneMetadata(typed)
|
||||
case []any:
|
||||
out := make([]any, len(typed))
|
||||
for i := range typed {
|
||||
out[i] = cloneMetadataValue(typed[i])
|
||||
}
|
||||
return out
|
||||
case json.RawMessage:
|
||||
return append(json.RawMessage(nil), typed...)
|
||||
case []byte:
|
||||
return append([]byte(nil), typed...)
|
||||
case []string:
|
||||
return append([]string(nil), typed...)
|
||||
case map[string]string:
|
||||
out := make(map[string]string, len(typed))
|
||||
for key, item := range typed {
|
||||
out[key] = item
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return value
|
||||
cloned, err := source.CloneMetadata(metadata)
|
||||
if err != nil {
|
||||
return metadata
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func cloneLLMProfiles(profiles []artifacts.LLMProfileManifest) []artifacts.LLMProfileManifest {
|
||||
|
||||
@@ -16,6 +16,24 @@ type extractCaptureRecorder struct {
|
||||
checkpoint ExtractCheckpoint
|
||||
}
|
||||
|
||||
type pipelineTypedMetadata map[string]any
|
||||
|
||||
type mutatingMetadataValidator struct {
|
||||
seen string
|
||||
}
|
||||
|
||||
func (*mutatingMetadataValidator) Name() string { return "test/mutating-metadata" }
|
||||
func (*mutatingMetadataValidator) ExecutionClass() contracts.ExecutionClass {
|
||||
return contracts.ExecutionClassDeterministic
|
||||
}
|
||||
func (v *mutatingMetadataValidator) Validate(_ context.Context, request contracts.ChunkValidationRequest) (contracts.ValidationResult, error) {
|
||||
nested := request.Chunks[0].Units[0].Metadata["typed"].(pipelineTypedMetadata)
|
||||
v.seen = string(nested["raw"].(json.RawMessage))
|
||||
nested["raw"].(json.RawMessage)[0] = '['
|
||||
nested["changed"] = true
|
||||
return contracts.ValidationResult{Approved: true}, nil
|
||||
}
|
||||
|
||||
func (r *extractCaptureRecorder) ExtractSucceeded(_ string, _ string, _ []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput, warnings []contracts.Warning) error {
|
||||
r.checkpoint = ExtractCheckpoint{
|
||||
Outputs: cloneCheckpointArtifacts(outputs),
|
||||
@@ -77,6 +95,46 @@ func TestRunnerPassesIndependentAnnotationScopesToExtractors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerPassesIndependentConcreteMetadataToValidatorsAndExtractors(t *testing.T) {
|
||||
prepared := preparedAttemptDebugPipeline(t)
|
||||
input := prepared.input.(*typedTestInput)
|
||||
input.doc.Units[0].Metadata = map[string]any{
|
||||
"typed": pipelineTypedMetadata{"raw": json.RawMessage(`{"value":true}`)},
|
||||
}
|
||||
var err error
|
||||
input.doc.Digest, err = source.DigestDocument(input.doc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
chunker := prepared.chunker.(*typedTestChunker)
|
||||
chunker.plan = typedTestPlan(input.doc)
|
||||
|
||||
validator := &mutatingMetadataValidator{}
|
||||
prepared.chunkValidators.validators = []preparedValidator{{
|
||||
resolved: ResolvedValidator{Binding: Binding(validator.Name()), Target: ValidatorTargetChunk},
|
||||
chunk: validator,
|
||||
}}
|
||||
var extractorSeen string
|
||||
installExtractOperation(prepared, 0, func(_ context.Context, request contracts.TypedExtractionRequest) (erasedTypedResult, error) {
|
||||
nested := request.Chunk.Units[0].Metadata["typed"].(pipelineTypedMetadata)
|
||||
extractorSeen = string(nested["raw"].(json.RawMessage))
|
||||
nested["raw"].(json.RawMessage)[0] = '['
|
||||
nested["changed"] = true
|
||||
return erasedTypedResult{Value: typedValueForLane(0, request.Chunk.Index)}, nil
|
||||
})
|
||||
|
||||
if _, err := New().Run(context.Background(), RunInput{Prepared: prepared, RawInput: []byte("input")}); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
if validator.seen != `{"value":true}` || extractorSeen != `{"value":true}` {
|
||||
t.Fatalf("validator/extractor metadata = %q / %q", validator.seen, extractorSeen)
|
||||
}
|
||||
original := input.doc.Units[0].Metadata["typed"].(pipelineTypedMetadata)
|
||||
if _, changed := original["changed"]; changed || string(original["raw"].(json.RawMessage)) != `{"value":true}` {
|
||||
t.Fatalf("source metadata changed through runner handoff: %#v", original)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerContinuesFromFreshAndReusedExtractResults(t *testing.T) {
|
||||
prepared := preparedAttemptDebugPipeline(t)
|
||||
extractCalls := 0
|
||||
|
||||
Reference in New Issue
Block a user