go-cli-flag-overrides
Gap (UHLP U-GAP-002): the spec promised --port, --docker-sock, and --log-level; the CLI only had --config, so operators could not override YAML settings at launch. The naive fix (defaults of 0/"" applied unconditionally) would clobber YAML values with zero sentinels on every run.
Fix — explicit-pass-only overrides via flag.Visit. flag.Visit invokes its callback only for flags actually present on the command line, so absent flags never touch the config. The sentinel guards (port != 0, strings != "") additionally make even an explicit --port 0 / --docker-sock "" a no-op instead of a wipe. The log level is threaded through framework.New(cfg, logLevel) with info as the final fallback, keeping the CLI the single source of truth.
// Flag registration — full spec surface, sentinel defaults (0 / "" = unset).
func defineFlags(fs *flag.FlagSet, cfgPath *string) (port *int, dockerSock *string, logLevel *string) {
fs.StringVar(cfgPath, "config", "config.yaml", "path to configuration file")
port = fs.Int("port", 0, "override config port (0 = keep config value)")
dockerSock = fs.String("docker-sock", "", "override docker socket path (empty = keep config value)")
logLevel = fs.String("log-level", "", "override log level (empty = keep config value)")
return port, dockerSock, logLevel
}
// Merge: explicit flags only, sentinels never clobber YAML.
func applyOverrides(cfg *framework.Config, fs *flag.FlagSet, port *int, dockerSock *string, logLevel *string) error {
fs.Visit(func(f *flag.Flag) { // callback fires only for passed flags
switch f.Name {
case "port":
if *port != 0 { cfg.Port = *port }
case "docker-sock":
if *dockerSock != "" { cfg.DockerSock = *dockerSock }
case "log-level":
if *logLevel != "" { cfg.LogLevel = *logLevel }
}
})
return nil
}
// Defaulting: config file value, else "info". (An explicit flag has already
// overwritten cfg.LogLevel before this runs.)
func resolveLogLevel(cfg *framework.Config) string {
if cfg.LogLevel != "" { return cfg.LogLevel }
return "info"
}
// Wiring — single framework.New(cfg, logLevel) call site (low-risk signature change).
func run(args []string) error {
fs := flag.NewFlagSet("cli", flag.ContinueOnError)
var cfgPath string
port, dockerSock, logLevel := defineFlags(fs, &cfgPath)
if err := fs.Parse(args); err != nil { return err }
cfg, err := loadFileConfig(cfgPath) // YAML/JSON load
if err != nil { return err }
if err := applyOverrides(cfg, fs, port, dockerSock, logLevel); err != nil { return err }
fw, err := framework.New(cfg, resolveLogLevel(cfg)) // log level threaded in
if err != nil { return err }
return fw.Serve()
}
framework.New accepts the resolved level directly and falls back to "info" only if still empty — the constructor no longer re-derives policy.
Built a self-contained reproduction (`/tmp/go-cli-flag-overrides`, Go 1.26, `go vet` clean, `gofmt` clean) and verified both unit and live behavior: **Automated — 18/18 test executions pass** (6 test functions incl. 12 table subtests): spec flag surface present; 9 override cases; 3 log-level defaulting cases; unknown-flag errors; missing-config fail-fast; live bind test. | Case | Result | |---|---| | No flags → YAML untouched | `port=8080 sock=/var/run/docker.sock level=debug` preserved | | `--port 9091` only | port overridden; socket + level still from YAML | | `--docker-sock` / `--log-level` individually | only that field overridden | | All three flags together | all overridden | | Sentinels `--port 0`, `--docker-sock ""`, `--log-level ""` explicitly passed | YAML **not** clobbered (verified live: bound on YAML port, not 0) | | `--log-level` absent everywhere | resolves to `info` | | Unknown flag | clean usage error, no silent swallow | | Missing config file | fail-fast error, no default fallback | | Port conflict (env service on 8080) | fail-fast `bind: address already in use` — never silently hijacks | **Live runs** (real TCP binds, health endpoint inspected via curl): - `./cli --config cfg.yaml --port 9091` → `ok port=9091 log=debug sock=/var/run/docker.sock` — **`--port 9091` bound correctly**, flag wins over YAML 8080. - No `--port` → YAML port binds; `--port 0`/`--docker-sock ""` → YAML values survive. - `--port P --docker-sock /tmp/x.sock --log-level trace` → `ok port=P log=trace sock=/tmp/x.sock`. **Signature-change risk:** `grep` confirms exactly one `framework.New(` call site (`main.go:106`) and **zero** test callers of `framework.New` — the added `logLevel` parameter compiles cleanly with no collateral test breakage.
{"model": "deepseek-v4-flash", "problem_class": "go-cli-flag-overrides", "result": "passed", "tests": 18}