v0.45.0 cb619f7

resident_tools_test.go RawHistoryFolder

1package tools
2
3import (
4	"context"
5	"encoding/json"
6	"errors"
7	"strings"
8	"sync"
9	"sync/atomic"
10	"testing"
11	"time"
12
13	"github.com/bnema/zut/packages/agent/subagents"
14	"github.com/bnema/zut/packages/core"
15	"github.com/bnema/zut/packages/provider"
16)
17
18func TestResidentStatusRetrievesFailedResultWithoutExecution(t *testing.T) {
19	failure := errors.New("worker failed")
20	manager := subagents.NewResidentManager(t.TempDir(), func(_ subagents.ResidentChildSpec, journal *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
21		return subagents.ResidentTurnRunner(func(context.Context, string) error {
22			if err := journal.RecordAgentEvent(core.EvAssistantMessage{Message: provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "partial finding"}}}}); err != nil {
23				return err
24			}
25			return failure
26		}), nil
27	})
28	t.Cleanup(func() { _ = manager.Close(context.Background()) })
29	spec := subagents.ResidentChildSpec{ID: "failed-status", InitialTurnID: "initial", SessionID: "session", Provider: "openai", Model: "test", Required: true}
30	completed, cancel := manager.WatchCompletion(spec.ID, spec.InitialTurnID)
31	defer cancel()
32	if _, err := manager.Spawn(t.Context(), spec, "investigate"); err != nil {
33		t.Fatal(err)
34	}
35	select {
36	case <-completed:
37	case <-time.After(time.Second):
38		t.Fatal("no completion")
39	}
40	status := &SubagentStatusTool{ResidentManager: manager, Enabled: func() bool { return true }}
41	for _, include := range []bool{false, true} {
42		raw := json.RawMessage(`{"agent_id":"failed-status"}`)
43		if include {
44			raw = json.RawMessage(`{"agent_id":"failed-status","include_result":true}`)
45		}
46		result, err := status.Execute(t.Context(), raw, nil)
47		if err != nil || result.IsError {
48			t.Fatalf("status: %#v, %v", result, err)
49		}
50		response := result.Details.(subagentStatusResponse)
51		if response.Agent.State != subagents.ResidentFailed || (response.Result != nil) != include {
52			t.Fatalf("response = %#v", response)
53		}
54		if include && (response.Result.Handoff != "" || response.Result.ErrorCode != "turn_failed" || response.Result.Summary != "partial finding") {
55			t.Fatalf("result = %#v", response.Result)
56		}
57	}
58	result, err := status.Execute(t.Context(), json.RawMessage(`{"include_result":true}`), nil)
59	if err != nil || !result.IsError {
60		t.Fatalf("missing child ID must fail: %#v, %v", result, err)
61	}
62	if len(manager.UnmetRequired()) != 1 {
63		t.Fatal("reading a result satisfied required work")
64	}
65}
66
67func TestResidentToolsUseManagerOnly(t *testing.T) {
68	runs := make(chan string, 2)
69	manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
70		return subagents.ResidentTurnRunner(func(_ context.Context, prompt string) error { runs <- prompt; return nil }), nil
71	})
72	t.Cleanup(func() { _ = manager.Close(context.Background()) })
73	spawn := &SubagentSpawnTool{
74		ResidentManager: manager,
75		Enabled:         func() bool { return true },
76		DefaultProvider: func() string { return "openai" },
77		DefaultModel:    func() string { return "gpt-5" },
78		BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
79			return subagents.ResidentChildSpec{ID: "resident-tool", SessionID: "child-session", Provider: request.Provider, Model: request.Model, Required: request.Required}, nil
80		},
81	}
82	result, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"initial"}`), nil)
83	if err != nil {
84		t.Fatal(err)
85	}
86	if got := toolResultText(t, result); !strings.Contains(got, "owns the delegated scope") || !strings.Contains(got, "Do not repeat that work in the parent") {
87		t.Fatalf("spawn result lacks ownership guidance: %q", got)
88	}
89	if got := <-runs; got != "initial" {
90		t.Fatalf("initial prompt = %q", got)
91	}
92	status := &SubagentStatusTool{ResidentManager: manager, Enabled: func() bool { return true }}
93	result, err = status.Execute(context.Background(), json.RawMessage(`{"agent_id":"resident-tool"}`), nil)
94	if err != nil {
95		t.Fatal(err)
96	}
97	if details, ok := result.Details.(subagentStatusResponse); !ok || details.Agent == nil || details.Agent.State == "" {
98		t.Fatalf("status details = %#v", result.Details)
99	}
100	resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
101	if _, err := resume.Execute(context.Background(), json.RawMessage(`{"agent_id":"resident-tool","prompt":"follow up"}`), nil); err != nil {
102		t.Fatal(err)
103	}
104	if got := <-runs; got != "follow up" {
105		t.Fatalf("follow-up prompt = %q", got)
106	}
107}
108
109func TestSubagentSpawnGuidanceRequiresIndependentOwnership(t *testing.T) {
110	facade := &SubagentTool{}
111	for _, want := range []string{"independent sidecar", "keep immediate blockers local", "never duplicate it in the parent", "end or yield the parent turn"} {
112		if got := facade.Description(); !strings.Contains(got, want) {
113			t.Fatalf("description missing %q: %s", want, got)
114		}
115	}
116	var schema struct {
117		Properties map[string]struct {
118			Description string `json:"description"`
119		} `json:"properties"`
120	}
121	if err := json.Unmarshal(facade.Schema(), &schema); err != nil {
122		t.Fatal(err)
123	}
124	task := schema.Properties["task"].Description
125	for _, want := range []string{"concrete, bounded scope", "explicit ownership", "does not overlap", "Shared isolation uses this working directory", "worktree isolation captures a patch without merging it"} {
126		if !strings.Contains(task, want) {
127			t.Fatalf("task schema missing %q: %s", want, task)
128		}
129	}
130}
131
132func TestResidentSpawnWaitReturnsInitialCompletion(t *testing.T) {
133	for _, tc := range []struct {
134		name   string
135		err    error
136		status string
137	}{
138		{"success", nil, "completed"},
139		{"failure", errors.New("worker failed"), "failed"},
140		{"cancellation", context.Canceled, "interrupted"},
141		{"cancellation with failure", errors.Join(context.Canceled, errors.New("worker failed")), "interrupted"},
142	} {
143		t.Run(tc.name, func(t *testing.T) {
144			manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
145				return subagents.ResidentTurnRunner(func(context.Context, string) error { return tc.err }), nil
146			})
147			t.Cleanup(func() { _ = manager.Close(context.Background()) })
148			spawn := &SubagentSpawnTool{
149				ResidentManager: manager,
150				Enabled:         func() bool { return true },
151				DefaultProvider: func() string { return "openai" },
152				DefaultModel:    func() string { return "gpt-5" },
153				BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
154					return subagents.ResidentChildSpec{ID: "wait-for-completion", SessionID: "child-session", Provider: request.Provider, Model: request.Model, Required: request.Required}, nil
155				},
156			}
157			result, err := spawn.Execute(t.Context(), json.RawMessage(`{"task":"finish now","wait":1,"required":true}`), nil)
158			if err != nil || result.IsError {
159				t.Fatalf("Execute = (%#v, %v)", result, err)
160			}
161			if got := toolResultText(t, result); !strings.Contains(got, "\nstate: "+tc.status+"\n") || !strings.Contains(got, "\nrequired: "+tc.status+"\n") {
162				t.Fatalf("wait result = %q, want %s state", got, tc.status)
163			}
164			if unmet := len(manager.UnmetRequired()); (unmet != 0) != (tc.err != nil) {
165				t.Fatalf("unmet required = %d, error = %v", unmet, tc.err)
166			}
167		})
168	}
169}
170
171func TestResidentSpawnWaitExpiryReportsQueuedChild(t *testing.T) {
172	started := make(chan struct{}, 1)
173	release := make(chan struct{})
174	var releaseOnce sync.Once
175	releaseBlocker := func() { releaseOnce.Do(func() { close(release) }) }
176	finished := make(chan string, 1)
177	manager := subagents.NewResidentManagerWithLimit(t.TempDir(), 1, func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
178		return subagents.ResidentTurnRunner(func(ctx context.Context, prompt string) error {
179			switch prompt {
180			case "blocker":
181				started <- struct{}{}
182				select {
183				case <-release:
184					return nil
185				case <-ctx.Done():
186					return ctx.Err()
187				}
188			case "waiter":
189				finished <- prompt
190			}
191			return nil
192		}), nil
193	})
194	t.Cleanup(func() {
195		releaseBlocker()
196		_ = manager.Close(context.Background())
197	})
198	spawn := &SubagentSpawnTool{
199		ResidentManager: manager,
200		Enabled:         func() bool { return true },
201		DefaultProvider: func() string { return "openai" },
202		DefaultModel:    func() string { return "gpt-5" },
203		BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
204			return subagents.ResidentChildSpec{ID: request.Task, SessionID: request.Task, Provider: request.Provider, Model: request.Model}, nil
205		},
206	}
207	if _, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"blocker"}`), nil); err != nil {
208		t.Fatalf("spawn blocker: %v", err)
209	}
210	select {
211	case <-started:
212	case <-time.After(5 * time.Second):
213		t.Fatal("blocker did not start")
214	}
215	result, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"waiter","wait":1}`), nil)
216	if err != nil || result.IsError {
217		t.Fatalf("spawn waiter = (%#v, %v)", result, err)
218	}
219	if got := toolResultText(t, result); !strings.Contains(got, "state: queued") || !strings.Contains(got, "wait: timed out after 1 seconds") {
220		t.Fatalf("wait expiry result = %q, want queued timeout", got)
221	}
222	if state, ok := manager.State("waiter"); !ok || state != subagents.ResidentQueued {
223		t.Fatalf("waiter state = %q (found=%t), want queued", state, ok)
224	}
225	releaseBlocker()
226	select {
227	case got := <-finished:
228		if got != "waiter" {
229			t.Fatalf("finished task = %q, want waiter", got)
230		}
231	case <-time.After(5 * time.Second):
232		t.Fatal("queued waiter did not execute")
233	}
234}
235
236func TestResidentSpawnRejectsInvalidInputBeforeCreatingChild(t *testing.T) {
237	manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
238		return subagents.ResidentTurnRunner(func(context.Context, string) error { return nil }), nil
239	})
240	t.Cleanup(func() { _ = manager.Close(context.Background()) })
241	built := 0
242	spawn := &SubagentSpawnTool{
243		ResidentManager: manager,
244		Enabled:         func() bool { return true },
245		BuildResidentSpec: func(context.Context, ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
246			built++
247			return subagents.ResidentChildSpec{ID: "should-not-exist", SessionID: "child"}, nil
248		},
249	}
250	for _, raw := range []string{
251		`{"task":"x","unexpected":true}`,
252		`{"task":"x"}{}`,
253		`{"task":"x","model":"gpt-5"}`,
254		`{"task":"x","wait":0}`,
255		`{"task":"x","wait":301}`,
256		`{"task":"x","isolation":"outside"}`,
257		`{"task":"x","budget_ratio":0.5}`,
258		`{"task":"x","budget_tokens":1000}`,
259	} {
260		result, err := spawn.Execute(context.Background(), json.RawMessage(raw), nil)
261		if err != nil && !strings.Contains(err.Error(), "invalid args") {
262			t.Fatalf("Execute(%s) error = %v", raw, err)
263		}
264		if err == nil && !result.IsError {
265			t.Fatalf("Execute(%s) = %#v, want protocol error", raw, result)
266		}
267	}
268	if built != 0 || len(manager.Snapshot()) != 0 {
269		t.Fatalf("invalid input reached resident manager: built=%d snapshots=%#v", built, manager.Snapshot())
270	}
271}
272
273func TestResidentSpawnProfileAndExplicitOverridesReachFactory(t *testing.T) {
274	manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
275		return subagents.ResidentTurnRunner(func(context.Context, string) error { return nil }), nil
276	})
277	t.Cleanup(func() { _ = manager.Close(context.Background()) })
278	profileFast := false
279	var got ResidentSpawnRequest
280	spawn := &SubagentSpawnTool{
281		ResidentManager:  manager,
282		Enabled:          func() bool { return true },
283		DefaultModel:     func() string { return "host-model" },
284		DefaultProvider:  func() string { return "host-provider" },
285		DefaultReasoning: func() string { return "medium" },
286		ResolveSubagent: func(name string) (*subagents.Profile, error) {
287			return &subagents.Profile{Name: name, Model: "openai/gpt-5.6-sol", Thinking: "low", FastMode: &profileFast}, nil
288		},
289		BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
290			got = request
291			return subagents.ResidentChildSpec{ID: "profiled", SessionID: "child", Provider: request.Provider, Model: request.Model, Profile: request.Profile.Name}, nil
292		},
293	}
294	result, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"review","agent":"reviewer","reasoning":"high","fast_mode":true}`), nil)
295	if err != nil || result.IsError {
296		t.Fatalf("Execute = (%#v, %v)", result, err)
297	}
298	if got.Model != "gpt-5.6-sol" || got.Provider != "openai" || got.Reasoning != "high" || got.FastMode == nil || !*got.FastMode {
299		t.Fatalf("factory request = %#v", got)
300	}
301}
302
303func TestResidentToolsRejectUnknownFields(t *testing.T) {
304	for _, tool := range []interface {
305		Execute(context.Context, json.RawMessage, func(string)) (core.ToolResult, error)
306	}{
307		&SubagentStatusTool{ResidentManager: subagents.NewResidentManager(t.TempDir(), nil), Enabled: func() bool { return true }},
308		&SubagentStopTool{ResidentManager: subagents.NewResidentManager(t.TempDir(), nil), Enabled: func() bool { return true }},
309		&SubagentResumeTool{ResidentManager: subagents.NewResidentManager(t.TempDir(), nil), Enabled: func() bool { return true }},
310	} {
311		_, err := tool.Execute(context.Background(), json.RawMessage(`{"unknown":true}`), nil)
312		if err == nil || !strings.Contains(err.Error(), "unknown field") {
313			t.Fatalf("unknown field error = %v", err)
314		}
315	}
316}
317
318// A resume wait subscribes to the accepted follow-up turn before it can finish,
319// so it returns that turn's completion instead of timing out.
320func TestResidentResumeWaitReturnsFollowUpCompletion(t *testing.T) {
321	manager := subagents.NewResidentManager(t.TempDir(), func(_ subagents.ResidentChildSpec, journal *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
322		return subagents.ResidentTurnRunner(func(_ context.Context, prompt string) error {
323			return journal.RecordAgentEvent(core.EvAssistantMessage{Message: provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "answer for " + prompt}}}})
324		}), nil
325	})
326	t.Cleanup(func() { _ = manager.Close(context.Background()) })
327	initial, cancelInitial := manager.WatchCompletion("waited-resume", "initial-turn")
328	defer cancelInitial()
329	if _, err := manager.Spawn(context.Background(), subagents.ResidentChildSpec{ID: "waited-resume", InitialTurnID: "initial-turn", SessionID: "child-session", Provider: "openai", Model: "gpt-5"}, "start"); err != nil {
330		t.Fatal(err)
331	}
332	select {
333	case <-initial:
334	case <-time.After(5 * time.Second):
335		t.Fatal("initial turn did not complete")
336	}
337	resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
338	result, err := resume.Execute(context.Background(), json.RawMessage(`{"agent_id":"waited-resume","prompt":"confirm","wait":5}`), nil)
339	if err != nil || result.IsError {
340		t.Fatalf("resume = (%#v, %v)", result, err)
341	}
342	response, ok := result.Details.(subagentResumeResponse)
343	if !ok || response.Wait == nil || response.Wait.TimedOut || response.Delivery != "queued" {
344		t.Fatalf("wait outcome = %#v", result.Details)
345	}
346	if response.Wait.Status != string(subagents.ResidentCompleted) || response.Wait.Summary != "answer for confirm" {
347		t.Fatalf("wait outcome = %#v", response.Wait)
348	}
349	if response.Action != "resumed" || response.Agent.ID != "waited-resume" {
350		t.Fatalf("response = %#v", response)
351	}
352}
353
354// An expired resume wait reports the timeout and leaves the child active.
355func TestResidentResumeWaitExpiryLeavesChildActive(t *testing.T) {
356	started := make(chan struct{}, 1)
357	release := make(chan struct{})
358	var releaseOnce sync.Once
359	releaseBlocker := func() { releaseOnce.Do(func() { close(release) }) }
360	manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
361		return subagents.ResidentTurnRunner(func(ctx context.Context, _ string) error {
362			started <- struct{}{}
363			select {
364			case <-release:
365				return nil
366			case <-ctx.Done():
367				return ctx.Err()
368			}
369		}), nil
370	})
371	t.Cleanup(func() {
372		releaseBlocker()
373		_ = manager.Close(context.Background())
374	})
375	if _, err := manager.Spawn(context.Background(), subagents.ResidentChildSpec{ID: "blocked-resume", InitialTurnID: "initial-turn", SessionID: "child-session", Provider: "openai", Model: "gpt-5"}, "blocker"); err != nil {
376		t.Fatal(err)
377	}
378	select {
379	case <-started:
380	case <-time.After(5 * time.Second):
381		t.Fatal("blocking turn did not start")
382	}
383	resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
384	result, err := resume.Execute(context.Background(), json.RawMessage(`{"agent_id":"blocked-resume","prompt":"follow up","mode":"queue","wait":1}`), nil)
385	if err != nil || result.IsError {
386		t.Fatalf("resume = (%#v, %v)", result, err)
387	}
388	response, ok := result.Details.(subagentResumeResponse)
389	if !ok || response.Wait == nil || !response.Wait.TimedOut || response.Wait.Seconds != 1 {
390		t.Fatalf("wait outcome = %#v", result.Details)
391	}
392	if state, found := manager.State("blocked-resume"); !found || state != subagents.ResidentRunning {
393		t.Fatalf("child state = %q (found=%t), want running", state, found)
394	}
395	releaseBlocker()
396}
397
398// A cancelled parent context wins over an expiring wait: the tool returns the
399// host context error rather than a timeout result.
400func TestResidentResumeWaitCancellationBeatsTimeout(t *testing.T) {
401	release := make(chan struct{})
402	var releaseOnce sync.Once
403	releaseBlocker := func() { releaseOnce.Do(func() { close(release) }) }
404	var calls atomic.Int32
405	followUpStarted := make(chan struct{}, 1)
406	manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
407		return subagents.ResidentTurnRunner(func(ctx context.Context, _ string) error {
408			if calls.Add(1) == 1 {
409				return nil
410			}
411			followUpStarted <- struct{}{}
412			select {
413			case <-release:
414				return nil
415			case <-ctx.Done():
416				return ctx.Err()
417			}
418		}), nil
419	})
420	t.Cleanup(func() {
421		releaseBlocker()
422		_ = manager.Close(context.Background())
423	})
424	initial, cancelInitial := manager.WatchCompletion("cancel-resume", "initial-turn")
425	defer cancelInitial()
426	if _, err := manager.Spawn(context.Background(), subagents.ResidentChildSpec{ID: "cancel-resume", InitialTurnID: "initial-turn", SessionID: "child-session", Provider: "openai", Model: "gpt-5"}, "start"); err != nil {
427		t.Fatal(err)
428	}
429	select {
430	case <-initial:
431	case <-time.After(5 * time.Second):
432		t.Fatal("initial turn did not complete")
433	}
434
435	resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
436	ctx, cancel := context.WithCancel(context.Background())
437	defer cancel()
438	type outcome struct {
439		result core.ToolResult
440		err    error
441	}
442	done := make(chan outcome, 1)
443	go func() {
444		result, err := resume.Execute(ctx, json.RawMessage(`{"agent_id":"cancel-resume","prompt":"follow up","wait":300}`), nil)
445		done <- outcome{result: result, err: err}
446	}()
447	select {
448	case <-followUpStarted:
449	case <-time.After(5 * time.Second):
450		t.Fatal("follow-up turn did not start")
451	}
452	cancel()
453	select {
454	case got := <-done:
455		if !errors.Is(got.err, context.Canceled) {
456			t.Fatalf("Execute error = %v, want context.Canceled (result=%#v)", got.err, got.result)
457		}
458		if got.result.IsError || len(got.result.Content) != 0 {
459			t.Fatalf("result = %#v, want an empty non-error result on host cancellation", got.result)
460		}
461	case <-time.After(5 * time.Second):
462		t.Fatal("resume did not return after cancellation")
463	}
464}
465
466func TestResidentResumeRejectsWaitOutsideBounds(t *testing.T) {
467	manager := subagents.NewResidentManager(t.TempDir(), nil)
468	t.Cleanup(func() { _ = manager.Close(context.Background()) })
469	resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
470	for _, raw := range []string{
471		`{"agent_id":"child","prompt":"x","wait":0}`,
472		`{"agent_id":"child","prompt":"x","wait":301}`,
473	} {
474		result, err := resume.Execute(context.Background(), json.RawMessage(raw), nil)
475		if err != nil || !result.IsError || !strings.Contains(toolResultText(t, result), "wait must be between 1 and 300 seconds") {
476			t.Fatalf("Execute(%s) = (%#v, %v)", raw, result, err)
477		}
478	}
479}
480
481func toolResultText(t *testing.T, result core.ToolResult) string {
482	t.Helper()
483	if len(result.Content) != 1 {
484		t.Fatalf("content = %#v, want one text block", result.Content)
485	}
486	block, ok := result.Content[0].(provider.TextBlock)
487	if !ok {
488		t.Fatalf("content = %#v, want text block", result.Content)
489	}
490	return block.Text
491}
492
493func TestPublicResidentStatusOmitsBudget(t *testing.T) {
494	entry := publicResidentStatus(subagents.ResidentSnapshot{
495		ID: "resident", State: subagents.ResidentIdle, Provider: "openai", Model: "test",
496	})
497	encoded, err := json.Marshal(entry)
498	if err != nil {
499		t.Fatal(err)
500	}
501	if strings.Contains(string(encoded), "budget") {
502		t.Fatalf("status entry carries budget = %s", encoded)
503	}
504}
505
506func TestFindResidentStatusSnapshotRejectsAmbiguousPrefix(t *testing.T) {
507	_, ok := findResidentStatusSnapshot([]subagents.ResidentSnapshot{{ID: "resident-abcd"}, {ID: "resident-abef"}}, "resident-ab")
508	if ok {
509		t.Fatal("ambiguous resident prefix resolved")
510	}
511}

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 477

↑↓ moveenter openesc close