Add ConfigKit application definition and attribution
This commit is contained in:
@@ -24,6 +24,20 @@ The [configuration reference](../config.md) owns Scriptorium's source-location
|
|||||||
fields and precedence. Maintained files under `examples/` are application
|
fields and precedence. Maintained files under `examples/` are application
|
||||||
inputs that use Promptkit's tagged formats.
|
inputs that use Promptkit's tagged formats.
|
||||||
|
|
||||||
|
## Native Application Definition In Preparation
|
||||||
|
|
||||||
|
`internal/config` also contains a ConfigKit-native `Document` definition. Its
|
||||||
|
fresh defaults come from Scriptorium's existing default constants; its resolver
|
||||||
|
records only actual trimming, path cleaning, and render-format normalization.
|
||||||
|
The validator reports application retry, size-limit, and format errors without
|
||||||
|
mutating the document or exposing rejected values. Application path values keep
|
||||||
|
working-directory-relative meaning. ConfigKit owns strict YAML decoding and
|
||||||
|
presence semantics, including dynamic integer values in backend parameters.
|
||||||
|
|
||||||
|
The executable commands still use the existing loader. The new definition is
|
||||||
|
internal preparation and is not yet an alternative user-selectable configuration
|
||||||
|
mode or a change to the current [configuration contract](../config.md).
|
||||||
|
|
||||||
## Restricted HTTP Artifact Reader
|
## Restricted HTTP Artifact Reader
|
||||||
|
|
||||||
`internal/adapter/http` implements `promptkit.ArtifactReader` for HTTP
|
`internal/adapter/http` implements `promptkit.ArtifactReader` for HTTP
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ License and notices: [LGPL v3](licenses/LGPL-3.0.txt) |
|
|||||||
|
|
||||||
Source: [Promptkit v0.11.0](https://gitea.maximumdirect.net/eric/promptkit/src/commit/db05b56166ca2d8650dfaf660e1cf0eed3932e1d)
|
Source: [Promptkit v0.11.0](https://gitea.maximumdirect.net/eric/promptkit/src/commit/db05b56166ca2d8650dfaf660e1cf0eed3932e1d)
|
||||||
|
|
||||||
|
## ConfigKit
|
||||||
|
|
||||||
|
Upstream copyright statement: no project-specific statement is present in the
|
||||||
|
selected module; the FSF copyright in its license text belongs to that license
|
||||||
|
document and is not substituted for a project notice.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
|
||||||
|
License and notices: [LGPL v3](licenses/LGPL-3.0.txt) |
|
||||||
|
[GPL v3 companion](licenses/GPL-3.0.txt) |
|
||||||
|
[upstream LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/e6ced0d3d1194b58e0b3dc8970089471ba175258/LICENSE)
|
||||||
|
|
||||||
|
Source: [ConfigKit v0.4.0](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/e6ced0d3d1194b58e0b3dc8970089471ba175258)
|
||||||
|
|
||||||
## Promptkit OpenRouter Catalog
|
## Promptkit OpenRouter Catalog
|
||||||
|
|
||||||
Upstream copyright statement: no project-specific statement is present in the
|
Upstream copyright statement: no project-specific statement is present in the
|
||||||
@@ -91,3 +105,23 @@ License and notices: [MIT](licenses/MIT.txt) |
|
|||||||
[upstream NOTICE](https://github.com/go-yaml/yaml/blob/f6f7691b1fdeb513f56608cd2c32c51f8194bf51/NOTICE)
|
[upstream NOTICE](https://github.com/go-yaml/yaml/blob/f6f7691b1fdeb513f56608cd2c32c51f8194bf51/NOTICE)
|
||||||
|
|
||||||
Source: [gopkg.in/yaml.v3 v3.0.1](https://github.com/go-yaml/yaml/tree/f6f7691b1fdeb513f56608cd2c32c51f8194bf51)
|
Source: [gopkg.in/yaml.v3 v3.0.1](https://github.com/go-yaml/yaml/tree/f6f7691b1fdeb513f56608cd2c32c51f8194bf51)
|
||||||
|
|
||||||
|
## go.yaml.in/yaml/v4
|
||||||
|
|
||||||
|
Copyright 2006-2010 Kirill Simonov
|
||||||
|
|
||||||
|
Copyright 2011-2019 Canonical Ltd
|
||||||
|
|
||||||
|
Copyright 2025 The go-yaml Project Contributors
|
||||||
|
|
||||||
|
SPDX-License-Identifier: MIT AND Apache-2.0
|
||||||
|
|
||||||
|
The upstream NOTICE assigns MIT to files ported from libyaml and Apache 2.0
|
||||||
|
to the remaining files; these are file-specific terms, not alternatives.
|
||||||
|
|
||||||
|
License and notices: [MIT](licenses/MIT.txt) |
|
||||||
|
[Apache 2.0](licenses/Apache-2.0.txt) |
|
||||||
|
[upstream LICENSE](https://github.com/yaml/go-yaml/blob/1c17b9cee72bf81ae73a8be50fbd7dd3a9985125/LICENSE) |
|
||||||
|
[upstream NOTICE](https://github.com/yaml/go-yaml/blob/1c17b9cee72bf81ae73a8be50fbd7dd3a9985125/NOTICE)
|
||||||
|
|
||||||
|
Source: [go.yaml.in/yaml/v4 v4.0.0-rc.6](https://github.com/yaml/go-yaml/tree/1c17b9cee72bf81ae73a8be50fbd7dd3a9985125)
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ command-specific validation retains those responsibilities where applicable.
|
|||||||
|
|
||||||
## Stage 1: Add The Dependency And Native Application Definition
|
## Stage 1: Add The Dependency And Native Application Definition
|
||||||
|
|
||||||
**Status: Not started.**
|
**Status: Complete.** The pinned dependency, native definition, focused
|
||||||
|
presence/normalization/validation tests, and commit-specific attribution are in
|
||||||
|
place. The existing executable loader remains active pending the command cutover.
|
||||||
|
|
||||||
Primary files: `go.mod`, `go.sum`, new focused files under `internal/config`,
|
Primary files: `go.mod`, `go.sum`, new focused files under `internal/config`,
|
||||||
and `docs/oss/attribution.md`.
|
and `docs/oss/attribution.md`.
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -3,6 +3,7 @@ module gitea.maximumdirect.net/eric/scriptorium
|
|||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
gitea.maximumdirect.net/eric/ConfigKit v0.4.0
|
||||||
gitea.maximumdirect.net/eric/promptkit v0.11.0
|
gitea.maximumdirect.net/eric/promptkit v0.11.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
@@ -11,5 +12,6 @@ require (
|
|||||||
gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2 v2.20260922.1 // indirect
|
gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2 v2.20260922.1 // indirect
|
||||||
gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/v2 v2.20260922.0 // indirect
|
gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/v2 v2.20260922.0 // indirect
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
gitea.maximumdirect.net/eric/ConfigKit v0.4.0 h1:fo+vKff3zTGeWqJIA38NtT84b6OUP//9ZmBH3lFuKgk=
|
||||||
|
gitea.maximumdirect.net/eric/ConfigKit v0.4.0/go.mod h1:tYWpsrbLyu05r8+1MapWp5fN6ZrX9I8hU2xEPedYmYk=
|
||||||
gitea.maximumdirect.net/eric/promptkit v0.11.0 h1:x7JguOUiInPVXK0GEitjyoRFqqvcuSTjYUvqJ+whtik=
|
gitea.maximumdirect.net/eric/promptkit v0.11.0 h1:x7JguOUiInPVXK0GEitjyoRFqqvcuSTjYUvqJ+whtik=
|
||||||
gitea.maximumdirect.net/eric/promptkit v0.11.0/go.mod h1:0zEBVlJJC+ldDdXazAgybYBXNSe74vwFx5dyOuSos/w=
|
gitea.maximumdirect.net/eric/promptkit v0.11.0/go.mod h1:0zEBVlJJC+ldDdXazAgybYBXNSe74vwFx5dyOuSos/w=
|
||||||
gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2 v2.20260922.1 h1:S2/Q/JDE+m8Ca3pDhAjVbfYrdlEhs9blj0STgNopHo0=
|
gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2 v2.20260922.1 h1:S2/Q/JDE+m8Ca3pDhAjVbfYrdlEhs9blj0STgNopHo0=
|
||||||
@@ -8,6 +10,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK
|
|||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
|||||||
109
internal/config/document.go
Normal file
109
internal/config/document.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/scriptorium/internal/defaults"
|
||||||
|
renderformat "gitea.maximumdirect.net/eric/scriptorium/internal/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Document is Scriptorium's ConfigKit-native application configuration shape.
|
||||||
|
type Document struct {
|
||||||
|
PromptDir string `yaml:"prompt_dir"`
|
||||||
|
ProfileDir string `yaml:"profile_dir"`
|
||||||
|
SchemaDir string `yaml:"schema_dir"`
|
||||||
|
TransientRetries *int `yaml:"transient_retries"`
|
||||||
|
Server ServerDocument `yaml:"server"`
|
||||||
|
Defaults DefaultsDocument `yaml:"defaults"`
|
||||||
|
Backends map[string]BackendDocument `yaml:"backends"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackendDocument struct {
|
||||||
|
Endpoint string `yaml:"endpoint"`
|
||||||
|
APIKeyEnv string `yaml:"api_key_env"`
|
||||||
|
ExtraParams map[string]any `yaml:"extra_params"`
|
||||||
|
ConcurrencyLimit int `yaml:"concurrency_limit"`
|
||||||
|
QueueCapacity *int `yaml:"queue_capacity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerDocument struct {
|
||||||
|
Addr string `yaml:"addr"`
|
||||||
|
ArtifactRoot string `yaml:"artifact_root"`
|
||||||
|
MaxRequestBytes int64 `yaml:"max_request_bytes"`
|
||||||
|
MaxArtifactBytes int64 `yaml:"max_artifact_bytes"`
|
||||||
|
MaxResponseBytes int64 `yaml:"max_response_bytes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultsDocument struct {
|
||||||
|
RenderFormat string `yaml:"render_format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentDefinition() configkit.Definition[Document] {
|
||||||
|
return configkit.Definition[Document]{
|
||||||
|
Defaults: documentDefaults,
|
||||||
|
Resolve: resolveDocument,
|
||||||
|
Validate: validateDocument,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentDefaults() Document {
|
||||||
|
return Document{
|
||||||
|
SchemaDir: defaults.SchemaDirDefault,
|
||||||
|
Server: ServerDocument{
|
||||||
|
Addr: defaults.HTTPAddrDefault,
|
||||||
|
MaxRequestBytes: defaults.HTTPMaxRequestBytesDefault,
|
||||||
|
MaxArtifactBytes: defaults.HTTPMaxArtifactBytesDefault,
|
||||||
|
MaxResponseBytes: defaults.HTTPMaxResponseBytesDefault,
|
||||||
|
},
|
||||||
|
Defaults: DefaultsDocument{RenderFormat: string(renderformat.DefaultPreparedRunOutputFormat)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDocument(document *Document, resolution *configkit.Resolution) error {
|
||||||
|
normalizePath := func(path configkit.Path, value *string) {
|
||||||
|
cleaned := strings.TrimSpace(*value)
|
||||||
|
if cleaned != "" {
|
||||||
|
cleaned = filepath.Clean(cleaned)
|
||||||
|
}
|
||||||
|
if cleaned != *value {
|
||||||
|
configkit.Normalize(resolution, path, value, cleaned, "normalized application path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalizeString := func(path configkit.Path, value *string, normalized, reason string) {
|
||||||
|
if normalized != *value {
|
||||||
|
configkit.Normalize(resolution, path, value, normalized, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizePath(configkit.Keys("prompt_dir"), &document.PromptDir)
|
||||||
|
normalizePath(configkit.Keys("profile_dir"), &document.ProfileDir)
|
||||||
|
normalizePath(configkit.Keys("schema_dir"), &document.SchemaDir)
|
||||||
|
normalizePath(configkit.Keys("server", "artifact_root"), &document.Server.ArtifactRoot)
|
||||||
|
normalizeString(configkit.Keys("server", "addr"), &document.Server.Addr,
|
||||||
|
strings.TrimSpace(document.Server.Addr), "trimmed server address")
|
||||||
|
normalizeString(configkit.Keys("defaults", "render_format"), &document.Defaults.RenderFormat,
|
||||||
|
strings.ToLower(strings.TrimSpace(document.Defaults.RenderFormat)), "normalized render format")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDocument(document *Document, validation *configkit.Validation) {
|
||||||
|
if document.TransientRetries != nil && *document.TransientRetries < 0 {
|
||||||
|
validation.Error(configkit.Keys("transient_retries"), "must be greater than or equal to zero")
|
||||||
|
}
|
||||||
|
if document.Server.MaxRequestBytes < 0 {
|
||||||
|
validation.Error(configkit.Keys("server", "max_request_bytes"), "must be greater than or equal to zero")
|
||||||
|
}
|
||||||
|
if document.Server.MaxArtifactBytes < 0 {
|
||||||
|
validation.Error(configkit.Keys("server", "max_artifact_bytes"), "must be greater than or equal to zero")
|
||||||
|
}
|
||||||
|
if document.Server.MaxResponseBytes < 0 {
|
||||||
|
validation.Error(configkit.Keys("server", "max_response_bytes"), "must be greater than or equal to zero")
|
||||||
|
}
|
||||||
|
switch document.Defaults.RenderFormat {
|
||||||
|
case string(renderformat.OutputFormatText), string(renderformat.OutputFormatJSON):
|
||||||
|
default:
|
||||||
|
validation.Error(configkit.Keys("defaults", "render_format"), "must be text or json")
|
||||||
|
}
|
||||||
|
}
|
||||||
163
internal/config/document_test.go
Normal file
163
internal/config/document_test.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadDocumentBytes(content string) (*configkit.Result[Document], error) {
|
||||||
|
return configkit.Load(configkit.Bytes("application.yml", []byte(content)), configkit.Options[Document]{
|
||||||
|
Definition: documentDefinition(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentDefaultsAndExplicitValues(t *testing.T) {
|
||||||
|
defaultsOnly, err := configkit.Load(configkit.NoSource(), configkit.Options[Document]{Definition: documentDefinition()})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("defaults-only load: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(defaultsOnly.Config, documentDefaults()) || len(defaultsOnly.Sources()) != 0 {
|
||||||
|
t.Fatalf("unexpected defaults-only result: %+v sources=%+v", defaultsOnly.Config, defaultsOnly.Sources())
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := loadDocumentBytes(`prompt_dir: ""
|
||||||
|
profile_dir: ""
|
||||||
|
schema_dir: ""
|
||||||
|
transient_retries: 0
|
||||||
|
server:
|
||||||
|
addr: ""
|
||||||
|
artifact_root: ""
|
||||||
|
max_request_bytes: 0
|
||||||
|
max_artifact_bytes: 0
|
||||||
|
max_response_bytes: 0
|
||||||
|
defaults:
|
||||||
|
render_format: " JSON "
|
||||||
|
backends:
|
||||||
|
local:
|
||||||
|
endpoint: "http://fixture.invalid/v1"
|
||||||
|
queue_capacity: 0
|
||||||
|
extra_params:
|
||||||
|
sample_count: 23
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("explicit-value load: %v", err)
|
||||||
|
}
|
||||||
|
config := result.Config
|
||||||
|
if config.SchemaDir != "" || config.Server.Addr != "" || config.Server.MaxRequestBytes != 0 || config.Server.MaxArtifactBytes != 0 || config.Server.MaxResponseBytes != 0 {
|
||||||
|
t.Fatalf("explicit empty and zero values did not replace defaults: %+v", config)
|
||||||
|
}
|
||||||
|
if config.TransientRetries == nil || *config.TransientRetries != 0 {
|
||||||
|
t.Fatalf("explicit zero retry budget lost: %+v", config.TransientRetries)
|
||||||
|
}
|
||||||
|
if backend := config.Backends["local"]; backend.QueueCapacity == nil || *backend.QueueCapacity != 0 || backend.ExtraParams["sample_count"] != int64(23) {
|
||||||
|
t.Fatalf("backend presence or dynamic integer changed: %+v", backend)
|
||||||
|
}
|
||||||
|
if config.Defaults.RenderFormat != "json" {
|
||||||
|
t.Fatalf("render format normalization failed: %q", config.Defaults.RenderFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
nullable, err := loadDocumentBytes("transient_retries: null\nbackends:\n local:\n queue_capacity: null\n")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("nullable-value load: %v", err)
|
||||||
|
}
|
||||||
|
if nullable.Config.TransientRetries != nil || nullable.Config.Backends["local"].QueueCapacity != nil {
|
||||||
|
t.Fatalf("nullable fields did not remain nil: %+v", nullable.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
nullBackends, err := loadDocumentBytes("backends: null\n")
|
||||||
|
if err != nil || nullBackends.Config.Backends != nil {
|
||||||
|
t.Fatalf("nullable backend map did not remain nil: %+v error=%v", nullBackends, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentRecordsEffectiveNormalization(t *testing.T) {
|
||||||
|
result, err := loadDocumentBytes(`prompt_dir: " ./one/../two "
|
||||||
|
profile_dir: " "
|
||||||
|
server:
|
||||||
|
artifact_root: " ./data/../artifacts "
|
||||||
|
addr: " :9000 "
|
||||||
|
defaults:
|
||||||
|
render_format: " JSON "
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load normalized document: %v", err)
|
||||||
|
}
|
||||||
|
if result.Config.PromptDir != "two" || result.Config.ProfileDir != "" || result.Config.Server.ArtifactRoot != "artifacts" || result.Config.Server.Addr != ":9000" {
|
||||||
|
t.Fatalf("unexpected normalized values: %+v", result.Config)
|
||||||
|
}
|
||||||
|
var effective bytes.Buffer
|
||||||
|
if err := result.WriteEffective(&effective); err != nil {
|
||||||
|
t.Fatalf("write effective configuration: %v", err)
|
||||||
|
}
|
||||||
|
for _, fragment := range []string{"prompt_dir: two", "profile_dir: ''", "artifact_root: artifacts", "addr: :9000", "render_format: json"} {
|
||||||
|
if !strings.Contains(effective.String(), fragment) {
|
||||||
|
t.Fatalf("effective output omitted %q: %s", fragment, effective.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
explanation, known := result.Explain(configkit.Keys("prompt_dir"))
|
||||||
|
if !known || explanation.Effective.YAML != "two\n" {
|
||||||
|
t.Fatalf("explanation does not agree with effective value: %+v known=%t", explanation, known)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, change := range explanation.History {
|
||||||
|
if change.Origin.Operation == configkit.ChangeNormalization {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("normalization was not recorded: %+v", explanation.History)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentValidationIsSafeAndPathSpecific(t *testing.T) {
|
||||||
|
for _, tc := range []struct{ name, content, path string }{
|
||||||
|
{"negative retry", "transient_retries: -9173\n", "/transient_retries"},
|
||||||
|
{"negative request limit", "server:\n max_request_bytes: -9173\n", "/server/max_request_bytes"},
|
||||||
|
{"negative artifact limit", "server:\n max_artifact_bytes: -9173\n", "/server/max_artifact_bytes"},
|
||||||
|
{"negative response limit", "server:\n max_response_bytes: -9173\n", "/server/max_response_bytes"},
|
||||||
|
{"empty format", "defaults:\n render_format: \"\"\n", "/defaults/render_format"},
|
||||||
|
{"unknown format", "defaults:\n render_format: private-invalid-value\n", "/defaults/render_format"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, err := loadDocumentBytes(tc.content)
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if !errors.As(err, &loadError) {
|
||||||
|
t.Fatalf("expected typed validation error, got %v", err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, diagnostic := range loadError.Diagnostics() {
|
||||||
|
if diagnostic.Code == configkit.CodeValidateError && diagnostic.Path.String() == tc.path {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
if strings.Contains(diagnostic.Message, "9173") || strings.Contains(diagnostic.Message, "private-invalid-value") {
|
||||||
|
t.Fatalf("diagnostic disclosed rejected value: %+v", diagnostic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("expected validation at %s, got %+v", tc.path, loadError.Diagnostics())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocumentRejectsNativeShapeErrors(t *testing.T) {
|
||||||
|
for _, content := range []string{
|
||||||
|
"server: null\n",
|
||||||
|
"defaults: null\n",
|
||||||
|
"server:\n addr: null\n",
|
||||||
|
"server:\n max_request_bytes: null\n",
|
||||||
|
"defaults:\n render_format: null\n",
|
||||||
|
"server:\n unknown_field: 1\n",
|
||||||
|
"backends:\n local:\n unknown_field: 1\n",
|
||||||
|
} {
|
||||||
|
_, err := loadDocumentBytes(content)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("unexpected acceptance of %q", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user