@rsktash/beads-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +28 -0
- package/app/protocol.js +216 -0
- package/bin/bdui +19 -0
- package/client/index.html +12 -0
- package/client/postcss.config.js +11 -0
- package/client/src/App.tsx +35 -0
- package/client/src/components/IssueCard.tsx +73 -0
- package/client/src/components/Layout.tsx +175 -0
- package/client/src/components/Markdown.tsx +77 -0
- package/client/src/components/PriorityBadge.tsx +26 -0
- package/client/src/components/SearchDialog.tsx +137 -0
- package/client/src/components/SectionEditor.tsx +212 -0
- package/client/src/components/StatusBadge.tsx +64 -0
- package/client/src/components/TypeBadge.tsx +26 -0
- package/client/src/hooks/use-mutation.ts +55 -0
- package/client/src/hooks/use-search.ts +19 -0
- package/client/src/hooks/use-subscription.ts +187 -0
- package/client/src/index.css +133 -0
- package/client/src/lib/avatar.ts +17 -0
- package/client/src/lib/types.ts +115 -0
- package/client/src/lib/ws-client.ts +214 -0
- package/client/src/lib/ws-context.tsx +28 -0
- package/client/src/main.tsx +10 -0
- package/client/src/views/Board.tsx +200 -0
- package/client/src/views/Detail.tsx +398 -0
- package/client/src/views/List.tsx +461 -0
- package/client/tailwind.config.ts +68 -0
- package/client/tsconfig.json +16 -0
- package/client/vite.config.ts +20 -0
- package/package.json +43 -0
- package/server/app.js +120 -0
- package/server/app.test.js +30 -0
- package/server/bd.js +227 -0
- package/server/bd.test.js +194 -0
- package/server/cli/cli.test.js +207 -0
- package/server/cli/commands.integration.test.js +148 -0
- package/server/cli/commands.js +285 -0
- package/server/cli/commands.unit.test.js +408 -0
- package/server/cli/daemon.js +340 -0
- package/server/cli/daemon.test.js +31 -0
- package/server/cli/index.js +135 -0
- package/server/cli/open.js +178 -0
- package/server/cli/open.test.js +26 -0
- package/server/cli/usage.js +27 -0
- package/server/config.js +36 -0
- package/server/db.js +154 -0
- package/server/db.test.js +169 -0
- package/server/dolt-pool.js +257 -0
- package/server/dolt-queries.js +646 -0
- package/server/index.js +97 -0
- package/server/list-adapters.js +395 -0
- package/server/list-adapters.test.js +208 -0
- package/server/logging.js +23 -0
- package/server/registry-watcher.js +200 -0
- package/server/subscriptions.js +299 -0
- package/server/subscriptions.test.js +128 -0
- package/server/validators.js +124 -0
- package/server/watcher.js +139 -0
- package/server/watcher.test.js +120 -0
- package/server/ws.comments.test.js +262 -0
- package/server/ws.delete.test.js +119 -0
- package/server/ws.js +1309 -0
- package/server/ws.labels.test.js +95 -0
- package/server/ws.list-refresh.coalesce.test.js +95 -0
- package/server/ws.list-subscriptions.test.js +403 -0
- package/server/ws.mutation-window.test.js +147 -0
- package/server/ws.mutations.test.js +389 -0
- package/server/ws.test.js +52 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
3
|
+
import { handleRestart, handleStart, handleStop } from './commands.js';
|
|
4
|
+
import * as daemon from './daemon.js';
|
|
5
|
+
import * as open from './open.js';
|
|
6
|
+
|
|
7
|
+
// Mock open.js to avoid external effects
|
|
8
|
+
vi.mock('./open.js', () => ({
|
|
9
|
+
openUrl: async () => true,
|
|
10
|
+
waitForServer: async () => {},
|
|
11
|
+
fetchWorkspacesFromServer: vi.fn(async () => []),
|
|
12
|
+
registerWorkspaceWithServer: vi.fn(async () => true)
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Mock db resolution
|
|
16
|
+
vi.mock('../db.js', () => ({
|
|
17
|
+
resolveDbPath: () => ({
|
|
18
|
+
path: path.join(process.cwd(), '.beads', 'workspace.db'),
|
|
19
|
+
source: 'nearest',
|
|
20
|
+
exists: true
|
|
21
|
+
}),
|
|
22
|
+
resolveWorkspaceDatabase: () => ({
|
|
23
|
+
path: path.join(process.cwd(), '.beads'),
|
|
24
|
+
source: 'metadata',
|
|
25
|
+
exists: true
|
|
26
|
+
})
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Mock config - mirrors real getConfig() so env var overrides are testable
|
|
30
|
+
vi.mock('../config.js', () => ({
|
|
31
|
+
getConfig: () => {
|
|
32
|
+
const port = Number.parseInt(process.env.PORT || '', 10) || 3000;
|
|
33
|
+
const host = process.env.HOST || '127.0.0.1';
|
|
34
|
+
return { host, port, url: `http://${host}:${port}` };
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
delete process.env.PORT;
|
|
40
|
+
delete process.env.HOST;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('handleStart (unit)', () => {
|
|
44
|
+
test('returns 1 when daemon start fails', async () => {
|
|
45
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
|
|
46
|
+
vi.spyOn(daemon, 'isProcessRunning').mockReturnValue(false);
|
|
47
|
+
vi.spyOn(daemon, 'findAvailablePort').mockResolvedValue(3000);
|
|
48
|
+
vi.spyOn(daemon, 'startDaemon').mockReturnValue(null);
|
|
49
|
+
|
|
50
|
+
const code = await handleStart({ open: false });
|
|
51
|
+
|
|
52
|
+
expect(code).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('returns 0 when already running', async () => {
|
|
56
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(12345);
|
|
57
|
+
vi.spyOn(daemon, 'isProcessRunning').mockReturnValue(true);
|
|
58
|
+
const print_url = vi
|
|
59
|
+
.spyOn(daemon, 'printServerUrl')
|
|
60
|
+
.mockImplementation(() => {});
|
|
61
|
+
|
|
62
|
+
const code = await handleStart({ open: false });
|
|
63
|
+
|
|
64
|
+
expect(code).toBe(0);
|
|
65
|
+
expect(print_url).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('registers workspace from metadata when already running', async () => {
|
|
69
|
+
const register_workspace_with_server =
|
|
70
|
+
/** @type {import('vitest').Mock} */ (open.registerWorkspaceWithServer);
|
|
71
|
+
register_workspace_with_server.mockReset();
|
|
72
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(12345);
|
|
73
|
+
vi.spyOn(daemon, 'isProcessRunning').mockReturnValue(true);
|
|
74
|
+
|
|
75
|
+
const code = await handleStart({ open: false });
|
|
76
|
+
|
|
77
|
+
expect(code).toBe(0);
|
|
78
|
+
expect(register_workspace_with_server).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(register_workspace_with_server).toHaveBeenCalledWith(
|
|
80
|
+
'http://127.0.0.1:3000',
|
|
81
|
+
{
|
|
82
|
+
path: process.cwd(),
|
|
83
|
+
database: path.join(process.cwd(), '.beads')
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('registers workspace at custom port when already running', async () => {
|
|
89
|
+
const register_workspace_with_server =
|
|
90
|
+
/** @type {import('vitest').Mock} */ (open.registerWorkspaceWithServer);
|
|
91
|
+
register_workspace_with_server.mockReset();
|
|
92
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(12345);
|
|
93
|
+
vi.spyOn(daemon, 'isProcessRunning').mockReturnValue(true);
|
|
94
|
+
|
|
95
|
+
const code = await handleStart({ open: false, port: 3030 });
|
|
96
|
+
|
|
97
|
+
expect(code).toBe(0);
|
|
98
|
+
expect(register_workspace_with_server).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(register_workspace_with_server).toHaveBeenCalledWith(
|
|
100
|
+
'http://127.0.0.1:3030',
|
|
101
|
+
{
|
|
102
|
+
path: process.cwd(),
|
|
103
|
+
database: path.join(process.cwd(), '.beads')
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('registers workspace with existing server when spawned daemon exits early', async () => {
|
|
109
|
+
const register_workspace_with_server =
|
|
110
|
+
/** @type {import('vitest').Mock} */ (open.registerWorkspaceWithServer);
|
|
111
|
+
register_workspace_with_server.mockReset();
|
|
112
|
+
|
|
113
|
+
const remove_pid = vi
|
|
114
|
+
.spyOn(daemon, 'removePidFile')
|
|
115
|
+
.mockImplementation(() => {});
|
|
116
|
+
|
|
117
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
|
|
118
|
+
vi.spyOn(daemon, 'findAvailablePort').mockResolvedValue(3000);
|
|
119
|
+
vi.spyOn(daemon, 'startDaemon').mockReturnValue({ pid: 7777 });
|
|
120
|
+
vi.spyOn(daemon, 'isProcessRunning').mockImplementation((pid) => pid === 1);
|
|
121
|
+
|
|
122
|
+
const code = await handleStart({ open: false });
|
|
123
|
+
|
|
124
|
+
expect(code).toBe(0);
|
|
125
|
+
expect(remove_pid).toHaveBeenCalledTimes(1);
|
|
126
|
+
expect(register_workspace_with_server).toHaveBeenCalledTimes(1);
|
|
127
|
+
expect(register_workspace_with_server).toHaveBeenCalledWith(
|
|
128
|
+
'http://127.0.0.1:3000',
|
|
129
|
+
{
|
|
130
|
+
path: process.cwd(),
|
|
131
|
+
database: path.join(process.cwd(), '.beads')
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('attempts workspace registration after successful daemon start', async () => {
|
|
137
|
+
const register_workspace_with_server =
|
|
138
|
+
/** @type {import('vitest').Mock} */ (open.registerWorkspaceWithServer);
|
|
139
|
+
register_workspace_with_server.mockReset();
|
|
140
|
+
|
|
141
|
+
const print_url = vi
|
|
142
|
+
.spyOn(daemon, 'printServerUrl')
|
|
143
|
+
.mockImplementation(() => {});
|
|
144
|
+
|
|
145
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
|
|
146
|
+
vi.spyOn(daemon, 'findAvailablePort').mockResolvedValue(3000);
|
|
147
|
+
vi.spyOn(daemon, 'startDaemon').mockReturnValue({ pid: 4321 });
|
|
148
|
+
vi.spyOn(daemon, 'isProcessRunning').mockImplementation(
|
|
149
|
+
(pid) => pid === 4321
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const code = await handleStart({ open: false });
|
|
153
|
+
|
|
154
|
+
expect(code).toBe(0);
|
|
155
|
+
expect(print_url).toHaveBeenCalledTimes(1);
|
|
156
|
+
expect(register_workspace_with_server).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(register_workspace_with_server).toHaveBeenCalledWith(
|
|
158
|
+
'http://127.0.0.1:3000',
|
|
159
|
+
{
|
|
160
|
+
path: process.cwd(),
|
|
161
|
+
database: path.join(process.cwd(), '.beads')
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('handleStop (unit)', () => {
|
|
168
|
+
test('returns 2 when not running and no PID file', async () => {
|
|
169
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
|
|
170
|
+
|
|
171
|
+
const code = await handleStop();
|
|
172
|
+
|
|
173
|
+
expect(code).toBe(2);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('returns 2 on stale PID and removes file', async () => {
|
|
177
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(1111);
|
|
178
|
+
vi.spyOn(daemon, 'isProcessRunning').mockReturnValue(false);
|
|
179
|
+
const remove_pid = vi
|
|
180
|
+
.spyOn(daemon, 'removePidFile')
|
|
181
|
+
.mockImplementation(() => {});
|
|
182
|
+
|
|
183
|
+
const code = await handleStop();
|
|
184
|
+
|
|
185
|
+
expect(code).toBe(2);
|
|
186
|
+
expect(remove_pid).toHaveBeenCalledTimes(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('returns 0 when process terminates and removes PID', async () => {
|
|
190
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(2222);
|
|
191
|
+
vi.spyOn(daemon, 'isProcessRunning').mockReturnValue(true);
|
|
192
|
+
vi.spyOn(daemon, 'terminateProcess').mockResolvedValue(true);
|
|
193
|
+
const remove_pid = vi
|
|
194
|
+
.spyOn(daemon, 'removePidFile')
|
|
195
|
+
.mockImplementation(() => {});
|
|
196
|
+
|
|
197
|
+
const code = await handleStop();
|
|
198
|
+
|
|
199
|
+
expect(code).toBe(0);
|
|
200
|
+
expect(remove_pid).toHaveBeenCalledTimes(1);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('handleRestart (unit)', () => {
|
|
205
|
+
test('reuses detected port when no explicit port given', async () => {
|
|
206
|
+
// First call: restart reads PID (running daemon)
|
|
207
|
+
// Second call: handleStop reads PID (to terminate)
|
|
208
|
+
// Third call: handleStart reads PID (no existing daemon after stop)
|
|
209
|
+
vi.spyOn(daemon, 'readPidFile')
|
|
210
|
+
.mockReturnValueOnce(3333) // restart: detect port
|
|
211
|
+
.mockReturnValueOnce(3333) // handleStop: find process
|
|
212
|
+
.mockReturnValueOnce(null); // handleStart: no existing
|
|
213
|
+
vi.spyOn(daemon, 'detectListeningPort').mockReturnValue(4000);
|
|
214
|
+
vi.spyOn(daemon, 'findAvailablePort').mockResolvedValue(4000);
|
|
215
|
+
vi.spyOn(daemon, 'isProcessRunning').mockImplementation(
|
|
216
|
+
(pid) => pid === 3333 || pid === 5555
|
|
217
|
+
);
|
|
218
|
+
vi.spyOn(daemon, 'terminateProcess').mockResolvedValue(true);
|
|
219
|
+
vi.spyOn(daemon, 'removePidFile').mockImplementation(() => {});
|
|
220
|
+
vi.spyOn(daemon, 'printServerUrl').mockImplementation(() => {});
|
|
221
|
+
|
|
222
|
+
const start_daemon = vi
|
|
223
|
+
.spyOn(daemon, 'startDaemon')
|
|
224
|
+
.mockReturnValue({ pid: 5555 });
|
|
225
|
+
|
|
226
|
+
const code = await handleRestart();
|
|
227
|
+
|
|
228
|
+
expect(code).toBe(0);
|
|
229
|
+
expect(start_daemon).toHaveBeenCalledWith(
|
|
230
|
+
expect.objectContaining({ port: 4000 })
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('explicit port overrides detected port', async () => {
|
|
235
|
+
vi.spyOn(daemon, 'readPidFile')
|
|
236
|
+
.mockReturnValueOnce(3333)
|
|
237
|
+
.mockReturnValueOnce(3333)
|
|
238
|
+
.mockReturnValueOnce(null);
|
|
239
|
+
vi.spyOn(daemon, 'detectListeningPort').mockReturnValue(4000);
|
|
240
|
+
vi.spyOn(daemon, 'isProcessRunning').mockImplementation(
|
|
241
|
+
(pid) => pid === 3333 || pid === 6666
|
|
242
|
+
);
|
|
243
|
+
vi.spyOn(daemon, 'terminateProcess').mockResolvedValue(true);
|
|
244
|
+
vi.spyOn(daemon, 'removePidFile').mockImplementation(() => {});
|
|
245
|
+
|
|
246
|
+
const start_daemon = vi
|
|
247
|
+
.spyOn(daemon, 'startDaemon')
|
|
248
|
+
.mockReturnValue({ pid: 6666 });
|
|
249
|
+
|
|
250
|
+
const code = await handleRestart({ port: 9999 });
|
|
251
|
+
|
|
252
|
+
expect(code).toBe(0);
|
|
253
|
+
expect(start_daemon).toHaveBeenCalledWith(
|
|
254
|
+
expect.objectContaining({ port: 9999 })
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('falls back to default when port detection fails', async () => {
|
|
259
|
+
vi.spyOn(daemon, 'readPidFile')
|
|
260
|
+
.mockReturnValueOnce(3333)
|
|
261
|
+
.mockReturnValueOnce(3333)
|
|
262
|
+
.mockReturnValueOnce(null);
|
|
263
|
+
vi.spyOn(daemon, 'detectListeningPort').mockReturnValue(null);
|
|
264
|
+
vi.spyOn(daemon, 'findAvailablePort').mockResolvedValue(3000);
|
|
265
|
+
vi.spyOn(daemon, 'isProcessRunning').mockImplementation(
|
|
266
|
+
(pid) => pid === 3333 || pid === 7777
|
|
267
|
+
);
|
|
268
|
+
vi.spyOn(daemon, 'terminateProcess').mockResolvedValue(true);
|
|
269
|
+
vi.spyOn(daemon, 'removePidFile').mockImplementation(() => {});
|
|
270
|
+
vi.spyOn(daemon, 'printServerUrl').mockImplementation(() => {});
|
|
271
|
+
|
|
272
|
+
const start_daemon = vi
|
|
273
|
+
.spyOn(daemon, 'startDaemon')
|
|
274
|
+
.mockReturnValue({ pid: 7777 });
|
|
275
|
+
|
|
276
|
+
const code = await handleRestart();
|
|
277
|
+
|
|
278
|
+
expect(code).toBe(0);
|
|
279
|
+
// port should not be set — falls through to default
|
|
280
|
+
expect(start_daemon.mock.calls[0]?.[0]).toEqual(
|
|
281
|
+
expect.not.objectContaining({ port: expect.any(Number) })
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('re-registers workspaces from previous server after restart', async () => {
|
|
286
|
+
const fetch_workspaces = /** @type {import('vitest').Mock} */ (
|
|
287
|
+
open.fetchWorkspacesFromServer
|
|
288
|
+
);
|
|
289
|
+
fetch_workspaces.mockResolvedValueOnce([
|
|
290
|
+
{ path: '/project/a', database: '/project/a/.beads' },
|
|
291
|
+
{ path: '/project/b', database: '/project/b/.beads' }
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
const register_workspace = /** @type {import('vitest').Mock} */ (
|
|
295
|
+
open.registerWorkspaceWithServer
|
|
296
|
+
);
|
|
297
|
+
register_workspace.mockReset();
|
|
298
|
+
|
|
299
|
+
vi.spyOn(daemon, 'readPidFile')
|
|
300
|
+
.mockReturnValueOnce(3333) // restart: detect port
|
|
301
|
+
.mockReturnValueOnce(3333) // handleStop: find process
|
|
302
|
+
.mockReturnValueOnce(null); // handleStart: no existing
|
|
303
|
+
vi.spyOn(daemon, 'detectListeningPort').mockReturnValue(null);
|
|
304
|
+
vi.spyOn(daemon, 'isProcessRunning').mockImplementation(
|
|
305
|
+
(pid) => pid === 3333 || pid === 9999
|
|
306
|
+
);
|
|
307
|
+
vi.spyOn(daemon, 'terminateProcess').mockResolvedValue(true);
|
|
308
|
+
vi.spyOn(daemon, 'removePidFile').mockImplementation(() => {});
|
|
309
|
+
vi.spyOn(daemon, 'printServerUrl').mockImplementation(() => {});
|
|
310
|
+
vi.spyOn(daemon, 'startDaemon').mockReturnValue({ pid: 9999 });
|
|
311
|
+
|
|
312
|
+
const code = await handleRestart();
|
|
313
|
+
|
|
314
|
+
expect(code).toBe(0);
|
|
315
|
+
// The cwd workspace is registered by handleStart, plus the two saved ones
|
|
316
|
+
expect(register_workspace).toHaveBeenCalledWith('http://127.0.0.1:3000', {
|
|
317
|
+
path: '/project/a',
|
|
318
|
+
database: '/project/a/.beads'
|
|
319
|
+
});
|
|
320
|
+
expect(register_workspace).toHaveBeenCalledWith('http://127.0.0.1:3000', {
|
|
321
|
+
path: '/project/b',
|
|
322
|
+
database: '/project/b/.beads'
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('port auto-increment (unit)', () => {
|
|
328
|
+
test('auto-increments port when default is in use by non-bdui', async () => {
|
|
329
|
+
const register_workspace = /** @type {import('vitest').Mock} */ (
|
|
330
|
+
open.registerWorkspaceWithServer
|
|
331
|
+
);
|
|
332
|
+
// Registration fails — not a bdui instance on that port
|
|
333
|
+
register_workspace.mockResolvedValueOnce(false);
|
|
334
|
+
|
|
335
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
|
|
336
|
+
vi.spyOn(daemon, 'findAvailablePort').mockResolvedValue(3001);
|
|
337
|
+
vi.spyOn(daemon, 'isProcessRunning').mockImplementation(
|
|
338
|
+
(pid) => pid === 8888
|
|
339
|
+
);
|
|
340
|
+
vi.spyOn(daemon, 'printServerUrl').mockImplementation(() => {});
|
|
341
|
+
|
|
342
|
+
const start_daemon = vi
|
|
343
|
+
.spyOn(daemon, 'startDaemon')
|
|
344
|
+
.mockReturnValue({ pid: 8888 });
|
|
345
|
+
|
|
346
|
+
const code = await handleStart({ open: false });
|
|
347
|
+
|
|
348
|
+
expect(code).toBe(0);
|
|
349
|
+
expect(start_daemon).toHaveBeenCalledWith(
|
|
350
|
+
expect.objectContaining({ port: 3001 })
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('reuses existing bdui when default port is occupied', async () => {
|
|
355
|
+
const register_workspace = /** @type {import('vitest').Mock} */ (
|
|
356
|
+
open.registerWorkspaceWithServer
|
|
357
|
+
);
|
|
358
|
+
// Registration succeeds — existing bdui on that port
|
|
359
|
+
register_workspace.mockResolvedValueOnce(true);
|
|
360
|
+
|
|
361
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
|
|
362
|
+
vi.spyOn(daemon, 'findAvailablePort').mockResolvedValue(3001);
|
|
363
|
+
|
|
364
|
+
const start_daemon = vi
|
|
365
|
+
.spyOn(daemon, 'startDaemon')
|
|
366
|
+
.mockReturnValue({ pid: 8888 });
|
|
367
|
+
|
|
368
|
+
const code = await handleStart({ open: false });
|
|
369
|
+
|
|
370
|
+
expect(code).toBe(0);
|
|
371
|
+
// Should NOT have started a new daemon — reused existing
|
|
372
|
+
expect(start_daemon).not.toHaveBeenCalled();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('does not auto-increment when explicit port is given', async () => {
|
|
376
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
|
|
377
|
+
vi.spyOn(daemon, 'isProcessRunning').mockImplementation(
|
|
378
|
+
(pid) => pid === 8888
|
|
379
|
+
);
|
|
380
|
+
vi.spyOn(daemon, 'printServerUrl').mockImplementation(() => {});
|
|
381
|
+
|
|
382
|
+
const find_port = vi
|
|
383
|
+
.spyOn(daemon, 'findAvailablePort')
|
|
384
|
+
.mockResolvedValue(5000);
|
|
385
|
+
|
|
386
|
+
const start_daemon = vi
|
|
387
|
+
.spyOn(daemon, 'startDaemon')
|
|
388
|
+
.mockReturnValue({ pid: 8888 });
|
|
389
|
+
|
|
390
|
+
const code = await handleStart({ open: false, port: 5000 });
|
|
391
|
+
|
|
392
|
+
expect(code).toBe(0);
|
|
393
|
+
// findAvailablePort should not be called when port is explicit
|
|
394
|
+
expect(find_port).not.toHaveBeenCalled();
|
|
395
|
+
expect(start_daemon).toHaveBeenCalledWith(
|
|
396
|
+
expect.objectContaining({ port: 5000 })
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test('returns 1 when no port is available', async () => {
|
|
401
|
+
vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
|
|
402
|
+
vi.spyOn(daemon, 'findAvailablePort').mockResolvedValue(null);
|
|
403
|
+
|
|
404
|
+
const code = await handleStart({ open: false });
|
|
405
|
+
|
|
406
|
+
expect(code).toBe(1);
|
|
407
|
+
});
|
|
408
|
+
});
|