@hartvig/developer-control-center 0.8.6 → 0.8.8

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 (103) hide show
  1. package/.developer-control-center/metrics.json +1 -1
  2. package/.developer-control-center/status.json +1 -1
  3. package/.developer-control-center/timings.jsonl +25 -0
  4. package/.github/workflows/ci.yml +1 -7
  5. package/coverage/Developer Control Center/dcc.config.js.html +628 -0
  6. package/coverage/Developer Control Center/index.html +116 -0
  7. package/coverage/Developer Control Center/src/config/index.html +116 -0
  8. package/coverage/Developer Control Center/src/config/loader.ts.html +454 -0
  9. package/coverage/Developer Control Center/src/core/ci.ts.html +163 -0
  10. package/coverage/Developer Control Center/src/core/event-bus.ts.html +187 -0
  11. package/coverage/Developer Control Center/src/core/index.html +191 -0
  12. package/coverage/Developer Control Center/src/core/notifier.ts.html +187 -0
  13. package/coverage/Developer Control Center/src/core/persistence.ts.html +88 -0
  14. package/coverage/Developer Control Center/src/core/task-runner.ts.html +1498 -0
  15. package/coverage/Developer Control Center/src/core/workspaces.ts.html +304 -0
  16. package/coverage/Developer Control Center/src/plugins/index.html +116 -0
  17. package/coverage/Developer Control Center/src/plugins/manager.ts.html +259 -0
  18. package/coverage/Developer Control Center/src/status/index.html +116 -0
  19. package/coverage/Developer Control Center/src/status/store.ts.html +349 -0
  20. package/coverage/Developer Control Center/src/ui/command-list.tsx.html +574 -0
  21. package/coverage/Developer Control Center/src/ui/index.html +161 -0
  22. package/coverage/Developer Control Center/src/ui/metrics-panel.tsx.html +787 -0
  23. package/coverage/Developer Control Center/src/ui/panel.tsx.html +313 -0
  24. package/coverage/Developer Control Center/src/ui/status-panel.tsx.html +565 -0
  25. package/coverage/base.css +224 -0
  26. package/coverage/block-navigation.js +87 -0
  27. package/coverage/clover.xml +588 -0
  28. package/coverage/coverage-final.json +15 -0
  29. package/coverage/favicon.png +0 -0
  30. package/coverage/index.html +191 -0
  31. package/coverage/prettify.css +1 -0
  32. package/coverage/prettify.js +2 -0
  33. package/coverage/sort-arrow-sprite.png +0 -0
  34. package/coverage/sorter.js +210 -0
  35. package/dcc.config.js +2 -2
  36. package/dist/cli.js +1 -1
  37. package/dist/core/persistence.d.ts +2 -0
  38. package/dist/core/persistence.d.ts.map +1 -0
  39. package/dist/core/persistence.js +2 -0
  40. package/dist/core/persistence.js.map +1 -0
  41. package/dist/core/runtime.d.ts.map +1 -1
  42. package/dist/core/runtime.js +5 -3
  43. package/dist/core/runtime.js.map +1 -1
  44. package/dist/core/task-runner.d.ts +1 -0
  45. package/dist/core/task-runner.d.ts.map +1 -1
  46. package/dist/core/task-runner.js +81 -24
  47. package/dist/core/task-runner.js.map +1 -1
  48. package/dist/core/task-runner.test.d.ts +2 -0
  49. package/dist/core/task-runner.test.d.ts.map +1 -0
  50. package/dist/core/task-runner.test.js +326 -0
  51. package/dist/core/task-runner.test.js.map +1 -0
  52. package/dist/core/timer-plugin.d.ts.map +1 -1
  53. package/dist/core/timer-plugin.js +2 -1
  54. package/dist/core/timer-plugin.js.map +1 -1
  55. package/dist/plugins/manager.d.ts +2 -0
  56. package/dist/plugins/manager.d.ts.map +1 -1
  57. package/dist/plugins/manager.js +6 -2
  58. package/dist/plugins/manager.js.map +1 -1
  59. package/dist/plugins/manager.test.js +5 -2
  60. package/dist/plugins/manager.test.js.map +1 -1
  61. package/dist/ui/app.d.ts.map +1 -1
  62. package/dist/ui/app.js +124 -30
  63. package/dist/ui/app.js.map +1 -1
  64. package/dist/ui/app.test.d.ts +2 -0
  65. package/dist/ui/app.test.d.ts.map +1 -0
  66. package/dist/ui/app.test.js +157 -0
  67. package/dist/ui/app.test.js.map +1 -0
  68. package/dist/ui/command-list.test.d.ts +2 -0
  69. package/dist/ui/command-list.test.d.ts.map +1 -0
  70. package/dist/ui/command-list.test.js +104 -0
  71. package/dist/ui/command-list.test.js.map +1 -0
  72. package/dist/ui/metrics-panel.d.ts.map +1 -1
  73. package/dist/ui/metrics-panel.js +10 -9
  74. package/dist/ui/metrics-panel.js.map +1 -1
  75. package/dist/ui/metrics-panel.test.d.ts +2 -0
  76. package/dist/ui/metrics-panel.test.d.ts.map +1 -0
  77. package/dist/ui/metrics-panel.test.js +111 -0
  78. package/dist/ui/metrics-panel.test.js.map +1 -0
  79. package/dist/ui/panel.test.d.ts +2 -0
  80. package/dist/ui/panel.test.d.ts.map +1 -0
  81. package/dist/ui/panel.test.js +51 -0
  82. package/dist/ui/panel.test.js.map +1 -0
  83. package/dist/ui/status-panel.test.d.ts +2 -0
  84. package/dist/ui/status-panel.test.d.ts.map +1 -0
  85. package/dist/ui/status-panel.test.js +88 -0
  86. package/dist/ui/status-panel.test.js.map +1 -0
  87. package/package.json +4 -2
  88. package/src/cli.ts +1 -1
  89. package/src/core/persistence.ts +1 -0
  90. package/src/core/runtime.ts +7 -3
  91. package/src/core/task-runner.test.ts +395 -0
  92. package/src/core/task-runner.ts +80 -24
  93. package/src/core/timer-plugin.ts +2 -1
  94. package/src/plugins/manager.test.ts +5 -2
  95. package/src/plugins/manager.ts +6 -2
  96. package/src/ui/app.test.tsx +177 -0
  97. package/src/ui/app.tsx +167 -41
  98. package/src/ui/command-list.test.tsx +124 -0
  99. package/src/ui/metrics-panel.test.tsx +128 -0
  100. package/src/ui/metrics-panel.tsx +10 -10
  101. package/src/ui/panel.test.tsx +84 -0
  102. package/src/ui/status-panel.test.tsx +116 -0
  103. package/vitest.config.ts +1 -1
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { StatusPanel } from './status-panel.js';
5
+ function makeTask(overrides = {}) {
6
+ return {
7
+ id: 'test',
8
+ label: 'Test Task',
9
+ status: 'running',
10
+ output: '',
11
+ startTime: Date.now(),
12
+ ...overrides,
13
+ };
14
+ }
15
+ describe('StatusPanel', () => {
16
+ const baseProps = {
17
+ tasks: new Map(),
18
+ scrollOffsets: new Map(),
19
+ focusedPane: 'commands',
20
+ width: 60,
21
+ menuRows: 10,
22
+ };
23
+ it('shows no tasks yet when empty', () => {
24
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps }));
25
+ expect(lastFrame()).toContain('No tasks yet');
26
+ });
27
+ it('displays the task label and status', () => {
28
+ const tasks = new Map([['test', makeTask({ label: 'My Task', status: 'running' })]]);
29
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, tasks: tasks }));
30
+ const frame = lastFrame();
31
+ expect(frame).toContain('My Task');
32
+ expect(frame).toContain('RUNNING');
33
+ });
34
+ it('shows success status with green icon', () => {
35
+ const tasks = new Map([['test', makeTask({ status: 'success' })]]);
36
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, tasks: tasks }));
37
+ expect(lastFrame()).toContain('PASS');
38
+ });
39
+ it('shows failure status', () => {
40
+ const tasks = new Map([['test', makeTask({ status: 'failure', exitCode: 1 })]]);
41
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, tasks: tasks }));
42
+ expect(lastFrame()).toContain('FAIL');
43
+ expect(lastFrame()).toContain('exit 1');
44
+ });
45
+ it('displays output lines', () => {
46
+ const tasks = new Map([['test', makeTask({ output: 'line1\nline2\nline3' })]]);
47
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, tasks: tasks }));
48
+ const frame = lastFrame();
49
+ expect(frame).toContain('line1');
50
+ expect(frame).toContain('line2');
51
+ expect(frame).toContain('line3');
52
+ });
53
+ it('shows confirm prompt when confirmingCommand is set', () => {
54
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, confirmingCommand: { id: 'deploy', label: 'Deploy', command: 'npm run deploy' } }));
55
+ const frame = lastFrame();
56
+ expect(frame).toContain('Run');
57
+ expect(frame).toContain('Deploy');
58
+ });
59
+ it('shows input prompt when inputCommand is set', () => {
60
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, inputCommand: { id: 'greet', label: 'Greet', command: 'echo', input: { message: 'Enter name:' } } }));
61
+ const frame = lastFrame();
62
+ expect(frame).toContain('Enter name:');
63
+ });
64
+ it('shows input value', () => {
65
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, inputCommand: { id: 'greet', label: 'Greet', command: 'echo', input: {} }, inputValue: "hello" }));
66
+ expect(lastFrame()).toContain('hello');
67
+ });
68
+ it('sanitizes ANSI escape codes from output', () => {
69
+ const tasks = new Map([['test', makeTask({ output: '\x1B[31mred\x1B[0m' })]]);
70
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, tasks: tasks }));
71
+ expect(lastFrame()).not.toContain('\x1B[');
72
+ expect(lastFrame()).toContain('red');
73
+ });
74
+ it('shows duration for completed tasks', () => {
75
+ const now = Date.now();
76
+ const tasks = new Map([
77
+ ['test', makeTask({ startTime: now - 5000, endTime: now, status: 'success' })],
78
+ ]);
79
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, tasks: tasks }));
80
+ expect(lastFrame()).toContain('5.0s');
81
+ });
82
+ it('shows watch mode indicator', () => {
83
+ const tasks = new Map([['test', makeTask({ watchMode: true, status: 'running' })]]);
84
+ const { lastFrame } = render(_jsx(StatusPanel, { ...baseProps, tasks: tasks }));
85
+ expect(lastFrame()).toContain('WATCH');
86
+ });
87
+ });
88
+ //# sourceMappingURL=status-panel.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-panel.test.js","sourceRoot":"","sources":["../../src/ui/status-panel.test.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hartvig/developer-control-center",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
@@ -22,8 +22,10 @@
22
22
  "react": "^19.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@types/node": "^25.0.0",
25
+ "@types/node": "^26.0.0",
26
26
  "@types/react": "^19.0.0",
27
+ "@vitest/coverage-v8": "^4.1.9",
28
+ "ink-testing-library": "^4.0.0",
27
29
  "typescript": "^6.0.0",
28
30
  "vitest": "^4.1.7"
29
31
  }
package/src/cli.ts CHANGED
@@ -6,7 +6,7 @@ import { loadConfig } from './config/index.js';
6
6
  import { Runtime, detectCI } from './core/index.js';
7
7
  import { startUI } from './ui/index.js';
8
8
 
9
- const VERSION = '0.1.0';
9
+ const VERSION = '0.8.7';
10
10
 
11
11
  function printHelp(): void {
12
12
  console.log(`
@@ -0,0 +1 @@
1
+ export const PERSISTENCE_DIR = '.developer-control-center';
@@ -8,6 +8,7 @@ import { detectWorkspaces, WorkspacePackage } from './workspaces.js';
8
8
  import { sendNotification } from './notifier.js';
9
9
  import { detectCI, CIInfo } from './ci.js';
10
10
  import { timerPlugin } from './timer-plugin.js';
11
+ import { PERSISTENCE_DIR } from './persistence.js';
11
12
 
12
13
  function getGitBranch(): string | undefined {
13
14
  try {
@@ -25,12 +26,16 @@ export class Runtime {
25
26
  readonly eventBus = new EventBus();
26
27
  readonly statusStore = new StatusStore();
27
28
  readonly taskRunner: TaskRunner;
28
- readonly pluginManager = new PluginManager();
29
+ readonly pluginManager = new PluginManager(
30
+ (msg: string, err: unknown) => {
31
+ this.eventBus.emit('task:output' as any, '_plugins', `${msg}: ${err instanceof Error ? err.message : String(err)}\n`);
32
+ },
33
+ );
29
34
  readonly gitBranch: string | undefined;
30
35
  readonly workspaces: WorkspacePackage[];
31
36
  readonly ci: CIInfo;
32
37
 
33
- private persistenceDir = '.developer-control-center';
38
+ private persistenceDir = PERSISTENCE_DIR;
34
39
  private unsubPersistence?: () => void;
35
40
  private unsubNotifier?: () => void;
36
41
 
@@ -66,7 +71,6 @@ export class Runtime {
66
71
 
67
72
  stop(): void {
68
73
  this.taskRunner.abortAll();
69
- this.eventBus.removeAll();
70
74
  this.unsubPersistence?.();
71
75
  this.unsubNotifier?.();
72
76
  }
@@ -0,0 +1,395 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { TaskRunner } from './task-runner.js';
3
+ import { StatusStore } from '../status/store.js';
4
+ import { EventBus } from './event-bus.js';
5
+
6
+ vi.mock('child_process', () => {
7
+ function createMockChild() {
8
+ const { EventEmitter } = require('events');
9
+ const child = new EventEmitter() as any;
10
+ child.pid = 12345;
11
+ child.stdout = new EventEmitter() as any;
12
+ child.stdout.readable = true;
13
+ child.stderr = new EventEmitter() as any;
14
+ child.stderr.readable = true;
15
+ child.kill = vi.fn();
16
+ child.unref = vi.fn();
17
+ return child;
18
+ }
19
+
20
+ return {
21
+ spawn: vi.fn(() => createMockChild()),
22
+ execSync: vi.fn(() => ''),
23
+ };
24
+ });
25
+
26
+ import { spawn as _spawn, execSync as _execSync } from 'child_process';
27
+
28
+ const spawn = vi.mocked(_spawn);
29
+ const execSync = vi.mocked(_execSync);
30
+
31
+ const mockCommand = (overrides: Record<string, any> = {}) => ({
32
+ id: 'test',
33
+ label: 'Test',
34
+ command: 'echo hello',
35
+ ...overrides,
36
+ });
37
+
38
+ function flushMicrotasks(): Promise<void> {
39
+ return new Promise((resolve) => setImmediate(resolve));
40
+ }
41
+
42
+ describe('TaskRunner', () => {
43
+ let store: StatusStore;
44
+ let bus: EventBus;
45
+ let runner: TaskRunner;
46
+
47
+ beforeEach(() => {
48
+ spawn.mockClear();
49
+ execSync.mockClear();
50
+ store = new StatusStore();
51
+ bus = new EventBus();
52
+ runner = new TaskRunner(store, bus);
53
+ });
54
+
55
+ afterEach(() => {
56
+ runner?.abortAll();
57
+ });
58
+
59
+ describe('run', () => {
60
+ it('spawns a process with the command', async () => {
61
+ const cmd = mockCommand({ command: 'echo hello' });
62
+ const promise = runner.run(cmd);
63
+ await flushMicrotasks();
64
+ const child = spawn.mock.results[0]?.value;
65
+ child.emit('close', 0);
66
+ await promise;
67
+
68
+ expect(spawn).toHaveBeenCalledWith('echo hello', [], {
69
+ shell: true,
70
+ detached: true,
71
+ windowsHide: true,
72
+ stdio: ['ignore', 'pipe', 'pipe'],
73
+ cwd: undefined,
74
+ });
75
+ });
76
+
77
+ it('marks the task as success on exit code 0', async () => {
78
+ const cmd = mockCommand({ command: 'true' });
79
+ const promise = runner.run(cmd);
80
+ await flushMicrotasks();
81
+ const child = spawn.mock.results[0]?.value;
82
+ child.emit('close', 0);
83
+ await promise;
84
+
85
+ const task = store.getTask('test');
86
+ expect(task?.status).toBe('success');
87
+ expect(task?.exitCode).toBe(0);
88
+ });
89
+
90
+ it('marks the task as failure on non-zero exit code', async () => {
91
+ const cmd = mockCommand({ command: 'false' });
92
+ const promise = runner.run(cmd);
93
+ await flushMicrotasks();
94
+ const child = spawn.mock.results[0]?.value;
95
+ child.emit('close', 1);
96
+ await promise;
97
+
98
+ const task = store.getTask('test');
99
+ expect(task?.status).toBe('failure');
100
+ expect(task?.exitCode).toBe(1);
101
+ });
102
+
103
+ it('does nothing if command string is empty', async () => {
104
+ await runner.run(mockCommand({ command: '' }));
105
+ expect(spawn).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it('does nothing if task is already running', async () => {
109
+ store.updateTask('test', { id: 'test', label: 'T', status: 'running' });
110
+ await runner.run(mockCommand({ command: 'echo x' }));
111
+ expect(spawn).not.toHaveBeenCalled();
112
+ });
113
+
114
+ it('captures stdout output', async () => {
115
+ const cmd = mockCommand({ command: 'echo hello' });
116
+ const promise = runner.run(cmd);
117
+ await flushMicrotasks();
118
+ const child = spawn.mock.results[0]?.value;
119
+ child.stdout.emit('data', Buffer.from('hello\n'));
120
+ child.emit('close', 0);
121
+ await promise;
122
+
123
+ const task = store.getTask('test');
124
+ expect(task?.output).toBe('hello\n');
125
+ });
126
+
127
+ it('captures stderr output', async () => {
128
+ const cmd = mockCommand({ command: 'cmd' });
129
+ const promise = runner.run(cmd);
130
+ await flushMicrotasks();
131
+ const child = spawn.mock.results[0]?.value;
132
+ child.stderr.emit('data', Buffer.from('error\n'));
133
+ child.emit('close', 1);
134
+ await promise;
135
+
136
+ const task = store.getTask('test');
137
+ expect(task?.output).toBe('error\n');
138
+ });
139
+
140
+ it('handles spawn error', async () => {
141
+ const cmd = mockCommand({ command: 'bad' });
142
+ const promise = runner.run(cmd);
143
+ await flushMicrotasks();
144
+ const child = spawn.mock.results[0]?.value;
145
+ child.emit('error', new Error('spawn failed'));
146
+ await promise;
147
+
148
+ const task = store.getTask('test');
149
+ expect(task?.status).toBe('failure');
150
+ expect(task?.exitCode).toBe(-1);
151
+ expect(task?.output).toContain('spawn failed');
152
+ });
153
+
154
+ it('emits task:start and task:complete events', async () => {
155
+ const events: string[] = [];
156
+ bus.on('task:start', () => events.push('start'));
157
+ bus.on('task:complete', () => events.push('complete'));
158
+
159
+ const cmd = mockCommand({ command: 'echo x' });
160
+ const promise = runner.run(cmd);
161
+ await flushMicrotasks();
162
+ const child = spawn.mock.results[0]?.value;
163
+ child.emit('close', 0);
164
+ await promise;
165
+
166
+ expect(events).toEqual(['start', 'complete']);
167
+ });
168
+
169
+ it('emits task:error on spawn failure', async () => {
170
+ const events: string[] = [];
171
+ bus.on('task:error', () => events.push('error'));
172
+
173
+ const cmd = mockCommand({ command: 'bad' });
174
+ const promise = runner.run(cmd);
175
+ await flushMicrotasks();
176
+ const child = spawn.mock.results[0]?.value;
177
+ child.emit('error', new Error('fail'));
178
+ await promise;
179
+
180
+ expect(events).toEqual(['error']);
181
+ });
182
+
183
+ it('uses toggle.start for toggle commands', async () => {
184
+ const cmd = mockCommand({
185
+ toggle: { start: 'npm run dev' },
186
+ command: 'ignored',
187
+ });
188
+ const promise = runner.run(cmd);
189
+ await flushMicrotasks();
190
+ const child = spawn.mock.results[0]?.value;
191
+ child.emit('close', 0);
192
+ await promise;
193
+
194
+ expect(spawn).toHaveBeenCalledWith('npm run dev', [], expect.any(Object));
195
+ });
196
+
197
+ it('does not mark as complete for toggle.check commands', async () => {
198
+ const cmd = mockCommand({
199
+ toggle: { start: 'npm run dev', check: 'curl localhost' },
200
+ });
201
+ const promise = runner.run(cmd);
202
+ await flushMicrotasks();
203
+ const child = spawn.mock.results[0]?.value;
204
+ child.emit('close', 0);
205
+ await promise;
206
+
207
+ const task = store.getTask('test');
208
+ expect(task?.status).toBe('running');
209
+ });
210
+ });
211
+
212
+ describe('stop', () => {
213
+ it('kills a running process', async () => {
214
+ const cmd = mockCommand({ command: 'sleep 100' });
215
+ runner.run(cmd);
216
+ await flushMicrotasks();
217
+ const child = spawn.mock.results[0]?.value;
218
+
219
+ runner.stop(cmd);
220
+ child.emit('close', null);
221
+
222
+ expect(child.kill).toHaveBeenCalled();
223
+ const task = store.getTask('test');
224
+ expect(task?.status).toBe('success');
225
+ });
226
+
227
+ it('spawns toggle.stop command if defined', async () => {
228
+ const cmd = mockCommand({
229
+ toggle: { start: 'npm run dev', stop: 'pkill -f dev' },
230
+ });
231
+ const runPromise = runner.run(cmd);
232
+ await runPromise;
233
+
234
+ runner.stop(cmd);
235
+
236
+ expect(spawn).toHaveBeenCalledWith('pkill -f dev', {
237
+ shell: true,
238
+ stdio: 'ignore',
239
+ cwd: undefined,
240
+ });
241
+ });
242
+
243
+ it('is a no-op if no process is running', () => {
244
+ const cmd = mockCommand({ command: 'echo x' });
245
+ expect(() => runner.stop(cmd)).not.toThrow();
246
+ });
247
+ });
248
+
249
+ describe('abort / abortAll', () => {
250
+ it('aborts a single task', async () => {
251
+ const cmd = mockCommand({ command: 'sleep 100' });
252
+ runner.run(cmd);
253
+ await flushMicrotasks();
254
+ const child = spawn.mock.results[0]?.value;
255
+
256
+ runner.abort('test');
257
+
258
+ expect(child.kill).toHaveBeenCalled();
259
+ });
260
+
261
+ it('aborts all running tasks', async () => {
262
+ const cmd1 = mockCommand({ id: 'a', command: 'sleep 1' });
263
+ const cmd2 = mockCommand({ id: 'b', command: 'sleep 2' });
264
+ runner.run(cmd1);
265
+ await flushMicrotasks();
266
+ runner.run(cmd2);
267
+ await flushMicrotasks();
268
+ const child1 = spawn.mock.results[0]?.value;
269
+ const child2 = spawn.mock.results[1]?.value;
270
+
271
+ runner.abortAll();
272
+
273
+ expect(child1.kill).toHaveBeenCalled();
274
+ expect(child2.kill).toHaveBeenCalled();
275
+ });
276
+
277
+ it('abort does nothing for unknown id', () => {
278
+ expect(() => runner.abort('nope')).not.toThrow();
279
+ });
280
+ });
281
+
282
+ describe('setCommands', () => {
283
+ it('updates the internal command list', () => {
284
+ const cmds = [mockCommand({ id: 'a' }), mockCommand({ id: 'b' })];
285
+ runner.setCommands(cmds);
286
+ });
287
+ });
288
+
289
+ describe('pipeline', () => {
290
+ it('runs pipeline steps sequentially', async () => {
291
+ runner.setCommands([
292
+ mockCommand({ id: 'step1', command: 'echo step1' }),
293
+ mockCommand({ id: 'step2', command: 'echo step2' }),
294
+ ]);
295
+
296
+ const cmd = mockCommand({ pipelineSteps: ['step1', 'step2'] });
297
+ const promise = runner.run(cmd);
298
+ await flushMicrotasks();
299
+
300
+ const child1 = spawn.mock.results[0]?.value;
301
+ child1.emit('close', 0);
302
+ await flushMicrotasks();
303
+
304
+ const child2 = spawn.mock.results[1]?.value;
305
+ child2.emit('close', 0);
306
+ await promise;
307
+
308
+ const task = store.getTask('test');
309
+ expect(task?.status).toBe('success');
310
+ expect(task?.output).toContain('Pipeline completed');
311
+ });
312
+
313
+ it('fails if a step is not found', async () => {
314
+ const cmd = mockCommand({ pipelineSteps: ['missing-step'] });
315
+ await runner.run(cmd);
316
+
317
+ const task = store.getTask('test');
318
+ expect(task?.status).toBe('failure');
319
+ expect(task?.output).toContain('not found');
320
+ });
321
+
322
+ it('fails pipeline on step failure', async () => {
323
+ runner.setCommands([
324
+ mockCommand({ id: 'ok', command: 'true' }),
325
+ mockCommand({ id: 'bad', command: 'false' }),
326
+ ]);
327
+
328
+ const cmd = mockCommand({ pipelineSteps: ['ok', 'bad'] });
329
+ const promise = runner.run(cmd);
330
+ await flushMicrotasks();
331
+
332
+ const child1 = spawn.mock.results[0]?.value;
333
+ child1.emit('close', 0);
334
+ await flushMicrotasks();
335
+
336
+ const child2 = spawn.mock.results[1]?.value;
337
+ child2.emit('close', 1);
338
+ await promise;
339
+
340
+ const task = store.getTask('test');
341
+ expect(task?.status).toBe('failure');
342
+ expect(task?.output).toContain('Pipeline failed at step 2');
343
+ });
344
+ });
345
+
346
+ describe('parallel steps', () => {
347
+ it('runs steps in parallel and succeeds on all passing', async () => {
348
+ runner.setCommands([
349
+ mockCommand({ id: 'p1', command: 'echo 1' }),
350
+ mockCommand({ id: 'p2', command: 'echo 2' }),
351
+ ]);
352
+
353
+ const cmd = mockCommand({ parallelSteps: ['p1', 'p2'] });
354
+ const promise = runner.run(cmd);
355
+ await flushMicrotasks();
356
+
357
+ const children = spawn.mock.results;
358
+ children[0]?.value.emit('close', 0);
359
+ children[1]?.value.emit('close', 0);
360
+ await promise;
361
+
362
+ const task = store.getTask('test');
363
+ expect(task?.status).toBe('success');
364
+ expect(task?.output).toContain('All parallel steps passed');
365
+ });
366
+
367
+ it('fails if any step fails', async () => {
368
+ runner.setCommands([
369
+ mockCommand({ id: 'p1', command: 'echo 1' }),
370
+ mockCommand({ id: 'p2', command: 'echo 2' }),
371
+ ]);
372
+
373
+ const cmd = mockCommand({ parallelSteps: ['p1', 'p2'] });
374
+ const promise = runner.run(cmd);
375
+ await flushMicrotasks();
376
+
377
+ const children = spawn.mock.results;
378
+ children[0]?.value.emit('close', 0);
379
+ children[1]?.value.emit('close', 1);
380
+ await promise;
381
+
382
+ const task = store.getTask('test');
383
+ expect(task?.status).toBe('failure');
384
+ });
385
+
386
+ it('fails if a step is not found', async () => {
387
+ const cmd = mockCommand({ parallelSteps: ['missing'] });
388
+ await runner.run(cmd);
389
+
390
+ const task = store.getTask('test');
391
+ expect(task?.status).toBe('failure');
392
+ expect(task?.output).toContain('not found');
393
+ });
394
+ });
395
+ });