Record reference provenance in manifests
This commit is contained in:
@@ -206,6 +206,9 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
||||
if err := runDir.WriteResolvedPipeline(effective.ResolvedPipeline); err != nil {
|
||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics resolved pipeline: %w", err))
|
||||
}
|
||||
if err := runDir.WriteResolvedReferences(pipeline.ReferenceProvenance(effective.ResolvedPipeline)); err != nil {
|
||||
return failPipelineCommand(stderr, runDir, cfg.Diagnostics.Retention, fmt.Errorf("write diagnostics resolved references: %w", err))
|
||||
}
|
||||
|
||||
profileIDs := effectiveLLMProfileIDs(effective.ResolvedPipeline)
|
||||
if len(profileIDs) != 1 {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/diagnostics"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/chunk/dnd/scenes"
|
||||
@@ -984,6 +985,74 @@ func TestRunPipelineWritesDurableOutputFiles(t *testing.T) {
|
||||
assertNoTemporaryFiles(t, runOutputDir)
|
||||
}
|
||||
|
||||
func TestRunPipelineReferenceBytesProduceDistinctManifests(t *testing.T) {
|
||||
run := func(t *testing.T, referenceText string) artifacts.RunManifest {
|
||||
t.Helper()
|
||||
|
||||
diagnosticsDir := t.TempDir()
|
||||
outputDir := t.TempDir()
|
||||
configDir := t.TempDir()
|
||||
referencePath := filepath.Join(configDir, "roster.txt")
|
||||
if err := os.WriteFile(referencePath, []byte(referenceText), 0o644); err != nil {
|
||||
t.Fatalf("write reference: %v", err)
|
||||
}
|
||||
configPath := filepath.Join(configDir, "config.yml")
|
||||
if err := os.WriteFile(configPath, []byte(testConfigYAMLWithReferencesAndDiagnostics("example", "events", diagnosticsDir, map[string]string{"roster": "roster.txt"})), 0o644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
inputPath := writeFile(t, "source.txt", "source text")
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
code := RunWithOptions([]string{"run", "example", "--config", configPath, "--input", inputPath, "--output-dir", outputDir}, &stdout, &stderr, Options{
|
||||
Catalog: fakeCatalog(t, pipeline.ModuleSpec{
|
||||
Key: "fake/extract",
|
||||
Stage: pipeline.StageExtract,
|
||||
Requires: []string{"chunks"},
|
||||
Provides: []string{"artifact"},
|
||||
ReferenceSlots: []contracts.ReferenceSlot{
|
||||
{Name: "roster"},
|
||||
},
|
||||
}),
|
||||
Registries: fakeExecutionRegistries(t),
|
||||
LLMClientFactory: fakeLLMFactory(newFakeRunLLMClient(false), nil),
|
||||
})
|
||||
if code != 0 {
|
||||
t.Fatalf("RunWithOptions() code = %d, stderr=%q", code, stderr.String())
|
||||
}
|
||||
|
||||
var manifest artifacts.RunManifest
|
||||
readJSONFile(t, filepath.Join(onlyChildDir(t, outputDir), "manifest.json"), &manifest)
|
||||
if len(manifest.References) != 1 {
|
||||
t.Fatalf("manifest references = %#v, want one entry", manifest.References)
|
||||
}
|
||||
if !reflect.DeepEqual(manifest.SourceDigests, []string{"sha256:source"}) {
|
||||
t.Fatalf("source digests = %#v, want source-only digest", manifest.SourceDigests)
|
||||
}
|
||||
|
||||
var resolvedReferences []artifacts.ReferenceProvenance
|
||||
readJSONFile(t, filepath.Join(onlyChildDir(t, diagnosticsDir), diagnostics.ArtifactResolvedReferences), &resolvedReferences)
|
||||
if !reflect.DeepEqual(resolvedReferences, manifest.References) {
|
||||
t.Fatalf("resolved references = %#v, want manifest references %#v", resolvedReferences, manifest.References)
|
||||
}
|
||||
resolvedReferenceJSON := string(readFile(t, filepath.Join(onlyChildDir(t, diagnosticsDir), diagnostics.ArtifactResolvedReferences)))
|
||||
if strings.Contains(resolvedReferenceJSON, referenceText) || strings.Contains(resolvedReferenceJSON, "content") {
|
||||
t.Fatalf("resolved references diagnostics contains content: %s", resolvedReferenceJSON)
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
|
||||
first := run(t, "first roster")
|
||||
second := run(t, "second roster")
|
||||
|
||||
if first.References[0].Digest == second.References[0].Digest {
|
||||
t.Fatalf("reference digests match for different bytes: %q", first.References[0].Digest)
|
||||
}
|
||||
if first.PipelineDigest != second.PipelineDigest {
|
||||
t.Fatalf("pipeline digests differ = %q vs %q, want reference bytes outside pipeline identity", first.PipelineDigest, second.PipelineDigest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPipelineRejectsUnsafeOutputFileName(t *testing.T) {
|
||||
diagnosticsDir := t.TempDir()
|
||||
outputDir := t.TempDir()
|
||||
@@ -1540,6 +1609,30 @@ func testConfigYAMLWithReferences(pipelineID string, laneID string, references m
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func testConfigYAMLWithReferencesAndDiagnostics(pipelineID string, laneID string, diagnosticsDir string, references map[string]string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("version: 1\n")
|
||||
b.WriteString("diagnostics:\n")
|
||||
b.WriteString(" work_dir: " + diagnosticsDir + "\n")
|
||||
b.WriteString(" retention: always\n")
|
||||
b.WriteString("pipelines:\n")
|
||||
b.WriteString(" " + pipelineID + ":\n")
|
||||
b.WriteString(" input: fake/input\n")
|
||||
b.WriteString(" artifacts:\n")
|
||||
b.WriteString(" " + laneID + ":\n")
|
||||
b.WriteString(" extract: fake/extract\n")
|
||||
b.WriteString(" references:\n")
|
||||
keys := make([]string, 0, len(references))
|
||||
for key := range references {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
b.WriteString(" " + key + ": " + references[key] + "\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func mvpConfigYAML(pipelineID string, extractor string) string {
|
||||
return `version: 1
|
||||
pipelines:
|
||||
@@ -1788,6 +1881,153 @@ func registriesWithOutput(t *testing.T, encoder contracts.OutputEncoder) pipelin
|
||||
return registries
|
||||
}
|
||||
|
||||
func fakeExecutionRegistries(t *testing.T) pipeline.Registries {
|
||||
t.Helper()
|
||||
inputs := pipeline.NewInputAdapterRegistry()
|
||||
chunkers := pipeline.NewChunkerRegistry()
|
||||
extractors := pipeline.NewExtractorRegistry()
|
||||
mergers := pipeline.NewMergerRegistry()
|
||||
normalizers := pipeline.NewNormalizerRegistry()
|
||||
outputs := pipeline.NewOutputEncoderRegistry()
|
||||
|
||||
if err := inputs.RegisterWithSpec(pipeline.ModuleSpec{Key: "fake/input", Stage: pipeline.StageInput, Provides: []string{"source"}}, func() (contracts.InputAdapter, error) {
|
||||
return fakeRunInputAdapter{}, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("register fake input: %v", err)
|
||||
}
|
||||
if err := chunkers.RegisterWithSpec(pipeline.ModuleSpec{Key: "generic", Stage: pipeline.StageChunk, Requires: []string{"source"}, Provides: []string{"chunks"}}, func() (contracts.Chunker, error) {
|
||||
return fakeRunChunker{}, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("register fake chunker: %v", err)
|
||||
}
|
||||
if err := extractors.RegisterWithSpec(pipeline.ModuleSpec{
|
||||
Key: "fake/extract",
|
||||
Stage: pipeline.StageExtract,
|
||||
Requires: []string{"chunks"},
|
||||
Provides: []string{"artifact"},
|
||||
ReferenceSlots: []contracts.ReferenceSlot{
|
||||
{Name: "roster"},
|
||||
},
|
||||
}, func() (contracts.Extractor, error) {
|
||||
return fakeRunExtractor{}, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("register fake extractor: %v", err)
|
||||
}
|
||||
if err := mergers.RegisterWithSpec(pipeline.ModuleSpec{Key: "appendorder", Stage: pipeline.StageMerge, Requires: []string{"artifact"}, Provides: []string{"merged"}}, func() (contracts.Merger, error) {
|
||||
return fakeRunMerger{}, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("register fake merger: %v", err)
|
||||
}
|
||||
if err := normalizers.RegisterWithSpec(pipeline.ModuleSpec{Key: "noop", Stage: pipeline.StageNormalize, Requires: []string{"merged"}, Provides: []string{"normalized"}}, func() (contracts.Normalizer, error) {
|
||||
return fakeRunNormalizer{}, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("register fake normalizer: %v", err)
|
||||
}
|
||||
if err := jsonoutput.Register(outputs); err != nil {
|
||||
t.Fatalf("register json output: %v", err)
|
||||
}
|
||||
|
||||
return pipeline.Registries{
|
||||
Inputs: inputs,
|
||||
Chunkers: chunkers,
|
||||
Extractors: extractors,
|
||||
Mergers: mergers,
|
||||
Normalizers: normalizers,
|
||||
Outputs: outputs,
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRunInputAdapter struct{}
|
||||
|
||||
func (fakeRunInputAdapter) Key() string {
|
||||
return "fake/input"
|
||||
}
|
||||
|
||||
func (fakeRunInputAdapter) Parse(ctx context.Context, req contracts.ParseRequest) (*source.SourceDocument, error) {
|
||||
return &source.SourceDocument{
|
||||
ID: "source",
|
||||
Kind: "text",
|
||||
Format: "test",
|
||||
Digest: "sha256:source",
|
||||
Units: []source.SourceUnit{
|
||||
{ID: "unit-1", Kind: "text", Text: string(req.Raw)},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type fakeRunChunker struct{}
|
||||
|
||||
func (fakeRunChunker) Key() string {
|
||||
return "generic"
|
||||
}
|
||||
|
||||
func (fakeRunChunker) Chunk(ctx context.Context, req contracts.ChunkRequest) (contracts.ChunkResult, error) {
|
||||
return contracts.ChunkResult{
|
||||
Chunks: []contracts.SourceChunk{
|
||||
{ID: "chunk-1", SourceID: req.Source.ID, Index: 0, Units: req.Source.Units},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type fakeRunExtractor struct{}
|
||||
|
||||
func (fakeRunExtractor) Key() string {
|
||||
return "fake/extract"
|
||||
}
|
||||
|
||||
func (fakeRunExtractor) ArtifactType() string {
|
||||
return "fake.artifact"
|
||||
}
|
||||
|
||||
func (fakeRunExtractor) SchemaVersion() string {
|
||||
return "v1"
|
||||
}
|
||||
|
||||
func (fakeRunExtractor) ReferenceSlots() []contracts.ReferenceSlot {
|
||||
return []contracts.ReferenceSlot{{Name: "roster"}}
|
||||
}
|
||||
|
||||
func (fakeRunExtractor) Validators() []contracts.Validator {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fakeRunExtractor) Extract(ctx context.Context, req contracts.ExtractionRequest) (contracts.ExtractionResult, error) {
|
||||
return contracts.ExtractionResult{
|
||||
Candidates: []artifacts.ArtifactCandidate{
|
||||
{
|
||||
Payload: []byte(`{"value":true}`),
|
||||
SourceRefs: []source.SourceRef{
|
||||
{SourceID: "source", StartUnitID: "unit-1", EndUnitID: "unit-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type fakeRunMerger struct{}
|
||||
|
||||
func (fakeRunMerger) Key() string {
|
||||
return "appendorder"
|
||||
}
|
||||
|
||||
func (fakeRunMerger) Merge(ctx context.Context, req contracts.MergeRequest) (contracts.MergeResult, error) {
|
||||
var candidates []artifacts.ArtifactCandidate
|
||||
for _, chunkArtifacts := range req.ChunkArtifacts {
|
||||
candidates = append(candidates, chunkArtifacts.Candidates...)
|
||||
}
|
||||
return contracts.MergeResult{Candidates: candidates}, nil
|
||||
}
|
||||
|
||||
type fakeRunNormalizer struct{}
|
||||
|
||||
func (fakeRunNormalizer) Key() string {
|
||||
return "noop"
|
||||
}
|
||||
|
||||
func (fakeRunNormalizer) Normalize(ctx context.Context, req contracts.NormalizeRequest) (contracts.NormalizeResult, error) {
|
||||
return contracts.NormalizeResult{Candidates: append([]artifacts.ArtifactCandidate(nil), req.Candidates...)}, nil
|
||||
}
|
||||
|
||||
func onlyChildDir(t *testing.T, root string) string {
|
||||
t.Helper()
|
||||
children := childDirs(t, root)
|
||||
|
||||
Reference in New Issue
Block a user