Make composition and boundary tests extension-friendly

This commit is contained in:
2026-07-18 23:50:24 +00:00
parent d88bcb6070
commit 4f96abf42c
9 changed files with 148 additions and 102 deletions

View File

@@ -10,6 +10,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/extract/spells"
)
@@ -19,12 +20,10 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if err := Register(registries, assets); err != nil {
t.Fatalf("Register() error = %v, want nil", err)
}
assertKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
assertKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells"})
if got := registries.ArtifactCodecs.RegisteredKinds(); !reflect.DeepEqual(got, []contracts.ArtifactKind{"dnd/spell-list"}) {
t.Fatalf("artifact codec kinds = %#v, want dnd/spell-list", got)
}
assertKeys(t, "validators", registries.Validators.RegisteredKeys(), []string{
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"dnd/scenes"})
assertContainsKeys(t, "extractors", registries.Extractors.RegisteredKeys(), []string{"dnd/spells"})
assertContainsArtifactKinds(t, registries.ArtifactCodecs.RegisteredKinds(), []contracts.ArtifactKind{dnd.SpellListKind})
assertContainsKeys(t, "validators", registries.Validators.RegisteredKeys(), []string{
"extract/dnd/spells/shape",
"extract/dnd/spells/source_refs",
"extract/dnd/spells/source_relatedness",
@@ -41,7 +40,7 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
if got := registries.ValidatorChains.Validators(pipeline.StageExtract, spells.Key); !reflect.DeepEqual(got, wantChain) {
t.Fatalf("spell validator chain = %#v, want %#v", got, wantChain)
}
assertAssetNames(t, assets.PromptFS, []string{
assertAssetNamesContain(t, assets.PromptFS, []string{
"dnd.scenes/dnd.scenes.yaml",
"dnd.scenes/instructions.md",
"dnd.scenes/sharedassets/common-dnd-references.md",
@@ -55,10 +54,16 @@ func TestRegisterAddsDNDFamily(t *testing.T) {
"dnd.spells/sharedassets/common-dnd-transcript.md",
"dnd.spells/task.md",
})
assertAssetNames(t, assets.SchemaFS, []string{
assertAssetNamesContain(t, assets.SchemaFS, []string{
"dnd_scenes.v1.json",
"dnd_spells_llm.v1.json",
})
if spec, ok := registries.Chunkers.Spec("dnd/scenes"); !ok || spec.Key != "dnd/scenes" {
t.Fatalf("scene chunker spec = %#v, present = %t; want family-owned spec", spec, ok)
}
if spec, ok := registries.Extractors.Spec(spells.Key); !ok || spec.ArtifactKind != dnd.SpellListKind {
t.Fatalf("spell extractor spec = %#v, present = %t; want dnd spell-list artifact", spec, ok)
}
}
func TestRegisterRejectsMissingDNDDependenciesBeforeMutation(t *testing.T) {
@@ -118,14 +123,33 @@ func completeRegistries() pipeline.Registries {
}
}
func assertKeys(t *testing.T, name string, got, want []string) {
func assertContainsKeys(t *testing.T, name string, got, want []string) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Fatalf("%s keys = %#v, want %#v", name, got, want)
seen := make(map[string]struct{}, len(got))
for _, key := range got {
seen[key] = struct{}{}
}
for _, key := range want {
if _, ok := seen[key]; !ok {
t.Fatalf("%s keys = %#v, want required key %q", name, got, key)
}
}
}
func assertAssetNames(t *testing.T, getFS func() (fs.FS, error), want []string) {
func assertContainsArtifactKinds(t *testing.T, got, want []contracts.ArtifactKind) {
t.Helper()
seen := make(map[contracts.ArtifactKind]struct{}, len(got))
for _, kind := range got {
seen[kind] = struct{}{}
}
for _, kind := range want {
if _, ok := seen[kind]; !ok {
t.Fatalf("artifact codec kinds = %#v, want required kind %q", got, kind)
}
}
}
func assertAssetNamesContain(t *testing.T, getFS func() (fs.FS, error), want []string) {
t.Helper()
fSys, err := getFS()
if err != nil {
@@ -141,7 +165,13 @@ func assertAssetNames(t *testing.T, getFS func() (fs.FS, error), want []string)
t.Fatalf("walk assets: %v", err)
}
sort.Strings(got)
if !reflect.DeepEqual(got, want) {
t.Fatalf("asset names = %#v, want %#v", got, want)
seen := make(map[string]struct{}, len(got))
for _, name := range got {
seen[name] = struct{}{}
}
for _, name := range want {
if _, ok := seen[name]; !ok {
t.Fatalf("asset names = %#v, want required asset %q", got, name)
}
}
}

View File

@@ -1,7 +1,6 @@
package register
import (
"reflect"
"strings"
"testing"
@@ -13,21 +12,23 @@ func TestRegisterAddsGenericFamily(t *testing.T) {
if err := Register(registries, nil); err != nil {
t.Fatalf("Register() error = %v, want nil", err)
}
assertKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"generic"})
assertKeys(t, "mergers", registries.Mergers.RegisteredKeys(), nil)
assertKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), nil)
assertKeys(t, "validators", registries.Validators.RegisteredKeys(), []string{
assertContainsKeys(t, "chunkers", registries.Chunkers.RegisteredKeys(), []string{"generic"})
assertContainsKeys(t, "validators", registries.Validators.RegisteredKeys(), []string{
"generic/always_accept",
"generic/always_reject",
"generic/valid_json",
"generic/valid_json_schema",
})
assertKeys(t, "outputs", registries.Outputs.RegisteredKeys(), []string{"json"})
if got := registries.Inputs.RegisteredKeys(); len(got) != 0 {
t.Fatalf("input keys = %#v, want generic registrar to leave inputs unchanged", got)
assertContainsKeys(t, "outputs", registries.Outputs.RegisteredKeys(), []string{"json"})
assertNoKeys(t, "inputs", registries.Inputs.RegisteredKeys(), "generic registrar to leave inputs unchanged")
assertNoKeys(t, "extractors", registries.Extractors.RegisteredKeys(), "generic registrar to leave extractors unchanged")
assertNoKeys(t, "mergers", registries.Mergers.RegisteredKeys(), "generic registrar to leave mergers for typed family composition")
assertNoKeys(t, "normalizers", registries.Normalizers.RegisteredKeys(), "generic registrar to leave normalizers for typed family composition")
if chunker, err := registries.Chunkers.Build("generic"); err != nil || chunker.Key() != "generic" {
t.Fatalf("build generic chunker = %v, %v; want generic implementation", chunker, err)
}
if got := registries.Extractors.RegisteredKeys(); len(got) != 0 {
t.Fatalf("extractor keys = %#v, want generic registrar to leave extractors unchanged", got)
if output, err := registries.Outputs.Build("json"); err != nil || output.Key() != "json" {
t.Fatalf("build json output = %v, %v; want json implementation", output, err)
}
}
@@ -83,9 +84,22 @@ func completeRegistries() pipeline.Registries {
}
}
func assertKeys(t *testing.T, name string, got, want []string) {
func assertContainsKeys(t *testing.T, name string, got, want []string) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Fatalf("%s keys = %#v, want %#v", name, got, want)
seen := make(map[string]struct{}, len(got))
for _, key := range got {
seen[key] = struct{}{}
}
for _, key := range want {
if _, ok := seen[key]; !ok {
t.Fatalf("%s keys = %#v, want required key %q", name, got, key)
}
}
}
func assertNoKeys(t *testing.T, name string, got []string, reason string) {
t.Helper()
if len(got) != 0 {
t.Fatalf("%s keys = %#v, want %s", name, got, reason)
}
}

View File

@@ -13,6 +13,7 @@ import (
)
const moduleImportPrefix = "gitea.maximumdirect.net/eric/notarius/internal/modules/"
const internalImportPrefix = "gitea.maximumdirect.net/eric/notarius/internal/"
func TestProductionImportBoundaries(t *testing.T) {
repositoryRoot := testRepositoryRoot(t)
@@ -252,6 +253,39 @@ func TestImportBoundaryRules(t *testing.T) {
importPath: moduleImportPrefix + "generic/chunk/units",
wantError: true,
},
{
name: "source production may import another core package",
filename: "internal/core/source/source.go",
sourcePackage: "source",
importPath: internalImportPrefix + "artifacts",
},
{
name: "source production cannot import framework",
filename: "internal/core/source/source.go",
sourcePackage: "source",
importPath: internalImportPrefix + "framework/contracts",
wantError: true,
},
{
name: "source production cannot import modules",
filename: "internal/core/source/source.go",
sourcePackage: "source",
importPath: moduleImportPrefix + "dnd",
wantError: true,
},
{
name: "chunkplan production may import framework",
filename: "internal/framework/chunkplan/store.go",
sourcePackage: "chunkplan",
importPath: internalImportPrefix + "framework/contracts",
},
{
name: "chunkplan production cannot import modules",
filename: "internal/framework/chunkplan/store.go",
sourcePackage: "chunkplan",
importPath: moduleImportPrefix + "dnd",
wantError: true,
},
{
name: "framework test may import module implementation",
filename: "internal/framework/pipeline/compatibility_test.go",
@@ -345,6 +379,9 @@ func checkImportBoundaries(repositoryRoot string, filename string) error {
}
func validateImport(filename string, sourcePackage string, importPath string) error {
if !strings.HasSuffix(filename, "_test.go") && strings.HasPrefix(filename, "internal/core/source/") && strings.HasPrefix(importPath, internalImportPrefix+"framework/") {
return importBoundaryViolation(filename, importPath, "core/source production code must not import framework or module implementations")
}
target, ok := moduleTargetForImport(importPath)
if !ok {
return nil
@@ -384,6 +421,12 @@ func validateImport(filename string, sourcePackage string, importPath string) er
}
return importBoundaryViolation(filename, importPath, "direct module imports from non-module tests are allowed only in CLI, core, and framework compatibility-test roots")
}
if strings.HasPrefix(filename, "internal/core/source/") && strings.HasPrefix(importPath, moduleImportPrefix) {
return importBoundaryViolation(filename, importPath, "core/source production code must not import framework or module implementations")
}
if strings.HasPrefix(filename, "internal/framework/chunkplan/") && strings.HasPrefix(importPath, moduleImportPrefix) {
return importBoundaryViolation(filename, importPath, "framework/chunkplan production code must not import module implementations")
}
if strings.HasPrefix(filename, "internal/framework/") || strings.HasPrefix(filename, "internal/core/") {
return importBoundaryViolation(filename, importPath, "core and framework production code must not import module implementations")
}

View File

@@ -1,7 +1,6 @@
package register
import (
"reflect"
"strings"
"testing"
@@ -13,8 +12,22 @@ func TestRegisterAddsSeriatimFamily(t *testing.T) {
if err := Register(registries, nil); err != nil {
t.Fatalf("Register() error = %v, want nil", err)
}
if got, want := registries.Inputs.RegisteredKeys(), []string{"seriatim"}; !reflect.DeepEqual(got, want) {
t.Fatalf("input keys = %#v, want %#v", got, want)
assertContainsKeys(t, "inputs", registries.Inputs.RegisteredKeys(), []string{"seriatim"})
if adapter, err := registries.Inputs.Build("seriatim"); err != nil || adapter.Key() != "seriatim" {
t.Fatalf("build seriatim input = %v, %v; want seriatim implementation", adapter, err)
}
}
func assertContainsKeys(t *testing.T, name string, got, want []string) {
t.Helper()
seen := make(map[string]struct{}, len(got))
for _, key := range got {
seen[key] = struct{}{}
}
for _, key := range want {
if _, ok := seen[key]; !ok {
t.Fatalf("%s keys = %#v, want required key %q", name, got, key)
}
}
}