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.
- package/CHANGELOG.md +28 -217
- package/bun.lock +10 -0
- package/eslint.config.mjs +1 -1
- package/package.json +13 -5
- package/src/bin/cli.js +414 -499
- package/src/lib/args-parser.js +126 -0
- package/src/lib/command-stream.js +258 -0
- package/src/lib/execution-store.js +722 -0
- package/src/lib/failure-handler.js +397 -0
- package/src/lib/isolation.js +51 -0
- package/src/lib/status-formatter.js +121 -0
- package/src/lib/version.js +143 -0
- package/test/args-parser.test.js +140 -0
- package/test/cli.test.js +11 -1
- package/test/docker-autoremove.test.js +11 -16
- package/test/execution-store.test.js +483 -0
- package/test/isolation-cleanup.test.js +11 -16
- package/test/isolation.test.js +11 -17
- package/test/public-exports.test.js +105 -0
- package/test/status-query.test.js +195 -0
- package/.github/workflows/release.yml +0 -334
- package/.husky/pre-commit +0 -1
- package/ARCHITECTURE.md +0 -297
- package/LICENSE +0 -24
- package/README.md +0 -339
- package/REQUIREMENTS.md +0 -299
- package/docs/PIPES.md +0 -243
- package/docs/USAGE.md +0 -194
- package/docs/case-studies/issue-15/README.md +0 -208
- package/docs/case-studies/issue-18/README.md +0 -343
- package/docs/case-studies/issue-18/issue-comments.json +0 -1
- package/docs/case-studies/issue-18/issue-data.json +0 -7
- package/docs/case-studies/issue-22/analysis.md +0 -547
- package/docs/case-studies/issue-22/issue-data.json +0 -12
- package/docs/case-studies/issue-25/README.md +0 -232
- package/docs/case-studies/issue-25/issue-data.json +0 -21
- package/docs/case-studies/issue-28/README.md +0 -405
- package/docs/case-studies/issue-28/issue-data.json +0 -105
- package/docs/case-studies/issue-28/raw-issue-data.md +0 -92
- package/experiments/debug-regex.js +0 -49
- package/experiments/isolation-design.md +0 -131
- package/experiments/screen-output-test.js +0 -265
- package/experiments/test-cli.sh +0 -42
- package/experiments/test-screen-attached.js +0 -126
- package/experiments/test-screen-logfile.js +0 -286
- package/experiments/test-screen-modes.js +0 -128
- package/experiments/test-screen-output.sh +0 -27
- package/experiments/test-screen-tee-debug.js +0 -237
- package/experiments/test-screen-tee-fallback.js +0 -230
- package/experiments/test-substitution.js +0 -143
- package/experiments/user-isolation-research.md +0 -83
- package/scripts/changeset-version.mjs +0 -38
- package/scripts/check-file-size.mjs +0 -103
- package/scripts/create-github-release.mjs +0 -93
- package/scripts/create-manual-changeset.mjs +0 -89
- package/scripts/format-github-release.mjs +0 -83
- package/scripts/format-release-notes.mjs +0 -219
- package/scripts/instant-version-bump.mjs +0 -121
- package/scripts/publish-to-npm.mjs +0 -129
- package/scripts/setup-npm.mjs +0 -37
- package/scripts/validate-changeset.mjs +0 -107
- 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
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
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 (!
|
|
256
|
-
console.log(
|
|
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 (!
|
|
329
|
-
console.log(
|
|
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
|
|
package/test/isolation.test.js
CHANGED
|
@@ -444,23 +444,15 @@ describe('Isolation Keep-Alive Behavior', () => {
|
|
|
444
444
|
});
|
|
445
445
|
|
|
446
446
|
describe('runInDocker keep-alive messages', () => {
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
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 (!
|
|
463
|
-
console.log(
|
|
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 (!
|
|
491
|
-
console.log(
|
|
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
|
+
});
|