Validate search filesystem context after selection

This commit is contained in:
2026-10-24 19:13:35 +00:00
parent f64497cb13
commit 4dc3e70877
3 changed files with 63 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
# Configuration Discovery Review Findings
Status: five open findings. Implementation has not been changed by these reviews.
Status: finding 1 complete; findings 2–5 remain open.
Reviewed commit: `e50fee2`, including the discovery implementation commits after
`424c897`. The review compared the implementation with the
@@ -10,7 +10,13 @@ configuration and architecture contracts.
## 1. Defer Absence-Only Context Requirements Until The Search Outcome Is Known
Priority: P2. Status: open.
Priority: P2. Status: complete.
**Completed:** search prevalidation now defers absence-only root requirements
until selection. Ordinary, layered-main, and underlay regression cases compare
searched files with explicit files under an empty context and retain errors for
absent inputs. The regression failed before the fix. Focused context/absence
tests and `./scripts/check.sh` passed with Go 1.26.10 on Linux.
**Location:** [filesystem.go](../../internal/engine/filesystem.go), lines 77–123,
and the outcome-specific validation in

View File

@@ -204,6 +204,53 @@ func TestSearchContextCompatibilityUsesSelectedOutcome(t *testing.T) {
}
}
func TestSearchEmptyContextUsesSelectedOutcome(t *testing.T) {
t.Parallel()
root := t.TempDir()
selected := writeImportFile(t, root, "selected.yml", "value: selected\n")
missing := filepath.Join(root, "missing.yml")
context := &FilesystemContext{}
for _, entry := range []struct {
name string
load func(Source) (*Loaded[discoveryLoadConfig], error)
}{
{"ordinary", func(source Source) (*Loaded[discoveryLoadConfig], error) {
return Load(source, testSecretBinding(), Options[discoveryLoadConfig]{Imports: true, Filesystem: context})
}},
{"layered main", func(source Source) (*Loaded[discoveryLoadConfig], error) {
return LoadLayered(Input{Source: source, Imports: true, Filesystem: context}, testSecretBinding(), LayeredOptions[discoveryLoadConfig]{})
}},
{"underlay", func(source Source) (*Loaded[discoveryLoadConfig], error) {
return LoadLayered(Input{Source: NoSource()}, testSecretBinding(), LayeredOptions[discoveryLoadConfig]{
Underlays: []Underlay{{Inputs: []Input{{Source: source, Imports: true, Filesystem: context}}}},
})
}},
} {
t.Run(entry.name, func(t *testing.T) {
explicit, err := entry.load(File(selected))
if err != nil {
t.Fatal(err)
}
searched, err := entry.load(SearchConfig("", Paths(missing, selected)))
if err != nil {
t.Fatal(err)
}
assertEquivalentDiscoveryLoads(t, searched.Config, searched.Inspection, explicit.Config, explicit.Inspection, Keys("value"))
if searched.Config.Value != "selected" {
t.Fatalf("selected value = %q", searched.Config.Value)
}
for _, source := range []Source{NoSource(), SearchConfig("", Paths(missing))} {
result, err := entry.load(source)
var failure *LoadError
if result != nil || !errors.As(err, &failure) || len(failure.Diagnostics()) != 1 || failure.Diagnostics()[0].Code != CodeOptionsInvalid {
t.Fatalf("absent input with empty context = %#v, %v", result, err)
}
}
})
}
}
func TestSearchSourceObservesFileChangesBetweenLoads(t *testing.T) {
t.Parallel()

View File

@@ -79,7 +79,14 @@ func filesystemOptionDiagnostics(source Source, context *FilesystemContext) []Di
case sourceNone:
return absentFilesystemOptionDiagnostics(context)
case sourceSearch:
return absentFilesystemOptionDiagnostics(context)
if context.FS != nil {
return absentFilesystemOptionDiagnostics(context)
}
// A selected host file can derive its root; absence requires an
// explicit root. Check that compatibility only after selection.
if context.BaseDir != "" && !filepath.IsAbs(context.BaseDir) {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "filesystem base directory must be an absolute host path")}
}
case sourceFile:
if context.FS != nil {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "host file sources cannot use a native filesystem context")}