Cover search exhaustion and fallback selection

This commit is contained in:
2026-11-09 21:18:49 +00:00
parent 68de431bb9
commit 37ea670747
2 changed files with 75 additions and 0 deletions

View File

@@ -74,6 +74,8 @@ sequence-item, and budget checks to verify the extraction.
## Finding 3 — Exhaustion and lazy fallback lack focused regression coverage ## Finding 3 — Exhaustion and lazy fallback lack focused regression coverage
**Status** — Complete. Public tests cover search exhaustion, root-only main context, and missing fallbacks when they win or are shadowed.
**Priority P3.** The context-selection test uses an existing fallback when **Priority P3.** The context-selection test uses an existing fallback when
checking that main derivation wins. It would still pass if the loader checking that main derivation wins. It would still pass if the loader
prematurely opened that fallback. The no-base tests use `NoSource()`, and prematurely opened that fallback. The no-base tests use `NoSource()`, and

View File

@@ -122,3 +122,76 @@ func TestMainRelativeSourceVersionEvidence(t *testing.T) {
} }
} }
} }
func TestMainRelativeOptionalSearchExhaustion(t *testing.T) {
root := t.TempDir()
if err := os.Mkdir(filepath.Join(root, "overlays"), 0700); err != nil {
t.Fatal(err)
}
for name, content := range map[string]string{
"fragment.yml": "settings: {companion: loaded}\n",
"overlays/live.yml": "count: 9\n",
} {
if err := os.WriteFile(filepath.Join(root, name), []byte(content), 0600); err != nil {
t.Fatal(err)
}
}
search := configkit.SearchConfig("", configkit.SearchPaths(filepath.Join(root, "absent.yml")))
missing := filepath.Join(root, "missing-fallback")
fallback := &configkit.FilesystemContext{Root: missing, BaseDir: missing}
for _, tc := range []struct {
name string
mainContext *configkit.FilesystemContext
bindingFallback *configkit.FilesystemContext
overlayFallback *configkit.FilesystemContext
overlay string
wantCount int
wantSources int
}{
{name: "skip without context", wantCount: 3, wantSources: 1},
{
name: "root-only main context wins", mainContext: &configkit.FilesystemContext{Root: root},
bindingFallback: fallback, overlayFallback: fallback, overlay: "live", wantCount: 9, wantSources: 3,
},
{name: "selected missing companion fallback", bindingFallback: fallback},
{name: "selected missing overlay fallback", overlay: "live", overlayFallback: fallback},
} {
t.Run(tc.name, func(t *testing.T) {
underlay := configkit.Input{
Source: configkit.Bytes("companion", []byte("count: 3\nconfig: {imports: [{path: fragment.yml}]}\n")),
Imports: true, MainRelative: &configkit.MainRelativeContext{Fallback: tc.bindingFallback},
}
result, err := configkit.LoadLayered(
configkit.Input{Source: search, Filesystem: tc.mainContext},
configkit.LayeredOptions[layeredFacadeConfig]{
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{underlay}}},
Overlay: tc.overlay, OverlayFallback: tc.overlayFallback,
},
)
if tc.wantSources == 0 {
if err == nil {
t.Fatal("missing selected fallback succeeded")
}
diagnostics := err.(*configkit.LoadError).Diagnostics()
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportInvalid {
t.Fatalf("diagnostics = %#v", diagnostics)
}
return
}
if err != nil {
t.Fatal(err)
}
sources := result.Sources()
if result.Config.Count != tc.wantCount || len(sources) != tc.wantSources {
t.Fatalf("config = %#v, sources = %#v", result.Config, sources)
}
if sources[0].Layer != configkit.LayerUnderlay || len(sources[0].EmptyImportSelections) != 0 {
t.Fatalf("declaring source = %#v", sources[0])
}
if tc.mainContext != nil && (result.Config.Settings["companion"] != "loaded" || sources[2].Layer != configkit.LayerOverlay) {
t.Fatalf("config = %#v, sources = %#v", result.Config, sources)
}
})
}
}