main 5041353

instance_launch_test.go RawHistoryFolder

1package bootstrap
2
3import (
4	"context"
5	"errors"
6	"io"
7	"os"
8	"path/filepath"
9	"testing"
10	"time"
11
12	"github.com/bnema/dumber/internal/application/port"
13	"github.com/bnema/dumber/internal/infrastructure/runtimeprofile"
14	"github.com/stretchr/testify/require"
15)
16
17type instanceRelayStub struct {
18	port.BrowserLaunchRelay
19	err           error
20	prepared      bool
21	lease         io.Closer
22	deliverCalls  int
23	deliverAfter  int
24	deliveredKind string
25}
26
27func (r *instanceRelayStub) deliver(kind string) (bool, error) {
28	r.deliveredKind = kind
29	r.deliverCalls++
30	return r.deliverCalls >= r.deliverAfter, nil
31}
32
33func (r *instanceRelayStub) DeliverOpenExternalURL(context.Context, string) (bool, error) {
34	return r.deliver("external")
35}
36
37func (r *instanceRelayStub) DeliverOpenFreshWindow(context.Context, string) (bool, error) {
38	return r.deliver("fresh")
39}
40
41func (r *instanceRelayStub) DeliverOpenInstance(context.Context, string, string) (bool, error) {
42	return r.deliver("instance")
43}
44
45func (r *instanceRelayStub) Prepare(lease io.Closer) error {
46	r.prepared = true
47	r.lease = lease
48	return r.err
49}
50func testInstanceProfile(t *testing.T) runtimeprofile.Profile {
51	t.Helper()
52	root := t.TempDir()
53	return runtimeprofile.Profile{InstanceRoot: root, IPC: runtimeprofile.IPCPaths{BrowserLaunchSocket: filepath.Join(root, "browser-launch.sock")}}
54}
55
56// testColdDefaultProfile mirrors main's default-profile path before
57// normalization: InstanceRoot is empty while IPC.RuntimeDir is populated.
58func testColdDefaultProfile(t *testing.T) runtimeprofile.Profile {
59	t.Helper()
60	runtimeDir := t.TempDir()
61	return runtimeprofile.Profile{IPC: runtimeprofile.IPCPaths{
62		RuntimeDir:          runtimeDir,
63		BrowserLaunchSocket: filepath.Join(runtimeDir, "browser-launch.sock"),
64	}}
65}
66
67func TestRunWithInstanceRelayFailsBeforeEngineStartup(t *testing.T) {
68	failure := errors.New("bind failed")
69	relay := &instanceRelayStub{err: failure}
70	code, err := RunWithInstanceRelay(context.Background(), testInstanceProfile(t), relay, func() int { t.Fatal("must not run"); return 0 })
71	require.Equal(t, 1, code)
72	require.ErrorIs(t, err, failure)
73	require.True(t, relay.prepared)
74}
75func TestDeliverOrRunWithInstanceRelayPreservesLaunchIntent(t *testing.T) {
76	for _, test := range []struct {
77		name        string
78		instance    string
79		freshWindow bool
80		want        string
81	}{
82		{name: "default external", want: "external"},
83		{name: "named instance", instance: "work", want: "instance"},
84		{name: "fresh window", instance: "work", freshWindow: true, want: "fresh"},
85		{name: "fresh window without instance", freshWindow: true, want: "fresh"},
86	} {
87		t.Run(test.name, func(t *testing.T) {
88			profile := testInstanceProfile(t)
89			owner, err := acquireInstanceNamespace(context.Background(), profile)
90			require.NoError(t, err)
91			defer owner.Close()
92			relay := &instanceRelayStub{deliverAfter: 1}
93			code, err := DeliverOrRunWithInstanceRelay(context.Background(), profile, relay, test.instance, "https://example.com", test.freshWindow, func() int {
94				t.Fatal("delivered request must not start host")
95				return 1
96			})
97			require.NoError(t, err)
98			require.Zero(t, code)
99			require.Equal(t, test.want, relay.deliveredKind)
100		})
101	}
102}
103
104func TestDeliverOrRunWithInstanceRelayRetriesWhileHostStarts(t *testing.T) {
105	profile := testInstanceProfile(t)
106	owner, err := acquireInstanceNamespace(context.Background(), profile)
107	require.NoError(t, err)
108	defer owner.Close()
109	relay := &instanceRelayStub{deliverAfter: 3}
110
111	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
112	defer cancel()
113	code, err := DeliverOrRunWithInstanceRelay(ctx, profile, relay, "work", "https://example.com", false, func() int {
114		t.Fatal("loser must not start host")
115		return 1
116	})
117	require.NoError(t, err)
118	require.Zero(t, code)
119	require.Equal(t, 3, relay.deliverCalls)
120}
121
122// With a cold default profile no host is running, so the first process must
123// normalize InstanceRoot to IPC.RuntimeDir, acquire the lease, prepare the
124// relay, and start the host instead of spinning on an empty runtime path. The
125// real default cold start carries an empty instance name (main passes
126// args.Instance verbatim), so delivery must use the external-URL path.
127func TestDeliverOrRunWithInstanceRelayStartsHostForColdDefaultProfile(t *testing.T) {
128	profile := testColdDefaultProfile(t)
129	relay := &instanceRelayStub{deliverAfter: 1 << 30}
130	ran := false
131
132	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
133	defer cancel()
134	code, err := DeliverOrRunWithInstanceRelay(ctx, profile, relay, "", "https://example.com", false, func() int {
135		ran = true
136		return 0
137	})
138	require.NoError(t, err)
139	require.Zero(t, code)
140	require.True(t, ran, "cold default profile must start the host")
141	require.True(t, relay.prepared, "host must prepare the relay")
142	require.Equal(t, 1, relay.deliverCalls)
143	require.Equal(t, "external", relay.deliveredKind, "empty instance name must not use the instance relay")
144}
145
146// A permanent acquisition failure (for example a runtime root that cannot be
147// created) must return immediately rather than retry until the context ends.
148func TestDeliverOrRunWithInstanceRelayReturnsPermanentAcquireError(t *testing.T) {
149	root := filepath.Join(t.TempDir(), "not-a-dir")
150	require.NoError(t, os.WriteFile(root, []byte("file"), 0o600))
151	profile := runtimeprofile.Profile{
152		InstanceRoot: root,
153		IPC:          runtimeprofile.IPCPaths{BrowserLaunchSocket: filepath.Join(root, "browser-launch.sock")},
154	}
155	relay := &instanceRelayStub{deliverAfter: 1 << 30}
156
157	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
158	defer cancel()
159	start := time.Now()
160	code, err := DeliverOrRunWithInstanceRelay(ctx, profile, relay, "default", "https://example.com", false, func() int {
161		t.Fatal("permanent failure must not start the host")
162		return 1
163	})
164	elapsed := time.Since(start)
165	require.Error(t, err)
166	require.Equal(t, 1, code)
167	require.NotErrorIs(t, err, errInstanceNamespaceContended)
168	require.ErrorContains(t, err, "create named instance runtime directory")
169	require.Less(t, elapsed, instanceElectionSlice, "permanent failure must not retry")
170	require.Equal(t, 1, relay.deliverCalls, "delivery is attempted once before election")
171}
172
173func TestRunWithInstanceRelayOwnsLeaseThroughGUILifetime(t *testing.T) {
174	profile := testInstanceProfile(t)
175	relay := &instanceRelayStub{}
176	code, err := RunWithInstanceRelay(context.Background(), profile, relay, func() int {
177		ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
178		defer cancel()
179		_, competitorErr := acquireInstanceNamespace(ctx, profile)
180		require.Error(t, competitorErr)
181		return 0
182	})
183	require.NoError(t, err)
184	require.Zero(t, code)
185	lease, err := acquireInstanceNamespace(context.Background(), profile)
186	require.NoError(t, err)
187	require.NoError(t, lease.Close())
188}

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 8

Tags 42

↑↓ moveenter openesc close