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.
- package/CHANGES.md +8 -0
- package/README.md +7 -3
- package/package.json +12 -2
- package/.beads/issues.jsonl +0 -107
- package/.editorconfig +0 -10
- package/.eslintrc.json +0 -36
- package/.github/workflows/ci.yml +0 -38
- package/.prettierignore +0 -5
- package/AGENTS.md +0 -85
- package/app/data/providers.test.js +0 -126
- package/app/main.board-switch.test.js +0 -94
- package/app/main.deep-link.test.js +0 -64
- package/app/main.live-updates.test.js +0 -229
- package/app/main.test.js +0 -17
- package/app/main.theme.test.js +0 -41
- package/app/main.view-sync.test.js +0 -54
- package/app/protocol.test.js +0 -57
- package/app/router.test.js +0 -34
- package/app/state.test.js +0 -21
- package/app/utils/markdown.test.js +0 -103
- package/app/utils/type-badge.test.js +0 -30
- package/app/views/board.test.js +0 -184
- package/app/views/detail.acceptance-notes.test.js +0 -67
- package/app/views/detail.assignee.test.js +0 -161
- package/app/views/detail.deps.test.js +0 -97
- package/app/views/detail.edits.test.js +0 -146
- package/app/views/detail.labels.test.js +0 -73
- package/app/views/detail.priority.test.js +0 -86
- package/app/views/detail.test.js +0 -188
- package/app/views/detail.ui47.test.js +0 -78
- package/app/views/epics.test.js +0 -283
- package/app/views/list.inline-edits.test.js +0 -84
- package/app/views/list.test.js +0 -479
- package/app/views/nav.test.js +0 -43
- package/app/ws.test.js +0 -168
- package/eslint.config.js +0 -59
- package/media/bdui-board.png +0 -0
- package/media/bdui-epics.png +0 -0
- package/media/bdui-issues.png +0 -0
- package/prettier.config.js +0 -13
- package/server/app.test.js +0 -29
- package/server/bd.test.js +0 -93
- package/server/cli/cli.test.js +0 -109
- package/server/cli/commands.integration.test.js +0 -155
- package/server/cli/commands.unit.test.js +0 -94
- package/server/cli/open.test.js +0 -26
- package/server/db.test.js +0 -70
- package/server/protocol.test.js +0 -87
- package/server/watcher.test.js +0 -100
- package/server/ws.handlers.test.js +0 -174
- package/server/ws.labels.test.js +0 -95
- package/server/ws.mutations.test.js +0 -261
- package/server/ws.subscriptions.test.js +0 -116
- package/server/ws.test.js +0 -52
- package/test/setup-vitest.js +0 -12
- package/tsconfig.json +0 -23
- 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
|
-
]);
|
package/media/bdui-board.png
DELETED
|
Binary file
|
package/media/bdui-epics.png
DELETED
|
Binary file
|
package/media/bdui-issues.png
DELETED
|
Binary file
|
package/prettier.config.js
DELETED
|
@@ -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
|
-
};
|
package/server/app.test.js
DELETED
|
@@ -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
|
-
});
|
package/server/cli/cli.test.js
DELETED
|
@@ -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
|
-
});
|
package/server/cli/open.test.js
DELETED
|
@@ -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
|
-
});
|