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[fakeArtifact] = fakeExtractor{} var _ Merger[fakeArtifact] = fakeMerger{} var _ Normalizer[fakeArtifact] = fakeNormalizer{} var _ StructuredLLMClient = fakeLLMClient{} var _ OutputEncoder = fakeOutputEncoder{} func TestFakeExtractorReturnsTypedOutput(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(), TypedExtractionRequest{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.Value.Value != "example" { t.Fatalf("Value = %q, want example", result.Value.Value) } } func TestFakeChunkerReturnsSourcePlan(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.Plan(context.Background(), ChunkRequest{Source: doc}) if err != nil { t.Fatalf("Plan() error = %v, want nil", err) } if chunker.Key() != "generic-chunker" { t.Fatalf("Key() = %q, want generic-chunker", chunker.Key()) } if len(result.Plan.Ranges) != 1 { t.Fatalf("len(Ranges) = %d, want 1", len(result.Plan.Ranges)) } if result.Plan.SourceDigest != doc.Digest || result.Plan.Ranges[0].StartUnitID != 1 || result.Plan.Ranges[0].EndUnitID != 1 { t.Fatalf("Plan = %#v, want source digest and unit range", result.Plan) } } func TestFakeChunkerReceivesPerRunContext(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 := &recordingChunker{key: "llm-chunker"} if _, err := chunker.Plan(context.Background(), ChunkRequest{Source: doc, SessionID: "session", LLMProfile: "profile"}); err != nil { t.Fatalf("Plan() error = %v, want nil", err) } if chunker.request.SessionID != "session" || chunker.request.LLMProfile != "profile" { t.Fatalf("ChunkRequest = %#v, want per-run session and profile", chunker.request) } } 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 := source.Chunk{ ID: "source-1:chunk:1", SourceID: doc.ID, Index: 1, Ref: source.SourceRef{SourceID: doc.ID, 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(), TypedExtractionRequest{ Source: doc, Chunk: &chunk, AmbientContext: map[string]any{"mode": "chunked"}, }) if err != nil { t.Fatalf("Extract() error = %v, want nil", err) } if result.Value.Value != "chunked" { t.Fatalf("Value = %q, want chunked", result.Value.Value) } } 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 TestArtifactSchemaJSONOmitsSchemaContent(t *testing.T) { schema := ArtifactSchema{ ID: "schema-id", Name: "schema-name", Version: "v1", JSONSchema: []byte(`{"type":"object"}`), } encoded, err := json.Marshal(schema) 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["id"] != "schema-id" || got["name"] != "schema-name" || got["version"] != "v1" { t.Fatalf("encoded schema = %#v, want schema provenance", got) } if _, ok := got["json_schema"]; ok { t.Fatalf("encoded schema leaked raw schema content: %s", encoded) } if _, ok := got["JSONSchema"]; ok { t.Fatalf("encoded schema leaked raw schema content: %s", encoded) } } func TestFakeMergeNormalizeAndOutputContracts(t *testing.T) { extractOutput := ExtractArtifact[fakeArtifact]{LaneID: "generic-lane", ExtractorKey: "generic-extractor", SourceID: "source-1", ChunkID: "source-1:chunk:0", ChunkIndex: 0, Value: fakeArtifact{Value: "example"}} merger := fakeMerger{key: "generic-merger"} normalizer := fakeNormalizer{key: "generic-normalizer"} encoder := fakeOutputEncoder{key: "generic-output"} merged, err := merger.Merge(context.Background(), TypedMergeRequest[fakeArtifact]{ LaneID: "generic-lane", ExtractOutputs: []ExtractArtifact[fakeArtifact]{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 merged.Value.Value != "example" { t.Fatalf("merged value = %q, want example", merged.Value.Value) } normalized, err := normalizer.Normalize(context.Background(), TypedNormalizeRequest[fakeArtifact]{ LaneID: "generic-lane", MergeOutput: MergeArtifact[fakeArtifact]{LaneID: "generic-lane", MergerKey: merger.Key(), SourceID: "source-1", Value: merged.Value}, }) 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 normalized.Value.Value != "example" { t.Fatalf("normalized value = %q, want example", normalized.Value.Value) } encoded, err := encoder.Encode(context.Background(), OutputRequest{ Manifest: artifacts.RunManifest{RunID: "run-1"}, NormalizeOutputs: []SerializedOutput{{LaneID: "generic-lane", NormalizerKey: normalizer.Key(), SourceID: "source-1", Artifact: SerializedArtifact{Kind: "test/artifact", Schema: ArtifactSchema{ID: "schema-id", Name: "schema-name", Version: "v1"}, MediaType: "application/json", Content: []byte(`{"value":"example"}`)}}}, }) 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) Plan(ctx context.Context, req ChunkRequest) (ChunkPlanResult, error) { return ChunkPlanResult{ Plan: source.ChunkPlan{ SourceDigest: req.Source.Digest, Ranges: []source.ChunkRange{{ StartUnitID: req.Source.Units[0].ID, EndUnitID: req.Source.Units[len(req.Source.Units)-1].ID, }}, }, }, 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) Plan(ctx context.Context, req ChunkRequest) (ChunkPlanResult, error) { chunker.request = req return fakeChunker{key: chunker.key}.Plan(ctx, req) } type fakeExtractor struct { key string } type fakeArtifact struct{ Value string } func (extractor fakeExtractor) Key() string { return extractor.key } func (extractor fakeExtractor) ReferenceSlots() []ReferenceSlot { return nil } func (extractor fakeExtractor) Extract(ctx context.Context, req TypedExtractionRequest) (TypedExtractionResult[fakeArtifact], error) { value := "example" if req.AmbientContext["mode"] == "chunked" { value = "chunked" } return TypedExtractionResult[fakeArtifact]{Value: fakeArtifact{Value: value}}, nil } type fakeMerger struct { key string } func (merger fakeMerger) Key() string { return merger.key } func (merger fakeMerger) Merge(ctx context.Context, req TypedMergeRequest[fakeArtifact]) (TypedMergeResult[fakeArtifact], error) { return TypedMergeResult[fakeArtifact]{Value: req.ExtractOutputs[0].Value}, 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 TypedNormalizeRequest[fakeArtifact]) (TypedNormalizeResult[fakeArtifact], error) { return TypedNormalizeResult[fakeArtifact]{Value: req.MergeOutput.Value}, 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 }