From 6eb40c549228c446cf567695c7b0fcfd6e334b96 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 12 Sep 2026 00:28:35 +0000 Subject: [PATCH] Cover integer version decoding boundaries --- docs/roadmap/implementation.md | 10 +- load_test.go | 217 +++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 1 deletion(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 0f2fc0d..d5f69ef 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -58,7 +58,7 @@ the scalar, destination-type, and semantic-path boundary matrix. ## Stage 2 — Close scalar and path-boundary gaps -Status: not started. Depends on: Stage 1. +Status: complete (2026-09-12). Depends on: Stage 1. Scope: focused tests in `load_test.go`, `decode_test.go`, and relevant path/decoder helpers only where those tests expose defects. Avoid broad schema refactoring. @@ -82,6 +82,14 @@ loads. Expand a stage-1 test instead of duplicating the same behavior. Validation: `go test . -run 'Version|Decode|Path'`, then the everyday checker. +Completion record: boundary coverage now includes exact zero, negative, negative +zero, unbounded textual integer, rejected scalar/container kinds, parsed numeric +tokens in maps and sequences, escaped keys, named and nested-pointer strings, and +ordinary numeric, dynamic, codec, duration, and secret destinations. Existing +parser-policy tests continue to own non-JSON number rejection. The focused command +and `./scripts/check.sh` pass with Go 1.26.7 on Linux. Stage 3 should verify retained +history, rendering, composition ownership, detachment, and redaction invariants. + ## Stage 3 — Verify provenance and pipeline invariants Status: not started. Depends on: Stage 2. diff --git a/load_test.go b/load_test.go index c455c26..bdd5c75 100644 --- a/load_test.go +++ b/load_test.go @@ -8,6 +8,7 @@ import ( "strings" "sync/atomic" "testing" + "time" ) func TestLoadSingleSourceKindsAndDefaultsOnly(t *testing.T) { @@ -216,6 +217,222 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) { } } +func TestLoadVersionScalarAndPathBoundaries(t *testing.T) { + t.Parallel() + + type config struct { + Version string `yaml:"version"` + } + for _, test := range []struct { + name string + value string + want string + }{ + {name: "zero", value: "0", want: "0"}, + {name: "negative", value: "-7", want: "-7"}, + {name: "negative zero", value: "-0", want: "-0"}, + {name: "beyond int64", value: "123456789012345678901234567890", want: "123456789012345678901234567890"}, + {name: "empty string", value: `""`, want: ""}, + } { + t.Run(test.name, func(t *testing.T) { + var checked Version + rule := &VersionRule{Path: Keys("version"), Check: func(version Version, _ *Validation) error { + checked = version + return nil + }} + result, err := Load(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}}) + if err != nil { + t.Fatal(err) + } + if result.Config.Version != test.want || checked.Text != test.want || !checked.Present || checked.Null { + t.Fatalf("configuration = %#v, checked = %#v", result.Config, checked) + } + }) + } + + for _, test := range []struct { + name string + value string + }{ + {name: "fraction", value: "4.0"}, + {name: "exponent", value: "4e0"}, + {name: "boolean", value: "true"}, + {name: "mapping", value: "{}"}, + {name: "sequence", value: "[]"}, + } { + t.Run(test.name, func(t *testing.T) { + called := false + rule := &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { + called = true + return nil + }} + result, err := Load(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}}) + if result != nil || called { + t.Fatalf("result = %#v, checker called = %v", result, called) + } + requireDiagnostic(t, err, CodeVersionType, "/version") + }) + } + + type namedVersion string + type nested struct { + Versions map[string]**namedVersion `yaml:"versions"` + Sibling namedVersion `yaml:"sibling"` + } + parsed, err := ParsePath("/versions/0") + if err != nil { + t.Fatal(err) + } + rule := &VersionRule{Path: parsed, Check: func(version Version, _ *Validation) error { + if version.Text != "4" { + t.Errorf("version = %#v", version) + } + return nil + }} + result, err := Load(Bytes("nested.yml", []byte("versions:\n \"0\": 4\nsibling: quoted\n")), Options[nested]{Definition: Definition[nested]{Version: rule}}) + if err != nil { + t.Fatal(err) + } + if result.Config.Versions["0"] == nil || *result.Config.Versions["0"] == nil || **result.Config.Versions["0"] != "4" || result.Config.Sibling != "quoted" { + t.Fatalf("nested configuration = %#v", result.Config) + } + type sequenceConfig struct { + Versions []namedVersion `yaml:"versions"` + } + sequenceResult, err := Load(Bytes("sequence.yml", []byte("versions: [6]\n")), Options[sequenceConfig]{Definition: Definition[sequenceConfig]{Version: &VersionRule{Path: parsed, Check: func(Version, *Validation) error { return nil }}}}) + if err != nil || len(sequenceResult.Config.Versions) != 1 || sequenceResult.Config.Versions[0] != "6" { + t.Fatalf("sequence configuration = %#v, %v", sequenceResult, err) + } + + type escaped struct { + Versions map[string]namedVersion `yaml:"versions"` + } + escapedPath, err := ParsePath("/versions/a~1b") + if err != nil { + t.Fatal(err) + } + escapedResult, err := Load(Bytes("escaped.yml", []byte("versions:\n a/b: 9\n")), Options[escaped]{Definition: Definition[escaped]{Version: &VersionRule{Path: escapedPath, Check: func(Version, *Validation) error { return nil }}}}) + if err != nil || escapedResult.Config.Versions["a/b"] != "9" { + t.Fatalf("escaped configuration = %#v, %v", escapedResult, err) + } + + siblingRule := &VersionRule{Path: Keys("versions", "target"), Check: func(Version, *Validation) error { return nil }} + if result, err := Load(Bytes("sibling.yml", []byte("versions:\n target: 4\n other: 5\n")), Options[escaped]{Definition: Definition[escaped]{Version: siblingRule}}); result != nil { + t.Fatalf("same-type sibling result = %#v, %v", result, err) + } else { + requireDiagnostic(t, err, CodeDecodeType, "/versions/other") + } +} + +func TestLoadVersionDestinationTypesKeepOrdinaryDecoding(t *testing.T) { + t.Parallel() + + check := func(Version, *Validation) error { return nil } + t.Run("integer destination", func(t *testing.T) { + type config struct { + Version int8 `yaml:"version"` + } + result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}}) + if err != nil || result.Config.Version != 4 { + t.Fatalf("result = %#v, %v", result, err) + } + result, err = Load(Bytes("version.yml", []byte("version: 999\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}}) + if result != nil { + t.Fatalf("range result = %#v", result) + } + requireDiagnostic(t, err, CodeDecodeRange, "/version") + result, err = Load(Bytes("version.yml", []byte("version: \"4\"\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}}) + if result != nil { + t.Fatalf("quoted integer result = %#v", result) + } + requireDiagnostic(t, err, CodeDecodeType, "/version") + }) + + t.Run("dynamic destination", func(t *testing.T) { + type config struct { + Version any `yaml:"version"` + } + result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}}) + if err != nil || result.Config.Version != int64(4) { + t.Fatalf("result = %#v, %v", result, err) + } + result, err = Load(Bytes("version.yml", []byte("version: 123456789012345678901234567890\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}}) + if result != nil { + t.Fatalf("range result = %#v", result) + } + requireDiagnostic(t, err, CodeDecodeRange, "/version") + }) + + for _, test := range []struct { + name string + load func() error + }{ + {name: "text codec", load: func() error { + type config struct { + Version validatedText `yaml:"version"` + } + _, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}}) + return err + }}, + {name: "duration", load: func() error { + type config struct { + Version time.Duration `yaml:"version"` + } + _, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}}) + return err + }}, + {name: "secret", load: func() error { + type config struct { + Version Secret `yaml:"version"` + } + _, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}}) + return err + }}, + } { + t.Run(test.name, func(t *testing.T) { + requireDiagnostic(t, test.load(), CodeDecodeType, "/version") + }) + } +} + +func TestLoadVersionPolicyStopsLaterHooks(t *testing.T) { + t.Parallel() + + type config struct { + Version string `yaml:"version"` + } + var defaultsCalled, resolutionCalled, validationCalled bool + rule := &VersionRule{Path: Keys("version"), Check: func(version Version, validation *Validation) error { + if version.Text != "4" { + t.Errorf("version = %#v", version) + } + validation.Error(Keys("version"), "unsupported configuration version") + return nil + }} + result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{ + Defaults: func() config { defaultsCalled = true; return config{} }, + Version: rule, + Resolve: func(*config, *Resolution) error { resolutionCalled = true; return nil }, + Validate: func(*config, *Validation) { validationCalled = true }, + }}) + if result != nil || defaultsCalled || resolutionCalled || validationCalled { + t.Fatalf("result = %#v, hooks = %v/%v/%v", result, defaultsCalled, resolutionCalled, validationCalled) + } + requireDiagnostic(t, err, CodeVersionError, "/version") +} + +func requireDiagnostic(t *testing.T, err error, code Code, path string) { + t.Helper() + var loadError *LoadError + if !errors.As(err, &loadError) { + t.Fatalf("error = %T %v", err, err) + } + diagnostics := loadError.Diagnostics() + if len(diagnostics) != 1 || diagnostics[0].Code != code || diagnostics[0].Path.String() != path || diagnostics[0].Primary == nil { + t.Fatalf("diagnostics = %#v", diagnostics) + } +} + func TestLoadVersionReportSuppressesReturnedErrorTextAndDefaults(t *testing.T) { t.Parallel()