fix: address follow-up review findings
- commit
- 8e835cc58bf41aec7010db4330e0d70c118617b7
- author
- brice · · 2026-06-26 03:48 UTC
- parent
- fd1a5db
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +145 -10
M internal/application/usecase/list_sessions.go +4 -1
| @@ -9,6 +9,9 @@ import ( | |||
| 9 | 9 | "github.com/bnema/dumber/internal/logging" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | + | // ListAllSessionsLimit asks Execute to return every known session. | |
| 13 | + | const ListAllSessionsLimit = -1 | |
| 14 | + | ||
| 12 | 15 | // ListSessionsUseCase handles listing sessions with their state information. | |
| 13 | 16 | type ListSessionsUseCase struct { | |
| 14 | 17 | sessionRepo repository.SessionRepository | |
| @@ -36,7 +39,7 @@ type ListSessionsOutput struct { | |||
| 36 | 39 | func (uc *ListSessionsUseCase) Execute(ctx context.Context, currentSessionID entity.SessionID, limit int) (*ListSessionsOutput, error) { | |
| 37 | 40 | log := logging.FromContext(ctx) | |
| 38 | 41 | ||
| 39 | - | if limit <= 0 { | |
| 42 | + | if limit == 0 { | |
| 40 | 43 | limit = 50 | |
| 41 | 44 | } | |
| 42 | 45 | ||
| @@ -9,6 +9,9 @@ import ( | |||||
| 9 | "github.com/bnema/dumber/internal/logging" | 9 | "github.com/bnema/dumber/internal/logging" | ||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | + | // ListAllSessionsLimit asks Execute to return every known session. | |||
| 13 | + | const ListAllSessionsLimit = -1 | |||
| 14 | + | ||||
| 12 | // ListSessionsUseCase handles listing sessions with their state information. | 15 | // ListSessionsUseCase handles listing sessions with their state information. | ||
| 13 | type ListSessionsUseCase struct { | 16 | type ListSessionsUseCase struct { | ||
| 14 | sessionRepo repository.SessionRepository | 17 | sessionRepo repository.SessionRepository | ||
| @@ -36,7 +39,7 @@ type ListSessionsOutput struct { | |||||
| 36 | func (uc *ListSessionsUseCase) Execute(ctx context.Context, currentSessionID entity.SessionID, limit int) (*ListSessionsOutput, error) { | 39 | func (uc *ListSessionsUseCase) Execute(ctx context.Context, currentSessionID entity.SessionID, limit int) (*ListSessionsOutput, error) { | ||
| 37 | log := logging.FromContext(ctx) | 40 | log := logging.FromContext(ctx) | ||
| 38 | 41 | ||||
| 39 | - | if limit <= 0 { | 42 | + | if limit == 0 { |
| 40 | limit = 50 | 43 | limit = 50 | ||
| 41 | } | 44 | } | ||
| 42 | 45 | ||||
M internal/application/usecase/navigation_url_normalizer.go +16 -1
| @@ -3,6 +3,7 @@ package usecase | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | stdurl "net/url" | |
| 6 | + | "strings" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 9 | domainurl "github.com/bnema/dumber/internal/domain/url" | |
| @@ -55,5 +56,19 @@ func hasPreservedNavigationScheme(input string) bool { | |||
| 55 | 56 | if err != nil || parsed.Scheme == "" { | |
| 56 | 57 | return false | |
| 57 | 58 | } | |
| 58 | - | return domainurl.Normalize(input) == input | |
| 59 | + | ||
| 60 | + | scheme := strings.ToLower(parsed.Scheme) | |
| 61 | + | lowerInput := strings.ToLower(input) | |
| 62 | + | ||
| 63 | + | switch scheme { | |
| 64 | + | case "http", "https", "dumb", "file", "vscode", "vscode-insiders", "spotify", "steam": | |
| 65 | + | return strings.HasPrefix(lowerInput, scheme+"://") && domainurl.Normalize(input) == input | |
| 66 | + | case "blob": | |
| 67 | + | return strings.HasPrefix(lowerInput, "blob:") && strings.Contains(lowerInput, "://") && | |
| 68 | + | domainurl.Normalize(input) == input | |
| 69 | + | case "about", "data", "javascript", "mailto": | |
| 70 | + | return strings.HasPrefix(lowerInput, scheme+":") && domainurl.Normalize(input) == input | |
| 71 | + | default: | |
| 72 | + | return false | |
| 73 | + | } | |
| 59 | 74 | } | |
| @@ -3,6 +3,7 @@ package usecase | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | stdurl "net/url" | 5 | stdurl "net/url" | ||
| 6 | + | "strings" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/dumber/internal/application/port" | 8 | "github.com/bnema/dumber/internal/application/port" | ||
| 8 | domainurl "github.com/bnema/dumber/internal/domain/url" | 9 | domainurl "github.com/bnema/dumber/internal/domain/url" | ||
| @@ -55,5 +56,19 @@ func hasPreservedNavigationScheme(input string) bool { | |||||
| 55 | if err != nil || parsed.Scheme == "" { | 56 | if err != nil || parsed.Scheme == "" { | ||
| 56 | return false | 57 | return false | ||
| 57 | } | 58 | } | ||
| 58 | - | return domainurl.Normalize(input) == input | 59 | + | |
| 60 | + | scheme := strings.ToLower(parsed.Scheme) | |||
| 61 | + | lowerInput := strings.ToLower(input) | |||
| 62 | + | ||||
| 63 | + | switch scheme { | |||
| 64 | + | case "http", "https", "dumb", "file", "vscode", "vscode-insiders", "spotify", "steam": | |||
| 65 | + | return strings.HasPrefix(lowerInput, scheme+"://") && domainurl.Normalize(input) == input | |||
| 66 | + | case "blob": | |||
| 67 | + | return strings.HasPrefix(lowerInput, "blob:") && strings.Contains(lowerInput, "://") && | |||
| 68 | + | domainurl.Normalize(input) == input | |||
| 69 | + | case "about", "data", "javascript", "mailto": | |||
| 70 | + | return strings.HasPrefix(lowerInput, scheme+":") && domainurl.Normalize(input) == input | |||
| 71 | + | default: | |||
| 72 | + | return false | |||
| 73 | + | } | |||
| 59 | } | 74 | } | ||
M internal/application/usecase/navigation_url_normalizer_test.go +7 -4
| @@ -21,12 +21,15 @@ func TestNavigationURLNormalizerResolvesExistingLocalPaths(t *testing.T) { | |||
| 21 | 21 | ctx := context.Background() | |
| 22 | 22 | absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html") | |
| 23 | 23 | normalizer := NewNavigationURLNormalizer(fakeLocalPathResolver{paths: map[string]string{ | |
| 24 | - | absFile: absFile, | |
| 25 | - | "page.html": absFile, | |
| 26 | - | "~/page.html": absFile, | |
| 24 | + | absFile: absFile, | |
| 25 | + | "page.html": absFile, | |
| 26 | + | "~/page.html": absFile, | |
| 27 | + | "notes:2026.txt": absFile, | |
| 28 | + | "http:notes.txt": absFile, | |
| 29 | + | "http:notes": absFile, | |
| 27 | 30 | }}) | |
| 28 | 31 | ||
| 29 | - | for _, input := range []string{absFile, "page.html", "~/page.html"} { | |
| 32 | + | for _, input := range []string{absFile, "page.html", "~/page.html", "notes:2026.txt", "http:notes.txt", "http:notes"} { | |
| 30 | 33 | t.Run(input, func(t *testing.T) { | |
| 31 | 34 | got := normalizer.Normalize(ctx, input) | |
| 32 | 35 | want := (&stdurl.URL{Scheme: "file", Path: absFile}).String() | |
| @@ -21,12 +21,15 @@ func TestNavigationURLNormalizerResolvesExistingLocalPaths(t *testing.T) { | |||||
| 21 | ctx := context.Background() | 21 | ctx := context.Background() | ||
| 22 | absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html") | 22 | absFile := filepath.Join(string(filepath.Separator), "tmp", "page.html") | ||
| 23 | normalizer := NewNavigationURLNormalizer(fakeLocalPathResolver{paths: map[string]string{ | 23 | normalizer := NewNavigationURLNormalizer(fakeLocalPathResolver{paths: map[string]string{ | ||
| 24 | - | absFile: absFile, | 24 | + | absFile: absFile, |
| 25 | - | "page.html": absFile, | 25 | + | "page.html": absFile, |
| 26 | - | "~/page.html": absFile, | 26 | + | "~/page.html": absFile, |
| 27 | + | "notes:2026.txt": absFile, | |||
| 28 | + | "http:notes.txt": absFile, | |||
| 29 | + | "http:notes": absFile, | |||
| 27 | }}) | 30 | }}) | ||
| 28 | 31 | ||||
| 29 | - | for _, input := range []string{absFile, "page.html", "~/page.html"} { | 32 | + | for _, input := range []string{absFile, "page.html", "~/page.html", "notes:2026.txt", "http:notes.txt", "http:notes"} { |
| 30 | t.Run(input, func(t *testing.T) { | 33 | t.Run(input, func(t *testing.T) { | ||
| 31 | got := normalizer.Normalize(ctx, input) | 34 | got := normalizer.Normalize(ctx, input) | ||
| 32 | want := (&stdurl.URL{Scheme: "file", Path: absFile}).String() | 35 | want := (&stdurl.URL{Scheme: "file", Path: absFile}).String() | ||
M internal/cli/cmd/sessions.go +9 -3
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package cmd | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "encoding/json" | |
| 5 | 6 | "fmt" | |
| 6 | 7 | "os" | |
| @@ -269,14 +270,19 @@ func findSessionByIDOrSuffix(idOrSuffix string) (*entity.SessionInfo, error) { | |||
| 269 | 270 | return nil, fmt.Errorf("session management not available") | |
| 270 | 271 | } | |
| 271 | 272 | ||
| 273 | + | ctx := cliApp.Ctx() | |
| 274 | + | if ctx == nil { | |
| 275 | + | ctx = context.Background() | |
| 276 | + | } | |
| 277 | + | ||
| 272 | 278 | // Get current session ID (empty if not running as browser) | |
| 273 | 279 | var currentSessionID entity.SessionID | |
| 274 | - | if active, err := cliApp.SessionUC.GetActiveSession(cliApp.Ctx()); err == nil && active != nil { | |
| 280 | + | if active, err := cliApp.SessionUC.GetActiveSession(ctx); err == nil && active != nil { | |
| 275 | 281 | currentSessionID = active.ID | |
| 276 | 282 | } | |
| 277 | 283 | ||
| 278 | - | // List all sessions | |
| 279 | - | output, err := cliApp.ListSessionsUC.Execute(cliApp.Ctx(), currentSessionID, defaultSessionsLimit*5) | |
| 284 | + | // List all sessions so restore/delete suffix lookup can find older sessions. | |
| 285 | + | output, err := cliApp.ListSessionsUC.Execute(ctx, currentSessionID, usecase.ListAllSessionsLimit) | |
| 280 | 286 | if err != nil { | |
| 281 | 287 | return nil, fmt.Errorf("list sessions: %w", err) | |
| 282 | 288 | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package cmd | 1 | package cmd | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "encoding/json" | 5 | "encoding/json" | ||
| 5 | "fmt" | 6 | "fmt" | ||
| 6 | "os" | 7 | "os" | ||
| @@ -269,14 +270,19 @@ func findSessionByIDOrSuffix(idOrSuffix string) (*entity.SessionInfo, error) { | |||||
| 269 | return nil, fmt.Errorf("session management not available") | 270 | return nil, fmt.Errorf("session management not available") | ||
| 270 | } | 271 | } | ||
| 271 | 272 | ||||
| 273 | + | ctx := cliApp.Ctx() | |||
| 274 | + | if ctx == nil { | |||
| 275 | + | ctx = context.Background() | |||
| 276 | + | } | |||
| 277 | + | ||||
| 272 | // Get current session ID (empty if not running as browser) | 278 | // Get current session ID (empty if not running as browser) | ||
| 273 | var currentSessionID entity.SessionID | 279 | var currentSessionID entity.SessionID | ||
| 274 | - | if active, err := cliApp.SessionUC.GetActiveSession(cliApp.Ctx()); err == nil && active != nil { | 280 | + | if active, err := cliApp.SessionUC.GetActiveSession(ctx); err == nil && active != nil { |
| 275 | currentSessionID = active.ID | 281 | currentSessionID = active.ID | ||
| 276 | } | 282 | } | ||
| 277 | 283 | ||||
| 278 | - | // List all sessions | 284 | + | // List all sessions so restore/delete suffix lookup can find older sessions. |
| 279 | - | output, err := cliApp.ListSessionsUC.Execute(cliApp.Ctx(), currentSessionID, defaultSessionsLimit*5) | 285 | + | output, err := cliApp.ListSessionsUC.Execute(ctx, currentSessionID, usecase.ListAllSessionsLimit) |
| 280 | if err != nil { | 286 | if err != nil { | ||
| 281 | return nil, fmt.Errorf("list sessions: %w", err) | 287 | return nil, fmt.Errorf("list sessions: %w", err) | ||
| 282 | } | 288 | } | ||
M internal/cli/cmd/sessions_test.go +104 -0
| @@ -1,13 +1,17 @@ | |||
| 1 | 1 | package cmd | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 5 | + | "fmt" | |
| 4 | 6 | "io" | |
| 5 | 7 | "os" | |
| 6 | 8 | "testing" | |
| 9 | + | "time" | |
| 7 | 10 | ||
| 8 | 11 | "github.com/bnema/dumber/internal/application/usecase" | |
| 9 | 12 | "github.com/bnema/dumber/internal/cli" | |
| 10 | 13 | "github.com/bnema/dumber/internal/cli/styles" | |
| 14 | + | "github.com/bnema/dumber/internal/domain/entity" | |
| 11 | 15 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| 12 | 16 | "github.com/stretchr/testify/require" | |
| 13 | 17 | ) | |
| @@ -59,6 +63,38 @@ func TestSessionRestoreReturnsRestorationErrorWhenSessionUseCaseMissing(t *testi | |||
| 59 | 63 | require.ErrorContains(t, err, "session restoration not available") | |
| 60 | 64 | } | |
| 61 | 65 | ||
| 66 | + | func TestFindSessionByIDOrSuffixSearchesBeyondDefaultLimit(t *testing.T) { | |
| 67 | + | targetID := entity.SessionID("20251224_000000_targetolder") | |
| 68 | + | sessions := make([]*entity.Session, 0, defaultSessionsLimit*5+1) | |
| 69 | + | startedAt := time.Date(2025, 12, 24, 12, 0, 0, 0, time.UTC) | |
| 70 | + | for i := 0; i < defaultSessionsLimit*5; i++ { | |
| 71 | + | sessions = append(sessions, &entity.Session{ | |
| 72 | + | ID: entity.SessionID(fmt.Sprintf("20251224_1200%02d_recent", i)), | |
| 73 | + | Type: entity.SessionTypeBrowser, | |
| 74 | + | StartedAt: startedAt.Add(-time.Duration(i) * time.Minute), | |
| 75 | + | }) | |
| 76 | + | } | |
| 77 | + | sessions = append(sessions, &entity.Session{ | |
| 78 | + | ID: targetID, | |
| 79 | + | Type: entity.SessionTypeBrowser, | |
| 80 | + | StartedAt: startedAt.Add(-time.Hour), | |
| 81 | + | }) | |
| 82 | + | ||
| 83 | + | sessionRepo := &sessionCommandSessionRepo{sessions: sessions} | |
| 84 | + | cfg := config.DefaultConfig() | |
| 85 | + | withApp(t, &cli.App{ | |
| 86 | + | Config: cfg, | |
| 87 | + | Theme: styles.NewTheme(cfg), | |
| 88 | + | SessionUC: usecase.NewManageSessionUseCase(sessionRepo, nil), | |
| 89 | + | ListSessionsUC: usecase.NewListSessionsUseCase(sessionRepo, sessionCommandStateRepo{}), | |
| 90 | + | }) | |
| 91 | + | ||
| 92 | + | got, err := findSessionByIDOrSuffix("targetolder") | |
| 93 | + | ||
| 94 | + | require.NoError(t, err) | |
| 95 | + | require.Equal(t, targetID, got.Session.ID) | |
| 96 | + | } | |
| 97 | + | ||
| 62 | 98 | func sessionCommandTestApp() *cli.App { | |
| 63 | 99 | cfg := config.DefaultConfig() | |
| 64 | 100 | return &cli.App{ | |
| @@ -90,3 +126,71 @@ func discardStderr(t *testing.T) { | |||
| 90 | 126 | require.NoError(t, reader.Close()) | |
| 91 | 127 | }) | |
| 92 | 128 | } | |
| 129 | + | ||
| 130 | + | type sessionCommandSessionRepo struct { | |
| 131 | + | sessions []*entity.Session | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | func (*sessionCommandSessionRepo) Save(context.Context, *entity.Session) error { | |
| 135 | + | return nil | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func (r *sessionCommandSessionRepo) FindByID(_ context.Context, id entity.SessionID) (*entity.Session, error) { | |
| 139 | + | for _, session := range r.sessions { | |
| 140 | + | if session.ID == id { | |
| 141 | + | return session, nil | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | return nil, nil | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | func (*sessionCommandSessionRepo) GetActive(context.Context) (*entity.Session, error) { | |
| 148 | + | return nil, nil | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | func (r *sessionCommandSessionRepo) GetRecent(_ context.Context, limit int) ([]*entity.Session, error) { | |
| 152 | + | if limit < 0 || limit > len(r.sessions) { | |
| 153 | + | limit = len(r.sessions) | |
| 154 | + | } | |
| 155 | + | out := make([]*entity.Session, limit) | |
| 156 | + | copy(out, r.sessions[:limit]) | |
| 157 | + | return out, nil | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | func (*sessionCommandSessionRepo) MarkEnded(context.Context, entity.SessionID, time.Time) error { | |
| 161 | + | return nil | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | func (*sessionCommandSessionRepo) Delete(context.Context, entity.SessionID) error { | |
| 165 | + | return nil | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | func (*sessionCommandSessionRepo) DeleteOldestExited(context.Context, int) (int64, error) { | |
| 169 | + | return 0, nil | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | func (*sessionCommandSessionRepo) DeleteExitedBefore(context.Context, time.Time) (int64, error) { | |
| 173 | + | return 0, nil | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | type sessionCommandStateRepo struct{} | |
| 177 | + | ||
| 178 | + | func (sessionCommandStateRepo) SaveSnapshot(context.Context, *entity.SessionState) error { | |
| 179 | + | return nil | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | func (sessionCommandStateRepo) GetSnapshot(context.Context, entity.SessionID) (*entity.SessionState, error) { | |
| 183 | + | return nil, nil | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | func (sessionCommandStateRepo) DeleteSnapshot(context.Context, entity.SessionID) error { | |
| 187 | + | return nil | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | func (sessionCommandStateRepo) GetAllSnapshots(context.Context) ([]*entity.SessionState, error) { | |
| 191 | + | return nil, nil | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | func (sessionCommandStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { | |
| 195 | + | return 0, nil | |
| 196 | + | } | |
| @@ -1,13 +1,17 @@ | |||||
| 1 | package cmd | 1 | package cmd | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 5 | + | "fmt" | |||
| 4 | "io" | 6 | "io" | ||
| 5 | "os" | 7 | "os" | ||
| 6 | "testing" | 8 | "testing" | ||
| 9 | + | "time" | |||
| 7 | 10 | ||||
| 8 | "github.com/bnema/dumber/internal/application/usecase" | 11 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 9 | "github.com/bnema/dumber/internal/cli" | 12 | "github.com/bnema/dumber/internal/cli" | ||
| 10 | "github.com/bnema/dumber/internal/cli/styles" | 13 | "github.com/bnema/dumber/internal/cli/styles" | ||
| 14 | + | "github.com/bnema/dumber/internal/domain/entity" | |||
| 11 | "github.com/bnema/dumber/internal/infrastructure/config" | 15 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| 12 | "github.com/stretchr/testify/require" | 16 | "github.com/stretchr/testify/require" | ||
| 13 | ) | 17 | ) | ||
| @@ -59,6 +63,38 @@ func TestSessionRestoreReturnsRestorationErrorWhenSessionUseCaseMissing(t *testi | |||||
| 59 | require.ErrorContains(t, err, "session restoration not available") | 63 | require.ErrorContains(t, err, "session restoration not available") | ||
| 60 | } | 64 | } | ||
| 61 | 65 | ||||
| 66 | + | func TestFindSessionByIDOrSuffixSearchesBeyondDefaultLimit(t *testing.T) { | |||
| 67 | + | targetID := entity.SessionID("20251224_000000_targetolder") | |||
| 68 | + | sessions := make([]*entity.Session, 0, defaultSessionsLimit*5+1) | |||
| 69 | + | startedAt := time.Date(2025, 12, 24, 12, 0, 0, 0, time.UTC) | |||
| 70 | + | for i := 0; i < defaultSessionsLimit*5; i++ { | |||
| 71 | + | sessions = append(sessions, &entity.Session{ | |||
| 72 | + | ID: entity.SessionID(fmt.Sprintf("20251224_1200%02d_recent", i)), | |||
| 73 | + | Type: entity.SessionTypeBrowser, | |||
| 74 | + | StartedAt: startedAt.Add(-time.Duration(i) * time.Minute), | |||
| 75 | + | }) | |||
| 76 | + | } | |||
| 77 | + | sessions = append(sessions, &entity.Session{ | |||
| 78 | + | ID: targetID, | |||
| 79 | + | Type: entity.SessionTypeBrowser, | |||
| 80 | + | StartedAt: startedAt.Add(-time.Hour), | |||
| 81 | + | }) | |||
| 82 | + | ||||
| 83 | + | sessionRepo := &sessionCommandSessionRepo{sessions: sessions} | |||
| 84 | + | cfg := config.DefaultConfig() | |||
| 85 | + | withApp(t, &cli.App{ | |||
| 86 | + | Config: cfg, | |||
| 87 | + | Theme: styles.NewTheme(cfg), | |||
| 88 | + | SessionUC: usecase.NewManageSessionUseCase(sessionRepo, nil), | |||
| 89 | + | ListSessionsUC: usecase.NewListSessionsUseCase(sessionRepo, sessionCommandStateRepo{}), | |||
| 90 | + | }) | |||
| 91 | + | ||||
| 92 | + | got, err := findSessionByIDOrSuffix("targetolder") | |||
| 93 | + | ||||
| 94 | + | require.NoError(t, err) | |||
| 95 | + | require.Equal(t, targetID, got.Session.ID) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 62 | func sessionCommandTestApp() *cli.App { | 98 | func sessionCommandTestApp() *cli.App { | ||
| 63 | cfg := config.DefaultConfig() | 99 | cfg := config.DefaultConfig() | ||
| 64 | return &cli.App{ | 100 | return &cli.App{ | ||
| @@ -90,3 +126,71 @@ func discardStderr(t *testing.T) { | |||||
| 90 | require.NoError(t, reader.Close()) | 126 | require.NoError(t, reader.Close()) | ||
| 91 | }) | 127 | }) | ||
| 92 | } | 128 | } | ||
| 129 | + | ||||
| 130 | + | type sessionCommandSessionRepo struct { | |||
| 131 | + | sessions []*entity.Session | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | func (*sessionCommandSessionRepo) Save(context.Context, *entity.Session) error { | |||
| 135 | + | return nil | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | func (r *sessionCommandSessionRepo) FindByID(_ context.Context, id entity.SessionID) (*entity.Session, error) { | |||
| 139 | + | for _, session := range r.sessions { | |||
| 140 | + | if session.ID == id { | |||
| 141 | + | return session, nil | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | return nil, nil | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | func (*sessionCommandSessionRepo) GetActive(context.Context) (*entity.Session, error) { | |||
| 148 | + | return nil, nil | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | func (r *sessionCommandSessionRepo) GetRecent(_ context.Context, limit int) ([]*entity.Session, error) { | |||
| 152 | + | if limit < 0 || limit > len(r.sessions) { | |||
| 153 | + | limit = len(r.sessions) | |||
| 154 | + | } | |||
| 155 | + | out := make([]*entity.Session, limit) | |||
| 156 | + | copy(out, r.sessions[:limit]) | |||
| 157 | + | return out, nil | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | func (*sessionCommandSessionRepo) MarkEnded(context.Context, entity.SessionID, time.Time) error { | |||
| 161 | + | return nil | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | func (*sessionCommandSessionRepo) Delete(context.Context, entity.SessionID) error { | |||
| 165 | + | return nil | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | func (*sessionCommandSessionRepo) DeleteOldestExited(context.Context, int) (int64, error) { | |||
| 169 | + | return 0, nil | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | func (*sessionCommandSessionRepo) DeleteExitedBefore(context.Context, time.Time) (int64, error) { | |||
| 173 | + | return 0, nil | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | type sessionCommandStateRepo struct{} | |||
| 177 | + | ||||
| 178 | + | func (sessionCommandStateRepo) SaveSnapshot(context.Context, *entity.SessionState) error { | |||
| 179 | + | return nil | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | func (sessionCommandStateRepo) GetSnapshot(context.Context, entity.SessionID) (*entity.SessionState, error) { | |||
| 183 | + | return nil, nil | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | func (sessionCommandStateRepo) DeleteSnapshot(context.Context, entity.SessionID) error { | |||
| 187 | + | return nil | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | func (sessionCommandStateRepo) GetAllSnapshots(context.Context) ([]*entity.SessionState, error) { | |||
| 191 | + | return nil, nil | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | func (sessionCommandStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { | |||
| 195 | + | return 0, nil | |||
| 196 | + | } | |||
M internal/infrastructure/config/validation_test.go +1 -0
| @@ -382,6 +382,7 @@ func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingBareRelativeValue(t *te | |||
| 382 | 382 | validationErr := validateConfig(cfg) | |
| 383 | 383 | require.Error(t, validationErr) | |
| 384 | 384 | assert.Contains(t, validationErr.Error(), "workspace.new_pane_url") | |
| 385 | + | assert.Contains(t, validationErr.Error(), "relative local path cannot be resolved without config file context") | |
| 385 | 386 | } | |
| 386 | 387 | ||
| 387 | 388 | func TestValidateConfig_WebKitDefaultProfileIgnoresZeroGPUThreads(t *testing.T) { | |
| @@ -382,6 +382,7 @@ func TestValidateConfig_WorkspaceNewPaneURLRejectsMissingBareRelativeValue(t *te | |||||
| 382 | validationErr := validateConfig(cfg) | 382 | validationErr := validateConfig(cfg) | ||
| 383 | require.Error(t, validationErr) | 383 | require.Error(t, validationErr) | ||
| 384 | assert.Contains(t, validationErr.Error(), "workspace.new_pane_url") | 384 | assert.Contains(t, validationErr.Error(), "workspace.new_pane_url") | ||
| 385 | + | assert.Contains(t, validationErr.Error(), "relative local path cannot be resolved without config file context") | |||
| 385 | } | 386 | } | ||
| 386 | 387 | ||||
| 387 | func TestValidateConfig_WebKitDefaultProfileIgnoresZeroGPUThreads(t *testing.T) { | 388 | func TestValidateConfig_WebKitDefaultProfileIgnoresZeroGPUThreads(t *testing.T) { | ||
M internal/infrastructure/persistence/sqlite/session_repo.go +4 -1
| @@ -69,9 +69,12 @@ func (r *sessionRepo) GetActive(ctx context.Context) (*entity.Session, error) { | |||
| 69 | 69 | } | |
| 70 | 70 | ||
| 71 | 71 | func (r *sessionRepo) GetRecent(ctx context.Context, limit int) ([]*entity.Session, error) { | |
| 72 | - | if limit <= 0 { | |
| 72 | + | if limit == 0 { | |
| 73 | 73 | limit = 20 | |
| 74 | 74 | } | |
| 75 | + | if limit < 0 { | |
| 76 | + | limit = -1 | |
| 77 | + | } | |
| 75 | 78 | rows, err := r.queries.GetRecentSessions(ctx, int64(limit)) | |
| 76 | 79 | if err != nil { | |
| 77 | 80 | return nil, err | |
| @@ -69,9 +69,12 @@ func (r *sessionRepo) GetActive(ctx context.Context) (*entity.Session, error) { | |||||
| 69 | } | 69 | } | ||
| 70 | 70 | ||||
| 71 | func (r *sessionRepo) GetRecent(ctx context.Context, limit int) ([]*entity.Session, error) { | 71 | func (r *sessionRepo) GetRecent(ctx context.Context, limit int) ([]*entity.Session, error) { | ||
| 72 | - | if limit <= 0 { | 72 | + | if limit == 0 { |
| 73 | limit = 20 | 73 | limit = 20 | ||
| 74 | } | 74 | } | ||
| 75 | + | if limit < 0 { | |||
| 76 | + | limit = -1 | |||
| 77 | + | } | |||
| 75 | rows, err := r.queries.GetRecentSessions(ctx, int64(limit)) | 78 | rows, err := r.queries.GetRecentSessions(ctx, int64(limit)) | ||
| 76 | if err != nil { | 79 | if err != nil { | ||
| 77 | return nil, err | 80 | return nil, err | ||