start-command 0.11.0 → 0.15.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 (62) hide show
  1. package/CHANGELOG.md +28 -217
  2. package/bun.lock +10 -0
  3. package/eslint.config.mjs +1 -1
  4. package/package.json +13 -5
  5. package/src/bin/cli.js +414 -499
  6. package/src/lib/args-parser.js +126 -0
  7. package/src/lib/command-stream.js +258 -0
  8. package/src/lib/execution-store.js +722 -0
  9. package/src/lib/failure-handler.js +397 -0
  10. package/src/lib/isolation.js +51 -0
  11. package/src/lib/status-formatter.js +121 -0
  12. package/src/lib/version.js +143 -0
  13. package/test/args-parser.test.js +140 -0
  14. package/test/cli.test.js +11 -1
  15. package/test/docker-autoremove.test.js +11 -16
  16. package/test/execution-store.test.js +483 -0
  17. package/test/isolation-cleanup.test.js +11 -16
  18. package/test/isolation.test.js +11 -17
  19. package/test/public-exports.test.js +105 -0
  20. package/test/status-query.test.js +195 -0
  21. package/.github/workflows/release.yml +0 -334
  22. package/.husky/pre-commit +0 -1
  23. package/ARCHITECTURE.md +0 -297
  24. package/LICENSE +0 -24
  25. package/README.md +0 -339
  26. package/REQUIREMENTS.md +0 -299
  27. package/docs/PIPES.md +0 -243
  28. package/docs/USAGE.md +0 -194
  29. package/docs/case-studies/issue-15/README.md +0 -208
  30. package/docs/case-studies/issue-18/README.md +0 -343
  31. package/docs/case-studies/issue-18/issue-comments.json +0 -1
  32. package/docs/case-studies/issue-18/issue-data.json +0 -7
  33. package/docs/case-studies/issue-22/analysis.md +0 -547
  34. package/docs/case-studies/issue-22/issue-data.json +0 -12
  35. package/docs/case-studies/issue-25/README.md +0 -232
  36. package/docs/case-studies/issue-25/issue-data.json +0 -21
  37. package/docs/case-studies/issue-28/README.md +0 -405
  38. package/docs/case-studies/issue-28/issue-data.json +0 -105
  39. package/docs/case-studies/issue-28/raw-issue-data.md +0 -92
  40. package/experiments/debug-regex.js +0 -49
  41. package/experiments/isolation-design.md +0 -131
  42. package/experiments/screen-output-test.js +0 -265
  43. package/experiments/test-cli.sh +0 -42
  44. package/experiments/test-screen-attached.js +0 -126
  45. package/experiments/test-screen-logfile.js +0 -286
  46. package/experiments/test-screen-modes.js +0 -128
  47. package/experiments/test-screen-output.sh +0 -27
  48. package/experiments/test-screen-tee-debug.js +0 -237
  49. package/experiments/test-screen-tee-fallback.js +0 -230
  50. package/experiments/test-substitution.js +0 -143
  51. package/experiments/user-isolation-research.md +0 -83
  52. package/scripts/changeset-version.mjs +0 -38
  53. package/scripts/check-file-size.mjs +0 -103
  54. package/scripts/create-github-release.mjs +0 -93
  55. package/scripts/create-manual-changeset.mjs +0 -89
  56. package/scripts/format-github-release.mjs +0 -83
  57. package/scripts/format-release-notes.mjs +0 -219
  58. package/scripts/instant-version-bump.mjs +0 -121
  59. package/scripts/publish-to-npm.mjs +0 -129
  60. package/scripts/setup-npm.mjs +0 -37
  61. package/scripts/validate-changeset.mjs +0 -107
  62. package/scripts/version-and-commit.mjs +0 -237
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Unit tests for execution-store.js
3
+ */
4
+
5
+ const { describe, it, expect, beforeEach, afterEach } = require('bun:test');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const {
11
+ ExecutionStore,
12
+ ExecutionRecord,
13
+ ExecutionStatus,
14
+ LockManager,
15
+ isClinkInstalled,
16
+ LINO_DB_FILE,
17
+ LINKS_DB_FILE,
18
+ LOCK_FILE,
19
+ } = require('../src/lib/execution-store');
20
+
21
+ // Use temp directory for tests
22
+ const TEST_APP_FOLDER = path.join(
23
+ os.tmpdir(),
24
+ `execution-store-test-${Date.now()}`
25
+ );
26
+
27
+ // Helper to clean up test directory
28
+ function cleanupTestDir() {
29
+ if (fs.existsSync(TEST_APP_FOLDER)) {
30
+ fs.rmSync(TEST_APP_FOLDER, { recursive: true, force: true });
31
+ }
32
+ }
33
+
34
+ describe('ExecutionRecord', () => {
35
+ it('should create a new execution record with default values', () => {
36
+ const record = new ExecutionRecord();
37
+
38
+ expect(record.uuid).toBeTruthy();
39
+ expect(record.uuid).toMatch(
40
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
41
+ );
42
+ expect(record.pid).toBeNull();
43
+ expect(record.status).toBe(ExecutionStatus.EXECUTING);
44
+ expect(record.exitCode).toBeNull();
45
+ expect(record.command).toBe('');
46
+ expect(record.logPath).toBe('');
47
+ expect(record.startTime).toBeTruthy();
48
+ expect(record.endTime).toBeNull();
49
+ expect(record.workingDirectory).toBeTruthy();
50
+ expect(record.platform).toBe(process.platform);
51
+ });
52
+
53
+ it('should create a new execution record with custom values', () => {
54
+ const customOptions = {
55
+ uuid: '12345678-1234-1234-1234-123456789abc',
56
+ pid: 12345,
57
+ status: ExecutionStatus.EXECUTED,
58
+ exitCode: 0,
59
+ command: 'echo hello',
60
+ logPath: '/tmp/test.log',
61
+ startTime: '2024-01-01T00:00:00.000Z',
62
+ endTime: '2024-01-01T00:00:01.000Z',
63
+ workingDirectory: '/home/user',
64
+ shell: '/bin/zsh',
65
+ platform: 'darwin',
66
+ options: { custom: 'option' },
67
+ };
68
+
69
+ const record = new ExecutionRecord(customOptions);
70
+
71
+ expect(record.uuid).toBe(customOptions.uuid);
72
+ expect(record.pid).toBe(customOptions.pid);
73
+ expect(record.status).toBe(customOptions.status);
74
+ expect(record.exitCode).toBe(customOptions.exitCode);
75
+ expect(record.command).toBe(customOptions.command);
76
+ expect(record.logPath).toBe(customOptions.logPath);
77
+ expect(record.startTime).toBe(customOptions.startTime);
78
+ expect(record.endTime).toBe(customOptions.endTime);
79
+ expect(record.workingDirectory).toBe(customOptions.workingDirectory);
80
+ expect(record.shell).toBe(customOptions.shell);
81
+ expect(record.platform).toBe(customOptions.platform);
82
+ expect(record.options.custom).toBe('option');
83
+ });
84
+
85
+ it('should mark execution as completed', () => {
86
+ const record = new ExecutionRecord({ command: 'echo hello' });
87
+
88
+ expect(record.status).toBe(ExecutionStatus.EXECUTING);
89
+ expect(record.exitCode).toBeNull();
90
+ expect(record.endTime).toBeNull();
91
+
92
+ record.complete(0);
93
+
94
+ expect(record.status).toBe(ExecutionStatus.EXECUTED);
95
+ expect(record.exitCode).toBe(0);
96
+ expect(record.endTime).toBeTruthy();
97
+ });
98
+
99
+ it('should convert to plain object and back', () => {
100
+ const record = new ExecutionRecord({
101
+ command: 'echo hello',
102
+ pid: 12345,
103
+ logPath: '/tmp/test.log',
104
+ });
105
+
106
+ const obj = record.toObject();
107
+ expect(obj.uuid).toBe(record.uuid);
108
+ expect(obj.command).toBe('echo hello');
109
+ expect(obj.pid).toBe(12345);
110
+
111
+ const restored = ExecutionRecord.fromObject(obj);
112
+ expect(restored.uuid).toBe(record.uuid);
113
+ expect(restored.command).toBe('echo hello');
114
+ expect(restored.pid).toBe(12345);
115
+ });
116
+ });
117
+
118
+ describe('LockManager', () => {
119
+ const testLockPath = path.join(TEST_APP_FOLDER, 'test.lock');
120
+
121
+ beforeEach(() => {
122
+ cleanupTestDir();
123
+ fs.mkdirSync(TEST_APP_FOLDER, { recursive: true });
124
+ });
125
+
126
+ afterEach(() => {
127
+ cleanupTestDir();
128
+ });
129
+
130
+ it('should acquire and release a lock', () => {
131
+ const lock = new LockManager(testLockPath);
132
+
133
+ expect(lock.acquire()).toBe(true);
134
+ expect(fs.existsSync(testLockPath)).toBe(true);
135
+
136
+ lock.release();
137
+ expect(fs.existsSync(testLockPath)).toBe(false);
138
+ });
139
+
140
+ it('should fail to acquire lock if already held by another process', () => {
141
+ // Create a lock file manually
142
+ fs.writeFileSync(
143
+ testLockPath,
144
+ JSON.stringify({
145
+ pid: 999999, // Non-existent PID but still check the mechanism
146
+ timestamp: Date.now(),
147
+ hostname: os.hostname(),
148
+ })
149
+ );
150
+
151
+ const lock = new LockManager(testLockPath);
152
+
153
+ // The lock should be acquired because the PID 999999 doesn't exist
154
+ // (so the lock is considered stale)
155
+ expect(lock.acquire(1000)).toBe(true);
156
+
157
+ lock.release();
158
+ });
159
+
160
+ it('should detect stale locks', () => {
161
+ const lock = new LockManager(testLockPath);
162
+
163
+ // Create a stale lock (old timestamp)
164
+ const staleData = {
165
+ pid: 999999,
166
+ timestamp: Date.now() - 120000, // 2 minutes ago
167
+ hostname: os.hostname(),
168
+ };
169
+
170
+ expect(lock.isLockStale(staleData)).toBe(true);
171
+
172
+ // Create a fresh lock
173
+ const freshData = {
174
+ pid: process.pid,
175
+ timestamp: Date.now(),
176
+ hostname: os.hostname(),
177
+ };
178
+
179
+ expect(lock.isLockStale(freshData)).toBe(false);
180
+ });
181
+ });
182
+
183
+ describe('ExecutionStore', () => {
184
+ let store;
185
+
186
+ beforeEach(() => {
187
+ cleanupTestDir();
188
+ store = new ExecutionStore({
189
+ appFolder: TEST_APP_FOLDER,
190
+ useLinks: false, // Disable links for unit tests
191
+ verbose: false,
192
+ });
193
+ });
194
+
195
+ afterEach(() => {
196
+ cleanupTestDir();
197
+ });
198
+
199
+ it('should create app folder on initialization', () => {
200
+ expect(fs.existsSync(TEST_APP_FOLDER)).toBe(true);
201
+ });
202
+
203
+ it('should save and retrieve an execution record', () => {
204
+ const record = new ExecutionRecord({
205
+ command: 'echo hello',
206
+ pid: 12345,
207
+ logPath: '/tmp/test.log',
208
+ });
209
+
210
+ store.save(record);
211
+
212
+ const retrieved = store.get(record.uuid);
213
+ expect(retrieved).toBeTruthy();
214
+ expect(retrieved.uuid).toBe(record.uuid);
215
+ expect(retrieved.command).toBe('echo hello');
216
+ expect(retrieved.pid).toBe(12345);
217
+ });
218
+
219
+ it('should update an existing record', () => {
220
+ const record = new ExecutionRecord({
221
+ command: 'echo hello',
222
+ pid: 12345,
223
+ });
224
+
225
+ store.save(record);
226
+
227
+ // Update the record
228
+ record.complete(0);
229
+ store.save(record);
230
+
231
+ const retrieved = store.get(record.uuid);
232
+ expect(retrieved.status).toBe(ExecutionStatus.EXECUTED);
233
+ expect(retrieved.exitCode).toBe(0);
234
+ expect(retrieved.endTime).toBeTruthy();
235
+ });
236
+
237
+ it('should get all records', () => {
238
+ const record1 = new ExecutionRecord({ command: 'echo 1' });
239
+ const record2 = new ExecutionRecord({ command: 'echo 2' });
240
+ const record3 = new ExecutionRecord({ command: 'echo 3' });
241
+
242
+ store.save(record1);
243
+ store.save(record2);
244
+ store.save(record3);
245
+
246
+ const all = store.getAll();
247
+ expect(all.length).toBe(3);
248
+ });
249
+
250
+ it('should get records by status', () => {
251
+ const executing1 = new ExecutionRecord({ command: 'echo 1' });
252
+ const executing2 = new ExecutionRecord({ command: 'echo 2' });
253
+ const executed = new ExecutionRecord({ command: 'echo 3' });
254
+ executed.complete(0);
255
+
256
+ store.save(executing1);
257
+ store.save(executing2);
258
+ store.save(executed);
259
+
260
+ const executingRecords = store.getExecuting();
261
+ expect(executingRecords.length).toBe(2);
262
+
263
+ const executedRecords = store.getByStatus(ExecutionStatus.EXECUTED);
264
+ expect(executedRecords.length).toBe(1);
265
+ expect(executedRecords[0].uuid).toBe(executed.uuid);
266
+ });
267
+
268
+ it('should get recent records', () => {
269
+ // Create records with different start times
270
+ const record1 = new ExecutionRecord({
271
+ command: 'echo 1',
272
+ startTime: '2024-01-01T00:00:00.000Z',
273
+ });
274
+ const record2 = new ExecutionRecord({
275
+ command: 'echo 2',
276
+ startTime: '2024-01-02T00:00:00.000Z',
277
+ });
278
+ const record3 = new ExecutionRecord({
279
+ command: 'echo 3',
280
+ startTime: '2024-01-03T00:00:00.000Z',
281
+ });
282
+
283
+ store.save(record1);
284
+ store.save(record2);
285
+ store.save(record3);
286
+
287
+ const recent = store.getRecent(2);
288
+ expect(recent.length).toBe(2);
289
+ expect(recent[0].command).toBe('echo 3'); // Most recent first
290
+ expect(recent[1].command).toBe('echo 2');
291
+ });
292
+
293
+ it('should delete a record', () => {
294
+ const record = new ExecutionRecord({ command: 'echo hello' });
295
+
296
+ store.save(record);
297
+ expect(store.get(record.uuid)).toBeTruthy();
298
+
299
+ const deleted = store.delete(record.uuid);
300
+ expect(deleted).toBe(true);
301
+ expect(store.get(record.uuid)).toBeNull();
302
+ });
303
+
304
+ it('should return false when deleting non-existent record', () => {
305
+ const deleted = store.delete('non-existent-uuid');
306
+ expect(deleted).toBe(false);
307
+ });
308
+
309
+ it('should clear all records', () => {
310
+ const record1 = new ExecutionRecord({ command: 'echo 1' });
311
+ const record2 = new ExecutionRecord({ command: 'echo 2' });
312
+
313
+ store.save(record1);
314
+ store.save(record2);
315
+ expect(store.getAll().length).toBe(2);
316
+
317
+ store.clear();
318
+ expect(store.getAll().length).toBe(0);
319
+ });
320
+
321
+ it('should get statistics', () => {
322
+ const executing = new ExecutionRecord({ command: 'echo 1' });
323
+ const success = new ExecutionRecord({ command: 'echo 2' });
324
+ success.complete(0);
325
+ const failure = new ExecutionRecord({ command: 'echo 3' });
326
+ failure.complete(1);
327
+
328
+ store.save(executing);
329
+ store.save(success);
330
+ store.save(failure);
331
+
332
+ const stats = store.getStats();
333
+ expect(stats.total).toBe(3);
334
+ expect(stats.executing).toBe(1);
335
+ expect(stats.executed).toBe(2);
336
+ expect(stats.successful).toBe(1);
337
+ expect(stats.failed).toBe(1);
338
+ expect(stats.linoDbPath).toContain(LINO_DB_FILE);
339
+ });
340
+
341
+ it('should handle concurrent saves with locking', async () => {
342
+ // Create multiple records quickly
343
+ const records = [];
344
+ for (let i = 0; i < 10; i++) {
345
+ records.push(new ExecutionRecord({ command: `echo ${i}` }));
346
+ }
347
+
348
+ // Save all records
349
+ for (const record of records) {
350
+ store.save(record);
351
+ }
352
+
353
+ // Verify all are saved
354
+ const all = store.getAll();
355
+ expect(all.length).toBe(10);
356
+ });
357
+
358
+ it('should persist data to lino file', () => {
359
+ const record = new ExecutionRecord({
360
+ command: 'echo hello',
361
+ pid: 12345,
362
+ });
363
+
364
+ store.save(record);
365
+
366
+ // Verify file exists
367
+ const linoPath = path.join(TEST_APP_FOLDER, LINO_DB_FILE);
368
+ expect(fs.existsSync(linoPath)).toBe(true);
369
+
370
+ // Read and verify content - lino format uses base64 encoding for strings
371
+ const content = fs.readFileSync(linoPath, 'utf8');
372
+ expect(content.length).toBeGreaterThan(0);
373
+ // Verify content has array structure
374
+ expect(content).toContain('(array');
375
+ // Verify PID is stored (as int)
376
+ expect(content).toContain('(int 12345)');
377
+ });
378
+ });
379
+
380
+ describe('isClinkInstalled', () => {
381
+ it('should return a boolean', () => {
382
+ const result = isClinkInstalled();
383
+ expect(typeof result).toBe('boolean');
384
+ // Note: The actual value depends on whether clink is installed in the test environment
385
+ });
386
+ });
387
+
388
+ describe('ExecutionStore with lino-objects-codec', () => {
389
+ let store;
390
+
391
+ beforeEach(() => {
392
+ cleanupTestDir();
393
+ store = new ExecutionStore({
394
+ appFolder: TEST_APP_FOLDER,
395
+ useLinks: false,
396
+ });
397
+ });
398
+
399
+ afterEach(() => {
400
+ cleanupTestDir();
401
+ });
402
+
403
+ it('should properly encode and decode complex options', () => {
404
+ const record = new ExecutionRecord({
405
+ command: 'npm test',
406
+ options: {
407
+ substitutionMatched: true,
408
+ originalCommand: 'run tests',
409
+ runtime: 'Bun',
410
+ runtimeVersion: '1.0.0',
411
+ nested: {
412
+ deep: {
413
+ value: 'test',
414
+ },
415
+ },
416
+ array: [1, 2, 3],
417
+ },
418
+ });
419
+
420
+ store.save(record);
421
+
422
+ const retrieved = store.get(record.uuid);
423
+ expect(retrieved.options.substitutionMatched).toBe(true);
424
+ expect(retrieved.options.originalCommand).toBe('run tests');
425
+ expect(retrieved.options.nested.deep.value).toBe('test');
426
+ expect(retrieved.options.array).toEqual([1, 2, 3]);
427
+ });
428
+
429
+ it('should handle special characters in command', () => {
430
+ const record = new ExecutionRecord({
431
+ command: 'echo "hello world" | grep "world"',
432
+ });
433
+
434
+ store.save(record);
435
+
436
+ const retrieved = store.get(record.uuid);
437
+ expect(retrieved.command).toBe('echo "hello world" | grep "world"');
438
+ });
439
+
440
+ it('should handle unicode characters', () => {
441
+ const record = new ExecutionRecord({
442
+ command: 'echo "Hello 世界 🌍"',
443
+ logPath: '/tmp/unicode-日本語.log',
444
+ });
445
+
446
+ store.save(record);
447
+
448
+ const retrieved = store.get(record.uuid);
449
+ expect(retrieved.command).toBe('echo "Hello 世界 🌍"');
450
+ expect(retrieved.logPath).toBe('/tmp/unicode-日本語.log');
451
+ });
452
+ });
453
+
454
+ describe('ExecutionStore verifyConsistency', () => {
455
+ let store;
456
+
457
+ beforeEach(() => {
458
+ cleanupTestDir();
459
+ store = new ExecutionStore({
460
+ appFolder: TEST_APP_FOLDER,
461
+ useLinks: false, // clink likely not available in test environment
462
+ });
463
+ });
464
+
465
+ afterEach(() => {
466
+ cleanupTestDir();
467
+ });
468
+
469
+ it('should report consistency status for lino-only store', () => {
470
+ const record1 = new ExecutionRecord({ command: 'echo 1' });
471
+ const record2 = new ExecutionRecord({ command: 'echo 2' });
472
+
473
+ store.save(record1);
474
+ store.save(record2);
475
+
476
+ const result = store.verifyConsistency();
477
+ expect(result.linoCount).toBe(2);
478
+ // Without clink, there will be an error about it not being installed
479
+ expect(result.errors.length).toBeGreaterThan(0);
480
+ });
481
+ });
482
+
483
+ console.log('=== Execution Store Unit Tests ===');
@@ -238,22 +238,15 @@ describe('Isolation Resource Cleanup Verification', () => {
238
238
  });
239
239
 
240
240
  describe('docker resource cleanup', () => {
241
- // Helper function to check if docker daemon is running
242
- function isDockerRunning() {
243
- if (!isCommandAvailable('docker')) {
244
- return false;
245
- }
246
- try {
247
- execSync('docker info', { stdio: 'ignore', timeout: 5000 });
248
- return true;
249
- } catch {
250
- return false;
251
- }
252
- }
241
+ // Use the canRunLinuxDockerImages function from isolation module
242
+ // to properly detect if Linux containers can run (handles Windows containers mode)
243
+ const { canRunLinuxDockerImages } = require('../src/lib/isolation');
253
244
 
254
245
  it('should show docker container as exited after command completes (auto-exit by default)', async () => {
255
- if (!isDockerRunning()) {
256
- console.log(' Skipping: docker not available or daemon not running');
246
+ if (!canRunLinuxDockerImages()) {
247
+ console.log(
248
+ ' Skipping: docker not available, daemon not running, or Linux containers not supported'
249
+ );
257
250
  return;
258
251
  }
259
252
 
@@ -325,8 +318,10 @@ describe('Isolation Resource Cleanup Verification', () => {
325
318
  });
326
319
 
327
320
  it('should keep docker container running when keepAlive is true', async () => {
328
- if (!isDockerRunning()) {
329
- console.log(' Skipping: docker not available or daemon not running');
321
+ if (!canRunLinuxDockerImages()) {
322
+ console.log(
323
+ ' Skipping: docker not available, daemon not running, or Linux containers not supported'
324
+ );
330
325
  return;
331
326
  }
332
327
 
@@ -444,23 +444,15 @@ describe('Isolation Keep-Alive Behavior', () => {
444
444
  });
445
445
 
446
446
  describe('runInDocker keep-alive messages', () => {
447
- // Helper function to check if docker daemon is running
448
- function isDockerRunning() {
449
- if (!isCommandAvailable('docker')) {
450
- return false;
451
- }
452
- try {
453
- // Try to ping the docker daemon
454
- execSync('docker info', { stdio: 'ignore', timeout: 5000 });
455
- return true;
456
- } catch {
457
- return false;
458
- }
459
- }
447
+ // Use the canRunLinuxDockerImages function from isolation module
448
+ // to properly detect if Linux containers can run (handles Windows containers mode)
449
+ const { canRunLinuxDockerImages } = require('../src/lib/isolation');
460
450
 
461
451
  it('should include auto-exit message by default in detached mode', async () => {
462
- if (!isDockerRunning()) {
463
- console.log(' Skipping: docker not available or daemon not running');
452
+ if (!canRunLinuxDockerImages()) {
453
+ console.log(
454
+ ' Skipping: docker not available, daemon not running, or Linux containers not supported'
455
+ );
464
456
  return;
465
457
  }
466
458
 
@@ -487,8 +479,10 @@ describe('Isolation Keep-Alive Behavior', () => {
487
479
  });
488
480
 
489
481
  it('should include keep-alive message when keepAlive is true', async () => {
490
- if (!isDockerRunning()) {
491
- console.log(' Skipping: docker not available or daemon not running');
482
+ if (!canRunLinuxDockerImages()) {
483
+ console.log(
484
+ ' Skipping: docker not available, daemon not running, or Linux containers not supported'
485
+ );
492
486
  return;
493
487
  }
494
488
 
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Tests for public exports of start-command package
3
+ * Verifies that ExecutionStore can be imported via package exports
4
+ */
5
+
6
+ const { describe, it, expect } = require('bun:test');
7
+
8
+ describe('Public Exports', () => {
9
+ describe('execution-store export', () => {
10
+ it('should export ExecutionStore class', () => {
11
+ const { ExecutionStore } = require('../src/lib/execution-store');
12
+ expect(ExecutionStore).toBeDefined();
13
+ expect(typeof ExecutionStore).toBe('function');
14
+ });
15
+
16
+ it('should export ExecutionRecord class', () => {
17
+ const { ExecutionRecord } = require('../src/lib/execution-store');
18
+ expect(ExecutionRecord).toBeDefined();
19
+ expect(typeof ExecutionRecord).toBe('function');
20
+ });
21
+
22
+ it('should export ExecutionStatus enum', () => {
23
+ const { ExecutionStatus } = require('../src/lib/execution-store');
24
+ expect(ExecutionStatus).toBeDefined();
25
+ expect(ExecutionStatus.EXECUTING).toBe('executing');
26
+ expect(ExecutionStatus.EXECUTED).toBe('executed');
27
+ });
28
+
29
+ it('should export LockManager class', () => {
30
+ const { LockManager } = require('../src/lib/execution-store');
31
+ expect(LockManager).toBeDefined();
32
+ expect(typeof LockManager).toBe('function');
33
+ });
34
+
35
+ it('should export isClinkInstalled function', () => {
36
+ const { isClinkInstalled } = require('../src/lib/execution-store');
37
+ expect(isClinkInstalled).toBeDefined();
38
+ expect(typeof isClinkInstalled).toBe('function');
39
+ });
40
+
41
+ it('should export configuration constants', () => {
42
+ const {
43
+ DEFAULT_APP_FOLDER,
44
+ LINO_DB_FILE,
45
+ LINKS_DB_FILE,
46
+ LOCK_FILE,
47
+ } = require('../src/lib/execution-store');
48
+ expect(DEFAULT_APP_FOLDER).toBeDefined();
49
+ expect(typeof DEFAULT_APP_FOLDER).toBe('string');
50
+ expect(LINO_DB_FILE).toBe('executions.lino');
51
+ expect(LINKS_DB_FILE).toBe('executions.links');
52
+ expect(LOCK_FILE).toBe('executions.lock');
53
+ });
54
+
55
+ it('should allow creating and using ExecutionStore instance', () => {
56
+ const os = require('os');
57
+ const path = require('path');
58
+ const fs = require('fs');
59
+
60
+ const {
61
+ ExecutionStore,
62
+ ExecutionRecord,
63
+ ExecutionStatus,
64
+ } = require('../src/lib/execution-store');
65
+
66
+ // Create a temporary folder for testing
67
+ const testFolder = path.join(
68
+ os.tmpdir(),
69
+ `public-export-test-${Date.now()}`
70
+ );
71
+
72
+ try {
73
+ const store = new ExecutionStore({ appFolder: testFolder });
74
+ expect(store).toBeDefined();
75
+
76
+ // Create and save a record
77
+ const record = new ExecutionRecord({
78
+ command: 'echo "test"',
79
+ logPath: '/tmp/test.log',
80
+ });
81
+ expect(record.status).toBe(ExecutionStatus.EXECUTING);
82
+
83
+ store.save(record);
84
+
85
+ // Retrieve the record
86
+ const retrieved = store.get(record.uuid);
87
+ expect(retrieved).toBeDefined();
88
+ expect(retrieved.command).toBe('echo "test"');
89
+
90
+ // Complete the record
91
+ record.complete(0);
92
+ store.save(record);
93
+
94
+ const completed = store.get(record.uuid);
95
+ expect(completed.status).toBe(ExecutionStatus.EXECUTED);
96
+ expect(completed.exitCode).toBe(0);
97
+ } finally {
98
+ // Cleanup
99
+ if (fs.existsSync(testFolder)) {
100
+ fs.rmSync(testFolder, { recursive: true, force: true });
101
+ }
102
+ }
103
+ });
104
+ });
105
+ });