Consolidate the D&D configuration examples

This commit is contained in:
2026-07-25 15:48:48 +00:00
parent 29ee68824d
commit 9614469b45
27 changed files with 296 additions and 313 deletions

View File

@@ -15,8 +15,7 @@ import (
func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-combat-turns.config.yml")
cfg := loadMaintainedExample(t, configPath)
cfg := productionCombatContractConfig()
effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-combat", Catalog: catalogFromRegistries(components.registries)})
if err != nil {
t.Fatalf("Resolve() error = %v, want nil", err)
@@ -97,9 +96,8 @@ func TestProductionCombatConfigurationResolvesTypedLane(t *testing.T) {
func TestProductionCombatConfigurationRejectsLooseOptionsAndLaneValidators(t *testing.T) {
components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-combat-turns.config.yml")
resolve := func(mutate func(*pipeline.PipelineProfile)) error {
cfg := loadMaintainedExample(t, configPath)
cfg := productionCombatContractConfig()
profile := cfg.Pipelines["dnd-combat"]
mutate(&profile)
cfg.Pipelines["dnd-combat"] = profile
@@ -131,7 +129,7 @@ func TestProductionCombatConfigurationRejectsLooseOptionsAndLaneValidators(t *te
func TestProductionCombatConfigurationResolvesTypedUnconditionalValidators(t *testing.T) {
components := productionTestComponents(t)
cfg := loadMaintainedExample(t, repositoryPath("examples", "dnd-combat-turns.config.yml"))
cfg := productionCombatContractConfig()
profile := cfg.Pipelines["dnd-combat"]
lane := profile.Artifacts["combat"]
lane.Extract.Validators = pipeline.ValidatorOverride{Set: true, Validators: []pipeline.ModuleBinding{pipeline.Binding("generic/always_accept")}}
@@ -150,6 +148,22 @@ func TestProductionCombatConfigurationResolvesTypedUnconditionalValidators(t *te
}
}
func productionCombatContractConfig() config.Config {
cfg := config.Default()
cfg.Pipelines["dnd-combat"] = pipeline.PipelineProfile{
ID: "dnd-combat",
Input: pipeline.Binding("seriatim"),
Chunk: pipeline.Binding(pipeline.DefaultChunkModule),
Artifacts: map[string]pipeline.ArtifactLaneProfile{
"combat": {
Extract: pipeline.ModuleBinding{Module: combatextract.Key, Retries: 2},
Normalize: pipeline.Binding(combatnormalize.Key),
},
},
}
return cfg
}
func hasReferenceSlot(slots []contracts.ReferenceSlot, name string) bool {
for _, slot := range slots {
if slot.Name == name {

View File

@@ -16,8 +16,7 @@ import (
func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
components := productionTestComponents(t)
catalog := catalogFromRegistries(components.registries)
configPath := repositoryPath("examples", "dnd-npcs.config.yml")
cfg := loadMaintainedExample(t, configPath)
cfg := productionNPCContractConfig()
effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalog})
if err != nil {
t.Fatalf("Resolve() error = %v, want nil", err)
@@ -81,9 +80,8 @@ func TestProductionNPCConfigurationResolvesTypedLane(t *testing.T) {
func TestProductionNPCConfigurationValidatesOptionsReferencesAndPlacement(t *testing.T) {
components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-npcs.config.yml")
resolve := func(mutate func(*pipeline.PipelineProfile)) error {
cfg := loadMaintainedExample(t, configPath)
cfg := productionNPCContractConfig()
profile := cfg.Pipelines["dnd-session"]
mutate(&profile)
cfg.Pipelines["dnd-session"] = profile
@@ -123,6 +121,22 @@ func TestProductionNPCConfigurationValidatesOptionsReferencesAndPlacement(t *tes
}
}
func productionNPCContractConfig() config.Config {
cfg := config.Default()
cfg.Pipelines["dnd-session"] = pipeline.PipelineProfile{
ID: "dnd-session",
Input: pipeline.Binding("seriatim"),
Chunk: pipeline.Binding(pipeline.DefaultChunkModule),
Artifacts: map[string]pipeline.ArtifactLaneProfile{
"npcs": {
Extract: pipeline.ModuleBinding{Module: npcextract.Key, Retries: 2},
Normalize: pipeline.Binding(npcnormalize.Key),
},
},
}
return cfg
}
func validatorChain(resolved pipeline.ResolvedPipeline, stage pipeline.ModuleStage, module string) []pipeline.ModuleBinding {
for _, chain := range resolved.ValidatorChains {
if chain.Stage == stage && chain.ModuleKey == module {

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"sort"
"strings"
"testing"
@@ -32,11 +33,14 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
if err != nil {
t.Fatalf("materialize maintained example references for %q: %v", pipelineID, err)
}
if example.name == "production" {
if len(materialized.Steps[0].ArtifactLanes) != 1 ||
len(materialized.Steps[0].ArtifactLanes[0].ExtractReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 ||
len(materialized.Steps[0].ArtifactLanes[0].NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 {
t.Fatalf("production spell catalog reference was not materialized: %#v", materialized.Steps[0].ArtifactLanes)
if example.name == "complete" {
if got := exampleStepLaneIDs(materialized); strings.Join(got, "|") != "describe-session:npcs,scene-descriptions|extract-events:combat-turns,npc-interactions,spells" {
t.Fatalf("complete example steps and lanes = %v, want every D&D extractor in the documented two-step composition", got)
}
spellLane := referenceContractLane(t, materialized, "spells")
if len(spellLane.ExtractReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 ||
len(spellLane.NormalizeReferences.ReferenceSet.Slots["spell_catalog"].Items) != 1 {
t.Fatalf("complete example spell catalog reference was not materialized: %#v", spellLane)
}
}
}
@@ -49,6 +53,36 @@ func TestMaintainedExamplesLoadResolveAndList(t *testing.T) {
}
}
func TestMaintainedConfigurationExampleSet(t *testing.T) {
entries, err := os.ReadDir(repositoryPath("examples"))
if err != nil {
t.Fatal(err)
}
var names []string
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".config.yml") {
names = append(names, entry.Name())
}
}
sort.Strings(names)
if got := strings.Join(names, ","); got != "dnd-complete.config.yml,dnd-minimal.config.yml" {
t.Fatalf("maintained configuration examples = %q, want only the minimal and complete D&D examples", got)
}
}
func exampleStepLaneIDs(resolved pipeline.ResolvedPipeline) []string {
result := make([]string, 0, len(resolved.Steps))
for _, step := range resolved.Steps {
laneIDs := make([]string, 0, len(step.ArtifactLanes))
for _, lane := range step.ArtifactLanes {
laneIDs = append(laneIDs, lane.ID)
}
sort.Strings(laneIDs)
result = append(result, step.ID+":"+strings.Join(laneIDs, ","))
}
return result
}
func TestMaintainedMinimalInvocationProducesJSONBundle(t *testing.T) {
outputRoot := filepath.Join(t.TempDir(), "output")
fake := &productionFakeLLMClient{}
@@ -56,7 +90,7 @@ func TestMaintainedMinimalInvocationProducesJSONBundle(t *testing.T) {
var stdout, stderr strings.Builder
code := RunWithOptions([]string{
"run", "dnd-session",
"--config", repositoryPath("examples", "dnd-spells.config.yml"),
"--config", repositoryPath("examples", "dnd-minimal.config.yml"),
"--input", repositoryPath("examples", "seriatim-minimal-transcript.json"),
"--only", "spells", "--chunk_cache", "bypass", "--output-dir", outputRoot,
}, &stdout, &stderr, options)
@@ -130,7 +164,7 @@ func TestMaintainedMalformedInputOnlyRecordsDebugFailureWhenRequested(t *testing
options := productionRunOptions(t, &productionFakeLLMClient{})
args := []string{
"run", "dnd-session",
"--config", repositoryPath("examples", "dnd-spells.config.yml"),
"--config", repositoryPath("examples", "dnd-minimal.config.yml"),
"--input", malformed, "--chunk_cache", "bypass", "--output-dir", outputRoot,
}
if debug {

View File

@@ -3,6 +3,7 @@ package cli
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
@@ -22,9 +23,24 @@ func TestOversizedNPCRegistryFailsBeforeRuntimeAndCheckpointConstruction(t *test
t.Fatal(err)
}
checkpointRoot := filepath.Join(t.TempDir(), "checkpoints")
content := string(readRepositoryFile(t, "examples", "dnd-spells.config.yml"))
content = replaceRequiredOnce(t, content, " extract: dnd/spells", " extract:\n module: dnd/spells\n references:\n npcs: "+npcPath)
content = replaceRequiredOnce(t, content, " enabled: false\n directory: \"\"", " enabled: true\n directory: "+checkpointRoot)
content := fmt.Sprintf(`version: 3
cache:
chunk_plans:
mode: bypass
checkpoints:
enabled: true
directory: %q
pipelines:
dnd-session:
input: seriatim
artifacts:
spells:
extract:
module: dnd/spells
references:
npcs: %q
normalize: dnd/spells
`, checkpointRoot, npcPath)
configPath := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil {
t.Fatal(err)

View File

@@ -147,7 +147,7 @@ func TestProductionCatalogCoversMaintainedConfigurations(t *testing.T) {
func TestDefaultCLICompositionValidatesRepresentativeConfiguration(t *testing.T) {
var stdout, stderr strings.Builder
code := RunWithOptions([]string{
"config", "validate", "--config", repositoryPath("examples", "dnd-spells.config.yml"), "--pipeline", "dnd-session",
"config", "validate", "--config", repositoryPath("examples", "dnd-minimal.config.yml"), "--pipeline", "dnd-session",
}, &stdout, &stderr, Options{})
if code != 0 || stderr.Len() != 0 {
t.Fatalf("validate representative config with default composition: code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
@@ -176,7 +176,7 @@ func TestProductionPromptAssetsPrepareWithoutProviderCredentials(t *testing.T) {
func TestProductionSpellValidatorsPrepareFromMaterializedCatalog(t *testing.T) {
components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml")
configPath := writeProductionSpellCatalogContractConfig(t)
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
if err != nil {
t.Fatalf("resolve production spell configuration: %v", err)
@@ -203,7 +203,7 @@ func TestProductionSpellValidatorsPrepareFromMaterializedCatalog(t *testing.T) {
func TestProductionSpellNormalizerRejectsInvalidCatalogReferencesBeforeExecution(t *testing.T) {
components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml")
configPath := writeProductionSpellCatalogContractConfig(t)
resolve := func(t *testing.T) pipeline.ResolvedPipeline {
t.Helper()
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
@@ -261,12 +261,15 @@ func TestProductionSpellNormalizerRejectsInvalidCatalogReferencesBeforeExecution
t.Fatal(err)
}
checkpointRoot := filepath.Join(t.TempDir(), "checkpoints")
content := string(readRepositoryFile(t, "examples", "dnd-spells-production.config.yml"))
content = replaceRequiredOnce(t, content, "./dnd-spells-roster.txt", repositoryPath("examples", "dnd-spells-roster.txt"))
content = replaceRequiredOnce(t, content, "./dnd-spells-glossary.txt", repositoryPath("examples", "dnd-spells-glossary.txt"))
content = strings.Replace(content, "./dnd-spells-catalog.json", repositoryPath("examples", "dnd-spells-catalog.json"), 1)
content = replaceRequiredOnce(t, content, "./dnd-spells-catalog.json", catalogPath)
content = replaceRequiredOnce(t, content, " enabled: false\n directory: /var/cache/notarius/checkpoints", " enabled: true\n directory: "+checkpointRoot)
content := productionSpellCatalogContractConfig(t)
catalogSource := repositoryPath("examples", "dnd-spell-catalog.json")
if count := strings.Count(content, catalogSource); count != 2 {
t.Fatalf("spell catalog source occurs %d times, want extract and normalize bindings", count)
}
content = strings.Replace(content, catalogSource, "__extract_catalog__", 1)
content = replaceRequiredOnce(t, content, catalogSource, catalogPath)
content = replaceRequiredOnce(t, content, "__extract_catalog__", catalogSource)
content = replaceRequiredOnce(t, content, " enabled: false\n directory: \"\"", " enabled: true\n directory: "+checkpointRoot)
configFile := filepath.Join(t.TempDir(), "config.yml")
if err := os.WriteFile(configFile, []byte(content), 0o600); err != nil {
t.Fatal(err)
@@ -382,7 +385,7 @@ func TestProductionLLMClientFactoriesRejectInvalidConstruction(t *testing.T) {
}
func TestProductionConfigValidationCoversModuleAndVariantFailures(t *testing.T) {
base := string(readRepositoryFile(t, "examples", "dnd-spells.config.yml"))
base := string(readRepositoryFile(t, "examples", "dnd-minimal.config.yml"))
validPath := writeProductionContractConfig(t, base)
options := productionCLIOptions(t)
var stdout, stderr strings.Builder
@@ -438,7 +441,7 @@ func TestProductionConfigValidationCoversModuleAndVariantFailures(t *testing.T)
}
func TestProductionNormalizeValidatorOverrideRemainsAuthoritative(t *testing.T) {
base := string(readRepositoryFile(t, "examples", "dnd-spells.config.yml"))
base := string(readRepositoryFile(t, "examples", "dnd-minimal.config.yml"))
content := replaceRequiredOnce(t, base, " normalize: dnd/spells\n", " normalize:\n module: dnd/spells\n validators:\n - module: generic/always_accept\n - module: generic/valid_json\n")
path := writeProductionContractConfig(t, content)
components := productionTestComponents(t)
@@ -531,17 +534,45 @@ type maintainedExample struct {
func maintainedExampleFiles(t *testing.T) []maintainedExample {
t.Helper()
return []maintainedExample{
{name: "minimal", path: repositoryPath("examples", "dnd-spells.config.yml"), pipelineIDs: []string{"dnd-session"}},
{name: "production", path: repositoryPath("examples", "dnd-spells-production.config.yml"), pipelineIDs: []string{"dnd-session"}},
{name: "npcs", path: repositoryPath("examples", "dnd-npcs.config.yml"), pipelineIDs: []string{"dnd-session"}},
{name: "combat", path: repositoryPath("examples", "dnd-combat-turns.config.yml"), pipelineIDs: []string{"dnd-combat"}},
{name: "npc-grounded", path: repositoryPath("examples", "dnd-npc-grounded.config.yml"), pipelineIDs: []string{"dnd-npc-grounded"}},
{name: "npc-interactions", path: repositoryPath("examples", "dnd-npc-interactions.config.yml"), pipelineIDs: []string{"dnd-npc-interactions"}},
{name: "scene-descriptions", path: repositoryPath("examples", "dnd-scene-descriptions.config.yml"), pipelineIDs: []string{"dnd-scene-descriptions"}},
{name: "scene-chunk-map", path: repositoryPath("examples", "dnd-scene-chunk-map.config.yml"), pipelineIDs: []string{"dnd-scene-chunk-map"}},
{name: "minimal", path: repositoryPath("examples", "dnd-minimal.config.yml"), pipelineIDs: []string{"dnd-session"}},
{name: "complete", path: repositoryPath("examples", "dnd-complete.config.yml"), pipelineIDs: []string{"dnd-session"}},
}
}
func productionSpellCatalogContractConfig(t *testing.T) string {
t.Helper()
return fmt.Sprintf(`version: 3
cache:
chunk_plans:
mode: bypass
checkpoints:
enabled: false
directory: ""
pipelines:
dnd-session:
input: seriatim
references:
party: %q
glossary: %q
artifacts:
spells:
extract:
module: dnd/spells
retries: 2
references:
spell_catalog: %q
normalize:
module: dnd/spells
references:
spell_catalog: %q
`, repositoryPath("examples", "dnd-party.txt"), repositoryPath("examples", "dnd-glossary.txt"), repositoryPath("examples", "dnd-spell-catalog.json"), repositoryPath("examples", "dnd-spell-catalog.json"))
}
func writeProductionSpellCatalogContractConfig(t *testing.T) string {
t.Helper()
return writeProductionContractConfig(t, productionSpellCatalogContractConfig(t))
}
func loadMaintainedExample(t *testing.T, path string) config.Config {
t.Helper()
fileConfig, err := config.LoadFileConfig(path)

View File

@@ -418,9 +418,11 @@ func (referenceContractCodecB) Decode([]byte) (stateTestArtifact, error) {
func referenceContractLane(t *testing.T, resolved pipeline.ResolvedPipeline, id string) pipeline.ResolvedArtifactLane {
t.Helper()
for _, lane := range resolved.Steps[0].ArtifactLanes {
if lane.ID == id {
return lane
for _, step := range resolved.Steps {
for _, lane := range step.ArtifactLanes {
if lane.ID == id {
return lane
}
}
}
t.Fatalf("lane %q not found", id)

View File

@@ -24,7 +24,7 @@ import (
func TestSpellCatalogBytesAffectCheckpointIdentityButNotSemanticDigest(t *testing.T) {
components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml")
configPath := writeProductionSpellCatalogContractConfig(t)
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
if err != nil {
t.Fatalf("resolve production configuration: %v", err)
@@ -108,8 +108,8 @@ func TestSpellCatalogBytesAffectCheckpointIdentityButNotSemanticDigest(t *testin
}
func TestConfiguredSpellCatalogBindingChangesResolvedPipelineIdentity(t *testing.T) {
base := string(readRepositoryFile(t, "examples", "dnd-spells-production.config.yml"))
changed := strings.Replace(base, "./dnd-spells-catalog.json", "./alternate-spell-catalog.json", 1)
base := productionSpellCatalogContractConfig(t)
changed := strings.Replace(base, repositoryPath("examples", "dnd-spell-catalog.json"), filepath.Join(t.TempDir(), "alternate-spell-catalog.json"), 1)
if changed == base {
t.Fatal("production configuration did not contain the maintained catalog binding")
}
@@ -138,7 +138,7 @@ func TestConfiguredSpellCatalogBindingChangesResolvedPipelineIdentity(t *testing
func TestSemanticSpellCatalogFingerprintChangesCheckpointIdentityWithoutReferenceChange(t *testing.T) {
components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml")
configPath := writeProductionSpellCatalogContractConfig(t)
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
if err != nil {
t.Fatal(err)
@@ -200,7 +200,7 @@ func TestSemanticSpellCatalogFingerprintChangesCheckpointIdentityWithoutReferenc
func TestChangedSemanticSpellCatalogFingerprintCannotResumeRecordedCheckpoint(t *testing.T) {
components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml")
configPath := writeProductionSpellCatalogContractConfig(t)
effective, err := loadMaintainedExample(t, configPath).Resolve(resolveInputForMaintainedExample(components, "dnd-session"))
if err != nil {
t.Fatal(err)
@@ -328,7 +328,7 @@ func TestMaintainedProductionOverlayRunAlignsGroundingValidationAndProvenance(t
var stdout, stderr strings.Builder
code := RunWithOptions([]string{
"run", "dnd-session",
"--config", repositoryPath("examples", "dnd-spells-production.config.yml"),
"--config", writeProductionSpellCatalogContractConfig(t),
"--input", repositoryPath("examples", "seriatim-minimal-transcript.json"),
"--only", "spells", "--chunk_cache", "bypass", "--output-dir", outputRoot,
}, &stdout, &stderr, options)
@@ -373,12 +373,12 @@ func TestMaintainedProductionOverlayRunAlignsGroundingValidationAndProvenance(t
if len(catalogProvenances) != 2 {
t.Fatalf("manifest references = %#v, want independently materialized extract and normalize catalog provenance", manifest.References)
}
overlayBytes := readRepositoryFile(t, "examples", "dnd-spells-catalog.json")
overlayBytes := readRepositoryFile(t, "examples", "dnd-spell-catalog.json")
for _, catalogProvenance := range catalogProvenances {
if catalogProvenance.Stage != "extract" && catalogProvenance.Stage != "normalize" {
t.Fatalf("catalog provenance = %#v, want extract or normalize scope", catalogProvenance)
}
if catalogProvenance.LaneID != "spells" || catalogProvenance.OriginType != "file" || catalogProvenance.MediaType != "application/json" || catalogProvenance.SizeBytes != int64(len(overlayBytes)) || catalogProvenance.Digest != digestBytes(overlayBytes) || !strings.Contains(catalogProvenance.OriginURI, "dnd-spells-catalog.json") {
if catalogProvenance.LaneID != "spells" || catalogProvenance.OriginType != "file" || catalogProvenance.MediaType != "application/json" || catalogProvenance.SizeBytes != int64(len(overlayBytes)) || catalogProvenance.Digest != digestBytes(overlayBytes) || !strings.Contains(catalogProvenance.OriginURI, "dnd-spell-catalog.json") {
t.Fatalf("catalog provenance = %#v, want raw overlay provenance in both scopes", catalogProvenance)
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"path/filepath"
"sync"
"testing"
@@ -50,7 +51,7 @@ func TestProductionSpellCatalogValidationRetries(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
components := productionTestComponents(t)
configPath := repositoryPath("examples", "dnd-spells-production.config.yml")
configPath := writeProductionSpellCatalogContractConfig(t)
cfg := loadMaintainedExample(t, configPath)
effective, err := cfg.Resolve(config.ResolveInput{PipelineID: "dnd-session", Catalog: catalogFromRegistries(components.registries)})
if err != nil {
@@ -58,7 +59,7 @@ func TestProductionSpellCatalogValidationRetries(t *testing.T) {
}
materialized, _, err := pipeline.MaterializeReferences(effective.ResolvedPipeline, catalogFromRegistries(components.registries), pipeline.ReferenceMaterializationOptions{
ConfigPath: configPath,
WorkingDir: repositoryPath("examples"),
WorkingDir: filepath.Dir(configPath),
})
if err != nil {
t.Fatalf("materialize production references: %v", err)