v2.16.0 ebd2ab3

images_test.go RawHistoryFolder

1package cli
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"testing"
8	"time"
9
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12
13	"github.com/bnema/gordon/internal/adapters/dto"
14)
15
16type fakeImagesClient struct {
17	listImagesResp []dto.Image
18	listImagesErr  error
19	pruneResp      *dto.ImagePruneResponse
20	pruneErr       error
21
22	listImagesCalls int
23	pruneCalls      int
24	lastPruneKeep   int
25}
26
27func (f *fakeImagesClient) ListImages(_ context.Context) ([]dto.Image, error) {
28	f.listImagesCalls++
29	if f.listImagesErr != nil {
30		return nil, f.listImagesErr
31	}
32	return f.listImagesResp, nil
33}
34
35func (f *fakeImagesClient) PruneImages(_ context.Context, keepLast int) (*dto.ImagePruneResponse, error) {
36	f.pruneCalls++
37	f.lastPruneKeep = keepLast
38	if f.pruneErr != nil {
39		return nil, f.pruneErr
40	}
41	return f.pruneResp, nil
42}
43
44func TestRunImagesList_PrintsRowsAndSummary(t *testing.T) {
45	createdAt := time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
46	client := &fakeImagesClient{
47		listImagesResp: []dto.Image{
48			{Repository: "registry.example.com/app", Tag: "latest", Size: 12_000_000, Created: createdAt, ID: "sha256:1111", Dangling: false},
49			{Repository: "<none>", Tag: "<none>", Size: 512_000, Created: createdAt, ID: "sha256:2222", Dangling: true},
50		},
51	}
52
53	var out bytes.Buffer
54	err := runImagesList(context.Background(), client, &out)
55	require.NoError(t, err)
56
57	text := out.String()
58	assert.Contains(t, text, "REPOSITORY")
59	assert.Contains(t, text, "registry.example.com/app")
60	assert.Contains(t, text, "latest")
61	assert.Contains(t, text, "Total images: 2 (dangling: 1)")
62	assert.Equal(t, 1, client.listImagesCalls)
63}
64
65func TestRunImagesList_EmptyOutput(t *testing.T) {
66	client := &fakeImagesClient{listImagesResp: []dto.Image{}}
67
68	var out bytes.Buffer
69	err := runImagesList(context.Background(), client, &out)
70	require.NoError(t, err)
71
72	text := out.String()
73	assert.Contains(t, text, "No images found")
74	assert.Contains(t, text, "Total images: 0")
75	assert.Equal(t, 1, client.listImagesCalls)
76}
77
78func TestRunImagesPrune_DryRunCallsListOnly(t *testing.T) {
79	client := &fakeImagesClient{
80		listImagesResp: []dto.Image{{Dangling: true}, {Dangling: false}, {Dangling: true}},
81	}
82
83	var out bytes.Buffer
84	err := runImagesPrune(context.Background(), client, imagesPruneOptions{DryRun: true, KeepLast: 3}, &out)
85	require.NoError(t, err)
86
87	text := out.String()
88	assert.Contains(t, text, "Dry run")
89	assert.Contains(t, text, "would prune 2 dangling runtime images")
90	assert.Contains(t, text, "would keep latest + 3 previous tags")
91	assert.Equal(t, 1, client.listImagesCalls)
92	assert.Equal(t, 0, client.pruneCalls)
93}
94
95func TestRunImagesPrune_DryRunKeepZeroSkipsRegistry(t *testing.T) {
96	client := &fakeImagesClient{listImagesResp: []dto.Image{{Dangling: true}}}
97
98	var out bytes.Buffer
99	err := runImagesPrune(context.Background(), client, imagesPruneOptions{DryRun: true, KeepLast: 0}, &out)
100	require.NoError(t, err)
101
102	text := out.String()
103	assert.Contains(t, text, "Registry cleanup skipped (--keep=0)")
104	assert.Equal(t, 1, client.listImagesCalls)
105	assert.Equal(t, 0, client.pruneCalls)
106}
107
108func TestRunImagesPrune_RuntimeOnlyForcesKeepZero(t *testing.T) {
109	client := &fakeImagesClient{pruneResp: &dto.ImagePruneResponse{}}
110
111	var out bytes.Buffer
112	err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepLast: 9, RuntimeOnly: true}, &out)
113	require.NoError(t, err)
114
115	assert.Equal(t, 1, client.pruneCalls)
116	assert.Equal(t, 0, client.lastPruneKeep)
117	assert.Contains(t, out.String(), "Registry cleanup skipped")
118}
119
120func TestRunImagesPrune_UsesKeepFlagWhenNotRuntimeOnly(t *testing.T) {
121	client := &fakeImagesClient{
122		pruneResp: &dto.ImagePruneResponse{
123			Runtime:  dto.RuntimePruneResult{DeletedCount: 2, SpaceReclaimed: 4096},
124			Registry: dto.RegistryPruneResult{TagsRemoved: 3, BlobsRemoved: 1, SpaceReclaimed: 8192},
125		},
126	}
127
128	var out bytes.Buffer
129	err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepLast: 5}, &out)
130	require.NoError(t, err)
131
132	text := out.String()
133	assert.Equal(t, 1, client.pruneCalls)
134	assert.Equal(t, 5, client.lastPruneKeep)
135	assert.Contains(t, text, "Runtime: deleted=2")
136	assert.Contains(t, text, "Registry: tags_removed=3")
137}
138
139func TestRunImagesPrune_ReturnsRemoteErrors(t *testing.T) {
140	client := &fakeImagesClient{pruneErr: errors.New("request failed")}
141
142	err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepLast: 2}, &bytes.Buffer{})
143	require.Error(t, err)
144	assert.Contains(t, err.Error(), "failed to prune images")
145}
146
147func TestRunImagesPrune_RejectsNegativeKeep(t *testing.T) {
148	err := runImagesPrune(context.Background(), &fakeImagesClient{}, imagesPruneOptions{KeepLast: -1}, &bytes.Buffer{})
149	require.Error(t, err)
150	assert.Contains(t, err.Error(), "--keep must be >= 0")
151}
152
153func TestRunImagesPrune_RejectsEmptyResponse(t *testing.T) {
154	client := &fakeImagesClient{pruneResp: nil}
155	err := runImagesPrune(context.Background(), client, imagesPruneOptions{KeepLast: 1}, &bytes.Buffer{})
156	require.Error(t, err)
157	assert.Contains(t, err.Error(), "empty response")
158}

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