325 lines
9.1 KiB
Go
325 lines
9.1 KiB
Go
package contracts
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"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 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 TestCloneReferenceSlotsCopiesAcceptedArtifactKinds(t *testing.T) {
|
|
slots := []ReferenceSlot{{Name: "npcs", AcceptedArtifactKinds: []ArtifactKind{"dnd/npc-list"}}}
|
|
clone := CloneReferenceSlots(slots)
|
|
clone[0].AcceptedArtifactKinds[0] = "changed"
|
|
if slots[0].AcceptedArtifactKinds[0] != "dnd/npc-list" {
|
|
t.Fatalf("source AcceptedArtifactKinds aliased clone: %#v", slots[0].AcceptedArtifactKinds)
|
|
}
|
|
}
|
|
|
|
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 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 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
|
|
}
|