@mytechtoday/augment-sdd 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/out/beads/BeadsAdapter.js +30 -0
  2. package/out/beads/BeadsAdapter.js.map +1 -0
  3. package/out/beads/ClassicBeadsAdapter.js +84 -0
  4. package/out/beads/ClassicBeadsAdapter.js.map +1 -0
  5. package/out/beads/PowerShellBeadsAdapter.js +118 -0
  6. package/out/beads/PowerShellBeadsAdapter.js.map +1 -0
  7. package/out/beads/getBeadsAdapter.js +94 -0
  8. package/out/beads/getBeadsAdapter.js.map +1 -0
  9. package/out/commands/executeBeadsBatch.js +29 -15
  10. package/out/commands/executeBeadsBatch.js.map +1 -1
  11. package/out/commands/generateBeads.js +25 -22
  12. package/out/commands/generateBeads.js.map +1 -1
  13. package/out/commands/initWizard.js +184 -0
  14. package/out/commands/initWizard.js.map +1 -0
  15. package/out/extension.js +10 -0
  16. package/out/extension.js.map +1 -1
  17. package/out/test/integration/executeBeadsBatch.test.js +83 -57
  18. package/out/test/integration/executeBeadsBatch.test.js.map +1 -1
  19. package/out/test/integration/generateBeads.test.js +156 -0
  20. package/out/test/integration/generateBeads.test.js.map +1 -0
  21. package/out/test/integration/generateOpenSpec.test.js +16 -39
  22. package/out/test/integration/generateOpenSpec.test.js.map +1 -1
  23. package/out/test/unit/ClassicBeadsAdapter.test.js +121 -0
  24. package/out/test/unit/ClassicBeadsAdapter.test.js.map +1 -0
  25. package/out/test/unit/PowerShellBeadsAdapter.test.js +120 -0
  26. package/out/test/unit/PowerShellBeadsAdapter.test.js.map +1 -0
  27. package/out/test/unit/getBeadsAdapter.test.js +97 -0
  28. package/out/test/unit/getBeadsAdapter.test.js.map +1 -0
  29. package/out/test/unit/initWizard.test.js +170 -0
  30. package/out/test/unit/initWizard.test.js.map +1 -0
  31. package/out/test/unit/runCli.test.js +11 -24
  32. package/out/test/unit/runCli.test.js.map +1 -1
  33. package/out/utils/detectCli.js +47 -1
  34. package/out/utils/detectCli.js.map +1 -1
  35. package/out/utils/runCli.js +9 -3
  36. package/out/utils/runCli.js.map +1 -1
  37. package/package.json +22 -1
  38. package/.eslintrc.json +0 -20
  39. package/src/commands/executeBeadsBatch.ts +0 -153
  40. package/src/commands/fullPipeline.ts +0 -120
  41. package/src/commands/generateBeads.ts +0 -127
  42. package/src/commands/generateOpenSpec.ts +0 -227
  43. package/src/dashboard/DashboardPanel.ts +0 -168
  44. package/src/extension.ts +0 -77
  45. package/src/parsers/parseBeadUpdates.ts +0 -26
  46. package/src/parsers/parseTasksMarkdown.ts +0 -61
  47. package/src/test/integration/executeBeadsBatch.test.ts +0 -129
  48. package/src/test/integration/generateOpenSpec.test.ts +0 -129
  49. package/src/test/runTest.ts +0 -15
  50. package/src/test/suite/index.ts +0 -37
  51. package/src/test/unit/parseBeadUpdates.test.ts +0 -48
  52. package/src/test/unit/parseTasksMarkdown.test.ts +0 -41
  53. package/src/test/unit/runCli.test.ts +0 -109
  54. package/src/utils/detectCli.ts +0 -28
  55. package/src/utils/getConfig.ts +0 -25
  56. package/src/utils/logger.ts +0 -42
  57. package/src/utils/runCli.ts +0 -102
  58. package/tsconfig.json +0 -18
package/src/extension.ts DELETED
@@ -1,77 +0,0 @@
1
- import * as vscode from 'vscode';
2
- import { initLogger, log, revealOutputChannel } from './utils/logger';
3
- import { detectCli } from './utils/detectCli';
4
- import { generateOpenSpec } from './commands/generateOpenSpec';
5
- import { generateBeads } from './commands/generateBeads';
6
- import { executeBeadsBatch } from './commands/executeBeadsBatch';
7
- import { fullPipeline } from './commands/fullPipeline';
8
-
9
- /** CLI definitions: name on PATH and the npm package to install if missing. */
10
- const REQUIRED_CLIS: { name: string; pkg: string }[] = [
11
- { name: 'auggie', pkg: 'auggie' },
12
- { name: 'openspec', pkg: 'openspec' },
13
- { name: 'bd', pkg: '@beads/cli' },
14
- ];
15
-
16
- /**
17
- * Run CLI presence checks on activation. For each missing CLI, show an error
18
- * message with the exact `npm install -g <pkg>` command and a "View Logs" button.
19
- */
20
- async function checkRequiredClis(): Promise<void> {
21
- for (const { name, pkg } of REQUIRED_CLIS) {
22
- const found = await detectCli(name);
23
- if (!found) {
24
- log(`CLI not found on PATH: ${name} (install with: npm install -g ${pkg})`);
25
- const action = await vscode.window.showErrorMessage(
26
- `Augment SDD: Required CLI "${name}" not found. Install it with: npm install -g ${pkg}`,
27
- 'View Logs'
28
- );
29
- if (action === 'View Logs') {
30
- revealOutputChannel();
31
- }
32
- } else {
33
- log(`CLI detected: ${name}`);
34
- }
35
- }
36
- }
37
-
38
- export function activate(context: vscode.ExtensionContext): void {
39
- // Create the Output Channel first and initialise the shared logger so all
40
- // subsequent code (including commands) can call log() / revealOutputChannel().
41
- const outputChannel = vscode.window.createOutputChannel('Augment SDD');
42
- initLogger(outputChannel);
43
- context.subscriptions.push(outputChannel);
44
-
45
- log('Augment SDD extension activating…');
46
-
47
- // Run CLI detection (non-blocking — failures are surfaced as error messages).
48
- checkRequiredClis().catch(err => {
49
- log(`CLI detection error: ${String(err)}`);
50
- });
51
-
52
- // Register commands (implementations wired in subsequent beads).
53
- context.subscriptions.push(
54
- vscode.commands.registerCommand('augmentSdd.generateOpenSpec', async () => {
55
- await generateOpenSpec();
56
- }),
57
-
58
- vscode.commands.registerCommand('augmentSdd.generateBeads', async () => {
59
- await generateBeads();
60
- }),
61
-
62
- vscode.commands.registerCommand('augmentSdd.executeBeadsBatch', async () => {
63
- await executeBeadsBatch();
64
- }),
65
-
66
- vscode.commands.registerCommand('augmentSdd.fullPipeline', async () => {
67
- await fullPipeline();
68
- })
69
- );
70
-
71
- log('Augment SDD extension activated.');
72
- }
73
-
74
- export function deactivate(): void {
75
- // Nothing to clean up; subscriptions are disposed automatically.
76
- }
77
-
@@ -1,26 +0,0 @@
1
- /**
2
- * parseBeadUpdates — Sentinel line parser for Bead 7.
3
- *
4
- * Extracts all lines that match the Auggie completion sentinel format
5
- * from a free-form text response. Each matched line can be executed
6
- * directly as a CLI command via runCli.
7
- *
8
- * Design D4: Sentinel-based parsing is format-agnostic — it works
9
- * regardless of how much prose Auggie wraps around the commands.
10
- */
11
-
12
- /**
13
- * Extract all `bd update <ID> --status done` lines from Auggie output.
14
- *
15
- * @param output Raw text returned by `auggie --print --quiet`.
16
- * @returns Array of matched command strings (may be empty).
17
- *
18
- * @example
19
- * parseBeadUpdates('done\nbd update bd-abc --status done\nmore text')
20
- * // => ['bd update bd-abc --status done']
21
- */
22
- export function parseBeadUpdates(output: string): string[] {
23
- const matches = [...output.matchAll(/^bd update \S+ --status done$/gm)];
24
- return matches.map(m => m[0]);
25
- }
26
-
@@ -1,61 +0,0 @@
1
- /**
2
- * parseTasksMarkdown — heading-based task extractor for Auggie-generated tasks.md.
3
- *
4
- * Design (D3): Uses a deterministic heading-based regex (`/^#{1,3}\s+(.+)/gm`)
5
- * to extract task titles and their body text. No external parser dependency is
6
- * required; Auggie's structured output is regular enough for this approach.
7
- *
8
- * Each H1–H3 heading becomes a Bead title; the body text between that heading
9
- * and the next heading (or EOF) becomes the Bead description.
10
- */
11
-
12
- /** A single extracted task ready to be created as a Bead. */
13
- export interface BeadTask {
14
- /** The heading text, trimmed (becomes the `bd create` title). */
15
- title: string;
16
- /** All text between this heading and the next, trimmed (becomes the description). */
17
- description: string;
18
- }
19
-
20
- /**
21
- * Parse `content` (the raw text of a `tasks.md` file) into an ordered array of
22
- * `{ title, description }` objects.
23
- *
24
- * - Recognises H1, H2, and H3 headings (`#`, `##`, `###`).
25
- * - The heading line itself is the title; everything after it up to the next
26
- * heading (or end-of-file) is the description (whitespace-trimmed).
27
- * - Returns an empty array when no headings are found.
28
- *
29
- * @param content Raw markdown text of a `tasks.md` file.
30
- */
31
- export function parseTasksMarkdown(content: string): BeadTask[] {
32
- const headingRegex = /^#{1,3}\s+(.+)/gm;
33
- const tasks: BeadTask[] = [];
34
-
35
- // Collect all heading positions and titles in one pass.
36
- const positions: Array<{ index: number; title: string }> = [];
37
- let match: RegExpExecArray | null;
38
-
39
- while ((match = headingRegex.exec(content)) !== null) {
40
- positions.push({ index: match.index, title: match[1].trim() });
41
- }
42
-
43
- // For each heading, extract the body as everything between its start and the
44
- // start of the next heading (or end-of-file).
45
- for (let i = 0; i < positions.length; i++) {
46
- const { index, title } = positions[i];
47
- const nextIndex = i + 1 < positions.length ? positions[i + 1].index : content.length;
48
-
49
- // Slice the segment, skip the heading line itself, then trim whitespace.
50
- const segment = content.slice(index, nextIndex);
51
- const firstNewline = segment.indexOf('\n');
52
- const description = firstNewline >= 0
53
- ? segment.slice(firstNewline + 1).trim()
54
- : '';
55
-
56
- tasks.push({ title, description });
57
- }
58
-
59
- return tasks;
60
- }
61
-
@@ -1,129 +0,0 @@
1
- /**
2
- * Integration tests for executeBeadsBatch command.
3
- * IT-3: 3 beads ready — all 3 bd update commands executed successfully.
4
- * IT-4: Partial failure — second bd update fails; others succeed, warning shown.
5
- *
6
- * Strategy: stub cp.spawn via sinon so no real CLIs are invoked.
7
- */
8
- import * as assert from 'assert';
9
- import * as cp from 'child_process';
10
- import * as fs from 'fs';
11
- import * as os from 'os';
12
- import * as path from 'path';
13
- import * as sinon from 'sinon';
14
- import * as vscode from 'vscode';
15
- import { EventEmitter } from 'events';
16
- import { executeBeadsBatch } from '../../commands/executeBeadsBatch';
17
-
18
- // ---------------------------------------------------------------------------
19
- // Fake process factory
20
- // ---------------------------------------------------------------------------
21
- function fakeProc(opts: { stdout?: string; stderr?: string; code?: number }): cp.ChildProcess {
22
- const stdoutEE = new EventEmitter();
23
- const stderrEE = new EventEmitter();
24
- const procEE = new EventEmitter();
25
- const proc = {
26
- stdout: stdoutEE,
27
- stderr: stderrEE,
28
- kill: () => { /* noop */ },
29
- on: procEE.on.bind(procEE),
30
- once: procEE.once.bind(procEE),
31
- removeListener: procEE.removeListener.bind(procEE),
32
- emit: procEE.emit.bind(procEE),
33
- };
34
- setImmediate(() => {
35
- if (opts.stdout) { stdoutEE.emit('data', Buffer.from(opts.stdout)); }
36
- if (opts.stderr) { stderrEE.emit('data', Buffer.from(opts.stderr)); }
37
- procEE.emit('close', opts.code ?? 0);
38
- });
39
- return proc as unknown as cp.ChildProcess;
40
- }
41
-
42
- // ---------------------------------------------------------------------------
43
- // Helpers
44
- // ---------------------------------------------------------------------------
45
- const FAKE_BEADS = JSON.stringify([
46
- { id: 'bd-a1', title: 'Bead A1', description: 'Do A1' },
47
- { id: 'bd-b2', title: 'Bead B2', description: 'Do B2' },
48
- { id: 'bd-b3', title: 'Bead B3', description: 'Do B3' },
49
- ]);
50
-
51
- const AUGGIE_SENTINELS =
52
- 'bd update bd-a1 --status done\n' +
53
- 'bd update bd-b2 --status done\n' +
54
- 'bd update bd-b3 --status done';
55
-
56
- function stubWorkspace(dir: string): () => void {
57
- const orig = Object.getOwnPropertyDescriptor(vscode.workspace, 'workspaceFolders');
58
- Object.defineProperty(vscode.workspace, 'workspaceFolders', {
59
- configurable: true,
60
- get: () => [{ uri: vscode.Uri.file(dir), name: 'test', index: 0 }],
61
- });
62
- return () => {
63
- if (orig) { Object.defineProperty(vscode.workspace, 'workspaceFolders', orig); }
64
- };
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // Tests
69
- // ---------------------------------------------------------------------------
70
- suite('executeBeadsBatch — integration tests', () => {
71
- let tmpDir: string;
72
- let restoreWF: () => void;
73
- let spawnStub: sinon.SinonStub;
74
- let infoStub: sinon.SinonStub;
75
- let warnStub: sinon.SinonStub;
76
- let errStub: sinon.SinonStub;
77
-
78
- setup(() => {
79
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'augsdd-batch-'));
80
- restoreWF = stubWorkspace(tmpDir);
81
- spawnStub = sinon.stub(cp, 'spawn') as sinon.SinonStub;
82
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
- infoStub = sinon.stub(vscode.window, 'showInformationMessage' as any).resolves(undefined);
84
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
- warnStub = sinon.stub(vscode.window, 'showWarningMessage' as any).resolves(undefined);
86
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
- errStub = sinon.stub(vscode.window, 'showErrorMessage' as any).resolves(undefined);
88
- });
89
-
90
- teardown(() => {
91
- sinon.restore();
92
- restoreWF();
93
- fs.rmSync(tmpDir, { recursive: true, force: true });
94
- });
95
-
96
- test('IT-3: 3 beads ready — all 3 bd update commands executed', async () => {
97
- // Call order: bd ready, auggie, bd update ×3
98
- spawnStub.onCall(0).returns(fakeProc({ stdout: FAKE_BEADS, code: 0 }));
99
- spawnStub.onCall(1).returns(fakeProc({ stdout: AUGGIE_SENTINELS, code: 0 }));
100
- spawnStub.onCall(2).returns(fakeProc({ stdout: '', code: 0 }));
101
- spawnStub.onCall(3).returns(fakeProc({ stdout: '', code: 0 }));
102
- spawnStub.onCall(4).returns(fakeProc({ stdout: '', code: 0 }));
103
-
104
- await executeBeadsBatch();
105
-
106
- assert.strictEqual(spawnStub.callCount, 5, 'spawn should be called 5 times total');
107
- assert.ok(infoStub.called, 'success info message should be shown');
108
- assert.ok(!warnStub.called, 'no warning should be shown on clean success');
109
- assert.ok(!errStub.called, 'no error should be shown on clean success');
110
- });
111
-
112
- test('IT-4: partial failure — second bd update fails, warning shown', async () => {
113
- // bd ready → auggie → bd-a1 ok → bd-b2 FAIL → bd-b3 ok
114
- spawnStub.onCall(0).returns(fakeProc({ stdout: FAKE_BEADS, code: 0 }));
115
- spawnStub.onCall(1).returns(fakeProc({ stdout: AUGGIE_SENTINELS, code: 0 }));
116
- spawnStub.onCall(2).returns(fakeProc({ stdout: '', code: 0 }));
117
- spawnStub.onCall(3).returns(fakeProc({ stdout: '', stderr: 'not found', code: 1 }));
118
- spawnStub.onCall(4).returns(fakeProc({ stdout: '', code: 0 }));
119
-
120
- await executeBeadsBatch();
121
-
122
- assert.ok(warnStub.called, 'warning message should be shown on partial failure');
123
- assert.ok(!errStub.called, 'no hard error should be shown on partial failure');
124
- // Verify all 5 spawns happened (pipeline did not abort)
125
- assert.strictEqual(spawnStub.callCount, 5, 'all 3 bd update attempts should be made');
126
- });
127
-
128
- });
129
-
@@ -1,129 +0,0 @@
1
- /**
2
- * Integration tests for generateOpenSpec command.
3
- * IT-1: Happy path — proposal.md written, validate passes.
4
- * IT-2: Auggie returns empty output on both attempts → showErrorMessage called.
5
- *
6
- * Strategy: stub cp.spawn via sinon so no real CLIs are invoked.
7
- * A temp directory is created on each test and wired as the workspace root.
8
- */
9
- import * as assert from 'assert';
10
- import * as cp from 'child_process';
11
- import * as fs from 'fs';
12
- import * as os from 'os';
13
- import * as path from 'path';
14
- import * as sinon from 'sinon';
15
- import * as vscode from 'vscode';
16
- import { EventEmitter } from 'events';
17
- import { generateOpenSpec } from '../../commands/generateOpenSpec';
18
-
19
- // ---------------------------------------------------------------------------
20
- // Fake process factory (mirrors the one in runCli.test.ts)
21
- // ---------------------------------------------------------------------------
22
- function fakeProc(opts: { stdout?: string; code?: number }): cp.ChildProcess {
23
- const stdoutEE = new EventEmitter();
24
- const procEE = new EventEmitter();
25
- const proc = {
26
- stdout: stdoutEE,
27
- stderr: new EventEmitter(),
28
- kill: () => { /* noop */ },
29
- on: procEE.on.bind(procEE),
30
- once: procEE.once.bind(procEE),
31
- removeListener: procEE.removeListener.bind(procEE),
32
- emit: procEE.emit.bind(procEE),
33
- };
34
- setImmediate(() => {
35
- if (opts.stdout) { stdoutEE.emit('data', Buffer.from(opts.stdout)); }
36
- procEE.emit('close', opts.code ?? 0);
37
- });
38
- return proc as unknown as cp.ChildProcess;
39
- }
40
-
41
- // ---------------------------------------------------------------------------
42
- // Helpers
43
- // ---------------------------------------------------------------------------
44
- function makeTempWorkspace(): string {
45
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'augsdd-it-'));
46
- // .jira folder with a single markdown ticket for auto-detection
47
- const jiraDir = path.join(dir, '.jira');
48
- fs.mkdirSync(jiraDir);
49
- fs.writeFileSync(path.join(jiraDir, 'TICKET-1.md'), '# Ticket\nDetails');
50
- // openspec/changes/<id> must exist so findNewestChangeDir works
51
- const changeDir = path.join(dir, 'openspec', 'changes', 'test-change');
52
- fs.mkdirSync(changeDir, { recursive: true });
53
- return dir;
54
- }
55
-
56
- function stubWorkspace(dir: string): () => void {
57
- const orig = Object.getOwnPropertyDescriptor(vscode.workspace, 'workspaceFolders');
58
- Object.defineProperty(vscode.workspace, 'workspaceFolders', {
59
- configurable: true,
60
- get: () => [{ uri: vscode.Uri.file(dir), name: 'test', index: 0 }],
61
- });
62
- return () => {
63
- if (orig) { Object.defineProperty(vscode.workspace, 'workspaceFolders', orig); }
64
- };
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // Tests
69
- // ---------------------------------------------------------------------------
70
- suite('generateOpenSpec — integration tests', () => {
71
- let tmpDir = '';
72
- let restoreWF: () => void;
73
- let spawnStub: sinon.SinonStub;
74
- let infoStub: sinon.SinonStub;
75
- let errStub: sinon.SinonStub;
76
-
77
- setup(() => {
78
- tmpDir = makeTempWorkspace();
79
- restoreWF = stubWorkspace(tmpDir);
80
- spawnStub = sinon.stub(cp, 'spawn') as sinon.SinonStub;
81
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
- infoStub = sinon.stub(vscode.window, 'showInformationMessage' as any).resolves(undefined);
83
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
- errStub = sinon.stub(vscode.window, 'showErrorMessage' as any).resolves(undefined);
85
- });
86
-
87
- teardown(() => {
88
- sinon.restore();
89
- restoreWF();
90
- fs.rmSync(tmpDir, { recursive: true, force: true });
91
- });
92
-
93
- test('IT-1: happy path — proposal.md written, validate exits 0', async () => {
94
- // Spawn sequence: init → instructions → auggie → validate
95
- spawnStub.onCall(0).returns(fakeProc({ stdout: '', code: 0 }));
96
- spawnStub.onCall(1).returns(fakeProc({ stdout: '{"template":"Write a proposal."}', code: 0 }));
97
- spawnStub.onCall(2).returns(fakeProc({ stdout: '# Proposal\nContent here.', code: 0 }));
98
- spawnStub.onCall(3).returns(fakeProc({ stdout: 'OK', code: 0 }));
99
-
100
- await generateOpenSpec();
101
-
102
- // proposal.md must have been written
103
- const proposalPath = path.join(tmpDir, 'openspec', 'changes', 'test-change', 'proposal.md');
104
- assert.ok(fs.existsSync(proposalPath), 'proposal.md should exist');
105
- const content = fs.readFileSync(proposalPath, 'utf8');
106
- assert.ok(content.includes('Proposal'), 'proposal.md should contain Auggie output');
107
-
108
- // Success info message shown, no error
109
- assert.ok(infoStub.called, 'showInformationMessage should be called on success');
110
- assert.ok(!errStub.called, 'showErrorMessage should NOT be called on success');
111
- });
112
-
113
- test('IT-2: Auggie returns empty on both attempts → showErrorMessage called', async () => {
114
- // init → instructions → auggie (empty) → auggie retry (empty)
115
- spawnStub.onCall(0).returns(fakeProc({ stdout: '', code: 0 }));
116
- spawnStub.onCall(1).returns(fakeProc({ stdout: '{"template":"template"}', code: 0 }));
117
- spawnStub.onCall(2).returns(fakeProc({ stdout: '', code: 0 }));
118
- spawnStub.onCall(3).returns(fakeProc({ stdout: '', code: 0 }));
119
-
120
- await generateOpenSpec();
121
-
122
- // Error message must be shown; no proposal file written
123
- assert.ok(errStub.called, 'showErrorMessage should be called after two empty Auggie attempts');
124
- const proposalPath = path.join(tmpDir, 'openspec', 'changes', 'test-change', 'proposal.md');
125
- assert.ok(!fs.existsSync(proposalPath), 'proposal.md should NOT be written on failure');
126
- });
127
-
128
- });
129
-
@@ -1,15 +0,0 @@
1
- import * as path from 'path';
2
- import { runTests } from '@vscode/test-electron';
3
-
4
- async function main(): Promise<void> {
5
- const extensionDevelopmentPath = path.resolve(__dirname, '../../');
6
- const extensionTestsPath = path.resolve(__dirname, './suite/index');
7
-
8
- await runTests({ extensionDevelopmentPath, extensionTestsPath });
9
- }
10
-
11
- main().catch(err => {
12
- console.error('Failed to run tests:', err);
13
- process.exit(1);
14
- });
15
-
@@ -1,37 +0,0 @@
1
- import * as path from 'path';
2
- import * as fs from 'fs';
3
- import Mocha from 'mocha';
4
-
5
- /** Recursively collect all *.test.js files under `dir`. */
6
- function findTestFiles(dir: string): string[] {
7
- const results: string[] = [];
8
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
9
- const full = path.join(dir, entry.name);
10
- if (entry.isDirectory()) {
11
- results.push(...findTestFiles(full));
12
- } else if (entry.isFile() && entry.name.endsWith('.test.js')) {
13
- results.push(full);
14
- }
15
- }
16
- return results;
17
- }
18
-
19
- export function run(): Promise<void> {
20
- const mocha = new Mocha({ ui: 'tdd', color: true, timeout: 10_000 });
21
-
22
- const testsRoot = path.resolve(__dirname, '..');
23
- for (const f of findTestFiles(testsRoot)) {
24
- mocha.addFile(f);
25
- }
26
-
27
- return new Promise<void>((resolve, reject) => {
28
- mocha.run(failures => {
29
- if (failures > 0) {
30
- reject(new Error(`${failures} test(s) failed.`));
31
- } else {
32
- resolve();
33
- }
34
- });
35
- });
36
- }
37
-
@@ -1,48 +0,0 @@
1
- /**
2
- * Unit tests for parseBeadUpdates.ts
3
- * UT-5: parses valid sentinel lines from Auggie output
4
- * UT-6: returns empty array when no sentinel lines found
5
- */
6
- import * as assert from 'assert';
7
- import { parseBeadUpdates } from '../../parsers/parseBeadUpdates';
8
-
9
- suite('parseBeadUpdates — unit tests', () => {
10
-
11
- test('UT-5: parses bd update lines from Auggie output', () => {
12
- const output =
13
- 'some text\n' +
14
- 'bd update bd-a1 --status done\n' +
15
- 'more text\n' +
16
- 'bd update bd-b2 --status done';
17
-
18
- const lines = parseBeadUpdates(output);
19
- assert.deepStrictEqual(lines, [
20
- 'bd update bd-a1 --status done',
21
- 'bd update bd-b2 --status done',
22
- ]);
23
- });
24
-
25
- test('UT-6: returns empty array when no sentinel lines found', () => {
26
- const lines = parseBeadUpdates('Auggie did not finish');
27
- assert.deepStrictEqual(lines, []);
28
- });
29
-
30
- test('ignores lines that are partial / malformed sentinels', () => {
31
- const output =
32
- 'bd update --status done\n' + // missing id
33
- ' bd update bd-x1 --status done\n' + // leading spaces (not at start)
34
- 'bd update bd-x2 --status pending\n' + // wrong status
35
- 'bd update bd-x3 --status done'; // valid
36
-
37
- const lines = parseBeadUpdates(output);
38
- assert.deepStrictEqual(lines, ['bd update bd-x3 --status done']);
39
- });
40
-
41
- test('handles CRLF line endings', () => {
42
- const output = 'preamble\r\nbd update bd-crlf --status done\r\npostamble';
43
- const lines = parseBeadUpdates(output);
44
- assert.deepStrictEqual(lines, ['bd update bd-crlf --status done']);
45
- });
46
-
47
- });
48
-
@@ -1,41 +0,0 @@
1
- /**
2
- * Unit tests for parseTasksMarkdown.ts
3
- * UT-4: Headings extracted as task titles with descriptions
4
- */
5
- import * as assert from 'assert';
6
- import { parseTasksMarkdown, BeadTask } from '../../parsers/parseTasksMarkdown';
7
-
8
- suite('parseTasksMarkdown — unit tests', () => {
9
-
10
- test('UT-4: extracts headings as task titles', () => {
11
- const md = '## Task 1\nDo this\n## Task 2\nDo that';
12
- const tasks: BeadTask[] = parseTasksMarkdown(md);
13
- assert.deepStrictEqual(tasks.map(t => t.title), ['Task 1', 'Task 2']);
14
- });
15
-
16
- test('extracts body text as task description', () => {
17
- const md = '## Alpha\nFirst line\nSecond line\n## Beta\nOther';
18
- const tasks = parseTasksMarkdown(md);
19
- assert.strictEqual(tasks[0].description, 'First line\nSecond line');
20
- assert.strictEqual(tasks[1].description, 'Other');
21
- });
22
-
23
- test('returns empty array when no headings are found', () => {
24
- const tasks = parseTasksMarkdown('No headings here.');
25
- assert.deepStrictEqual(tasks, []);
26
- });
27
-
28
- test('handles H1, H2, and H3 headings', () => {
29
- const md = '# One\na\n## Two\nb\n### Three\nc';
30
- const tasks = parseTasksMarkdown(md);
31
- assert.deepStrictEqual(tasks.map(t => t.title), ['One', 'Two', 'Three']);
32
- });
33
-
34
- test('trims extra whitespace from titles', () => {
35
- const md = '## Spaced Title \nBody';
36
- const tasks = parseTasksMarkdown(md);
37
- assert.strictEqual(tasks[0].title, 'Spaced Title');
38
- });
39
-
40
- });
41
-
@@ -1,109 +0,0 @@
1
- /**
2
- * Unit tests for runCli.ts
3
- * UT-1: resolves with trimmed stdout on exit code 0
4
- * UT-2: rejects on non-zero exit code
5
- * UT-3: rejects on timeout
6
- */
7
- import * as assert from 'assert';
8
- import * as cp from 'child_process';
9
- import * as sinon from 'sinon';
10
- import { EventEmitter } from 'events';
11
- import { runCli } from '../../utils/runCli';
12
-
13
- // ---------------------------------------------------------------------------
14
- // Fake ChildProcess factory
15
- // ---------------------------------------------------------------------------
16
-
17
- interface FakeProcOpts {
18
- stdout?: string;
19
- stderr?: string;
20
- /** Exit code to emit on 'close'. If undefined, defaults to 0. */
21
- code?: number | null;
22
- /**
23
- * If set, the process deliberately hangs for this many ms before emitting
24
- * 'close'. Use with a runCli timeoutMs smaller than hangMs to test timeout.
25
- */
26
- hangMs?: number;
27
- }
28
-
29
- function fakeProc(opts: FakeProcOpts): cp.ChildProcess {
30
- const stdoutEE = new EventEmitter();
31
- const stderrEE = new EventEmitter();
32
- const procEE = new EventEmitter();
33
-
34
- // Minimal object that satisfies what runCli actually accesses.
35
- const proc = {
36
- stdout: stdoutEE,
37
- stderr: stderrEE,
38
- kill: () => { /* noop – timeout test relies on this */ },
39
- on: procEE.on.bind(procEE),
40
- once: procEE.once.bind(procEE),
41
- removeListener: procEE.removeListener.bind(procEE),
42
- emit: procEE.emit.bind(procEE),
43
- };
44
-
45
- setImmediate(() => {
46
- if (opts.stdout) { stdoutEE.emit('data', Buffer.from(opts.stdout)); }
47
- if (opts.stderr) { stderrEE.emit('data', Buffer.from(opts.stderr)); }
48
-
49
- if (opts.hangMs === undefined) {
50
- procEE.emit('close', opts.code ?? 0);
51
- } else {
52
- // Deliberately hang so the runCli timer can fire first.
53
- setTimeout(() => procEE.emit('close', null), opts.hangMs);
54
- }
55
- });
56
-
57
- return proc as unknown as cp.ChildProcess;
58
- }
59
-
60
- // ---------------------------------------------------------------------------
61
- // Tests
62
- // ---------------------------------------------------------------------------
63
-
64
- suite('runCli — unit tests', () => {
65
-
66
- test('UT-1: resolves with trimmed stdout on exit code 0', async () => {
67
- const stub = sinon.stub(cp, 'spawn').returns(
68
- fakeProc({ stdout: 'hello\n', code: 0 })
69
- );
70
- try {
71
- const result = await runCli('echo', ['hello'], '/tmp');
72
- assert.strictEqual(result, 'hello');
73
- } finally {
74
- stub.restore();
75
- }
76
- });
77
-
78
- test('UT-2: rejects on non-zero exit code', async () => {
79
- const stub = sinon.stub(cp, 'spawn').returns(
80
- fakeProc({ stdout: '', stderr: 'err', code: 1 })
81
- );
82
- try {
83
- await assert.rejects(
84
- runCli('false', [], '/tmp'),
85
- /failed with exit code 1/
86
- );
87
- } finally {
88
- stub.restore();
89
- }
90
- });
91
-
92
- test('UT-3: rejects on timeout', async () => {
93
- // hangMs (5 000) >> timeoutMs (100) → timer fires first, process is killed,
94
- // promise rejects with a message containing "timed out".
95
- const stub = sinon.stub(cp, 'spawn').returns(
96
- fakeProc({ hangMs: 5_000, code: null })
97
- );
98
- try {
99
- await assert.rejects(
100
- runCli('sleep', ['5'], '/tmp', 100),
101
- /timed out/i
102
- );
103
- } finally {
104
- stub.restore();
105
- }
106
- });
107
-
108
- });
109
-