beads-ui 0.1.0 → 0.1.2

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 (57) hide show
  1. package/CHANGES.md +8 -0
  2. package/README.md +7 -3
  3. package/package.json +12 -2
  4. package/.beads/issues.jsonl +0 -107
  5. package/.editorconfig +0 -10
  6. package/.eslintrc.json +0 -36
  7. package/.github/workflows/ci.yml +0 -38
  8. package/.prettierignore +0 -5
  9. package/AGENTS.md +0 -85
  10. package/app/data/providers.test.js +0 -126
  11. package/app/main.board-switch.test.js +0 -94
  12. package/app/main.deep-link.test.js +0 -64
  13. package/app/main.live-updates.test.js +0 -229
  14. package/app/main.test.js +0 -17
  15. package/app/main.theme.test.js +0 -41
  16. package/app/main.view-sync.test.js +0 -54
  17. package/app/protocol.test.js +0 -57
  18. package/app/router.test.js +0 -34
  19. package/app/state.test.js +0 -21
  20. package/app/utils/markdown.test.js +0 -103
  21. package/app/utils/type-badge.test.js +0 -30
  22. package/app/views/board.test.js +0 -184
  23. package/app/views/detail.acceptance-notes.test.js +0 -67
  24. package/app/views/detail.assignee.test.js +0 -161
  25. package/app/views/detail.deps.test.js +0 -97
  26. package/app/views/detail.edits.test.js +0 -146
  27. package/app/views/detail.labels.test.js +0 -73
  28. package/app/views/detail.priority.test.js +0 -86
  29. package/app/views/detail.test.js +0 -188
  30. package/app/views/detail.ui47.test.js +0 -78
  31. package/app/views/epics.test.js +0 -283
  32. package/app/views/list.inline-edits.test.js +0 -84
  33. package/app/views/list.test.js +0 -479
  34. package/app/views/nav.test.js +0 -43
  35. package/app/ws.test.js +0 -168
  36. package/eslint.config.js +0 -59
  37. package/media/bdui-board.png +0 -0
  38. package/media/bdui-epics.png +0 -0
  39. package/media/bdui-issues.png +0 -0
  40. package/prettier.config.js +0 -13
  41. package/server/app.test.js +0 -29
  42. package/server/bd.test.js +0 -93
  43. package/server/cli/cli.test.js +0 -109
  44. package/server/cli/commands.integration.test.js +0 -155
  45. package/server/cli/commands.unit.test.js +0 -94
  46. package/server/cli/open.test.js +0 -26
  47. package/server/db.test.js +0 -70
  48. package/server/protocol.test.js +0 -87
  49. package/server/watcher.test.js +0 -100
  50. package/server/ws.handlers.test.js +0 -174
  51. package/server/ws.labels.test.js +0 -95
  52. package/server/ws.mutations.test.js +0 -261
  53. package/server/ws.subscriptions.test.js +0 -116
  54. package/server/ws.test.js +0 -52
  55. package/test/setup-vitest.js +0 -12
  56. package/tsconfig.json +0 -23
  57. package/vitest.config.mjs +0 -14
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
- });
@@ -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
- });