645 lines
19 KiB
Go
645 lines
19 KiB
Go
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 TestResponseSchemaJSONOmitRawSchemaContent(t *testing.T) {
|
|
schema := ResponseSchema{
|
|
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 := 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) ExecutionClass() ExecutionClass {
|
|
return ExecutionClassDeterministic
|
|
}
|
|
|
|
func (validator fakeValidator) Validate(ctx context.Context, req ValidationRequest) (ValidationResult, error) {
|
|
return ValidationResult{
|
|
Approved: true,
|
|
ReasonCode: "accepted",
|
|
Message: "output accepted",
|
|
}, 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
|
|
}
|