fix(deps): replace gopkg.in/yaml.v3 with github.com/goccy/go-yaml #89

Merged
aweiker merged 13 commits from review-bot-issue-87 into main 2026-05-13 03:47:02 +00:00
6 changed files with 384 additions and 116 deletions
+3 -1
View File
@@ -9,7 +9,7 @@
Review

[MINOR] The allowlist now permits github.com/goccy/go-yaml, while code imports subpackages github.com/goccy/go-yaml/ast and /parser. If the enforcement script checks exact matches, consider clarifying that subpackages of an approved module are allowed or list them explicitly to avoid future false positives.

**[MINOR]** The allowlist now permits `github.com/goccy/go-yaml`, while code imports subpackages `github.com/goccy/go-yaml/ast` and `/parser`. If the enforcement script checks exact matches, consider clarifying that subpackages of an approved module are allowed or list them explicitly to avoid future false positives.
| Package | Use Case | Scope | | Package | Use Case | Scope |
|---------|----------|-------| |---------|----------|-------|
| `gopkg.in/yaml.v3` | YAML parsing (persona files, config) | production | | `github.com/goccy/go-yaml` | YAML parsing (persona files, config) | production |
Review

[MAJOR] The CONVENTIONS.md process rule states: 'To request a new dependency: 1. Open a PR that ONLY updates this table. Requires explicit approval from Aaron. After merge, a separate PR may use the package.' This PR both updates the allowlist table AND uses the package in production code in the same PR. This violates the documented process — the allowlist change and the usage should be in separate PRs.

**[MAJOR]** The CONVENTIONS.md process rule states: 'To request a new dependency: 1. Open a PR that ONLY updates this table. Requires explicit approval from Aaron. After merge, a separate PR may use the package.' This PR both updates the allowlist table AND uses the package in production code in the same PR. This violates the documented process — the allowlist change and the usage should be in separate PRs.
Review

[MAJOR] The CONVENTIONS.md explicitly states the process for adding a new dependency: 'Open a PR that ONLY updates this table' and 'After merge, a separate PR may use the package.' This PR combines both the allowlist update AND the implementation in a single PR, directly violating the documented process. The dependency github.com/goccy/go-yaml should have been approved in a standalone PR first, then used in a follow-up PR.

**[MAJOR]** The CONVENTIONS.md explicitly states the process for adding a new dependency: 'Open a PR that ONLY updates this table' and 'After merge, a separate PR may use the package.' This PR combines both the allowlist update AND the implementation in a single PR, directly violating the documented process. The dependency `github.com/goccy/go-yaml` should have been approved in a standalone PR first, then used in a follow-up PR.
Review

[NIT] The allowlist specifies the module root "github.com/goccy/go-yaml" while code imports subpackages ("/ast", "/parser"). Ensure the dependency check script treats subpackages under the allowed module as permitted (CI passed, so this is likely already handled).

**[NIT]** The allowlist specifies the module root "github.com/goccy/go-yaml" while code imports subpackages ("/ast", "/parser"). Ensure the dependency check script treats subpackages under the allowed module as permitted (CI passed, so this is likely already handled).
Review

[MAJOR] The conventions explicitly state: 'To request a new dependency: 1. Open a PR that ONLY updates this table. 2. Requires explicit approval from Aaron. 3. After merge, a separate PR may use the package.' This PR both updates CONVENTIONS.md (the allowlist) AND uses the new package in the same PR. This directly violates the documented process. Even if Aaron approved this work, the mechanism defined in CONVENTIONS.md was not followed — the allowlist update and the implementation should be in separate PRs.

**[MAJOR]** The conventions explicitly state: 'To request a new dependency: 1. Open a PR that ONLY updates this table. 2. Requires explicit approval from Aaron. 3. After merge, a separate PR may use the package.' This PR both updates CONVENTIONS.md (the allowlist) AND uses the new package in the same PR. This directly violates the documented process. Even if Aaron approved this work, the mechanism defined in CONVENTIONS.md was not followed — the allowlist update and the implementation should be in separate PRs.
| `github.com/google/go-cmp` | Test comparisons (`cmp.Diff`) | test only | | `github.com/google/go-cmp` | Test comparisons (`cmp.Diff`) | test only |
Review

[NIT] The PR updates CONVENTIONS.md and implements the package in the same commit, which technically violates the documented two-step process. The PR description explains the rationale (pre-approval via issue #57), which is reasonable. This is a process note rather than a code defect.

**[NIT]** The PR updates `CONVENTIONS.md` and implements the package in the same commit, which technically violates the documented two-step process. The PR description explains the rationale (pre-approval via issue #57), which is reasonable. This is a process note rather than a code defect.
Review

[NIT] The PR description acknowledges that updating CONVENTIONS.md and implementation in the same PR deviates from the documented two-step process. The justification (pre-approval in issue #57) is reasonable, but it sets a precedent for bypassing the process. A brief inline comment in CONVENTIONS.md explaining the deviation and referencing issue #91 for the audit trail would make this cleaner for future maintainers.

**[NIT]** The PR description acknowledges that updating CONVENTIONS.md and implementation in the same PR deviates from the documented two-step process. The justification (pre-approval in issue #57) is reasonable, but it sets a precedent for bypassing the process. A brief inline comment in CONVENTIONS.md explaining the deviation and referencing issue #91 for the audit trail would make this cleaner for future maintainers.
Review

Acknowledged. The deviation is already documented in-file via the HTML comment added in this PR:

<!-- Deviation from step 1+3 for go-yaml migration: see #91 for rationale. -->

This provides the inline audit trail the reviewer suggests, referencing the tracking issue. No additional change needed.

Acknowledged. The deviation is already documented in-file via the HTML comment added in this PR: ``` <!-- Deviation from step 1+3 for go-yaml migration: see #91 for rationale. --> ``` This provides the inline audit trail the reviewer suggests, referencing the tracking issue. No additional change needed.
**Any import not in this table or the Go standard library is forbidden.** **Any import not in this table or the Go standard library is forbidden.**
@@ -21,6 +21,8 @@ To request a new dependency:
2. Requires explicit approval from Aaron 2. Requires explicit approval from Aaron
3. After merge, a separate PR may use the package 3. After merge, a separate PR may use the package
<!-- Deviation from step 1+3 for go-yaml migration: see #91 for rationale. -->
Review

[NIT] The deviation comment <!-- Deviation from step 1+3 for go-yaml migration: see #91 for rationale. --> is an HTML comment that won't render in most Markdown viewers, but it does serve as an audit trail. Consider whether a visible note (e.g., a footnote row in the table or a sentence after the table) would be more discoverable for future reviewers. Minor concern only.

**[NIT]** The deviation comment `<!-- Deviation from step 1+3 for go-yaml migration: see #91 for rationale. -->` is an HTML comment that won't render in most Markdown viewers, but it does serve as an audit trail. Consider whether a visible note (e.g., a footnote row in the table or a sentence after the table) would be more discoverable for future reviewers. Minor concern only.
Review

[NIT] The HTML comment <!-- Deviation from step 1+3 for go-yaml migration: see #91 for rationale. --> is reasonable, but as a comment inside Markdown it won't render visibly in most Markdown renderers — it'll be invisible. If this rationale needs to be discoverable for future reviewers, a visible footnote or a link in the table row might be more effective. That said, hiding it is arguably intentional to keep the table clean.

**[NIT]** The HTML comment `<!-- Deviation from step 1+3 for go-yaml migration: see #91 for rationale. -->` is reasonable, but as a comment inside Markdown it won't render visibly in most Markdown renderers — it'll be invisible. If this rationale needs to be discoverable for future reviewers, a visible footnote or a link in the table row might be more effective. That said, hiding it is arguably intentional to keep the table clean.
*Enforcement: `scripts/check-deps.sh` parses this table — update only here.* *Enforcement: `scripts/check-deps.sh` parses this table — update only here.*
## Error Handling ## Error Handling
+10 -31
View File
@@ -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 - Backwards compatibility: existing JSON personas must continue to work
- Security: protect against DoS via deeply nested YAML (AIKIDO-2024-10486) - Security: protect against DoS via deeply nested YAML (AIKIDO-2024-10486)
- Consistency: use `.yaml` extension (not `.yml`) - Consistency: use `.yaml` extension (not `.yml`)
- Library: use `gopkg.in/yaml.v3` (approved in CONVENTIONS.md) with explicit depth limiting - Library: use `github.com/goccy/go-yaml` v1.16.0+ (approved in CONVENTIONS.md); we implement custom AST-based depth/node-count checks for precise alias-aware validation
Outdated
Review

[MINOR] The design doc claims using go-yaml's built-in depth protection via MaxYAMLDepth/MaxYAMLNodes instead of a manual depth walk, but the implementation still performs a custom AST depth/node-count check. Update the doc to reflect the actual approach or adopt the library's built-in options if available.

**[MINOR]** The design doc claims using go-yaml's built-in depth protection via MaxYAMLDepth/MaxYAMLNodes instead of a manual depth walk, but the implementation still performs a custom AST depth/node-count check. Update the doc to reflect the actual approach or adopt the library's built-in options if available.
Review

[NIT] The design document still contains the pseudocode showing the old gopkg.in/yaml.v3 api (yaml.Node, yaml.NewDecoder, etc.) with a note saying it's outdated. Consider either removing the old pseudocode entirely or replacing it with the actual implementation approach, since having misleading pseudocode in design docs can confuse future contributors.

**[NIT]** The design document still contains the pseudocode showing the old gopkg.in/yaml.v3 api (yaml.Node, yaml.NewDecoder, etc.) with a note saying it's outdated. Consider either removing the old pseudocode entirely or replacing it with the actual implementation approach, since having misleading pseudocode in design docs can confuse future contributors.
## Proposed Approach ## Proposed Approach
Review

[MINOR] Documentation states the library’s built-in depth protection (MaxYAMLDepth/MaxYAMLNodes decoder options) is used instead of manual checks, but the code still performs an AST-based depth/node count walk before decoding. Update the design doc to reflect the current implementation or use the library-provided options if intended.

**[MINOR]** Documentation states the library’s built-in depth protection (MaxYAMLDepth/MaxYAMLNodes decoder options) is used instead of manual checks, but the code still performs an AST-based depth/node count walk before decoding. Update the design doc to reflect the current implementation or use the library-provided options if intended.
@@ -33,37 +33,16 @@ func parsePersona(data []byte, source string) (*Persona, error) {
Review

[MINOR] The design doc references the updated approach correctly but removes the prior code example and now relies on narrative. Consider adding a brief code snippet or explicit mention that MergeKey (<<) merges are traversed to avoid confusion about edge cases.

**[MINOR]** The design doc references the updated approach correctly but removes the prior code example and now relies on narrative. Consider adding a brief code snippet or explicit mention that MergeKey (<<) merges are traversed to avoid confusion about edge cases.
### YAML Parsing with Depth Protection ### YAML Parsing with Depth Protection
```go We implement a custom AST-based depth/node-count walk (`checkYAMLDepth` in
func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error { `review/persona.go`) rather than relying on library decoder options. Key design
var node yaml.Node decisions:
dec := yaml.NewDecoder(bytes.NewReader(data))
if err := dec.Decode(&node); err != nil {
return err
}
if err := checkYAMLDepth(&node, 0, maxDepth); err != nil {
return err
}
return node.Decode(out)
}
func checkYAMLDepth(node *yaml.Node, depth, maxDepth int) error { - **Library:** `github.com/goccy/go-yaml` with `ast.Node`-based traversal
if depth > maxDepth { - **Dual-map tracking:** `validated` (depth-aware short-circuit) + `visiting` (cycle detection)
return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth) - **Node-count limit:** Conservative overcounting bounds total validation work
} - **Alias-aware depth:** Aliases increment depth and are re-checked when encountered at greater depths
// Handle alias nodes by following the Alias pointer
if node.Kind == yaml.AliasNode && node.Alias != nil {
return checkYAMLDepth(node.Alias, depth, maxDepth)
}
for _, child := range node.Content {
if err := checkYAMLDepth(child, depth+1, maxDepth); err != nil {
return err
}
}
return nil
}
```
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 (including alias resolution), then decoding into the target struct. See `review/persona.go:checkYAMLDepth` for the authoritative implementation.
## State/Data Model ## State/Data Model
@@ -74,7 +53,7 @@ No new state. Same `Persona` struct, just different parsing.
| Error | Handling | | Error | Handling |
|-------|----------| |-------|----------|
| Invalid YAML syntax | Return parse error with source file | | Invalid YAML syntax | Return parse error with source file |
| Deeply nested YAML | Library rejects (v1.16.0+ fix) | | Deeply nested YAML | Custom AST walk (`checkYAMLDepth`) rejects before decode |
| Unknown extension | Fall back to JSON parsing | | Unknown extension | Fall back to JSON parsing |
| Missing required fields | Validation rejects after parse | | Missing required fields | Validation rejects after parse |
1
+1 -1
View File
@@ -2,4 +2,4 @@ module gitea.weiker.me/rodin/review-bot
go 1.26.2 go 1.26.2
Review

[MAJOR] The repository CONVENTIONS.md defines a strict allowlist of approved third-party packages. The only approved YAML package is gopkg.in/yaml.v3. The package github.com/goccy/go-yaml is NOT on the allowlist. Per the documented process: 'To request a new dependency: 1. Open a PR that ONLY updates this table, 2. Requires explicit approval from Aaron, 3. After merge, a separate PR may use the package.' This PR skips that process entirely. Even if the intent is correct (issue #57 may have specified goccy/go-yaml), the dependency gate must be updated first in a separate PR before this change can be merged.

**[MAJOR]** The repository CONVENTIONS.md defines a strict allowlist of approved third-party packages. The only approved YAML package is `gopkg.in/yaml.v3`. The package `github.com/goccy/go-yaml` is NOT on the allowlist. Per the documented process: 'To request a new dependency: 1. Open a PR that ONLY updates this table, 2. Requires explicit approval from Aaron, 3. After merge, a separate PR may use the package.' This PR skips that process entirely. Even if the intent is correct (issue #57 may have specified goccy/go-yaml), the dependency gate must be updated first in a separate PR before this change can be merged.
Review

[MAJOR] Introduces unapproved third-party dependency github.com/goccy/go-yaml, violating the repository's strict dependency allowlist (only gopkg.in/yaml.v3 and github.com/google/go-cmp are permitted). This bypasses established supply chain security controls.

**[MAJOR]** Introduces unapproved third-party dependency github.com/goccy/go-yaml, violating the repository's strict dependency allowlist (only gopkg.in/yaml.v3 and github.com/google/go-cmp are permitted). This bypasses established supply chain security controls.
Review

[MAJOR] The repository conventions define a strict allowlist of approved third-party packages. github.com/goccy/go-yaml is NOT on the allowlist. The only approved YAML package is gopkg.in/yaml.v3. Per conventions: 'To request a new dependency: 1. Open a PR that ONLY updates this table. 2. Requires explicit approval from Aaron. 3. After merge, a separate PR may use the package.' This PR skips that process entirely.

**[MAJOR]** The repository conventions define a strict allowlist of approved third-party packages. `github.com/goccy/go-yaml` is NOT on the allowlist. The only approved YAML package is `gopkg.in/yaml.v3`. Per conventions: 'To request a new dependency: 1. Open a PR that ONLY updates this table. 2. Requires explicit approval from Aaron. 3. After merge, a separate PR may use the package.' This PR skips that process entirely.
Review

[MAJOR] Introduces github.com/goccy/go-yaml, which is not in the repository's approved third-party dependency allowlist. Per the repository conventions, adding a new dependency requires a separate PR that ONLY updates the allowlist table and receives explicit approval before usage.

**[MAJOR]** Introduces github.com/goccy/go-yaml, which is not in the repository's approved third-party dependency allowlist. Per the repository conventions, adding a new dependency requires a separate PR that ONLY updates the allowlist table and receives explicit approval before usage.
require gopkg.in/yaml.v3 v3.0.1 require github.com/goccy/go-yaml v1.19.2
Review

[MAJOR] Introduces a non-allowlisted dependency github.com/goccy/go-yaml, which violates the repository’s STRICT ALLOWLIST policy. Update the approved dependencies table before using this package.

**[MAJOR]** Introduces a non-allowlisted dependency github.com/goccy/go-yaml, which violates the repository’s STRICT ALLOWLIST policy. Update the approved dependencies table before using this package.
Review

[MAJOR] New dependency github.com/goccy/go-yaml is introduced but not present in the repository's strict allowlist of approved packages. This violates the dependency policy and presents a supply chain risk.

**[MAJOR]** New dependency github.com/goccy/go-yaml is introduced but not present in the repository's strict allowlist of approved packages. This violates the dependency policy and presents a supply chain risk.
+2 -4
View File
@@ -1,4 +1,2 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+146 -38
View File
@@ -5,12 +5,15 @@ import (
"embed" "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"sort" "sort"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
Outdated
Review

[MAJOR] Production code imports github.com/goccy/go-yaml (and subpackages ast, parser), which is not listed in the approved third-party packages. This must be added to the allowlist per repository conventions.

**[MAJOR]** Production code imports github.com/goccy/go-yaml (and subpackages ast, parser), which is not listed in the approved third-party packages. This must be added to the allowlist per repository conventions.
Outdated
Review

[MAJOR] Imports github.com/goccy/go-yaml, github.com/goccy/go-yaml/ast, and github.com/goccy/go-yaml/parser — none of which are on the approved dependency allowlist. This is a direct violation of the conventions' strict allowlist rule.

**[MAJOR]** Imports `github.com/goccy/go-yaml`, `github.com/goccy/go-yaml/ast`, and `github.com/goccy/go-yaml/parser` — none of which are on the approved dependency allowlist. This is a direct violation of the conventions' strict allowlist rule.
"gopkg.in/yaml.v3" "github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/ast"
"github.com/goccy/go-yaml/parser"
) )
//go:embed personas/*.yaml //go:embed personas/*.yaml
@@ -118,9 +121,7 @@ func ListBuiltinPersonas() []string {
default: default:
continue continue
} }
if !seen[personaName] { seen[personaName] = true
seen[personaName] = true
}
} }
names := make([]string, 0, len(seen)) names := make([]string, 0, len(seen))
for name := range seen { for name := range seen {
@@ -142,10 +143,19 @@ func parsePersona(data []byte, source string) (*Persona, error) {
err = unmarshalYAMLWithDepthLimit(data, &p, MaxYAMLDepth) err = unmarshalYAMLWithDepthLimit(data, &p, MaxYAMLDepth)
} else { } else {
// Use json.Decoder with DisallowUnknownFields for consistency with // Use json.Decoder with DisallowUnknownFields for consistency with
// YAML's KnownFields(true) - both reject unknown fields to catch typos. // YAML's Strict() - both reject unknown fields to catch typos.
dec := json.NewDecoder(bytes.NewReader(data)) dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields() dec.DisallowUnknownFields()
Review

[MINOR] JSON decoding disallows unknown fields but does not verify EOF to catch trailing data after a valid JSON object. Consider an additional decode into a dummy value to ensure no extra tokens remain.

**[MINOR]** JSON decoding disallows unknown fields but does not verify EOF to catch trailing data after a valid JSON object. Consider an additional decode into a dummy value to ensure no extra tokens remain.
err = dec.Decode(&p) err = dec.Decode(&p)
if err == nil {
Review

[MINOR] The JSON trailing content check uses dec.More(), which is only meaningful inside arrays/objects and may be confusing at top level. Relying solely on attempting a second Decode and expecting io.EOF is sufficient and simpler.

**[MINOR]** The JSON trailing content check uses dec.More(), which is only meaningful inside arrays/objects and may be confusing at top level. Relying solely on attempting a second Decode and expecting io.EOF is sufficient and simpler.
// Reject trailing content after the first valid JSON object.
// Without this check, input like `{"name":"x"}garbage` would
// silently succeed because Decoder stops after one object.
var dummy json.RawMessage
Review

[MINOR] The trailing-content check for JSON has a subtle logic issue. dec.More() returns true if there are more values in the current JSON array/object — it's not designed to detect trailing content after a top-level value. The intended check should be dec.Decode(&dummy) != io.EOF alone (which returns nil if there's another value, or io.EOF if the stream is done). The dec.More() call before the Decode is redundant and slightly misleading: if More() is true, the subsequent Decode will also succeed (returning nil, not io.EOF), so the condition triggers correctly either way. However, the dec.More() shortcut could confuse readers since More() is documented for use inside arrays/objects. Consider simplifying to just if err2 := dec.Decode(&dummy); err2 != io.EOF { err = fmt.Errorf(...) }.

**[MINOR]** The trailing-content check for JSON has a subtle logic issue. `dec.More()` returns true if there are more values in the current JSON array/object — it's not designed to detect trailing content after a top-level value. The intended check should be `dec.Decode(&dummy) != io.EOF` alone (which returns nil if there's another value, or io.EOF if the stream is done). The `dec.More()` call before the Decode is redundant and slightly misleading: if More() is true, the subsequent Decode will also succeed (returning nil, not io.EOF), so the condition triggers correctly either way. However, the `dec.More()` shortcut could confuse readers since `More()` is documented for use inside arrays/objects. Consider simplifying to just `if err2 := dec.Decode(&dummy); err2 != io.EOF { err = fmt.Errorf(...) }`.
if err2 := dec.Decode(&dummy); err2 != io.EOF {
err = fmt.Errorf("unexpected trailing content after JSON object")
}
}
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("parse persona %s: %w", source, err) return nil, fmt.Errorf("parse persona %s: %w", source, err)
5
@@ -156,70 +166,164 @@ func parsePersona(data []byte, source string) (*Persona, error) {
return &p, nil return &p, nil
Outdated
Review

[MINOR] Depth/node-count enforcement occurs after parser.ParseBytes builds the AST. Extremely deep but small YAML (within 64KB) could still stress the parser via recursive descent before checks run, presenting a potential DoS vector. Consider a pre-parse sanity check (e.g., shallow indentation-depth scan) or using any available parser limits if supported.

**[MINOR]** Depth/node-count enforcement occurs after parser.ParseBytes builds the AST. Extremely deep but small YAML (within 64KB) could still stress the parser via recursive descent before checks run, presenting a potential DoS vector. Consider a pre-parse sanity check (e.g., shallow indentation-depth scan) or using any available parser limits if supported.
} }
Outdated
Review

[MINOR] Empty YAML detection now returns a generic "empty YAML document" error; consider adding a unit test for empty YAML files to ensure this path is covered and the message is stable.

**[MINOR]** Empty YAML detection now returns a generic "empty YAML document" error; consider adding a unit test for empty YAML files to ensure this path is covered and the message is stable.
Outdated
Review

[NIT] If go-yaml provides decoder/parser options to enforce depth/node limits during parsing, consider enabling them in addition to the manual AST check to ensure protection prior to AST traversal. This would be a defense-in-depth improvement.

**[NIT]** If go-yaml provides decoder/parser options to enforce depth/node limits during parsing, consider enabling them in addition to the manual AST check to ensure protection prior to AST traversal. This would be a defense-in-depth improvement.
// unmarshalYAMLWithDepthLimit unmarshals YAML data with explicit depth limiting // unmarshalYAMLWithDepthLimit unmarshals YAML data with three safety checks:
// and strict field checking. This protects against stack exhaustion from deeply // - Depth limiting: rejects AST trees exceeding maxDepth to prevent stack exhaustion.
// nested structures and catches typos in field names. // - Multi-document rejection: prevents silent data loss from ignored extra documents.
// Multi-document YAML files are rejected to prevent silent data loss. // - Strict field checking: rejects unknown YAML keys to catch typos early.
func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error { func unmarshalYAMLWithDepthLimit(data []byte, out any, maxDepth int) error {
// First pass: decode into a yaml.Node to check depth limits and node counts. // First pass: parse into AST to check depth limits, node counts, and
// This prevents stack exhaustion before we attempt to decode into structs. // multi-document rejection. This prevents stack exhaustion before we
var node yaml.Node // attempt to decode into structs.
dec := yaml.NewDecoder(bytes.NewReader(data)) file, err := parser.ParseBytes(data, 0)
if err := dec.Decode(&node); err != nil { if err != nil {
return err return err
} }
// Reject empty YAML input (whitespace-only, comment-only, or truly empty files).
// The parser returns a single doc with nil body for these cases.
if len(file.Docs) == 0 || file.Docs[0].Body == nil {
return fmt.Errorf("empty YAML document")
Outdated
Review

[MINOR] The empty document check (len(file.Docs) == 0) comes AFTER the multi-document check (len(file.Docs) > 1). While not a bug (both conditions are checked), it would be slightly more natural to check for empty first. More importantly: the previous gopkg.in/yaml.v3 implementation using dec.Decode(&node) would return an error on truly empty input, while parser.ParseBytes may return an empty Docs slice for whitespace-only or empty input. The new explicit empty-doc check is an improvement, but the error message 'empty YAML document' differs from what the old library would have returned — worth noting if callers match on error text.

**[MINOR]** The empty document check (`len(file.Docs) == 0`) comes AFTER the multi-document check (`len(file.Docs) > 1`). While not a bug (both conditions are checked), it would be slightly more natural to check for empty first. More importantly: the previous `gopkg.in/yaml.v3` implementation using `dec.Decode(&node)` would return an error on truly empty input, while `parser.ParseBytes` may return an empty `Docs` slice for whitespace-only or empty input. The new explicit empty-doc check is an improvement, but the error message 'empty YAML document' differs from what the old library would have returned — worth noting if callers match on error text.
}
Review

[MINOR] The go 1.26.2 in go.mod is a pre-release/future version (Go 1.26 has not been released). This may cause go mod tidy to behave unexpectedly on stable Go toolchains and suggests the module was initialized with an unstable toolchain. This is pre-existing and not introduced by this PR, but worth flagging.

**[MINOR]** The `go 1.26.2` in go.mod is a pre-release/future version (Go 1.26 has not been released). This may cause `go mod tidy` to behave unexpectedly on stable Go toolchains and suggests the module was initialized with an unstable toolchain. This is pre-existing and not introduced by this PR, but worth flagging.
// Reject multi-document YAML files - silently ignoring additional documents // Reject multi-document YAML files - silently ignoring additional documents
// could lead to confusing behavior where users think their changes take effect. // could lead to confusing behavior where users think their changes take effect.
var extra yaml.Node if len(file.Docs) > 1 {
if dec.Decode(&extra) == nil {
return fmt.Errorf("multi-document YAML is not supported; only single-document files are allowed") return fmt.Errorf("multi-document YAML is not supported; only single-document files are allowed")
} }
security-review-bot marked this conversation as resolved Outdated
Outdated
Review

[MINOR] Decoder is created with yaml.Strict() but does not additionally enforce decode-time depth/node limits. While the AST pre-check limits depth and nodes, consider also aligning with or configuring go-yaml's built-in depth/node limits (if available) during decoding for defense in depth.

**[MINOR]** Decoder is created with yaml.Strict() but does not additionally enforce decode-time depth/node limits. While the AST pre-check limits depth and nodes, consider also aligning with or configuring go-yaml's built-in depth/node limits (if available) during decoding for defense in depth.
nodeCount := 0 nodeCount := 0
if err := checkYAMLDepth(&node, 0, maxDepth, MaxYAMLNodes, make(map[*yaml.Node]struct{}), &nodeCount); err != nil { if err := checkYAMLDepth(file.Docs[0].Body, 0, maxDepth, MaxYAMLNodes, make(map[ast.Node]int), make(map[ast.Node]bool), &nodeCount); err != nil {
Review

[NIT] The comment on unmarshalYAMLWithDepthLimit says 'Multi-document YAML files are rejected to prevent confusing behavior where additional documents are silently ignored.' but the function header also mentions 'strict field checking'. The doc comment could be tightened to mention all three concerns (depth, multi-doc, strict fields) in the opening summary rather than leaving strict-field checking implicit.

**[NIT]** The comment on `unmarshalYAMLWithDepthLimit` says 'Multi-document YAML files are rejected to prevent confusing behavior where additional documents are silently ignored.' but the function header also mentions 'strict field checking'. The doc comment could be tightened to mention all three concerns (depth, multi-doc, strict fields) in the opening summary rather than leaving strict-field checking implicit.
return err return err
Outdated
Review

[NIT] The checkYAMLDepth function is exported via the test file (TestYAMLAliasCycleDetection calls it directly). Since checkYAMLDepth is unexported (lowercase), the test is in the same package (package review), which is fine. No issue here — just noting this is a white-box test of an internal function, which is appropriate given the security-critical nature of the cycle detection.

**[NIT]** The `checkYAMLDepth` function is exported via the test file (`TestYAMLAliasCycleDetection` calls it directly). Since `checkYAMLDepth` is unexported (lowercase), the test is in the same package (`package review`), which is fine. No issue here — just noting this is a white-box test of an internal function, which is appropriate given the security-critical nature of the cycle detection.
Outdated
Review

[MINOR] Depth counting walks both MappingNode (depth+1) and then MappingValueNode (depth+1 for Key and Value), effectively increasing depth by 2 per mapping level. This is stricter than the test comment's 'incrementing depth by 1 per level' and may unnecessarily reject shallower YAML. Consider clarifying the intended depth semantics or adjusting the increment so each structural level accounts for a single depth step.

**[MINOR]** Depth counting walks both MappingNode (depth+1) and then MappingValueNode (depth+1 for Key and Value), effectively increasing depth by 2 per mapping level. This is stricter than the test comment's 'incrementing depth by 1 per level' and may unnecessarily reject shallower YAML. Consider clarifying the intended depth semantics or adjusting the increment so each structural level accounts for a single depth step.
} }
Review

[MINOR] The checkYAMLDepth function increments *nodeCount before the depth-aware short-circuit check (validated map lookup). This means a node revisited at a shallower depth (which immediately returns via the short-circuit) still increments the counter. The comment acknowledges this as 'intentional conservative overcounting', which is a reasonable security posture. However, it also means the node count can be inflated by the number of times aliases reference the same shallow subtree, potentially causing false positives for legitimate YAML with many alias references to the same anchor. This is a trade-off that is documented and defensible, but worth noting.

**[MINOR]** The `checkYAMLDepth` function increments `*nodeCount` before the depth-aware short-circuit check (`validated` map lookup). This means a node revisited at a shallower depth (which immediately returns via the short-circuit) still increments the counter. The comment acknowledges this as 'intentional conservative overcounting', which is a reasonable security posture. However, it also means the node count can be inflated by the number of times aliases reference the same shallow subtree, potentially causing false positives for legitimate YAML with many alias references to the same anchor. This is a trade-off that is documented and defensible, but worth noting.
Review

[MINOR] The unmarshalYAMLWithDepthLimit function does a two-pass approach: first parser.ParseBytes then yaml.NewDecoder(...).Decode(out). This means the raw bytes are parsed twice. The second decode with yaml.Strict() also re-parses from a bytes.NewReader(data). For the file size constraint (64KB max), this is acceptable, but worth noting. A minor concern is that the two passes could theoretically behave differently if the library has any parse-then-decode divergence. However, this is standard practice for AST-pre-validation patterns and is not a bug.

**[MINOR]** The `unmarshalYAMLWithDepthLimit` function does a two-pass approach: first `parser.ParseBytes` then `yaml.NewDecoder(...).Decode(out)`. This means the raw bytes are parsed twice. The second decode with `yaml.Strict()` also re-parses from a `bytes.NewReader(data)`. For the file size constraint (64KB max), this is acceptable, but worth noting. A minor concern is that the two passes could theoretically behave differently if the library has any parse-then-decode divergence. However, this is standard practice for AST-pre-validation patterns and is not a bug.
Outdated
Review

[MINOR] The go 1.26.2 in go.mod references a Go version that does not exist as a stable release (current stable is 1.23/1.24 range). This is likely a typo or pre-release version. While not introduced by this PR, it's worth noting as it could cause toolchain issues.

**[MINOR]** The `go 1.26.2` in go.mod references a Go version that does not exist as a stable release (current stable is 1.23/1.24 range). This is likely a typo or pre-release version. While not introduced by this PR, it's worth noting as it could cause toolchain issues.
// Second pass: decode with strict field checking enabled. // Second pass: decode with strict field checking enabled.
Review

[NIT] The unmarshalYAMLWithDepthLimit function takes maxDepth int as a parameter but always calls checkYAMLDepth with the package-level constant MaxYAMLNodes directly rather than accepting it as a parameter. This asymmetry is mildly inconsistent — either both limits should be constants used directly, or both should be parameters. Low impact since callers always pass MaxYAMLDepth anyway.

**[NIT]** The `unmarshalYAMLWithDepthLimit` function takes `maxDepth int` as a parameter but always calls `checkYAMLDepth` with the package-level constant `MaxYAMLNodes` directly rather than accepting it as a parameter. This asymmetry is mildly inconsistent — either both limits should be constants used directly, or both should be parameters. Low impact since callers always pass `MaxYAMLDepth` anyway.
// KnownFields(true) rejects unknown keys, catching typos like "focuss" or "identiy". // Strict() rejects unknown keys, catching typos like "focuss" or "identiy".
Review

[MINOR] checkYAMLDepth breaks alias cycles by returning nil (skipping the cyclic subtree). Consider returning a specific error on detected cycles to fail-fast instead of relying on downstream decoder behavior, improving safety and transparency for malicious inputs.

**[MINOR]** checkYAMLDepth breaks alias cycles by returning nil (skipping the cyclic subtree). Consider returning a specific error on detected cycles to fail-fast instead of relying on downstream decoder behavior, improving safety and transparency for malicious inputs.
// We must re-decode from the original data because yaml.Node.Decode() doesn't //
// support the KnownFields option. // Safety note: goccy/go-yaml's decoder does not expand YAML aliases
strictDec := yaml.NewDecoder(bytes.NewReader(data)) // recursively — it resolves them via the pre-built AST, which our first
strictDec.KnownFields(true) // pass already depth-checked. Alias chains that would exceed depth limits
return strictDec.Decode(out) // are caught above; the decoder merely reads the resolved scalar values.
Review

[MINOR] The unmarshalYAMLWithDepthLimit function performs a two-pass decode (AST parse then full decode). The second pass with yaml.Strict() will also parse the YAML from scratch, so the file bytes are parsed twice. This is intentional per the comment but worth noting: if the goccy/go-yaml Strict() decoder also does alias resolution internally, the depth protection from the first pass only guards the structural AST walk, not the decoder's internal expansion. This appears acceptable given the design, but should be validated that yaml.Strict() doesn't recurse unboundedly on crafted alias chains during decode.

**[MINOR]** The `unmarshalYAMLWithDepthLimit` function performs a two-pass decode (AST parse then full decode). The second pass with `yaml.Strict()` will also parse the YAML from scratch, so the file bytes are parsed twice. This is intentional per the comment but worth noting: if the goccy/go-yaml `Strict()` decoder also does alias resolution internally, the depth protection from the first pass only guards the structural AST walk, not the decoder's internal expansion. This appears acceptable given the design, but should be validated that `yaml.Strict()` doesn't recurse unboundedly on crafted alias chains during decode.
Review

Already addressed. The comment block immediately above the dec := yaml.NewDecoder(...) call explicitly documents this:

Safety note: goccy/go-yaml's decoder does not expand YAML aliases recursively — it resolves them via the pre-built AST, which our first pass already depth-checked. Alias chains that would exceed depth limits are caught above; the decoder merely reads the resolved scalar values.

The two-pass design is intentional: pass 1 validates structure/depth on the AST (where we have full control), pass 2 uses Strict() for field validation (which doesn't re-expand aliases recursively). No change needed.

Already addressed. The comment block immediately above the `dec := yaml.NewDecoder(...)` call explicitly documents this: > Safety note: goccy/go-yaml's decoder does not expand YAML aliases recursively — it resolves them via the pre-built AST, which our first pass already depth-checked. Alias chains that would exceed depth limits are caught above; the decoder merely reads the resolved scalar values. The two-pass design is intentional: pass 1 validates structure/depth on the AST (where we have full control), pass 2 uses `Strict()` for field validation (which doesn't re-expand aliases recursively). No change needed.
dec := yaml.NewDecoder(bytes.NewReader(data), yaml.Strict())
Review

[MINOR] Alias cycles are treated as non-errors (cycle detection returns nil). While this prevents recursion issues, consider whether rejecting explicit alias cycles with an error would be a safer fail-fast behavior rather than relying on the decoder to handle them.

**[MINOR]** Alias cycles are treated as non-errors (cycle detection returns nil). While this prevents recursion issues, consider whether rejecting explicit alias cycles with an error would be a safer fail-fast behavior rather than relying on the decoder to handle them.
return dec.Decode(out)
} }
// checkYAMLDepth recursively checks that YAML nodes don't exceed the depth limit // checkYAMLDepth recursively checks that YAML AST nodes don't exceed the depth
Outdated
Review

[NIT] ListBuiltinPersonas treats .json and .yml as valid extensions although built-ins are embedded as YAML only; this is harmless but could be simplified if built-ins will never include those formats.

**[NIT]** ListBuiltinPersonas treats .json and .yml as valid extensions although built-ins are embedded as YAML only; this is harmless but could be simplified if built-ins will never include those formats.
// or the total node count limit. It also detects alias cycles to prevent infinite // limit or the total node count limit. It uses two tracking maps:
Review

[MINOR] The validated map stores the depth at which a node was last validated, but depth <= prevDepth as the short-circuit condition means a node visited at depth 3 that was previously validated at depth 5 would be skipped — which is correct (deeper previous validation is more conservative). However the comment says 'validated it at the same or deeper effective depth' which is slightly ambiguous. The logic is correct but 'same or shallower current depth compared to the previous validation depth' would be clearer.

**[MINOR]** The `validated` map stores the depth at which a node was last validated, but `depth <= prevDepth` as the short-circuit condition means a node visited at depth 3 that was previously validated at depth 5 would be skipped — which is correct (deeper previous validation is more conservative). However the comment says 'validated it at the same or deeper effective depth' which is slightly ambiguous. The logic is correct but 'same or shallower current depth compared to the previous validation depth' would be clearer.
// recursion from crafted YAML with self-referential aliases. // - validated: maps each node to the maximum depth at which it was previously
func checkYAMLDepth(node *yaml.Node, depth, maxDepth, maxNodes int, seen map[*yaml.Node]struct{}, nodeCount *int) error { // checked. If a node is revisited at a deeper depth (e.g., via an alias),
Outdated
Review

[MINOR] The nodeCount increment (line 213, *nodeCount++) happens before the depth-aware short-circuit check (line 222, if prevDepth, ok := validated[node]; ok && depth <= prevDepth). This means if a node is revisited at a shallower depth (triggering the short-circuit), it still increments the count, causing overcounting of shared subtrees referenced via aliases at multiple depths. This could cause false positives on the MaxYAMLNodes limit for legitimately large but valid documents that share anchors extensively. The nodeCount increment should arguably be inside the path where we actually traverse, or this behavior should be explicitly documented as intentional (conservative overcounting).

**[MINOR]** The nodeCount increment (line 213, `*nodeCount++`) happens before the depth-aware short-circuit check (line 222, `if prevDepth, ok := validated[node]; ok && depth <= prevDepth`). This means if a node is revisited at a shallower depth (triggering the short-circuit), it still increments the count, causing overcounting of shared subtrees referenced via aliases at multiple depths. This could cause false positives on the MaxYAMLNodes limit for legitimately large but valid documents that share anchors extensively. The nodeCount increment should arguably be inside the path where we actually traverse, or this behavior should be explicitly documented as intentional (conservative overcounting).
// we re-check it to ensure the combined effective depth doesn't exceed limits.
// - visiting: per-path recursion stack for true cycle detection. A node on the
// current path is a cycle (alias loop); we return nil to avoid infinite recursion.
//
// This design prevents the alias depth bypass where an anchored subtree validated
Outdated
Review

[MINOR] The checkYAMLDepth function uses ast.Node as a map key for cycle detection. ast.Node is an interface — map keys for interface types use the dynamic type and value for equality. For pointer types (which all the concrete ast node types appear to be), this works correctly (pointer identity). This is fine but worth a comment explaining that pointer identity is the intended comparison, not structural equality, to prevent future maintainers from being surprised.

**[MINOR]** The `checkYAMLDepth` function uses `ast.Node` as a map key for cycle detection. `ast.Node` is an interface — map keys for interface types use the dynamic type and value for equality. For pointer types (which all the concrete ast node types appear to be), this works correctly (pointer identity). This is fine but worth a comment explaining that pointer identity is the intended comparison, not structural equality, to prevent future maintainers from being surprised.
// at a shallow depth could be referenced via alias at a greater depth, effectively
Outdated
Review

[MAJOR] Depth check can be bypassed due to global 'seen' set: if an anchored/aliased subtree is first validated at a shallow depth and later referenced via an alias deeper in the structure, the early return on previously seen node skips re-checking at the deeper context. This can allow effective nesting depth to exceed MaxYAMLDepth, potentially leading to stack exhaustion/DoS during decode.

**[MAJOR]** Depth check can be bypassed due to global 'seen' set: if an anchored/aliased subtree is first validated at a shallow depth and later referenced via an alias deeper in the structure, the early return on previously seen node skips re-checking at the deeper context. This can allow effective nesting depth to exceed MaxYAMLDepth, potentially leading to stack exhaustion/DoS during decode.
// exceeding MaxYAMLDepth.
Outdated
Review

[MINOR] The nodeCount increment happens before the visiting and validated checks. This means nodes encountered in cycles (where visiting[node] is true and we return early) are still counted against the total, potentially causing false-positive node count limit errors for valid YAML with shared anchors referenced multiple times. The increment should ideally only count genuinely new work, or the behavior should be explicitly documented.

**[MINOR]** The `nodeCount` increment happens before the `visiting` and `validated` checks. This means nodes encountered in cycles (where `visiting[node]` is true and we return early) are still counted against the total, potentially causing false-positive node count limit errors for valid YAML with shared anchors referenced multiple times. The increment should ideally only count genuinely new work, or the behavior should be explicitly documented.
Outdated
Review

[MINOR] The nodeCount is incremented before the visiting cycle detection check. For cyclic structures, this means a node that triggers cycle detection (returns early) still increments nodeCount. This slightly over-counts nodes in cyclic test scenarios, but since real YAML parsed by parser.ParseBytes cannot have true reference cycles in the AST (only alias references that point forward), this is functionally harmless in production. However, in the unit tests that construct artificial cycles, the count will be inflated.

**[MINOR]** The `nodeCount` is incremented before the `visiting` cycle detection check. For cyclic structures, this means a node that triggers cycle detection (returns early) still increments `nodeCount`. This slightly over-counts nodes in cyclic test scenarios, but since real YAML parsed by `parser.ParseBytes` cannot have true reference cycles in the AST (only alias references that point forward), this is functionally harmless in production. However, in the unit tests that construct artificial cycles, the count will be inflated.
func checkYAMLDepth(node ast.Node, depth, maxDepth, maxNodes int, validated map[ast.Node]int, visiting map[ast.Node]bool, nodeCount *int) error {
if node == nil {
return nil
}
Review

[MINOR] The validated map stores depth as the value but is described as storing 'the maximum depth at which it was previously checked.' The current code stores the current visit depth, not the maximum. If a node is first visited at depth 10 and later at depth 5, validated[node] becomes 5 — the shorter path. The short-circuit condition depth <= prevDepth then allows re-traversal at depth 6 even though we already checked at depth 10. In practice this isn't a security issue (the alias depth bypass test covers the real attack vector), but the comment and variable name ('validated' implying completeness) are slightly misleading. Consider renaming to visitedAtDepth and updating the comment to say 'the last depth at which this node was validated' rather than 'maximum'.

**[MINOR]** The `validated` map stores `depth` as the value but is described as storing 'the maximum depth at which it was previously checked.' The current code stores the *current* visit depth, not the maximum. If a node is first visited at depth 10 and later at depth 5, `validated[node]` becomes 5 — the shorter path. The short-circuit condition `depth <= prevDepth` then allows re-traversal at depth 6 even though we already checked at depth 10. In practice this isn't a security issue (the alias depth bypass test covers the real attack vector), but the comment and variable name ('validated' implying completeness) are slightly misleading. Consider renaming to `visitedAtDepth` and updating the comment to say 'the last depth at which this node was validated' rather than 'maximum'.
Review

[MAJOR] checkYAMLDepth does not handle ast.MergeKeyNode and treats it as a leaf (per the default case comment). YAML merge keys (<<) can contain aliases to mappings; not traversing MergeKeyNode children can bypass the depth enforcement when deep structures are merged, undermining the DoS protections. Add an explicit case to traverse MergeKeyNode's referenced values (typically aliases) and continue depth/node counting.

**[MAJOR]** checkYAMLDepth does not handle ast.MergeKeyNode and treats it as a leaf (per the default case comment). YAML merge keys (<<) can contain aliases to mappings; not traversing MergeKeyNode children can bypass the depth enforcement when deep structures are merged, undermining the DoS protections. Add an explicit case to traverse MergeKeyNode's referenced values (typically aliases) and continue depth/node counting.
if depth > maxDepth { if depth > maxDepth {
return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth) return fmt.Errorf("YAML nesting depth exceeds maximum (%d)", maxDepth)
} }
Outdated
Review

[MINOR] Cycle detection in checkYAMLDepth returns nil without error. If the downstream decoder ever mishandles alias cycles, this could allow potentially problematic inputs to proceed, risking DoS. Consider failing closed by returning an explicit error on detected cycles.

**[MINOR]** Cycle detection in checkYAMLDepth returns nil without error. If the downstream decoder ever mishandles alias cycles, this could allow potentially problematic inputs to proceed, risking DoS. Consider failing closed by returning an explicit error on detected cycles.
// Cycle detection: if we're currently visiting this node on the current
// recursion path, it's a cycle (e.g., alias pointing to an ancestor).
// Return nil to break the cycle without error — cycles are a structural
// property, not a depth violation.
if visiting[node] {
Review

[MINOR] The validated map stores the depth at which a node was last validated, used for depth-aware short-circuiting. However, the map is keyed by ast.Node (interface type). Interface map keys use pointer identity for concrete pointer types, which is correct here. But the comment says "maps each node to the minimum depth" — the map is actually storing the current visit's depth (overwriting each time the node is visited at a greater depth), not the minimum. The comment should say "most recently validated depth" or "maximum depth at which validated" to avoid confusion.

**[MINOR]** The `validated` map stores the depth at which a node was last validated, used for depth-aware short-circuiting. However, the map is keyed by `ast.Node` (interface type). Interface map keys use pointer identity for concrete pointer types, which is correct here. But the comment says "maps each node to the minimum depth" — the map is actually storing the *current visit's* depth (overwriting each time the node is visited at a greater depth), not the minimum. The comment should say "most recently validated depth" or "maximum depth at which validated" to avoid confusion.
return nil
}
// Track total nodes visited as defense-in-depth against wide-but-shallow attacks. // Track total nodes visited as defense-in-depth against wide-but-shallow attacks.
// Placed after cycle detection but before the depth-aware short-circuit. This means
// nodes revisited at shallower depths (via aliases) are counted each time they are
// encountered — intentional conservative overcounting. This bounds the total work
// performed during validation rather than tracking unique nodes, which is the safer
// security posture for untrusted YAML input.
*nodeCount++ *nodeCount++
if *nodeCount > maxNodes { if *nodeCount > maxNodes {
return fmt.Errorf("YAML node count exceeds maximum (%d)", maxNodes) return fmt.Errorf("YAML node count exceeds maximum (%d)", maxNodes)
Outdated
Review

[MAJOR] Alias handling combined with the global 'seen' short-circuit lets deeply nested aliases evade depth checks. The code follows alias nodes (good), but because the target node may already be in the 'seen' set, subsequent deeper traversals are skipped, allowing overall nesting to exceed limits and enabling DoS.

**[MAJOR]** Alias handling combined with the global 'seen' short-circuit lets deeply nested aliases evade depth checks. The code follows alias nodes (good), but because the target node may already be in the 'seen' set, subsequent deeper traversals are skipped, allowing overall nesting to exceed limits and enabling DoS.
Review

[MINOR] The validated map uses ast.Node (interface) as a key, which relies on pointer equality for interface comparison. This works correctly for pointer types like *ast.MappingNode, but if any ast.Node implementation is a value type (non-pointer), two structurally identical nodes could hash to different entries or the same entry incorrectly. In practice, all goccy/go-yaml AST node types appear to be pointer types, so this is low risk, but it's an implicit assumption worth a comment.

**[MINOR]** The `validated` map uses `ast.Node` (interface) as a key, which relies on pointer equality for interface comparison. This works correctly for pointer types like `*ast.MappingNode`, but if any `ast.Node` implementation is a value type (non-pointer), two structurally identical nodes could hash to different entries or the same entry incorrectly. In practice, all goccy/go-yaml AST node types appear to be pointer types, so this is low risk, but it's an implicit assumption worth a comment.
Review

Already addressed. There's an explicit comment at the validated map access (the depth-aware short-circuit block):

Note: using ast.Node (interface) as map key relies on pointer identity, which is correct because all goccy/go-yaml AST node types are pointer receivers (*MappingNode, *SequenceNode, etc.), never value types.

This documents the assumption directly at the usage site. No change needed.

Already addressed. There's an explicit comment at the `validated` map access (the depth-aware short-circuit block): > Note: using ast.Node (interface) as map key relies on pointer identity, which is correct because all goccy/go-yaml AST node types are pointer receivers (*MappingNode, *SequenceNode, etc.), never value types. This documents the assumption directly at the usage site. No change needed.
} }
Outdated
Review

[MINOR] The validated[node] = depth assignment stores the current depth as the 'minimum depth at which it was validated'. However the comment says 'maps each node to the minimum depth at which it was previously checked' and the short-circuit condition is depth <= prevDepth (skip if current depth is shallower or equal). This means validated stores the first depth seen (or the deepest so far), not the minimum. On the first visit validated[node] is set to depth, and subsequent visits only proceed if depth > prevDepth. So the map actually stores the shallowest depth visited. The comment is slightly confusing — 'minimum depth' is correct, but the explanation of re-checking logic could be clearer.

**[MINOR]** The `validated[node] = depth` assignment stores the current depth as the 'minimum depth at which it was validated'. However the comment says 'maps each node to the minimum depth at which it was previously checked' and the short-circuit condition is `depth <= prevDepth` (skip if current depth is shallower or equal). This means `validated` stores the *first* depth seen (or the deepest so far), not the minimum. On the first visit `validated[node]` is set to `depth`, and subsequent visits only proceed if `depth > prevDepth`. So the map actually stores the *shallowest* depth visited. The comment is slightly confusing — 'minimum depth' is correct, but the explanation of re-checking logic could be clearer.
Review

[NIT] The checkYAMLDepth function receives maxNodes int as a parameter but MaxYAMLNodes is passed as the constant from the call site. The maxNodes parameter is never varied between call sites — it's always MaxYAMLNodes. Could simplify by using the constant directly in the function, but having it as a parameter makes testing easier (allows injecting lower limits in tests). Current approach is acceptable.

**[NIT]** The `checkYAMLDepth` function receives `maxNodes int` as a parameter but `MaxYAMLNodes` is passed as the constant from the call site. The `maxNodes` parameter is never varied between call sites — it's always `MaxYAMLNodes`. Could simplify by using the constant directly in the function, but having it as a parameter makes testing easier (allows injecting lower limits in tests). Current approach is acceptable.
// Cycle detection: if we've seen this node before, we're in a cycle. // Depth-aware short-circuit: skip re-validation only when the current visit
if _, ok := seen[node]; ok { // depth is the same or shallower than the depth at which this node was
Outdated
Review

[MINOR] The checkYAMLDepth switch statement has no default case. While this is intentional (scalar types are leaf nodes and need no recursion), a comment inside the switch or a default: // scalar leaf node, no children case would make it clearer to future readers that the omission is deliberate rather than an oversight. The existing comment above the closing brace partially covers this but is outside the switch body.

**[MINOR]** The `checkYAMLDepth` switch statement has no default case. While this is intentional (scalar types are leaf nodes and need no recursion), a comment inside the switch or a `default: // scalar leaf node, no children` case would make it clearer to future readers that the omission is deliberate rather than an oversight. The existing comment above the closing brace partially covers this but is outside the switch body.
return nil // Already validated this subtree, skip to avoid infinite recursion. // previously validated. A shallower (or equal) current depth means the
// prior, deeper validation already covered any subtree depth violations.
// If the current depth exceeds the previous validation depth (e.g., an alias
// references this node deeper in the tree), we must re-traverse to ensure
// the combined effective depth doesn't exceed maxDepth.
//
// Note: using ast.Node (interface) as map key relies on pointer identity,
// which is correct because all goccy/go-yaml AST node types are pointer
Review

[MINOR] The MappingValueNode case visits both Key and Value at depth+1 relative to the MappingValueNode's own depth. Since MappingValueNode is itself visited at depth+1 from its parent MappingNode, keys end up at depth+2 from the mapping. This asymmetry between key depth and value depth means scalar keys consume an extra depth level relative to what might be intuitive, but it's consistent and the tests validate the behavior works. Worth a brief comment explaining that keys consume a depth level intentionally.

**[MINOR]** The `MappingValueNode` case visits both Key and Value at `depth+1` relative to the MappingValueNode's own depth. Since `MappingValueNode` is itself visited at `depth+1` from its parent `MappingNode`, keys end up at `depth+2` from the mapping. This asymmetry between key depth and value depth means scalar keys consume an extra depth level relative to what might be intuitive, but it's consistent and the tests validate the behavior works. Worth a brief comment explaining that keys consume a depth level intentionally.
// receivers (*MappingNode, *SequenceNode, etc.), never value types.
if prevDepth, ok := validated[node]; ok && depth <= prevDepth {
return nil
} }
seen[node] = struct{}{} validated[node] = depth
// Handle alias nodes: follow the alias to its anchor target. // Mark as visiting (on the current recursion path) for cycle detection.
// Increment depth when following aliases since they expand the effective structure. visiting[node] = true
if node.Kind == yaml.AliasNode && node.Alias != nil { defer func() { visiting[node] = false }()
return checkYAMLDepth(node.Alias, depth+1, maxDepth, maxNodes, seen, nodeCount)
}
for _, child := range node.Content { // Walk children based on node type.
if err := checkYAMLDepth(child, depth+1, maxDepth, maxNodes, seen, nodeCount); err != nil { switch n := node.(type) {
case *ast.MappingNode:
for _, value := range n.Values {
if err := checkYAMLDepth(value, depth+1, maxDepth, maxNodes, validated, visiting, nodeCount); err != nil {
return err
}
}
Review

[NIT] The AnchorNode case increments depth for the anchor definition. The comment explains this is intentional and asymmetric. However, this means an anchored value at the top level that nests 10 levels will consume 11 depth budget (1 for anchor + 10 for content), and when aliased at depth 5, the alias expands to depth 5+1+10=16. The comment says 'combined budget is halved' which isn't quite accurate — it's more nuanced. The comment could be clearer, but this doesn't affect correctness.

**[NIT]** The `AnchorNode` case increments depth for the anchor definition. The comment explains this is intentional and asymmetric. However, this means an anchored value at the top level that nests 10 levels will consume 11 depth budget (1 for anchor + 10 for content), and when aliased at depth 5, the alias expands to depth 5+1+10=16. The comment says 'combined budget is halved' which isn't quite accurate — it's more nuanced. The comment could be clearer, but this doesn't affect correctness.
Review

Acknowledged. The AnchorNode comment already explains the asymmetry in detail — both the definition site and the reference site each consume a level, making deeply nested anchor/alias pairs hit the limit sooner. The reviewer's observation about the budget not being exactly "halved" is correct (the comment says "reduced" not "halved"), and the existing phrasing captures the design intent accurately. No change — doesn't affect correctness and the comment already explains the reasoning.

Acknowledged. The AnchorNode comment already explains the asymmetry in detail — both the definition site and the reference site each consume a level, making deeply nested anchor/alias pairs hit the limit sooner. The reviewer's observation about the budget not being exactly "halved" is correct (the comment says "reduced" not "halved"), and the existing phrasing captures the design intent accurately. No change — doesn't affect correctness and the comment already explains the reasoning.
case *ast.MappingValueNode:
// Both Key and Value are visited at depth+1 relative to this
// MappingValueNode. Since MappingNode visits its MappingValueNode
// children at depth+1 as well, keys and values end up at depth+2
// from the parent MappingNode. This is intentional: it mirrors the
// actual nesting structure (mapping → key-value pair → key/value).
if err := checkYAMLDepth(n.Key, depth+1, maxDepth, maxNodes, validated, visiting, nodeCount); err != nil {
return err return err
} }
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, validated, visiting, nodeCount); err != nil {
return err
}
case *ast.SequenceNode:
for _, value := range n.Values {
if err := checkYAMLDepth(value, depth+1, maxDepth, maxNodes, validated, visiting, nodeCount); err != nil {
return err
}
}
case *ast.AliasNode:
// Follow alias to its target, incrementing depth since aliases expand
// the effective structure.
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, validated, visiting, nodeCount); err != nil {
return err
}
case *ast.AnchorNode:
// Increment depth for anchor values as a conservative measure: the
// anchor definition itself is structural, and treating it as a depth
// level ensures that deeply nested anchors are caught at definition
// time rather than only when referenced via alias. This +1 is
// asymmetric with alias (which also increments) — by design, the
// effective depth budget for anchored-then-aliased content is reduced
// because both the definition site and the reference site each consume
// a level, making deeply nested anchor/alias pairs hit the limit sooner.
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, validated, visiting, nodeCount); err != nil {
return err
Review

[MAJOR] checkYAMLDepth treats MergeKeyNode as a leaf (listed in the default case comment) and does not recurse into its referenced value(s). YAML merge keys (<<: *anchor) can introduce aliased content at a deeper effective depth without being traversed, potentially bypassing the MaxYAMLDepth and enabling stack exhaustion/DoS during decoding. Explicitly handle *ast.MergeKeyNode by recursing into its child node(s) (e.g., its Value/Values and any contained AliasNode) similar to other node types.

**[MAJOR]** checkYAMLDepth treats MergeKeyNode as a leaf (listed in the default case comment) and does not recurse into its referenced value(s). YAML merge keys (<<: *anchor) can introduce aliased content at a deeper effective depth without being traversed, potentially bypassing the MaxYAMLDepth and enabling stack exhaustion/DoS during decoding. Explicitly handle *ast.MergeKeyNode by recursing into its child node(s) (e.g., its Value/Values and any contained AliasNode) similar to other node types.
}
case *ast.TagNode:
if err := checkYAMLDepth(n.Value, depth+1, maxDepth, maxNodes, validated, visiting, nodeCount); err != nil {
return err
}
case *ast.MergeKeyNode:
// MergeKeyNode represents the literal "<<" merge key token. It has no
// child nodes — the value side of a merge (e.g., *alias) lives in the
// parent MappingValueNode.Value, which is already recursed into above.
// Explicitly listed here (rather than in the default case) to prevent
// future library changes from silently bypassing depth checks.
default:
// Scalar leaf nodes (StringNode, IntegerNode, FloatNode, BoolNode,
// NullNode, InfinityNode, NanNode, LiteralNode) have no children to
// recurse into.
} }
return nil return nil
} }
@@ -227,7 +331,11 @@ func checkYAMLDepth(node *yaml.Node, depth, maxDepth, maxNodes int, seen map[*ya
// ParsePersonaBytes parses persona data from bytes with a source label for errors. // ParsePersonaBytes parses persona data from bytes with a source label for errors.
// This is useful for parsing personas fetched from external sources (e.g., Gitea API) // This is useful for parsing personas fetched from external sources (e.g., Gitea API)
// without requiring filesystem access. Format is detected by source extension. // without requiring filesystem access. Format is detected by source extension.
// Input is bounded by MaxPersonaFileSize to prevent resource exhaustion.
func ParsePersonaBytes(data []byte, source string) (*Persona, error) { func ParsePersonaBytes(data []byte, source string) (*Persona, error) {
if len(data) > MaxPersonaFileSize {
return nil, fmt.Errorf("persona data from %s exceeds maximum size (%d bytes, limit %d)", source, len(data), MaxPersonaFileSize)
}
return parsePersona(data, source) return parsePersona(data, source)
} }
+222 -41
View File
@@ -7,7 +7,7 @@ import (
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
"strings" "strings"
"testing" "testing"
"gopkg.in/yaml.v3" "github.com/goccy/go-yaml/ast"
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

[MAJOR] Tests import github.com/goccy/go-yaml/ast, which is also subject to the dependency allowlist and currently not approved (only github.com/google/go-cmp is allowed for tests).

**[MAJOR]** Tests import github.com/goccy/go-yaml/ast, which is also subject to the dependency allowlist and currently not approved (only github.com/google/go-cmp is allowed for tests).
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
) )
func TestLoadBuiltinPersona(t *testing.T) { func TestLoadBuiltinPersona(t *testing.T) {
security-review-bot marked this conversation as resolved
Review

[MINOR] Tests import github.com/goccy/go-yaml/ast, which is also not in the approved dependency allowlist. Even for tests, third-party dependencies must be explicitly approved.

**[MINOR]** Tests import github.com/goccy/go-yaml/ast, which is also not in the approved dependency allowlist. Even for tests, third-party dependencies must be explicitly approved.
@@ -459,7 +459,14 @@ func TestYAMLDeeplyNestedRejection(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
path := filepath.Join(dir, "deeply-nested.yaml") path := filepath.Join(dir, "deeply-nested.yaml")
// Build a deeply nested YAML structure that exceeds MaxYAMLDepth (20). // Build a deeply nested YAML structure that exceeds MaxYAMLDepth (20).
Review

[MINOR] TestYAMLDeeplyNestedRejection comment says 'With 25 levels, we exceed MaxYAMLDepth (20)' but the depth counting depends on both MappingNode and MappingValueNode traversal (each contributes depth+1 for key, depth+1 for value). The comment would benefit from a concrete trace of how the depth accumulates to make the test self-documenting and easier to maintain if MaxYAMLDepth changes.

**[MINOR]** TestYAMLDeeplyNestedRejection comment says 'With 25 levels, we exceed MaxYAMLDepth (20)' but the depth counting depends on both MappingNode and MappingValueNode traversal (each contributes depth+1 for key, depth+1 for value). The comment would benefit from a concrete trace of how the depth accumulates to make the test self-documenting and easier to maintain if MaxYAMLDepth changes.
// Each level adds 2 to the depth count (key + value mapping). // Depth accumulation trace for "nested: \n level0: \n level1: ...":
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Outdated
Review

[NIT] The comment in TestYAMLDeeplyNestedRejection was updated from 'Each level adds 2 to the depth count (key + value mapping)' to 'Each level adds to the depth count via mapping values.' The new comment is vague — it doesn't explain the actual counting semantics with the new AST walker, making it harder to reason about whether the test correctly triggers the depth limit. Consider specifying exactly how many depth units each level of nesting adds under the new implementation.

**[NIT]** The comment in `TestYAMLDeeplyNestedRejection` was updated from 'Each level adds 2 to the depth count (key + value mapping)' to 'Each level adds to the depth count via mapping values.' The new comment is vague — it doesn't explain the actual counting semantics with the new AST walker, making it harder to reason about whether the test correctly triggers the depth limit. Consider specifying exactly how many depth units each level of nesting adds under the new implementation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// - Document root parsed at depth 0
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// - Root MappingNode children (MappingValueNodes) visited at depth 1
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// - "nested" MappingValueNode: key at depth 2, value at depth 2
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// - Each levelN adds depth via MappingValueNode traversal (key + value)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// - Exact depth per level depends on AST structure (MappingNode wrapping),
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// but 25 levels reliably exceeds MaxYAMLDepth (20) with comfortable margin.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// The test uses 25 levels rather than exactly 21 to avoid brittleness.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

[NIT] The TestYAMLDeeplyNestedRejection comment says 'Each levelN mapping adds +1 depth (MappingNode → MappingValueNode → value)' but the trace shows MappingValueNode contributes depth via both Key and Value children. The actual depth accumulation might differ from the comment's description depending on how goccy/go-yaml structures the AST for nested maps. The test uses 25 levels with a 'comfortable margin' which compensates, but the depth trace comment may be imprecise.

**[NIT]** The `TestYAMLDeeplyNestedRejection` comment says 'Each levelN mapping adds +1 depth (MappingNode → MappingValueNode → value)' but the trace shows MappingValueNode contributes depth via both Key and Value children. The actual depth accumulation might differ from the comment's description depending on how goccy/go-yaml structures the AST for nested maps. The test uses 25 levels with a 'comfortable margin' which compensates, but the depth trace comment may be imprecise.
Review

Acknowledged. The test comment was revised to avoid the imprecise per-level arithmetic:

Exact depth per level depends on AST structure (MappingNode wrapping), but 25 levels reliably exceeds MaxYAMLDepth (20) with comfortable margin.

This intentionally avoids claiming a specific depth-per-level formula. The 25-level margin makes the test robust regardless of the precise per-level increment. No change needed.

Acknowledged. The test comment was revised to avoid the imprecise per-level arithmetic: > Exact depth per level depends on AST structure (MappingNode wrapping), but 25 levels reliably exceeds MaxYAMLDepth (20) with comfortable margin. This intentionally avoids claiming a specific depth-per-level formula. The 25-level margin makes the test robust regardless of the precise per-level increment. No change needed.
var sb strings.Builder var sb strings.Builder
sb.WriteString("name: test\nidentity: test\nnested:\n") sb.WriteString("name: test\nidentity: test\nnested:\n")
indent := " " indent := " "
7
@@ -483,6 +490,35 @@ func TestYAMLDeeplyNestedRejection(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
} }
} }
Review

[MINOR] The TestYAMLEmptyFileRejection test uses tc.name (e.g., "completely empty") as the filename fragment but writes all files to the same dir. The test case names contain spaces which are valid in filenames but could cause issues on some filesystems. More importantly, the test writes files with names like completely empty.yaml — using spaces in filenames is unusual and could cause subtle test issues. Prefer replacing spaces with underscores or hyphens.

**[MINOR]** The `TestYAMLEmptyFileRejection` test uses `tc.name` (e.g., `"completely empty"`) as the filename fragment but writes all files to the same `dir`. The test case names contain spaces which are valid in filenames but could cause issues on some filesystems. More importantly, the test writes files with names like `completely empty.yaml` — using spaces in filenames is unusual and could cause subtle test issues. Prefer replacing spaces with underscores or hyphens.
Review

[NIT] TestYAMLEmptyFileRejection has a redundant if err != nil guard on the second condition: if err != nil && !strings.Contains(...). Since the preceding if err == nil { t.Error(...); return } (implicit — actually the test doesn't return on the first error, it falls through) — actually the first check is if err == nil { t.Error(...) } without a return, so err could still be nil here. This means the strings.Contains check is guarded correctly with err != nil, but the test won't t.Error the wrong message if err == nil (it will have already errored on the first check). The pattern is slightly unusual compared to the rest of the file which uses early returns. Consistent style would be to add a return after the first t.Error, then drop the err != nil guard.

**[NIT]** `TestYAMLEmptyFileRejection` has a redundant `if err != nil` guard on the second condition: `if err != nil && !strings.Contains(...)`. Since the preceding `if err == nil { t.Error(...); return }` (implicit — actually the test doesn't return on the first error, it falls through) — actually the first check is `if err == nil { t.Error(...) }` without a return, so `err` could still be nil here. This means the `strings.Contains` check is guarded correctly with `err != nil`, but the test won't `t.Error` the wrong message if `err == nil` (it will have already errored on the first check). The pattern is slightly unusual compared to the rest of the file which uses early returns. Consistent style would be to add a `return` after the first `t.Error`, then drop the `err != nil` guard.
func TestYAMLEmptyFileRejection(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
tests := []struct {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
name string
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
content string
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{"completely_empty", ""},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{"whitespace_only", " \n\n "},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{"comment_only", "# just a comment\n"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
for _, tc := range tests {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Run(tc.name, func(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
dir := t.TempDir()
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
path := filepath.Join(dir, tc.name+".yaml")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err := os.WriteFile(path, []byte(tc.content), 0644); err != nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatalf("failed to write test file: %v", err)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
_, err := LoadPersona(path)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err == nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatal("expected error for empty YAML input, got nil")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if !strings.Contains(err.Error(), "empty YAML document") {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Errorf("expected error containing %q, got: %v", "empty YAML document", err)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
})
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
func TestYAMLFileSizeLimit(t *testing.T) { func TestYAMLFileSizeLimit(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
path := filepath.Join(dir, "huge.yaml") path := filepath.Join(dir, "huge.yaml")
Review

[NIT] TestYAMLEmptyFileRejection has a redundant if err != nil guard before checking err.Error() — since the previous block already verified err != nil, the outer guard is unnecessary. This is a style nit that doesn't affect correctness.

**[NIT]** `TestYAMLEmptyFileRejection` has a redundant `if err != nil` guard before checking `err.Error()` — since the previous block already verified `err != nil`, the outer guard is unnecessary. This is a style nit that doesn't affect correctness.
@@ -504,41 +540,41 @@ func TestYAMLFileSizeLimit(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
func TestYAMLAliasCycleDetection(t *testing.T) { func TestYAMLAliasCycleDetection(t *testing.T) {
// Test that our checkYAMLDepth function handles alias cycles gracefully // Test that our checkYAMLDepth function handles alias cycles gracefully
// by using the seen map to prevent infinite recursion. // by using the visiting map to prevent infinite recursion.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// We test this directly because go-yaml's parser handles most cycles
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// at parse time, but we need to ensure our checker is robust.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Create a node structure where an alias points to a parent node, // Create a node structure where an alias points to a parent node,
// simulating what could happen with malicious input that bypasses // simulating what could happen with crafted input.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// go-yaml's cycle detection. parent := &ast.MappingNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
parent := &yaml.Node{ Values: []*ast.MappingValueNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Kind: yaml.MappingNode, {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Content: []*yaml.Node{ Key: &ast.StringNode{Value: "name"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "name"}, Value: &ast.StringNode{Value: "test"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "test"}, },
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "nested"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}, },
} }
// Create a child that aliases back to the parent (artificial cycle) // Create a child that aliases back to the parent (artificial cycle)
aliasToParent := &yaml.Node{ aliasToParent := &ast.AliasNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Kind: yaml.AliasNode, Value: parent,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Alias: parent,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
} }
parent.Content = append(parent.Content, aliasToParent) parent.Values = append(parent.Values, &ast.MappingValueNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Key: &ast.StringNode{Value: "nested"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Value: aliasToParent,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
})
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
nodeCount := 0 nodeCount := 0
seen := make(map[*yaml.Node]struct{}) validated := make(map[ast.Node]int)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
visiting := make(map[ast.Node]bool)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// This should NOT hang or stack overflow - the seen map prevents infinite recursion // This should NOT hang or stack overflow - cycle detection prevents infinite recursion
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
err := checkYAMLDepth(parent, 0, MaxYAMLDepth, MaxYAMLNodes, seen, &nodeCount) err := checkYAMLDepth(parent, 0, MaxYAMLDepth, MaxYAMLNodes, validated, visiting, &nodeCount)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err != nil { if err != nil {
t.Errorf("unexpected error traversing cyclic structure: %v", err) t.Errorf("unexpected error traversing cyclic structure: %v", err)
} }
// Verify we tracked the parent in the seen map // Verify we tracked the parent in the validated map
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if _, ok := seen[parent]; !ok { if _, ok := validated[parent]; !ok {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Error("parent node not tracked in seen map") t.Error("parent node not tracked in validated map")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
} }
} }
@@ -594,36 +630,82 @@ func TestYAMLNodeCountLimit(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
func TestCheckYAMLDepthCycleDetectionDirect(t *testing.T) { func TestCheckYAMLDepthCycleDetectionDirect(t *testing.T) {
// Direct test of cycle detection in checkYAMLDepth by creating // Direct test of cycle detection in checkYAMLDepth by creating
// a node structure with an artificial cycle. // a node structure with an artificial cycle.
// This tests the seen map logic independent of go-yaml's parsing. node := &ast.MappingNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
node := &yaml.Node{ Values: []*ast.MappingValueNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Kind: yaml.MappingNode, {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Content: []*yaml.Node{ Key: &ast.StringNode{Value: "key"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "key"}, Value: &ast.StringNode{Value: "value"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{Kind: yaml.ScalarNode, Value: "value"}, },
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}, },
} }
// Create a cycle by making a child reference the parent // Create a cycle by making a child reference the parent
cycleChild := &yaml.Node{ cycleChild := &ast.AliasNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Kind: yaml.AliasNode, Value: node, // Points back to the parent
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Alias: node, // Points back to the parent
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
} }
node.Content = append(node.Content, node.Values = append(node.Values, &ast.MappingValueNode{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
&yaml.Node{Kind: yaml.ScalarNode, Value: "cyclic"}, Key: &ast.StringNode{Value: "cyclic"},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
cycleChild, Value: cycleChild,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
) })
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
nodeCount := 0 nodeCount := 0
seen := make(map[*yaml.Node]struct{}) validated := make(map[ast.Node]int)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
err := checkYAMLDepth(node, 0, MaxYAMLDepth, MaxYAMLNodes, seen, &nodeCount) visiting := make(map[ast.Node]bool)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
err := checkYAMLDepth(node, 0, MaxYAMLDepth, MaxYAMLNodes, validated, visiting, &nodeCount)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Should complete without infinite recursion due to cycle detection // Should complete without infinite recursion due to cycle detection
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
// The seen map should contain multiple entries // The validated map should contain multiple entries
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if len(seen) < 2 { if len(validated) < 2 {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Errorf("seen map has %d entries, expected at least 2", len(seen)) t.Errorf("validated map has %d entries, expected at least 2", len(validated))
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
func TestYAMLAliasDepthBypass(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Test that an anchored subtree first validated at a shallow depth is
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// re-checked when referenced via alias at a deeper position. Without the
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// depth-aware validated map, the alias reference would skip re-checking
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// and allow the effective nesting to exceed MaxYAMLDepth.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
dir := t.TempDir()
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
path := filepath.Join(dir, "alias-depth-bypass.yaml")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Build YAML with an anchor at shallow depth containing a subtree near the limit,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// then reference it via alias deep enough that effective depth exceeds MaxYAMLDepth.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
var sb strings.Builder
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString("name: test\nidentity: test\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Create the anchored subtree at depth 1 (key level) that nests 15 levels deep.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString("anchor_key: &deep_anchor\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
for i := 0; i < 15; i++ {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString(strings.Repeat(" ", i+1))
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString(fmt.Sprintf("level%d:\n", i))
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString(strings.Repeat(" ", 16))
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString("leaf: value\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Create a wrapper that nests 6 levels deep, then references the anchor.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Effective depth at alias target = 6 (wrapper nesting) + 1 (alias) + 15 (subtree) = 22 > 20
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString("wrapper:\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
for i := 0; i < 6; i++ {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString(strings.Repeat(" ", i+1))
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString(fmt.Sprintf("n%d:\n", i))
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString(strings.Repeat(" ", 7))
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString("alias_ref: *deep_anchor\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err := os.WriteFile(path, []byte(sb.String()), 0644); err != nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatalf("failed to write test file: %v", err)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
_, err := LoadPersona(path)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err == nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatal("expected error for alias depth bypass, got nil")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if !strings.Contains(err.Error(), "nesting depth exceeds") {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Errorf("error = %q, want containing 'nesting depth exceeds'", err.Error())
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
} }
} }
@@ -776,3 +858,102 @@ identity: test identity
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Errorf("Name = %q, want %q", p.Name, "test") t.Errorf("Name = %q, want %q", p.Name, "test")
} }
} }
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
func TestJSONTrailingContentRejected(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
tests := []struct {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
name string
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
content string
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
name: "trailing garbage after object",
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
content: `{"name":"test","identity":"test identity"}garbage`,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
name: "two JSON objects",
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
content: `{"name":"test","identity":"test identity"}{"name":"other"}`,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
{
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
name: "trailing array",
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
content: `{"name":"test","identity":"test identity"}[]`,
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
},
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
for _, tt := range tests {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Run(tt.name, func(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
dir := t.TempDir()
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
path := filepath.Join(dir, "test.json")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err := os.WriteFile(path, []byte(tt.content), 0644); err != nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatalf("failed to write test file: %v", err)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
_, err := LoadPersona(path)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err == nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatal("expected error for trailing content, got nil")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if !strings.Contains(err.Error(), "trailing content") {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Errorf("error = %q, want to contain 'trailing content'", err.Error())
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
})
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
func TestParsePersonaBytesSizeLimit(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// ParsePersonaBytes should reject input exceeding MaxPersonaFileSize
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
oversized := make([]byte, MaxPersonaFileSize+1)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
for i := range oversized {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
oversized[i] = 'x'
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
_, err := ParsePersonaBytes(oversized, "oversized.yaml")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err == nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatal("expected error for oversized input, got nil")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if !strings.Contains(err.Error(), "exceeds maximum size") {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Errorf("error = %q, want to contain 'exceeds maximum size'", err.Error())
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Just under the limit should not trigger size error (may fail parse, but not size)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
underLimit := []byte("name: test\nidentity: test persona\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
p, err := ParsePersonaBytes(underLimit, "valid.yaml")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err != nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatalf("unexpected error for valid input: %v", err)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if p.Name != "test" {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Errorf("Name = %q, want %q", p.Name, "test")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
func TestYAMLMergeKeyDepthCheck(t *testing.T) {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Verify that YAML merge keys (<<: *alias) are properly handled by the
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// depth checker. The merge key content is in the MappingValueNode.Value
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// (an AliasNode), not in the MergeKeyNode itself.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
p, err := ParsePersonaBytes([]byte("name: merge-test\nidentity: test\n"), "merge.yaml")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err != nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatalf("basic parse failed: %v", err)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if p.Name != "merge-test" {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Errorf("Name = %q, want %q", p.Name, "merge-test")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Test that deeply nested merge keys still hit depth limit.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
// Build YAML with merge key content nested beyond MaxYAMLDepth.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
var sb strings.Builder
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString("name: deep-merge\nidentity: deep merge persona\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString("anchor: &deep\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
indent := " "
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
for i := 0; i < MaxYAMLDepth+5; i++ {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString(indent)
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString(fmt.Sprintf("level%d:\n", i))
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
indent += " "
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString(indent + "leaf: value\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
sb.WriteString("target:\n <<: *deep\n")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
_, err = ParsePersonaBytes([]byte(sb.String()), "deep-merge.yaml")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if err == nil {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Fatal("expected error for deeply nested merge key content, got nil")
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
if !strings.Contains(err.Error(), "depth") {
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
t.Errorf("error = %q, want to contain 'depth'", err.Error())
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
}
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.
Review

Fixed in 0b16c41: moved t.TempDir() inside each subtest for proper isolation.

Fixed in 0b16c41: moved `t.TempDir()` inside each subtest for proper isolation.