feat: add YAML support for persona files #58
@@ -9,7 +9,7 @@ AI-powered code review bot for Gitea pull requests. Fetches diff + context, send
|
||||
- **Smart budget**: Automatically trims context to fit model token limits
|
||||
- **Idempotent reviews**: Posts new review, then cleans up stale ones (one review per bot)
|
||||
- **Custom prompts**: Load additional instructions from a file (e.g. security-focused review)
|
||||
- **Zero dependencies**: Go stdlib only
|
||||
- **Minimal dependencies**: Go stdlib + `gopkg.in/yaml.v3` only
|
||||
|
||||
## Quick Start: Composite Action
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ JSON is awkward for persona files that contain multi-line text (identity, severi
|
||||
- Backwards compatibility: existing JSON personas must continue to work
|
||||
- Security: protect against DoS via deeply nested YAML (AIKIDO-2024-10486)
|
||||
|
|
||||
- Consistency: use `.yaml` extension (not `.yml`)
|
||||
|
gpt-review-bot
commented
[NIT] The design constraint says to use '.yaml' (not '.yml') for consistency, but the implementation and README support both. Clarify the recommendation vs. support to avoid confusion. **[NIT]** The design constraint says to use '.yaml' (not '.yml') for consistency, but the implementation and README support both. Clarify the recommendation vs. support to avoid confusion.
|
||||
- Library: use `github.com/goccy/go-yaml` v1.16.0+ (actively maintained, security fix applied)
|
||||
- Library: use `gopkg.in/yaml.v3` (approved in CONVENTIONS.md) with explicit depth limiting
|
||||
|
gpt-review-bot
commented
[NIT] The design doc checklist references adding a "go-yaml" dependency at v1.16.0+, which doesn't align with the actual **[NIT]** The design doc checklist references adding a "go-yaml" dependency at v1.16.0+, which doesn't align with the actual `gopkg.in/yaml.v3 v3.0.1` dependency used. Consider updating the doc to avoid confusion.
|
||||
|
||||
## Proposed Approach
|
||||
|
sonnet-review-bot
commented
[MINOR] The design document says to use **[MINOR]** The design document says to use `github.com/goccy/go-yaml` but the implementation uses `gopkg.in/yaml.v3`. The design document is now inaccurate/misleading. If the decision was made to use `gopkg.in/yaml.v3` instead, the design document should be updated to reflect this, and the security claim about depth protection should be addressed explicitly.
|
||||
|
||||
@@ -36,8 +36,8 @@ func parsePersona(data []byte, source string) (*Persona, error) {
|
||||
```go
|
||||
func parseYAML(data []byte, source string) (*Persona, error) {
|
||||
var p Persona
|
||||
// go-yaml has built-in protection against deeply nested structures
|
||||
// but we add explicit decoder options for defense in depth
|
||||
// gopkg.in/yaml.v3 does NOT have built-in depth limiting.
|
||||
// Use explicit depth check via yaml.Node API.
|
||||
if err := yaml.Unmarshal(data, &p); err != nil {
|
||||
return nil, fmt.Errorf("parse persona %s: %w", source, err)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func parseYAML(data []byte, source string) (*Persona, error) {
|
||||
}
|
||||
```
|
||||
|
||||
The `goccy/go-yaml` library since v1.16.0 limits nesting depth by default.
|
||||
The `gopkg.in/yaml.v3` library does not have built-in depth protection, so we implement explicit depth checking by first decoding into a `yaml.Node`, walking the tree to verify depth, then decoding into the target struct.
|
||||
|
||||
## State/Data Model
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package review
|
||||
|
gpt-review-bot
commented
[NIT] LoadPersona wraps os.Stat errors with message 'read persona file ...', which can be slightly misleading; consider using a 'stat persona file' prefix for the stat error path to improve specificity. **[NIT]** LoadPersona wraps os.Stat errors with message 'read persona file ...', which can be slightly misleading; consider using a 'stat persona file' prefix for the stat error path to improve specificity.
gpt-review-bot
commented
[NIT] unmarshalYAMLWithDepthLimit accepts multi-document YAML but silently ignores subsequent documents. Consider rejecting multi-document inputs to avoid ambiguity or add a comment in README clarifying this behavior. **[NIT]** unmarshalYAMLWithDepthLimit accepts multi-document YAML but silently ignores subsequent documents. Consider rejecting multi-document inputs to avoid ambiguity or add a comment in README clarifying this behavior.
gpt-review-bot
commented
[NIT] LoadBuiltinPersona falls back to JSON for embedded personas, but only *.yaml files are embedded now; the JSON fallback is effectively dead code. Optional: either embed both formats or remove the fallback to simplify. **[NIT]** LoadBuiltinPersona falls back to JSON for embedded personas, but only *.yaml files are embedded now; the JSON fallback is effectively dead code. Optional: either embed both formats or remove the fallback to simplify.
gpt-review-bot
commented
[NIT] ListBuiltinPersonas filters by case-sensitive suffixes; while embedded filenames are controlled and lowercase now, using a case-insensitive check would be slightly more robust if naming conventions change. **[NIT]** ListBuiltinPersonas filters by case-sensitive suffixes; while embedded filenames are controlled and lowercase now, using a case-insensitive check would be slightly more robust if naming conventions change.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
gpt-review-bot
commented
[MAJOR] Introduces external dependency github.com/goccy/go-yaml, which violates the repository convention of using only the Go standard library ("Zero dependencies"). This is a policy-breaking change. **[MAJOR]** Introduces external dependency github.com/goccy/go-yaml, which violates the repository convention of using only the Go standard library ("Zero dependencies"). This is a policy-breaking change.
|
||||
|
||||
|
sonnet-review-bot
commented
[MAJOR] The design document ( **[MAJOR]** The design document (`docs/DESIGN-57-yaml-persona.md`) explicitly specifies `github.com/goccy/go-yaml` v1.16.0+ as the required library, citing its built-in protection against deeply nested structures (AIKIDO-2024-10486 / DoS vulnerability fix). The implementation instead uses `gopkg.in/yaml.v3`, which does NOT have the same built-in depth limiting. The comment in `parsePersona` says "go-yaml v1.16.0+ has built-in protection against deeply nested structures" — this is factually incorrect for `gopkg.in/yaml.v3`. This is a security gap: a maliciously crafted deeply-nested YAML file could cause a stack overflow. Either switch to `github.com/goccy/go-yaml` (and add it to the allowlist), or add explicit depth limiting when using `gopkg.in/yaml.v3`.
|
||||
@@ -14,6 +16,14 @@ import (
|
||||
//go:embed personas/*.yaml
|
||||
|
gpt-review-bot
commented
[NIT] Only YAML files are embedded (//go:embed personas/*.yaml) while the design doc mentions embedding both formats; consider updating the comment/design doc or embedding both if JSON files may ever return. **[NIT]** Only YAML files are embedded (//go:embed personas/*.yaml) while the design doc mentions embedding both formats; consider updating the comment/design doc or embedding both if JSON files may ever return.
|
||||
var embeddedPersonas embed.FS
|
||||
|
||||
// MaxPersonaFileSize is the maximum size for persona files (64 KB).
|
||||
// This prevents denial-of-service via excessively large files.
|
||||
const MaxPersonaFileSize = 64 * 1024
|
||||
|
||||
// MaxYAMLDepth is the maximum nesting depth allowed in YAML persona files.
|
||||
// This prevents stack exhaustion from deeply nested structures.
|
||||
const MaxYAMLDepth = 20
|
||||
|
||||
// Persona defines a specialized review role with focused expertise.
|
||||
type Persona struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
@@ -36,7 +46,15 @@ type Severity struct {
|
||||
|
||||
// LoadPersona loads a persona from a JSON or YAML file path.
|
||||
// Format is detected by file extension: .yaml/.yml for YAML, .json or other for JSON.
|
||||
// Files larger than MaxPersonaFileSize are rejected.
|
||||
func LoadPersona(path string) (*Persona, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read persona file %s: %w", path, err)
|
||||
}
|
||||
if info.Size() > MaxPersonaFileSize {
|
||||
|
sonnet-review-bot
commented
[NIT] The fallback to JSON in **[NIT]** The fallback to JSON in `LoadBuiltinPersona` has a comment 'Fall back to JSON for backwards compatibility', but the `embeddedPersonas` embed directive now only includes `*.yaml` files (`//go:embed personas/*.yaml`). The JSON files have been deleted. This means the JSON fallback path in `LoadBuiltinPersona` is dead code — it can never succeed because no `.json` files are embedded. The fallback should either be removed or the embed directive should include `*.json` if JSON built-in personas need to be supported.
|
||||
return nil, fmt.Errorf("persona file %s exceeds maximum size (%d bytes)", path, MaxPersonaFileSize)
|
||||
}
|
||||
|
gpt-review-bot
commented
[MINOR] Built-in persona loader falls back to JSON if YAML not found, but the embedded FS only includes **[MINOR]** Built-in persona loader falls back to JSON if YAML not found, but the embedded FS only includes `*.yaml`. Since the JSON files were removed and not embedded, this fallback will never succeed for built-ins and can be simplified or the embed pattern expanded if JSON fallback is intended.
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
gpt-review-bot
commented
[MINOR] LoadPersona rejects non-regular files (e.g., symlinks) by checking Mode().IsRegular(). This may be overly strict for CI or repo setups that use symlinks. Consider allowing symlinks by resolving them (os.Stat vs. Lstat) or relaxing the check. **[MINOR]** LoadPersona rejects non-regular files (e.g., symlinks) by checking Mode().IsRegular(). This may be overly strict for CI or repo setups that use symlinks. Consider allowing symlinks by resolving them (os.Stat vs. Lstat) or relaxing the check.
|
||||
return nil, fmt.Errorf("read persona file %s: %w", path, err)
|
||||
|
sonnet-review-bot
commented
[NIT] The comment on **[NIT]** The comment on `LoadBuiltinPersona` says 'Try YAML first (preferred format)' but then falls back to JSON. Since the embed directive is now `//go:embed personas/*.yaml` only (JSON files were deleted), the JSON fallback in `LoadBuiltinPersona` is dead code for built-in personas. It's harmless and provides forwards-compatibility for the unlikely case someone adds a `.json` builtin later, but could be simplified.
|
||||
@@ -65,7 +83,7 @@ func LoadBuiltinPersona(name string) (*Persona, error) {
|
||||
return parsePersona(data, "builtin:"+jsonFile)
|
||||
}
|
||||
|
||||
// ListBuiltinPersonas returns the names of all built-in personas.
|
||||
// ListBuiltinPersonas returns the names of all built-in personas in sorted order.
|
||||
// Returns an empty slice if the embedded directory cannot be read.
|
||||
func ListBuiltinPersonas() []string {
|
||||
|
gpt-review-bot
commented
[NIT] The comment in parsePersona mentions 'go-yaml v1.16.0+ has built-in protection against deeply nested structures' which refers to a different library (goccy/go-yaml). Update the comment to reflect gopkg.in/yaml.v3 or remove the claim. **[NIT]** The comment in parsePersona mentions 'go-yaml v1.16.0+ has built-in protection against deeply nested structures' which refers to a different library (goccy/go-yaml). Update the comment to reflect gopkg.in/yaml.v3 or remove the claim.
|
||||
entries, err := embeddedPersonas.ReadDir("personas")
|
||||
@@ -94,10 +112,11 @@ func ListBuiltinPersonas() []string {
|
||||
seen[personaName] = true
|
||||
|
gpt-review-bot
commented
[MINOR] Relying solely on library defaults for YAML depth/alias protections. Consider using an explicit decoder with defensive options (if supported by the library) to enforce a depth/alias limit for defense in depth, aligning with the design doc intent. **[MINOR]** Relying solely on library defaults for YAML depth/alias protections. Consider using an explicit decoder with defensive options (if supported by the library) to enforce a depth/alias limit for defense in depth, aligning with the design doc intent.
gpt-review-bot
commented
[NIT] ListBuiltinPersonas handles .yml and .json extensions even though only *.yaml files are embedded. This is harmless but could be simplified or clarified in comments to reflect the embed pattern. **[NIT]** ListBuiltinPersonas handles .yml and .json extensions even though only *.yaml files are embedded. This is harmless but could be simplified or clarified in comments to reflect the embed pattern.
|
||||
}
|
||||
}
|
||||
|
[MINOR] YAML parsing uses yaml.Unmarshal without explicit decoder limits. Although goccy/go-yaml >= v1.16.0 adds default depth protections, setting explicit limits (e.g., maximum nesting depth/collection sizes if supported) would add defense-in-depth against resource exhaustion in pathological inputs. **[MINOR]** YAML parsing uses yaml.Unmarshal without explicit decoder limits. Although goccy/go-yaml >= v1.16.0 adds default depth protections, setting explicit limits (e.g., maximum nesting depth/collection sizes if supported) would add defense-in-depth against resource exhaustion in pathological inputs.
[MAJOR] Unbounded YAML deserialization with yaml.Unmarshal on potentially untrusted persona files can allow resource exhaustion (e.g., deeply nested structures or alias expansion bombs). There is no explicit limit on nesting, aliases, or input size, contrary to the comment suggesting built-in protection. **[MAJOR]** Unbounded YAML deserialization with yaml.Unmarshal on potentially untrusted persona files can allow resource exhaustion (e.g., deeply nested structures or alias expansion bombs). There is no explicit limit on nesting, aliases, or input size, contrary to the comment suggesting built-in protection.
|
||||
var names []string
|
||||
names := make([]string, 0, len(seen))
|
||||
for name := range seen {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
sonnet-review-bot
commented
[NIT] The comment **[NIT]** The comment `// go-yaml v1.16.0+ has built-in protection against deeply nested structures` is misleading/incorrect for `gopkg.in/yaml.v3`. It appears to have been written for `goccy/go-yaml` and copied over without updating. This should either be removed or corrected to accurately describe `gopkg.in/yaml.v3`'s actual behavior.
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -110,8 +129,7 @@ func parsePersona(data []byte, source string) (*Persona, error) {
|
||||
var p Persona
|
||||
var err error
|
||||
if isYAML {
|
||||
// go-yaml v1.16.0+ has built-in protection against deeply nested structures
|
||||
err = yaml.Unmarshal(data, &p)
|
||||
err = unmarshalYAMLWithDepthLimit(data, &p, MaxYAMLDepth)
|
||||
} else {
|
||||
err = json.Unmarshal(data, &p)
|
||||
}
|
||||
@@ -124,6 +142,35 @@ func parsePersona(data []byte, source string) (*Persona, error) {
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// unmarshalYAMLWithDepthLimit unmarshals YAML data with explicit depth limiting.
|
||||
// This protects against stack exhaustion from deeply nested structures.
|
||||
|
gpt-review-bot
commented
[MINOR] YAML decoder does not enable KnownFields/strict mode, so unknown keys in persona YAML are silently ignored. Enabling strict field checking would help catch typos (e.g., dec.KnownFields(true)) before validation. **[MINOR]** YAML decoder does not enable KnownFields/strict mode, so unknown keys in persona YAML are silently ignored. Enabling strict field checking would help catch typos (e.g., dec.KnownFields(true)) before validation.
gpt-review-bot
commented
[MINOR] JSON parsing uses json.Unmarshal which accepts unknown fields, while YAML is parsed with KnownFields(true). For parity and to catch typos in JSON persona files, consider switching to a json.Decoder with DisallowUnknownFields(). **[MINOR]** JSON parsing uses json.Unmarshal which accepts unknown fields, while YAML is parsed with KnownFields(true). For parity and to catch typos in JSON persona files, consider switching to a json.Decoder with DisallowUnknownFields().
|
||||
func unmarshalYAMLWithDepthLimit(data []byte, out interface{}, maxDepth int) error {
|
||||
var node yaml.Node
|
||||
|
gpt-review-bot
commented
[MINOR] JSON parsing does not enforce single-document input. After dec.Decode(&p), any trailing JSON values would be silently ignored. Consider verifying EOF by attempting a second decode and expecting io.EOF to ensure there's no extra data. **[MINOR]** JSON parsing does not enforce single-document input. After dec.Decode(&p), any trailing JSON values would be silently ignored. Consider verifying EOF by attempting a second decode and expecting io.EOF to ensure there's no extra data.
|
||||
dec := yaml.NewDecoder(bytes.NewReader(data))
|
||||
if err := dec.Decode(&node); err != nil {
|
||||
return err
|
||||
|
gpt-review-bot
commented
[MINOR] YAML depth check traverses node.Content but ignores Alias nodes (node.Alias). Malicious YAML could leverage anchors/aliases to create effective deep structures without increasing Content depth. Consider handling Alias nodes explicitly. **[MINOR]** YAML depth check traverses node.Content but ignores Alias nodes (node.Alias). Malicious YAML could leverage anchors/aliases to create effective deep structures without increasing Content depth. Consider handling Alias nodes explicitly.
|
||||
}
|
||||
|
||||
|
sonnet-review-bot
commented
[MINOR] The **[MINOR]** The `unmarshalYAMLWithDepthLimit` function uses `interface{}` as the parameter type for `out` instead of `any`. The project targets the latest stable Go release, and `any` is the idiomatic alias since Go 1.18. This is inconsistent with the rest of the codebase convention.
sonnet-review-bot
commented
[MINOR] The two-pass decode approach (first into yaml.Node for depth check, then strict decode from raw bytes) is necessary because KnownFields doesn't work on yaml.Node.Decode(), but this means the input bytes are parsed twice. The comment explains this correctly. A minor concern: if **[MINOR]** The two-pass decode approach (first into yaml.Node for depth check, then strict decode from raw bytes) is necessary because KnownFields doesn't work on yaml.Node.Decode(), but this means the input bytes are parsed twice. The comment explains this correctly. A minor concern: if `gopkg.in/yaml.v3` ever adds KnownFields support to node.Decode(), this could be simplified — but for now this is the correct workaround and is well-documented.
|
||||
if err := checkYAMLDepth(&node, 0, maxDepth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return node.Decode(out)
|
||||
}
|
||||
|
[MAJOR] The recursive YAML depth checker does not detect alias/anchor cycles. If a YAML document contains cyclic aliases (e.g., an alias ultimately pointing back to an anchored node that references the alias), checkYAMLDepth can recurse indefinitely, leading to stack exhaustion or a hang (DoS). Add cycle detection (visited set of *yaml.Node) or a total node visitation cap to prevent infinite recursion. **[MAJOR]** The recursive YAML depth checker does not detect alias/anchor cycles. If a YAML document contains cyclic aliases (e.g., an alias ultimately pointing back to an anchored node that references the alias), checkYAMLDepth can recurse indefinitely, leading to stack exhaustion or a hang (DoS). Add cycle detection (visited set of *yaml.Node) or a total node visitation cap to prevent infinite recursion.
|
||||
|
||||
|
[MINOR] JSON parsing uses json.Unmarshal, which silently ignores unknown fields. For consistency with YAML's KnownFields(true) and to harden against typos or unintended keys, consider switching to json.Decoder with DisallowUnknownFields() to reject unknown JSON fields. **[MINOR]** JSON parsing uses json.Unmarshal, which silently ignores unknown fields. For consistency with YAML's KnownFields(true) and to harden against typos or unintended keys, consider switching to json.Decoder with DisallowUnknownFields() to reject unknown JSON fields.
|
||||
// checkYAMLDepth recursively checks that YAML nodes don't exceed the depth limit.
|
||||
|
sonnet-review-bot
commented
[MINOR] The decoder created with **[MINOR]** The decoder created with `yaml.NewDecoder` is used to decode a single document but does not check for a second call to `Decode` returning `io.EOF` to confirm there's only one document. For untrusted user-supplied YAML files, silently ignoring additional documents is acceptable (they won't affect the parsed struct), but it's worth noting that multi-document YAML files will have their additional documents silently ignored rather than surfacing an error. This is a minor UX issue, not a correctness bug.
sonnet-review-bot
commented
[MINOR] The **[MINOR]** The `unmarshalYAMLWithDepthLimit` function silently ignores multi-document YAML files (only parses the first document). While the comment acknowledges this, it could be a footgun: a user who accidentally writes `---` in their persona file will get no error, just silent truncation. Consider calling `dec.Decode` a second time and returning an error if a second document is found, to give users a clearer signal.
|
||||
func checkYAMLDepth(node *yaml.Node, depth, maxDepth int) error {
|
||||
if depth > maxDepth {
|
||||
return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth)
|
||||
|
[MINOR] When encountering an alias node, the depth check calls checkYAMLDepth on the alias target without incrementing depth, potentially undercounting nesting by one compared to non-alias children. While limited in practical bypass (one level), this slightly weakens the intended MaxYAMLDepth enforcement. Consider passing depth+1 when following an alias used as a value. **[MINOR]** When encountering an alias node, the depth check calls checkYAMLDepth on the alias target without incrementing depth, potentially undercounting nesting by one compared to non-alias children. While limited in practical bypass (one level), this slightly weakens the intended MaxYAMLDepth enforcement. Consider passing depth+1 when following an alias used as a value.
[MINOR] Depth enforcement occurs after decoding the full YAML into a yaml.Node. While the 64KB file-size cap mitigates risk, an attacker could still supply deeply nested but small YAML to stress the decoder before the depth check. Consider additional safeguards (e.g., limiting total nodes visited or using a visited set, or further reducing allowed size/depth) for defense in depth. **[MINOR]** Depth enforcement occurs after decoding the full YAML into a yaml.Node. While the 64KB file-size cap mitigates risk, an attacker could still supply deeply nested but small YAML to stress the decoder before the depth check. Consider additional safeguards (e.g., limiting total nodes visited or using a visited set, or further reducing allowed size/depth) for defense in depth.
|
||||
}
|
||||
|
gpt-review-bot
commented
[MINOR] checkYAMLDepth follows alias pointers without guarding against potential alias cycles; consider tracking visited nodes or limiting alias dereference to avoid possible infinite recursion on pathological inputs. **[MINOR]** checkYAMLDepth follows alias pointers without guarding against potential alias cycles; consider tracking visited nodes or limiting alias dereference to avoid possible infinite recursion on pathological inputs.
|
||||
for _, child := range node.Content {
|
||||
if err := checkYAMLDepth(child, depth+1, maxDepth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
sonnet-review-bot
commented
[MINOR] The multi-document rejection check ( **[MINOR]** The multi-document rejection check (`dec.Decode(&extra)`) re-uses the same decoder after the first `dec.Decode(&node)` call. If the first document exhausted the decoder but there's trailing non-document content (e.g., a trailing `...` end-of-document marker), the behavior depends on go-yaml internals. This is likely fine in practice, but the comment could clarify that this relies on go-yaml's decoder advancing past the first document.
|
||||
}
|
||||
|
||||
func validatePersona(p *Persona, source string) error {
|
||||
if p.Name == "" {
|
||||
return fmt.Errorf("persona %s: name is required", source)
|
||||
|
sonnet-review-bot
commented
[MINOR] The alias-following logic in **[MINOR]** The alias-following logic in `checkYAMLDepth` does not count the alias node's own depth level before recursing into the alias target. If an alias node appears near the depth limit, the target's content is checked starting from the same `depth` value as the alias itself (not `depth+1`). This means a deeply aliased structure could potentially bypass the limit by 1 level. While the impact is minimal given the depth=20 limit, incrementing depth before recursing into the alias target would be more consistent.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package review
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -449,3 +450,65 @@ severity:
|
||||
t.Errorf("Focus[0] = %q, want %q", p.Focus[0], "item")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAMLDeeplyNestedRejection(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "deeply-nested.yaml")
|
||||
|
||||
// Build a deeply nested YAML structure that exceeds MaxYAMLDepth (20).
|
||||
// Each level adds 2 to the depth count (key + value mapping).
|
||||
var sb strings.Builder
|
||||
sb.WriteString("name: test\nidentity: test\nnested:\n")
|
||||
indent := " "
|
||||
for i := 0; i < 25; i++ {
|
||||
sb.WriteString(strings.Repeat(indent, i+1))
|
||||
sb.WriteString(fmt.Sprintf("level%d:\n", i))
|
||||
}
|
||||
sb.WriteString(strings.Repeat(indent, 26))
|
||||
sb.WriteString("value: too-deep\n")
|
||||
|
||||
if err := os.WriteFile(path, []byte(sb.String()), 0644); err != nil {
|
||||
t.Fatalf("failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
_, err := LoadPersona(path)
|
||||
if err == nil {
|
||||
t.Error("expected error for deeply nested YAML, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "nesting depth exceeds") {
|
||||
t.Errorf("error = %q, want containing 'nesting depth exceeds'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAMLFileSizeLimit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "huge.yaml")
|
||||
|
||||
// Create a file larger than MaxPersonaFileSize (64 KB)
|
||||
content := "name: test\nidentity: " + strings.Repeat("x", MaxPersonaFileSize+1) + "\n"
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
_, err := LoadPersona(path)
|
||||
if err == nil {
|
||||
t.Error("expected error for oversized file, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "exceeds maximum size") {
|
||||
t.Errorf("error = %q, want containing 'exceeds maximum size'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListBuiltinPersonasSortedOrder(t *testing.T) {
|
||||
names := ListBuiltinPersonas()
|
||||
if len(names) < 2 {
|
||||
t.Skip("need at least 2 personas to test ordering")
|
||||
}
|
||||
|
||||
// Verify the list is sorted
|
||||
for i := 1; i < len(names); i++ {
|
||||
if names[i-1] > names[i] {
|
||||
t.Errorf("ListBuiltinPersonas not sorted: %q > %q", names[i-1], names[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MINOR] The design doc says 'Consistency: use
.yamlextension (not.yml)' as a constraint, but the actual implementation supports both.yamland.yml(and the open questions section documents this decision). The constraint section of the design doc is now inconsistent with the implemented behavior and should be updated to reflect the actual decision.