start-command 0.30.4 → 0.30.5

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # start-command
2
2
 
3
+ ## 0.30.5
4
+
5
+ ### Patch Changes
6
+
7
+ - d0656c2: Stop `--status` from fabricating a detached session exit code out of the command's own output: the terminal exit code is now read from the anchored three-line footer `start` writes (separator / `Finished:` / `Exit Code:`) in the tail of the log only, and Docker's own `.State.ExitCode` takes precedence over the log text.
8
+
3
9
  ## 0.30.4
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.30.4",
3
+ "version": "0.30.5",
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": {
@@ -175,20 +175,88 @@ function isDetachedSessionAlive(record) {
175
175
  }
176
176
  }
177
177
 
178
+ /**
179
+ * Number of trailing bytes scanned for the terminal log footer. The footer is
180
+ * always appended at the very end of the log, so there is no reason to read
181
+ * (potentially megabytes of) command output on every `--status` call.
182
+ */
183
+ const LOG_TAIL_BYTES = 16 * 1024;
184
+
185
+ /**
186
+ * The three-line terminal footer that `start` itself writes (see
187
+ * `createLogFooter()` and `createShellLogFooterSnippet()`):
188
+ *
189
+ * ==================================================
190
+ * Finished: 2026-07-30 23:36:20.295
191
+ * Exit Code: 0
192
+ *
193
+ * The whole block is anchored at line starts so that a bare `Exit Code: N`
194
+ * substring emitted by the wrapped command (inside a JSON payload, a quoted
195
+ * log excerpt, an `rg` dump, ...) can no longer be mistaken for the footer
196
+ * `start` appends itself (issue #150).
197
+ */
198
+ const LOG_FOOTER_PATTERN =
199
+ /^={10,}[ \t]*\r?\n^Finished:[^\r\n]*\r?\n^Exit Code:[ \t]*(-?\d+)[ \t]*(?![^\r\n])/gm;
200
+
201
+ /**
202
+ * Read the last `bytes` bytes of a file as UTF-8 text.
203
+ *
204
+ * A partial first line is dropped: the slice can start in the middle of a line,
205
+ * and that fragment must not be treated as the beginning of a line by the
206
+ * anchored footer pattern.
207
+ *
208
+ * @param {string} logPath - Path to the log file
209
+ * @param {number} [bytes] - Maximum number of trailing bytes to read
210
+ * @returns {string|null} Tail content, or null when the file cannot be read
211
+ */
212
+ function readLogTail(logPath, bytes = LOG_TAIL_BYTES) {
213
+ let fd;
214
+ try {
215
+ fd = fs.openSync(logPath, 'r');
216
+ const size = fs.fstatSync(fd).size;
217
+ const length = Math.min(size, bytes);
218
+ const buffer = Buffer.alloc(length);
219
+ fs.readSync(fd, buffer, 0, length, size - length);
220
+ const tail = buffer.toString('utf8');
221
+ if (size <= bytes) {
222
+ return tail;
223
+ }
224
+ const firstNewline = tail.indexOf('\n');
225
+ return firstNewline === -1 ? '' : tail.slice(firstNewline + 1);
226
+ } catch {
227
+ return null;
228
+ } finally {
229
+ if (fd !== undefined) {
230
+ try {
231
+ fs.closeSync(fd);
232
+ } catch {
233
+ // ignore
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Read the terminal exit code from the anchored footer at the end of a log.
241
+ * Returns null when the log has no footer yet (command still running, or the
242
+ * host-side watcher has not appended it yet) — never a number parsed out of the
243
+ * command's own output.
244
+ * @param {string} logPath - Path to the log file
245
+ * @returns {number|null}
246
+ */
178
247
  function readExitCodeFromLog(logPath) {
179
248
  if (!logPath) {
180
249
  return null;
181
250
  }
182
- try {
183
- const content = fs.readFileSync(logPath, 'utf8');
184
- const matches = [...content.matchAll(/Exit Code:\s*(-?\d+)/g)];
185
- if (matches.length === 0) {
186
- return null;
187
- }
188
- return parseInt(matches[matches.length - 1][1], 10);
189
- } catch {
251
+ const tail = readLogTail(logPath);
252
+ if (tail === null) {
190
253
  return null;
191
254
  }
255
+ const matches = [...tail.matchAll(LOG_FOOTER_PATTERN)];
256
+ if (matches.length === 0) {
257
+ return null;
258
+ }
259
+ return parseInt(matches[matches.length - 1][1], 10);
192
260
  }
193
261
 
194
262
  /**
@@ -280,15 +348,17 @@ function enrichDetachedStatus(record) {
280
348
  // Otherwise keep the recorded/footer exit code - the command has finished.
281
349
  } else if (!alive && enriched.status === 'executing') {
282
350
  // Session ended but record says executing - correct it. Resolve a real exit
283
- // code: prefer the log footer, then the backend's own record (e.g.
284
- // `docker inspect .State.ExitCode`), then `137` when the only evidence left
285
- // is the OOM observation, and only fall back to the `-1` sentinel as a last
286
- // resort when no real code can be obtained (issues #136, #151).
351
+ // code: prefer the backend's own record (e.g. `docker inspect
352
+ // .State.ExitCode`, which is authoritative and cannot be spoofed by command
353
+ // output — issue #150), then the anchored log footer, then `137` when the
354
+ // only evidence left is the OOM observation, and only fall back to the `-1`
355
+ // sentinel as a last resort when no real code can be obtained (issues #136,
356
+ // #151).
287
357
  enriched.status = 'executed';
288
358
  if (enriched.exitCode === null || enriched.exitCode === undefined) {
289
359
  enriched.exitCode =
290
- footerExit ??
291
360
  backendExitCode(dockerState) ??
361
+ footerExit ??
292
362
  (oomKilled === true ? 137 : -1);
293
363
  }
294
364
  if (!enriched.endTime) {
@@ -638,6 +708,8 @@ module.exports = {
638
708
  listExecutions,
639
709
  isDetachedSessionAlive,
640
710
  enrichDetachedStatus,
711
+ readExitCodeFromLog,
712
+ readLogTail,
641
713
  attachCurrentTime,
642
714
  attachProcessIds,
643
715
  };
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Regression tests for issue #150:
4
+ * "`$ --status` fabricates a detached session exit code from the command's own
5
+ * output (unanchored `Exit Code:` scan over the whole log)"
6
+ *
7
+ * The exit code of a detached session used to be derived from an *unanchored*
8
+ * `Exit Code: N` scan over the *whole* session log. Any text the wrapped
9
+ * command printed that merely contained the substring `Exit Code: N` was
10
+ * indistinguishable from the terminal footer `start` appends itself, so
11
+ * `$ --status` could report an exit code the command never produced.
12
+ *
13
+ * The fix anchors the scan on the three-line footer block `start` writes
14
+ * (separator / `Finished:` / `Exit Code:`) and only reads the tail of the log,
15
+ * where the footer always is.
16
+ *
17
+ * Reference: https://github.com/link-foundation/start/issues/150
18
+ */
19
+
20
+ const { describe, it, expect, beforeEach, afterEach } = require('bun:test');
21
+ const fs = require('fs');
22
+ const os = require('os');
23
+ const path = require('path');
24
+
25
+ const { ExecutionRecord } = require('../src/lib/execution-store');
26
+ const {
27
+ readExitCodeFromLog,
28
+ readLogTail,
29
+ enrichDetachedStatus,
30
+ } = require('../src/lib/status-formatter');
31
+
32
+ const SEPARATOR = '='.repeat(50);
33
+
34
+ function realFooter(exitCode) {
35
+ return `\n${SEPARATOR}\nFinished: 2026-07-30 23:36:20.295\nExit Code: ${exitCode}\n`;
36
+ }
37
+
38
+ describe('anchored log footer parsing (issue #150)', () => {
39
+ let tempDir;
40
+ let logPath;
41
+
42
+ beforeEach(() => {
43
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'start-150-'));
44
+ logPath = path.join(tempDir, 'session.log');
45
+ });
46
+
47
+ afterEach(() => {
48
+ fs.rmSync(tempDir, { recursive: true, force: true });
49
+ });
50
+
51
+ it('ignores an `Exit Code:` substring inside the command output', () => {
52
+ // Exactly the payload from the incident: an older session log dumped by
53
+ // `rg -n` into the JSON output of the running command.
54
+ fs.writeFileSync(
55
+ logPath,
56
+ '{"type":"item.completed","item":{"aggregated_output":' +
57
+ '"40-==================================================\\n41-Finished: 2026-07-28 20:04:52.316\\n42-Exit Code: 1\\n",' +
58
+ '"exit_code":0,"status":"completed"}}\n'
59
+ );
60
+ expect(readExitCodeFromLog(logPath)).toBeNull();
61
+ });
62
+
63
+ it('ignores a bare `Exit Code:` line without the footer block around it', () => {
64
+ fs.writeFileSync(logPath, 'Exit Code: 1\nstill running\n');
65
+ expect(readExitCodeFromLog(logPath)).toBeNull();
66
+ });
67
+
68
+ it('ignores an `Exit Code:` substring in the middle of a line', () => {
69
+ fs.writeFileSync(
70
+ logPath,
71
+ `${SEPARATOR}\nFinished: now\nlog: Exit Code: 3\n`
72
+ );
73
+ expect(readExitCodeFromLog(logPath)).toBeNull();
74
+ });
75
+
76
+ it('reads the real footer that `start` appends', () => {
77
+ fs.writeFileSync(logPath, `hello\n${realFooter(0)}`);
78
+ expect(readExitCodeFromLog(logPath)).toBe(0);
79
+ });
80
+
81
+ it('reads the real footer even when the output forged one earlier', () => {
82
+ fs.writeFileSync(
83
+ logPath,
84
+ `${SEPARATOR}\nFinished: fake\nExit Code: 1\n${realFooter(0)}`
85
+ );
86
+ // The last anchored footer wins - here the genuine one, appended last.
87
+ expect(readExitCodeFromLog(logPath)).toBe(0);
88
+ });
89
+
90
+ it('reads negative and multi-digit codes, and tolerates CRLF logs', () => {
91
+ fs.writeFileSync(
92
+ logPath,
93
+ `${SEPARATOR}\r\nFinished: t\r\nExit Code: 137\r\n`
94
+ );
95
+ expect(readExitCodeFromLog(logPath)).toBe(137);
96
+ fs.writeFileSync(logPath, realFooter(-1));
97
+ expect(readExitCodeFromLog(logPath)).toBe(-1);
98
+ });
99
+
100
+ it('returns null for a log without a footer and for a missing file', () => {
101
+ fs.writeFileSync(logPath, 'still running, no footer yet\n');
102
+ expect(readExitCodeFromLog(logPath)).toBeNull();
103
+ expect(readExitCodeFromLog(path.join(tempDir, 'missing.log'))).toBeNull();
104
+ expect(readExitCodeFromLog(null)).toBeNull();
105
+ });
106
+
107
+ it('finds the footer at the end of a large log without reading it all', () => {
108
+ fs.writeFileSync(
109
+ logPath,
110
+ `${'x'.repeat(2 * 1024 * 1024)}\n${realFooter(42)}`
111
+ );
112
+ expect(readExitCodeFromLog(logPath)).toBe(42);
113
+ // Only the tail is read, so an `Exit Code:` far from the end is invisible.
114
+ const tail = readLogTail(logPath);
115
+ expect(tail.length).toBeLessThan(32 * 1024);
116
+ });
117
+
118
+ it('drops the partial first line of the tail so it cannot be a line start', () => {
119
+ const filler = 'y'.repeat(32 * 1024);
120
+ fs.writeFileSync(
121
+ logPath,
122
+ `${filler}${SEPARATOR}\nFinished: t\nExit Code: 7\n`
123
+ );
124
+ // The separator is a line *continuation* (the line starts with the filler),
125
+ // so it is not a footer even though the tail slice may begin mid-line.
126
+ expect(readExitCodeFromLog(logPath)).toBeNull();
127
+ });
128
+ });
129
+
130
+ describe('detached status is not derailed by forged output (issue #150)', () => {
131
+ let tempDir;
132
+ let logPath;
133
+
134
+ beforeEach(() => {
135
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'start-150-enrich-'));
136
+ logPath = path.join(tempDir, 'session.log');
137
+ });
138
+
139
+ afterEach(() => {
140
+ fs.rmSync(tempDir, { recursive: true, force: true });
141
+ });
142
+
143
+ function makeRecord() {
144
+ return new ExecutionRecord({
145
+ command: 'solve.mjs',
146
+ logPath,
147
+ options: {
148
+ // A container name that cannot be inspected: liveness is unknown, the
149
+ // exact window in which the incident happened.
150
+ sessionName: `issue150-absent-${process.pid}-${Date.now()}`,
151
+ isolated: 'docker',
152
+ isolationMode: 'detached',
153
+ },
154
+ });
155
+ }
156
+
157
+ it('keeps an executing record executing when only the output claims an exit code', () => {
158
+ fs.writeFileSync(
159
+ logPath,
160
+ '{"aggregated_output":"41-Finished: x\\n42-Exit Code: 1\\n","exit_code":0}\n'
161
+ );
162
+ const enriched = enrichDetachedStatus(makeRecord());
163
+ expect(enriched.status).toBe('executing');
164
+ expect(enriched.exitCode).toBeNull();
165
+ expect(enriched.endTime).toBeNull();
166
+ });
167
+
168
+ it('marks the record executed once the genuine footer is appended', () => {
169
+ fs.writeFileSync(
170
+ logPath,
171
+ `{"aggregated_output":"42-Exit Code: 1\\n"}\n${realFooter(0)}`
172
+ );
173
+ const enriched = enrichDetachedStatus(makeRecord());
174
+ expect(enriched.status).toBe('executed');
175
+ expect(enriched.exitCode).toBe(0);
176
+ expect(enriched.endTime).not.toBeNull();
177
+ });
178
+ });
@@ -846,7 +846,12 @@ describe('Issue #151: OOMKilled is an observation, not a verdict', () => {
846
846
  });
847
847
 
848
848
  it('prefers the log footer exit code over the OOM fallback when the container is gone', () => {
849
- fs.writeFileSync(logPath, 'Finished: now\nExit Code: 0\n', 'utf8');
849
+ // The anchored footer block `start` itself writes (issue #150).
850
+ fs.writeFileSync(
851
+ logPath,
852
+ `${'='.repeat(50)}\nFinished: now\nExit Code: 0\n`,
853
+ 'utf8'
854
+ );
850
855
  const record = saveDockerRecord({ oomKilled: true });
851
856
 
852
857
  withFakeDockerMissingContainer(() => {