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,142 +0,0 @@
1
- # beads-ui Quickstart
2
-
3
- This project provides a local-first SPA for the `bd` (beads) CLI. It runs a
4
- local HTTP + WebSocket server that serves the UI and proxies edits to the `bd`
5
- CLI. Changes to the active beads database are pushed live to the browser.
6
-
7
- ## Prerequisites
8
-
9
- - Node.js >= 22
10
- - The `bd` CLI on your PATH (or set `BD_BIN=/path/to/bd`)
11
- - An initialized beads database (see below)
12
-
13
- ## Install
14
-
15
- ```sh
16
- npm install
17
- ```
18
-
19
- ## Run
20
-
21
- Use the CLI to daemonize the server and open your browser:
22
-
23
- ```sh
24
- bdui start
25
- ```
26
-
27
- Or run in the foreground for quick debugging:
28
-
29
- ```sh
30
- npm start
31
- ```
32
-
33
- - Server binds to `127.0.0.1:3000` by default.
34
- - Open http://127.0.0.1:3000 in your browser.
35
-
36
- Environment knobs:
37
-
38
- - `PORT` to change the listen port (default: `3000`). The server always binds to
39
- `127.0.0.1` for local‑only access.
40
- - `BD_BIN` to point at a non-default `bd` binary.
41
-
42
- ## Database Resolution and Watching
43
-
44
- The server and watcher resolve the active beads database in this order:
45
-
46
- 1. `--db <path>` injected by the server when invoking `bd` (derived from
47
- resolution below)
48
- 2. `BEADS_DB` environment variable, if set
49
- 3. Nearest `.beads/*.db` by walking up from the server `root_dir`
50
- 4. `~/.beads/default.db`
51
-
52
- The watcher listens for changes to the resolved SQLite DB and broadcasts an
53
- `issues-changed` event to all connected clients. See `docs/db-watching.md` for
54
- details.
55
-
56
- ## Initialize a Workspace (if needed)
57
-
58
- From your project root:
59
-
60
- ```sh
61
- # create a workspace DB
62
- bd init
63
-
64
- # create a few issues
65
- bd create "First issue" -t task -p 2 -d "Initial work"
66
- bd create "Bug: wrong color" -t bug -p 1
67
- ```
68
-
69
- The UI should list these after startup. Edits in the UI map to `bd update`
70
- commands executed by the server.
71
-
72
- ## Development Workflow
73
-
74
- - Type check: `npm run typecheck`
75
- - Tests: `npm test`
76
- - Lint: `npm run lint`
77
- - Format: `npm run format`
78
-
79
- Tests cover protocol handlers, WebSocket client/server behavior, and core UI
80
- flows (list and detail views, edits, and dependency management).
81
-
82
- ## CLI (`bdui`) Local Link
83
-
84
- The `bdui` CLI is exposed via npm’s `bin` field for local development. To make
85
- it available on your PATH:
86
-
87
- ```sh
88
- npm link
89
- ```
90
-
91
- Common commands:
92
-
93
- ```sh
94
- bdui start # daemonize the server and open the browser
95
- bdui start --no-open # start without opening a browser (or set BDUI_NO_OPEN=1)
96
- bdui stop # stop the daemon (exit code 2 if not running)
97
- bdui restart # stop then start
98
- bdui --help # usage
99
- ```
100
-
101
- Runtime directory and logs:
102
-
103
- - PID and log files live under `$XDG_RUNTIME_DIR/beads-ui` or the system temp
104
- directory. Override with `BDUI_RUNTIME_DIR=/path`.
105
-
106
- Environment knobs also used by `bdui`:
107
-
108
- - `PORT` to change the listen port (default: `3000`)
109
- - `BDUI_NO_OPEN=1` to disable auto-opening the browser on `start`
110
- - `BDUI_RUNTIME_DIR` to set a custom runtime directory
111
-
112
- ## Protocol
113
-
114
- The WebSocket protocol is documented in `app/protocol.md` and shared by server
115
- and client via `server/protocol.js` re-exports.
116
-
117
- ## Troubleshooting
118
-
119
- - If the UI shows no issues, verify a beads DB exists or run `bd init` in your
120
- workspace.
121
- - To target a specific DB, set `BEADS_DB=/path/to/file.db` before `npm start`.
122
- - If `bd` isn’t on your PATH, set `BD_BIN` to the full path.
123
-
124
- ### `bdui` specific
125
-
126
- - Logs and PID: check the runtime dir for `daemon.log` and `server.pid`.
127
- - Default: `$XDG_RUNTIME_DIR/beads-ui` (Linux), otherwise your system temp
128
- directory (see `os.tmpdir()`).
129
- - Override: set `BDUI_RUNTIME_DIR=/path`.
130
- - Stale process: if `bdui stop` reports exit code `2` but `server.pid` exists,
131
- remove the PID file and try again:
132
-
133
- ```sh
134
- rm "$(bdui --help >/dev/null 2>&1; echo ${BDUI_RUNTIME_DIR:-$(echo ${XDG_RUNTIME_DIR:-/tmp})/beads-ui})/server.pid" 2>/dev/null || true
135
- bdui stop
136
- ```
137
-
138
- - Port in use: set a different port and restart:
139
-
140
- ```sh
141
- PORT=4000 bdui restart
142
- ```
package/eslint.config.js DELETED
@@ -1,59 +0,0 @@
1
- import js from '@eslint/js';
2
- import plugin_jsdoc from 'eslint-plugin-jsdoc';
3
- import plugin_n from 'eslint-plugin-n';
4
- import { defineConfig } from 'eslint/config';
5
- import globals from 'globals';
6
-
7
- export default defineConfig([
8
- {
9
- ignores: ['node_modules', 'coverage', 'dist', '.beads']
10
- },
11
- js.configs.recommended,
12
- plugin_jsdoc.configs['flat/recommended'],
13
- {
14
- settings: {
15
- jsdoc: {
16
- mode: 'typescript',
17
- preferredTypes: {
18
- object: 'Object'
19
- }
20
- }
21
- },
22
- rules: {
23
- 'jsdoc/require-jsdoc': 'off',
24
- 'jsdoc/require-param-description': 'off',
25
- 'jsdoc/require-returns-description': 'off',
26
- 'jsdoc/require-property-description': 'off',
27
- 'jsdoc/reject-any-type': 'off',
28
- 'jsdoc/require-returns': 'off'
29
- }
30
- },
31
- {
32
- files: ['**/*.test.js'],
33
- languageOptions: {
34
- globals: globals.vitest
35
- }
36
- },
37
- {
38
- files: ['server/**/*.js'],
39
- ...plugin_n.configs['flat/recommended'],
40
- languageOptions: {
41
- globals: globals.node
42
- },
43
- rules: {
44
- 'n/no-unpublished-import': 'off'
45
- }
46
- },
47
- {
48
- files: ['bin/**/*.js'],
49
- languageOptions: {
50
- globals: globals.node
51
- }
52
- },
53
- {
54
- files: ['app/**/*.js'],
55
- languageOptions: {
56
- globals: globals.browser
57
- }
58
- }
59
- ]);
Binary file
Binary file
Binary file
@@ -1,13 +0,0 @@
1
- /**
2
- * @import { Config } from 'prettier'
3
- */
4
-
5
- /** @type {Config} */
6
- export default {
7
- singleQuote: true,
8
- trailingComma: 'none',
9
- proseWrap: 'always',
10
- plugins: ['@trivago/prettier-plugin-sort-imports'],
11
- importOrder: ['^@(.*)$', '<THIRD_PARTY_MODULES>', '^[./]'],
12
- importOrderSortSpecifiers: true
13
- };
@@ -1,29 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { describe, expect, test } from 'vitest';
4
- import { createApp } from './app.js';
5
- import { getConfig } from './config.js';
6
-
7
- /**
8
- * Narrow to function type for basic checks.
9
- * @param {unknown} value
10
- * @returns {value is Function}
11
- */
12
- function isFunction(value) {
13
- return typeof value === 'function';
14
- }
15
-
16
- describe('server app wiring (no listen)', () => {
17
- test('createApp returns an express-like app', () => {
18
- const config = getConfig();
19
- const app = createApp(config);
20
- expect(isFunction(app.get)).toBe(true);
21
- expect(isFunction(app.use)).toBe(true);
22
- });
23
-
24
- test('index.html exists in configured app_dir', () => {
25
- const config = getConfig();
26
- const index_path = path.join(config.app_dir, 'index.html');
27
- expect(fs.existsSync(index_path)).toBe(true);
28
- });
29
- });
package/server/bd.test.js DELETED
@@ -1,93 +0,0 @@
1
- import { spawn as spawnMock } from 'node:child_process';
2
- import { EventEmitter } from 'node:events';
3
- import { PassThrough } from 'node:stream';
4
- import { beforeEach, describe, expect, test, vi } from 'vitest';
5
- import { getBdBin, runBd, runBdJson } from './bd.js';
6
-
7
- // Mock child_process.spawn before importing the module under test
8
- vi.mock('node:child_process', () => ({ spawn: vi.fn() }));
9
-
10
- /**
11
- * @param {string} stdoutText
12
- * @param {string} stderrText
13
- * @param {number} code
14
- */
15
- function makeFakeProc(stdoutText, stderrText, code) {
16
- const cp = /** @type {any} */ (new EventEmitter());
17
- const out = new PassThrough();
18
- const err = new PassThrough();
19
- cp.stdout = out;
20
- cp.stderr = err;
21
- // Simulate async emission
22
- queueMicrotask(() => {
23
- if (stdoutText) {
24
- out.write(stdoutText);
25
- }
26
- out.end();
27
- if (stderrText) {
28
- err.write(stderrText);
29
- }
30
- err.end();
31
- cp.emit('close', code);
32
- });
33
- return cp;
34
- }
35
-
36
- const mockedSpawn = /** @type {import('vitest').Mock} */ (spawnMock);
37
-
38
- beforeEach(() => {
39
- mockedSpawn.mockReset();
40
- });
41
-
42
- describe('getBdBin', () => {
43
- test('returns env BD_BIN when set', () => {
44
- const prev = process.env.BD_BIN;
45
- process.env.BD_BIN = '/custom/bd';
46
- expect(getBdBin()).toBe('/custom/bd');
47
- if (prev) {
48
- process.env.BD_BIN = prev;
49
- } else {
50
- delete process.env.BD_BIN;
51
- }
52
- });
53
- });
54
-
55
- describe('runBd', () => {
56
- test('returns stdout/stderr and exit code', async () => {
57
- mockedSpawn.mockReturnValueOnce(makeFakeProc('ok', '', 0));
58
- const res = await runBd(['--version']);
59
- expect(res.code).toBe(0);
60
- expect(res.stdout).toContain('ok');
61
- });
62
-
63
- test('non-zero exit propagates code and stderr', async () => {
64
- mockedSpawn.mockReturnValueOnce(makeFakeProc('', 'boom', 1));
65
- const res = await runBd(['list']);
66
- expect(res.code).toBe(1);
67
- expect(res.stderr).toContain('boom');
68
- });
69
- });
70
-
71
- describe('runBdJson', () => {
72
- test('parses valid JSON output', async () => {
73
- const json = JSON.stringify([{ id: 'UI-1' }]);
74
- mockedSpawn.mockReturnValueOnce(makeFakeProc(json, '', 0));
75
- const res = await runBdJson(['list', '--json']);
76
- expect(res.code).toBe(0);
77
- expect(Array.isArray(res.stdoutJson)).toBe(true);
78
- });
79
-
80
- test('invalid JSON yields stderr message with code 0', async () => {
81
- mockedSpawn.mockReturnValueOnce(makeFakeProc('not-json', '', 0));
82
- const res = await runBdJson(['list', '--json']);
83
- expect(res.code).toBe(0);
84
- expect(res.stderr).toContain('Invalid JSON');
85
- });
86
-
87
- test('non-zero exit returns code and stderr', async () => {
88
- mockedSpawn.mockReturnValueOnce(makeFakeProc('', 'oops', 2));
89
- const res = await runBdJson(['list', '--json']);
90
- expect(res.code).toBe(2);
91
- expect(res.stderr).toContain('oops');
92
- });
93
- });
@@ -1,109 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
- import * as commands from './commands.js';
3
- import { main, parseArgs } from './index.js';
4
-
5
- vi.mock('./commands.js', () => ({
6
- handleStart: vi.fn().mockResolvedValue(0),
7
- handleStop: vi.fn().mockResolvedValue(0),
8
- handleRestart: vi.fn().mockResolvedValue(0)
9
- }));
10
-
11
- /** @type {import('vitest').MockInstance} */
12
- let write_mock;
13
-
14
- beforeEach(() => {
15
- write_mock = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
16
- });
17
-
18
- afterEach(() => {
19
- write_mock.mockRestore();
20
- });
21
-
22
- describe('parseArgs', () => {
23
- test('returns help flag when -h or --help present', () => {
24
- const r1 = parseArgs(['-h']);
25
- const r2 = parseArgs(['--help']);
26
-
27
- expect(r1.flags.includes('help')).toBe(true);
28
- expect(r2.flags.includes('help')).toBe(true);
29
- });
30
-
31
- test('returns command token when valid', () => {
32
- expect(parseArgs(['start']).command).toBe('start');
33
- expect(parseArgs(['stop']).command).toBe('stop');
34
- expect(parseArgs(['restart']).command).toBe('restart');
35
- });
36
-
37
- test('recognizes --no-open flag', () => {
38
- const r = parseArgs(['start', '--no-open']);
39
-
40
- expect(r.flags.includes('no-open')).toBe(true);
41
- });
42
- });
43
-
44
- describe('main', () => {
45
- test('prints usage and exits 0 on --help', async () => {
46
- const code = await main(['--help']);
47
-
48
- expect(code).toBe(0);
49
- expect(write_mock).toHaveBeenCalled();
50
- });
51
-
52
- test('prints usage and exits 1 on no command', async () => {
53
- const code = await main([]);
54
-
55
- expect(code).toBe(1);
56
- expect(write_mock).toHaveBeenCalled();
57
- });
58
-
59
- test('dispatches to start handler', async () => {
60
- const code = await main(['start']);
61
-
62
- expect(code).toBe(0);
63
- expect(commands.handleStart).toHaveBeenCalledTimes(1);
64
- });
65
-
66
- test('propagates --no-open to start handler', async () => {
67
- await main(['start', '--no-open']);
68
-
69
- expect(commands.handleStart).toHaveBeenCalledWith({ no_open: true });
70
- });
71
-
72
- test('reads BDUI_NO_OPEN=1 to disable open', async () => {
73
- const prev = process.env.BDUI_NO_OPEN;
74
- try {
75
- process.env.BDUI_NO_OPEN = '1';
76
-
77
- await main(['start']);
78
-
79
- expect(commands.handleStart).toHaveBeenCalledWith({ no_open: true });
80
- } finally {
81
- if (prev === undefined) {
82
- delete process.env.BDUI_NO_OPEN;
83
- } else {
84
- process.env.BDUI_NO_OPEN = prev;
85
- }
86
- }
87
- });
88
-
89
- test('dispatches to stop handler', async () => {
90
- const code = await main(['stop']);
91
-
92
- expect(code).toBe(0);
93
- expect(commands.handleStop).toHaveBeenCalledTimes(1);
94
- });
95
-
96
- test('dispatches to restart handler', async () => {
97
- const code = await main(['restart']);
98
-
99
- expect(code).toBe(0);
100
- expect(commands.handleRestart).toHaveBeenCalledTimes(1);
101
- });
102
-
103
- test('unknown command prints usage and exits 1', async () => {
104
- const code = await main(['unknown']);
105
-
106
- expect(code).toBe(1);
107
- expect(write_mock).toHaveBeenCalled();
108
- });
109
- });
@@ -1,155 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import {
5
- afterAll,
6
- afterEach,
7
- beforeAll,
8
- describe,
9
- expect,
10
- test,
11
- vi
12
- } from 'vitest';
13
- import { handleRestart, handleStart, handleStop } from './commands.js';
14
- import * as daemon from './daemon.js';
15
-
16
- // Mock browser open + readiness wait to avoid external effects and flakiness
17
- vi.mock('./open.js', () => ({
18
- openUrl: async () => true,
19
- waitForServer: async () => {}
20
- }));
21
-
22
- /** @type {string} */
23
- let tmp_runtime_dir;
24
- /** @type {Record<string, string | undefined>} */
25
- let prev_env;
26
-
27
- beforeAll(() => {
28
- // Snapshot selected env vars to restore later
29
- prev_env = {
30
- BDUI_RUNTIME_DIR: process.env.BDUI_RUNTIME_DIR,
31
- PORT: process.env.PORT,
32
- BDUI_NO_OPEN: process.env.BDUI_NO_OPEN
33
- };
34
-
35
- tmp_runtime_dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bdui-it-'));
36
- process.env.BDUI_RUNTIME_DIR = tmp_runtime_dir;
37
- // Use port 0 so OS assigns an ephemeral port; URL printing still occurs
38
- process.env.PORT = '0';
39
- // Ensure default start path would not attempt to open the browser if called via CLI
40
- process.env.BDUI_NO_OPEN = '1';
41
- });
42
-
43
- afterEach(async () => {
44
- // Ensure no stray daemon is left between tests
45
- const pid = daemon.readPidFile();
46
- if (pid && daemon.isProcessRunning(pid)) {
47
- await daemon.terminateProcess(pid, 2000);
48
- }
49
- daemon.removePidFile();
50
- // Clear the daemon log to keep noise down in CI
51
- try {
52
- fs.writeFileSync(daemon.getLogFilePath(), '', 'utf8');
53
- } catch {
54
- // ignore
55
- }
56
- });
57
-
58
- afterAll(() => {
59
- // Restore env
60
- if (prev_env.BDUI_RUNTIME_DIR === undefined) {
61
- delete process.env.BDUI_RUNTIME_DIR;
62
- } else {
63
- process.env.BDUI_RUNTIME_DIR = prev_env.BDUI_RUNTIME_DIR;
64
- }
65
-
66
- if (prev_env.PORT === undefined) {
67
- delete process.env.PORT;
68
- } else {
69
- process.env.PORT = prev_env.PORT;
70
- }
71
-
72
- if (prev_env.BDUI_NO_OPEN === undefined) {
73
- delete process.env.BDUI_NO_OPEN;
74
- } else {
75
- process.env.BDUI_NO_OPEN = prev_env.BDUI_NO_OPEN;
76
- }
77
-
78
- try {
79
- fs.rmSync(tmp_runtime_dir, { recursive: true, force: true });
80
- } catch {
81
- // ignore
82
- }
83
- });
84
-
85
- describe('commands integration', () => {
86
- test('start then stop returns 0 and manages PID file', async () => {
87
- // setup
88
- const print_spy = vi
89
- .spyOn(daemon, 'printServerUrl')
90
- .mockImplementation(() => {});
91
-
92
- // execution
93
- const start_code = await handleStart({ no_open: true });
94
-
95
- // assertion
96
- expect(start_code).toBe(0);
97
- const pid_after_start = daemon.readPidFile();
98
- expect(typeof pid_after_start).toBe('number');
99
- expect(Number(pid_after_start)).toBeGreaterThan(0);
100
-
101
- // execution
102
- const stop_code = await handleStop();
103
-
104
- // assertion
105
- expect(stop_code).toBe(0);
106
- const pid_after_stop = daemon.readPidFile();
107
- expect(pid_after_stop).toBeNull();
108
-
109
- print_spy.mockRestore();
110
- });
111
-
112
- test('stop returns 2 when not running', async () => {
113
- // execution
114
- const code = await handleStop();
115
-
116
- // assertion
117
- expect(code).toBe(2);
118
- });
119
-
120
- test('start is idempotent when already running', async () => {
121
- // setup
122
- await handleStart({ no_open: true });
123
- const start_spy = vi.spyOn(daemon, 'startDaemon');
124
-
125
- // execution
126
- const code = await handleStart({ no_open: true });
127
-
128
- // assertion
129
- expect(code).toBe(0);
130
- expect(start_spy).not.toHaveBeenCalled();
131
-
132
- // cleanup
133
- start_spy.mockRestore();
134
- await handleStop();
135
- });
136
-
137
- test('restart stops (when needed) and starts', async () => {
138
- // setup
139
- const print_spy = vi
140
- .spyOn(daemon, 'printServerUrl')
141
- .mockImplementation(() => {});
142
-
143
- // execution
144
- const code = await handleRestart();
145
-
146
- // assertion
147
- expect(code).toBe(0);
148
- const pid = daemon.readPidFile();
149
- expect(typeof pid).toBe('number');
150
-
151
- // cleanup
152
- await handleStop();
153
- print_spy.mockRestore();
154
- });
155
- });
@@ -1,94 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { handleStart, handleStop } from './commands.js';
3
- import * as daemon from './daemon.js';
4
-
5
- describe('handleStart (unit)', () => {
6
- test('returns 1 when daemon start fails', async () => {
7
- const read_pid = vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
8
- const is_running = vi
9
- .spyOn(daemon, 'isProcessRunning')
10
- .mockReturnValue(false);
11
- const start = vi.spyOn(daemon, 'startDaemon').mockReturnValue(null);
12
-
13
- const code = await handleStart({ no_open: true });
14
-
15
- expect(code).toBe(1);
16
-
17
- read_pid.mockRestore();
18
- is_running.mockRestore();
19
- start.mockRestore();
20
- });
21
-
22
- test('returns 0 when already running', async () => {
23
- const read_pid = vi.spyOn(daemon, 'readPidFile').mockReturnValue(12345);
24
- const is_running = vi
25
- .spyOn(daemon, 'isProcessRunning')
26
- .mockReturnValue(true);
27
- const print_url = vi
28
- .spyOn(daemon, 'printServerUrl')
29
- .mockImplementation(() => {});
30
-
31
- const code = await handleStart({ no_open: true });
32
-
33
- expect(code).toBe(0);
34
- expect(print_url).toHaveBeenCalledTimes(1);
35
-
36
- read_pid.mockRestore();
37
- is_running.mockRestore();
38
- print_url.mockRestore();
39
- });
40
- });
41
-
42
- describe('handleStop (unit)', () => {
43
- test('returns 2 when not running and no PID file', async () => {
44
- const read_pid = vi.spyOn(daemon, 'readPidFile').mockReturnValue(null);
45
-
46
- const code = await handleStop();
47
-
48
- expect(code).toBe(2);
49
-
50
- read_pid.mockRestore();
51
- });
52
-
53
- test('returns 2 on stale PID and removes file', async () => {
54
- const read_pid = vi.spyOn(daemon, 'readPidFile').mockReturnValue(1111);
55
- const is_running = vi
56
- .spyOn(daemon, 'isProcessRunning')
57
- .mockReturnValue(false);
58
- const remove_pid = vi
59
- .spyOn(daemon, 'removePidFile')
60
- .mockImplementation(() => {});
61
-
62
- const code = await handleStop();
63
-
64
- expect(code).toBe(2);
65
- expect(remove_pid).toHaveBeenCalledTimes(1);
66
-
67
- read_pid.mockRestore();
68
- is_running.mockRestore();
69
- remove_pid.mockRestore();
70
- });
71
-
72
- test('returns 0 when process terminates and removes PID', async () => {
73
- const read_pid = vi.spyOn(daemon, 'readPidFile').mockReturnValue(2222);
74
- const is_running = vi
75
- .spyOn(daemon, 'isProcessRunning')
76
- .mockReturnValue(true);
77
- const terminate = vi
78
- .spyOn(daemon, 'terminateProcess')
79
- .mockResolvedValue(true);
80
- const remove_pid = vi
81
- .spyOn(daemon, 'removePidFile')
82
- .mockImplementation(() => {});
83
-
84
- const code = await handleStop();
85
-
86
- expect(code).toBe(0);
87
- expect(remove_pid).toHaveBeenCalledTimes(1);
88
-
89
- read_pid.mockRestore();
90
- is_running.mockRestore();
91
- terminate.mockRestore();
92
- remove_pid.mockRestore();
93
- });
94
- });