Document ordered pipeline operations and retire sequential examples
This commit is contained in:
@@ -152,79 +152,6 @@ func TestCombatNormalizerRejectsCampaignReferenceBinding(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequentialNPCOutputGroundsCombatAtBothStageLocalReferences(t *testing.T) {
|
||||
registries := productionNPCRegistries(t)
|
||||
catalog := moduleCatalog(registries)
|
||||
configValue := loadSequentialCombatPipelineConfig(t)
|
||||
npcEffective, err := configValue.Resolve(config.ResolveInput{PipelineID: "dnd-npcs", Catalog: catalog})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve(NPC) error = %v", err)
|
||||
}
|
||||
npcClient := &fakeNPCProductionLLMClient{response: npcProductionResponse{NPCs: []npcProductionRecord{
|
||||
{Name: "Mira Thorn", Aliases: []string{"The Greencloak", "Mira"}, Description: "A ranger.", Relationships: []npcProductionRelationship{}, SourceRefs: []npcProductionSourceRef{{StartUnitID: 1, EndUnitID: 2}}},
|
||||
{Name: "Hooded Guard", Aliases: []string{}, Description: "A sentry.", Relationships: []npcProductionRelationship{}, SourceRefs: []npcProductionSourceRef{{StartUnitID: 3, EndUnitID: 3}}},
|
||||
}}}
|
||||
npcOutput, err := runPreparedPipeline(t, registries, npcEffective.ResolvedPipeline, npcClient, pipeline.RunInput{RawInput: readNPCFixture(t)})
|
||||
if err != nil || len(npcOutput.NormalizeOutputs) != 1 {
|
||||
t.Fatalf("NPC run error = %v output = %#v, want one normalized NPC lane", err, npcOutput.NormalizeOutputs)
|
||||
}
|
||||
npcPayload := npcOutput.NormalizeOutputs[0].Artifact.Content
|
||||
if _, err := npccodec.New().Decode(npcPayload); err != nil {
|
||||
t.Fatalf("Decode(NPC output) error = %v", err)
|
||||
}
|
||||
npcPath := filepath.Join(t.TempDir(), "npc-run", "lanes", "npcs.json")
|
||||
if err := os.MkdirAll(filepath.Dir(npcPath), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(npcPath, npcPayload, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
combatEffective, err := configValue.Resolve(config.ResolveInput{
|
||||
PipelineID: "dnd-combat",
|
||||
Catalog: catalog,
|
||||
ReferenceOverrides: []pipeline.ReferenceBinding{
|
||||
{Stage: pipeline.StageExtract, LaneID: "combat", SlotName: "npcs", Source: npcPath, BindingSource: contracts.ReferenceBindingSourceCLI},
|
||||
{Stage: pipeline.StageNormalize, LaneID: "combat", SlotName: "npcs", Source: npcPath, BindingSource: contracts.ReferenceBindingSourceCLI},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve(combat) error = %v", err)
|
||||
}
|
||||
materialized, warnings, err := pipeline.MaterializeReferences(combatEffective.ResolvedPipeline, catalog, pipeline.ReferenceMaterializationOptions{})
|
||||
if err != nil || len(warnings) != 0 {
|
||||
t.Fatalf("MaterializeReferences() error = %v warnings = %#v", err, warnings)
|
||||
}
|
||||
client := &fakeCombatLLMClient{responses: []string{combatTestTurnResponse("The Greencloak", "turn", "watches", "Hooded Guard", 1, 1)}}
|
||||
output, err := runPreparedPipeline(t, registries, materialized, client, pipeline.RunInput{RawInput: readNPCFixture(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("Run(combat) error = %v", err)
|
||||
}
|
||||
value, err := combatcodec.New().Decode(output.NormalizeOutputs[0].Artifact.Content)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode(combat output) error = %v", err)
|
||||
}
|
||||
if len(value.CombatTurns) != 1 || value.CombatTurns[0].Actor != "Mira Thorn" || value.CombatTurns[0].Actions[0].Targets[0] != "Hooded Guard" {
|
||||
t.Fatalf("combat value = %#v, want canonical actor and target", value)
|
||||
}
|
||||
for _, ref := range value.CombatTurns[0].SourceRefs {
|
||||
if ref.SourceID != "npc-session" {
|
||||
t.Fatalf("combat source ref = %#v, want transcript evidence only", ref)
|
||||
}
|
||||
}
|
||||
if len(output.Manifest.References) != 2 {
|
||||
t.Fatalf("manifest references = %#v, want both stage-local NPC provenance entries", output.Manifest.References)
|
||||
}
|
||||
for _, provenance := range output.Manifest.References {
|
||||
if provenance.SlotName != "npcs" || provenance.LaneID != "combat" || !strings.Contains(provenance.OriginURI, "npcs.json") || provenance.BindingSource != contracts.ReferenceBindingSourceCLI {
|
||||
t.Fatalf("NPC provenance = %#v, want combat stage-local CLI binding", provenance)
|
||||
}
|
||||
}
|
||||
if len(client.requests) != 1 || string(client.requests[0].Inputs[combatextract.NPCRegistryReferenceSlot].Content) != string(npcPayload) || client.requests[0].Inputs[combatextract.NPCRegistryReferenceSlot].OriginURI != "" {
|
||||
t.Fatalf("combat NPC prompt input = %#v, want canonical payload without path provenance", client.requests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombatPreparationRejectsMalformedOrOversizedNPCReferencesBeforeExecution(t *testing.T) {
|
||||
registries := productionNPCRegistries(t)
|
||||
catalog := moduleCatalog(registries)
|
||||
@@ -313,19 +240,6 @@ func combatOnlyConfig() config.Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func loadSequentialCombatPipelineConfig(t *testing.T) config.Config {
|
||||
t.Helper()
|
||||
fileConfig, err := config.LoadFileConfig(repositoryPathForIntegration("examples", "dnd-npc-combat-sequential.config.yml"))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFileConfig() error = %v", err)
|
||||
}
|
||||
cfg := config.Default()
|
||||
if err := cfg.ApplyFileConfig(fileConfig); err != nil {
|
||||
t.Fatalf("ApplyFileConfig() error = %v", err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func repositoryPathForIntegration(parts ...string) string {
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
return filepath.Join(append([]string{filepath.Dir(file), "..", "..", ".."}, parts...)...)
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
|
||||
)
|
||||
|
||||
func TestSequentialNPCOutputCanGroundIndependentSpellRun(t *testing.T) {
|
||||
registries := productionNPCRegistries(t)
|
||||
catalog := moduleCatalog(registries)
|
||||
configValue := loadSequentialPipelineConfig(t)
|
||||
|
||||
npcEffective, err := configValue.Resolve(config.ResolveInput{PipelineID: "dnd-npcs", Catalog: catalog})
|
||||
if err != nil {
|
||||
t.Fatalf("resolve NPC pipeline: %v", err)
|
||||
}
|
||||
npcClient := &fakeNPCProductionLLMClient{response: npcProductionResponse{NPCs: []npcProductionRecord{{
|
||||
Name: "Mira Thorn",
|
||||
Aliases: []string{"The Greencloak"},
|
||||
Description: "A guarded ranger who watches the northern road.",
|
||||
Relationships: []npcProductionRelationship{{
|
||||
Target: "Captain Vale", Relationship: "reports to",
|
||||
}},
|
||||
SourceRefs: []npcProductionSourceRef{{StartUnitID: 1, EndUnitID: 2}},
|
||||
}}}}
|
||||
npcOutput, err := runPreparedPipeline(t, registries, npcEffective.ResolvedPipeline, npcClient, pipeline.RunInput{RawInput: readNPCFixture(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("run NPC pipeline: %v", err)
|
||||
}
|
||||
if len(npcOutput.NormalizeOutputs) != 1 || npcOutput.NormalizeOutputs[0].LaneID != "npcs" {
|
||||
t.Fatalf("NPC normalized outputs = %#v, want one npcs lane", npcOutput.NormalizeOutputs)
|
||||
}
|
||||
npcPayload := npcOutput.NormalizeOutputs[0].Artifact.Content
|
||||
if _, err := npccodec.New().Decode(npcPayload); err != nil {
|
||||
t.Fatalf("decode normalized NPC payload: %v", err)
|
||||
}
|
||||
|
||||
npcRunDir := t.TempDir()
|
||||
npcPath := filepath.Join(npcRunDir, "lanes", "npcs.json")
|
||||
if err := os.MkdirAll(filepath.Dir(npcPath), 0o700); err != nil {
|
||||
t.Fatalf("create NPC output directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(npcPath, npcPayload, 0o600); err != nil {
|
||||
t.Fatalf("write NPC output payload: %v", err)
|
||||
}
|
||||
|
||||
spellEffective, err := configValue.Resolve(config.ResolveInput{PipelineID: "dnd-spells", Catalog: catalog})
|
||||
if err != nil {
|
||||
t.Fatalf("resolve spell pipeline: %v", err)
|
||||
}
|
||||
spellEffective.ResolvedPipeline.Steps[0].ArtifactLanes[0].ExtractReferences.Bindings = []pipeline.ReferenceBinding{{
|
||||
Stage: pipeline.StageExtract,
|
||||
LaneID: "spells",
|
||||
SlotName: spells.NPCRegistryReferenceSlot,
|
||||
Source: npcPath,
|
||||
BindingSource: contracts.ReferenceBindingSourceCLI,
|
||||
}}
|
||||
materialized, warnings, err := pipeline.MaterializeReferences(spellEffective.ResolvedPipeline, catalog, pipeline.ReferenceMaterializationOptions{WorkingDir: npcRunDir})
|
||||
if err != nil {
|
||||
t.Fatalf("materialize NPC registry reference: %v", err)
|
||||
}
|
||||
if len(warnings) != 0 {
|
||||
t.Fatalf("reference materialization warnings = %#v, want none", warnings)
|
||||
}
|
||||
|
||||
spellClient := &fakeSpellsLLMClient{response: extractionResponse{SpellCasts: []spellCastResponse{{
|
||||
Caster: "Mira Thorn",
|
||||
Spell: "Cure Wounds",
|
||||
Effect: "Restores the injured ally.",
|
||||
NarrativeDescription: "Mira Thorn restores the ally after the fight.",
|
||||
SourceRefs: responseSourceRefs("spell-session", 1, 1),
|
||||
}}}}
|
||||
spellOutput, err := runPreparedPipeline(t, registries, materialized, spellClient, pipeline.RunInput{RawInput: readDNDSpellsFixture(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("run spell pipeline: %v", err)
|
||||
}
|
||||
if len(spellOutput.NormalizeOutputs) != 1 || spellOutput.NormalizeOutputs[0].LaneID != "spells" {
|
||||
t.Fatalf("spell normalized outputs = %#v, want one spells lane", spellOutput.NormalizeOutputs)
|
||||
}
|
||||
spellValue := decodeRunnerSpellResponse(t, spellOutput.NormalizeOutputs[0].Artifact.Content)
|
||||
if len(spellValue.SpellCasts) != 1 || spellValue.SpellCasts[0].Caster != "Mira Thorn" {
|
||||
t.Fatalf("spell output = %#v, want one registry-grounded caster", spellValue)
|
||||
}
|
||||
if len(spellValue.SpellCasts[0].SourceRefs) != 1 || spellValue.SpellCasts[0].SourceRefs[0].SourceID != "spell-session" {
|
||||
t.Fatalf("spell source refs = %#v, want current spell session only", spellValue.SpellCasts[0].SourceRefs)
|
||||
}
|
||||
if len(spellClient.requests) != 1 {
|
||||
t.Fatalf("spell LLM requests = %d, want one", len(spellClient.requests))
|
||||
}
|
||||
registryInput := spellClient.requests[0].Inputs[spells.NPCRegistryReferenceSlot]
|
||||
if string(registryInput.Content) != string(npcPayload) || registryInput.MediaType != npccodec.MediaType || registryInput.OriginURI != "" {
|
||||
t.Fatalf("spell NPC prompt input = %#v, want canonical payload without origin", registryInput)
|
||||
}
|
||||
if len(spellOutput.Manifest.References) != 1 {
|
||||
t.Fatalf("spell manifest references = %#v, want one NPC provenance entry", spellOutput.Manifest.References)
|
||||
}
|
||||
provenance := spellOutput.Manifest.References[0]
|
||||
if provenance.Stage != "extract" || provenance.LaneID != "spells" || provenance.SlotName != spells.NPCRegistryReferenceSlot || provenance.BindingSource != contracts.ReferenceBindingSourceCLI || !strings.Contains(provenance.OriginURI, "npcs.json") {
|
||||
t.Fatalf("spell NPC provenance = %#v, want extract CLI reference provenance", provenance)
|
||||
}
|
||||
metadata, ok := spellOutput.Manifest.ArtifactLanes[0].Metadata["extractor"].(map[string]any)
|
||||
if !ok || metadata["npc_count"] != 1 || metadata["npc_registry_digest"] != registryInput.Digest {
|
||||
t.Fatalf("spell extractor metadata = %#v, want NPC count and semantic digest", spellOutput.Manifest.ArtifactLanes[0].Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func loadSequentialPipelineConfig(t *testing.T) config.Config {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile("testdata/dnd_npc_spell_sequential_pipeline.yml")
|
||||
if err != nil {
|
||||
t.Fatalf("read sequential pipeline config: %v", err)
|
||||
}
|
||||
fileConfig, err := config.ParseFileConfigYAML(data)
|
||||
if err != nil {
|
||||
t.Fatalf("parse sequential pipeline config: %v", err)
|
||||
}
|
||||
configValue := config.Default()
|
||||
if err := configValue.ApplyFileConfig(fileConfig); err != nil {
|
||||
t.Fatalf("apply sequential pipeline config: %v", err)
|
||||
}
|
||||
return configValue
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
version: 3
|
||||
output:
|
||||
directory: ./notarius-output
|
||||
cache:
|
||||
chunk_plans:
|
||||
mode: bypass
|
||||
checkpoints: {}
|
||||
debug:
|
||||
directory: ./notarius-debug
|
||||
pipelines:
|
||||
dnd-npcs:
|
||||
input: seriatim
|
||||
chunk: generic
|
||||
artifacts:
|
||||
npcs:
|
||||
extract:
|
||||
module: dnd/npcs
|
||||
normalize: dnd/npcs
|
||||
dnd-spells:
|
||||
input: seriatim
|
||||
chunk: generic
|
||||
artifacts:
|
||||
spells:
|
||||
extract: dnd/spells
|
||||
normalize: dnd/spells
|
||||
Reference in New Issue
Block a user