package contracts import ( "context" "encoding/json" "reflect" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/artifacts" "gitea.maximumdirect.net/eric/notarius/internal/core/source" ) var _ InputAdapter = fakeAdapter{} var _ Chunker = fakeChunker{} var _ Extractor = fakeExtractor{} var _ Merger = fakeMerger{} var _ Normalizer = fakeNormalizer{} var _ Validator = fakeValidator{} var _ StructuredLLMClient = fakeLLMClient{} var _ OutputEncoder = fakeOutputEncoder{} func TestFakeExtractorReturnsRawOutput(t *testing.T) { extractor := fakeExtractor{ key: "generic-extractor", } doc := &source.SourceDocument{ ID: "source-1", Kind: "document", Format: "text/plain", Digest: "sha256:abc123", Units: []source.SourceUnit{ {ID: 1, Kind: "section", Text: "Source text."}, }, } result, err := extractor.Extract(context.Background(), ExtractionRequest{Source: doc}) if err != nil { t.Fatalf("Extract() error = %v, want nil", err) } if extractor.Key() != "generic-extractor" { t.Fatalf("Key() = %q, want generic-extractor", extractor.Key()) } if result.Output.ExtractorKey != "" { t.Fatalf("ExtractorKey = %q, want runner-owned empty value", result.Output.ExtractorKey) } if result.Output.Schema.Version != "v1" { t.Fatalf("Schema.Version = %q, want v1", result.Output.Schema.Version) } if result.Output.Payload.MediaType != "application/json" || string(result.Output.Payload.Content) != `{"value":"example"}` { t.Fatalf("payload = %q %s, want JSON raw output", result.Output.Payload.MediaType, result.Output.Payload.Content) } } func TestFakeChunkerReturnsSourceChunks(t *testing.T) { doc := &source.SourceDocument{ ID: "source-1", Kind: "document", Format: "text/plain", Digest: "sha256:abc123", Units: []source.SourceUnit{ {ID: 1, Kind: "section", Text: "Source text."}, }, } chunker := fakeChunker{key: "generic-chunker"} result, err := chunker.Chunk(context.Background(), ChunkRequest{Source: doc}) if err != nil { t.Fatalf("Chunk() error = %v, want nil", err) } if chunker.Key() != "generic-chunker" { t.Fatalf("Key() = %q, want generic-chunker", chunker.Key()) } if len(result.Chunks) != 1 { t.Fatalf("len(Chunks) = %d, want 1", len(result.Chunks)) } chunk := result.Chunks[0] if chunk.ID != "source-1:chunk:0" { t.Fatalf("SourceChunk.ID = %q, want source-1:chunk:0", chunk.ID) } if chunk.SourceID != doc.ID { t.Fatalf("SourceChunk.SourceID = %q, want %q", chunk.SourceID, doc.ID) } if chunk.Index != 0 { t.Fatalf("SourceChunk.Index = %d, want 0", chunk.Index) } if chunk.StartUnitID != 1 || chunk.EndUnitID != 1 { t.Fatalf("SourceChunk boundaries = %d-%d, want 1-1", chunk.StartUnitID, chunk.EndUnitID) } if chunk.MediaType != "application/json" || string(chunk.Content) != `{"units":[{"id":1,"kind":"section","text":"Source text."}]}` { t.Fatalf("SourceChunk payload = %q %s, want JSON units", chunk.MediaType, chunk.Content) } if len(chunk.Units) != 1 { t.Fatalf("len(SourceChunk.Units) = %d, want 1", len(chunk.Units)) } } func TestFakeChunkerReceivesLLMClient(t *testing.T) { doc := &source.SourceDocument{ ID: "source-1", Kind: "document", Format: "text/plain", Digest: "sha256:abc123", Units: []source.SourceUnit{ {ID: 1, Kind: "section", Text: "Source text."}, }, } client := fakeLLMClient{} chunker := &recordingChunker{key: "llm-chunker"} if _, err := chunker.Chunk(context.Background(), ChunkRequest{Source: doc, LLMClient: client}); err != nil { t.Fatalf("Chunk() error = %v, want nil", err) } if chunker.request.LLMClient == nil { t.Fatal("ChunkRequest.LLMClient = nil, want structured LLM client") } } func TestFakeExtractorReceivesChunkAndAmbientContext(t *testing.T) { extractor := fakeExtractor{key: "generic-extractor"} doc := &source.SourceDocument{ ID: "source-1", Kind: "document", Format: "text/plain", Digest: "sha256:abc123", Units: []source.SourceUnit{ {ID: 1, Kind: "section", Text: "First source text."}, {ID: 2, Kind: "section", Text: "Second source text."}, }, } chunk := SourceChunk{ ID: "source-1:chunk:1", SourceID: doc.ID, Index: 1, StartUnitID: 2, EndUnitID: 2, Content: []byte(`{"units":[{"id":2,"kind":"section","text":"Second source text."}]}`), MediaType: "application/json", Units: []source.SourceUnit{doc.Units[1]}, } result, err := extractor.Extract(context.Background(), ExtractionRequest{ Source: doc, Chunk: &chunk, AmbientContext: map[string]any{"mode": "chunked"}, }) if err != nil { t.Fatalf("Extract() error = %v, want nil", err) } if result.Output.ChunkID != "" || result.Output.ChunkIndex != 0 { t.Fatalf("chunk provenance = %q/%d, want runner-owned zero values", result.Output.ChunkID, result.Output.ChunkIndex) } if string(result.Output.Payload.Content) != `{"value":"chunked"}` { t.Fatalf("Payload.Content = %s, want chunked payload", result.Output.Payload.Content) } } func TestReferenceSetDataTypes(t *testing.T) { references := ReferenceSet{ Slots: map[string]ResolvedReferenceSlot{ "roster": { Slot: ReferenceSlot{ Name: "roster", Description: "Known characters", Required: true, AcceptedMediaTypes: []string{"text/plain"}, Multiple: true, MaxBytes: 4096, }, Items: []ReferenceItem{ { SlotName: "roster", MediaType: "text/plain", Content: []byte("Aria\nBryn\n"), Digest: "sha256:reference", Origin: ReferenceOrigin{ Type: "file", URI: "file:///tmp/roster.txt", }, SizeBytes: 10, BindingSource: ReferenceBindingSourceConfig, }, }, }, }, } item := references.Slots["roster"].Items[0] if item.SlotName != "roster" || item.MediaType != "text/plain" || string(item.Content) != "Aria\nBryn\n" { t.Fatalf("reference item = %#v, want constructed item fields", item) } if item.BindingSource != ReferenceBindingSourceConfig { t.Fatalf("BindingSource = %q, want %q", item.BindingSource, ReferenceBindingSourceConfig) } } func TestCloneReferenceSlotsEmptyInputReturnsNil(t *testing.T) { if got := CloneReferenceSlots(nil); got != nil { t.Fatalf("CloneReferenceSlots(nil) = %#v, want nil", got) } if got := CloneReferenceSlots([]ReferenceSlot{}); got != nil { t.Fatalf("CloneReferenceSlots(empty) = %#v, want nil", got) } } func TestCloneReferenceSlotsPreservesFields(t *testing.T) { slots := []ReferenceSlot{ { Name: "roster", Description: "Known characters", Required: true, AcceptedMediaTypes: []string{"text/plain", "text/markdown"}, Multiple: true, MaxBytes: 4096, }, { Name: "glossary", Description: "Campaign terms", MaxBytes: 2048, }, } got := CloneReferenceSlots(slots) if !reflect.DeepEqual(got, slots) { t.Fatalf("CloneReferenceSlots() = %#v, want %#v", got, slots) } } func TestCloneReferenceSlotsCopiesAcceptedMediaTypes(t *testing.T) { slots := []ReferenceSlot{ { Name: "party", AcceptedMediaTypes: []string{"application/json", "text/plain"}, }, } got := CloneReferenceSlots(slots) got[0].Name = "changed" got[0].AcceptedMediaTypes[0] = "text/markdown" if slots[0].Name != "party" { t.Fatalf("source slot name = %q, want unchanged", slots[0].Name) } if slots[0].AcceptedMediaTypes[0] != "application/json" { t.Fatalf("source AcceptedMediaTypes aliased clone: %#v", slots[0].AcceptedMediaTypes) } } func TestReferenceItemJSONOmitsContent(t *testing.T) { item := ReferenceItem{ SlotName: "roster", MediaType: "text/plain", Content: []byte("reference content"), Digest: "sha256:reference", Origin: ReferenceOrigin{Type: "file", URI: "file:///tmp/roster.txt"}, } encoded, err := json.Marshal(item) if err != nil { t.Fatalf("json.Marshal() error = %v, want nil", err) } var got map[string]any if err := json.Unmarshal(encoded, &got); err != nil { t.Fatalf("json.Unmarshal() error = %v, want nil", err) } if _, ok := got["content"]; ok { t.Fatalf("encoded reference item leaked content: %s", encoded) } if _, ok := got["Content"]; ok { t.Fatalf("encoded reference item leaked Content: %s", encoded) } } func TestLLMInputMaterialCopiesContentAndOmitsContentFromJSON(t *testing.T) { content := []byte("raw source bytes") material := NewLLMInputMaterial("transcript", "application/json", content, "sha256:source", "file:///tmp/source.json") content[0] = 'R' if got := string(material.Content); got != "raw source bytes" { t.Fatalf("material content = %q, want defensive copy", got) } if material.SizeBytes != int64(len("raw source bytes")) { t.Fatalf("SizeBytes = %d, want content length", material.SizeBytes) } clone := material.Clone() clone.Content[0] = 'X' if got := string(material.Content); got != "raw source bytes" { t.Fatalf("cloned material content aliased original: %q", got) } encoded, err := json.Marshal(material) if err != nil { t.Fatalf("json.Marshal() error = %v, want nil", err) } var got map[string]any if err := json.Unmarshal(encoded, &got); err != nil { t.Fatalf("json.Unmarshal() error = %v, want nil", err) } if _, ok := got["content"]; ok { t.Fatalf("encoded material leaked content: %s", encoded) } if _, ok := got["Content"]; ok { t.Fatalf("encoded material leaked Content: %s", encoded) } if got["digest"] != "sha256:source" || got["origin_uri"] != "file:///tmp/source.json" { t.Fatalf("encoded material = %#v, want non-secret provenance", got) } } func TestLLMInputSetCloneCopiesContent(t *testing.T) { set := LLMInputSet{ "transcript": NewLLMInputMaterial("transcript", "application/json", []byte("source"), "sha256:source", "file:///tmp/source.json"), } clone := set.Clone() clone["transcript"].Content[0] = 'S' if got := string(set["transcript"].Content); got != "source" { t.Fatalf("input set clone aliased content: %q", got) } } func TestFakeMergeNormalizeAndOutputContracts(t *testing.T) { extractOutput := ExtractOutput{ LaneID: "generic-lane", ExtractorKey: "generic-extractor", SourceID: "source-1", ChunkID: "source-1:chunk:0", ChunkIndex: 0, Schema: ResponseSchema{ID: "schema-id", Name: "schema-name", Version: "v1"}, Payload: RawPayload{ Content: []byte(`{"value":"example"}`), MediaType: "application/json", Metadata: map[string]any{"confidence": 0.75}, }, } merger := fakeMerger{key: "generic-merger"} normalizer := fakeNormalizer{key: "generic-normalizer"} encoder := fakeOutputEncoder{key: "generic-output"} merged, err := merger.Merge(context.Background(), MergeRequest{ LaneID: "generic-lane", ExtractOutputs: []ExtractOutput{extractOutput}, }) if err != nil { t.Fatalf("Merge() error = %v, want nil", err) } if merger.Key() != "generic-merger" { t.Fatalf("Merger.Key() = %q, want generic-merger", merger.Key()) } if string(merged.Output.Payload.Content) != `{"value":"example"}` { t.Fatalf("merged content = %s, want raw extract content", merged.Output.Payload.Content) } normalized, err := normalizer.Normalize(context.Background(), NormalizeRequest{ LaneID: "generic-lane", MergeOutput: merged.Output, }) if err != nil { t.Fatalf("Normalize() error = %v, want nil", err) } if normalizer.Key() != "generic-normalizer" { t.Fatalf("Normalizer.Key() = %q, want generic-normalizer", normalizer.Key()) } if string(normalized.Output.Payload.Content) != `{"value":"example"}` { t.Fatalf("normalized content = %s, want raw merge content", normalized.Output.Payload.Content) } encoded, err := encoder.Encode(context.Background(), OutputRequest{ Manifest: artifacts.RunManifest{RunID: "run-1"}, NormalizeOutputs: []NormalizeOutput{normalized.Output}, }) if err != nil { t.Fatalf("Encode() error = %v, want nil", err) } if encoder.Key() != "generic-output" { t.Fatalf("OutputEncoder.Key() = %q, want generic-output", encoder.Key()) } if len(encoded.Files) != 1 { t.Fatalf("len(Files) = %d, want 1", len(encoded.Files)) } if encoded.Files[0].ContentType != "application/json" { t.Fatalf("ContentType = %q, want application/json", encoded.Files[0].ContentType) } if string(encoded.Files[0].Bytes) != `{"run_id":"run-1","output_count":1}` { t.Fatalf("Bytes = %s, want encoded output", encoded.Files[0].Bytes) } } func TestOutputFileJSONShapeOmitsBytes(t *testing.T) { file := OutputFile{ Name: "artifacts/events.json", ContentType: "application/json", Bytes: []byte(`{"ignored":true}`), } encoded, err := json.Marshal(file) if err != nil { t.Fatalf("json.Marshal() error = %v, want nil", err) } var got map[string]any if err := json.Unmarshal(encoded, &got); err != nil { t.Fatalf("json.Unmarshal() error = %v, want nil", err) } if got["name"] != "artifacts/events.json" { t.Fatalf("name = %#v, want logical file name", got["name"]) } if got["content_type"] != "application/json" { t.Fatalf("content_type = %#v, want application/json", got["content_type"]) } if _, ok := got["Bytes"]; ok { t.Fatalf("encoded output file leaked Bytes: %s", encoded) } if _, ok := got["bytes"]; ok { t.Fatalf("encoded output file leaked bytes: %s", encoded) } } type fakeAdapter struct { key string doc *source.SourceDocument } func (adapter fakeAdapter) Key() string { return adapter.key } func (adapter fakeAdapter) Parse(ctx context.Context, req ParseRequest) (*source.SourceDocument, error) { return adapter.doc, nil } type fakeChunker struct { key string } func (chunker fakeChunker) Key() string { return chunker.key } func (chunker fakeChunker) ReferenceSlots() []ReferenceSlot { return nil } func (chunker fakeChunker) Chunk(ctx context.Context, req ChunkRequest) (ChunkResult, error) { return ChunkResult{ Chunks: []SourceChunk{ { ID: req.Source.ID + ":chunk:0", SourceID: req.Source.ID, Index: 0, StartUnitID: req.Source.Units[0].ID, EndUnitID: req.Source.Units[len(req.Source.Units)-1].ID, Content: []byte(`{"units":[{"id":1,"kind":"section","text":"Source text."}]}`), MediaType: "application/json", Units: append([]source.SourceUnit(nil), req.Source.Units...), }, }, }, nil } type recordingChunker struct { key string request ChunkRequest } func (chunker *recordingChunker) Key() string { return chunker.key } func (chunker *recordingChunker) ReferenceSlots() []ReferenceSlot { return nil } func (chunker *recordingChunker) Chunk(ctx context.Context, req ChunkRequest) (ChunkResult, error) { chunker.request = req return fakeChunker{key: chunker.key}.Chunk(ctx, req) } type fakeExtractor struct { key string } func (extractor fakeExtractor) Key() string { return extractor.key } func (extractor fakeExtractor) ReferenceSlots() []ReferenceSlot { return nil } func (extractor fakeExtractor) Extract(ctx context.Context, req ExtractionRequest) (ExtractionResult, error) { payload := json.RawMessage(`{"value":"example"}`) if req.AmbientContext["mode"] == "chunked" { payload = json.RawMessage(`{"value":"chunked"}`) } return ExtractionResult{ Output: ExtractOutput{ Schema: ResponseSchema{ID: "schema-id", Name: "schema-name", Version: "v1"}, Payload: RawPayload{ Content: append([]byte(nil), payload...), MediaType: "application/json", }, }, }, nil } type fakeMerger struct { key string } func (merger fakeMerger) Key() string { return merger.key } func (merger fakeMerger) Merge(ctx context.Context, req MergeRequest) (MergeResult, error) { output := req.ExtractOutputs[0] return MergeResult{Output: MergeOutput{ LaneID: req.LaneID, MergerKey: merger.key, SourceID: output.SourceID, Schema: output.Schema, Payload: cloneTestRawPayload(output.Payload), }}, nil } type fakeNormalizer struct { key string } func (normalizer fakeNormalizer) Key() string { return normalizer.key } func (normalizer fakeNormalizer) ReferenceSlots() []ReferenceSlot { return nil } func (normalizer fakeNormalizer) Normalize(ctx context.Context, req NormalizeRequest) (NormalizeResult, error) { return NormalizeResult{Output: NormalizeOutput{ LaneID: req.LaneID, NormalizerKey: normalizer.key, SourceID: req.MergeOutput.SourceID, Schema: req.MergeOutput.Schema, Payload: cloneTestRawPayload(req.MergeOutput.Payload), }}, nil } func cloneTestRawPayload(payload RawPayload) RawPayload { return RawPayload{ Content: append([]byte(nil), payload.Content...), MediaType: payload.MediaType, Metadata: cloneTestMetadata(payload.Metadata), Warnings: append([]Warning(nil), payload.Warnings...), } } func cloneTestMetadata(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] = value } return out } type fakeValidator struct { name string } func (validator fakeValidator) Name() string { return validator.name } func (validator fakeValidator) Validate(ctx context.Context, req ValidationRequest) (ValidationResult, error) { decisions := make([]ValidationDecision, 0, len(req.Candidates)) for _, candidate := range req.Candidates { decisions = append(decisions, ValidationDecision{ CandidateIndex: candidate.Index, Approved: true, ReasonCode: "accepted", Message: "candidate accepted", }) } return ValidationResult{ ValidatorName: validator.name, Decisions: decisions, }, nil } type fakeLLMClient struct{} func (client fakeLLMClient) CompleteStructured(ctx context.Context, req StructuredCompletionRequest, out any) (StructuredCompletionResponse, error) { return StructuredCompletionResponse{ Content: json.RawMessage(`{"value":"example"}`), }, nil } type fakeOutputEncoder struct { key string } func (encoder fakeOutputEncoder) Key() string { return encoder.key } func (encoder fakeOutputEncoder) Encode(ctx context.Context, req OutputRequest) (OutputResult, error) { return OutputResult{ Files: []OutputFile{ { Name: "artifacts/generic.json", ContentType: "application/json", Bytes: []byte(`{"run_id":"` + req.Manifest.RunID + `","output_count":1}`), }, }, }, nil }