v0.50.1 a637010

interactive_model_refresh_test.go RawHistoryFolder

1package modes
2
3import (
4	"context"
5	"errors"
6	"strings"
7	"testing"
8	"time"
9)
10
11func awaitModelRefresh(t *testing.T, i *Interactive) modelRefreshResult {
12	t.Helper()
13	select {
14	case result := <-i.modelRefresh:
15		return result
16	case <-time.After(5 * time.Second):
17		t.Fatal("refresh result never delivered")
18	}
19	return modelRefreshResult{}
20}
21
22func TestModelPickerRefreshesCustomDiscoveryWithoutLlama(t *testing.T) {
23	calls := make(chan struct{}, 2)
24	i := &Interactive{
25		cfg: InteractiveConfig{
26			CustomDiscoveryConfigured: func() bool { return true },
27			RefreshCustomProviderModels: func(ctx context.Context) error {
28				if _, ok := ctx.Deadline(); !ok {
29					t.Error("custom refresh has no deadline")
30				}
31				calls <- struct{}{}
32				return nil
33			},
34			RefreshLlamaCPPModels: func(context.Context) error { t.Error("llama refreshed while unconfigured"); return nil },
35		},
36		modelRefresh: make(chan modelRefreshResult, 1),
37		modelDialog:  newModelDialog(),
38	}
39
40	i.runSlash(context.Background(), "/model")
41	if i.modelDialog.Active() || i.statusOK != "Refreshing models" || !i.modelRefreshing {
42		t.Fatalf("picker opened early or status wrong: active=%v status=%q", i.modelDialog.Active(), i.statusOK)
43	}
44	result := awaitModelRefresh(t, i)
45	<-calls
46	i.openModelPickerAfterRefresh(result.err)
47	if !i.modelDialog.Active() {
48		t.Fatal("picker did not open after refresh")
49	}
50}
51
52func TestModelPickerCustomDiscoveryOptOutOpensImmediately(t *testing.T) {
53	i := &Interactive{
54		cfg: InteractiveConfig{
55			CustomDiscoveryConfigured:   func() bool { return false },
56			RefreshCustomProviderModels: func(context.Context) error { t.Error("unexpected refresh"); return nil },
57		},
58		modelRefresh: make(chan modelRefreshResult, 1),
59		modelDialog:  newModelDialog(),
60	}
61	i.runSlash(context.Background(), "/model")
62	if !i.modelDialog.Active() || i.modelRefreshing {
63		t.Fatal("picker did not open immediately without opt-in")
64	}
65}
66
67func TestModelPickerRunsLlamaAndCustomIndependentlyAndJoinsErrors(t *testing.T) {
68	var order []string
69	llamaErr, customErr := errors.New("llama down"), errors.New("custom down")
70	i := &Interactive{
71		llamaConfigured: true,
72		cfg: InteractiveConfig{
73			RefreshLlamaCPPModels: func(ctx context.Context) error {
74				order = append(order, "llama")
75				return llamaErr
76			},
77			CustomDiscoveryConfigured: func() bool { return true },
78			RefreshCustomProviderModels: func(ctx context.Context) error {
79				// A failed sibling must not consume this refresh's own context.
80				if ctx.Err() != nil {
81					t.Errorf("custom refresh context already done: %v", ctx.Err())
82				}
83				order = append(order, "custom")
84				return customErr
85			},
86		},
87		modelRefresh: make(chan modelRefreshResult, 1),
88		modelDialog:  newModelDialog(),
89	}
90	i.runSlash(context.Background(), "/model")
91	result := awaitModelRefresh(t, i)
92	if strings.Join(order, ",") != "llama,custom" {
93		t.Fatalf("refresh order = %v", order)
94	}
95	if !errors.Is(result.err, llamaErr) || !errors.Is(result.err, customErr) {
96		t.Fatalf("errors not joined: %v", result.err)
97	}
98	i.openModelPickerAfterRefresh(result.err)
99	if !i.modelDialog.Active() {
100		t.Fatal("picker unusable after refresh errors")
101	}
102	if !strings.HasPrefix(i.statusErr, "model refresh: ") || !strings.Contains(i.statusErr, "llama down") || !strings.Contains(i.statusErr, "custom down") {
103		t.Fatalf("status = %q", i.statusErr)
104	}
105}
106
107func TestModelPickerRefreshDoesNotLeakWhenContextCancelled(t *testing.T) {
108	ctx, cancel := context.WithCancel(context.Background())
109	done := make(chan struct{})
110	i := &Interactive{
111		cfg: InteractiveConfig{
112			CustomDiscoveryConfigured: func() bool { return true },
113			RefreshCustomProviderModels: func(ctx context.Context) error {
114				cancel()
115				close(done)
116				return nil
117			},
118		},
119		// Unbuffered and never read: delivery must give up on ctx.Done.
120		modelRefresh: make(chan modelRefreshResult),
121		modelDialog:  newModelDialog(),
122	}
123	i.runSlash(ctx, "/model")
124	select {
125	case <-done:
126	case <-time.After(5 * time.Second):
127		t.Fatal("refresh not invoked")
128	}
129	// The goroutine must exit without a receiver; a leaked sender would keep
130	// this send from ever being received by a fresh reader.
131	select {
132	case r := <-i.modelRefresh:
133		t.Fatalf("result delivered after cancellation: %+v", r)
134	case <-time.After(100 * time.Millisecond):
135	}
136}

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 1

Tags 476

↑↓ moveenter openesc close