From a5bd7dd1eb63a2be332ecda50fbeccfe5d5de0a8 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 11 Jun 2026 08:49:20 +0300 Subject: appconfig: split FeatureConfig into cohesive per-subsystem structs The FeatureConfig section was a grab-bag mixing five unrelated non-LLM subsystems (ignore filtering, stats, tmux popup editor, tmux action menu, MCP server). Decompose it into named per-subsystem structs (IgnoreConfig, StatsConfig, TmuxEditConfig, TmuxActionConfig, MCPConfig) embedded into FeatureConfig so the subsystem boundaries are explicit. Embedding keeps Go field promotion intact, so existing flat read access (e.g. cfg.MCPPromptsDir) and the JSON/TOML on-disk shape are unchanged; only composite literals that set these leaf fields directly were updated to the nested form. Add fine-grained, defensive-copy section accessors on App (IgnoreSection, StatsSection, TmuxEditSection, TmuxActionSection, MCPSection) so consumers can depend on a single subsystem's config instead of the whole App God-struct. Decouple slashcommands.NewSyncer to accept appconfig.MCPConfig rather than appconfig.App. All tests pass with -race; appconfig coverage 91.5%, total 86.2%. Co-Authored-By: Claude Opus 4.8 --- internal/appconfig/app_feature_sections.go | 40 +++++++++++++++ internal/appconfig/app_feature_sections_test.go | 68 +++++++++++++++++++++++++ internal/appconfig/app_sections.go | 30 ++++------- internal/appconfig/app_sections_test.go | 50 ++++++++++-------- internal/appconfig/config_features_test.go | 4 +- internal/appconfig/config_load.go | 4 +- internal/appconfig/config_types.go | 8 +-- internal/appconfig/feature_sections.go | 50 ++++++++++++++++++ internal/hexaimcp/run.go | 2 +- internal/hexaimcp/run_test.go | 18 +++---- internal/lsp/ignore_test.go | 2 +- internal/slashcommands/syncer.go | 3 +- internal/slashcommands/syncer_test.go | 22 ++++---- internal/tmuxedit/run_test.go | 4 +- 14 files changed, 232 insertions(+), 73 deletions(-) create mode 100644 internal/appconfig/app_feature_sections.go create mode 100644 internal/appconfig/app_feature_sections_test.go create mode 100644 internal/appconfig/feature_sections.go (limited to 'internal') diff --git a/internal/appconfig/app_feature_sections.go b/internal/appconfig/app_feature_sections.go new file mode 100644 index 0000000..2513946 --- /dev/null +++ b/internal/appconfig/app_feature_sections.go @@ -0,0 +1,40 @@ +package appconfig + +import "slices" + +// This file provides fine-grained, read-only accessors for the individual +// feature subsystems embedded in FeatureConfig. They let a consumer depend on +// just the subsystem config it needs (e.g. the MCP server only needs MCPConfig) +// instead of receiving the whole App God-struct. Each accessor returns a deep +// copy so callers cannot mutate App's internal slices. + +// IgnoreSection returns a copy of the gitignore-aware filtering settings. +func (a *App) IgnoreSection() IgnoreConfig { + c := a.IgnoreConfig + c.IgnoreExtraPatterns = slices.Clone(a.IgnoreExtraPatterns) + return c +} + +// StatsSection returns the usage statistics settings. +func (a *App) StatsSection() StatsConfig { + return a.StatsConfig +} + +// TmuxEditSection returns a copy of the tmux popup editor settings. +func (a *App) TmuxEditSection() TmuxEditConfig { + c := a.TmuxEditConfig + c.TmuxEditAgents = append([]TmuxEditAgentCfg{}, a.TmuxEditAgents...) + return c +} + +// TmuxActionSection returns a copy of the tmux action menu settings. +func (a *App) TmuxActionSection() TmuxActionConfig { + c := a.TmuxActionConfig + c.TmuxActionMenu = append([]TmuxActionMenuEntry{}, a.TmuxActionMenu...) + return c +} + +// MCPSection returns the Model Context Protocol server settings. +func (a *App) MCPSection() MCPConfig { + return a.MCPConfig +} diff --git a/internal/appconfig/app_feature_sections_test.go b/internal/appconfig/app_feature_sections_test.go new file mode 100644 index 0000000..1f9ac2d --- /dev/null +++ b/internal/appconfig/app_feature_sections_test.go @@ -0,0 +1,68 @@ +package appconfig + +import ( + "reflect" + "testing" +) + +// buildFeatureApp returns an App whose FeatureConfig subsystems are all +// populated, used to verify the fine-grained section accessors. +func buildFeatureApp() App { + cfg := App{} + cfg.ApplyFeatureSection(testFeatureConfig()) + return cfg +} + +func TestIgnoreSectionCopiesAndReads(t *testing.T) { + cfg := buildFeatureApp() + got := cfg.IgnoreSection() + if !reflect.DeepEqual(got.IgnoreExtraPatterns, []string{"vendor/**", "tmp/**"}) { + t.Fatalf("IgnoreExtraPatterns = %v", got.IgnoreExtraPatterns) + } + // Mutating the returned copy must not affect the source App. + got.IgnoreExtraPatterns[0] = "mutated" + if cfg.IgnoreExtraPatterns[0] == "mutated" { + t.Fatal("IgnoreSection did not return a defensive copy") + } +} + +func TestStatsSectionReads(t *testing.T) { + cfg := buildFeatureApp() + if got := cfg.StatsSection(); got.StatsWindowMinutes != 15 { + t.Fatalf("StatsWindowMinutes = %d, want 15", got.StatsWindowMinutes) + } +} + +func TestTmuxEditSectionCopies(t *testing.T) { + cfg := buildFeatureApp() + got := cfg.TmuxEditSection() + if got.TmuxEditDefaultAgent != "codex" || len(got.TmuxEditAgents) != 1 { + t.Fatalf("unexpected tmux edit section: %+v", got) + } + got.TmuxEditAgents[0].Name = "mutated" + if cfg.TmuxEditAgents[0].Name == "mutated" { + t.Fatal("TmuxEditSection did not return a defensive copy") + } +} + +func TestTmuxActionSectionCopies(t *testing.T) { + cfg := App{} + cfg.TmuxActionMenu = []TmuxActionMenuEntry{{Kind: "rewrite"}} + got := cfg.TmuxActionSection() + if len(got.TmuxActionMenu) != 1 || got.TmuxActionMenu[0].Kind != "rewrite" { + t.Fatalf("unexpected tmux action section: %+v", got) + } + got.TmuxActionMenu[0].Kind = "mutated" + if cfg.TmuxActionMenu[0].Kind == "mutated" { + t.Fatal("TmuxActionSection did not return a defensive copy") + } +} + +func TestMCPSectionReads(t *testing.T) { + cfg := buildFeatureApp() + got := cfg.MCPSection() + if got.MCPPromptsDir != ".hexai/prompts" || !got.MCPSlashCommandSync || + got.MCPSlashCommandDir != ".hexai/slash" { + t.Fatalf("unexpected MCP section: %+v", got) + } +} diff --git a/internal/appconfig/app_sections.go b/internal/appconfig/app_sections.go index 05b3171..5919db1 100644 --- a/internal/appconfig/app_sections.go +++ b/internal/appconfig/app_sections.go @@ -102,26 +102,18 @@ type PromptConfig struct { TmuxCustomMenuHotkey string `json:"-"` } -// FeatureConfig contains non-LLM feature toggles/integration settings. -// It is embedded in App; fields use json:"-" since features are not exposed via JSON. +// FeatureConfig groups the non-LLM feature subsystems. Rather than a flat +// grab-bag, it now embeds one cohesive struct per subsystem (see +// feature_sections.go). Embedding keeps field promotion intact so existing flat +// access (e.g. cfg.MCPPromptsDir) and the JSON shape are unchanged; the split +// just makes the subsystem boundaries explicit and lets callers depend on a +// single subsystem via the App.*Section accessors. type FeatureConfig struct { - // Stats - StatsWindowMinutes int `json:"-"` - // Ignore: gitignore-aware file filtering for LSP - IgnoreGitignore *bool `json:"-"` - IgnoreExtraPatterns []string `json:"-"` - IgnoreLSPNotify *bool `json:"-"` - // TmuxEdit: popup editor settings for hexai-tmux-edit - TmuxEditPopupWidth string `json:"-"` - TmuxEditPopupHeight string `json:"-"` - TmuxEditDefaultAgent string `json:"-"` - TmuxEditAgents []TmuxEditAgentCfg `json:"-"` - // TmuxAction: configurable main menu for hexai-tmux-action - TmuxActionMenu []TmuxActionMenuEntry `json:"-"` - // MCP: Model Context Protocol server settings - MCPPromptsDir string `json:"-"` // Directory for prompt storage - MCPSlashCommandSync bool `json:"-"` // Enable slash command sync - MCPSlashCommandDir string `json:"-"` // Directory for slash command files + StatsConfig // usage statistics window + IgnoreConfig // gitignore-aware file filtering for LSP + TmuxEditConfig // popup editor settings for hexai-tmux-edit + TmuxActionConfig // configurable main menu for hexai-tmux-action + MCPConfig // Model Context Protocol server settings } // AppSections is the focused split of App into subsystem-specific config groups. diff --git a/internal/appconfig/app_sections_test.go b/internal/appconfig/app_sections_test.go index e26d3f3..bcd1cbe 100644 --- a/internal/appconfig/app_sections_test.go +++ b/internal/appconfig/app_sections_test.go @@ -169,28 +169,34 @@ func testPromptConfig() PromptConfig { func testFeatureConfig() FeatureConfig { return FeatureConfig{ - StatsWindowMinutes: 15, - IgnoreGitignore: sectionBoolPtr(true), - IgnoreExtraPatterns: []string{"vendor/**", "tmp/**"}, - IgnoreLSPNotify: sectionBoolPtr(false), - TmuxEditPopupWidth: "80%", - TmuxEditPopupHeight: "75%", - TmuxEditDefaultAgent: "codex", - TmuxEditAgents: []TmuxEditAgentCfg{{ - Name: "codex", - DisplayName: "Codex", - DetectPattern: "(?i)codex", - SectionPattern: "section", - PromptPattern: "prompt", - StripPatterns: []string{"x", "y"}, - ClearFirst: sectionBoolPtr(true), - ClearKeys: "C-u", - NewlineKeys: "S-Enter", - SubmitKeys: "Enter", - }}, - MCPPromptsDir: ".hexai/prompts", - MCPSlashCommandSync: true, - MCPSlashCommandDir: ".hexai/slash", + StatsConfig: StatsConfig{StatsWindowMinutes: 15}, + IgnoreConfig: IgnoreConfig{ + IgnoreGitignore: sectionBoolPtr(true), + IgnoreExtraPatterns: []string{"vendor/**", "tmp/**"}, + IgnoreLSPNotify: sectionBoolPtr(false), + }, + TmuxEditConfig: TmuxEditConfig{ + TmuxEditPopupWidth: "80%", + TmuxEditPopupHeight: "75%", + TmuxEditDefaultAgent: "codex", + TmuxEditAgents: []TmuxEditAgentCfg{{ + Name: "codex", + DisplayName: "Codex", + DetectPattern: "(?i)codex", + SectionPattern: "section", + PromptPattern: "prompt", + StripPatterns: []string{"x", "y"}, + ClearFirst: sectionBoolPtr(true), + ClearKeys: "C-u", + NewlineKeys: "S-Enter", + SubmitKeys: "Enter", + }}, + }, + MCPConfig: MCPConfig{ + MCPPromptsDir: ".hexai/prompts", + MCPSlashCommandSync: true, + MCPSlashCommandDir: ".hexai/slash", + }, } } diff --git a/internal/appconfig/config_features_test.go b/internal/appconfig/config_features_test.go index 77beb07..2b8c769 100644 --- a/internal/appconfig/config_features_test.go +++ b/internal/appconfig/config_features_test.go @@ -183,13 +183,13 @@ func TestTmuxEditConfig_Merge(t *testing.T) { clearHexaiEnv(t) a := newDefaultConfig() b := App{ - FeatureConfig: FeatureConfig{ + FeatureConfig: FeatureConfig{TmuxEditConfig: TmuxEditConfig{ TmuxEditPopupWidth: "70%", TmuxEditDefaultAgent: "amp", TmuxEditAgents: []TmuxEditAgentCfg{ {Name: "amp", DisplayName: "Amp"}, }, - }, + }}, } a.mergeWith(&b) if a.TmuxEditPopupWidth != "70%" { diff --git a/internal/appconfig/config_load.go b/internal/appconfig/config_load.go index 1e4c6b7..ccbf49f 100644 --- a/internal/appconfig/config_load.go +++ b/internal/appconfig/config_load.go @@ -388,11 +388,11 @@ func applyIgnoreSection(fc *fileConfig, out *App) { if fc.Ignore.Gitignore == nil && len(fc.Ignore.ExtraPatterns) == 0 && fc.Ignore.LSPNotifyIgnored == nil { return } - tmp := App{FeatureConfig: FeatureConfig{ + tmp := App{FeatureConfig: FeatureConfig{IgnoreConfig: IgnoreConfig{ IgnoreGitignore: fc.Ignore.Gitignore, IgnoreExtraPatterns: fc.Ignore.ExtraPatterns, IgnoreLSPNotify: fc.Ignore.LSPNotifyIgnored, - }} + }}} out.mergeBasics(&tmp) } diff --git a/internal/appconfig/config_types.go b/internal/appconfig/config_types.go index 8d50f35..7069b21 100644 --- a/internal/appconfig/config_types.go +++ b/internal/appconfig/config_types.go @@ -106,10 +106,12 @@ func newDefaultConfig() App { }, PromptConfig: defaultPromptConfig(), FeatureConfig: FeatureConfig{ - StatsWindowMinutes: 60, + StatsConfig: StatsConfig{StatsWindowMinutes: 60}, // Ignore: respect .gitignore by default, notify in LSP by default - IgnoreGitignore: boolPtr(true), - IgnoreLSPNotify: boolPtr(true), + IgnoreConfig: IgnoreConfig{ + IgnoreGitignore: boolPtr(true), + IgnoreLSPNotify: boolPtr(true), + }, }, } } diff --git a/internal/appconfig/feature_sections.go b/internal/appconfig/feature_sections.go new file mode 100644 index 0000000..9216400 --- /dev/null +++ b/internal/appconfig/feature_sections.go @@ -0,0 +1,50 @@ +package appconfig + +// This file defines the cohesive per-subsystem config structs that make up +// FeatureConfig. The old FeatureConfig was a grab-bag that mixed five unrelated +// non-LLM subsystems (ignore filtering, stats, tmux popup editor, tmux action +// menu, MCP server). Splitting them into named structs documents the seams +// between subsystems and lets consumers depend on a single subsystem's config +// (via the *Section accessors on App) instead of the whole App God-struct. +// +// Each struct is embedded into FeatureConfig, so Go field promotion keeps the +// historical flat access (e.g. cfg.MCPPromptsDir) working for read sites across +// the codebase. Only composite literals that set these leaf fields directly on +// FeatureConfig had to move to the nested form. + +// IgnoreConfig controls gitignore-aware file filtering for the LSP server. +// Files matching these patterns are skipped for completions and code actions. +type IgnoreConfig struct { + // IgnoreGitignore enables respecting .gitignore entries (default true). + IgnoreGitignore *bool `json:"-"` + // IgnoreExtraPatterns are additional glob patterns to always ignore. + IgnoreExtraPatterns []string `json:"-"` + // IgnoreLSPNotify controls whether the LSP notifies when ignoring a file. + IgnoreLSPNotify *bool `json:"-"` +} + +// StatsConfig holds settings for the usage statistics subsystem. +type StatsConfig struct { + // StatsWindowMinutes is the rolling window (in minutes) used for stats. + StatsWindowMinutes int `json:"-"` +} + +// TmuxEditConfig configures the tmux popup editor feature (hexai-tmux-edit). +type TmuxEditConfig struct { + TmuxEditPopupWidth string `json:"-"` + TmuxEditPopupHeight string `json:"-"` + TmuxEditDefaultAgent string `json:"-"` + TmuxEditAgents []TmuxEditAgentCfg `json:"-"` +} + +// TmuxActionConfig configures the main menu for hexai-tmux-action. +type TmuxActionConfig struct { + TmuxActionMenu []TmuxActionMenuEntry `json:"-"` +} + +// MCPConfig holds Model Context Protocol server settings. +type MCPConfig struct { + MCPPromptsDir string `json:"-"` // Directory for prompt storage + MCPSlashCommandSync bool `json:"-"` // Enable slash command sync + MCPSlashCommandDir string `json:"-"` // Directory for slash command files +} diff --git a/internal/hexaimcp/run.go b/internal/hexaimcp/run.go index 7c487c3..7043ed2 100644 --- a/internal/hexaimcp/run.go +++ b/internal/hexaimcp/run.go @@ -205,7 +205,7 @@ func expandPath(path string) (string, error) { // createSyncer creates a slash command syncer from config. // Returns nil syncer if sync is disabled. func createSyncer(cfg appconfig.App, logger *log.Logger) (*slashcommands.Syncer, error) { - syncer, err := slashcommands.NewSyncer(cfg) + syncer, err := slashcommands.NewSyncer(cfg.MCPSection()) if err != nil { return nil, err } diff --git a/internal/hexaimcp/run_test.go b/internal/hexaimcp/run_test.go index 09f4f87..d567f2e 100644 --- a/internal/hexaimcp/run_test.go +++ b/internal/hexaimcp/run_test.go @@ -113,7 +113,7 @@ func TestGetPromptsDir(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{MCPPromptsDir: tt.cfgValue}, + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{MCPPromptsDir: tt.cfgValue}}, } result, err := getPromptsDir(cfg) @@ -425,7 +425,7 @@ func TestGetPromptsDir_XDGDataHome(t *testing.T) { // TestGetPromptsDir_TildeInConfig verifies tilde expansion for config path. func TestGetPromptsDir_TildeInConfig(t *testing.T) { cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{MCPPromptsDir: "~/my-prompts"}, + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{MCPPromptsDir: "~/my-prompts"}}, } result, err := getPromptsDir(cfg) @@ -450,7 +450,7 @@ func TestGetPromptsDir_TildeInConfig(t *testing.T) { func TestCreateSyncer_Disabled(t *testing.T) { logger := log.New(io.Discard, "", 0) cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{MCPSlashCommandSync: false}, + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{MCPSlashCommandSync: false}}, } syncer, err := createSyncer(cfg, logger) @@ -468,10 +468,10 @@ func TestCreateSyncer_Enabled(t *testing.T) { tmpDir := t.TempDir() logger := log.New(io.Discard, "", 0) cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{ + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{ MCPSlashCommandSync: true, MCPSlashCommandDir: tmpDir, - }, + }}, } syncer, err := createSyncer(cfg, logger) @@ -488,10 +488,10 @@ func TestCreateSyncer_Enabled(t *testing.T) { func TestCreateSyncer_Error(t *testing.T) { logger := log.New(io.Discard, "", 0) cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{ + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{ MCPSlashCommandSync: true, MCPSlashCommandDir: "", - }, + }}, } _, err := createSyncer(cfg, logger) @@ -651,11 +651,11 @@ func TestApplyOverrides(t *testing.T) { t.Run("does not overwrite with zero values", func(t *testing.T) { cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{ + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{ MCPPromptsDir: "/existing/prompts", MCPSlashCommandSync: true, MCPSlashCommandDir: "/existing/cmds", - }, + }}, } overrides := MCPOverrides{} // all zero values applyOverrides(&cfg, overrides) diff --git a/internal/lsp/ignore_test.go b/internal/lsp/ignore_test.go index 31ed828..b1899f2 100644 --- a/internal/lsp/ignore_test.go +++ b/internal/lsp/ignore_test.go @@ -21,7 +21,7 @@ func newIgnoreTestServer(gitRoot string, useGI bool, extra []string, notifyIgnor ChatSuffix: ">", ChatPrefixes: []string{"?", "!", ":", ";"}, }, - FeatureConfig: appconfig.FeatureConfig{IgnoreLSPNotify: notifyIgnored}, + FeatureConfig: appconfig.FeatureConfig{IgnoreConfig: appconfig.IgnoreConfig{IgnoreLSPNotify: notifyIgnored}}, } s := &Server{ logger: log.New(io.Discard, "", 0), diff --git a/internal/slashcommands/syncer.go b/internal/slashcommands/syncer.go index 1268e7b..674b974 100644 --- a/internal/slashcommands/syncer.go +++ b/internal/slashcommands/syncer.go @@ -30,7 +30,8 @@ type Syncer struct { // NewSyncer creates a new syncer and validates the commands directory. // Returns error if directory cannot be created or is not writable. -func NewSyncer(cfg appconfig.App) (*Syncer, error) { +// It depends only on the MCP subsystem config rather than the whole App. +func NewSyncer(cfg appconfig.MCPConfig) (*Syncer, error) { if !cfg.MCPSlashCommandSync { return &Syncer{enabled: false}, nil } diff --git a/internal/slashcommands/syncer_test.go b/internal/slashcommands/syncer_test.go index 01c6d28..b7ffe47 100644 --- a/internal/slashcommands/syncer_test.go +++ b/internal/slashcommands/syncer_test.go @@ -13,10 +13,10 @@ import ( func TestNewSyncer_Disabled(t *testing.T) { cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{MCPSlashCommandSync: false}, + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{MCPSlashCommandSync: false}}, } - syncer, err := NewSyncer(cfg) + syncer, err := NewSyncer(cfg.MCPSection()) if err != nil { t.Fatalf("NewSyncer() with disabled sync failed: %v", err) } @@ -28,13 +28,13 @@ func TestNewSyncer_Disabled(t *testing.T) { func TestNewSyncer_NoDirectory(t *testing.T) { cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{ + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{ MCPSlashCommandSync: true, MCPSlashCommandDir: "", - }, + }}, } - _, err := NewSyncer(cfg) + _, err := NewSyncer(cfg.MCPSection()) if err == nil { t.Error("NewSyncer() should fail when directory is not configured") } @@ -45,13 +45,13 @@ func TestNewSyncer_CreatesDirectory(t *testing.T) { testDir := filepath.Join(tmpDir, "test-commands") cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{ + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{ MCPSlashCommandSync: true, MCPSlashCommandDir: testDir, - }, + }}, } - syncer, err := NewSyncer(cfg) + syncer, err := NewSyncer(cfg.MCPSection()) if err != nil { t.Fatalf("NewSyncer() failed: %v", err) } @@ -75,13 +75,13 @@ func TestNewSyncer_ExpandsHomeDirectory(t *testing.T) { defer os.Setenv("HOME", home) cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{ + FeatureConfig: appconfig.FeatureConfig{MCPConfig: appconfig.MCPConfig{ MCPSlashCommandSync: true, MCPSlashCommandDir: "~/test-commands", - }, + }}, } - syncer, err := NewSyncer(cfg) + syncer, err := NewSyncer(cfg.MCPSection()) if err != nil { t.Fatalf("NewSyncer() failed: %v", err) } diff --git a/internal/tmuxedit/run_test.go b/internal/tmuxedit/run_test.go index e8ca6c1..f528b95 100644 --- a/internal/tmuxedit/run_test.go +++ b/internal/tmuxedit/run_test.go @@ -167,10 +167,10 @@ func TestRunWithConfig_CustomDimensions(t *testing.T) { sendKeys = func(string, ...string) error { return nil } cfg := appconfig.App{ - FeatureConfig: appconfig.FeatureConfig{ + FeatureConfig: appconfig.FeatureConfig{TmuxEditConfig: appconfig.TmuxEditConfig{ TmuxEditPopupWidth: "90%", TmuxEditPopupHeight: "85%", - }, + }}, } err := runWithConfig(Options{}, cfg) if err != nil { -- cgit v1.2.3