detached 6577047

fix(cli): preserve routes loader errors

Surface route loader and remotes config failures in list and status output so broken targets render explicit errors instead of appearing empty.
commit
65770474de4ef4c2bfa6a137fc91821b7018480e
author
brice · · 2026-04-15 04:39 UTC
parent
4648a7b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +206 -56
M internal/adapters/in/cli/routes.go +50 -20
@@ -128,14 +128,14 @@ type routeStatusSection struct {
128128 }
129129
130130 type routesListDeps struct {
131- explicitRemote func() (*remote.ResolvedRemote, bool)
131+ explicitRemote func() (*remote.ResolvedRemote, bool, error)
132132 loadLocal func(context.Context, string) (routeListSection, error)
133133 listRemotes func() (map[string]remote.RemoteEntry, string, error)
134134 loadRemote func(context.Context, string, remote.RemoteEntry) (routeListSection, error)
135135 }
136136
137137 type routesStatusDeps struct {
138- explicitRemote func() (*remote.ResolvedRemote, bool)
138+ explicitRemote func() (*remote.ResolvedRemote, bool, error)
139139 loadLocal func(context.Context, string) (routeStatusSection, error)
140140 listRemotes func() (map[string]remote.RemoteEntry, string, error)
141141 loadRemote func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error)
@@ -265,7 +265,9 @@ func collectRoutesListSections(ctx context.Context, cfgPath string, deps routesL
265265 deps.loadRemote = loadRoutesListRemoteSection
266266 }
267267
268- if resolved, ok := deps.explicitRemote(); ok {
268+ if resolved, ok, err := deps.explicitRemote(); err != nil {
269+ return nil, err
270+ } else if ok {
269271 section := loadRoutesListExplicitRemoteSection(ctx, deps, resolved)
270272 return []routeListSection{section}, nil
271273 }
@@ -281,7 +283,10 @@ func collectRoutesListSections(ctx context.Context, cfgPath string, deps routesL
281283 }
282284
283285 func loadRoutesListExplicitRemoteSection(ctx context.Context, deps routesListDeps, resolved *remote.ResolvedRemote) routeListSection {
284- section, _ := deps.loadRemote(ctx, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
286+ section, err := deps.loadRemote(ctx, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
287+ if err != nil && section.Error == "" {
288+ section.Error = err.Error()
289+ }
285290 return normalizeRouteListRemoteSection(section, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
286291 }
287292
@@ -289,6 +294,7 @@ func collectRoutesListAggregateSections(ctx context.Context, cfgPath string, dep
289294
290295 type localResult struct {
291296 section routeListSection
297+ err error
292298 }
293299 type remotesResult struct {
294300 entries map[string]remote.RemoteEntry
@@ -299,16 +305,22 @@ func collectRoutesListAggregateSections(ctx context.Context, cfgPath string, dep
299305 remotesCh := make(chan remotesResult, 1)
300306
301307 go func() {
302- section, _ := deps.loadLocal(ctx, cfgPath)
303- localCh <- localResult{section: section}
308+ section, err := deps.loadLocal(ctx, cfgPath)
309+ localCh <- localResult{section: section, err: err}
304310 }()
305311 go func() {
306312 entries, _, err := deps.listRemotes()
307313 remotesCh <- remotesResult{entries: entries, err: err}
308314 }()
309315
310- local := (<-localCh).section
316+ localRes := <-localCh
311317 remotes := <-remotesCh
318+ local := localRes.section
319+ if err := localRes.err; err != nil {
320+ if local.Error == "" {
321+ local.Error = err.Error()
322+ }
323+ }
312324
313325 sections := make([]routeListSection, 0, 1)
314326 sections = append(sections, normalizeRouteListLocalSection(local))
@@ -327,7 +339,10 @@ func collectRoutesListAggregateSections(ctx context.Context, cfgPath string, dep
327339 wg.Add(1)
328340 go func() {
329341 defer wg.Done()
330- section, _ := deps.loadRemote(ctx, name, entry)
342+ section, err := deps.loadRemote(ctx, name, entry)
343+ if err != nil && section.Error == "" {
344+ section.Error = err.Error()
345+ }
331346 remoteSections[i] = normalizeRouteListRemoteSection(section, name, entry)
332347 }()
333348 }
@@ -374,7 +389,9 @@ func collectRoutesStatusSections(ctx context.Context, cfgPath string, deps route
374389 deps.loadRemote = loadRoutesStatusRemoteSection
375390 }
376391
377- if resolved, ok := deps.explicitRemote(); ok {
392+ if resolved, ok, err := deps.explicitRemote(); err != nil {
393+ return nil, err
394+ } else if ok {
378395 section := loadRoutesStatusExplicitRemoteSection(ctx, deps, resolved)
379396 return []routeStatusSection{section}, nil
380397 }
@@ -390,7 +407,10 @@ func collectRoutesStatusSections(ctx context.Context, cfgPath string, deps route
390407 }
391408
392409 func loadRoutesStatusExplicitRemoteSection(ctx context.Context, deps routesStatusDeps, resolved *remote.ResolvedRemote) routeStatusSection {
393- section, _ := deps.loadRemote(ctx, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
410+ section, err := deps.loadRemote(ctx, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
411+ if err != nil && section.Error == "" {
412+ section.Error = err.Error()
413+ }
394414 return normalizeRouteStatusRemoteSection(section, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
395415 }
396416
@@ -398,6 +418,7 @@ func collectRoutesStatusAggregateSections(ctx context.Context, cfgPath string, d
398418
399419 type localResult struct {
400420 section routeStatusSection
421+ err error
401422 }
402423 type remotesResult struct {
403424 entries map[string]remote.RemoteEntry
@@ -408,16 +429,22 @@ func collectRoutesStatusAggregateSections(ctx context.Context, cfgPath string, d
408429 remotesCh := make(chan remotesResult, 1)
409430
410431 go func() {
411- section, _ := deps.loadLocal(ctx, cfgPath)
412- localCh <- localResult{section: section}
432+ section, err := deps.loadLocal(ctx, cfgPath)
433+ localCh <- localResult{section: section, err: err}
413434 }()
414435 go func() {
415436 entries, _, err := deps.listRemotes()
416437 remotesCh <- remotesResult{entries: entries, err: err}
417438 }()
418439
419- local := (<-localCh).section
440+ localRes := <-localCh
420441 remotes := <-remotesCh
442+ local := localRes.section
443+ if err := localRes.err; err != nil {
444+ if local.Error == "" {
445+ local.Error = err.Error()
446+ }
447+ }
421448
422449 sections := make([]routeStatusSection, 0, 1)
423450 sections = append(sections, normalizeRouteStatusLocalSection(local))
@@ -436,7 +463,10 @@ func collectRoutesStatusAggregateSections(ctx context.Context, cfgPath string, d
436463 wg.Add(1)
437464 go func() {
438465 defer wg.Done()
439- section, _ := deps.loadRemote(ctx, name, entry)
466+ section, err := deps.loadRemote(ctx, name, entry)
467+ if err != nil && section.Error == "" {
468+ section.Error = err.Error()
469+ }
440470 remoteSections[i] = normalizeRouteStatusRemoteSection(section, name, entry)
441471 }()
442472 }
@@ -603,10 +633,10 @@ func unresolvedRoutesTargetError(target string) string {
603633 return fmt.Sprintf("could not resolve remote target %q", target)
604634 }
605635
606-func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool) {
636+func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool, error) {
607637 target, ok := resolveRoutesExplicitTarget()
608638 if !ok {
609- return nil, false
639+ return nil, false, nil
610640 }
611641
612642 if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
@@ -614,12 +644,12 @@ func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool) {
614644 URL: target,
615645 Token: resolveRoutesTokenForTarget("", remote.RemoteEntry{}),
616646 InsecureTLS: resolveRoutesInsecureForTarget("", remote.RemoteEntry{}),
617- }, true
647+ }, true, nil
618648 }
619649
620650 remotes, err := remote.LoadRemotes("")
621651 if err != nil {
622- return nil, false
652+ return nil, false, err
623653 }
624654
625655 if remotes != nil {
@@ -629,11 +659,11 @@ func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool) {
629659 URL: entry.URL,
630660 Token: resolveRoutesTokenForTarget(target, entry),
631661 InsecureTLS: resolveRoutesInsecureForTarget(target, entry),
632- }, true
662+ }, true, nil
633663 }
634664 }
635665
636- return nil, false
666+ return nil, false, nil
637667 }
638668
639669 func resolveRoutesExplicitTarget() (string, bool) {
@@ -128,14 +128,14 @@ type routeStatusSection struct {
128 }128 }
129 129
130 type routesListDeps struct {130 type routesListDeps struct {
131- explicitRemote func() (*remote.ResolvedRemote, bool)131+ explicitRemote func() (*remote.ResolvedRemote, bool, error)
132 loadLocal func(context.Context, string) (routeListSection, error)132 loadLocal func(context.Context, string) (routeListSection, error)
133 listRemotes func() (map[string]remote.RemoteEntry, string, error)133 listRemotes func() (map[string]remote.RemoteEntry, string, error)
134 loadRemote func(context.Context, string, remote.RemoteEntry) (routeListSection, error)134 loadRemote func(context.Context, string, remote.RemoteEntry) (routeListSection, error)
135 }135 }
136 136
137 type routesStatusDeps struct {137 type routesStatusDeps struct {
138- explicitRemote func() (*remote.ResolvedRemote, bool)138+ explicitRemote func() (*remote.ResolvedRemote, bool, error)
139 loadLocal func(context.Context, string) (routeStatusSection, error)139 loadLocal func(context.Context, string) (routeStatusSection, error)
140 listRemotes func() (map[string]remote.RemoteEntry, string, error)140 listRemotes func() (map[string]remote.RemoteEntry, string, error)
141 loadRemote func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error)141 loadRemote func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error)
@@ -265,7 +265,9 @@ func collectRoutesListSections(ctx context.Context, cfgPath string, deps routesL
265 deps.loadRemote = loadRoutesListRemoteSection265 deps.loadRemote = loadRoutesListRemoteSection
266 }266 }
267 267
268- if resolved, ok := deps.explicitRemote(); ok {268+ if resolved, ok, err := deps.explicitRemote(); err != nil {
269+ return nil, err
270+ } else if ok {
269 section := loadRoutesListExplicitRemoteSection(ctx, deps, resolved)271 section := loadRoutesListExplicitRemoteSection(ctx, deps, resolved)
270 return []routeListSection{section}, nil272 return []routeListSection{section}, nil
271 }273 }
@@ -281,7 +283,10 @@ func collectRoutesListSections(ctx context.Context, cfgPath string, deps routesL
281 }283 }
282 284
283 func loadRoutesListExplicitRemoteSection(ctx context.Context, deps routesListDeps, resolved *remote.ResolvedRemote) routeListSection {285 func loadRoutesListExplicitRemoteSection(ctx context.Context, deps routesListDeps, resolved *remote.ResolvedRemote) routeListSection {
284- section, _ := deps.loadRemote(ctx, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})286+ section, err := deps.loadRemote(ctx, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
287+ if err != nil && section.Error == "" {
288+ section.Error = err.Error()
289+ }
285 return normalizeRouteListRemoteSection(section, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})290 return normalizeRouteListRemoteSection(section, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
286 }291 }
287 292
@@ -289,6 +294,7 @@ func collectRoutesListAggregateSections(ctx context.Context, cfgPath string, dep
289 294
290 type localResult struct {295 type localResult struct {
291 section routeListSection296 section routeListSection
297+ err error
292 }298 }
293 type remotesResult struct {299 type remotesResult struct {
294 entries map[string]remote.RemoteEntry300 entries map[string]remote.RemoteEntry
@@ -299,16 +305,22 @@ func collectRoutesListAggregateSections(ctx context.Context, cfgPath string, dep
299 remotesCh := make(chan remotesResult, 1)305 remotesCh := make(chan remotesResult, 1)
300 306
301 go func() {307 go func() {
302- section, _ := deps.loadLocal(ctx, cfgPath)308+ section, err := deps.loadLocal(ctx, cfgPath)
303- localCh <- localResult{section: section}309+ localCh <- localResult{section: section, err: err}
304 }()310 }()
305 go func() {311 go func() {
306 entries, _, err := deps.listRemotes()312 entries, _, err := deps.listRemotes()
307 remotesCh <- remotesResult{entries: entries, err: err}313 remotesCh <- remotesResult{entries: entries, err: err}
308 }()314 }()
309 315
310- local := (<-localCh).section316+ localRes := <-localCh
311 remotes := <-remotesCh317 remotes := <-remotesCh
318+ local := localRes.section
319+ if err := localRes.err; err != nil {
320+ if local.Error == "" {
321+ local.Error = err.Error()
322+ }
323+ }
312 324
313 sections := make([]routeListSection, 0, 1)325 sections := make([]routeListSection, 0, 1)
314 sections = append(sections, normalizeRouteListLocalSection(local))326 sections = append(sections, normalizeRouteListLocalSection(local))
@@ -327,7 +339,10 @@ func collectRoutesListAggregateSections(ctx context.Context, cfgPath string, dep
327 wg.Add(1)339 wg.Add(1)
328 go func() {340 go func() {
329 defer wg.Done()341 defer wg.Done()
330- section, _ := deps.loadRemote(ctx, name, entry)342+ section, err := deps.loadRemote(ctx, name, entry)
343+ if err != nil && section.Error == "" {
344+ section.Error = err.Error()
345+ }
331 remoteSections[i] = normalizeRouteListRemoteSection(section, name, entry)346 remoteSections[i] = normalizeRouteListRemoteSection(section, name, entry)
332 }()347 }()
333 }348 }
@@ -374,7 +389,9 @@ func collectRoutesStatusSections(ctx context.Context, cfgPath string, deps route
374 deps.loadRemote = loadRoutesStatusRemoteSection389 deps.loadRemote = loadRoutesStatusRemoteSection
375 }390 }
376 391
377- if resolved, ok := deps.explicitRemote(); ok {392+ if resolved, ok, err := deps.explicitRemote(); err != nil {
393+ return nil, err
394+ } else if ok {
378 section := loadRoutesStatusExplicitRemoteSection(ctx, deps, resolved)395 section := loadRoutesStatusExplicitRemoteSection(ctx, deps, resolved)
379 return []routeStatusSection{section}, nil396 return []routeStatusSection{section}, nil
380 }397 }
@@ -390,7 +407,10 @@ func collectRoutesStatusSections(ctx context.Context, cfgPath string, deps route
390 }407 }
391 408
392 func loadRoutesStatusExplicitRemoteSection(ctx context.Context, deps routesStatusDeps, resolved *remote.ResolvedRemote) routeStatusSection {409 func loadRoutesStatusExplicitRemoteSection(ctx context.Context, deps routesStatusDeps, resolved *remote.ResolvedRemote) routeStatusSection {
393- section, _ := deps.loadRemote(ctx, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})410+ section, err := deps.loadRemote(ctx, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
411+ if err != nil && section.Error == "" {
412+ section.Error = err.Error()
413+ }
394 return normalizeRouteStatusRemoteSection(section, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})414 return normalizeRouteStatusRemoteSection(section, resolved.DisplayName(), remote.RemoteEntry{URL: resolved.URL, Token: resolved.Token, InsecureTLS: resolved.InsecureTLS})
395 }415 }
396 416
@@ -398,6 +418,7 @@ func collectRoutesStatusAggregateSections(ctx context.Context, cfgPath string, d
398 418
399 type localResult struct {419 type localResult struct {
400 section routeStatusSection420 section routeStatusSection
421+ err error
401 }422 }
402 type remotesResult struct {423 type remotesResult struct {
403 entries map[string]remote.RemoteEntry424 entries map[string]remote.RemoteEntry
@@ -408,16 +429,22 @@ func collectRoutesStatusAggregateSections(ctx context.Context, cfgPath string, d
408 remotesCh := make(chan remotesResult, 1)429 remotesCh := make(chan remotesResult, 1)
409 430
410 go func() {431 go func() {
411- section, _ := deps.loadLocal(ctx, cfgPath)432+ section, err := deps.loadLocal(ctx, cfgPath)
412- localCh <- localResult{section: section}433+ localCh <- localResult{section: section, err: err}
413 }()434 }()
414 go func() {435 go func() {
415 entries, _, err := deps.listRemotes()436 entries, _, err := deps.listRemotes()
416 remotesCh <- remotesResult{entries: entries, err: err}437 remotesCh <- remotesResult{entries: entries, err: err}
417 }()438 }()
418 439
419- local := (<-localCh).section440+ localRes := <-localCh
420 remotes := <-remotesCh441 remotes := <-remotesCh
442+ local := localRes.section
443+ if err := localRes.err; err != nil {
444+ if local.Error == "" {
445+ local.Error = err.Error()
446+ }
447+ }
421 448
422 sections := make([]routeStatusSection, 0, 1)449 sections := make([]routeStatusSection, 0, 1)
423 sections = append(sections, normalizeRouteStatusLocalSection(local))450 sections = append(sections, normalizeRouteStatusLocalSection(local))
@@ -436,7 +463,10 @@ func collectRoutesStatusAggregateSections(ctx context.Context, cfgPath string, d
436 wg.Add(1)463 wg.Add(1)
437 go func() {464 go func() {
438 defer wg.Done()465 defer wg.Done()
439- section, _ := deps.loadRemote(ctx, name, entry)466+ section, err := deps.loadRemote(ctx, name, entry)
467+ if err != nil && section.Error == "" {
468+ section.Error = err.Error()
469+ }
440 remoteSections[i] = normalizeRouteStatusRemoteSection(section, name, entry)470 remoteSections[i] = normalizeRouteStatusRemoteSection(section, name, entry)
441 }()471 }()
442 }472 }
@@ -603,10 +633,10 @@ func unresolvedRoutesTargetError(target string) string {
603 return fmt.Sprintf("could not resolve remote target %q", target)633 return fmt.Sprintf("could not resolve remote target %q", target)
604 }634 }
605 635
606-func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool) {636+func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool, error) {
607 target, ok := resolveRoutesExplicitTarget()637 target, ok := resolveRoutesExplicitTarget()
608 if !ok {638 if !ok {
609- return nil, false639+ return nil, false, nil
610 }640 }
611 641
612 if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {642 if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
@@ -614,12 +644,12 @@ func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool) {
614 URL: target,644 URL: target,
615 Token: resolveRoutesTokenForTarget("", remote.RemoteEntry{}),645 Token: resolveRoutesTokenForTarget("", remote.RemoteEntry{}),
616 InsecureTLS: resolveRoutesInsecureForTarget("", remote.RemoteEntry{}),646 InsecureTLS: resolveRoutesInsecureForTarget("", remote.RemoteEntry{}),
617- }, true647+ }, true, nil
618 }648 }
619 649
620 remotes, err := remote.LoadRemotes("")650 remotes, err := remote.LoadRemotes("")
621 if err != nil {651 if err != nil {
622- return nil, false652+ return nil, false, err
623 }653 }
624 654
625 if remotes != nil {655 if remotes != nil {
@@ -629,11 +659,11 @@ func resolveRoutesExplicitRemote() (*remote.ResolvedRemote, bool) {
629 URL: entry.URL,659 URL: entry.URL,
630 Token: resolveRoutesTokenForTarget(target, entry),660 Token: resolveRoutesTokenForTarget(target, entry),
631 InsecureTLS: resolveRoutesInsecureForTarget(target, entry),661 InsecureTLS: resolveRoutesInsecureForTarget(target, entry),
632- }, true662+ }, true, nil
633 }663 }
634 }664 }
635 665
636- return nil, false666+ return nil, false, nil
637 }667 }
638 668
639 func resolveRoutesExplicitTarget() (string, bool) {669 func resolveRoutesExplicitTarget() (string, bool) {
M internal/adapters/in/cli/routes_test.go +156 -36
@@ -249,13 +249,43 @@ func TestResolveRoutesExplicitRemote_AllowsAdHocURLWhenSavedRemotesConfigUnreada
249249 configPath := filepath.Join(configHome, "gordon", "remotes.toml")
250250 require.NoError(t, os.MkdirAll(configPath, 0o755))
251251
252- resolved, ok := resolveRoutesExplicitRemote()
252+ resolved, ok, err := resolveRoutesExplicitRemote()
253253 require.True(t, ok)
254+ require.NoError(t, err)
254255 require.NotNil(t, resolved)
255256 assert.Equal(t, "https://ad-hoc.example.com", resolved.URL)
256257 assert.Empty(t, resolved.Name)
257258 }
258259
260+func TestResolveRoutesExplicitRemote_ReturnsErrorForUnreadableNamedRemoteConfig(t *testing.T) {
261+ originalRemoteFlag := remoteFlag
262+ originalTokenFlag := tokenFlag
263+ originalInsecureTLSFlag := insecureTLSFlag
264+ t.Cleanup(func() {
265+ remoteFlag = originalRemoteFlag
266+ tokenFlag = originalTokenFlag
267+ insecureTLSFlag = originalInsecureTLSFlag
268+ })
269+
270+ remoteFlag = "missing-remote"
271+ tokenFlag = ""
272+ insecureTLSFlag = false
273+
274+ configHome := t.TempDir()
275+ t.Setenv("XDG_CONFIG_HOME", configHome)
276+ t.Setenv("HOME", t.TempDir())
277+ t.Setenv("GORDON_REMOTE", "")
278+
279+ configPath := filepath.Join(configHome, "gordon", "remotes.toml")
280+ require.NoError(t, os.MkdirAll(configPath, 0o755))
281+
282+ resolved, ok, err := resolveRoutesExplicitRemote()
283+ require.Error(t, err)
284+ require.False(t, ok)
285+ require.Nil(t, resolved)
286+ assert.Contains(t, err.Error(), "failed to read remotes")
287+}
288+
259289 func TestRouteStatusTitle_PreservesProbeFailureError(t *testing.T) {
260290 item := routeStatusItem{
261291 Domain: "app.example.com",
@@ -325,8 +355,8 @@ func TestRunRoutesShow_TextIncludesHealthWarning(t *testing.T) {
325355
326356 func TestCollectRoutesListSections_DefaultModeIncludesLocalThenSortedRemotes(t *testing.T) {
327357 testsDeps := routesListDeps{
328- explicitRemote: func() (*remote.ResolvedRemote, bool) {
329- return nil, false
358+ explicitRemote: func() (*remote.ResolvedRemote, bool, error) {
359+ return nil, false, nil
330360 },
331361 loadLocal: func(context.Context, string) (routeListSection, error) {
332362 return routeListSection{
@@ -378,11 +408,11 @@ func TestCollectRoutesListSections_DefaultModeIncludesLocalThenSortedRemotes(t *
378408
379409 func TestCollectRoutesListSections_ExplicitRemoteSkipsAggregate(t *testing.T) {
380410 testsDeps := routesListDeps{
381- explicitRemote: func() (*remote.ResolvedRemote, bool) {
411+ explicitRemote: func() (*remote.ResolvedRemote, bool, error) {
382412 return &remote.ResolvedRemote{
383413 Name: "igor",
384414 URL: "https://gordon.supri.xyz",
385- }, true
415+ }, true, nil
386416 },
387417 loadLocal: func(context.Context, string) (routeListSection, error) {
388418 t.Fatal("loadLocal should not be called when an explicit remote is selected")
@@ -413,23 +443,7 @@ func TestCollectRoutesListSections_ExplicitRemoteSkipsAggregate(t *testing.T) {
413443 assert.Equal(t, "test.supri.xyz", sections[0].Routes[0].Domain)
414444 }
415445
416-func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *testing.T) {
417- originalRemoteFlag := remoteFlag
418- originalTokenFlag := tokenFlag
419- originalInsecureTLSFlag := insecureTLSFlag
420- t.Cleanup(func() {
421- remoteFlag = originalRemoteFlag
422- tokenFlag = originalTokenFlag
423- insecureTLSFlag = originalInsecureTLSFlag
424- })
425-
426- remoteFlag = "missing-remote"
427- tokenFlag = ""
428- insecureTLSFlag = false
429- configHome := t.TempDir()
430- t.Setenv("XDG_CONFIG_HOME", configHome)
431- t.Setenv("HOME", t.TempDir())
432- t.Setenv("GORDON_REMOTE", "")
446+func TestCollectRoutesSections_ExplicitRemoteResolutionErrorReturnsError(t *testing.T) {
433447
434448 t.Run("list", func(t *testing.T) {
435449 var loadLocalCalls atomic.Int32
@@ -437,7 +451,9 @@ func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *
437451 var loadRemoteCalls atomic.Int32
438452
439453 deps := routesListDeps{
440- explicitRemote: resolveRoutesExplicitRemote,
454+ explicitRemote: func() (*remote.ResolvedRemote, bool, error) {
455+ return nil, false, errors.New("failed to read remotes: boom")
456+ },
441457 loadLocal: func(context.Context, string) (routeListSection, error) {
442458 loadLocalCalls.Add(1)
443459 return routeListSection{}, nil
@@ -454,12 +470,9 @@ func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *
454470
455471 sections, err := collectRoutesListSections(context.Background(), "", deps)
456472
457- require.NoError(t, err)
458- require.Len(t, sections, 1)
459- assert.Equal(t, "remote", sections[0].Kind)
460- assert.Equal(t, "missing-remote", sections[0].Name)
461- require.NotEmpty(t, sections[0].Error)
462- assert.Contains(t, sections[0].Error, "missing-remote")
473+ require.Error(t, err)
474+ require.Nil(t, sections)
475+ assert.Contains(t, err.Error(), "failed to read remotes")
463476 assert.Zero(t, loadLocalCalls.Load())
464477 assert.Zero(t, listRemotesCalls.Load())
465478 assert.Zero(t, loadRemoteCalls.Load())
@@ -471,7 +484,9 @@ func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *
471484 var loadRemoteCalls atomic.Int32
472485
473486 deps := routesStatusDeps{
474- explicitRemote: resolveRoutesExplicitRemote,
487+ explicitRemote: func() (*remote.ResolvedRemote, bool, error) {
488+ return nil, false, errors.New("failed to read remotes: boom")
489+ },
475490 loadLocal: func(context.Context, string) (routeStatusSection, error) {
476491 loadLocalCalls.Add(1)
477492 return routeStatusSection{}, nil
@@ -488,18 +503,123 @@ func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *
488503
489504 sections, err := collectRoutesStatusSections(context.Background(), "", deps)
490505
491- require.NoError(t, err)
492- require.Len(t, sections, 1)
493- assert.Equal(t, "remote", sections[0].Kind)
494- assert.Equal(t, "missing-remote", sections[0].Name)
495- require.NotEmpty(t, sections[0].Error)
496- assert.Contains(t, sections[0].Error, "missing-remote")
506+ require.Error(t, err)
507+ require.Nil(t, sections)
508+ assert.Contains(t, err.Error(), "failed to read remotes")
497509 assert.Zero(t, loadLocalCalls.Load())
498510 assert.Zero(t, listRemotesCalls.Load())
499511 assert.Zero(t, loadRemoteCalls.Load())
500512 })
501513 }
502514
515+func TestLoadRoutesListExplicitRemoteSection_PropagatesLoaderError(t *testing.T) {
516+ section := loadRoutesListExplicitRemoteSection(context.Background(), routesListDeps{
517+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeListSection, error) {
518+ return routeListSection{}, errors.New("boom")
519+ },
520+ }, &remote.ResolvedRemote{Name: "igor", URL: "https://gordon.supri.xyz"})
521+
522+ assert.Equal(t, "remote", section.Kind)
523+ assert.Equal(t, "igor", section.Name)
524+ assert.Equal(t, "https://gordon.supri.xyz", section.URL)
525+ assert.Equal(t, "boom", section.Error)
526+}
527+
528+func TestLoadRoutesStatusExplicitRemoteSection_PropagatesLoaderError(t *testing.T) {
529+ section := loadRoutesStatusExplicitRemoteSection(context.Background(), routesStatusDeps{
530+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error) {
531+ return routeStatusSection{}, errors.New("boom")
532+ },
533+ }, &remote.ResolvedRemote{Name: "igor", URL: "https://gordon.supri.xyz"})
534+
535+ assert.Equal(t, "remote", section.Kind)
536+ assert.Equal(t, "igor", section.Name)
537+ assert.Equal(t, "https://gordon.supri.xyz", section.URL)
538+ assert.Equal(t, "boom", section.Error)
539+}
540+
541+func TestCollectRoutesListAggregateSections_EncodesLoaderErrors(t *testing.T) {
542+ t.Run("local loader error", func(t *testing.T) {
543+ sections, err := collectRoutesListAggregateSections(context.Background(), "config.toml", routesListDeps{
544+ loadLocal: func(context.Context, string) (routeListSection, error) {
545+ return routeListSection{Kind: "local", Name: "local"}, errors.New("local failed")
546+ },
547+ listRemotes: func() (map[string]remote.RemoteEntry, string, error) {
548+ return map[string]remote.RemoteEntry{}, "", nil
549+ },
550+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeListSection, error) {
551+ return routeListSection{Kind: "remote", Name: "igor", URL: "https://gordon.supri.xyz"}, nil
552+ },
553+ })
554+
555+ require.NoError(t, err)
556+ require.Len(t, sections, 1)
557+ assert.Equal(t, "local", sections[0].Kind)
558+ assert.Equal(t, "local failed", sections[0].Error)
559+ })
560+
561+ t.Run("remote loader error", func(t *testing.T) {
562+ sections, err := collectRoutesListAggregateSections(context.Background(), "config.toml", routesListDeps{
563+ loadLocal: func(context.Context, string) (routeListSection, error) {
564+ return routeListSection{Kind: "local", Name: "local"}, nil
565+ },
566+ listRemotes: func() (map[string]remote.RemoteEntry, string, error) {
567+ return map[string]remote.RemoteEntry{"igor": {URL: "https://gordon.supri.xyz"}}, "", nil
568+ },
569+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeListSection, error) {
570+ return routeListSection{Kind: "remote", Name: "igor", URL: "https://gordon.supri.xyz"}, errors.New("remote failed")
571+ },
572+ })
573+
574+ require.NoError(t, err)
575+ require.Len(t, sections, 2)
576+ assert.Equal(t, "local", sections[0].Kind)
577+ assert.Equal(t, "remote", sections[1].Kind)
578+ assert.Equal(t, "remote failed", sections[1].Error)
579+ })
580+}
581+
582+func TestCollectRoutesStatusAggregateSections_EncodesLoaderErrors(t *testing.T) {
583+ t.Run("local loader error", func(t *testing.T) {
584+ sections, err := collectRoutesStatusAggregateSections(context.Background(), "config.toml", routesStatusDeps{
585+ loadLocal: func(context.Context, string) (routeStatusSection, error) {
586+ return routeStatusSection{Kind: "local", Name: "local"}, errors.New("local failed")
587+ },
588+ listRemotes: func() (map[string]remote.RemoteEntry, string, error) {
589+ return map[string]remote.RemoteEntry{}, "", nil
590+ },
591+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error) {
592+ return routeStatusSection{Kind: "remote", Name: "igor", URL: "https://gordon.supri.xyz"}, nil
593+ },
594+ })
595+
596+ require.NoError(t, err)
597+ require.Len(t, sections, 1)
598+ assert.Equal(t, "local", sections[0].Kind)
599+ assert.Equal(t, "local failed", sections[0].Error)
600+ })
601+
602+ t.Run("remote loader error", func(t *testing.T) {
603+ sections, err := collectRoutesStatusAggregateSections(context.Background(), "config.toml", routesStatusDeps{
604+ loadLocal: func(context.Context, string) (routeStatusSection, error) {
605+ return routeStatusSection{Kind: "local", Name: "local"}, nil
606+ },
607+ listRemotes: func() (map[string]remote.RemoteEntry, string, error) {
608+ return map[string]remote.RemoteEntry{"igor": {URL: "https://gordon.supri.xyz"}}, "", nil
609+ },
610+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error) {
611+ return routeStatusSection{Kind: "remote", Name: "igor", URL: "https://gordon.supri.xyz"}, errors.New("remote failed")
612+ },
613+ })
614+
615+ require.NoError(t, err)
616+ require.Len(t, sections, 2)
617+ assert.Equal(t, "local", sections[0].Kind)
618+ assert.Equal(t, "remote", sections[1].Kind)
619+ assert.Equal(t, "remote failed", sections[1].Error)
620+ })
621+}
622+
503623 func TestRenderRoutesStatusSections_IncludesSectionHeadingsAndErrors(t *testing.T) {
504624 sections := []routeStatusSection{
505625 {
@@ -249,13 +249,43 @@ func TestResolveRoutesExplicitRemote_AllowsAdHocURLWhenSavedRemotesConfigUnreada
249 configPath := filepath.Join(configHome, "gordon", "remotes.toml")249 configPath := filepath.Join(configHome, "gordon", "remotes.toml")
250 require.NoError(t, os.MkdirAll(configPath, 0o755))250 require.NoError(t, os.MkdirAll(configPath, 0o755))
251 251
252- resolved, ok := resolveRoutesExplicitRemote()252+ resolved, ok, err := resolveRoutesExplicitRemote()
253 require.True(t, ok)253 require.True(t, ok)
254+ require.NoError(t, err)
254 require.NotNil(t, resolved)255 require.NotNil(t, resolved)
255 assert.Equal(t, "https://ad-hoc.example.com", resolved.URL)256 assert.Equal(t, "https://ad-hoc.example.com", resolved.URL)
256 assert.Empty(t, resolved.Name)257 assert.Empty(t, resolved.Name)
257 }258 }
258 259
260+func TestResolveRoutesExplicitRemote_ReturnsErrorForUnreadableNamedRemoteConfig(t *testing.T) {
261+ originalRemoteFlag := remoteFlag
262+ originalTokenFlag := tokenFlag
263+ originalInsecureTLSFlag := insecureTLSFlag
264+ t.Cleanup(func() {
265+ remoteFlag = originalRemoteFlag
266+ tokenFlag = originalTokenFlag
267+ insecureTLSFlag = originalInsecureTLSFlag
268+ })
269+
270+ remoteFlag = "missing-remote"
271+ tokenFlag = ""
272+ insecureTLSFlag = false
273+
274+ configHome := t.TempDir()
275+ t.Setenv("XDG_CONFIG_HOME", configHome)
276+ t.Setenv("HOME", t.TempDir())
277+ t.Setenv("GORDON_REMOTE", "")
278+
279+ configPath := filepath.Join(configHome, "gordon", "remotes.toml")
280+ require.NoError(t, os.MkdirAll(configPath, 0o755))
281+
282+ resolved, ok, err := resolveRoutesExplicitRemote()
283+ require.Error(t, err)
284+ require.False(t, ok)
285+ require.Nil(t, resolved)
286+ assert.Contains(t, err.Error(), "failed to read remotes")
287+}
288+
259 func TestRouteStatusTitle_PreservesProbeFailureError(t *testing.T) {289 func TestRouteStatusTitle_PreservesProbeFailureError(t *testing.T) {
260 item := routeStatusItem{290 item := routeStatusItem{
261 Domain: "app.example.com",291 Domain: "app.example.com",
@@ -325,8 +355,8 @@ func TestRunRoutesShow_TextIncludesHealthWarning(t *testing.T) {
325 355
326 func TestCollectRoutesListSections_DefaultModeIncludesLocalThenSortedRemotes(t *testing.T) {356 func TestCollectRoutesListSections_DefaultModeIncludesLocalThenSortedRemotes(t *testing.T) {
327 testsDeps := routesListDeps{357 testsDeps := routesListDeps{
328- explicitRemote: func() (*remote.ResolvedRemote, bool) {358+ explicitRemote: func() (*remote.ResolvedRemote, bool, error) {
329- return nil, false359+ return nil, false, nil
330 },360 },
331 loadLocal: func(context.Context, string) (routeListSection, error) {361 loadLocal: func(context.Context, string) (routeListSection, error) {
332 return routeListSection{362 return routeListSection{
@@ -378,11 +408,11 @@ func TestCollectRoutesListSections_DefaultModeIncludesLocalThenSortedRemotes(t *
378 408
379 func TestCollectRoutesListSections_ExplicitRemoteSkipsAggregate(t *testing.T) {409 func TestCollectRoutesListSections_ExplicitRemoteSkipsAggregate(t *testing.T) {
380 testsDeps := routesListDeps{410 testsDeps := routesListDeps{
381- explicitRemote: func() (*remote.ResolvedRemote, bool) {411+ explicitRemote: func() (*remote.ResolvedRemote, bool, error) {
382 return &remote.ResolvedRemote{412 return &remote.ResolvedRemote{
383 Name: "igor",413 Name: "igor",
384 URL: "https://gordon.supri.xyz",414 URL: "https://gordon.supri.xyz",
385- }, true415+ }, true, nil
386 },416 },
387 loadLocal: func(context.Context, string) (routeListSection, error) {417 loadLocal: func(context.Context, string) (routeListSection, error) {
388 t.Fatal("loadLocal should not be called when an explicit remote is selected")418 t.Fatal("loadLocal should not be called when an explicit remote is selected")
@@ -413,23 +443,7 @@ func TestCollectRoutesListSections_ExplicitRemoteSkipsAggregate(t *testing.T) {
413 assert.Equal(t, "test.supri.xyz", sections[0].Routes[0].Domain)443 assert.Equal(t, "test.supri.xyz", sections[0].Routes[0].Domain)
414 }444 }
415 445
416-func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *testing.T) {446+func TestCollectRoutesSections_ExplicitRemoteResolutionErrorReturnsError(t *testing.T) {
417- originalRemoteFlag := remoteFlag
418- originalTokenFlag := tokenFlag
419- originalInsecureTLSFlag := insecureTLSFlag
420- t.Cleanup(func() {
421- remoteFlag = originalRemoteFlag
422- tokenFlag = originalTokenFlag
423- insecureTLSFlag = originalInsecureTLSFlag
424- })
425-
426- remoteFlag = "missing-remote"
427- tokenFlag = ""
428- insecureTLSFlag = false
429- configHome := t.TempDir()
430- t.Setenv("XDG_CONFIG_HOME", configHome)
431- t.Setenv("HOME", t.TempDir())
432- t.Setenv("GORDON_REMOTE", "")
433 447
434 t.Run("list", func(t *testing.T) {448 t.Run("list", func(t *testing.T) {
435 var loadLocalCalls atomic.Int32449 var loadLocalCalls atomic.Int32
@@ -437,7 +451,9 @@ func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *
437 var loadRemoteCalls atomic.Int32451 var loadRemoteCalls atomic.Int32
438 452
439 deps := routesListDeps{453 deps := routesListDeps{
440- explicitRemote: resolveRoutesExplicitRemote,454+ explicitRemote: func() (*remote.ResolvedRemote, bool, error) {
455+ return nil, false, errors.New("failed to read remotes: boom")
456+ },
441 loadLocal: func(context.Context, string) (routeListSection, error) {457 loadLocal: func(context.Context, string) (routeListSection, error) {
442 loadLocalCalls.Add(1)458 loadLocalCalls.Add(1)
443 return routeListSection{}, nil459 return routeListSection{}, nil
@@ -454,12 +470,9 @@ func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *
454 470
455 sections, err := collectRoutesListSections(context.Background(), "", deps)471 sections, err := collectRoutesListSections(context.Background(), "", deps)
456 472
457- require.NoError(t, err)473+ require.Error(t, err)
458- require.Len(t, sections, 1)474+ require.Nil(t, sections)
459- assert.Equal(t, "remote", sections[0].Kind)475+ assert.Contains(t, err.Error(), "failed to read remotes")
460- assert.Equal(t, "missing-remote", sections[0].Name)
461- require.NotEmpty(t, sections[0].Error)
462- assert.Contains(t, sections[0].Error, "missing-remote")
463 assert.Zero(t, loadLocalCalls.Load())476 assert.Zero(t, loadLocalCalls.Load())
464 assert.Zero(t, listRemotesCalls.Load())477 assert.Zero(t, listRemotesCalls.Load())
465 assert.Zero(t, loadRemoteCalls.Load())478 assert.Zero(t, loadRemoteCalls.Load())
@@ -471,7 +484,9 @@ func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *
471 var loadRemoteCalls atomic.Int32484 var loadRemoteCalls atomic.Int32
472 485
473 deps := routesStatusDeps{486 deps := routesStatusDeps{
474- explicitRemote: resolveRoutesExplicitRemote,487+ explicitRemote: func() (*remote.ResolvedRemote, bool, error) {
488+ return nil, false, errors.New("failed to read remotes: boom")
489+ },
475 loadLocal: func(context.Context, string) (routeStatusSection, error) {490 loadLocal: func(context.Context, string) (routeStatusSection, error) {
476 loadLocalCalls.Add(1)491 loadLocalCalls.Add(1)
477 return routeStatusSection{}, nil492 return routeStatusSection{}, nil
@@ -488,18 +503,123 @@ func TestCollectRoutesSections_ExplicitUnknownNamedTargetReturnsErrorSection(t *
488 503
489 sections, err := collectRoutesStatusSections(context.Background(), "", deps)504 sections, err := collectRoutesStatusSections(context.Background(), "", deps)
490 505
491- require.NoError(t, err)506+ require.Error(t, err)
492- require.Len(t, sections, 1)507+ require.Nil(t, sections)
493- assert.Equal(t, "remote", sections[0].Kind)508+ assert.Contains(t, err.Error(), "failed to read remotes")
494- assert.Equal(t, "missing-remote", sections[0].Name)
495- require.NotEmpty(t, sections[0].Error)
496- assert.Contains(t, sections[0].Error, "missing-remote")
497 assert.Zero(t, loadLocalCalls.Load())509 assert.Zero(t, loadLocalCalls.Load())
498 assert.Zero(t, listRemotesCalls.Load())510 assert.Zero(t, listRemotesCalls.Load())
499 assert.Zero(t, loadRemoteCalls.Load())511 assert.Zero(t, loadRemoteCalls.Load())
500 })512 })
501 }513 }
502 514
515+func TestLoadRoutesListExplicitRemoteSection_PropagatesLoaderError(t *testing.T) {
516+ section := loadRoutesListExplicitRemoteSection(context.Background(), routesListDeps{
517+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeListSection, error) {
518+ return routeListSection{}, errors.New("boom")
519+ },
520+ }, &remote.ResolvedRemote{Name: "igor", URL: "https://gordon.supri.xyz"})
521+
522+ assert.Equal(t, "remote", section.Kind)
523+ assert.Equal(t, "igor", section.Name)
524+ assert.Equal(t, "https://gordon.supri.xyz", section.URL)
525+ assert.Equal(t, "boom", section.Error)
526+}
527+
528+func TestLoadRoutesStatusExplicitRemoteSection_PropagatesLoaderError(t *testing.T) {
529+ section := loadRoutesStatusExplicitRemoteSection(context.Background(), routesStatusDeps{
530+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error) {
531+ return routeStatusSection{}, errors.New("boom")
532+ },
533+ }, &remote.ResolvedRemote{Name: "igor", URL: "https://gordon.supri.xyz"})
534+
535+ assert.Equal(t, "remote", section.Kind)
536+ assert.Equal(t, "igor", section.Name)
537+ assert.Equal(t, "https://gordon.supri.xyz", section.URL)
538+ assert.Equal(t, "boom", section.Error)
539+}
540+
541+func TestCollectRoutesListAggregateSections_EncodesLoaderErrors(t *testing.T) {
542+ t.Run("local loader error", func(t *testing.T) {
543+ sections, err := collectRoutesListAggregateSections(context.Background(), "config.toml", routesListDeps{
544+ loadLocal: func(context.Context, string) (routeListSection, error) {
545+ return routeListSection{Kind: "local", Name: "local"}, errors.New("local failed")
546+ },
547+ listRemotes: func() (map[string]remote.RemoteEntry, string, error) {
548+ return map[string]remote.RemoteEntry{}, "", nil
549+ },
550+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeListSection, error) {
551+ return routeListSection{Kind: "remote", Name: "igor", URL: "https://gordon.supri.xyz"}, nil
552+ },
553+ })
554+
555+ require.NoError(t, err)
556+ require.Len(t, sections, 1)
557+ assert.Equal(t, "local", sections[0].Kind)
558+ assert.Equal(t, "local failed", sections[0].Error)
559+ })
560+
561+ t.Run("remote loader error", func(t *testing.T) {
562+ sections, err := collectRoutesListAggregateSections(context.Background(), "config.toml", routesListDeps{
563+ loadLocal: func(context.Context, string) (routeListSection, error) {
564+ return routeListSection{Kind: "local", Name: "local"}, nil
565+ },
566+ listRemotes: func() (map[string]remote.RemoteEntry, string, error) {
567+ return map[string]remote.RemoteEntry{"igor": {URL: "https://gordon.supri.xyz"}}, "", nil
568+ },
569+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeListSection, error) {
570+ return routeListSection{Kind: "remote", Name: "igor", URL: "https://gordon.supri.xyz"}, errors.New("remote failed")
571+ },
572+ })
573+
574+ require.NoError(t, err)
575+ require.Len(t, sections, 2)
576+ assert.Equal(t, "local", sections[0].Kind)
577+ assert.Equal(t, "remote", sections[1].Kind)
578+ assert.Equal(t, "remote failed", sections[1].Error)
579+ })
580+}
581+
582+func TestCollectRoutesStatusAggregateSections_EncodesLoaderErrors(t *testing.T) {
583+ t.Run("local loader error", func(t *testing.T) {
584+ sections, err := collectRoutesStatusAggregateSections(context.Background(), "config.toml", routesStatusDeps{
585+ loadLocal: func(context.Context, string) (routeStatusSection, error) {
586+ return routeStatusSection{Kind: "local", Name: "local"}, errors.New("local failed")
587+ },
588+ listRemotes: func() (map[string]remote.RemoteEntry, string, error) {
589+ return map[string]remote.RemoteEntry{}, "", nil
590+ },
591+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error) {
592+ return routeStatusSection{Kind: "remote", Name: "igor", URL: "https://gordon.supri.xyz"}, nil
593+ },
594+ })
595+
596+ require.NoError(t, err)
597+ require.Len(t, sections, 1)
598+ assert.Equal(t, "local", sections[0].Kind)
599+ assert.Equal(t, "local failed", sections[0].Error)
600+ })
601+
602+ t.Run("remote loader error", func(t *testing.T) {
603+ sections, err := collectRoutesStatusAggregateSections(context.Background(), "config.toml", routesStatusDeps{
604+ loadLocal: func(context.Context, string) (routeStatusSection, error) {
605+ return routeStatusSection{Kind: "local", Name: "local"}, nil
606+ },
607+ listRemotes: func() (map[string]remote.RemoteEntry, string, error) {
608+ return map[string]remote.RemoteEntry{"igor": {URL: "https://gordon.supri.xyz"}}, "", nil
609+ },
610+ loadRemote: func(context.Context, string, remote.RemoteEntry) (routeStatusSection, error) {
611+ return routeStatusSection{Kind: "remote", Name: "igor", URL: "https://gordon.supri.xyz"}, errors.New("remote failed")
612+ },
613+ })
614+
615+ require.NoError(t, err)
616+ require.Len(t, sections, 2)
617+ assert.Equal(t, "local", sections[0].Kind)
618+ assert.Equal(t, "remote", sections[1].Kind)
619+ assert.Equal(t, "remote failed", sections[1].Error)
620+ })
621+}
622+
503 func TestRenderRoutesStatusSections_IncludesSectionHeadingsAndErrors(t *testing.T) {623 func TestRenderRoutesStatusSections_IncludesSectionHeadingsAndErrors(t *testing.T) {
504 sections := []routeStatusSection{624 sections := []routeStatusSection{
505 {625 {

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 7

Tags 106

↑↓ moveenter openesc close