308 lines
12 KiB
Go
308 lines
12 KiB
Go
package integration_test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
|
"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/dnd"
|
|
spellcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/spells"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/merge/appendorder"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/generic/normalize/noop"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/seriatim/input/transcript"
|
|
)
|
|
|
|
func TestDNDSpellCapabilityFailures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(pipeline.ModuleSpec, pipeline.ModuleSpec) (pipeline.ModuleSpec, pipeline.ModuleSpec)
|
|
wantModule string
|
|
wantCap string
|
|
}{
|
|
{
|
|
name: "spell extractor requires transcript source",
|
|
mutate: func(input, extractor pipeline.ModuleSpec) (pipeline.ModuleSpec, pipeline.ModuleSpec) {
|
|
input.Provides = withoutCapability(input.Provides, "source.transcript")
|
|
return input, extractor
|
|
},
|
|
wantModule: spells.Key,
|
|
wantCap: "source.transcript",
|
|
},
|
|
{
|
|
name: "append-order merger requires spell casts",
|
|
mutate: func(input, extractor pipeline.ModuleSpec) (pipeline.ModuleSpec, pipeline.ModuleSpec) {
|
|
extractor.Provides = withoutCapability(extractor.Provides, "dnd.spell_casts")
|
|
return input, extractor
|
|
},
|
|
wantModule: pipeline.DefaultMergeModule,
|
|
wantCap: "dnd.spell_casts",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
inputSpec, extractorSpec := tt.mutate(transcript.ModuleSpec(), spells.ModuleSpec())
|
|
_, err := pipeline.ResolvePipeline(dndCapabilityProfile(), pipeline.ResolveOptions{}, dndCapabilityCatalog(t, inputSpec, extractorSpec))
|
|
if err == nil || !strings.Contains(err.Error(), "missing capability") || !strings.Contains(err.Error(), tt.wantCap) || !strings.Contains(err.Error(), tt.wantModule) {
|
|
t.Fatalf("ResolvePipeline() error = %v, want %s missing %s capability", err, tt.wantModule, tt.wantCap)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func dndCapabilityProfile() pipeline.PipelineProfile {
|
|
return pipeline.PipelineProfile{
|
|
ID: "dnd-capability",
|
|
Input: pipeline.Binding(transcript.Key),
|
|
Chunk: pipeline.Binding("fake/chunk"),
|
|
Artifacts: map[string]pipeline.ArtifactLaneProfile{
|
|
"spells": {Extract: pipeline.Binding(spells.Key)},
|
|
},
|
|
}
|
|
}
|
|
|
|
func dndCapabilityCatalog(t *testing.T, inputSpec, extractorSpec pipeline.ModuleSpec) pipeline.ModuleCatalog {
|
|
t.Helper()
|
|
inputs := pipeline.NewInputAdapterRegistry()
|
|
if err := inputs.RegisterWithSpec(inputSpec, func() (contracts.InputAdapter, error) { return transcript.New(), nil }); err != nil {
|
|
t.Fatalf("register capability input: %v", err)
|
|
}
|
|
|
|
chunkers := pipeline.NewChunkerRegistry()
|
|
if err := chunkers.RegisterWithSpec(pipeline.ModuleSpec{Key: "fake/chunk", Stage: pipeline.StageChunk, ExecutionClass: contracts.ExecutionClassDeterministic, Provides: []string{"chunks"}}, func() (contracts.Chunker, error) { return dndSpellsChunker{}, nil }); err != nil {
|
|
t.Fatalf("register capability chunker: %v", err)
|
|
}
|
|
|
|
extractors := pipeline.NewExtractorRegistry()
|
|
extractorSpec.ArtifactKind = dnd.SpellListKind
|
|
if err := pipeline.RegisterExtractor[dnd.SpellList](extractors, extractorSpec, func() (contracts.Extractor[dnd.SpellList], error) {
|
|
return configExtractor{key: extractorSpec.Key}, nil
|
|
}); err != nil {
|
|
t.Fatalf("register capability extractor: %v", err)
|
|
}
|
|
|
|
codecs := pipeline.NewArtifactCodecRegistry()
|
|
evidence := pipeline.NewArtifactEvidenceRegistry()
|
|
if err := pipeline.RegisterArtifactCodec(codecs, spellcodec.New()); err != nil {
|
|
t.Fatalf("register capability codec: %v", err)
|
|
}
|
|
|
|
mergers := pipeline.NewMergerRegistry()
|
|
if err := pipeline.RegisterMerger[dnd.SpellList](mergers, pipeline.ModuleSpec{
|
|
Key: pipeline.DefaultMergeModule, Stage: pipeline.StageMerge, ExecutionClass: contracts.ExecutionClassDeterministic, ArtifactKind: dnd.SpellListKind, Requires: []string{"dnd.spell_casts"},
|
|
}, func() (contracts.Merger[dnd.SpellList], error) { return appendorder.NewTyped(appendSpellLists) }); err != nil {
|
|
t.Fatalf("register capability merger: %v", err)
|
|
}
|
|
|
|
normalizers := pipeline.NewNormalizerRegistry()
|
|
if err := pipeline.RegisterNormalizer[dnd.SpellList](normalizers, pipeline.ModuleSpec{Key: pipeline.DefaultNormalizeModule, Stage: pipeline.StageNormalize, ExecutionClass: contracts.ExecutionClassDeterministic, ArtifactKind: dnd.SpellListKind}, func() (contracts.Normalizer[dnd.SpellList], error) {
|
|
return noop.NewTyped[dnd.SpellList](), nil
|
|
}); err != nil {
|
|
t.Fatalf("register capability normalizer: %v", err)
|
|
}
|
|
|
|
outputs := pipeline.NewOutputEncoderRegistry()
|
|
if err := outputs.RegisterWithSpec(pipeline.ModuleSpec{Key: pipeline.DefaultOutputModule, Stage: pipeline.StageOutput, ExecutionClass: contracts.ExecutionClassDeterministic}, func() (contracts.OutputEncoder, error) { return dndSpellsOutput{}, nil }); err != nil {
|
|
t.Fatalf("register capability output: %v", err)
|
|
}
|
|
|
|
return pipeline.ModuleCatalog{
|
|
Inputs: inputs, Chunkers: chunkers, ArtifactCodecs: codecs, ArtifactEvidence: evidence, Extractors: extractors,
|
|
Mergers: mergers, Normalizers: normalizers, ValidatorChains: pipeline.NewValidatorChainRegistry(), Outputs: outputs,
|
|
}
|
|
}
|
|
|
|
func loadDNDSpellsPipelineConfig(t *testing.T) config.Config {
|
|
t.Helper()
|
|
|
|
data, err := os.ReadFile("testdata/pipeline.yml")
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(pipeline.yml) error = %v, want nil", err)
|
|
}
|
|
fileCfg, err := config.ParseFileConfigYAML(data)
|
|
if err != nil {
|
|
t.Fatalf("ParseFileConfigYAML() error = %v, want nil", err)
|
|
}
|
|
cfg := config.Default()
|
|
if err := cfg.ApplyFileConfig(fileCfg); err != nil {
|
|
t.Fatalf("ApplyFileConfig() error = %v, want nil", err)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
type dndSpellsCatalogSpecs struct {
|
|
input pipeline.ModuleSpec
|
|
chunk pipeline.ModuleSpec
|
|
extractor pipeline.ModuleSpec
|
|
}
|
|
|
|
func dndSpellsTestCatalog(t *testing.T, specs dndSpellsCatalogSpecs) pipeline.ModuleCatalog {
|
|
t.Helper()
|
|
|
|
inputs := pipeline.NewInputAdapterRegistry()
|
|
chunkers := pipeline.NewChunkerRegistry()
|
|
extractors := pipeline.NewExtractorRegistry()
|
|
codecs := pipeline.NewArtifactCodecRegistry()
|
|
evidence := pipeline.NewArtifactEvidenceRegistry()
|
|
mergers := pipeline.NewMergerRegistry()
|
|
normalizers := pipeline.NewNormalizerRegistry()
|
|
outputs := pipeline.NewOutputEncoderRegistry()
|
|
|
|
if specs.input.Key == "" {
|
|
if err := transcript.Register(inputs); err != nil {
|
|
t.Fatalf("register seriatim input: %v", err)
|
|
}
|
|
} else if err := inputs.RegisterWithSpec(specs.input, func() (contracts.InputAdapter, error) {
|
|
return transcript.New(), nil
|
|
}); err != nil {
|
|
t.Fatalf("register seriatim input override: %v", err)
|
|
}
|
|
|
|
chunkSpec := specs.chunk
|
|
if chunkSpec.Key == "" {
|
|
chunkSpec = dndSpellsChunkerSpec()
|
|
}
|
|
if err := chunkers.RegisterWithSpec(chunkSpec, func() (contracts.Chunker, error) {
|
|
return dndSpellsChunker{}, nil
|
|
}); err != nil {
|
|
t.Fatalf("register chunker: %v", err)
|
|
}
|
|
|
|
codec := spellcodec.New()
|
|
if err := pipeline.RegisterArtifactCodec(codecs, codec); err != nil {
|
|
t.Fatalf("register dnd spells codec: %v", err)
|
|
}
|
|
if specs.extractor.Key == "" {
|
|
if err := spells.Register(extractors); err != nil {
|
|
t.Fatalf("register dnd spells extractor: %v", err)
|
|
}
|
|
} else {
|
|
specs.extractor.ArtifactKind = dnd.SpellListKind
|
|
if err := pipeline.RegisterExtractor[dnd.SpellList](extractors, specs.extractor, func() (contracts.Extractor[dnd.SpellList], error) {
|
|
return configExtractor{key: specs.extractor.Key}, nil
|
|
}); err != nil {
|
|
t.Fatalf("register dnd spells extractor override: %v", err)
|
|
}
|
|
}
|
|
|
|
if err := pipeline.RegisterMerger[dnd.SpellList](mergers, pipeline.ModuleSpec{
|
|
Key: pipeline.DefaultMergeModule,
|
|
Stage: pipeline.StageMerge,
|
|
ExecutionClass: contracts.ExecutionClassDeterministic,
|
|
ArtifactKind: dnd.SpellListKind,
|
|
Requires: []string{"dnd.spell_casts"},
|
|
}, func() (contracts.Merger[dnd.SpellList], error) {
|
|
return appendorder.NewTyped(appendSpellLists)
|
|
}); err != nil {
|
|
t.Fatalf("register merger: %v", err)
|
|
}
|
|
if err := pipeline.RegisterNormalizer[dnd.SpellList](normalizers, pipeline.ModuleSpec{
|
|
Key: pipeline.DefaultNormalizeModule,
|
|
Stage: pipeline.StageNormalize,
|
|
ExecutionClass: contracts.ExecutionClassDeterministic,
|
|
ArtifactKind: dnd.SpellListKind,
|
|
}, func() (contracts.Normalizer[dnd.SpellList], error) {
|
|
return noop.NewTyped[dnd.SpellList](), nil
|
|
}); err != nil {
|
|
t.Fatalf("register normalizer: %v", err)
|
|
}
|
|
if err := outputs.RegisterWithSpec(pipeline.ModuleSpec{
|
|
Key: pipeline.DefaultOutputModule,
|
|
Stage: pipeline.StageOutput,
|
|
ExecutionClass: contracts.ExecutionClassDeterministic,
|
|
}, func() (contracts.OutputEncoder, error) {
|
|
return dndSpellsOutput{}, nil
|
|
}); err != nil {
|
|
t.Fatalf("register output: %v", err)
|
|
}
|
|
|
|
return pipeline.ModuleCatalog{
|
|
Inputs: inputs,
|
|
Chunkers: chunkers,
|
|
ArtifactCodecs: codecs,
|
|
ArtifactEvidence: evidence,
|
|
Extractors: extractors,
|
|
Mergers: mergers,
|
|
Normalizers: normalizers,
|
|
ValidatorChains: pipeline.NewValidatorChainRegistry(),
|
|
Outputs: outputs,
|
|
}
|
|
}
|
|
|
|
type configExtractor struct{ key string }
|
|
|
|
func (extractor configExtractor) Key() string { return extractor.key }
|
|
func (configExtractor) ReferenceSlots() []contracts.ReferenceSlot { return nil }
|
|
func (configExtractor) Extract(context.Context, contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.SpellList], error) {
|
|
return contracts.TypedExtractionResult[dnd.SpellList]{}, nil
|
|
}
|
|
|
|
func appendSpellLists(values []dnd.SpellList) (dnd.SpellList, error) {
|
|
combined := dnd.SpellList{SpellCasts: []dnd.SpellCast{}}
|
|
for _, value := range values {
|
|
combined.SpellCasts = append(combined.SpellCasts, value.SpellCasts...)
|
|
}
|
|
return combined, nil
|
|
}
|
|
|
|
func dndSpellsChunkerSpec() pipeline.ModuleSpec {
|
|
return pipeline.ModuleSpec{
|
|
Key: "fake/chunk",
|
|
Stage: pipeline.StageChunk,
|
|
ExecutionClass: contracts.ExecutionClassDeterministic,
|
|
Requires: []string{"source.transcript"},
|
|
Provides: []string{"chunks"},
|
|
}
|
|
}
|
|
|
|
type dndSpellsChunker struct{}
|
|
|
|
func (dndSpellsChunker) Key() string {
|
|
return "fake/chunk"
|
|
}
|
|
|
|
func (dndSpellsChunker) ReferenceSlots() []contracts.ReferenceSlot {
|
|
return nil
|
|
}
|
|
|
|
func (dndSpellsChunker) Plan(ctx context.Context, req contracts.ChunkRequest) (contracts.ChunkPlanResult, error) {
|
|
return contracts.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 dndSpellsOutput struct{}
|
|
|
|
func (dndSpellsOutput) Key() string {
|
|
return pipeline.DefaultOutputModule
|
|
}
|
|
|
|
func (dndSpellsOutput) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
|
|
return contracts.OutputResult{
|
|
Files: []contracts.OutputFile{
|
|
{Name: "output.json", ContentType: "application/json", Bytes: []byte(`{"encoded":true}`)},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func withoutCapability(capabilities []string, capability string) []string {
|
|
filtered := make([]string, 0, len(capabilities))
|
|
for _, candidate := range capabilities {
|
|
if candidate != capability {
|
|
filtered = append(filtered, candidate)
|
|
}
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
var (
|
|
_ contracts.Chunker = dndSpellsChunker{}
|
|
_ contracts.OutputEncoder = dndSpellsOutput{}
|
|
)
|