feat(v0.6): config file parser + resolver + source attribution

Phase 1 foundation. Adds:

- internal/config/configfile.go — strict-key YAML parser for the
  global config file. Unknown top-level keys invalidate the entire
  file (no partial apply); future schema versions are rejected with
  an upgrade message. Platform-appropriate path (XDG_CONFIG_HOME or
  ~/.config on Unix, %APPDATA% on native Windows).

- internal/config/resolver.go — Resolver / ResolvedSetting /
  SettingSource (Builtin, ConfigFileSrc, EnvVar, CLIFlag,
  PlatformOverride). Each setting carries provenance plus an
  Override chain so callers can render "env var (overrides config
  file: X)" lines. session_mode applies the native-Windows
  platform override at the resolver layer with the configured
  value chained as Override. Exports SetConfigPathForTest and
  SetIsNativeWindowsForTest so cross-package tests can isolate
  themselves from host config or simulate the override.

- internal/config/config.go — legacy ResolveRoot / ResolveAgent /
  ResolveSeedDir / ResolveProjectRoot / ResolveSessionMode now
  delegate to LoadResolver so config-file values take effect for
  entry commands. ResolveProjectRoot preserves the "" sentinel for
  built-in source so SearchRoots and doctor checkProjectRoot keep
  their existing fallback semantics. ResolveSessionMode preserves
  the v0.5.3 unknown-value stderr warning, distinguishing env-var
  and config-file sources in the message.

- Tests: 6 LoadConfigFile cases (missing, basic, partial, unknown
  key, future schema, malformed YAML), 3 ConfigFilePath cases
  (XDG / ~/.config / %APPDATA%), 11 resolver cases including
  layering, override chains, path expansion, platform override,
  and SettingSource string rendering.

- Test isolation: vulnerable tests in config_test.go and
  config_roots_test.go that asserted Builtin-source defaults now
  call SetConfigPathForTest to point at a nonexistent path,
  insulating them from developer-host config files. Three cmd
  tests that asserted persistent session_mode behavior now call
  SetIsNativeWindowsForTest to disable the platform override
  (Phase 1 introduces the override; the layering-only behavior
  these tests cover is tested separately in resolver_test.go).

No new dependencies (gopkg.in/yaml.v3 was already in go.mod).
No version bump (lands at the end of Phase 3 per the v0.6 spec).
This commit is contained in:
2026-05-14 21:47:25 -04:00
parent 8304545840
commit 6f80c8bf5c
9 changed files with 970 additions and 39 deletions
+10
View File
@@ -8,6 +8,8 @@ import (
"path/filepath"
"strings"
"testing"
"github.com/warrenronsiek/ctask/internal/config"
)
// captureStdout runs fn while capturing os.Stdout and returns the output.
@@ -196,6 +198,10 @@ func TestCheckTmuxConfiguredAndPresent(t *testing.T) {
if _, err := exec.LookPath("tmux"); err != nil {
t.Skip("tmux not on PATH")
}
// Disable the v0.6 platform override so this test can verify the
// post-resolver doctor output for persistent mode without being
// short-circuited on native Windows.
config.SetIsNativeWindowsForTest(t, func() bool { return false })
os.Setenv("CTASK_SESSION_MODE", "persistent")
defer os.Unsetenv("CTASK_SESSION_MODE")
out := captureStdout(t, func() {
@@ -211,6 +217,10 @@ func TestCheckTmuxConfiguredAndPresent(t *testing.T) {
}
func TestCheckTmuxConfiguredAndMissing(t *testing.T) {
// Disable the v0.6 platform override so the test reaches the
// tmux lookup path (rather than being short-circuited on native
// Windows hosts).
config.SetIsNativeWindowsForTest(t, func() bool { return false })
orig := os.Getenv("PATH")
defer os.Setenv("PATH", orig)
os.Setenv("PATH", "")
+8
View File
@@ -4,6 +4,8 @@ import (
"os"
"strings"
"testing"
"github.com/warrenronsiek/ctask/internal/config"
)
// Tests in this file mutate package globals (isTTYCheck, runWorkspaceEntry).
@@ -18,6 +20,12 @@ func TestNewDirectModeSkipsPreflight(t *testing.T) {
}
func TestNewDirectFlagUnderPersistentEmitsWarningAndProceeds(t *testing.T) {
// Disable the v0.6 platform override: this test verifies the
// --direct bypass warning that fires when persistent mode is in
// effect. On native Windows the resolver coerces persistent → direct
// before this function sees it, which is correct production
// behavior but bypasses the codepath under test.
config.SetIsNativeWindowsForTest(t, func() bool { return false })
os.Setenv("CTASK_SESSION_MODE", "persistent")
t.Cleanup(func() { os.Unsetenv("CTASK_SESSION_MODE") })