start-command 0.37.1 → 0.37.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # start-command
2
2
 
3
+ ## 0.37.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 65314a3: Attribute exit 137 to recent Docker OOM events when the last cgroup sample misses the final kill. Preserve daemon restart and ordinary-exit safeguards, apply attributed evidence to recovery memory limits, and recompute each archived attempt's status diagnostics independently.
8
+
3
9
  ## 0.37.1
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-command",
3
- "version": "0.37.1",
3
+ "version": "0.37.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": {
@@ -90,6 +90,9 @@ function archiveAttempt(record) {
90
90
  status: record.status,
91
91
  exitCode: record.exitCode,
92
92
  endTime: record.endTime,
93
+ ...(record.options?.exitEvidence
94
+ ? { exitEvidence: { ...record.options.exitEvidence } }
95
+ : {}),
93
96
  };
94
97
  for (const field of TERMINAL_FIELDS) {
95
98
  if (record[field] !== undefined) {
@@ -142,7 +145,31 @@ function readAttemptLogTail(record, bytes) {
142
145
  return readLogTail(record.logPath, bytes, record.attempt?.logOffset || 0);
143
146
  }
144
147
 
148
+ function enrichAttemptHistory(record, enrich, bytes) {
149
+ return (record.attemptHistory || []).map((attempt, index, history) => {
150
+ const next = history[index + 1];
151
+ const end = next ? next.logOffset : record.attempt?.logOffset;
152
+ const bounded =
153
+ Number.isSafeInteger(attempt.logOffset) &&
154
+ attempt.logOffset >= 0 &&
155
+ Number.isSafeInteger(end) &&
156
+ end >= attempt.logOffset;
157
+ const tail =
158
+ record.logPath && bounded
159
+ ? readLogTail(record.logPath, bytes, attempt.logOffset, end)
160
+ : null;
161
+ const input = {
162
+ ...attempt,
163
+ options: { exitEvidence: attempt.exitEvidence },
164
+ };
165
+ const output = enrich(input, tail);
166
+ delete output.options;
167
+ return output;
168
+ });
169
+ }
170
+
145
171
  module.exports = {
172
+ enrichAttemptHistory,
146
173
  activityPath,
147
174
  appendLifecycle,
148
175
  archiveAttempt,
@@ -323,14 +323,20 @@ function recoverKilledExecution(params = {}) {
323
323
  }
324
324
  }
325
325
  const evidence = require('./exit-evidence');
326
+ const logged = evidence.fromLog(
327
+ record.logPath
328
+ ? require('./execution-attempt').readAttemptLogTail(record, 65536)
329
+ : null
330
+ );
326
331
  const terminalOom =
327
- (params.oomKilled === true || params.oomKilled === 'true') &&
328
- evidence.recentOomDelta(params.cgroupMemory, params.finishedAt) &&
329
- !evidence.fromLog(
330
- record.logPath
331
- ? require('./execution-attempt').readAttemptLogTail(record, 65536)
332
- : null
333
- ).daemonRestart;
332
+ !logged.daemonRestart &&
333
+ (logged.mainOom ||
334
+ ((params.oomKilled === true || params.oomKilled === 'true') &&
335
+ evidence.recentOomDelta(params.cgroupMemory, params.finishedAt)));
336
+ record.options.exitEvidence = {
337
+ mainOom: terminalOom,
338
+ daemonRestart: logged.daemonRestart,
339
+ };
334
340
  let recoveryLimits = null;
335
341
  if (opts.onKillResumeMemory && terminalOom) {
336
342
  try {
@@ -2,6 +2,7 @@
2
2
  const { spawnSync } = require('child_process');
3
3
  const { shellQuote } = require('./isolation-log-utils');
4
4
  const MAIN_OOM = 'Exit evidence: main-oom (recent cgroup oom_kill delta)';
5
+ const DOCKER_OOM = 'Exit evidence: main-oom (recent Docker oom event)';
5
6
  const DAEMON_RESTART =
6
7
  'Exit evidence: docker-daemon-restart (Docker service journal)';
7
8
 
@@ -38,6 +39,22 @@ function daemonRestartEvidence(journal, containerId) {
38
39
  return Boolean(exited && forced);
39
40
  }
40
41
 
42
+ function recentDockerOom(events, finishedAt) {
43
+ const finished = Date.parse(finishedAt) / 1000;
44
+ return (
45
+ Number.isFinite(finished) &&
46
+ String(events || '')
47
+ .split('\n')
48
+ .some((line) => {
49
+ if (!/^\d+$/.test(line.trim())) {
50
+ return false;
51
+ }
52
+ const at = Number(line.trim());
53
+ return at > 0 && finished - at >= -1 && finished - at <= 3;
54
+ })
55
+ );
56
+ }
57
+
41
58
  function collectExitEvidence(
42
59
  { containerName, sample, finishedAt, exitCode, oomKilled },
43
60
  runner
@@ -52,31 +69,56 @@ function collectExitEvidence(
52
69
  const local =
53
70
  !process.env.DOCKER_HOST || process.env.DOCKER_HOST.startsWith('unix://');
54
71
  if (
55
- local &&
56
72
  Number(exitCode) === 137 &&
73
+ containerName &&
57
74
  Number.isFinite(Date.parse(finishedAt))
58
75
  ) {
59
76
  const docker = require('./docker-cleanup').getDockerCommand();
60
77
  const id = run(docker, ['inspect', '-f', '{{.Id}}', containerName]);
61
78
  if (id.success && /^[a-f0-9]{64}$/.test(id.stdout.trim())) {
62
79
  const end = Date.parse(finishedAt);
63
- const journal = run('journalctl', [
64
- '-u',
65
- 'docker.service',
66
- '--since',
67
- new Date(end - 60000).toISOString(),
68
- '--until',
69
- new Date(end + 1000).toISOString(),
70
- '--no-pager',
71
- '-o',
72
- 'cat',
73
- ]);
80
+ const journal = local
81
+ ? run('journalctl', [
82
+ '-u',
83
+ 'docker.service',
84
+ '--since',
85
+ new Date(end - 60000).toISOString(),
86
+ '--until',
87
+ new Date(end + 1000).toISOString(),
88
+ '--no-pager',
89
+ '-o',
90
+ 'cat',
91
+ ])
92
+ : { success: false, stdout: '' };
74
93
  if (
75
94
  journal.success &&
76
95
  daemonRestartEvidence(journal.stdout, id.stdout.trim())
77
96
  ) {
78
97
  return DAEMON_RESTART;
79
98
  }
99
+ // Query the daemon's bounded event history: works with DooD and remote
100
+ // sockets even after the final kill destroys the sampled cgroup.
101
+ const events = run(docker, [
102
+ 'events',
103
+ '--since',
104
+ new Date(end - 60000).toISOString(),
105
+ '--until',
106
+ new Date(end + 1000).toISOString(),
107
+ '--filter',
108
+ `container=${id.stdout.trim()}`,
109
+ '--filter',
110
+ 'event=oom',
111
+ '--format',
112
+ '{{.Time}}',
113
+ ]);
114
+ if (events.success && recentDockerOom(events.stdout, finishedAt)) {
115
+ return DOCKER_OOM;
116
+ }
117
+ if (process.env.START_DEBUG === '1') {
118
+ console.error(
119
+ `[DEBUG] Exit evidence: container=${id.stdout.trim()} finishedAt=${finishedAt} eventsAvailable=${events.success} oomEvents=${JSON.stringify(events.stdout)} sample=${JSON.stringify(sample || '')}`
120
+ );
121
+ }
80
122
  }
81
123
  }
82
124
  if (
@@ -92,17 +134,23 @@ function collectExitEvidence(
92
134
  function fromLog(log) {
93
135
  const lines = String(log || '').split('\n');
94
136
  const last = lines.filter((line) => line.startsWith('Exit evidence:')).at(-1);
95
- return { mainOom: last === MAIN_OOM, daemonRestart: last === DAEMON_RESTART };
137
+ return {
138
+ mainOom: last === MAIN_OOM || last === DOCKER_OOM,
139
+ daemonRestart: last === DAEMON_RESTART,
140
+ };
96
141
  }
97
142
 
98
143
  function buildExitEvidenceSnippet(containerName, quotedLogPath) {
99
- return `${shellQuote(process.execPath)} ${shellQuote(__filename)} ${shellQuote(containerName)} "$__start_command_cgroup" "$__start_command_finished" "$__start_command_exit" "$__start_command_oom" >> ${quotedLogPath} 2>/dev/null`;
144
+ const stderr = process.env.START_DEBUG === '1' ? '2>&1' : '2>/dev/null';
145
+ return `${shellQuote(process.execPath)} ${shellQuote(__filename)} ${shellQuote(containerName)} "$__start_command_cgroup" "$__start_command_finished" "$__start_command_exit" "$__start_command_oom" >> ${quotedLogPath} ${stderr}`;
100
146
  }
101
147
 
102
148
  module.exports = {
103
149
  MAIN_OOM,
150
+ DOCKER_OOM,
104
151
  DAEMON_RESTART,
105
152
  recentOomDelta,
153
+ recentDockerOom,
106
154
  daemonRestartEvidence,
107
155
  collectExitEvidence,
108
156
  fromLog,
@@ -206,6 +206,13 @@ function resolveMemoryExhaustion(input) {
206
206
  memoryExhaustedReason: `cgroup memory.events reported oom_kill=${oomKills}`,
207
207
  };
208
208
  }
209
+ if (isOomKillOfCommand(input)) {
210
+ return {
211
+ memoryExhausted: true,
212
+ memoryExhaustedReason:
213
+ 'Exit-time evidence reported a main-process OOM kill',
214
+ };
215
+ }
209
216
  return null;
210
217
  }
211
218
 
@@ -240,11 +240,16 @@ const FATAL_MARKER_TAIL_BYTES = 64 * 1024;
240
240
  * @param {number} [bytes] - Maximum number of trailing bytes to read
241
241
  * @returns {string|null} Tail content, or null when the file cannot be read
242
242
  */
243
- function readLogTail(logPath, bytes = LOG_TAIL_BYTES, offset = 0) {
243
+ function readLogTail(
244
+ logPath,
245
+ bytes = LOG_TAIL_BYTES,
246
+ offset = 0,
247
+ end = Infinity
248
+ ) {
244
249
  let fd;
245
250
  try {
246
251
  fd = fs.openSync(logPath, 'r');
247
- const size = fs.fstatSync(fd).size;
252
+ const size = Math.min(fs.fstatSync(fd).size, end);
248
253
  const length = Math.min(Math.max(0, size - offset), bytes);
249
254
  const buffer = Buffer.alloc(length);
250
255
  fs.readSync(fd, buffer, 0, length, size - length);
@@ -500,7 +500,8 @@ function attachMemoryExhaustion(record, logTail) {
500
500
  if (!memory) {
501
501
  if (
502
502
  record.memoryExhaustedReason?.startsWith('Docker reported') ||
503
- record.memoryExhaustedReason?.startsWith('cgroup memory.events')
503
+ record.memoryExhaustedReason?.startsWith('cgroup memory.events') ||
504
+ record.memoryExhaustedReason?.startsWith('Exit-time evidence')
504
505
  ) {
505
506
  const cleared = Object.assign(
506
507
  Object.create(Object.getPrototypeOf(record)),
@@ -558,6 +559,14 @@ function enrichDetachedStatus(record) {
558
559
  Object.create(Object.getPrototypeOf(enriched)),
559
560
  enriched
560
561
  );
562
+ if (record.attemptHistory) {
563
+ copy.attemptHistory = require('./execution-attempt').enrichAttemptHistory(
564
+ record,
565
+ (attempt, tail) =>
566
+ attachMemoryExhaustion(attachExitReason(attempt, tail), tail),
567
+ FATAL_MARKER_TAIL_BYTES
568
+ );
569
+ }
561
570
  return readAttemptActivity(copy);
562
571
  }
563
572
 
@@ -97,15 +97,32 @@ it('manual snapshot override applies limits to the new container at creation', a
97
97
  fs.rmSync(f.dir, { recursive: true, force: true });
98
98
  }
99
99
  });
100
- for (const [exitCode, oomKilled, sample, change] of [
100
+ for (const [exitCode, oomKilled, sample, change, marker] of [
101
101
  [137, true, '67108864 66000000 1 1 100 100', true],
102
102
  [137, true, '67108864 66000000 1 1 0 100', false],
103
103
  [137, false, '67108864 66000000 0 0 0 100', false],
104
104
  [1, true, '67108864 66000000 1 1 100 100', false],
105
+ [
106
+ 137,
107
+ true,
108
+ '67108864 66000000 1 1 62 62',
109
+ true,
110
+ 'Exit evidence: main-oom (recent Docker oom event)',
111
+ ],
112
+ [
113
+ 137,
114
+ true,
115
+ '67108864 66000000 1 1 62 62',
116
+ false,
117
+ 'Exit evidence: docker-daemon-restart (Docker service journal)',
118
+ ],
105
119
  ]) {
106
120
  it(`automatic memory change requires a qualifying OOM (exit=${exitCode}, fresh=${change})`, () => {
107
121
  const f = fixture();
108
122
  try {
123
+ if (marker) {
124
+ fs.writeFileSync(f.record.logPath, `${marker}\n`);
125
+ }
109
126
  const result = recoverKilledExecution({
110
127
  store: f.store,
111
128
  executionId: f.record.uuid,
@@ -0,0 +1,240 @@
1
+ const { it, expect } = require('bun:test');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const evidence = require('../src/lib/exit-evidence');
6
+ const {
7
+ resolveExitReason,
8
+ resolveMemoryExhaustion,
9
+ } = require('../src/lib/exit-reason');
10
+ const { enrichDetachedStatus } = require('../src/lib/status-formatter');
11
+ const { archiveAttempt } = require('../src/lib/execution-attempt');
12
+ const { ExecutionRecord } = require('../src/lib/execution-store');
13
+
14
+ const finishedAt = '1970-01-01T00:01:40.592Z';
15
+ const id = 'a'.repeat(64);
16
+ const facts = {
17
+ containerName: 'task',
18
+ sample: '3135369216 3135373312 1 1 62 62',
19
+ finishedAt,
20
+ exitCode: 137,
21
+ oomKilled: true,
22
+ };
23
+ function collect(events, overrides = {}, journal = '') {
24
+ const calls = [];
25
+ const line = evidence.collectExitEvidence(
26
+ { ...facts, ...overrides },
27
+ (cmd, args) => {
28
+ calls.push([cmd, args]);
29
+ if (args[0] === 'inspect') {
30
+ return { success: true, stdout: id };
31
+ }
32
+ if (args[0] === 'events') {
33
+ return { success: true, stdout: events };
34
+ }
35
+ return { success: Boolean(journal), stdout: journal };
36
+ }
37
+ );
38
+ return { line, calls };
39
+ }
40
+
41
+ it('attributes the final Docker OOM event despite a 38-second-old child sample', () => {
42
+ const { line, calls } = collect('62\n99\n');
43
+ expect(line).toBe('Exit evidence: main-oom (recent Docker oom event)');
44
+ const args = calls.find(([, args]) => args[0] === 'events')[1];
45
+ expect(args).toContain(`container=${id}`);
46
+ expect(args).toContain('event=oom');
47
+ expect(args).toContain('1970-01-01T00:00:40.592Z');
48
+ expect(args).toContain('1970-01-01T00:01:41.592Z');
49
+ const input = { exitCode: 137, oomKilled: true, logTail: line };
50
+ expect(resolveExitReason(input)).toBe(
51
+ 'memory-exhaustion (cgroup-oom-killer)'
52
+ );
53
+ expect(resolveMemoryExhaustion(input).memoryExhausted).toBe(true);
54
+ expect(
55
+ resolveMemoryExhaustion({ ...input, oomKilled: false }).memoryExhausted
56
+ ).toBe(true);
57
+ });
58
+
59
+ it('uses daemon events over remote Docker connections without a host journal', () => {
60
+ const previous = process.env.DOCKER_HOST;
61
+ process.env.DOCKER_HOST = 'tcp://remote:2376';
62
+ try {
63
+ const { line, calls } = collect('100');
64
+ expect(evidence.fromLog(line).mainOom).toBe(true);
65
+ expect(calls.some(([cmd]) => cmd === 'journalctl')).toBe(false);
66
+ } finally {
67
+ if (previous === undefined) {
68
+ delete process.env.DOCKER_HOST;
69
+ } else {
70
+ process.env.DOCKER_HOST = previous;
71
+ }
72
+ }
73
+ });
74
+
75
+ it('rejects old, future and malformed events and ordinary exits', () => {
76
+ for (const events of ['62', '96', '102', 'nope', '', '0', 'Infinity']) {
77
+ expect(evidence.fromLog(collect(events).line).mainOom).toBe(false);
78
+ }
79
+ for (const exitCode of [0, 1, 139, -1]) {
80
+ const { line, calls } = collect('99', { exitCode });
81
+ expect(evidence.fromLog(line).mainOom).toBe(false);
82
+ expect(calls.some(([, args]) => args[0] === 'events')).toBe(false);
83
+ }
84
+ });
85
+
86
+ it('keeps daemon restart evidence ahead of even a recent OOM event', () => {
87
+ const journal = `Main process exited\nContainer failed to exit - using the force container=${id}`;
88
+ const { line } = collect('99', {}, journal);
89
+ expect(line).toBe(evidence.DAEMON_RESTART);
90
+ expect(
91
+ resolveMemoryExhaustion({ exitCode: 137, oomKilled: true, logTail: line })
92
+ ).toBeNull();
93
+ });
94
+
95
+ it('archives attributed evidence before a resume clears current options', () => {
96
+ const record = new ExecutionRecord({
97
+ command: 'work',
98
+ status: 'executed',
99
+ options: {
100
+ exitEvidence: { mainOom: true, daemonRestart: false },
101
+ },
102
+ });
103
+ archiveAttempt(record);
104
+ expect(record.attemptHistory[0].exitEvidence).toEqual(
105
+ record.options.exitEvidence
106
+ );
107
+ });
108
+
109
+ it('recomputes history independently while the current attempt is executing', () => {
110
+ const record = new ExecutionRecord({
111
+ command: 'work',
112
+ status: 'executing',
113
+ options: {},
114
+ });
115
+ record.attemptHistory = [
116
+ {
117
+ status: 'executed',
118
+ exitCode: 137,
119
+ oomKilled: true,
120
+ exitReason: 'signal (SIGKILL; cause unknown)',
121
+ exitEvidence: { mainOom: true, daemonRestart: false },
122
+ },
123
+ {
124
+ status: 'executed',
125
+ exitCode: 1,
126
+ oomKilled: true,
127
+ exitReason: 'memory-exhaustion (cgroup-oom-killer)',
128
+ memoryExhausted: true,
129
+ memoryExhaustedReason: 'Docker reported State.OOMKilled=true',
130
+ },
131
+ ];
132
+ const result = enrichDetachedStatus(record);
133
+ expect(result.attemptHistory[0].exitReason).toBe(
134
+ 'memory-exhaustion (cgroup-oom-killer)'
135
+ );
136
+ expect(result.attemptHistory[0].memoryExhausted).toBe(true);
137
+ expect(result.attemptHistory[1].exitReason).toBeUndefined();
138
+ expect(result.attemptHistory[1].memoryExhausted).toBeUndefined();
139
+ expect(record.attemptHistory[0].exitReason).toBe(
140
+ 'signal (SIGKILL; cause unknown)'
141
+ );
142
+ });
143
+
144
+ it('reads only each archived attempt log range and never leaks current fatal output', () => {
145
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'issue-209-'));
146
+ try {
147
+ const first = 'Exit evidence: main-oom (recent Docker oom event)\n';
148
+ const second = 'ordinary exit\n';
149
+ const logPath = path.join(dir, 'run.log');
150
+ fs.writeFileSync(
151
+ logPath,
152
+ `${first + second}FATAL ERROR: JavaScript heap out of memory\n`
153
+ );
154
+ const record = new ExecutionRecord({
155
+ command: 'work',
156
+ status: 'executed',
157
+ logPath,
158
+ options: {},
159
+ });
160
+ record.exitCode = 1;
161
+ record.attempt = {
162
+ number: 3,
163
+ logOffset: Buffer.byteLength(first + second),
164
+ };
165
+ record.attemptHistory = [
166
+ {
167
+ status: 'executed',
168
+ number: 1,
169
+ logOffset: 0,
170
+ exitCode: 137,
171
+ oomKilled: true,
172
+ },
173
+ {
174
+ status: 'executed',
175
+ number: 2,
176
+ logOffset: Buffer.byteLength(first),
177
+ exitCode: 1,
178
+ oomKilled: true,
179
+ },
180
+ ];
181
+ const result = enrichDetachedStatus(record);
182
+ expect(result.attemptHistory[0].exitReason).toBe(
183
+ 'memory-exhaustion (cgroup-oom-killer)'
184
+ );
185
+ expect(result.attemptHistory[1].memoryExhausted).toBeUndefined();
186
+ expect(result.exitReason).toBe('memory-exhaustion (v8-heap-limit)');
187
+ record.attemptHistory[1].logOffset = null;
188
+ expect(enrichDetachedStatus(record).attemptHistory[0].exitReason).toBe(
189
+ 'signal (SIGKILL; cause unknown)'
190
+ );
191
+ } finally {
192
+ fs.rmSync(dir, { recursive: true, force: true });
193
+ }
194
+ });
195
+
196
+ it('finalizes and preserves Docker event attribution after the log is removed', () => {
197
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'issue-209-finalize-'));
198
+ try {
199
+ const { ExecutionStore } = require('../src/lib/execution-store');
200
+ const {
201
+ finalizeDetachedExecution,
202
+ } = require('../src/lib/detached-finalize');
203
+ const store = new ExecutionStore({ appFolder: dir, useLinks: false });
204
+ const logPath = path.join(dir, 'run.log');
205
+ fs.writeFileSync(logPath, `${evidence.DOCKER_OOM}\n`);
206
+ const record = new ExecutionRecord({
207
+ command: 'work',
208
+ logPath,
209
+ options: {},
210
+ });
211
+ store.save(record);
212
+ finalizeDetachedExecution({
213
+ store,
214
+ executionId: record.uuid,
215
+ ...facts,
216
+ running: false,
217
+ });
218
+ const current = store.get(record.uuid);
219
+ expect(current.options.exitEvidence.mainOom).toBe(true);
220
+ expect(current.exitReason).toBe('memory-exhaustion (cgroup-oom-killer)');
221
+ const stored = current.options.exitEvidence;
222
+ current.options.exitEvidence = null;
223
+ expect(enrichDetachedStatus(current).memoryExhausted).toBe(true);
224
+ current.options.exitEvidence = { mainOom: false, daemonRestart: false };
225
+ expect(enrichDetachedStatus(current).memoryExhausted).toBeUndefined();
226
+ current.options.exitEvidence = stored;
227
+ const message =
228
+ require('../src/lib/docker-cleanup').buildAttachedDockerKeptMessage({
229
+ containerName: 'task',
230
+ exitCode: 137,
231
+ oomKilled: true,
232
+ logPath,
233
+ });
234
+ expect(message).toContain('reports it was OOM-killed');
235
+ fs.unlinkSync(logPath);
236
+ expect(enrichDetachedStatus(current).memoryExhausted).toBe(true);
237
+ } finally {
238
+ fs.rmSync(dir, { recursive: true, force: true });
239
+ }
240
+ });
@@ -5,12 +5,36 @@ const { resolve } = require('node:path');
5
5
  const { parseArgs } = require('../src/lib/args-parser');
6
6
  const { buildDockerRuntimeArgs } = require('../src/lib/docker-runtime-args');
7
7
  const { ExecutionRecord } = require('../src/lib/execution-store');
8
- const { formatRecord } = require('../src/lib/status-formatter');
8
+ const {
9
+ formatRecord,
10
+ enrichDetachedStatus,
11
+ } = require('../src/lib/status-formatter');
9
12
  const fixtures = JSON.parse(
10
13
  readFileSync(resolve(__dirname, '../../parity/fixtures/contracts.json'))
11
14
  );
12
15
 
13
16
  describe('shared JavaScript and Rust golden contracts', () => {
17
+ for (const fixture of fixtures['attempt-status-output']) {
18
+ it(`attempt diagnostics: ${fixture.name}`, () => {
19
+ const record = ExecutionRecord.fromObject(fixture.record);
20
+ const output = JSON.parse(
21
+ formatRecord(enrichDetachedStatus(record), 'json')
22
+ );
23
+ assert.deepEqual(output.attemptHistory, fixture.attemptHistory);
24
+ assert.deepEqual(record.toObject(), fixture.record);
25
+ });
26
+ }
27
+ for (const fixture of fixtures['docker-oom-event']) {
28
+ it(`Docker OOM event: ${fixture.name}`, () => {
29
+ assert.equal(
30
+ require('../src/lib/exit-evidence').recentDockerOom(
31
+ fixture.events,
32
+ fixture.finishedAt
33
+ ),
34
+ fixture.recent
35
+ );
36
+ });
37
+ }
14
38
  for (const fixture of fixtures['cli-parsing']) {
15
39
  it(`CLI parsing: ${fixture.name}`, () => {
16
40
  if (fixture.error) {