Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,21 @@ Configuration priority (highest to lowest):
3. YAML config file (`dingo.yaml`)
4. Hardcoded defaults

After all sources are merged (including CLI flags), `Config.Validate()`
(`internal/config`) checks the resulting configuration before any services
start: mode enums, port ranges (privileged/out-of-range/duplicate), load-mode
`immutableDbPath` requirement, path-traversal guards, TLS cert/key pairing,
mempool watermarks, block-producer credential paths, and duration/strategy
strings that are otherwise only parsed at their point of use. The relay,
private, and metrics listener ports are required only for the serving modes;
the one-shot subcommands (`load`, `sync`, `mithril`) start none of them. Because
those subcommands run a fixed operation regardless of the configured `runMode`
(which defaults to `serve`), `cmd/dingo` passes `Validate()` an *effective* run
mode derived from the invoked command, so listener and ImmutableDB-source
requirements match what the command actually does. All violations are reported
together in a single startup error. The informational `version` and `list`
subcommands are exempt so they still run against an otherwise-invalid config.

Key configuration areas:
- Network selection (preview, preprod, mainnet)
- Storage mode (`core` or `api`)
Expand Down
136 changes: 136 additions & 0 deletions cmd/dingo/command_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2026 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/require"

"github.com/blinklabs-io/dingo/internal/config"
)

// newTestRootCmd wires the real subcommands under a root command,
// mirroring main() so the command-classification helpers see the same
// tree.
func newTestRootCmd() *cobra.Command {
root := &cobra.Command{Use: "dingo"}
root.AddCommand(
serveCommand(),
loadCommand(),
listCommand(),
versionCommand(),
mithrilCommand(),
syncCommand(),
)
return root
}

func findCmd(t *testing.T, root *cobra.Command, path ...string) *cobra.Command {
t.Helper()
if len(path) == 0 {
return root
}
cmd, _, err := root.Find(path)
require.NoError(t, err)
require.Equal(t, path[len(path)-1], cmd.Name())
return cmd
}

func TestEffectiveRunMode(t *testing.T) {
root := newTestRootCmd()
tests := []struct {
name string
path []string
runMode config.RunMode
want config.RunMode
}{
{"bare serve", nil, config.RunModeServe, config.RunModeServe},
{"bare load", nil, config.RunModeLoad, config.RunModeLoad},
{"bare dev", nil, config.RunModeDev, config.RunModeDev},
{"bare empty defaults to serve", nil, "", config.RunModeServe},
// An invalid configured runMode falls through to serve (matching
// rootCmd's dispatch default) so serving-listener checks still
// apply; the invalid mode is reported separately by Validate.
{"bare invalid falls back to serve", nil, "batch", config.RunModeServe},
// Subcommands run a fixed operation regardless of configured runMode.
{
"serve subcommand ignores load config",
[]string{"serve"},
config.RunModeLoad,
config.RunModeServe,
},
{
"load subcommand",
[]string{"load"},
config.RunModeServe,
config.RunModeLoad,
},
{
"sync is a utility",
[]string{"sync"},
config.RunModeServe,
config.RunModeUtility,
},
{
"mithril list is a utility",
[]string{"mithril", "list"},
config.RunModeServe,
config.RunModeUtility,
},
{
"mithril sync is a utility",
[]string{"mithril", "sync"},
config.RunModeServe,
config.RunModeUtility,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := findCmd(t, root, tt.path...)
top := topLevelCommand(cmd)
got := effectiveRunMode(top, &config.Config{RunMode: tt.runMode})
require.Equal(t, tt.want, got)
})
}
}

func TestIsInformationalCommand(t *testing.T) {
root := newTestRootCmd()
tests := []struct {
name string
path []string
want bool
}{
{"bare root", nil, false},
{"version", []string{"version"}, true},
{"list", []string{"list"}, true},
{"serve", []string{"serve"}, false},
{"sync", []string{"sync"}, false},
// Nested `mithril list` must not be mistaken for top-level `list`.
{"mithril list", []string{"mithril", "list"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := findCmd(t, root, tt.path...)
require.Equal(
t,
tt.want,
isInformationalCommand(topLevelCommand(cmd)),
)
})
}
}
131 changes: 131 additions & 0 deletions cmd/dingo/config_parity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2026 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"maps"
"testing"

"github.com/blinklabs-io/dingo/chainsync"
"github.com/blinklabs-io/dingo/internal/config"
"github.com/blinklabs-io/dingo/mithril"
)

// internal/config cannot import chainsync or mithril without pulling
// node subsystems into the config package, so the accepted-value sets
// it validates against (config.AcceptedChainsyncStrategies and
// config.AcceptedMithrilBackends) are duplicated from those downstream
// parsers. These parity tests live in cmd/dingo, which can import all
// three, and guard against drift in both directions:
//
// - forward: every value config accepts must be accepted by the real
// parser, so a config never passes Validate() only to fail at
// startup;
// - reverse: config's accepted set must match the parser's contract
// exactly, so a value added to the parser without updating config
// (which would spuriously reject a valid config) is caught too.

// TestChainsyncStrategyWhitelistParity checks config.AcceptedChainsyncStrategies
// against chainsync.ParseHeaderSyncStrategy.
func TestChainsyncStrategyWhitelistParity(t *testing.T) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
// canonical is the contract of chainsync.ParseHeaderSyncStrategy;
// it mirrors that function's switch. The set-equality check below
// ties config's list to it, and the parse check ties it to the real
// parser, so a change to the parser's switch fails this test until
// both this list and config's list are updated.
canonical := []string{
"", "primary", "parallel", "round-robin", "roundrobin", "round_robin",
}
assertWhitelistParity(
t,
"chainsync.strategy",
config.AcceptedChainsyncStrategies,
canonical,
func(v string) error {
_, err := chainsync.ParseHeaderSyncStrategy(v)
return err
},
)
Comment on lines +26 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the parity test and the referenced parsers/contracts.
git ls-files cmd/dingo | sed -n '1,120p'
printf '\n--- config_parity_test.go ---\n'
cat -n cmd/dingo/config_parity_test.go | sed -n '1,220p'

printf '\n--- parser definitions ---\n'
rg -n "ParseHeaderSyncStrategy|AcceptedChainsyncStrategies|AcceptedMithrilBackends|Parse.*Mithril" -S .

Repository: blinklabs-io/dingo

Length of output: 8149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map candidate files before reading implementation slices.
fd -a ".*(chainsync|mithril).*" .
fd -a "config.*\.go|.*parity.*\.go" cmd/dingo

Repository: blinklabs-io/dingo

Length of output: 1892


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n cmd/dingo/config_parity_test.go | sed -n '1,220p'

Repository: blinklabs-io/dingo

Length of output: 5358


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'pwd=%s\n' "$PWD"
git ls-files cmd/dingo/config_parity_test.go
cat -n cmd/dingo/config_parity_test.go | sed -n '1,220p'

Repository: blinklabs-io/dingo

Length of output: 5413


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "TestChainsyncStrategyWhitelistParity|assertWhitelistParity|canonical := \[\]string" cmd/dingo/config_parity_test.go

Repository: blinklabs-io/dingo

Length of output: 547


Use a shared accepted-value table

Both parity tests still depend on hand-maintained canonical slices, so a new value accepted by chainsync.ParseHeaderSyncStrategy or resolveMithrilBackend can slip past this check until the list is updated manually. Define the accepted values once and reuse them here and in internal/config.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/dingo/config_parity_test.go` around lines 26 - 60, Replace the
hand-maintained canonical slices in TestChainsyncStrategyWhitelistParity and the
corresponding Mithril parity test with shared accepted-value tables. Define
those tables in a package accessible to both cmd/dingo and internal/config, then
update config.AcceptedChainsyncStrategies and config.AcceptedMithrilBackends and
the parity tests to reuse them while preserving the existing parser checks.

}

// TestMithrilBackendWhitelistParity checks config.AcceptedMithrilBackends
// against resolveMithrilBackend.
func TestMithrilBackendWhitelistParity(t *testing.T) {
// canonical is the contract of resolveMithrilBackend: the empty
// string (which selects v2) plus the two backend constants it
// switches on.
canonical := []string{"", mithril.BackendV1, mithril.BackendV2}
assertWhitelistParity(
t,
"mithril.backend",
config.AcceptedMithrilBackends,
canonical,
func(v string) error {
_, err := resolveMithrilBackend(v)
return err
},
)
}

// assertWhitelistParity verifies that configList (the values
// internal/config accepts) and canonical (the downstream parser's
// contract) describe the same set, and that both are actually accepted
// by the real parser via accepts.
func assertWhitelistParity(
t *testing.T,
name string,
configList, canonical []string,
accepts func(string) error,
) {
t.Helper()
// Forward: everything config accepts must parse.
for _, v := range configList {
if err := accepts(v); err != nil {
t.Errorf(
"%s: config accepts %q but the parser rejects it: %v",
name, v, err,
)
}
}
// Reverse: every value in the parser's contract must actually parse
// (anchoring the contract to the real parser) and config's set must
// match the contract exactly.
for _, v := range canonical {
if err := accepts(v); err != nil {
t.Errorf(
"%s: canonical value %q is rejected by the parser; "+
"update the canonical set: %v",
name, v, err,
)
}
}
if got, want := toStringSet(configList), toStringSet(canonical); !maps.Equal(
got,
want,
) {
t.Errorf(
"%s: config accepted set %v does not match parser contract %v",
name, configList, canonical,
)
}
}

func toStringSet(values []string) map[string]struct{} {
set := make(map[string]struct{}, len(values))
for _, v := range values {
set[v] = struct{}{}
}
return set
}
Loading
Loading