start-command 0.25.0 → 0.25.2
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 +18 -0
- package/package.json +2 -2
- package/src/bin/cli.js +6 -8
- package/src/lib/args-parser.js +2 -2
- package/src/lib/execution-store.js +14 -5
- package/src/lib/status-formatter.js +108 -4
- package/test/session-name-status.test.js +311 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.25.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8f58d2b: fix: support --session name lookups in --status and track detached session lifecycle
|
|
8
|
+
|
|
9
|
+
`--status` now accepts session names in addition to UUIDs. Detached mode no longer incorrectly reports immediate completion — status is determined at query time by checking if the actual session is still running.
|
|
10
|
+
|
|
11
|
+
## 0.25.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- da6df0e: fix: correct license field from MIT to Unlicense (public domain)
|
|
16
|
+
|
|
17
|
+
Updated `package.json` to correctly reflect the Unlicense (public domain) license instead of MIT. The project's `LICENSE` file has always contained the Unlicense text; this change aligns the metadata with the actual license.
|
|
18
|
+
|
|
19
|
+
Fixes #99
|
|
20
|
+
|
|
3
21
|
## 0.25.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "start-command",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
|
|
5
5
|
"main": "src/bin/cli.js",
|
|
6
6
|
"exports": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"automation"
|
|
33
33
|
],
|
|
34
34
|
"author": "",
|
|
35
|
-
"license": "
|
|
35
|
+
"license": "Unlicense",
|
|
36
36
|
"engines": {
|
|
37
37
|
"bun": ">=1.0.0"
|
|
38
38
|
},
|
package/src/bin/cli.js
CHANGED
|
@@ -333,7 +333,7 @@ Options:
|
|
|
333
333
|
--auto-remove-docker-container Auto-remove docker container after exit
|
|
334
334
|
--shell <shell> Shell to use in isolation environments: auto, bash, zsh, sh (default: auto)
|
|
335
335
|
--use-command-stream Use command-stream library for execution (experimental)
|
|
336
|
-
--status <
|
|
336
|
+
--status <id> Show status of execution by UUID or session name (--output-format: links-notation|json|text)
|
|
337
337
|
--cleanup Clean up stale "executing" records (crashed/killed processes)
|
|
338
338
|
--cleanup-dry-run Show stale records that would be cleaned up (without cleaning)
|
|
339
339
|
--version, -v Show version information
|
|
@@ -468,12 +468,9 @@ async function runWithIsolation(
|
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
// Add isolation info to extra lines
|
|
471
|
+
// Add isolation info to extra lines (session name for reconnecting, see issue #67)
|
|
472
472
|
if (environment) {
|
|
473
473
|
extraLines.push(`[Isolation] Environment: ${environment}, Mode: ${mode}`);
|
|
474
|
-
// Always add the session name so users can reconnect to detached sessions
|
|
475
|
-
// This is important for screen, tmux, docker where the session/container name
|
|
476
|
-
// is different from the session UUID used for tracking (see issue #67)
|
|
477
474
|
extraLines.push(`[Isolation] Session: ${sessionName}`);
|
|
478
475
|
}
|
|
479
476
|
if (effectiveImage) {
|
|
@@ -602,9 +599,11 @@ async function runWithIsolation(
|
|
|
602
599
|
// Write log file
|
|
603
600
|
writeLogFile(logFilePath, logContent);
|
|
604
601
|
|
|
605
|
-
// Update execution record
|
|
602
|
+
// Update execution record: detached keeps "executing" (resolved at query time)
|
|
606
603
|
if (executionRecord && store) {
|
|
607
|
-
|
|
604
|
+
if (mode !== 'detached') {
|
|
605
|
+
executionRecord.complete(exitCode);
|
|
606
|
+
}
|
|
608
607
|
try {
|
|
609
608
|
store.save(executionRecord);
|
|
610
609
|
} catch (err) {
|
|
@@ -614,7 +613,6 @@ async function runWithIsolation(
|
|
|
614
613
|
);
|
|
615
614
|
}
|
|
616
615
|
}
|
|
617
|
-
// Clear global reference since we've completed normally
|
|
618
616
|
currentExecutionRecord = null;
|
|
619
617
|
}
|
|
620
618
|
|
package/src/lib/args-parser.js
CHANGED
|
@@ -418,13 +418,13 @@ function parseOption(args, index, options) {
|
|
|
418
418
|
return 1;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
-
// --status <uuid>
|
|
421
|
+
// --status <uuid-or-session-name>
|
|
422
422
|
if (arg === '--status') {
|
|
423
423
|
if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
424
424
|
options.status = args[index + 1];
|
|
425
425
|
return 2;
|
|
426
426
|
} else {
|
|
427
|
-
throw new Error(`Option ${arg} requires a UUID argument`);
|
|
427
|
+
throw new Error(`Option ${arg} requires a UUID or session name argument`);
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
|
|
@@ -470,14 +470,23 @@ class ExecutionStore {
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
/**
|
|
473
|
-
* Get an execution record by UUID
|
|
474
|
-
*
|
|
473
|
+
* Get an execution record by UUID or session name
|
|
474
|
+
* First tries exact UUID match, then falls back to session name lookup
|
|
475
|
+
* @param {string} identifier - UUID or session name
|
|
475
476
|
* @returns {ExecutionRecord|null}
|
|
476
477
|
*/
|
|
477
|
-
get(
|
|
478
|
+
get(identifier) {
|
|
478
479
|
const records = this.readLinoRecords();
|
|
479
|
-
|
|
480
|
-
|
|
480
|
+
// First try exact UUID match
|
|
481
|
+
const byUuid = records.find((r) => r.uuid === identifier);
|
|
482
|
+
if (byUuid) {
|
|
483
|
+
return byUuid;
|
|
484
|
+
}
|
|
485
|
+
// Fall back to session name lookup (stored in options.sessionName)
|
|
486
|
+
const bySessionName = records.find(
|
|
487
|
+
(r) => r.options && r.options.sessionName === identifier
|
|
488
|
+
);
|
|
489
|
+
return bySessionName || null;
|
|
481
490
|
}
|
|
482
491
|
|
|
483
492
|
/**
|
|
@@ -7,11 +7,108 @@
|
|
|
7
7
|
* - Text: Human-readable text format
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
const { execSync } = require('child_process');
|
|
10
11
|
const {
|
|
11
12
|
escapeForLinksNotation,
|
|
12
13
|
formatAsNestedLinksNotation,
|
|
13
14
|
} = require('./output-blocks');
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Check if a detached isolation session is still running
|
|
18
|
+
* @param {Object} record - Execution record
|
|
19
|
+
* @returns {boolean|null} true if running, false if not, null if unable to determine
|
|
20
|
+
*/
|
|
21
|
+
function isDetachedSessionAlive(record) {
|
|
22
|
+
const opts = record.options || {};
|
|
23
|
+
const sessionName = opts.sessionName;
|
|
24
|
+
const isolationMode = opts.isolationMode;
|
|
25
|
+
const isolated = opts.isolated;
|
|
26
|
+
|
|
27
|
+
if (!sessionName || isolationMode !== 'detached') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
switch (isolated) {
|
|
33
|
+
case 'screen': {
|
|
34
|
+
const output = execSync('screen -ls', {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
37
|
+
});
|
|
38
|
+
return output.includes(sessionName);
|
|
39
|
+
}
|
|
40
|
+
case 'tmux': {
|
|
41
|
+
execSync(`tmux has-session -t ${sessionName}`, {
|
|
42
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
+
});
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
case 'docker': {
|
|
47
|
+
const output = execSync(
|
|
48
|
+
`docker inspect -f "{{.State.Running}}" ${sessionName}`,
|
|
49
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
50
|
+
);
|
|
51
|
+
return output.trim() === 'true';
|
|
52
|
+
}
|
|
53
|
+
case 'ssh': {
|
|
54
|
+
// For SSH, check if the PID is still running on remote would require
|
|
55
|
+
// re-connecting. Fall back to checking the local wrapper PID.
|
|
56
|
+
if (record.pid) {
|
|
57
|
+
try {
|
|
58
|
+
process.kill(record.pid, 0);
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Command failed - session is likely not running
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Enrich execution record with live session status for detached executions.
|
|
77
|
+
* If a record shows "executing" but the detached session has actually ended,
|
|
78
|
+
* returns an updated copy with status "executed". If it shows "executed" but
|
|
79
|
+
* the session is still running, returns a copy with status "executing".
|
|
80
|
+
* @param {Object} record - Execution record
|
|
81
|
+
* @returns {Object} Possibly updated execution record
|
|
82
|
+
*/
|
|
83
|
+
function enrichDetachedStatus(record) {
|
|
84
|
+
const alive = isDetachedSessionAlive(record);
|
|
85
|
+
if (alive === null) {
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Create a shallow copy to avoid mutating the original
|
|
90
|
+
const enriched = Object.create(Object.getPrototypeOf(record));
|
|
91
|
+
Object.assign(enriched, record);
|
|
92
|
+
|
|
93
|
+
if (alive && enriched.status === 'executed') {
|
|
94
|
+
// Session still running but record says executed - correct it
|
|
95
|
+
enriched.status = 'executing';
|
|
96
|
+
enriched.exitCode = null;
|
|
97
|
+
enriched.endTime = null;
|
|
98
|
+
} else if (!alive && enriched.status === 'executing') {
|
|
99
|
+
// Session ended but record says executing - correct it
|
|
100
|
+
enriched.status = 'executed';
|
|
101
|
+
if (enriched.exitCode === null || enriched.exitCode === undefined) {
|
|
102
|
+
enriched.exitCode = -1; // Unknown exit code
|
|
103
|
+
}
|
|
104
|
+
if (!enriched.endTime) {
|
|
105
|
+
enriched.endTime = new Date().toISOString();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return enriched;
|
|
110
|
+
}
|
|
111
|
+
|
|
15
112
|
/**
|
|
16
113
|
* Format execution record as Links Notation (indented style)
|
|
17
114
|
* Uses nested Links notation for object values (like options) instead of JSON
|
|
@@ -122,18 +219,23 @@ function formatRecord(record, format) {
|
|
|
122
219
|
* @param {string|null} outputFormat - Output format (links-notation, json, text)
|
|
123
220
|
* @returns {{success: boolean, output?: string, error?: string}}
|
|
124
221
|
*/
|
|
125
|
-
function queryStatus(store,
|
|
222
|
+
function queryStatus(store, identifier, outputFormat) {
|
|
126
223
|
if (!store) {
|
|
127
224
|
return { success: false, error: 'Execution tracking is disabled.' };
|
|
128
225
|
}
|
|
129
|
-
const record = store.get(
|
|
226
|
+
const record = store.get(identifier);
|
|
130
227
|
if (!record) {
|
|
131
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: `No execution found with UUID or session name: ${identifier}`,
|
|
231
|
+
};
|
|
132
232
|
}
|
|
133
233
|
try {
|
|
234
|
+
// Enrich detached execution status with live session check
|
|
235
|
+
const enrichedRecord = enrichDetachedStatus(record);
|
|
134
236
|
return {
|
|
135
237
|
success: true,
|
|
136
|
-
output: formatRecord(
|
|
238
|
+
output: formatRecord(enrichedRecord, outputFormat || 'links-notation'),
|
|
137
239
|
};
|
|
138
240
|
} catch (err) {
|
|
139
241
|
return { success: false, error: err.message };
|
|
@@ -145,4 +247,6 @@ module.exports = {
|
|
|
145
247
|
formatRecordAsText,
|
|
146
248
|
formatRecord,
|
|
147
249
|
queryStatus,
|
|
250
|
+
isDetachedSessionAlive,
|
|
251
|
+
enrichDetachedStatus,
|
|
148
252
|
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for --status lookup by session name and detached status enrichment
|
|
3
|
+
* Issue #101: --session name not usable with --status, and --detached reports immediate completion
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { describe, it, expect, beforeEach, afterEach } = require('bun:test');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
ExecutionStore,
|
|
14
|
+
ExecutionRecord,
|
|
15
|
+
ExecutionStatus,
|
|
16
|
+
} = require('../src/lib/execution-store');
|
|
17
|
+
const {
|
|
18
|
+
queryStatus,
|
|
19
|
+
isDetachedSessionAlive,
|
|
20
|
+
enrichDetachedStatus,
|
|
21
|
+
} = require('../src/lib/status-formatter');
|
|
22
|
+
|
|
23
|
+
// Use temp directory for tests
|
|
24
|
+
const TEST_APP_FOLDER = path.join(
|
|
25
|
+
os.tmpdir(),
|
|
26
|
+
`session-name-status-test-${Date.now()}`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Path to CLI
|
|
30
|
+
const CLI_PATH = path.join(__dirname, '../src/bin/cli.js');
|
|
31
|
+
|
|
32
|
+
// Helper to clean up test directory
|
|
33
|
+
function cleanupTestDir() {
|
|
34
|
+
if (fs.existsSync(TEST_APP_FOLDER)) {
|
|
35
|
+
fs.rmSync(TEST_APP_FOLDER, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Helper to run CLI command
|
|
40
|
+
function runCli(args, env = {}) {
|
|
41
|
+
const result = spawnSync('bun', [CLI_PATH, ...args], {
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
START_APP_FOLDER: TEST_APP_FOLDER,
|
|
46
|
+
...env,
|
|
47
|
+
},
|
|
48
|
+
timeout: 10000,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
stdout: result.stdout || '',
|
|
52
|
+
stderr: result.stderr || '',
|
|
53
|
+
exitCode: result.status,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('Issue #101: --status with session name lookup', () => {
|
|
58
|
+
let store;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
cleanupTestDir();
|
|
62
|
+
store = new ExecutionStore({
|
|
63
|
+
appFolder: TEST_APP_FOLDER,
|
|
64
|
+
useLinks: false,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
cleanupTestDir();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('ExecutionStore.get() session name lookup', () => {
|
|
73
|
+
it('should find record by UUID', () => {
|
|
74
|
+
const record = new ExecutionRecord({
|
|
75
|
+
command: 'echo hello',
|
|
76
|
+
pid: 12345,
|
|
77
|
+
logPath: '/tmp/test.log',
|
|
78
|
+
options: { sessionName: 'my-custom-session' },
|
|
79
|
+
});
|
|
80
|
+
record.complete(0);
|
|
81
|
+
store.save(record);
|
|
82
|
+
|
|
83
|
+
const found = store.get(record.uuid);
|
|
84
|
+
expect(found).not.toBeNull();
|
|
85
|
+
expect(found.uuid).toBe(record.uuid);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should find record by session name', () => {
|
|
89
|
+
const record = new ExecutionRecord({
|
|
90
|
+
command: 'sleep 60',
|
|
91
|
+
pid: 12345,
|
|
92
|
+
logPath: '/tmp/test.log',
|
|
93
|
+
options: {
|
|
94
|
+
sessionName: 'my-custom-session',
|
|
95
|
+
isolated: 'screen',
|
|
96
|
+
isolationMode: 'detached',
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
store.save(record);
|
|
100
|
+
|
|
101
|
+
const found = store.get('my-custom-session');
|
|
102
|
+
expect(found).not.toBeNull();
|
|
103
|
+
expect(found.uuid).toBe(record.uuid);
|
|
104
|
+
expect(found.options.sessionName).toBe('my-custom-session');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should prefer UUID match over session name', () => {
|
|
108
|
+
// Create two records: one whose session name matches the UUID of another
|
|
109
|
+
const record1 = new ExecutionRecord({
|
|
110
|
+
command: 'echo first',
|
|
111
|
+
pid: 111,
|
|
112
|
+
logPath: '/tmp/first.log',
|
|
113
|
+
options: { sessionName: 'some-session' },
|
|
114
|
+
});
|
|
115
|
+
record1.complete(0);
|
|
116
|
+
store.save(record1);
|
|
117
|
+
|
|
118
|
+
const record2 = new ExecutionRecord({
|
|
119
|
+
command: 'echo second',
|
|
120
|
+
pid: 222,
|
|
121
|
+
logPath: '/tmp/second.log',
|
|
122
|
+
options: { sessionName: record1.uuid }, // session name matches record1's UUID
|
|
123
|
+
});
|
|
124
|
+
store.save(record2);
|
|
125
|
+
|
|
126
|
+
// Looking up by record1's UUID should return record1, not record2
|
|
127
|
+
const found = store.get(record1.uuid);
|
|
128
|
+
expect(found.command).toBe('echo first');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should return null for non-existent session name', () => {
|
|
132
|
+
const found = store.get('nonexistent-session');
|
|
133
|
+
expect(found).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should return null for record without session name', () => {
|
|
137
|
+
const record = new ExecutionRecord({
|
|
138
|
+
command: 'echo hello',
|
|
139
|
+
pid: 12345,
|
|
140
|
+
logPath: '/tmp/test.log',
|
|
141
|
+
});
|
|
142
|
+
store.save(record);
|
|
143
|
+
|
|
144
|
+
const found = store.get('some-session-name');
|
|
145
|
+
expect(found).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('queryStatus() with session name', () => {
|
|
150
|
+
it('should query status by session name', () => {
|
|
151
|
+
const record = new ExecutionRecord({
|
|
152
|
+
command: 'sleep 60',
|
|
153
|
+
pid: 12345,
|
|
154
|
+
logPath: '/tmp/test.log',
|
|
155
|
+
options: {
|
|
156
|
+
sessionName: 'my-test-session',
|
|
157
|
+
isolated: 'screen',
|
|
158
|
+
isolationMode: 'attached',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
record.complete(0);
|
|
162
|
+
store.save(record);
|
|
163
|
+
|
|
164
|
+
const result = queryStatus(store, 'my-test-session', 'json');
|
|
165
|
+
expect(result.success).toBe(true);
|
|
166
|
+
|
|
167
|
+
const parsed = JSON.parse(result.output);
|
|
168
|
+
expect(parsed.uuid).toBe(record.uuid);
|
|
169
|
+
expect(parsed.command).toBe('sleep 60');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should show error for non-existent session name', () => {
|
|
173
|
+
const result = queryStatus(store, 'nonexistent-session', 'json');
|
|
174
|
+
expect(result.success).toBe(false);
|
|
175
|
+
expect(result.error).toContain('No execution found');
|
|
176
|
+
expect(result.error).toContain('nonexistent-session');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('CLI --status with session name', () => {
|
|
181
|
+
it('should query by session name via CLI', () => {
|
|
182
|
+
const record = new ExecutionRecord({
|
|
183
|
+
command: 'echo hello world',
|
|
184
|
+
pid: 12345,
|
|
185
|
+
logPath: '/tmp/test.log',
|
|
186
|
+
options: {
|
|
187
|
+
sessionName: 'cli-test-session',
|
|
188
|
+
isolated: 'screen',
|
|
189
|
+
isolationMode: 'attached',
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
record.complete(0);
|
|
193
|
+
store.save(record);
|
|
194
|
+
|
|
195
|
+
const result = runCli([
|
|
196
|
+
'--status',
|
|
197
|
+
'cli-test-session',
|
|
198
|
+
'--output-format',
|
|
199
|
+
'json',
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
expect(result.exitCode).toBe(0);
|
|
203
|
+
const parsed = JSON.parse(result.stdout);
|
|
204
|
+
expect(parsed.uuid).toBe(record.uuid);
|
|
205
|
+
expect(parsed.command).toBe('echo hello world');
|
|
206
|
+
expect(parsed.status).toBe('executed');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should show error for non-existent session name via CLI', () => {
|
|
210
|
+
const result = runCli(['--status', 'nonexistent-session']);
|
|
211
|
+
expect(result.exitCode).toBe(1);
|
|
212
|
+
expect(result.stderr).toContain('No execution found');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('Issue #101: Detached status enrichment', () => {
|
|
218
|
+
describe('isDetachedSessionAlive()', () => {
|
|
219
|
+
it('should return null for non-detached records', () => {
|
|
220
|
+
const record = new ExecutionRecord({
|
|
221
|
+
command: 'echo hello',
|
|
222
|
+
options: {
|
|
223
|
+
sessionName: 'test',
|
|
224
|
+
isolated: 'screen',
|
|
225
|
+
isolationMode: 'attached',
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
expect(isDetachedSessionAlive(record)).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should return null for records without session name', () => {
|
|
232
|
+
const record = new ExecutionRecord({
|
|
233
|
+
command: 'echo hello',
|
|
234
|
+
options: { isolationMode: 'detached' },
|
|
235
|
+
});
|
|
236
|
+
expect(isDetachedSessionAlive(record)).toBeNull();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should return false for non-existent screen session', () => {
|
|
240
|
+
const record = new ExecutionRecord({
|
|
241
|
+
command: 'sleep 60',
|
|
242
|
+
options: {
|
|
243
|
+
sessionName: 'nonexistent-screen-session-test-101',
|
|
244
|
+
isolated: 'screen',
|
|
245
|
+
isolationMode: 'detached',
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
const alive = isDetachedSessionAlive(record);
|
|
249
|
+
// May be false or null depending on whether screen is installed
|
|
250
|
+
if (alive !== null) {
|
|
251
|
+
expect(alive).toBe(false);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should return false for non-existent docker container', () => {
|
|
256
|
+
const record = new ExecutionRecord({
|
|
257
|
+
command: 'sleep 60',
|
|
258
|
+
options: {
|
|
259
|
+
sessionName: 'nonexistent-docker-container-test-101',
|
|
260
|
+
isolated: 'docker',
|
|
261
|
+
isolationMode: 'detached',
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const alive = isDetachedSessionAlive(record);
|
|
265
|
+
// May be false or null depending on whether docker is installed
|
|
266
|
+
if (alive !== null) {
|
|
267
|
+
expect(alive).toBe(false);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('enrichDetachedStatus()', () => {
|
|
273
|
+
it('should not modify non-detached records', () => {
|
|
274
|
+
const record = new ExecutionRecord({
|
|
275
|
+
command: 'echo hello',
|
|
276
|
+
options: {
|
|
277
|
+
sessionName: 'test',
|
|
278
|
+
isolated: 'screen',
|
|
279
|
+
isolationMode: 'attached',
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
record.complete(0);
|
|
283
|
+
|
|
284
|
+
const enriched = enrichDetachedStatus(record);
|
|
285
|
+
expect(enriched.status).toBe('executed');
|
|
286
|
+
expect(enriched.exitCode).toBe(0);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should mark non-running detached session as executed', () => {
|
|
290
|
+
const record = new ExecutionRecord({
|
|
291
|
+
command: 'sleep 60',
|
|
292
|
+
options: {
|
|
293
|
+
sessionName: 'nonexistent-session-enrich-test-101',
|
|
294
|
+
isolated: 'screen',
|
|
295
|
+
isolationMode: 'detached',
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
// Record says executing, but session doesn't exist
|
|
299
|
+
|
|
300
|
+
const enriched = enrichDetachedStatus(record);
|
|
301
|
+
// If screen is available, should mark as executed
|
|
302
|
+
// If screen is not available, should return unchanged
|
|
303
|
+
if (enriched.status === 'executed') {
|
|
304
|
+
expect(enriched.exitCode).toBe(-1); // Unknown exit code
|
|
305
|
+
expect(enriched.endTime).not.toBeNull();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
console.log('=== Session Name Status Tests (Issue #101) ===');
|