beads-ui 0.1.1 → 0.2.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.
Files changed (82) hide show
  1. package/CHANGES.md +27 -1
  2. package/README.md +39 -45
  3. package/app/data/providers.js +57 -26
  4. package/app/index.html +8 -0
  5. package/app/main.js +179 -33
  6. package/app/protocol.md +3 -4
  7. package/app/router.js +45 -9
  8. package/app/state.js +27 -11
  9. package/app/styles.css +170 -6
  10. package/app/utils/issue-id-renderer.js +71 -0
  11. package/app/utils/issue-url.js +9 -0
  12. package/app/utils/toast.js +35 -0
  13. package/app/views/board.js +347 -17
  14. package/app/views/detail.js +292 -92
  15. package/app/views/epics.js +2 -2
  16. package/app/views/issue-dialog.js +170 -0
  17. package/app/views/issue-row.js +9 -8
  18. package/app/views/list.js +85 -11
  19. package/app/views/new-issue-dialog.js +352 -0
  20. package/app/ws.js +30 -0
  21. package/docs/architecture.md +1 -1
  22. package/package.json +17 -1
  23. package/server/cli/commands.js +11 -3
  24. package/server/cli/index.js +35 -4
  25. package/server/cli/usage.js +1 -1
  26. package/server/watcher.js +3 -3
  27. package/server/ws.js +39 -19
  28. package/.beads/issues.jsonl +0 -107
  29. package/.editorconfig +0 -10
  30. package/.eslintrc.json +0 -36
  31. package/.github/workflows/ci.yml +0 -38
  32. package/.prettierignore +0 -5
  33. package/AGENTS.md +0 -85
  34. package/app/data/providers.test.js +0 -126
  35. package/app/main.board-switch.test.js +0 -94
  36. package/app/main.deep-link.test.js +0 -64
  37. package/app/main.live-updates.test.js +0 -229
  38. package/app/main.test.js +0 -17
  39. package/app/main.theme.test.js +0 -41
  40. package/app/main.view-sync.test.js +0 -54
  41. package/app/protocol.test.js +0 -57
  42. package/app/router.test.js +0 -34
  43. package/app/state.test.js +0 -21
  44. package/app/utils/markdown.test.js +0 -103
  45. package/app/utils/type-badge.test.js +0 -30
  46. package/app/views/board.test.js +0 -184
  47. package/app/views/detail.acceptance-notes.test.js +0 -67
  48. package/app/views/detail.assignee.test.js +0 -161
  49. package/app/views/detail.deps.test.js +0 -97
  50. package/app/views/detail.edits.test.js +0 -146
  51. package/app/views/detail.labels.test.js +0 -73
  52. package/app/views/detail.priority.test.js +0 -86
  53. package/app/views/detail.test.js +0 -188
  54. package/app/views/detail.ui47.test.js +0 -78
  55. package/app/views/epics.test.js +0 -283
  56. package/app/views/list.inline-edits.test.js +0 -84
  57. package/app/views/list.test.js +0 -479
  58. package/app/views/nav.test.js +0 -43
  59. package/app/ws.test.js +0 -168
  60. package/docs/quickstart.md +0 -142
  61. package/eslint.config.js +0 -59
  62. package/media/bdui-board.png +0 -0
  63. package/media/bdui-epics.png +0 -0
  64. package/media/bdui-issues.png +0 -0
  65. package/prettier.config.js +0 -13
  66. package/server/app.test.js +0 -29
  67. package/server/bd.test.js +0 -93
  68. package/server/cli/cli.test.js +0 -109
  69. package/server/cli/commands.integration.test.js +0 -155
  70. package/server/cli/commands.unit.test.js +0 -94
  71. package/server/cli/open.test.js +0 -26
  72. package/server/db.test.js +0 -70
  73. package/server/protocol.test.js +0 -87
  74. package/server/watcher.test.js +0 -100
  75. package/server/ws.handlers.test.js +0 -174
  76. package/server/ws.labels.test.js +0 -95
  77. package/server/ws.mutations.test.js +0 -261
  78. package/server/ws.subscriptions.test.js +0 -116
  79. package/server/ws.test.js +0 -52
  80. package/test/setup-vitest.js +0 -12
  81. package/tsconfig.json +0 -23
  82. package/vitest.config.mjs +0 -14
@@ -1,26 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { computeOpenCommand } from './open.js';
3
-
4
- describe('computeOpenCommand', () => {
5
- test('returns macOS open command', () => {
6
- const r = computeOpenCommand('http://127.0.0.1:3000', 'darwin');
7
-
8
- expect(r.cmd).toBe('open');
9
- expect(r.args).toEqual(['http://127.0.0.1:3000']);
10
- });
11
-
12
- test('returns Linux xdg-open command', () => {
13
- const r = computeOpenCommand('http://127.0.0.1:3000', 'linux');
14
-
15
- expect(r.cmd).toBe('xdg-open');
16
- expect(r.args).toEqual(['http://127.0.0.1:3000']);
17
- });
18
-
19
- test('returns Windows start command via cmd', () => {
20
- const r = computeOpenCommand('http://127.0.0.1:3000', 'win32');
21
-
22
- expect(r.cmd).toBe('cmd');
23
- expect(r.args.slice(0, 3)).toEqual(['/c', 'start', '']);
24
- expect(r.args[r.args.length - 1]).toBe('http://127.0.0.1:3000');
25
- });
26
- });
package/server/db.test.js DELETED
@@ -1,70 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
5
- import { findNearestBeadsDb, resolveDbPath } from './db.js';
6
-
7
- /** @type {string[]} */
8
- const tmps = [];
9
-
10
- function mkdtemp() {
11
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beads-ui-test-'));
12
- tmps.push(dir);
13
- return dir;
14
- }
15
-
16
- beforeEach(() => {
17
- vi.resetModules();
18
- });
19
-
20
- afterEach(() => {
21
- for (const d of tmps.splice(0)) {
22
- try {
23
- fs.rmSync(d, { recursive: true, force: true });
24
- } catch {
25
- // ignore cleanup errors
26
- }
27
- }
28
- });
29
-
30
- describe('resolveDbPath', () => {
31
- test('uses explicit_db when provided', () => {
32
- const res = resolveDbPath({ cwd: '/x', explicit_db: './my.db', env: {} });
33
- expect(res.path.endsWith('/x/my.db')).toBe(true);
34
- expect(res.source).toBe('flag');
35
- });
36
-
37
- test('uses BEADS_DB from env when set', () => {
38
- const res = resolveDbPath({ cwd: '/x', env: { BEADS_DB: '/abs/env.db' } });
39
- expect(res.path).toBe('/abs/env.db');
40
- expect(res.source).toBe('env');
41
- });
42
-
43
- test('finds nearest .beads/ui.db walking up', () => {
44
- const root = mkdtemp();
45
- const nested = path.join(root, 'a', 'b', 'c');
46
- fs.mkdirSync(nested, { recursive: true });
47
- const beads = path.join(root, '.beads');
48
- fs.mkdirSync(beads);
49
- const uiDb = path.join(beads, 'ui.db');
50
- fs.writeFileSync(uiDb, '');
51
-
52
- const found = findNearestBeadsDb(nested);
53
- expect(found).toBe(uiDb);
54
-
55
- const res = resolveDbPath({ cwd: nested, env: {} });
56
- expect(res.path).toBe(uiDb);
57
- expect(res.source).toBe('nearest');
58
- });
59
-
60
- test('falls back to ~/.beads/default.db when none found', async () => {
61
- // Mock os.homedir to a deterministic location using spy
62
- const home = mkdtemp();
63
- const spy = vi.spyOn(os, 'homedir').mockReturnValue(home);
64
- const mod = await import('./db.js');
65
- const res = mod.resolveDbPath({ cwd: '/no/db/here', env: {} });
66
- expect(res.path).toBe(path.join(home, '.beads', 'default.db'));
67
- expect(res.source).toBe('home-default');
68
- spy.mockRestore();
69
- });
70
- });
@@ -1,87 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import {
3
- MESSAGE_TYPES,
4
- PROTOCOL_VERSION,
5
- decodeReply,
6
- decodeRequest,
7
- isMessageType,
8
- isReply,
9
- isRequest,
10
- makeError,
11
- makeOk,
12
- makeRequest
13
- } from './protocol.js';
14
-
15
- describe('server/protocol', () => {
16
- test('isMessageType returns true for known type', () => {
17
- // execution
18
- const res = isMessageType('list-issues');
19
-
20
- // assertion
21
- expect(res).toBe(true);
22
- });
23
-
24
- test('isMessageType returns false for unknown type', () => {
25
- // execution
26
- const res = isMessageType('not-a-type');
27
-
28
- // assertion
29
- expect(res).toBe(false);
30
- });
31
-
32
- test('makeRequest and decodeRequest round-trip', () => {
33
- // setup
34
- const req = makeRequest('show-issue', { id: 'UI-9' }, 'r-9');
35
-
36
- // execution
37
- const decoded = decodeRequest(JSON.parse(JSON.stringify(req)));
38
-
39
- // assertion
40
- expect(isRequest(req)).toBe(true);
41
- expect(decoded.id).toBe('r-9');
42
- expect(decoded.type).toBe('show-issue');
43
- });
44
-
45
- test('makeOk and makeError create valid replies', () => {
46
- // setup
47
- const req = makeRequest('list-issues', undefined, 'r-10');
48
-
49
- // execution
50
- const ok = makeOk(req, [{ id: 'UI-1' }]);
51
- const err = makeError(req, 'boom', 'Something went wrong');
52
-
53
- // assertion
54
- expect(isReply(ok)).toBe(true);
55
- expect(isReply(err)).toBe(true);
56
- expect(ok.ok).toBe(true);
57
- expect(err.ok).toBe(false);
58
- });
59
-
60
- test('decodeReply accepts ok and error envelopes', () => {
61
- // setup
62
- const req = makeRequest('edit-text', { id: 'UI-1', text: 'x' }, 'r-11');
63
- const ok = makeOk(req, { id: 'UI-1' });
64
- const err = makeError(req, 'validation', 'Invalid');
65
-
66
- // execution
67
- const ok2 = decodeReply(JSON.parse(JSON.stringify(ok)));
68
- const err2 = decodeReply(JSON.parse(JSON.stringify(err)));
69
-
70
- // assertion
71
- expect(ok2.ok).toBe(true);
72
- expect(err2.ok).toBe(false);
73
- });
74
-
75
- test('invalid envelopes throw on decode', () => {
76
- // execution + assertion
77
- expect(() => decodeRequest({})).toThrow();
78
- expect(() => decodeReply({ ok: true })).toThrow();
79
- });
80
-
81
- test('exports protocol constants', () => {
82
- // assertion
83
- expect(typeof PROTOCOL_VERSION).toBe('string');
84
- expect(Array.isArray(MESSAGE_TYPES)).toBe(true);
85
- expect(MESSAGE_TYPES.length).toBeGreaterThan(0);
86
- });
87
- });
@@ -1,100 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
- import { watchDb } from './watcher.js';
3
-
4
- /** @type {{ dir: string, cb: (event: string, filename?: string) => void, w: { close: () => void } }[]} */
5
- const watchers = [];
6
-
7
- vi.mock('node:fs', () => {
8
- const watch = vi.fn((dir, _opts, cb) => {
9
- // Minimal event emitter interface for FSWatcher
10
- const handlers = /** @type {{ close: Array<() => void> }} */ ({
11
- close: []
12
- });
13
- const w = {
14
- close: () => handlers.close.forEach((fn) => fn())
15
- };
16
- watchers.push({ dir, cb, w });
17
- return /** @type {any} */ (w);
18
- });
19
- return { default: { watch }, watch };
20
- });
21
-
22
- beforeEach(() => {
23
- watchers.length = 0;
24
- vi.useFakeTimers();
25
- vi.spyOn(console, 'warn').mockImplementation(() => {});
26
- });
27
-
28
- afterEach(() => {
29
- vi.useRealTimers();
30
- vi.restoreAllMocks();
31
- });
32
-
33
- describe('watchDb', () => {
34
- test('debounces rapid change events', () => {
35
- const calls = [];
36
- const handle = watchDb('/repo', (p) => calls.push(p), {
37
- debounce_ms: 100,
38
- explicit_db: '/repo/.beads/ui.db'
39
- });
40
- expect(watchers.length).toBe(1);
41
- const { cb } = watchers[0];
42
-
43
- // Fire multiple changes in quick succession
44
- cb('change', 'ui.db');
45
- cb('change', 'ui.db');
46
- cb('rename', 'ui.db');
47
-
48
- // Nothing yet until debounce passes
49
- expect(calls.length).toBe(0);
50
- vi.advanceTimersByTime(99);
51
- expect(calls.length).toBe(0);
52
- vi.advanceTimersByTime(1);
53
- expect(calls.length).toBe(1);
54
-
55
- // Cleanup
56
- handle.close();
57
- });
58
-
59
- test('ignores other filenames', () => {
60
- const calls = [];
61
- const handle = watchDb('/repo', (p) => calls.push(p), {
62
- debounce_ms: 50,
63
- explicit_db: '/repo/.beads/ui.db'
64
- });
65
- const { cb } = watchers[0];
66
- cb('change', 'something-else.db');
67
- vi.advanceTimersByTime(60);
68
- expect(calls.length).toBe(0);
69
- handle.close();
70
- });
71
-
72
- test('rebind attaches to new db path', () => {
73
- const calls = [];
74
- const handle = watchDb('/repo', (p) => calls.push(p), {
75
- debounce_ms: 50,
76
- explicit_db: '/repo/.beads/ui.db'
77
- });
78
- expect(watchers.length).toBe(1);
79
- const first = watchers[0];
80
-
81
- // Rebind to a different DB path
82
- handle.rebind({ explicit_db: '/other/.beads/alt.db' });
83
-
84
- // A new watcher is created
85
- expect(watchers.length).toBe(2);
86
- const second = watchers[1];
87
-
88
- // Old watcher should ignore new file name
89
- first.cb('change', 'ui.db');
90
- vi.advanceTimersByTime(60);
91
- expect(calls.length).toBe(0);
92
-
93
- // New watcher reacts
94
- second.cb('change', 'alt.db');
95
- vi.advanceTimersByTime(60);
96
- expect(calls.length).toBe(1);
97
-
98
- handle.close();
99
- });
100
- });
@@ -1,174 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { runBdJson } from './bd.js';
3
- import { handleMessage } from './ws.js';
4
-
5
- vi.mock('./bd.js', () => ({ runBdJson: vi.fn() }));
6
-
7
- function makeStubSocket() {
8
- return {
9
- sent: /** @type {string[]} */ ([]),
10
- readyState: 1,
11
- OPEN: 1,
12
- /** @param {string} msg */
13
- send(msg) {
14
- this.sent.push(String(msg));
15
- }
16
- };
17
- }
18
-
19
- describe('ws handlers: list/show', () => {
20
- test('list-issues forwards payload from bd', async () => {
21
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
22
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [{ id: 'UI-1' }] });
23
- const ws = makeStubSocket();
24
- const req = {
25
- id: 'r1',
26
- type: 'list-issues',
27
- payload: { filters: { status: 'open' } }
28
- };
29
- await handleMessage(
30
- /** @type {any} */ (ws),
31
- Buffer.from(JSON.stringify(req))
32
- );
33
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
34
- expect(obj.ok).toBe(true);
35
- expect(Array.isArray(obj.payload)).toBe(true);
36
- });
37
-
38
- test('list-issues with filters.ready uses bd "ready"', async () => {
39
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
40
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [{ id: 'UI-2' }] });
41
- const ws = makeStubSocket();
42
- const req = {
43
- id: 'r1a',
44
- type: 'list-issues',
45
- payload: { filters: { ready: true } }
46
- };
47
- await handleMessage(
48
- /** @type {any} */ (ws),
49
- Buffer.from(JSON.stringify(req))
50
- );
51
- // Ensure we called the ready command
52
- const call = mocked.mock.calls[mocked.mock.calls.length - 1];
53
- expect(Array.isArray(call[0])).toBe(true);
54
- expect(call[0][0]).toBe('ready');
55
-
56
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
57
- expect(obj.ok).toBe(true);
58
- expect(Array.isArray(obj.payload)).toBe(true);
59
- });
60
-
61
- test('show-issue returns error on missing id', async () => {
62
- const ws = makeStubSocket();
63
- const req = { id: 'r2', type: 'show-issue', payload: {} };
64
- await handleMessage(
65
- /** @type {any} */ (ws),
66
- Buffer.from(JSON.stringify(req))
67
- );
68
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
69
- expect(obj.ok).toBe(false);
70
- expect(obj.error.code).toBe('bad_request');
71
- });
72
-
73
- test('show-issue forwards bd JSON on success', async () => {
74
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
75
- mocked.mockResolvedValueOnce({
76
- code: 0,
77
- stdoutJson: { id: 'UI-9', title: 'X' }
78
- });
79
- const ws = makeStubSocket();
80
- const req = { id: 'r3', type: 'show-issue', payload: { id: 'UI-9' } };
81
- await handleMessage(
82
- /** @type {any} */ (ws),
83
- Buffer.from(JSON.stringify(req))
84
- );
85
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
86
- expect(obj.ok).toBe(true);
87
- expect(obj.payload.id).toBe('UI-9');
88
- });
89
-
90
- test('show-issue unwraps single-element arrays from bd', async () => {
91
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
92
- mocked.mockResolvedValueOnce({
93
- code: 0,
94
- stdoutJson: [{ id: 'UI-9', title: 'X' }]
95
- });
96
- const ws = makeStubSocket();
97
- const req = { id: 'r3a', type: 'show-issue', payload: { id: 'UI-9' } };
98
- await handleMessage(
99
- /** @type {any} */ (ws),
100
- Buffer.from(JSON.stringify(req))
101
- );
102
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
103
- expect(obj.ok).toBe(true);
104
- expect(obj.payload && obj.payload.id).toBe('UI-9');
105
- });
106
-
107
- test('show-issue returns not_found when bd returns empty array', async () => {
108
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
109
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [] });
110
- const ws = makeStubSocket();
111
- const req = { id: 'r3b', type: 'show-issue', payload: { id: 'X' } };
112
- await handleMessage(
113
- /** @type {any} */ (ws),
114
- Buffer.from(JSON.stringify(req))
115
- );
116
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
117
- expect(obj.ok).toBe(false);
118
- expect(obj.error && obj.error.code).toBe('not_found');
119
- });
120
-
121
- test('epic-status forwards bd epic status', async () => {
122
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
123
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [] });
124
- const ws = makeStubSocket();
125
- const req = { id: 're', type: /** @type {any} */ ('epic-status') };
126
- await handleMessage(
127
- /** @type {any} */ (ws),
128
- Buffer.from(JSON.stringify(req))
129
- );
130
- const call = mocked.mock.calls[mocked.mock.calls.length - 1];
131
- expect(Array.isArray(call[0])).toBe(true);
132
- expect(call[0][0]).toBe('epic');
133
- expect(call[0][1]).toBe('status');
134
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
135
- expect(obj.ok).toBe(true);
136
- expect(Array.isArray(obj.payload)).toBe(true);
137
- });
138
-
139
- test('list-issues supports limit passthrough', async () => {
140
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
141
- mocked.mockResolvedValueOnce({ code: 0, stdoutJson: [] });
142
- const ws = makeStubSocket();
143
- const req = {
144
- id: 'rlim',
145
- type: 'list-issues',
146
- payload: { filters: { status: 'closed', limit: 10 } }
147
- };
148
- await handleMessage(
149
- /** @type {any} */ (ws),
150
- Buffer.from(JSON.stringify(req))
151
- );
152
- const call = mocked.mock.calls[mocked.mock.calls.length - 1];
153
- const args = /** @type {string[]} */ (call[0]);
154
- expect(args.includes('--limit')).toBe(true);
155
- expect(args.includes('10')).toBe(true);
156
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
157
- expect(obj.ok).toBe(true);
158
- expect(Array.isArray(obj.payload)).toBe(true);
159
- });
160
-
161
- test('bd error propagates as bd_error reply', async () => {
162
- const mocked = /** @type {import('vitest').Mock} */ (runBdJson);
163
- mocked.mockResolvedValueOnce({ code: 1, stderr: 'boom' });
164
- const ws = makeStubSocket();
165
- const req = { id: 'r4', type: 'list-issues', payload: {} };
166
- await handleMessage(
167
- /** @type {any} */ (ws),
168
- Buffer.from(JSON.stringify(req))
169
- );
170
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
171
- expect(obj.ok).toBe(false);
172
- expect(obj.error.code).toBe('bd_error');
173
- });
174
- });
@@ -1,95 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { runBd, runBdJson } from './bd.js';
3
- import { handleMessage } from './ws.js';
4
-
5
- vi.mock('./bd.js', () => ({
6
- runBd: vi.fn(),
7
- runBdJson: vi.fn()
8
- }));
9
-
10
- function makeStubSocket() {
11
- return {
12
- sent: /** @type {string[]} */ ([]),
13
- readyState: 1,
14
- OPEN: 1,
15
- /** @param {string} msg */
16
- send(msg) {
17
- this.sent.push(String(msg));
18
- }
19
- };
20
- }
21
-
22
- describe('ws labels handlers', () => {
23
- test('label-add validates payload', async () => {
24
- const ws = makeStubSocket();
25
- await handleMessage(
26
- /** @type {any} */ (ws),
27
- Buffer.from(
28
- JSON.stringify({
29
- id: 'x',
30
- type: /** @type {any} */ ('label-add'),
31
- payload: {}
32
- })
33
- )
34
- );
35
- const obj = JSON.parse(ws.sent[0]);
36
- expect(obj.ok).toBe(false);
37
- expect(obj.error.code).toBe('bad_request');
38
- });
39
-
40
- test('label-add runs bd and replies with show', async () => {
41
- const rb = /** @type {import('vitest').Mock} */ (runBd);
42
- const rj = /** @type {import('vitest').Mock} */ (runBdJson);
43
- rb.mockResolvedValueOnce({ code: 0, stdout: '', stderr: '' });
44
- rj.mockResolvedValueOnce({
45
- code: 0,
46
- stdoutJson: { id: 'UI-1', labels: ['frontend'] }
47
- });
48
-
49
- const ws = makeStubSocket();
50
- await handleMessage(
51
- /** @type {any} */ (ws),
52
- Buffer.from(
53
- JSON.stringify({
54
- id: 'a',
55
- type: /** @type {any} */ ('label-add'),
56
- payload: { id: 'UI-1', label: 'frontend' }
57
- })
58
- )
59
- );
60
-
61
- const call = rb.mock.calls[0][0];
62
- expect(call.slice(0, 3)).toEqual(['label', 'add', 'UI-1']);
63
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
64
- expect(obj.ok).toBe(true);
65
- expect(obj.payload && obj.payload.id).toBe('UI-1');
66
- });
67
-
68
- test('label-remove runs bd and replies with show', async () => {
69
- const rb = /** @type {import('vitest').Mock} */ (runBd);
70
- const rj = /** @type {import('vitest').Mock} */ (runBdJson);
71
- rb.mockResolvedValueOnce({ code: 0, stdout: '', stderr: '' });
72
- rj.mockResolvedValueOnce({
73
- code: 0,
74
- stdoutJson: { id: 'UI-1', labels: [] }
75
- });
76
-
77
- const ws = makeStubSocket();
78
- await handleMessage(
79
- /** @type {any} */ (ws),
80
- Buffer.from(
81
- JSON.stringify({
82
- id: 'b',
83
- type: /** @type {any} */ ('label-remove'),
84
- payload: { id: 'UI-1', label: 'frontend' }
85
- })
86
- )
87
- );
88
-
89
- const call = rb.mock.calls[rb.mock.calls.length - 1][0];
90
- expect(call.slice(0, 3)).toEqual(['label', 'remove', 'UI-1']);
91
- const obj = JSON.parse(ws.sent[ws.sent.length - 1]);
92
- expect(obj.ok).toBe(true);
93
- expect(obj.payload && obj.payload.id).toBe('UI-1');
94
- });
95
- });