From 66a1e591efbc73ce32696d30687803ba59176f03 Mon Sep 17 00:00:00 2001 From: amalsaidov Date: Sat, 14 Nov 2026 03:58:00 +0700 Subject: [PATCH] fix(config): emit comments for nested config sections in generated yaml SaveAsYaml walks the Config struct to build a comment map keyed by YAML path, but the recursion into nested structs passed only the field's own yaml tag as the prefix instead of the full dotted path. As a result, comments for any section nested three or more levels deep landed on a non-existent path and were silently dropped: da.tx_signer.*, signer.kms.*, signer.kms.aws.* and signer.kms.gcp.*. In addition, pointer-to-struct sections were skipped entirely because the traversal only recursed on reflect.Struct kinds, so the whole instrumentation block (an *InstrumentationConfig) had no comments. Build the nested prefix from the accumulated path and follow pointer fields to their element type so these sections are documented in the generated evnode.yaml like every other field. Add a regression test asserting the previously missing comments are present and sit directly above their keys. --- pkg/config/yaml.go | 21 ++++++++++----- pkg/config/yaml_test.go | 60 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/pkg/config/yaml.go b/pkg/config/yaml.go index fe5a8e0b08..dbcafd8ab1 100644 --- a/pkg/config/yaml.go +++ b/pkg/config/yaml.go @@ -63,9 +63,16 @@ func (c *Config) SaveAsYaml() error { continue } + // Follow pointers so that pointer-to-struct config sections + // (e.g. the optional instrumentation config) are traversed too. + fieldType := field.Type + if fieldType.Kind() == reflect.Pointer { + fieldType = fieldType.Elem() + } + // Handle embedded structs - if field.Type.Kind() == reflect.Struct && field.Anonymous { - processFields(field.Type, prefix) + if fieldType.Kind() == reflect.Struct && field.Anonymous { + processFields(fieldType, prefix) continue } @@ -74,16 +81,16 @@ func (c *Config) SaveAsYaml() error { if prefix != "" { fieldPath = prefix + "." + fieldPath } - fieldPath = "$." + fieldPath // Add comment if present if comment != "" { - addComment(fieldPath, comment) + addComment("$."+fieldPath, comment) } - // Process nested structs (non-anonymous) - if field.Type.Kind() == reflect.Struct && !field.Anonymous { - processFields(field.Type, yamlTag) + // Process nested structs (non-anonymous), preserving the full + // dotted path so nested comments map to the correct YAML node. + if fieldType.Kind() == reflect.Struct && !field.Anonymous { + processFields(fieldType, fieldPath) } } } diff --git a/pkg/config/yaml_test.go b/pkg/config/yaml_test.go index 2f245b1e0f..f443d43ed7 100644 --- a/pkg/config/yaml_test.go +++ b/pkg/config/yaml_test.go @@ -1,6 +1,8 @@ package config import ( + "os" + "strings" "testing" "github.com/spf13/cobra" @@ -71,3 +73,61 @@ func TestYamlConfigOperations(t *testing.T) { }) } } + +// TestSaveAsYamlNestedComments ensures that SaveAsYaml emits field comments for +// deeply nested config sections. Regression test: comments for sections nested +// three or more levels deep (e.g. signer.kms.*) and for pointer-to-struct +// sections (e.g. instrumentation) were previously dropped because the comment +// path lost its parent prefix and pointer fields were never traversed. +func TestSaveAsYamlNestedComments(t *testing.T) { + dir := t.TempDir() + cfg := DefaultConfig() + cfg.RootDir = dir + + require.NoError(t, cfg.SaveAsYaml()) + + data, err := os.ReadFile(cfg.ConfigPath()) + require.NoError(t, err) + yaml := string(data) + + // Each entry: the comment text and the yaml key it must sit directly above. + cases := []struct { + comment string + key string + }{ + // pointer-to-struct section (instrumentation) + {"Enable Prometheus metrics", "prometheus:"}, + // signer.kms (nested three levels deep) + {"KMS provider for signer type 'kms' (aws, gcp)", "provider:"}, + // signer.kms.aws (nested four levels deep) + {"AWS KMS Key ID or ARN", "key_id:"}, + // da.tx_signer (nested three levels deep) + {"Sign blob submissions locally and broadcast them over consensus gRPC instead of calling blob.Submit on the DA node.", "enabled:"}, + } + + for _, tc := range cases { + require.Contains(t, yaml, tc.comment, "missing comment %q in generated config", tc.comment) + require.True(t, commentPrecedesKey(yaml, tc.comment, tc.key), + "comment %q is not placed directly above key %q", tc.comment, tc.key) + } +} + +// commentPrecedesKey reports whether a line containing the comment text is +// immediately followed (ignoring blank lines) by a line whose first token is +// the given yaml key. +func commentPrecedesKey(yaml, comment, key string) bool { + lines := strings.Split(yaml, "\n") + for i, line := range lines { + if !strings.Contains(line, comment) { + continue + } + for j := i + 1; j < len(lines); j++ { + trimmed := strings.TrimSpace(lines[j]) + if trimmed == "" { + continue + } + return strings.HasPrefix(trimmed, key) + } + } + return false +}