detached f0af95e

refactor(config): extract DiffFormatter interface and clean up migration use case

commit
f0af95e44d1540f8cd0b8195c40cdd5a86bf0694
author
brice · · 2025-12-28 07:41 UTC
parent
47613bd
signature
unsigned
8 files · +179 -124
M .mockery.yaml +1 -0
@@ -10,6 +10,7 @@ packages:
1010 interfaces:
1111 ConfigMigrator: {}
1212 DatabaseProvider: {}
13+ DiffFormatter: {}
1314 SessionLogger: {}
1415 SessionSpawner: {}
1516 TabListProvider: {}
@@ -10,6 +10,7 @@ packages:
10 interfaces:10 interfaces:
11 ConfigMigrator: {}11 ConfigMigrator: {}
12 DatabaseProvider: {}12 DatabaseProvider: {}
13+ DiffFormatter: {}
13 SessionLogger: {}14 SessionLogger: {}
14 SessionSpawner: {}15 SessionSpawner: {}
15 TabListProvider: {}16 TabListProvider: {}
M internal/application/port/config_migrator.go +6 -4
@@ -28,8 +28,6 @@ const (
2828 KeyChangeRemoved
2929 // KeyChangeRenamed indicates a key was renamed (detected via similarity).
3030 KeyChangeRenamed
31- // KeyChangeConsolidated indicates multiple keys were merged into one.
32- KeyChangeConsolidated
3331 )
3432
3533 // String returns a display symbol for the change type.
@@ -41,8 +39,6 @@ func (t KeyChangeType) String() string {
4139 return "-"
4240 case KeyChangeRenamed:
4341 return "~"
44- case KeyChangeConsolidated:
45- return ">"
4642 default:
4743 return "?"
4844 }
@@ -74,3 +70,9 @@ type ConfigMigrator interface {
7470 // GetKeyInfo returns detailed information about a config key.
7571 GetKeyInfo(key string) KeyInfo
7672 }
73+
74+// DiffFormatter formats config changes for display.
75+type DiffFormatter interface {
76+ // FormatChangesAsDiff returns changes formatted as a diff-like string for display.
77+ FormatChangesAsDiff(changes []KeyChange) string
78+}
@@ -28,8 +28,6 @@ const (
28 KeyChangeRemoved28 KeyChangeRemoved
29 // KeyChangeRenamed indicates a key was renamed (detected via similarity).29 // KeyChangeRenamed indicates a key was renamed (detected via similarity).
30 KeyChangeRenamed30 KeyChangeRenamed
31- // KeyChangeConsolidated indicates multiple keys were merged into one.
32- KeyChangeConsolidated
33 )31 )
34 32
35 // String returns a display symbol for the change type.33 // String returns a display symbol for the change type.
@@ -41,8 +39,6 @@ func (t KeyChangeType) String() string {
41 return "-"39 return "-"
42 case KeyChangeRenamed:40 case KeyChangeRenamed:
43 return "~"41 return "~"
44- case KeyChangeConsolidated:
45- return ">"
46 default:42 default:
47 return "?"43 return "?"
48 }44 }
@@ -74,3 +70,9 @@ type ConfigMigrator interface {
74 // GetKeyInfo returns detailed information about a config key.70 // GetKeyInfo returns detailed information about a config key.
75 GetKeyInfo(key string) KeyInfo71 GetKeyInfo(key string) KeyInfo
76 }72 }
73+
74+// DiffFormatter formats config changes for display.
75+type DiffFormatter interface {
76+ // FormatChangesAsDiff returns changes formatted as a diff-like string for display.
77+ FormatChangesAsDiff(changes []KeyChange) string
78+}
M internal/application/port/mocks/mocks.go +78 -0
@@ -257,6 +257,84 @@ func (_c *MockConfigMigrator_Migrate_Call) RunAndReturn(run func() ([]string, er
257257 return _c
258258 }
259259
260+// NewMockDiffFormatter creates a new instance of MockDiffFormatter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
261+// The first argument is typically a *testing.T value.
262+func NewMockDiffFormatter(t interface {
263+ mock.TestingT
264+ Cleanup(func())
265+}) *MockDiffFormatter {
266+ mock := &MockDiffFormatter{}
267+ mock.Mock.Test(t)
268+
269+ t.Cleanup(func() { mock.AssertExpectations(t) })
270+
271+ return mock
272+}
273+
274+// MockDiffFormatter is an autogenerated mock type for the DiffFormatter type
275+type MockDiffFormatter struct {
276+ mock.Mock
277+}
278+
279+type MockDiffFormatter_Expecter struct {
280+ mock *mock.Mock
281+}
282+
283+func (_m *MockDiffFormatter) EXPECT() *MockDiffFormatter_Expecter {
284+ return &MockDiffFormatter_Expecter{mock: &_m.Mock}
285+}
286+
287+// FormatChangesAsDiff provides a mock function for the type MockDiffFormatter
288+func (_mock *MockDiffFormatter) FormatChangesAsDiff(changes []port.KeyChange) string {
289+ ret := _mock.Called(changes)
290+
291+ if len(ret) == 0 {
292+ panic("no return value specified for FormatChangesAsDiff")
293+ }
294+
295+ var r0 string
296+ if returnFunc, ok := ret.Get(0).(func([]port.KeyChange) string); ok {
297+ r0 = returnFunc(changes)
298+ } else {
299+ r0 = ret.Get(0).(string)
300+ }
301+ return r0
302+}
303+
304+// MockDiffFormatter_FormatChangesAsDiff_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FormatChangesAsDiff'
305+type MockDiffFormatter_FormatChangesAsDiff_Call struct {
306+ *mock.Call
307+}
308+
309+// FormatChangesAsDiff is a helper method to define mock.On call
310+// - changes []port.KeyChange
311+func (_e *MockDiffFormatter_Expecter) FormatChangesAsDiff(changes interface{}) *MockDiffFormatter_FormatChangesAsDiff_Call {
312+ return &MockDiffFormatter_FormatChangesAsDiff_Call{Call: _e.mock.On("FormatChangesAsDiff", changes)}
313+}
314+
315+func (_c *MockDiffFormatter_FormatChangesAsDiff_Call) Run(run func(changes []port.KeyChange)) *MockDiffFormatter_FormatChangesAsDiff_Call {
316+ _c.Call.Run(func(args mock.Arguments) {
317+ var arg0 []port.KeyChange
318+ if args[0] != nil {
319+ arg0 = args[0].([]port.KeyChange)
320+ }
321+ run(
322+ arg0,
323+ )
324+ })
325+ return _c
326+}
327+
328+func (_c *MockDiffFormatter_FormatChangesAsDiff_Call) Return(s string) *MockDiffFormatter_FormatChangesAsDiff_Call {
329+ _c.Call.Return(s)
330+ return _c
331+}
332+
333+func (_c *MockDiffFormatter_FormatChangesAsDiff_Call) RunAndReturn(run func(changes []port.KeyChange) string) *MockDiffFormatter_FormatChangesAsDiff_Call {
334+ _c.Call.Return(run)
335+ return _c
336+}
337+
260338 // NewMockDatabaseProvider creates a new instance of MockDatabaseProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
261339 // The first argument is typically a *testing.T value.
262340 func NewMockDatabaseProvider(t interface {
@@ -257,6 +257,84 @@ func (_c *MockConfigMigrator_Migrate_Call) RunAndReturn(run func() ([]string, er
257 return _c257 return _c
258 }258 }
259 259
260+// NewMockDiffFormatter creates a new instance of MockDiffFormatter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
261+// The first argument is typically a *testing.T value.
262+func NewMockDiffFormatter(t interface {
263+ mock.TestingT
264+ Cleanup(func())
265+}) *MockDiffFormatter {
266+ mock := &MockDiffFormatter{}
267+ mock.Mock.Test(t)
268+
269+ t.Cleanup(func() { mock.AssertExpectations(t) })
270+
271+ return mock
272+}
273+
274+// MockDiffFormatter is an autogenerated mock type for the DiffFormatter type
275+type MockDiffFormatter struct {
276+ mock.Mock
277+}
278+
279+type MockDiffFormatter_Expecter struct {
280+ mock *mock.Mock
281+}
282+
283+func (_m *MockDiffFormatter) EXPECT() *MockDiffFormatter_Expecter {
284+ return &MockDiffFormatter_Expecter{mock: &_m.Mock}
285+}
286+
287+// FormatChangesAsDiff provides a mock function for the type MockDiffFormatter
288+func (_mock *MockDiffFormatter) FormatChangesAsDiff(changes []port.KeyChange) string {
289+ ret := _mock.Called(changes)
290+
291+ if len(ret) == 0 {
292+ panic("no return value specified for FormatChangesAsDiff")
293+ }
294+
295+ var r0 string
296+ if returnFunc, ok := ret.Get(0).(func([]port.KeyChange) string); ok {
297+ r0 = returnFunc(changes)
298+ } else {
299+ r0 = ret.Get(0).(string)
300+ }
301+ return r0
302+}
303+
304+// MockDiffFormatter_FormatChangesAsDiff_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FormatChangesAsDiff'
305+type MockDiffFormatter_FormatChangesAsDiff_Call struct {
306+ *mock.Call
307+}
308+
309+// FormatChangesAsDiff is a helper method to define mock.On call
310+// - changes []port.KeyChange
311+func (_e *MockDiffFormatter_Expecter) FormatChangesAsDiff(changes interface{}) *MockDiffFormatter_FormatChangesAsDiff_Call {
312+ return &MockDiffFormatter_FormatChangesAsDiff_Call{Call: _e.mock.On("FormatChangesAsDiff", changes)}
313+}
314+
315+func (_c *MockDiffFormatter_FormatChangesAsDiff_Call) Run(run func(changes []port.KeyChange)) *MockDiffFormatter_FormatChangesAsDiff_Call {
316+ _c.Call.Run(func(args mock.Arguments) {
317+ var arg0 []port.KeyChange
318+ if args[0] != nil {
319+ arg0 = args[0].([]port.KeyChange)
320+ }
321+ run(
322+ arg0,
323+ )
324+ })
325+ return _c
326+}
327+
328+func (_c *MockDiffFormatter_FormatChangesAsDiff_Call) Return(s string) *MockDiffFormatter_FormatChangesAsDiff_Call {
329+ _c.Call.Return(s)
330+ return _c
331+}
332+
333+func (_c *MockDiffFormatter_FormatChangesAsDiff_Call) RunAndReturn(run func(changes []port.KeyChange) string) *MockDiffFormatter_FormatChangesAsDiff_Call {
334+ _c.Call.Return(run)
335+ return _c
336+}
337+
260 // NewMockDatabaseProvider creates a new instance of MockDatabaseProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.338 // NewMockDatabaseProvider creates a new instance of MockDatabaseProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
261 // The first argument is typically a *testing.T value.339 // The first argument is typically a *testing.T value.
262 func NewMockDatabaseProvider(t interface {340 func NewMockDatabaseProvider(t interface {
M internal/application/usecase/migrate_config.go +6 -32
@@ -2,8 +2,6 @@ package usecase
22
33 import (
44 "context"
5- "fmt"
6- "strings"
75
86 "github.com/bnema/dumber/internal/application/port"
97 "github.com/bnema/dumber/internal/logging"
@@ -48,13 +46,15 @@ type MigrateConfigOutput struct {
4846
4947 // MigrateConfigUseCase handles config migration operations.
5048 type MigrateConfigUseCase struct {
51- migrator port.ConfigMigrator
49+ migrator port.ConfigMigrator
50+ diffFormatter port.DiffFormatter
5251 }
5352
5453 // NewMigrateConfigUseCase creates a new migrate config use case.
55-func NewMigrateConfigUseCase(migrator port.ConfigMigrator) *MigrateConfigUseCase {
54+func NewMigrateConfigUseCase(migrator port.ConfigMigrator, diffFormatter port.DiffFormatter) *MigrateConfigUseCase {
5655 return &MigrateConfigUseCase{
57- migrator: migrator,
56+ migrator: migrator,
57+ diffFormatter: diffFormatter,
5858 }
5959 }
6060
@@ -122,7 +122,7 @@ func (uc *MigrateConfigUseCase) DetectChanges(ctx context.Context, _ DetectChang
122122 return &DetectChangesOutput{
123123 HasChanges: true,
124124 Changes: changes,
125- DiffText: formatChangesAsDiff(changes),
125+ DiffText: uc.diffFormatter.FormatChangesAsDiff(changes),
126126 }, nil
127127 }
128128
@@ -161,29 +161,3 @@ func (uc *MigrateConfigUseCase) Execute(ctx context.Context, _ MigrateConfigInpu
161161 ConfigFile: checkResult.ConfigFile,
162162 }, nil
163163 }
164-
165-// formatChangesAsDiff returns changes formatted as a diff for display.
166-func formatChangesAsDiff(changes []port.KeyChange) string {
167- if len(changes) == 0 {
168- return "No changes detected."
169- }
170-
171- var sb strings.Builder
172- sb.WriteString("Config migration changes:\n\n")
173-
174- for _, change := range changes {
175- switch change.Type {
176- case port.KeyChangeAdded:
177- sb.WriteString(fmt.Sprintf(" + %s = %s\n", change.NewKey, change.NewValue))
178- case port.KeyChangeRemoved:
179- sb.WriteString(fmt.Sprintf(" - %s = %s (deprecated)\n", change.OldKey, change.OldValue))
180- case port.KeyChangeRenamed:
181- sb.WriteString(fmt.Sprintf(" ~ %s -> %s\n", change.OldKey, change.NewKey))
182- sb.WriteString(fmt.Sprintf(" (value: %s)\n", change.OldValue))
183- case port.KeyChangeConsolidated:
184- sb.WriteString(fmt.Sprintf(" > %s -> %s\n", change.OldKey, change.NewKey))
185- }
186- }
187-
188- return sb.String()
189-}
@@ -2,8 +2,6 @@ package usecase
2 2
3 import (3 import (
4 "context"4 "context"
5- "fmt"
6- "strings"
7 5
8 "github.com/bnema/dumber/internal/application/port"6 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/logging"7 "github.com/bnema/dumber/internal/logging"
@@ -48,13 +46,15 @@ type MigrateConfigOutput struct {
48 46
49 // MigrateConfigUseCase handles config migration operations.47 // MigrateConfigUseCase handles config migration operations.
50 type MigrateConfigUseCase struct {48 type MigrateConfigUseCase struct {
51- migrator port.ConfigMigrator49+ migrator port.ConfigMigrator
50+ diffFormatter port.DiffFormatter
52 }51 }
53 52
54 // NewMigrateConfigUseCase creates a new migrate config use case.53 // NewMigrateConfigUseCase creates a new migrate config use case.
55-func NewMigrateConfigUseCase(migrator port.ConfigMigrator) *MigrateConfigUseCase {54+func NewMigrateConfigUseCase(migrator port.ConfigMigrator, diffFormatter port.DiffFormatter) *MigrateConfigUseCase {
56 return &MigrateConfigUseCase{55 return &MigrateConfigUseCase{
57- migrator: migrator,56+ migrator: migrator,
57+ diffFormatter: diffFormatter,
58 }58 }
59 }59 }
60 60
@@ -122,7 +122,7 @@ func (uc *MigrateConfigUseCase) DetectChanges(ctx context.Context, _ DetectChang
122 return &DetectChangesOutput{122 return &DetectChangesOutput{
123 HasChanges: true,123 HasChanges: true,
124 Changes: changes,124 Changes: changes,
125- DiffText: formatChangesAsDiff(changes),125+ DiffText: uc.diffFormatter.FormatChangesAsDiff(changes),
126 }, nil126 }, nil
127 }127 }
128 128
@@ -161,29 +161,3 @@ func (uc *MigrateConfigUseCase) Execute(ctx context.Context, _ MigrateConfigInpu
161 ConfigFile: checkResult.ConfigFile,161 ConfigFile: checkResult.ConfigFile,
162 }, nil162 }, nil
163 }163 }
164-
165-// formatChangesAsDiff returns changes formatted as a diff for display.
166-func formatChangesAsDiff(changes []port.KeyChange) string {
167- if len(changes) == 0 {
168- return "No changes detected."
169- }
170-
171- var sb strings.Builder
172- sb.WriteString("Config migration changes:\n\n")
173-
174- for _, change := range changes {
175- switch change.Type {
176- case port.KeyChangeAdded:
177- sb.WriteString(fmt.Sprintf(" + %s = %s\n", change.NewKey, change.NewValue))
178- case port.KeyChangeRemoved:
179- sb.WriteString(fmt.Sprintf(" - %s = %s (deprecated)\n", change.OldKey, change.OldValue))
180- case port.KeyChangeRenamed:
181- sb.WriteString(fmt.Sprintf(" ~ %s -> %s\n", change.OldKey, change.NewKey))
182- sb.WriteString(fmt.Sprintf(" (value: %s)\n", change.OldValue))
183- case port.KeyChangeConsolidated:
184- sb.WriteString(fmt.Sprintf(" > %s -> %s\n", change.OldKey, change.NewKey))
185- }
186- }
187-
188- return sb.String()
189-}
M internal/application/usecase/migrate_config_test.go +44 -60
@@ -15,9 +15,10 @@ import (
1515
1616 func TestMigrateConfigUseCase_Check_NoMigrationNeeded(t *testing.T) {
1717 mockMigrator := mocks.NewMockConfigMigrator(t)
18+ mockFormatter := mocks.NewMockDiffFormatter(t)
1819 mockMigrator.EXPECT().CheckMigration().Return(nil, nil)
1920
20- uc := NewMigrateConfigUseCase(mockMigrator)
21+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
2122 ctx := context.Background()
2223
2324 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -29,6 +30,7 @@ func TestMigrateConfigUseCase_Check_NoMigrationNeeded(t *testing.T) {
2930
3031 func TestMigrateConfigUseCase_Check_MigrationNeeded(t *testing.T) {
3132 mockMigrator := mocks.NewMockConfigMigrator(t)
33+ mockFormatter := mocks.NewMockDiffFormatter(t)
3234
3335 migrationResult := &port.MigrationResult{
3436 MissingKeys: []string{"key1", "key2"},
@@ -48,7 +50,7 @@ func TestMigrateConfigUseCase_Check_MigrationNeeded(t *testing.T) {
4850 DefaultValue: `"default"`,
4951 })
5052
51- uc := NewMigrateConfigUseCase(mockMigrator)
53+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
5254 ctx := context.Background()
5355
5456 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -67,10 +69,11 @@ func TestMigrateConfigUseCase_Check_MigrationNeeded(t *testing.T) {
6769
6870 func TestMigrateConfigUseCase_Check_Error(t *testing.T) {
6971 mockMigrator := mocks.NewMockConfigMigrator(t)
72+ mockFormatter := mocks.NewMockDiffFormatter(t)
7073 expectedErr := errors.New("check failed")
7174 mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr)
7275
73- uc := NewMigrateConfigUseCase(mockMigrator)
76+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
7477 ctx := context.Background()
7578
7679 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -82,9 +85,10 @@ func TestMigrateConfigUseCase_Check_Error(t *testing.T) {
8285
8386 func TestMigrateConfigUseCase_Execute_NoMigrationNeeded(t *testing.T) {
8487 mockMigrator := mocks.NewMockConfigMigrator(t)
88+ mockFormatter := mocks.NewMockDiffFormatter(t)
8589 mockMigrator.EXPECT().CheckMigration().Return(nil, nil)
8690
87- uc := NewMigrateConfigUseCase(mockMigrator)
91+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
8892 ctx := context.Background()
8993
9094 result, err := uc.Execute(ctx, MigrateConfigInput{})
@@ -95,6 +99,7 @@ func TestMigrateConfigUseCase_Execute_NoMigrationNeeded(t *testing.T) {
9599
96100 func TestMigrateConfigUseCase_Execute_Success(t *testing.T) {
97101 mockMigrator := mocks.NewMockConfigMigrator(t)
102+ mockFormatter := mocks.NewMockDiffFormatter(t)
98103
99104 migrationResult := &port.MigrationResult{
100105 MissingKeys: []string{"key1", "key2"},
@@ -103,7 +108,7 @@ func TestMigrateConfigUseCase_Execute_Success(t *testing.T) {
103108 mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil)
104109 mockMigrator.EXPECT().Migrate().Return([]string{"key1", "key2"}, nil)
105110
106- uc := NewMigrateConfigUseCase(mockMigrator)
111+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
107112 ctx := context.Background()
108113
109114 result, err := uc.Execute(ctx, MigrateConfigInput{})
@@ -115,6 +120,7 @@ func TestMigrateConfigUseCase_Execute_Success(t *testing.T) {
115120
116121 func TestMigrateConfigUseCase_Execute_MigrateError(t *testing.T) {
117122 mockMigrator := mocks.NewMockConfigMigrator(t)
123+ mockFormatter := mocks.NewMockDiffFormatter(t)
118124
119125 migrationResult := &port.MigrationResult{
120126 MissingKeys: []string{"key1"},
@@ -125,7 +131,7 @@ func TestMigrateConfigUseCase_Execute_MigrateError(t *testing.T) {
125131 expectedErr := errors.New("migrate failed")
126132 mockMigrator.EXPECT().Migrate().Return(nil, expectedErr)
127133
128- uc := NewMigrateConfigUseCase(mockMigrator)
134+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
129135 ctx := context.Background()
130136
131137 result, err := uc.Execute(ctx, MigrateConfigInput{})
@@ -137,11 +143,12 @@ func TestMigrateConfigUseCase_Execute_MigrateError(t *testing.T) {
137143
138144 func TestMigrateConfigUseCase_Execute_CheckError(t *testing.T) {
139145 mockMigrator := mocks.NewMockConfigMigrator(t)
146+ mockFormatter := mocks.NewMockDiffFormatter(t)
140147
141148 expectedErr := errors.New("check failed")
142149 mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr)
143150
144- uc := NewMigrateConfigUseCase(mockMigrator)
151+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
145152 ctx := context.Background()
146153
147154 result, err := uc.Execute(ctx, MigrateConfigInput{})
@@ -153,6 +160,7 @@ func TestMigrateConfigUseCase_Execute_CheckError(t *testing.T) {
153160
154161 func TestMigrateConfigUseCase_Check_EmptyMissingKeys(t *testing.T) {
155162 mockMigrator := mocks.NewMockConfigMigrator(t)
163+ mockFormatter := mocks.NewMockDiffFormatter(t)
156164
157165 // Return a result with empty missing keys
158166 migrationResult := &port.MigrationResult{
@@ -161,7 +169,7 @@ func TestMigrateConfigUseCase_Check_EmptyMissingKeys(t *testing.T) {
161169 }
162170 mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil)
163171
164- uc := NewMigrateConfigUseCase(mockMigrator)
172+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
165173 ctx := context.Background()
166174
167175 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -173,15 +181,18 @@ func TestMigrateConfigUseCase_Check_EmptyMissingKeys(t *testing.T) {
173181
174182 func TestNewMigrateConfigUseCase(t *testing.T) {
175183 mockMigrator := mocks.NewMockConfigMigrator(t)
176- uc := NewMigrateConfigUseCase(mockMigrator)
184+ mockFormatter := mocks.NewMockDiffFormatter(t)
185+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
177186
178187 assert.NotNil(t, uc)
179188 assert.Equal(t, mockMigrator, uc.migrator)
189+ assert.Equal(t, mockFormatter, uc.diffFormatter)
180190 }
181191
182192 // Ensure mock expectations are set up correctly
183193 func TestMigrateConfigUseCase_MockExpectations(t *testing.T) {
184194 mockMigrator := mocks.NewMockConfigMigrator(t)
195+ mockFormatter := mocks.NewMockDiffFormatter(t)
185196
186197 // Set up expectations with mock.Anything for flexibility
187198 mockMigrator.On("CheckMigration").Return(&port.MigrationResult{
@@ -195,7 +206,7 @@ func TestMigrateConfigUseCase_MockExpectations(t *testing.T) {
195206 DefaultValue: `"test"`,
196207 }).Once()
197208
198- uc := NewMigrateConfigUseCase(mockMigrator)
209+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
199210 ctx := context.Background()
200211
201212 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -207,9 +218,10 @@ func TestMigrateConfigUseCase_MockExpectations(t *testing.T) {
207218
208219 func TestMigrateConfigUseCase_DetectChanges_NoChanges(t *testing.T) {
209220 mockMigrator := mocks.NewMockConfigMigrator(t)
221+ mockFormatter := mocks.NewMockDiffFormatter(t)
210222 mockMigrator.EXPECT().DetectChanges().Return(nil, nil)
211223
212- uc := NewMigrateConfigUseCase(mockMigrator)
224+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
213225 ctx := context.Background()
214226
215227 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -222,6 +234,7 @@ func TestMigrateConfigUseCase_DetectChanges_NoChanges(t *testing.T) {
222234
223235 func TestMigrateConfigUseCase_DetectChanges_WithAddedKey(t *testing.T) {
224236 mockMigrator := mocks.NewMockConfigMigrator(t)
237+ mockFormatter := mocks.NewMockDiffFormatter(t)
225238
226239 changes := []port.KeyChange{
227240 {
@@ -231,8 +244,9 @@ func TestMigrateConfigUseCase_DetectChanges_WithAddedKey(t *testing.T) {
231244 },
232245 }
233246 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)
247+ mockFormatter.EXPECT().FormatChangesAsDiff(changes).Return("Config migration changes:\n\n + workspace.styling.mode_indicator_toaster_enabled = true\n")
234248
235- uc := NewMigrateConfigUseCase(mockMigrator)
249+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
236250 ctx := context.Background()
237251
238252 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -245,6 +259,7 @@ func TestMigrateConfigUseCase_DetectChanges_WithAddedKey(t *testing.T) {
245259
246260 func TestMigrateConfigUseCase_DetectChanges_WithRenamedKey(t *testing.T) {
247261 mockMigrator := mocks.NewMockConfigMigrator(t)
262+ mockFormatter := mocks.NewMockDiffFormatter(t)
248263
249264 changes := []port.KeyChange{
250265 {
@@ -255,8 +270,9 @@ func TestMigrateConfigUseCase_DetectChanges_WithRenamedKey(t *testing.T) {
255270 },
256271 }
257272 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)
273+ mockFormatter.EXPECT().FormatChangesAsDiff(changes).Return("Config migration changes:\n\n ~ workspace.styling.pane_mode_border_color -> workspace.styling.pane_mode_color\n (value: \"#4A90E2\")\n")
258274
259- uc := NewMigrateConfigUseCase(mockMigrator)
275+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
260276 ctx := context.Background()
261277
262278 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -269,6 +285,7 @@ func TestMigrateConfigUseCase_DetectChanges_WithRenamedKey(t *testing.T) {
269285
270286 func TestMigrateConfigUseCase_DetectChanges_WithRemovedKey(t *testing.T) {
271287 mockMigrator := mocks.NewMockConfigMigrator(t)
288+ mockFormatter := mocks.NewMockDiffFormatter(t)
272289
273290 changes := []port.KeyChange{
274291 {
@@ -278,8 +295,9 @@ func TestMigrateConfigUseCase_DetectChanges_WithRemovedKey(t *testing.T) {
278295 },
279296 }
280297 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)
298+ mockFormatter.EXPECT().FormatChangesAsDiff(changes).Return("Config migration changes:\n\n - workspace.styling.pane_mode_border_width = 4 (deprecated)\n")
281299
282- uc := NewMigrateConfigUseCase(mockMigrator)
300+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
283301 ctx := context.Background()
284302
285303 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -293,10 +311,11 @@ func TestMigrateConfigUseCase_DetectChanges_WithRemovedKey(t *testing.T) {
293311
294312 func TestMigrateConfigUseCase_DetectChanges_Error(t *testing.T) {
295313 mockMigrator := mocks.NewMockConfigMigrator(t)
314+ mockFormatter := mocks.NewMockDiffFormatter(t)
296315 expectedErr := errors.New("detect changes failed")
297316 mockMigrator.EXPECT().DetectChanges().Return(nil, expectedErr)
298317
299- uc := NewMigrateConfigUseCase(mockMigrator)
318+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
300319 ctx := context.Background()
301320
302321 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -308,6 +327,7 @@ func TestMigrateConfigUseCase_DetectChanges_Error(t *testing.T) {
308327
309328 func TestMigrateConfigUseCase_DetectChanges_MultipleChanges(t *testing.T) {
310329 mockMigrator := mocks.NewMockConfigMigrator(t)
330+ mockFormatter := mocks.NewMockDiffFormatter(t)
311331
312332 changes := []port.KeyChange{
313333 {
@@ -328,8 +348,15 @@ func TestMigrateConfigUseCase_DetectChanges_MultipleChanges(t *testing.T) {
328348 },
329349 }
330350 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)
331-
332- uc := NewMigrateConfigUseCase(mockMigrator)
351+ mockFormatter.EXPECT().FormatChangesAsDiff(changes).Return(
352+ "Config migration changes:\n\n" +
353+ " + workspace.styling.mode_border_width = 4\n" +
354+ " ~ workspace.styling.pane_mode_border_color -> workspace.styling.pane_mode_color\n" +
355+ " (value: \"#4A90E2\")\n" +
356+ " - workspace.styling.pane_mode_border_width = 4 (deprecated)\n",
357+ )
358+
359+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
333360 ctx := context.Background()
334361
335362 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -343,46 +370,3 @@ func TestMigrateConfigUseCase_DetectChanges_MultipleChanges(t *testing.T) {
343370 assert.Contains(t, result.DiffText, "~ workspace.styling.pane_mode_border_color")
344371 assert.Contains(t, result.DiffText, "- workspace.styling.pane_mode_border_width")
345372 }
346-
347-func TestFormatChangesAsDiff_EmptyChanges(t *testing.T) {
348- result := formatChangesAsDiff(nil)
349- assert.Equal(t, "No changes detected.", result)
350-
351- result = formatChangesAsDiff([]port.KeyChange{})
352- assert.Equal(t, "No changes detected.", result)
353-}
354-
355-func TestFormatChangesAsDiff_AllChangeTypes(t *testing.T) {
356- changes := []port.KeyChange{
357- {
358- Type: port.KeyChangeAdded,
359- NewKey: "new.key",
360- NewValue: `"value"`,
361- },
362- {
363- Type: port.KeyChangeRemoved,
364- OldKey: "old.key",
365- OldValue: `"old_value"`,
366- },
367- {
368- Type: port.KeyChangeRenamed,
369- OldKey: "renamed.old",
370- NewKey: "renamed.new",
371- OldValue: `"renamed_value"`,
372- },
373- {
374- Type: port.KeyChangeConsolidated,
375- OldKey: "consolidated.old",
376- NewKey: "consolidated.new",
377- },
378- }
379-
380- result := formatChangesAsDiff(changes)
381-
382- assert.Contains(t, result, "Config migration changes:")
383- assert.Contains(t, result, `+ new.key = "value"`)
384- assert.Contains(t, result, `- old.key = "old_value" (deprecated)`)
385- assert.Contains(t, result, "~ renamed.old -> renamed.new")
386- assert.Contains(t, result, `(value: "renamed_value")`)
387- assert.Contains(t, result, "> consolidated.old -> consolidated.new")
388-}
@@ -15,9 +15,10 @@ import (
15 15
16 func TestMigrateConfigUseCase_Check_NoMigrationNeeded(t *testing.T) {16 func TestMigrateConfigUseCase_Check_NoMigrationNeeded(t *testing.T) {
17 mockMigrator := mocks.NewMockConfigMigrator(t)17 mockMigrator := mocks.NewMockConfigMigrator(t)
18+ mockFormatter := mocks.NewMockDiffFormatter(t)
18 mockMigrator.EXPECT().CheckMigration().Return(nil, nil)19 mockMigrator.EXPECT().CheckMigration().Return(nil, nil)
19 20
20- uc := NewMigrateConfigUseCase(mockMigrator)21+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
21 ctx := context.Background()22 ctx := context.Background()
22 23
23 result, err := uc.Check(ctx, CheckConfigMigrationInput{})24 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -29,6 +30,7 @@ func TestMigrateConfigUseCase_Check_NoMigrationNeeded(t *testing.T) {
29 30
30 func TestMigrateConfigUseCase_Check_MigrationNeeded(t *testing.T) {31 func TestMigrateConfigUseCase_Check_MigrationNeeded(t *testing.T) {
31 mockMigrator := mocks.NewMockConfigMigrator(t)32 mockMigrator := mocks.NewMockConfigMigrator(t)
33+ mockFormatter := mocks.NewMockDiffFormatter(t)
32 34
33 migrationResult := &port.MigrationResult{35 migrationResult := &port.MigrationResult{
34 MissingKeys: []string{"key1", "key2"},36 MissingKeys: []string{"key1", "key2"},
@@ -48,7 +50,7 @@ func TestMigrateConfigUseCase_Check_MigrationNeeded(t *testing.T) {
48 DefaultValue: `"default"`,50 DefaultValue: `"default"`,
49 })51 })
50 52
51- uc := NewMigrateConfigUseCase(mockMigrator)53+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
52 ctx := context.Background()54 ctx := context.Background()
53 55
54 result, err := uc.Check(ctx, CheckConfigMigrationInput{})56 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -67,10 +69,11 @@ func TestMigrateConfigUseCase_Check_MigrationNeeded(t *testing.T) {
67 69
68 func TestMigrateConfigUseCase_Check_Error(t *testing.T) {70 func TestMigrateConfigUseCase_Check_Error(t *testing.T) {
69 mockMigrator := mocks.NewMockConfigMigrator(t)71 mockMigrator := mocks.NewMockConfigMigrator(t)
72+ mockFormatter := mocks.NewMockDiffFormatter(t)
70 expectedErr := errors.New("check failed")73 expectedErr := errors.New("check failed")
71 mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr)74 mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr)
72 75
73- uc := NewMigrateConfigUseCase(mockMigrator)76+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
74 ctx := context.Background()77 ctx := context.Background()
75 78
76 result, err := uc.Check(ctx, CheckConfigMigrationInput{})79 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -82,9 +85,10 @@ func TestMigrateConfigUseCase_Check_Error(t *testing.T) {
82 85
83 func TestMigrateConfigUseCase_Execute_NoMigrationNeeded(t *testing.T) {86 func TestMigrateConfigUseCase_Execute_NoMigrationNeeded(t *testing.T) {
84 mockMigrator := mocks.NewMockConfigMigrator(t)87 mockMigrator := mocks.NewMockConfigMigrator(t)
88+ mockFormatter := mocks.NewMockDiffFormatter(t)
85 mockMigrator.EXPECT().CheckMigration().Return(nil, nil)89 mockMigrator.EXPECT().CheckMigration().Return(nil, nil)
86 90
87- uc := NewMigrateConfigUseCase(mockMigrator)91+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
88 ctx := context.Background()92 ctx := context.Background()
89 93
90 result, err := uc.Execute(ctx, MigrateConfigInput{})94 result, err := uc.Execute(ctx, MigrateConfigInput{})
@@ -95,6 +99,7 @@ func TestMigrateConfigUseCase_Execute_NoMigrationNeeded(t *testing.T) {
95 99
96 func TestMigrateConfigUseCase_Execute_Success(t *testing.T) {100 func TestMigrateConfigUseCase_Execute_Success(t *testing.T) {
97 mockMigrator := mocks.NewMockConfigMigrator(t)101 mockMigrator := mocks.NewMockConfigMigrator(t)
102+ mockFormatter := mocks.NewMockDiffFormatter(t)
98 103
99 migrationResult := &port.MigrationResult{104 migrationResult := &port.MigrationResult{
100 MissingKeys: []string{"key1", "key2"},105 MissingKeys: []string{"key1", "key2"},
@@ -103,7 +108,7 @@ func TestMigrateConfigUseCase_Execute_Success(t *testing.T) {
103 mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil)108 mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil)
104 mockMigrator.EXPECT().Migrate().Return([]string{"key1", "key2"}, nil)109 mockMigrator.EXPECT().Migrate().Return([]string{"key1", "key2"}, nil)
105 110
106- uc := NewMigrateConfigUseCase(mockMigrator)111+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
107 ctx := context.Background()112 ctx := context.Background()
108 113
109 result, err := uc.Execute(ctx, MigrateConfigInput{})114 result, err := uc.Execute(ctx, MigrateConfigInput{})
@@ -115,6 +120,7 @@ func TestMigrateConfigUseCase_Execute_Success(t *testing.T) {
115 120
116 func TestMigrateConfigUseCase_Execute_MigrateError(t *testing.T) {121 func TestMigrateConfigUseCase_Execute_MigrateError(t *testing.T) {
117 mockMigrator := mocks.NewMockConfigMigrator(t)122 mockMigrator := mocks.NewMockConfigMigrator(t)
123+ mockFormatter := mocks.NewMockDiffFormatter(t)
118 124
119 migrationResult := &port.MigrationResult{125 migrationResult := &port.MigrationResult{
120 MissingKeys: []string{"key1"},126 MissingKeys: []string{"key1"},
@@ -125,7 +131,7 @@ func TestMigrateConfigUseCase_Execute_MigrateError(t *testing.T) {
125 expectedErr := errors.New("migrate failed")131 expectedErr := errors.New("migrate failed")
126 mockMigrator.EXPECT().Migrate().Return(nil, expectedErr)132 mockMigrator.EXPECT().Migrate().Return(nil, expectedErr)
127 133
128- uc := NewMigrateConfigUseCase(mockMigrator)134+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
129 ctx := context.Background()135 ctx := context.Background()
130 136
131 result, err := uc.Execute(ctx, MigrateConfigInput{})137 result, err := uc.Execute(ctx, MigrateConfigInput{})
@@ -137,11 +143,12 @@ func TestMigrateConfigUseCase_Execute_MigrateError(t *testing.T) {
137 143
138 func TestMigrateConfigUseCase_Execute_CheckError(t *testing.T) {144 func TestMigrateConfigUseCase_Execute_CheckError(t *testing.T) {
139 mockMigrator := mocks.NewMockConfigMigrator(t)145 mockMigrator := mocks.NewMockConfigMigrator(t)
146+ mockFormatter := mocks.NewMockDiffFormatter(t)
140 147
141 expectedErr := errors.New("check failed")148 expectedErr := errors.New("check failed")
142 mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr)149 mockMigrator.EXPECT().CheckMigration().Return(nil, expectedErr)
143 150
144- uc := NewMigrateConfigUseCase(mockMigrator)151+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
145 ctx := context.Background()152 ctx := context.Background()
146 153
147 result, err := uc.Execute(ctx, MigrateConfigInput{})154 result, err := uc.Execute(ctx, MigrateConfigInput{})
@@ -153,6 +160,7 @@ func TestMigrateConfigUseCase_Execute_CheckError(t *testing.T) {
153 160
154 func TestMigrateConfigUseCase_Check_EmptyMissingKeys(t *testing.T) {161 func TestMigrateConfigUseCase_Check_EmptyMissingKeys(t *testing.T) {
155 mockMigrator := mocks.NewMockConfigMigrator(t)162 mockMigrator := mocks.NewMockConfigMigrator(t)
163+ mockFormatter := mocks.NewMockDiffFormatter(t)
156 164
157 // Return a result with empty missing keys165 // Return a result with empty missing keys
158 migrationResult := &port.MigrationResult{166 migrationResult := &port.MigrationResult{
@@ -161,7 +169,7 @@ func TestMigrateConfigUseCase_Check_EmptyMissingKeys(t *testing.T) {
161 }169 }
162 mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil)170 mockMigrator.EXPECT().CheckMigration().Return(migrationResult, nil)
163 171
164- uc := NewMigrateConfigUseCase(mockMigrator)172+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
165 ctx := context.Background()173 ctx := context.Background()
166 174
167 result, err := uc.Check(ctx, CheckConfigMigrationInput{})175 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -173,15 +181,18 @@ func TestMigrateConfigUseCase_Check_EmptyMissingKeys(t *testing.T) {
173 181
174 func TestNewMigrateConfigUseCase(t *testing.T) {182 func TestNewMigrateConfigUseCase(t *testing.T) {
175 mockMigrator := mocks.NewMockConfigMigrator(t)183 mockMigrator := mocks.NewMockConfigMigrator(t)
176- uc := NewMigrateConfigUseCase(mockMigrator)184+ mockFormatter := mocks.NewMockDiffFormatter(t)
185+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
177 186
178 assert.NotNil(t, uc)187 assert.NotNil(t, uc)
179 assert.Equal(t, mockMigrator, uc.migrator)188 assert.Equal(t, mockMigrator, uc.migrator)
189+ assert.Equal(t, mockFormatter, uc.diffFormatter)
180 }190 }
181 191
182 // Ensure mock expectations are set up correctly192 // Ensure mock expectations are set up correctly
183 func TestMigrateConfigUseCase_MockExpectations(t *testing.T) {193 func TestMigrateConfigUseCase_MockExpectations(t *testing.T) {
184 mockMigrator := mocks.NewMockConfigMigrator(t)194 mockMigrator := mocks.NewMockConfigMigrator(t)
195+ mockFormatter := mocks.NewMockDiffFormatter(t)
185 196
186 // Set up expectations with mock.Anything for flexibility197 // Set up expectations with mock.Anything for flexibility
187 mockMigrator.On("CheckMigration").Return(&port.MigrationResult{198 mockMigrator.On("CheckMigration").Return(&port.MigrationResult{
@@ -195,7 +206,7 @@ func TestMigrateConfigUseCase_MockExpectations(t *testing.T) {
195 DefaultValue: `"test"`,206 DefaultValue: `"test"`,
196 }).Once()207 }).Once()
197 208
198- uc := NewMigrateConfigUseCase(mockMigrator)209+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
199 ctx := context.Background()210 ctx := context.Background()
200 211
201 result, err := uc.Check(ctx, CheckConfigMigrationInput{})212 result, err := uc.Check(ctx, CheckConfigMigrationInput{})
@@ -207,9 +218,10 @@ func TestMigrateConfigUseCase_MockExpectations(t *testing.T) {
207 218
208 func TestMigrateConfigUseCase_DetectChanges_NoChanges(t *testing.T) {219 func TestMigrateConfigUseCase_DetectChanges_NoChanges(t *testing.T) {
209 mockMigrator := mocks.NewMockConfigMigrator(t)220 mockMigrator := mocks.NewMockConfigMigrator(t)
221+ mockFormatter := mocks.NewMockDiffFormatter(t)
210 mockMigrator.EXPECT().DetectChanges().Return(nil, nil)222 mockMigrator.EXPECT().DetectChanges().Return(nil, nil)
211 223
212- uc := NewMigrateConfigUseCase(mockMigrator)224+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
213 ctx := context.Background()225 ctx := context.Background()
214 226
215 result, err := uc.DetectChanges(ctx, DetectChangesInput{})227 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -222,6 +234,7 @@ func TestMigrateConfigUseCase_DetectChanges_NoChanges(t *testing.T) {
222 234
223 func TestMigrateConfigUseCase_DetectChanges_WithAddedKey(t *testing.T) {235 func TestMigrateConfigUseCase_DetectChanges_WithAddedKey(t *testing.T) {
224 mockMigrator := mocks.NewMockConfigMigrator(t)236 mockMigrator := mocks.NewMockConfigMigrator(t)
237+ mockFormatter := mocks.NewMockDiffFormatter(t)
225 238
226 changes := []port.KeyChange{239 changes := []port.KeyChange{
227 {240 {
@@ -231,8 +244,9 @@ func TestMigrateConfigUseCase_DetectChanges_WithAddedKey(t *testing.T) {
231 },244 },
232 }245 }
233 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)246 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)
247+ mockFormatter.EXPECT().FormatChangesAsDiff(changes).Return("Config migration changes:\n\n + workspace.styling.mode_indicator_toaster_enabled = true\n")
234 248
235- uc := NewMigrateConfigUseCase(mockMigrator)249+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
236 ctx := context.Background()250 ctx := context.Background()
237 251
238 result, err := uc.DetectChanges(ctx, DetectChangesInput{})252 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -245,6 +259,7 @@ func TestMigrateConfigUseCase_DetectChanges_WithAddedKey(t *testing.T) {
245 259
246 func TestMigrateConfigUseCase_DetectChanges_WithRenamedKey(t *testing.T) {260 func TestMigrateConfigUseCase_DetectChanges_WithRenamedKey(t *testing.T) {
247 mockMigrator := mocks.NewMockConfigMigrator(t)261 mockMigrator := mocks.NewMockConfigMigrator(t)
262+ mockFormatter := mocks.NewMockDiffFormatter(t)
248 263
249 changes := []port.KeyChange{264 changes := []port.KeyChange{
250 {265 {
@@ -255,8 +270,9 @@ func TestMigrateConfigUseCase_DetectChanges_WithRenamedKey(t *testing.T) {
255 },270 },
256 }271 }
257 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)272 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)
273+ mockFormatter.EXPECT().FormatChangesAsDiff(changes).Return("Config migration changes:\n\n ~ workspace.styling.pane_mode_border_color -> workspace.styling.pane_mode_color\n (value: \"#4A90E2\")\n")
258 274
259- uc := NewMigrateConfigUseCase(mockMigrator)275+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
260 ctx := context.Background()276 ctx := context.Background()
261 277
262 result, err := uc.DetectChanges(ctx, DetectChangesInput{})278 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -269,6 +285,7 @@ func TestMigrateConfigUseCase_DetectChanges_WithRenamedKey(t *testing.T) {
269 285
270 func TestMigrateConfigUseCase_DetectChanges_WithRemovedKey(t *testing.T) {286 func TestMigrateConfigUseCase_DetectChanges_WithRemovedKey(t *testing.T) {
271 mockMigrator := mocks.NewMockConfigMigrator(t)287 mockMigrator := mocks.NewMockConfigMigrator(t)
288+ mockFormatter := mocks.NewMockDiffFormatter(t)
272 289
273 changes := []port.KeyChange{290 changes := []port.KeyChange{
274 {291 {
@@ -278,8 +295,9 @@ func TestMigrateConfigUseCase_DetectChanges_WithRemovedKey(t *testing.T) {
278 },295 },
279 }296 }
280 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)297 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)
298+ mockFormatter.EXPECT().FormatChangesAsDiff(changes).Return("Config migration changes:\n\n - workspace.styling.pane_mode_border_width = 4 (deprecated)\n")
281 299
282- uc := NewMigrateConfigUseCase(mockMigrator)300+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
283 ctx := context.Background()301 ctx := context.Background()
284 302
285 result, err := uc.DetectChanges(ctx, DetectChangesInput{})303 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -293,10 +311,11 @@ func TestMigrateConfigUseCase_DetectChanges_WithRemovedKey(t *testing.T) {
293 311
294 func TestMigrateConfigUseCase_DetectChanges_Error(t *testing.T) {312 func TestMigrateConfigUseCase_DetectChanges_Error(t *testing.T) {
295 mockMigrator := mocks.NewMockConfigMigrator(t)313 mockMigrator := mocks.NewMockConfigMigrator(t)
314+ mockFormatter := mocks.NewMockDiffFormatter(t)
296 expectedErr := errors.New("detect changes failed")315 expectedErr := errors.New("detect changes failed")
297 mockMigrator.EXPECT().DetectChanges().Return(nil, expectedErr)316 mockMigrator.EXPECT().DetectChanges().Return(nil, expectedErr)
298 317
299- uc := NewMigrateConfigUseCase(mockMigrator)318+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
300 ctx := context.Background()319 ctx := context.Background()
301 320
302 result, err := uc.DetectChanges(ctx, DetectChangesInput{})321 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -308,6 +327,7 @@ func TestMigrateConfigUseCase_DetectChanges_Error(t *testing.T) {
308 327
309 func TestMigrateConfigUseCase_DetectChanges_MultipleChanges(t *testing.T) {328 func TestMigrateConfigUseCase_DetectChanges_MultipleChanges(t *testing.T) {
310 mockMigrator := mocks.NewMockConfigMigrator(t)329 mockMigrator := mocks.NewMockConfigMigrator(t)
330+ mockFormatter := mocks.NewMockDiffFormatter(t)
311 331
312 changes := []port.KeyChange{332 changes := []port.KeyChange{
313 {333 {
@@ -328,8 +348,15 @@ func TestMigrateConfigUseCase_DetectChanges_MultipleChanges(t *testing.T) {
328 },348 },
329 }349 }
330 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)350 mockMigrator.EXPECT().DetectChanges().Return(changes, nil)
331-351+ mockFormatter.EXPECT().FormatChangesAsDiff(changes).Return(
332- uc := NewMigrateConfigUseCase(mockMigrator)352+ "Config migration changes:\n\n" +
353+ " + workspace.styling.mode_border_width = 4\n" +
354+ " ~ workspace.styling.pane_mode_border_color -> workspace.styling.pane_mode_color\n" +
355+ " (value: \"#4A90E2\")\n" +
356+ " - workspace.styling.pane_mode_border_width = 4 (deprecated)\n",
357+ )
358+
359+ uc := NewMigrateConfigUseCase(mockMigrator, mockFormatter)
333 ctx := context.Background()360 ctx := context.Background()
334 361
335 result, err := uc.DetectChanges(ctx, DetectChangesInput{})362 result, err := uc.DetectChanges(ctx, DetectChangesInput{})
@@ -343,46 +370,3 @@ func TestMigrateConfigUseCase_DetectChanges_MultipleChanges(t *testing.T) {
343 assert.Contains(t, result.DiffText, "~ workspace.styling.pane_mode_border_color")370 assert.Contains(t, result.DiffText, "~ workspace.styling.pane_mode_border_color")
344 assert.Contains(t, result.DiffText, "- workspace.styling.pane_mode_border_width")371 assert.Contains(t, result.DiffText, "- workspace.styling.pane_mode_border_width")
345 }372 }
346-
347-func TestFormatChangesAsDiff_EmptyChanges(t *testing.T) {
348- result := formatChangesAsDiff(nil)
349- assert.Equal(t, "No changes detected.", result)
350-
351- result = formatChangesAsDiff([]port.KeyChange{})
352- assert.Equal(t, "No changes detected.", result)
353-}
354-
355-func TestFormatChangesAsDiff_AllChangeTypes(t *testing.T) {
356- changes := []port.KeyChange{
357- {
358- Type: port.KeyChangeAdded,
359- NewKey: "new.key",
360- NewValue: `"value"`,
361- },
362- {
363- Type: port.KeyChangeRemoved,
364- OldKey: "old.key",
365- OldValue: `"old_value"`,
366- },
367- {
368- Type: port.KeyChangeRenamed,
369- OldKey: "renamed.old",
370- NewKey: "renamed.new",
371- OldValue: `"renamed_value"`,
372- },
373- {
374- Type: port.KeyChangeConsolidated,
375- OldKey: "consolidated.old",
376- NewKey: "consolidated.new",
377- },
378- }
379-
380- result := formatChangesAsDiff(changes)
381-
382- assert.Contains(t, result, "Config migration changes:")
383- assert.Contains(t, result, `+ new.key = "value"`)
384- assert.Contains(t, result, `- old.key = "old_value" (deprecated)`)
385- assert.Contains(t, result, "~ renamed.old -> renamed.new")
386- assert.Contains(t, result, `(value: "renamed_value")`)
387- assert.Contains(t, result, "> consolidated.old -> consolidated.new")
388-}
M internal/cli/cmd/config.go +4 -2
@@ -56,7 +56,8 @@ func runConfigStatus(_ *cobra.Command, _ []string) error {
5656
5757 renderer := styles.NewConfigRenderer(app.Theme)
5858 migrator := config.NewMigrator()
59- uc := usecase.NewMigrateConfigUseCase(migrator)
59+ diffFormatter := config.NewDiffFormatter()
60+ uc := usecase.NewMigrateConfigUseCase(migrator, diffFormatter)
6061
6162 // Get config file path
6263 configFile, err := config.GetConfigFile()
@@ -100,7 +101,8 @@ func runConfigMigrate(_ *cobra.Command, _ []string) error {
100101
101102 renderer := styles.NewConfigRenderer(app.Theme)
102103 migrator := config.NewMigrator()
103- uc := usecase.NewMigrateConfigUseCase(migrator)
104+ diffFormatter := config.NewDiffFormatter()
105+ uc := usecase.NewMigrateConfigUseCase(migrator, diffFormatter)
104106
105107 // Get config file path
106108 configFile, err := config.GetConfigFile()
@@ -56,7 +56,8 @@ func runConfigStatus(_ *cobra.Command, _ []string) error {
56 56
57 renderer := styles.NewConfigRenderer(app.Theme)57 renderer := styles.NewConfigRenderer(app.Theme)
58 migrator := config.NewMigrator()58 migrator := config.NewMigrator()
59- uc := usecase.NewMigrateConfigUseCase(migrator)59+ diffFormatter := config.NewDiffFormatter()
60+ uc := usecase.NewMigrateConfigUseCase(migrator, diffFormatter)
60 61
61 // Get config file path62 // Get config file path
62 configFile, err := config.GetConfigFile()63 configFile, err := config.GetConfigFile()
@@ -100,7 +101,8 @@ func runConfigMigrate(_ *cobra.Command, _ []string) error {
100 101
101 renderer := styles.NewConfigRenderer(app.Theme)102 renderer := styles.NewConfigRenderer(app.Theme)
102 migrator := config.NewMigrator()103 migrator := config.NewMigrator()
103- uc := usecase.NewMigrateConfigUseCase(migrator)104+ diffFormatter := config.NewDiffFormatter()
105+ uc := usecase.NewMigrateConfigUseCase(migrator, diffFormatter)
104 106
105 // Get config file path107 // Get config file path
106 configFile, err := config.GetConfigFile()108 configFile, err := config.GetConfigFile()
A internal/infrastructure/config/diff_formatter.go +40 -0
@@ -0,0 +1,40 @@
1+package config
2+
3+import (
4+ "fmt"
5+ "strings"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+)
9+
10+// ConfigDiffFormatter implements port.DiffFormatter for formatting config changes.
11+type ConfigDiffFormatter struct{}
12+
13+// NewDiffFormatter creates a new ConfigDiffFormatter.
14+func NewDiffFormatter() *ConfigDiffFormatter {
15+ return &ConfigDiffFormatter{}
16+}
17+
18+// FormatChangesAsDiff returns changes formatted as a diff for display.
19+func (*ConfigDiffFormatter) FormatChangesAsDiff(changes []port.KeyChange) string {
20+ if len(changes) == 0 {
21+ return "No changes detected."
22+ }
23+
24+ var sb strings.Builder
25+ sb.WriteString("Config migration changes:\n\n")
26+
27+ for _, change := range changes {
28+ switch change.Type {
29+ case port.KeyChangeAdded:
30+ sb.WriteString(fmt.Sprintf(" + %s = %s\n", change.NewKey, change.NewValue))
31+ case port.KeyChangeRemoved:
32+ sb.WriteString(fmt.Sprintf(" - %s = %s (deprecated)\n", change.OldKey, change.OldValue))
33+ case port.KeyChangeRenamed:
34+ sb.WriteString(fmt.Sprintf(" ~ %s -> %s\n", change.OldKey, change.NewKey))
35+ sb.WriteString(fmt.Sprintf(" (value: %s)\n", change.OldValue))
36+ }
37+ }
38+
39+ return sb.String()
40+}
@@ -0,0 +1,40 @@
1+package config
2+
3+import (
4+ "fmt"
5+ "strings"
6+
7+ "github.com/bnema/dumber/internal/application/port"
8+)
9+
10+// ConfigDiffFormatter implements port.DiffFormatter for formatting config changes.
11+type ConfigDiffFormatter struct{}
12+
13+// NewDiffFormatter creates a new ConfigDiffFormatter.
14+func NewDiffFormatter() *ConfigDiffFormatter {
15+ return &ConfigDiffFormatter{}
16+}
17+
18+// FormatChangesAsDiff returns changes formatted as a diff for display.
19+func (*ConfigDiffFormatter) FormatChangesAsDiff(changes []port.KeyChange) string {
20+ if len(changes) == 0 {
21+ return "No changes detected."
22+ }
23+
24+ var sb strings.Builder
25+ sb.WriteString("Config migration changes:\n\n")
26+
27+ for _, change := range changes {
28+ switch change.Type {
29+ case port.KeyChangeAdded:
30+ sb.WriteString(fmt.Sprintf(" + %s = %s\n", change.NewKey, change.NewValue))
31+ case port.KeyChangeRemoved:
32+ sb.WriteString(fmt.Sprintf(" - %s = %s (deprecated)\n", change.OldKey, change.OldValue))
33+ case port.KeyChangeRenamed:
34+ sb.WriteString(fmt.Sprintf(" ~ %s -> %s\n", change.OldKey, change.NewKey))
35+ sb.WriteString(fmt.Sprintf(" (value: %s)\n", change.OldValue))
36+ }
37+ }
38+
39+ return sb.String()
40+}
M internal/infrastructure/config/migrate.go +0 -26
@@ -669,29 +669,3 @@ func (*Migrator) formatValue(value any) string {
669669 return fmt.Sprintf("%v", v)
670670 }
671671 }
672-
673-// FormatChangesAsDiff returns changes formatted as a diff for display.
674-func FormatChangesAsDiff(changes []port.KeyChange) string {
675- if len(changes) == 0 {
676- return "No changes detected."
677- }
678-
679- var sb strings.Builder
680- sb.WriteString("Config migration changes:\n\n")
681-
682- for _, change := range changes {
683- switch change.Type {
684- case port.KeyChangeAdded:
685- sb.WriteString(fmt.Sprintf(" + %s = %s\n", change.NewKey, change.NewValue))
686- case port.KeyChangeRemoved:
687- sb.WriteString(fmt.Sprintf(" - %s = %s (deprecated)\n", change.OldKey, change.OldValue))
688- case port.KeyChangeRenamed:
689- sb.WriteString(fmt.Sprintf(" ~ %s -> %s\n", change.OldKey, change.NewKey))
690- sb.WriteString(fmt.Sprintf(" (value: %s)\n", change.OldValue))
691- case port.KeyChangeConsolidated:
692- sb.WriteString(fmt.Sprintf(" > %s -> %s\n", change.OldKey, change.NewKey))
693- }
694- }
695-
696- return sb.String()
697-}
@@ -669,29 +669,3 @@ func (*Migrator) formatValue(value any) string {
669 return fmt.Sprintf("%v", v)669 return fmt.Sprintf("%v", v)
670 }670 }
671 }671 }
672-
673-// FormatChangesAsDiff returns changes formatted as a diff for display.
674-func FormatChangesAsDiff(changes []port.KeyChange) string {
675- if len(changes) == 0 {
676- return "No changes detected."
677- }
678-
679- var sb strings.Builder
680- sb.WriteString("Config migration changes:\n\n")
681-
682- for _, change := range changes {
683- switch change.Type {
684- case port.KeyChangeAdded:
685- sb.WriteString(fmt.Sprintf(" + %s = %s\n", change.NewKey, change.NewValue))
686- case port.KeyChangeRemoved:
687- sb.WriteString(fmt.Sprintf(" - %s = %s (deprecated)\n", change.OldKey, change.OldValue))
688- case port.KeyChangeRenamed:
689- sb.WriteString(fmt.Sprintf(" ~ %s -> %s\n", change.OldKey, change.NewKey))
690- sb.WriteString(fmt.Sprintf(" (value: %s)\n", change.OldValue))
691- case port.KeyChangeConsolidated:
692- sb.WriteString(fmt.Sprintf(" > %s -> %s\n", change.OldKey, change.NewKey))
693- }
694- }
695-
696- return sb.String()
697-}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close