2 Commits

Author SHA1 Message Date
b3e7b62119 Prepare ConfigKit v0.1.2 2026-09-07 13:02:22 +00:00
cd730d5c99 Prepare ConfigKit v0.1.1 2026-09-07 03:06:14 +00:00
17 changed files with 321 additions and 69 deletions

View File

@@ -7,6 +7,8 @@ import (
"gitea.maximumdirect.net/eric/ConfigKit/internal/document" "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
) )
const directiveKey = "configkit"
type importDeclaration struct { type importDeclaration struct {
path string path string
glob string glob string
@@ -19,11 +21,11 @@ func splitImportDirectives(root *document.Node) (*document.Node, []importDeclara
var declarations []importDeclaration var declarations []importDeclaration
directiveIndex := -1 directiveIndex := -1
for index, entry := range body.Mapping { for index, entry := range body.Mapping {
if entry.Key != "_configkit" { if entry.Key != directiveKey {
continue continue
} }
directiveIndex = index directiveIndex = index
parsed, diagnostics := parseImportDirective(entry.Value, Keys("_configkit")) parsed, diagnostics := parseImportDirective(entry.Value, Keys(directiveKey))
if len(diagnostics) != 0 { if len(diagnostics) != 0 {
return nil, nil, diagnostics return nil, nil, diagnostics
} }
@@ -102,7 +104,7 @@ func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic
if node.Kind == document.Mapping { if node.Kind == document.Mapping {
for _, entry := range node.Mapping { for _, entry := range node.Mapping {
childPath := current.Key(entry.Key) childPath := current.Key(entry.Key)
if entry.Key == "_configkit" { if entry.Key == directiveKey {
location := publicLocation(entry.KeyLocation) location := publicLocation(entry.KeyLocation)
return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: childPath, Message: "configuration directives are permitted only at the document root", Primary: &location}} return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: childPath, Message: "configuration directives are permitted only at the document root", Primary: &location}}
} }
@@ -183,7 +185,7 @@ func profileDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic
if node.Kind == document.Mapping { if node.Kind == document.Mapping {
for _, entry := range node.Mapping { for _, entry := range node.Mapping {
childPath := current.Key(entry.Key) childPath := current.Key(entry.Key)
if entry.Key == "_configkit" { if entry.Key == directiveKey {
location := publicLocation(entry.KeyLocation) location := publicLocation(entry.KeyLocation)
return []Diagnostic{{ return []Diagnostic{{
Code: CodeProfileDirective, Severity: SeverityError, Path: childPath, Code: CodeProfileDirective, Severity: SeverityError, Path: childPath,
@@ -209,7 +211,7 @@ func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
if node.Kind == document.Mapping { if node.Kind == document.Mapping {
for _, entry := range node.Mapping { for _, entry := range node.Mapping {
childPath := path.Key(entry.Key) childPath := path.Key(entry.Key)
if entry.Key == "_configkit" { if entry.Key == directiveKey {
location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column} location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column}
diagnostics = append(diagnostics, Diagnostic{ diagnostics = append(diagnostics, Diagnostic{
Code: CodeImportDisabled, Severity: SeverityError, Path: childPath, Code: CodeImportDisabled, Severity: SeverityError, Path: childPath,

View File

@@ -61,7 +61,7 @@ semantic validator, explicit path and glob imports, one selected profile, and
filesystem context. It also accepts explicit CLI override records, static secret filesystem context. It also accepts explicit CLI override records, static secret
tags, and a dynamic path predicate. `NoSource` rejects imports, active profiles, tags, and a dynamic path predicate. `NoSource` rejects imports, active profiles,
and filesystem context as invalid combinations but accepts overrides. Input and filesystem context as invalid combinations but accepts overrides. Input
containing a reserved `_configkit` mapping key is rejected while imports are containing a reserved `configkit` mapping key is rejected while imports are
disabled. disabled.
Every source, options, schema, version, defaults, decode, resolution, or validation Every source, options, schema, version, defaults, decode, resolution, or validation
@@ -99,6 +99,13 @@ equal-valued input. Profile replacement removes displaced descendants from effec
metadata while retaining their base definition and the parent replacement operation metadata while retaining their base definition and the parent replacement operation
in history. All metadata queries and returned nested records are detached. in history. All metadata queries and returned nested records are detached.
`Metadata.HistoricalPaths` enumerates observed paths, including removed paths,
without reloading input or resolving unselected credentials. Consumers can use
it with `History` to validate ownership of declarations hidden by an overlay;
domain ownership rules remain application responsibilities. The method's Go
documentation in [metadata.go](../metadata.go) defines enumeration and path-ordering
details.
History is chronological: a path can have a synthetic default record, its exact base History is chronological: a path can have a synthetic default record, its exact base
definition, a profile replacement, and a recorded resolution operation. A descendant definition, a profile replacement, and a recorded resolution operation. A descendant
query uses the ancestor's path for a whole-container or whole-sequence replacement; query uses the ancestor's path for a whole-container or whole-sequence replacement;
@@ -419,7 +426,7 @@ origin data; history retains the displaced definitions and identifies the parent
replacement. Surviving profile paths have profile origins, while recursively shared replacement. Surviving profile paths have profile origins, while recursively shared
mapping containers retain their contributing locations. mapping containers retain their contributing locations.
A profile may not contain `_configkit` anywhere, contribute the configured schema A profile may not contain `configkit` anywhere, contribute the configured schema
version, or replace an ancestor of that version with a scalar or null. These rules version, or replace an ancestor of that version with a scalar or null. These rules
apply even if the supplied version text would be unchanged. Profiles cannot mask a apply even if the supplied version text would be unchanged. Profiles cannot mask a
base parse, composition, or version failure because those checks run first. base parse, composition, or version failure because those checks run first.
@@ -452,7 +459,7 @@ All records are detached and validated before any is applied. Root and explicit
sequence-index paths are invalid; parsed numeric tokens remain mapping keys. A path sequence-index paths are invalid; parsed numeric tokens remain mapping keys. A path
may create missing mapping ancestors but cannot traverse an existing scalar, null, may create missing mapping ancestors but cannot traverse an existing scalar, null,
or sequence. Duplicate and ancestor/descendant-overlapping records are rejected. or sequence. Duplicate and ancestor/descendant-overlapping records are rejected.
Neither paths nor values may create `_configkit`, and no record may overlap the Neither paths nor values may create `configkit`, and no record may overlap the
configured schema-version path. Unknown fields and effective type mismatches remain configured schema-version path. Unknown fields and effective type mismatches remain
ordinary strict-decoder errors. ordinary strict-decoder errors.
@@ -475,12 +482,18 @@ zero.
## Explicit imports ## Explicit imports
Imports are opt-in. With `Options.Imports` enabled, a document may reserve its root Imports are opt-in. With `Options.Imports` enabled, a document may reserve its root
`_configkit` mapping for an `imports` sequence. ConfigKit removes that mapping before `configkit` mapping for an `imports` sequence. ConfigKit removes that mapping before
application decoding. Each sequence item is a mapping with exactly one nonempty application decoding. Each sequence item is a mapping with exactly one nonempty
string `path` or `glob` plus an optional boolean `optional`; all other shapes and string `path` or `glob` plus an optional boolean `optional`; all other shapes and
fields are `import.invalid`. A `_configkit` key below the root is invalid. With fields are `import.invalid`. A `configkit` key below the root is invalid. With
imports disabled, any occurrence remains `import.disabled`. imports disabled, any occurrence remains `import.disabled`.
The directive key is `configkit`, not the `_configkit` spelling used in v0.1.0.
There is no legacy alias: `_configkit` is ordinary application data, subject to
normal schema validation, and does not trigger imports. When upgrading, rename
directive keys in root files and imported fragments, and rename any application
fields previously named `configkit` to avoid the newly reserved key.
Path declarations use slash-separated relative paths. Absolute paths and network Path declarations use slash-separated relative paths. Absolute paths and network
URLs are invalid. Each path resolves from its declaring file's canonical directory, URLs are invalid. Each path resolves from its declaring file's canonical directory,
and files are traversed depth-first in declaration order: a file contributes its own and files are traversed depth-first in declaration order: a file contributes its own

View File

@@ -2,7 +2,7 @@
**Base Policy Version:** 0.1.0 **Base Policy Version:** 0.1.0
**Last Updated:** 2026-09-06 **Last Updated:** 2026-09-07
## Purpose ## Purpose
@@ -220,5 +220,12 @@ restating or redefining its requirements.
## Policy Exceptions ## Policy Exceptions
None. Candidate selection, publication, and post-publication verification remain For v0.1.1 only, the maintainer approved renaming the reserved `_configkit` key to
mandatory steps in the [release procedure](../release.md), not policy exceptions. `configkit` as an error-correcting point release because there are no current
consumers. This intentionally changes the v0.1.0 configuration contract without a
minor-version increment. The release note must explain the renamed directive,
the lack of a legacy alias, and the newly reserved application key. This exception
does not apply to subsequent releases or waive any validation or publication gate.
Candidate selection, publication, and post-publication verification remain
mandatory steps in the [release procedure](../release.md).

43
docs/releases/v0.1.1.md Normal file
View File

@@ -0,0 +1,43 @@
# ConfigKit v0.1.1
This release corrects the spelling of ConfigKit's reserved configuration key.
## Summary
The reserved directive key is now `configkit` instead of `_configkit`. Import
declarations continue to use the same `imports` sequence and path/glob syntax.
See the [configuration contract](../config.md#explicit-imports).
## Compatibility
This is an intentional configuration compatibility change, approved as a one-time
[patch-release exception](../policy/release.md#policy-exceptions) because there are
no current consumers. `_configkit` is no longer a directive or a reserved key; it
is ordinary application data and is subject to normal schema validation. There is
no legacy alias. `configkit` is now reserved, including in nested mappings, profiles,
and override paths and values.
The Go API, diagnostic codes, Go minimum version, and supported platforms are
unchanged. The private YAML dependency remains pinned to
`go.yaml.in/yaml/v4 v4.0.0-rc.6`, confirmed as the newest available v4 release
during candidate preparation.
## Upgrade
Rename `_configkit` directive keys to `configkit` in root configuration files and
imported fragments. Rename any application-owned `configkit` fields to avoid the
newly reserved key. Update diagnostic-path expectations to the new spelling.
Files retaining `_configkit` will not load their declared imports.
Consumers can select this release with:
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.1.1
```
## Changes
- Renamed the directive key consistently across imports, profiles, and overrides.
- Updated current configuration documentation and the composed consumer fixture.
- Updated behavioral tests and added coverage proving the old spelling is not an
import alias.

50
docs/releases/v0.1.2.md Normal file
View File

@@ -0,0 +1,50 @@
# ConfigKit v0.1.2
This release adds safe enumeration of effective and removed metadata paths.
## Summary
`Metadata.HistoricalPaths` returns the semantic paths observed during a load,
including paths removed by profiles, CLI overrides, or recorded resolution. This
allows applications to discover hidden declarations and inspect their ownership
through the existing `Metadata.History` API without reloading configuration or
resolving unselected credentials.
See the [layered metadata contract](../config.md#layered-metadata) for current
behavior.
## Compatibility
This is a backward-compatible API addition. Existing APIs, configuration syntax,
diagnostic codes, minimum Go version, and supported platforms are unchanged.
Existing consumers do not need to change their code or configuration.
Returned paths are detached, unique by canonical JSON Pointer spelling, and sorted
by that spelling. The enumeration includes defaults and the root, exposes no
configuration values, and performs no I/O or secret resolution.
The private YAML dependency remains pinned to
`go.yaml.in/yaml/v4 v4.0.0-rc.6`, confirmed as the newest available v4 release during
candidate preparation.
## Upgrade
No action required. Consumers that want historical path discovery can update with:
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.1.2
```
Then iterate over `result.Metadata().HistoricalPaths()` and use `History` for
chronological definitions or `Lookup` to determine whether each path remains
effective.
## Changes
- Added `Metadata.HistoricalPaths` with detached, deterministic path enumeration.
- Included effective, default, and removed paths while excluding paths that never
existed.
- Added coverage for parent removal, mapping-to-sequence shape replacement, escaped
mapping keys, detached results, zero metadata, composite base ownership, and
avoidance of superseded secret lookup.
- Documented historical path enumeration in the current configuration contract.

View File

@@ -1,4 +1,4 @@
_configkit: configkit:
imports: imports:
- path: artifacts.yml - path: artifacts.yml
poll_interval: 1m poll_interval: 1m

View File

@@ -23,7 +23,7 @@ type explanationConcurrency struct {
func TestExplainShowsDefaultBaseAndEffectiveProfileValue(t *testing.T) { func TestExplainShowsDefaultBaseAndEffectiveProfileValue(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "config.yml", "_configkit:\n imports: [{path: transcription.yml}]\n") mainPath := writeImportFile(t, directory, "config.yml", "configkit:\n imports: [{path: transcription.yml}]\n")
writeImportFile(t, directory, "transcription.yml", "transcription:\n concurrency: 8\n") writeImportFile(t, directory, "transcription.yml", "transcription:\n concurrency: 8\n")
writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), "transcription:\n concurrency: 1\n") writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), "transcription:\n concurrency: 1\n")
result, err := Load[explanationConcurrency](File(mainPath), Options[explanationConcurrency]{ result, err := Load[explanationConcurrency](File(mainPath), Options[explanationConcurrency]{

View File

@@ -10,13 +10,13 @@ import (
func TestLoadGlobImportsUseLexicalDeclarationOrderAndNestedDFS(t *testing.T) { func TestLoadGlobImportsUseLexicalDeclarationOrderAndNestedDFS(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `_configkit: mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports: imports:
- glob: fragments/*.yml - glob: fragments/*.yml
- path: after.yml - path: after.yml
items: {root: root} items: {root: root}
`) `)
aPath := writeImportFile(t, directory, "fragments/a.yml", `_configkit: aPath := writeImportFile(t, directory, "fragments/a.yml", `configkit:
imports: [{path: nested/child.yml}] imports: [{path: nested/child.yml}]
items: {alpha: alpha} items: {alpha: alpha}
`) `)
@@ -56,7 +56,7 @@ items: {alpha: alpha}
func TestLoadGlobImportsSupportEscapesClassesAndExplicitConfD(t *testing.T) { func TestLoadGlobImportsSupportEscapesClassesAndExplicitConfD(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `_configkit: mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports: imports:
- glob: 'conf.d/literal\*.yml' - glob: 'conf.d/literal\*.yml'
- glob: 'conf.d/item-[ab].yml' - glob: 'conf.d/item-[ab].yml'
@@ -126,7 +126,7 @@ func TestLoadGlobImportsPreserveIdentityAndFileRules(t *testing.T) {
t.Run("overlapping declarations", func(t *testing.T) { t.Run("overlapping declarations", func(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
writeImportFile(t, directory, "conf/value.yml", "items: {value: value}\n") writeImportFile(t, directory, "conf/value.yml", "items: {value: value}\n")
mainPath := writeImportFile(t, directory, "main.yml", `_configkit: mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports: imports:
- glob: conf/*.yml - glob: conf/*.yml
- glob: conf/value.* - glob: conf/value.*
@@ -269,5 +269,5 @@ func writeGlobRoot(t *testing.T, directory, pattern string, optional bool) strin
if optional { if optional {
optionalLine = "\n optional: true" optionalLine = "\n optional: true"
} }
return writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - glob: '"+pattern+"'"+optionalLine+"\n") return writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - glob: '"+pattern+"'"+optionalLine+"\n")
} }

View File

@@ -22,16 +22,37 @@ type importTestConfig struct {
Items map[string]string `yaml:"items"` Items map[string]string `yaml:"items"`
} }
func TestLoadFormerDirectiveKeyIsApplicationData(t *testing.T) {
type config struct {
Legacy struct {
Imports []struct {
Path string `yaml:"path"`
} `yaml:"imports"`
} `yaml:"_configkit"`
}
for _, imports := range []bool{false, true} {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: nonexistent.yml\n")
result, err := Load[config](File(mainPath), Options[config]{Imports: imports})
if err != nil {
t.Fatalf("Imports=%v: %v", imports, err)
}
if len(result.Config.Legacy.Imports) != 1 || result.Config.Legacy.Imports[0].Path != "nonexistent.yml" || len(result.Sources()) != 1 {
t.Fatalf("Imports=%v: former directive was not retained as application data: %#v", imports, result)
}
}
}
func TestLoadTraversesPathImportsDepthFirstAndComposesOrigins(t *testing.T) { func TestLoadTraversesPathImportsDepthFirstAndComposesOrigins(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `_configkit: mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports: imports:
- path: first.yml - path: first.yml
- path: second.yml - path: second.yml
settings: settings:
root: root root: root
`) `)
firstPath := writeImportFile(t, directory, "first.yml", `_configkit: firstPath := writeImportFile(t, directory, "first.yml", `configkit:
imports: imports:
- path: nested/third.yml - path: nested/third.yml
settings: settings:
@@ -71,7 +92,7 @@ settings:
func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) { func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) {
t.Run("all duplicate claimants", func(t *testing.T) { t.Run("all duplicate claimants", func(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `_configkit: mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports: imports:
- path: one.yml - path: one.yml
- path: two.yml - path: two.yml
@@ -97,7 +118,7 @@ func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) {
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: fragment.yml\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: fragment.yml\n")
writeImportFile(t, directory, "fragment.yml", test.fragment) writeImportFile(t, directory, "fragment.yml", test.fragment)
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true, Definition: Definition[importTestConfig]{Version: versionRule}}, CodeImportVersion) loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true, Definition: Definition[importTestConfig]{Version: versionRule}}, CodeImportVersion)
diagnostic := loadError.Diagnostics()[0] diagnostic := loadError.Diagnostics()[0]
@@ -114,15 +135,15 @@ func TestLoadImportDirectiveValidationAndTemporaryGlobGate(t *testing.T) {
body string body string
code Code code Code
}{ }{
{name: "directive kind", body: "_configkit: []\n", code: CodeImportInvalid}, {name: "directive kind", body: "configkit: []\n", code: CodeImportInvalid},
{name: "unknown directive field", body: "_configkit: {unknown: true}\n", code: CodeImportInvalid}, {name: "unknown directive field", body: "configkit: {unknown: true}\n", code: CodeImportInvalid},
{name: "imports kind", body: "_configkit: {imports: {}}\n", code: CodeImportInvalid}, {name: "imports kind", body: "configkit: {imports: {}}\n", code: CodeImportInvalid},
{name: "declaration kind", body: "_configkit: {imports: [file.yml]}\n", code: CodeImportInvalid}, {name: "declaration kind", body: "configkit: {imports: [file.yml]}\n", code: CodeImportInvalid},
{name: "missing target", body: "_configkit: {imports: [{optional: true}]}\n", code: CodeImportInvalid}, {name: "missing target", body: "configkit: {imports: [{optional: true}]}\n", code: CodeImportInvalid},
{name: "two targets", body: "_configkit: {imports: [{path: a.yml, glob: '*.yml'}]}\n", code: CodeImportInvalid}, {name: "two targets", body: "configkit: {imports: [{path: a.yml, glob: '*.yml'}]}\n", code: CodeImportInvalid},
{name: "unknown declaration field", body: "_configkit: {imports: [{path: a.yml, extra: true}]}\n", code: CodeImportInvalid}, {name: "unknown declaration field", body: "configkit: {imports: [{path: a.yml, extra: true}]}\n", code: CodeImportInvalid},
{name: "optional kind", body: "_configkit: {imports: [{path: a.yml, optional: 'yes'}]}\n", code: CodeImportInvalid}, {name: "optional kind", body: "configkit: {imports: [{path: a.yml, optional: 'yes'}]}\n", code: CodeImportInvalid},
{name: "nested directive", body: "nested: {_configkit: {}}\n", code: CodeImportInvalid}, {name: "nested directive", body: "nested: {configkit: {}}\n", code: CodeImportInvalid},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
@@ -136,7 +157,7 @@ func TestLoadImportDirectiveValidationAndTemporaryGlobGate(t *testing.T) {
func TestLoadImportMissingContainmentAndRegularFileRules(t *testing.T) { func TestLoadImportMissingContainmentAndRegularFileRules(t *testing.T) {
t.Run("optional missing", func(t *testing.T) { t.Run("optional missing", func(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `_configkit: mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports: imports:
- path: missing.yml - path: missing.yml
optional: true optional: true
@@ -172,7 +193,7 @@ settings: {root: retained}
if test.setup != nil { if test.setup != nil {
test.setup(t, directory) test.setup(t, directory)
} }
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: \""+test.path+"\"\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: \""+test.path+"\"\n")
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, test.code) requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, test.code)
}) })
} }
@@ -181,9 +202,9 @@ settings: {root: retained}
func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) { func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
t.Run("cycle", func(t *testing.T) { t.Run("cycle", func(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports: [{path: a.yml}]\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: a.yml}]\n")
writeImportFile(t, directory, "a.yml", "_configkit:\n imports: [{path: b.yml}]\n") writeImportFile(t, directory, "a.yml", "configkit:\n imports: [{path: b.yml}]\n")
writeImportFile(t, directory, "b.yml", "_configkit:\n imports: [{path: main.yml}]\n") writeImportFile(t, directory, "b.yml", "configkit:\n imports: [{path: main.yml}]\n")
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle) loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle)
diagnostic := loadError.Diagnostics()[0] diagnostic := loadError.Diagnostics()[0]
if diagnostic.Primary == nil || len(diagnostic.Related) != 2 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "a.yml") || !strings.Contains(diagnostic.Message, "b.yml") { if diagnostic.Primary == nil || len(diagnostic.Related) != 2 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "a.yml") || !strings.Contains(diagnostic.Message, "b.yml") {
@@ -193,9 +214,9 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
t.Run("diamond", func(t *testing.T) { t.Run("diamond", func(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n")
writeImportFile(t, directory, "left.yml", "_configkit:\n imports: [{path: shared.yml}]\n") writeImportFile(t, directory, "left.yml", "configkit:\n imports: [{path: shared.yml}]\n")
writeImportFile(t, directory, "right.yml", "_configkit:\n imports: [{path: shared.yml}]\n") writeImportFile(t, directory, "right.yml", "configkit:\n imports: [{path: shared.yml}]\n")
writeImportFile(t, directory, "shared.yml", "settings: {root: shared}\n") writeImportFile(t, directory, "shared.yml", "settings: {root: shared}\n")
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated) loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated)
diagnostic := loadError.Diagnostics()[0] diagnostic := loadError.Diagnostics()[0]
@@ -216,7 +237,7 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) {
if err := os.Symlink("target.yml", filepath.Join(directory, "second-link.yml")); err != nil { if err := os.Symlink("target.yml", filepath.Join(directory, "second-link.yml")); err != nil {
t.Skipf("symlinks unavailable: %v", err) t.Skipf("symlinks unavailable: %v", err)
} }
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: first-link.yml}, {path: second-link.yml}]}\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: first-link.yml}, {path: second-link.yml}]}\n")
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated) requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated)
}) })
} }
@@ -231,7 +252,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
if err := os.Symlink(target, filepath.Join(directory, "link.yml")); err != nil { if err := os.Symlink(target, filepath.Join(directory, "link.yml")); err != nil {
t.Skipf("symlinks unavailable: %v", err) t.Skipf("symlinks unavailable: %v", err)
} }
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: link.yml}]}\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: link.yml}]}\n")
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true}) result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
if err != nil || result.Config.Settings.First != "linked" || result.Sources()[1].Path != canonicalTestPath(t, target) { if err != nil || result.Config.Settings.First != "linked" || result.Sources()[1].Path != canonicalTestPath(t, target) {
t.Fatalf("symlink result = %#v, %v", result, err) t.Fatalf("symlink result = %#v, %v", result, err)
@@ -245,7 +266,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
if err := os.Symlink(target, filepath.Join(root, "escape.yml")); err != nil { if err := os.Symlink(target, filepath.Join(root, "escape.yml")); err != nil {
t.Skipf("symlinks unavailable: %v", err) t.Skipf("symlinks unavailable: %v", err)
} }
mainPath := writeImportFile(t, root, "main.yml", "_configkit: {imports: [{path: escape.yml}]}\n") mainPath := writeImportFile(t, root, "main.yml", "configkit: {imports: [{path: escape.yml}]}\n")
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot) requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot)
}) })
@@ -255,7 +276,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
if err := os.Link(one, filepath.Join(directory, "two.yml")); err != nil { if err := os.Link(one, filepath.Join(directory, "two.yml")); err != nil {
t.Skipf("hard links unavailable: %v", err) t.Skipf("hard links unavailable: %v", err)
} }
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n")
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeComposeConflict) requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeComposeConflict)
}) })
@@ -264,7 +285,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
if err := os.Symlink("loop.yml", filepath.Join(directory, "loop.yml")); err != nil { if err := os.Symlink("loop.yml", filepath.Join(directory, "loop.yml")); err != nil {
t.Skipf("symlinks unavailable: %v", err) t.Skipf("symlinks unavailable: %v", err)
} }
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: loop.yml}]}\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: loop.yml}]}\n")
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportInvalid) requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportInvalid)
}) })
} }
@@ -272,7 +293,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T) { func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
writeImportFile(t, directory, "fragment.yml", "settings: {first: imported}\n") writeImportFile(t, directory, "fragment.yml", "settings: {first: imported}\n")
input := []byte("_configkit: {imports: [{path: fragment.yml}]}\n") input := []byte("configkit: {imports: [{path: fragment.yml}]}\n")
context := &FilesystemContext{BaseDir: directory, Root: directory} context := &FilesystemContext{BaseDir: directory, Root: directory}
for _, test := range []struct { for _, test := range []struct {
name string name string
@@ -295,7 +316,7 @@ func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T)
func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) { func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) {
root := t.TempDir() root := t.TempDir()
mainPath := writeImportFile(t, root, "configs/main.yml", "_configkit: {imports: [{path: ../shared.yml}]}\n") mainPath := writeImportFile(t, root, "configs/main.yml", "configkit: {imports: [{path: ../shared.yml}]}\n")
writeImportFile(t, root, "shared.yml", "settings: {root: shared}\n") writeImportFile(t, root, "shared.yml", "settings: {root: shared}\n")
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{ result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{
Imports: true, Filesystem: &FilesystemContext{Root: root}, Imports: true, Filesystem: &FilesystemContext{Root: root},
@@ -309,7 +330,7 @@ func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) {
func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) { func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) {
t.Run("parse", func(t *testing.T) { t.Run("parse", func(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: broken.yml}]}\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: broken.yml}]}\n")
writeImportFile(t, directory, "broken.yml", "settings: [\n") writeImportFile(t, directory, "broken.yml", "settings: [\n")
loadError := requireAnyLoadError(t, File(mainPath), Options[importTestConfig]{Imports: true}) loadError := requireAnyLoadError(t, File(mainPath), Options[importTestConfig]{Imports: true})
diagnostics := loadError.Diagnostics() diagnostics := loadError.Diagnostics()
@@ -320,7 +341,7 @@ func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) {
t.Run("decode", func(t *testing.T) { t.Run("decode", func(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: unknown.yml}]}\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: unknown.yml}]}\n")
writeImportFile(t, directory, "unknown.yml", "unknown: value\n") writeImportFile(t, directory, "unknown.yml", "unknown: value\n")
loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeDecodeUnknownField) loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeDecodeUnknownField)
diagnostic := loadError.Diagnostics()[0] diagnostic := loadError.Diagnostics()[0]
@@ -332,7 +353,7 @@ func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) {
func TestOptionalImportDoesNotSuppressUnsafeOrUnreadableTargets(t *testing.T) { func TestOptionalImportDoesNotSuppressUnsafeOrUnreadableTargets(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "outside.yml", "_configkit: {imports: [{path: ../missing.yml, optional: true}]}\n") mainPath := writeImportFile(t, directory, "outside.yml", "configkit: {imports: [{path: ../missing.yml, optional: true}]}\n")
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot) requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot)
locked := writeImportFile(t, directory, "locked.yml", "settings: {root: locked}\n") locked := writeImportFile(t, directory, "locked.yml", "settings: {root: locked}\n")
@@ -344,13 +365,13 @@ func TestOptionalImportDoesNotSuppressUnsafeOrUnreadableTargets(t *testing.T) {
_ = file.Close() _ = file.Close()
t.Skip("filesystem does not enforce the unreadable-file fixture") t.Skip("filesystem does not enforce the unreadable-file fixture")
} }
mainPath = writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: locked.yml, optional: true}]}\n") mainPath = writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: locked.yml, optional: true}]}\n")
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeSourcePermission) requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeSourcePermission)
} }
func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) { func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n")
writeImportFile(t, directory, "one.yml", "settings: {first: one}\n") writeImportFile(t, directory, "one.yml", "settings: {first: one}\n")
writeImportFile(t, directory, "two.yml", "settings: {second: two}\n") writeImportFile(t, directory, "two.yml", "settings: {second: two}\n")
main := parseImportMain(t, mainPath) main := parseImportMain(t, mainPath)
@@ -370,7 +391,7 @@ func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) {
}) })
} }
writeImportFile(t, directory, "one.yml", "_configkit: {imports: [{path: nested.yml}]}\n") writeImportFile(t, directory, "one.yml", "configkit: {imports: [{path: nested.yml}]}\n")
writeImportFile(t, directory, "nested.yml", "settings: {nested: nested}\n") writeImportFile(t, directory, "nested.yml", "settings: {nested: nested}\n")
main = parseImportMain(t, mainPath) main = parseImportMain(t, mainPath)
_, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, importLimits{depth: 1, sources: 8, bytes: maxTotalBytes, nodes: maxDocumentNodes}) _, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, importLimits{depth: 1, sources: 8, bytes: maxTotalBytes, nodes: maxDocumentNodes})
@@ -381,7 +402,7 @@ func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) {
func TestImportTraversalAcceptsExactPrivateBudgets(t *testing.T) { func TestImportTraversalAcceptsExactPrivateBudgets(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: imported.yml}]}\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: imported.yml}]}\n")
importedPath := writeImportFile(t, directory, "imported.yml", "settings: {first: imported}\n") importedPath := writeImportFile(t, directory, "imported.yml", "settings: {first: imported}\n")
main := parseImportMain(t, mainPath) main := parseImportMain(t, mainPath)
imported := parseImportMain(t, importedPath) imported := parseImportMain(t, importedPath)

View File

@@ -240,7 +240,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) {
}) })
} }
result, err := Load[config](Bytes("directive.yml", []byte("nested:\n _configkit: {}\n"))) result, err := Load[config](Bytes("directive.yml", []byte("nested:\n configkit: {}\n")))
if result != nil || err == nil { if result != nil || err == nil {
t.Fatalf("result, err = %#v, %v", result, err) t.Fatalf("result, err = %#v, %v", result, err)
} }
@@ -249,7 +249,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
diagnostic := loadError.Diagnostics()[0] diagnostic := loadError.Diagnostics()[0]
if diagnostic.Code != CodeImportDisabled || diagnostic.Path.String() != "/nested/_configkit" || diagnostic.Primary == nil || diagnostic.Primary.Line != 2 { if diagnostic.Code != CodeImportDisabled || diagnostic.Path.String() != "/nested/configkit" || diagnostic.Primary == nil || diagnostic.Primary.Line != 2 {
t.Fatalf("diagnostic = %#v", diagnostic) t.Fatalf("diagnostic = %#v", diagnostic)
} }
} }

View File

@@ -1,6 +1,10 @@
package configkit package configkit
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" import (
"sort"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
// Metadata provides detached effective presence/origin and chronological history // Metadata provides detached effective presence/origin and chronological history
// queries for a load. Its zero value contains no paths. // queries for a load. Its zero value contains no paths.
@@ -50,6 +54,32 @@ func (m Metadata) History(path Path) []Change {
return cloned return cloned
} }
// HistoricalPaths returns the paths observed during loading, including effective
// paths and paths removed by overlays or recorded resolution. It includes default
// paths and the root, but not paths that never existed. The zero Metadata returns
// an empty slice.
//
// Paths are detached, unique by canonical JSON Pointer, and sorted by that
// spelling. Like ParsePath results, numeric tokens are resolved contextually;
// one pointer can identify a mapping key in an earlier layer and a sequence index
// in a later one. Use History to inspect definitions and Lookup to check whether
// a path remains effective. Enumeration exposes no configuration values and does
// not perform another load or resolve secrets.
func (m Metadata) HistoricalPaths() []Path {
pointers := make([]string, 0, len(m.historicalPaths))
for pointer := range m.historicalPaths {
pointers = append(pointers, pointer)
}
sort.Strings(pointers)
paths := make([]Path, 0, len(pointers))
for _, pointer := range pointers {
// The index contains only canonical pointers produced by Path.String.
path, _ := ParsePath(pointer)
paths = append(paths, path)
}
return paths
}
// Textual pointers are lookup keys, but explicit key/index kinds remain part of // Textual pointers are lookup keys, but explicit key/index kinds remain part of
// the query. Candidate paths are constructed from actual document containers. // the query. Candidate paths are constructed from actual document containers.
func matchesPathShape(query, candidate Path) bool { func matchesPathShape(query, candidate Path) bool {

View File

@@ -2,9 +2,95 @@ package configkit
import ( import (
"bytes" "bytes"
"reflect"
"testing" "testing"
) )
func TestMetadataHistoricalPaths(t *testing.T) {
t.Parallel()
result, err := Load[map[string]any](Bytes("base", []byte("retired: {child: 1}\nshape: {'0': base}\n'a/b~': {'': 2}\n")), Options[map[string]any]{
Overrides: []Override{
{Path: Keys("retired"), Value: nil, Flag: "--retired"},
{Path: Keys("shape"), Value: []any{"replacement"}, Flag: "--shape"},
},
})
if err != nil {
t.Fatal(err)
}
metadata := result.Metadata()
paths := metadata.HistoricalPaths()
var got []string
for _, path := range paths {
got = append(got, path.String())
if len(metadata.History(path)) == 0 {
t.Fatalf("enumerated path %s has no history", path)
}
}
want := []string{"", "/a~1b~0", "/a~1b~0/", "/retired", "/retired/child", "/shape", "/shape/0"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("historical paths = %v, want %v", got, want)
}
if _, ok := metadata.Lookup(paths[4]); ok {
t.Fatal("removed descendant remains effective")
}
if _, ok := metadata.Lookup(paths[6]); !ok {
t.Fatal("enumerated numeric path cannot query the new sequence shape")
}
// Returned slices and nested paths must not alias retained metadata.
paths[1].segments[0].key = "mutated"
paths[2] = Keys("replacement")
if again := metadata.HistoricalPaths(); again[1].String() != want[1] || again[2].String() != want[2] {
t.Fatal("enumeration returned aliased paths")
}
if len((Metadata{}).HistoricalPaths()) != 0 {
t.Fatal("zero metadata contains paths")
}
}
func TestHistoricalPathsExposeHiddenBaseOwnershipWithoutResolvingBaseSecrets(t *testing.T) {
t.Setenv("CONFIGKIT_HISTORY_UNUSED_PROD_KEY", "")
t.Setenv("CONFIGKIT_HISTORY_TEST_KEY", "synthetic-test-key")
type config struct {
Credential *Secret `yaml:"credential"`
Definitions map[string]map[string]string `yaml:"definitions"`
}
for _, credential := range []string{"CONFIGKIT_HISTORY_TEST_KEY", "null"} {
t.Run(credential, func(t *testing.T) {
dir := t.TempDir()
root := writeImportFile(t, dir, "main.yml", "configkit:\n imports: [{path: first.yml}, {path: second.yml}]\ncredential: CONFIGKIT_HISTORY_UNUSED_PROD_KEY\n")
writeImportFile(t, dir, "first.yml", "definitions: {recap: {prompt: recap}}\n")
writeImportFile(t, dir, "second.yml", "definitions: {recap: {output: recap.md}}\n")
writeImportFile(t, dir, "profiles/testing.yml", "definitions: null\ncredential: "+credential+"\n")
result, err := Load[config](File(root), Options[config]{Imports: true, Profile: "testing"})
if err != nil {
t.Fatal(err)
}
metadata := result.Metadata()
found := false
for _, path := range metadata.HistoricalPaths() {
if path.String() != "/definitions/recap" {
continue
}
found = true
owners := map[string]bool{}
for _, change := range metadata.History(path) {
if change.Origin.Layer == LayerBase {
for _, location := range change.Origin.Locations {
owners[location.SourceID] = true
}
}
}
if len(owners) != 2 {
t.Fatalf("hidden declaration owners = %v", owners)
}
}
if !found {
t.Fatal("hidden base declaration was not discoverable")
}
})
}
}
func TestMetadataQueriesReturnDetachedRecords(t *testing.T) { func TestMetadataQueriesReturnDetachedRecords(t *testing.T) {
t.Parallel() t.Parallel()

View File

@@ -188,7 +188,7 @@ func overlappingOverride(previous []Override, valid []bool, path Path) int {
func pathContainsDirective(path Path) bool { func pathContainsDirective(path Path) bool {
for _, segment := range path.segments { for _, segment := range path.segments {
if segment.key == "_configkit" { if segment.key == directiveKey {
return true return true
} }
} }
@@ -205,7 +205,7 @@ func overrideDirectivePath(node *document.Node, path Path) (Path, bool) {
if node.Kind == document.Mapping { if node.Kind == document.Mapping {
for _, entry := range node.Mapping { for _, entry := range node.Mapping {
child := path.Key(entry.Key) child := path.Key(entry.Key)
if entry.Key == "_configkit" { if entry.Key == directiveKey {
return child, true return child, true
} }
if found, ok := overrideDirectivePath(entry.Value, child); ok { if found, ok := overrideDirectivePath(entry.Value, child); ok {

View File

@@ -30,7 +30,7 @@ type overrideLifecycleConfig struct {
func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *testing.T) { func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports: [{path: base.yml}]\nversion: v1\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: base.yml}]\nversion: v1\n")
writeImportFile(t, directory, "base.yml", `settings: writeImportFile(t, directory, "base.yml", `settings:
target: base target: base
count: 4 count: 4
@@ -187,8 +187,8 @@ func TestOverrideValidationRejectsPathsOverlapValuesAndProtectedTargets(t *testi
{name: "nonfinite", overrides: []Override{{Path: Keys("scalar"), Value: math.Inf(1), Flag: "--scalar"}}, code: CodeOverrideInvalidValue}, {name: "nonfinite", overrides: []Override{{Path: Keys("scalar"), Value: math.Inf(1), Flag: "--scalar"}}, code: CodeOverrideInvalidValue},
{name: "depth", overrides: []Override{{Path: Keys("nested"), Value: deep, Flag: "--nested"}}, code: CodeOverrideLimitExceeded}, {name: "depth", overrides: []Override{{Path: Keys("nested"), Value: deep, Flag: "--nested"}}, code: CodeOverrideLimitExceeded},
{name: "record bytes", overrides: []Override{{Path: Keys("scalar"), Value: strings.Repeat("x", maxSourceBytes+1), Flag: "--scalar"}}, code: CodeOverrideLimitExceeded}, {name: "record bytes", overrides: []Override{{Path: Keys("scalar"), Value: strings.Repeat("x", maxSourceBytes+1), Flag: "--scalar"}}, code: CodeOverrideLimitExceeded},
{name: "directive path", overrides: []Override{{Path: Keys("_configkit", "x"), Value: true, Flag: "--directive"}}, code: CodeOverrideDirective}, {name: "directive path", overrides: []Override{{Path: Keys("configkit", "x"), Value: true, Flag: "--directive"}}, code: CodeOverrideDirective},
{name: "directive value", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{"_configkit": map[string]any{}}, Flag: "--nested"}}, code: CodeOverrideDirective}, {name: "directive value", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{"configkit": map[string]any{}}, Flag: "--nested"}}, code: CodeOverrideDirective},
{name: "version", overrides: []Override{{Path: Keys("version"), Value: "v2", Flag: "--version"}}, code: CodeOverrideVersion}, {name: "version", overrides: []Override{{Path: Keys("version"), Value: "v2", Flag: "--version"}}, code: CodeOverrideVersion},
{name: "version ancestor", overrides: []Override{{Path: Keys("version"), Value: map[string]any{"child": "v2"}, Flag: "--version"}}, code: CodeOverrideVersion}, {name: "version ancestor", overrides: []Override{{Path: Keys("version"), Value: map[string]any{"child": "v2"}, Flag: "--version"}}, code: CodeOverrideVersion},
{name: "label", overrides: []Override{{Path: Keys("scalar"), Value: "x", Flag: "\nCANARY"}}, code: CodeOverrideInvalidLabel}, {name: "label", overrides: []Override{{Path: Keys("scalar"), Value: "x", Flag: "\nCANARY"}}, code: CodeOverrideInvalidLabel},

View File

@@ -23,7 +23,7 @@ type profileTestConfig struct {
func TestLoadAppliesSelectedProfileAfterBaseAndBeforeDefaults(t *testing.T) { func TestLoadAppliesSelectedProfileAfterBaseAndBeforeDefaults(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `_configkit: mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports: [{path: base.yml}] imports: [{path: base.yml}]
settings: settings:
kept: main kept: main
@@ -133,8 +133,8 @@ func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) {
profile string profile string
code Code code Code
}{ }{
{name: "root directive", profile: "_configkit: {}\n", code: CodeProfileDirective}, {name: "root directive", profile: "configkit: {}\n", code: CodeProfileDirective},
{name: "nested directive", profile: "settings: {_configkit: {}}\n", code: CodeProfileDirective}, {name: "nested directive", profile: "settings: {configkit: {}}\n", code: CodeProfileDirective},
{name: "equal version", profile: "schema: {version: v1}\n", code: CodeProfileVersion}, {name: "equal version", profile: "schema: {version: v1}\n", code: CodeProfileVersion},
{name: "ancestor scalar", profile: "schema: null\n", code: CodeProfileVersion}, {name: "ancestor scalar", profile: "schema: null\n", code: CodeProfileVersion},
} { } {
@@ -158,7 +158,7 @@ func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) {
t.Run("profile aliases imported base", func(t *testing.T) { t.Run("profile aliases imported base", func(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: profiles/testing.yml}]}\n") mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: profiles/testing.yml}]}\n")
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: value}\n") writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: value}\n")
requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Imports: true, Profile: "testing"}, CodeProfileRepeated) requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Imports: true, Profile: "testing"}, CodeProfileRepeated)
}) })

View File

@@ -27,7 +27,7 @@ type layeredConfig struct {
func TestLayeredProvenanceAgreesWithResolvedSnapshot(t *testing.T) { func TestLayeredProvenanceAgreesWithResolvedSnapshot(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `_configkit: mainPath := writeImportFile(t, directory, "main.yml", `configkit:
imports: [{path: imported.yml}] imports: [{path: imported.yml}]
shared: {equal: same} shared: {equal: same}
values: [1, 2] values: [1, 2]

View File

@@ -43,7 +43,7 @@ func TestNonregularSourcesDoNotBlock(t *testing.T) {
root := t.TempDir() root := t.TempDir()
input := "value: 1\n" input := "value: 1\n"
if mode == "import" { if mode == "import" {
input = "_configkit:\n imports:\n - path: pipe.yml\n" input = "configkit:\n imports:\n - path: pipe.yml\n"
} }
if err := os.WriteFile(filepath.Join(root, "config.yml"), []byte(input), 0600); err != nil { if err := os.WriteFile(filepath.Join(root, "config.yml"), []byte(input), 0600); err != nil {
t.Fatal(err) t.Fatal(err)