start-command 0.17.3 → 0.17.4
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 +19 -0
- package/package.json +1 -1
- package/src/bin/cli.js +126 -3
- package/src/lib/args-parser.js +17 -0
- package/src/lib/execution-store.js +106 -0
- package/test/args-parser.test.js +22 -0
- package/test/execution-store.test.js +121 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.17.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 89d04d6: fix: Ensure execution status is updated when process is interrupted
|
|
8
|
+
|
|
9
|
+
This fix addresses Issue #60 where `$ --status` shows "executing" for finished commands.
|
|
10
|
+
|
|
11
|
+
Changes:
|
|
12
|
+
- Added signal handlers (SIGINT, SIGTERM, SIGHUP) to update execution status when process is interrupted
|
|
13
|
+
- Added `--cleanup` and `--cleanup-dry-run` flags to clean up stale records from crashed/killed processes
|
|
14
|
+
- Added `cleanupStale()` method to ExecutionStore to detect and clean stale records
|
|
15
|
+
|
|
16
|
+
The cleanup logic detects stale records by:
|
|
17
|
+
1. Checking if the PID is still running (if on same platform)
|
|
18
|
+
2. Checking if the record has exceeded max age (default: 24 hours)
|
|
19
|
+
|
|
20
|
+
Stale records are marked as "executed" with exit code -1 to indicate abnormal termination.
|
|
21
|
+
|
|
3
22
|
## 0.17.3
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -73,6 +73,10 @@ const config = {
|
|
|
73
73
|
// Global execution store instance (initialized lazily)
|
|
74
74
|
let executionStore = null;
|
|
75
75
|
|
|
76
|
+
// Global reference to current execution record for cleanup on signals
|
|
77
|
+
// This allows us to mark the execution as completed if the process is interrupted
|
|
78
|
+
let currentExecutionRecord = null;
|
|
79
|
+
|
|
76
80
|
/**
|
|
77
81
|
* Get the execution store instance
|
|
78
82
|
* @returns {ExecutionStore}
|
|
@@ -87,6 +91,57 @@ function getExecutionStore() {
|
|
|
87
91
|
return executionStore;
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Cleanup handler for signals and process exit
|
|
96
|
+
* Marks the current execution as completed with an appropriate exit code
|
|
97
|
+
* @param {string} signal - The signal that triggered the cleanup (e.g., 'SIGINT', 'SIGTERM')
|
|
98
|
+
* @param {number} exitCode - The exit code to use
|
|
99
|
+
*/
|
|
100
|
+
function cleanupExecution(signal, exitCode) {
|
|
101
|
+
if (currentExecutionRecord && executionStore) {
|
|
102
|
+
// Signal-based exit codes:
|
|
103
|
+
// SIGINT (Ctrl+C): 130 (128 + 2)
|
|
104
|
+
// SIGTERM: 143 (128 + 15)
|
|
105
|
+
// SIGHUP: 129 (128 + 1)
|
|
106
|
+
currentExecutionRecord.complete(exitCode);
|
|
107
|
+
try {
|
|
108
|
+
executionStore.save(currentExecutionRecord);
|
|
109
|
+
if (config.verbose) {
|
|
110
|
+
console.error(
|
|
111
|
+
`\n[Tracking] Execution ${currentExecutionRecord.uuid} marked as completed (${signal}, exit code: ${exitCode})`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (config.verbose) {
|
|
116
|
+
console.error(
|
|
117
|
+
`[Tracking] Warning: Could not save execution record on ${signal}: ${err.message}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Clear the reference to prevent double cleanup
|
|
122
|
+
currentExecutionRecord = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Set up signal handlers to ensure execution status is updated on interruption
|
|
127
|
+
// SIGINT (Ctrl+C) - exit code 130 (128 + signal number 2)
|
|
128
|
+
process.on('SIGINT', () => {
|
|
129
|
+
cleanupExecution('SIGINT', 130);
|
|
130
|
+
process.exit(130);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// SIGTERM (kill command) - exit code 143 (128 + signal number 15)
|
|
134
|
+
process.on('SIGTERM', () => {
|
|
135
|
+
cleanupExecution('SIGTERM', 143);
|
|
136
|
+
process.exit(143);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// SIGHUP (terminal closed) - exit code 129 (128 + signal number 1)
|
|
140
|
+
process.on('SIGHUP', () => {
|
|
141
|
+
cleanupExecution('SIGHUP', 129);
|
|
142
|
+
process.exit(129);
|
|
143
|
+
});
|
|
144
|
+
|
|
90
145
|
// Get all arguments passed after the command
|
|
91
146
|
const args = process.argv.slice(2);
|
|
92
147
|
|
|
@@ -138,6 +193,12 @@ if (wrapperOptions.status) {
|
|
|
138
193
|
process.exit(0);
|
|
139
194
|
}
|
|
140
195
|
|
|
196
|
+
// Handle --cleanup flag
|
|
197
|
+
if (wrapperOptions.cleanup) {
|
|
198
|
+
handleCleanup(wrapperOptions.cleanupDryRun);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
141
202
|
// Check if no command was provided
|
|
142
203
|
if (!parsedCommand || parsedCommand.trim() === '') {
|
|
143
204
|
console.error('Error: No command provided');
|
|
@@ -206,6 +267,53 @@ function handleStatusQuery(uuid, outputFormat) {
|
|
|
206
267
|
}
|
|
207
268
|
}
|
|
208
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Handle --cleanup flag
|
|
272
|
+
* Cleans up stale "executing" records (processes that crashed or were killed)
|
|
273
|
+
* @param {boolean} dryRun - If true, just report what would be cleaned
|
|
274
|
+
*/
|
|
275
|
+
function handleCleanup(dryRun) {
|
|
276
|
+
const store = getExecutionStore();
|
|
277
|
+
if (!store) {
|
|
278
|
+
console.error('Error: Execution tracking is disabled.');
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const result = store.cleanupStale({ dryRun });
|
|
283
|
+
|
|
284
|
+
if (result.errors.length > 0) {
|
|
285
|
+
for (const error of result.errors) {
|
|
286
|
+
console.error(`Error: ${error}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (result.records.length === 0) {
|
|
291
|
+
console.log('No stale records found.');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (dryRun) {
|
|
296
|
+
console.log(
|
|
297
|
+
`Found ${result.records.length} stale record(s) that would be cleaned up:\n`
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`Cleaned up ${result.cleaned} stale record(s):\n`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const record of result.records) {
|
|
304
|
+
const startTime = new Date(record.startTime).toLocaleString();
|
|
305
|
+
console.log(` UUID: ${record.uuid}`);
|
|
306
|
+
console.log(` Command: ${record.command}`);
|
|
307
|
+
console.log(` Started: ${startTime}`);
|
|
308
|
+
console.log(` PID: ${record.pid || 'N/A'}`);
|
|
309
|
+
console.log('');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (dryRun) {
|
|
313
|
+
console.log('Run with --cleanup to actually clean up these records.');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
209
317
|
/** Print usage information */
|
|
210
318
|
function printUsage() {
|
|
211
319
|
console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>]
|
|
@@ -225,6 +333,8 @@ Options:
|
|
|
225
333
|
--auto-remove-docker-container Auto-remove docker container after exit
|
|
226
334
|
--use-command-stream Use command-stream library for execution (experimental)
|
|
227
335
|
--status <uuid> Show status of execution by UUID (--output-format: links-notation|json|text)
|
|
336
|
+
--cleanup Clean up stale "executing" records (crashed/killed processes)
|
|
337
|
+
--cleanup-dry-run Show stale records that would be cleaned up (without cleaning)
|
|
228
338
|
--version, -v Show version information
|
|
229
339
|
|
|
230
340
|
Examples:
|
|
@@ -411,8 +521,9 @@ async function runWithIsolation(
|
|
|
411
521
|
);
|
|
412
522
|
console.log('');
|
|
413
523
|
|
|
414
|
-
// Save initial execution record
|
|
524
|
+
// Save initial execution record and set global reference for signal cleanup
|
|
415
525
|
if (executionRecord && store) {
|
|
526
|
+
currentExecutionRecord = executionRecord;
|
|
416
527
|
try {
|
|
417
528
|
store.save(executionRecord);
|
|
418
529
|
} catch (err) {
|
|
@@ -478,7 +589,7 @@ async function runWithIsolation(
|
|
|
478
589
|
// Write log file
|
|
479
590
|
writeLogFile(logFilePath, logContent);
|
|
480
591
|
|
|
481
|
-
// Update execution record as completed
|
|
592
|
+
// Update execution record as completed and clear global reference
|
|
482
593
|
if (executionRecord && store) {
|
|
483
594
|
executionRecord.complete(exitCode);
|
|
484
595
|
try {
|
|
@@ -490,6 +601,8 @@ async function runWithIsolation(
|
|
|
490
601
|
);
|
|
491
602
|
}
|
|
492
603
|
}
|
|
604
|
+
// Clear global reference since we've completed normally
|
|
605
|
+
currentExecutionRecord = null;
|
|
493
606
|
}
|
|
494
607
|
|
|
495
608
|
// Cleanup: delete the created user if we created one (unless --keep-user)
|
|
@@ -580,6 +693,8 @@ async function runDirect(cmd, sessionId) {
|
|
|
580
693
|
runtimeVersion,
|
|
581
694
|
},
|
|
582
695
|
});
|
|
696
|
+
// Set global reference for signal cleanup
|
|
697
|
+
currentExecutionRecord = executionRecord;
|
|
583
698
|
}
|
|
584
699
|
|
|
585
700
|
// Log header
|
|
@@ -615,6 +730,8 @@ async function runDirect(cmd, sessionId) {
|
|
|
615
730
|
|
|
616
731
|
// Completion callback
|
|
617
732
|
const onComplete = (exitCode, endTime, _logContent, durationMs) => {
|
|
733
|
+
// Clear global reference since execution completed normally
|
|
734
|
+
currentExecutionRecord = null;
|
|
618
735
|
console.log('');
|
|
619
736
|
console.log(
|
|
620
737
|
createFinishBlock({
|
|
@@ -633,6 +750,8 @@ async function runDirect(cmd, sessionId) {
|
|
|
633
750
|
|
|
634
751
|
// Error callback
|
|
635
752
|
const onError = (errorMessage, endTime, durationMs) => {
|
|
753
|
+
// Clear global reference since execution completed (with error)
|
|
754
|
+
currentExecutionRecord = null;
|
|
636
755
|
console.error(`\n${errorMessage}`);
|
|
637
756
|
console.log('');
|
|
638
757
|
console.log(
|
|
@@ -726,6 +845,8 @@ async function runDirectWithCommandStream(
|
|
|
726
845
|
executionMode: 'command-stream',
|
|
727
846
|
},
|
|
728
847
|
});
|
|
848
|
+
// Set global reference for signal cleanup
|
|
849
|
+
currentExecutionRecord = executionRecord;
|
|
729
850
|
}
|
|
730
851
|
|
|
731
852
|
// Log header
|
|
@@ -817,7 +938,7 @@ async function runDirectWithCommandStream(
|
|
|
817
938
|
console.error(`\nWarning: Could not save log file: ${err.message}`);
|
|
818
939
|
}
|
|
819
940
|
|
|
820
|
-
// Update execution record as completed
|
|
941
|
+
// Update execution record as completed and clear global reference
|
|
821
942
|
if (executionRecord && store) {
|
|
822
943
|
executionRecord.complete(exitCode);
|
|
823
944
|
try {
|
|
@@ -829,6 +950,8 @@ async function runDirectWithCommandStream(
|
|
|
829
950
|
);
|
|
830
951
|
}
|
|
831
952
|
}
|
|
953
|
+
// Clear global reference since we've completed normally
|
|
954
|
+
currentExecutionRecord = null;
|
|
832
955
|
}
|
|
833
956
|
|
|
834
957
|
// Print finish block
|
package/src/lib/args-parser.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* --use-command-stream Use command-stream library for command execution (experimental)
|
|
20
20
|
* --status <uuid> Show status of a previous command execution by UUID
|
|
21
21
|
* --output-format <format> Output format for status (links-notation, json, text)
|
|
22
|
+
* --cleanup Clean up stale "executing" records (processes that crashed or were killed)
|
|
23
|
+
* --cleanup-dry-run Show stale records that would be cleaned up (without actually cleaning)
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
// Debug mode from environment
|
|
@@ -91,6 +93,8 @@ function parseArgs(args) {
|
|
|
91
93
|
useCommandStream: false, // Use command-stream library for command execution
|
|
92
94
|
status: null, // UUID to show status for
|
|
93
95
|
outputFormat: null, // Output format for status (links-notation, json, text)
|
|
96
|
+
cleanup: false, // Clean up stale "executing" records
|
|
97
|
+
cleanupDryRun: false, // Show what would be cleaned without actually cleaning
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
let commandArgs = [];
|
|
@@ -339,6 +343,19 @@ function parseOption(args, index, options) {
|
|
|
339
343
|
return 1;
|
|
340
344
|
}
|
|
341
345
|
|
|
346
|
+
// --cleanup
|
|
347
|
+
if (arg === '--cleanup') {
|
|
348
|
+
options.cleanup = true;
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// --cleanup-dry-run
|
|
353
|
+
if (arg === '--cleanup-dry-run') {
|
|
354
|
+
options.cleanup = true;
|
|
355
|
+
options.cleanupDryRun = true;
|
|
356
|
+
return 1;
|
|
357
|
+
}
|
|
358
|
+
|
|
342
359
|
// Not a recognized wrapper option
|
|
343
360
|
return 0;
|
|
344
361
|
}
|
|
@@ -517,6 +517,112 @@ class ExecutionStore {
|
|
|
517
517
|
return records.slice(0, limit);
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
/**
|
|
521
|
+
* Clean up stale "executing" records
|
|
522
|
+
*
|
|
523
|
+
* Stale records are those that:
|
|
524
|
+
* 1. Have status "executing"
|
|
525
|
+
* 2. Either:
|
|
526
|
+
* - Their process (by PID) is no longer running (on same hostname)
|
|
527
|
+
* - They have been "executing" for longer than maxAgeMs (default: 24 hours)
|
|
528
|
+
*
|
|
529
|
+
* @param {object} options - Cleanup options
|
|
530
|
+
* @param {number} options.maxAgeMs - Max age for executing records (default: 24 hours)
|
|
531
|
+
* @param {boolean} options.dryRun - If true, just report what would be cleaned (default: false)
|
|
532
|
+
* @returns {{cleaned: number, records: ExecutionRecord[], errors: string[]}}
|
|
533
|
+
*/
|
|
534
|
+
cleanupStale(options = {}) {
|
|
535
|
+
const maxAgeMs = options.maxAgeMs || 24 * 60 * 60 * 1000; // 24 hours
|
|
536
|
+
const dryRun = options.dryRun || false;
|
|
537
|
+
|
|
538
|
+
const result = {
|
|
539
|
+
cleaned: 0,
|
|
540
|
+
records: [],
|
|
541
|
+
errors: [],
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const records = this.readLinoRecords();
|
|
545
|
+
const executingRecords = records.filter(
|
|
546
|
+
(r) => r.status === ExecutionStatus.EXECUTING
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
const staleRecords = [];
|
|
550
|
+
|
|
551
|
+
for (const record of executingRecords) {
|
|
552
|
+
let isStale = false;
|
|
553
|
+
let reason = '';
|
|
554
|
+
|
|
555
|
+
// Check if the process is still running (only if on same platform)
|
|
556
|
+
if (record.pid && record.platform === process.platform) {
|
|
557
|
+
try {
|
|
558
|
+
// Signal 0 just checks if process exists
|
|
559
|
+
process.kill(record.pid, 0);
|
|
560
|
+
// Process exists, check age
|
|
561
|
+
} catch {
|
|
562
|
+
// Process doesn't exist - record is stale
|
|
563
|
+
isStale = true;
|
|
564
|
+
reason = `process ${record.pid} no longer running`;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check age if not already determined to be stale
|
|
569
|
+
if (!isStale) {
|
|
570
|
+
const startTime = new Date(record.startTime).getTime();
|
|
571
|
+
const age = Date.now() - startTime;
|
|
572
|
+
if (age > maxAgeMs) {
|
|
573
|
+
isStale = true;
|
|
574
|
+
reason = `running for ${Math.round(age / 1000 / 60)} minutes (max: ${Math.round(maxAgeMs / 1000 / 60)} minutes)`;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (isStale) {
|
|
579
|
+
this.log(`Stale record found: ${record.uuid} (${reason})`);
|
|
580
|
+
staleRecords.push(record);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
result.records = staleRecords;
|
|
585
|
+
|
|
586
|
+
if (!dryRun && staleRecords.length > 0) {
|
|
587
|
+
const lock = new LockManager(this.lockFilePath);
|
|
588
|
+
|
|
589
|
+
if (!lock.acquire()) {
|
|
590
|
+
result.errors.push('Failed to acquire lock for cleanup');
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// Re-read records to ensure consistency
|
|
596
|
+
const currentRecords = this.readLinoRecords();
|
|
597
|
+
|
|
598
|
+
// Update stale records to "executed" status with exit code -1 (abnormal termination)
|
|
599
|
+
for (const staleRecord of staleRecords) {
|
|
600
|
+
const index = currentRecords.findIndex(
|
|
601
|
+
(r) => r.uuid === staleRecord.uuid
|
|
602
|
+
);
|
|
603
|
+
if (index >= 0) {
|
|
604
|
+
// Mark as executed with exit code -1 to indicate abnormal termination
|
|
605
|
+
currentRecords[index].status = ExecutionStatus.EXECUTED;
|
|
606
|
+
currentRecords[index].exitCode = -1;
|
|
607
|
+
currentRecords[index].endTime = new Date().toISOString();
|
|
608
|
+
result.cleaned++;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
this.writeLinoRecords(currentRecords);
|
|
613
|
+
this.log(`Cleaned up ${result.cleaned} stale records`);
|
|
614
|
+
} catch (err) {
|
|
615
|
+
result.errors.push(`Cleanup error: ${err.message}`);
|
|
616
|
+
} finally {
|
|
617
|
+
lock.release();
|
|
618
|
+
}
|
|
619
|
+
} else if (dryRun) {
|
|
620
|
+
result.cleaned = staleRecords.length;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
625
|
+
|
|
520
626
|
/**
|
|
521
627
|
* Delete an execution record
|
|
522
628
|
* @param {string} uuid
|
package/test/args-parser.test.js
CHANGED
|
@@ -889,3 +889,25 @@ describe('VALID_OUTPUT_FORMATS', () => {
|
|
|
889
889
|
assert.ok(VALID_OUTPUT_FORMATS.includes('text'));
|
|
890
890
|
});
|
|
891
891
|
});
|
|
892
|
+
|
|
893
|
+
describe('cleanup options', () => {
|
|
894
|
+
it('should parse --cleanup flag', () => {
|
|
895
|
+
const result = parseArgs(['--cleanup']);
|
|
896
|
+
assert.strictEqual(result.wrapperOptions.cleanup, true);
|
|
897
|
+
assert.strictEqual(result.wrapperOptions.cleanupDryRun, false);
|
|
898
|
+
assert.strictEqual(result.command, '');
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('should parse --cleanup-dry-run flag', () => {
|
|
902
|
+
const result = parseArgs(['--cleanup-dry-run']);
|
|
903
|
+
assert.strictEqual(result.wrapperOptions.cleanup, true);
|
|
904
|
+
assert.strictEqual(result.wrapperOptions.cleanupDryRun, true);
|
|
905
|
+
assert.strictEqual(result.command, '');
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('should default cleanup to false', () => {
|
|
909
|
+
const result = parseArgs(['echo', 'hello']);
|
|
910
|
+
assert.strictEqual(result.wrapperOptions.cleanup, false);
|
|
911
|
+
assert.strictEqual(result.wrapperOptions.cleanupDryRun, false);
|
|
912
|
+
});
|
|
913
|
+
});
|
|
@@ -480,4 +480,125 @@ describe('ExecutionStore verifyConsistency', () => {
|
|
|
480
480
|
});
|
|
481
481
|
});
|
|
482
482
|
|
|
483
|
+
describe('ExecutionStore cleanupStale', () => {
|
|
484
|
+
let store;
|
|
485
|
+
|
|
486
|
+
beforeEach(() => {
|
|
487
|
+
cleanupTestDir();
|
|
488
|
+
store = new ExecutionStore({
|
|
489
|
+
appFolder: TEST_APP_FOLDER,
|
|
490
|
+
useLinks: false,
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
afterEach(() => {
|
|
495
|
+
cleanupTestDir();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should return empty result when no stale records exist', () => {
|
|
499
|
+
// Create a completed record
|
|
500
|
+
const record = new ExecutionRecord({
|
|
501
|
+
command: 'echo 1',
|
|
502
|
+
pid: process.pid, // Current process
|
|
503
|
+
});
|
|
504
|
+
record.complete(0);
|
|
505
|
+
store.save(record);
|
|
506
|
+
|
|
507
|
+
const result = store.cleanupStale();
|
|
508
|
+
expect(result.cleaned).toBe(0);
|
|
509
|
+
expect(result.records.length).toBe(0);
|
|
510
|
+
expect(result.errors.length).toBe(0);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should detect stale records with non-existent PIDs', () => {
|
|
514
|
+
// Create an "executing" record with a non-existent PID
|
|
515
|
+
const record = new ExecutionRecord({
|
|
516
|
+
command: 'echo stale',
|
|
517
|
+
pid: 999999999, // Non-existent PID
|
|
518
|
+
platform: process.platform,
|
|
519
|
+
});
|
|
520
|
+
store.save(record);
|
|
521
|
+
|
|
522
|
+
const result = store.cleanupStale({ dryRun: true });
|
|
523
|
+
expect(result.records.length).toBe(1);
|
|
524
|
+
expect(result.records[0].uuid).toBe(record.uuid);
|
|
525
|
+
expect(result.cleaned).toBe(1);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('should detect stale records that exceed max age', () => {
|
|
529
|
+
// Create an "executing" record with an old start time
|
|
530
|
+
const oldTime = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(); // 25 hours ago
|
|
531
|
+
const record = new ExecutionRecord({
|
|
532
|
+
command: 'echo old',
|
|
533
|
+
pid: process.pid, // Current process (still running)
|
|
534
|
+
startTime: oldTime,
|
|
535
|
+
platform: process.platform,
|
|
536
|
+
});
|
|
537
|
+
store.save(record);
|
|
538
|
+
|
|
539
|
+
// Use default max age (24 hours)
|
|
540
|
+
const result = store.cleanupStale({ dryRun: true });
|
|
541
|
+
expect(result.records.length).toBe(1);
|
|
542
|
+
expect(result.records[0].uuid).toBe(record.uuid);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should not detect records within max age as stale', () => {
|
|
546
|
+
// Create an "executing" record with a recent start time
|
|
547
|
+
const record = new ExecutionRecord({
|
|
548
|
+
command: 'echo recent',
|
|
549
|
+
pid: process.pid, // Current process (still running)
|
|
550
|
+
platform: process.platform,
|
|
551
|
+
});
|
|
552
|
+
store.save(record);
|
|
553
|
+
|
|
554
|
+
const result = store.cleanupStale({ dryRun: true });
|
|
555
|
+
// Should not find this as stale because process is running and not too old
|
|
556
|
+
expect(result.records.length).toBe(0);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should clean up stale records when dryRun is false', () => {
|
|
560
|
+
// Create an "executing" record with a non-existent PID
|
|
561
|
+
const record = new ExecutionRecord({
|
|
562
|
+
command: 'echo cleanup-me',
|
|
563
|
+
pid: 999999999, // Non-existent PID
|
|
564
|
+
platform: process.platform,
|
|
565
|
+
});
|
|
566
|
+
store.save(record);
|
|
567
|
+
|
|
568
|
+
// Verify it's still "executing"
|
|
569
|
+
let retrieved = store.get(record.uuid);
|
|
570
|
+
expect(retrieved.status).toBe(ExecutionStatus.EXECUTING);
|
|
571
|
+
|
|
572
|
+
// Clean it up
|
|
573
|
+
const result = store.cleanupStale({ dryRun: false });
|
|
574
|
+
expect(result.cleaned).toBe(1);
|
|
575
|
+
|
|
576
|
+
// Verify it's now "executed" with exit code -1
|
|
577
|
+
retrieved = store.get(record.uuid);
|
|
578
|
+
expect(retrieved.status).toBe(ExecutionStatus.EXECUTED);
|
|
579
|
+
expect(retrieved.exitCode).toBe(-1);
|
|
580
|
+
expect(retrieved.endTime).toBeTruthy();
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('should handle custom max age', () => {
|
|
584
|
+
// Create an "executing" record with start time 5 minutes ago
|
|
585
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
|
586
|
+
const record = new ExecutionRecord({
|
|
587
|
+
command: 'echo custom-age',
|
|
588
|
+
pid: process.pid, // Current process (still running)
|
|
589
|
+
startTime: fiveMinutesAgo,
|
|
590
|
+
platform: process.platform,
|
|
591
|
+
});
|
|
592
|
+
store.save(record);
|
|
593
|
+
|
|
594
|
+
// Should not find as stale with default 24h max age
|
|
595
|
+
let result = store.cleanupStale({ dryRun: true });
|
|
596
|
+
expect(result.records.length).toBe(0);
|
|
597
|
+
|
|
598
|
+
// Should find as stale with 1 minute max age
|
|
599
|
+
result = store.cleanupStale({ dryRun: true, maxAgeMs: 60 * 1000 });
|
|
600
|
+
expect(result.records.length).toBe(1);
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
483
604
|
console.log('=== Execution Store Unit Tests ===');
|