start-command 0.43.0 → 0.44.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.
@@ -34,6 +34,7 @@ const ResumeAllAction = {
34
34
  RUNNING: 'running',
35
35
  RECONCILED: 'reconciled',
36
36
  UNKNOWN: 'unknown',
37
+ PAUSED: 'paused',
37
38
  };
38
39
 
39
40
  function formatResumeAllAsLinksNotation(summary) {
@@ -137,6 +138,19 @@ function resumeAllExecutions(store, deps = {}) {
137
138
  continue;
138
139
  }
139
140
 
141
+ if (probe.state === SessionState.PAUSED) {
142
+ // Neither restarted nor re-watched while frozen (issue #263).
143
+ executions.push(
144
+ describeRecord(
145
+ record,
146
+ probe,
147
+ ResumeAllAction.PAUSED,
148
+ `Skipped paused container: ${sessionName}. Use \`$ --unpause ${record.uuid}\` to continue it.`
149
+ )
150
+ );
151
+ continue;
152
+ }
153
+
140
154
  if (probe.alive && probe.backend === 'docker') {
141
155
  startWatcher(
142
156
  sessionName,
@@ -43,7 +43,11 @@ const {
43
43
  rollbackLaunch,
44
44
  failLaunchedPersistence,
45
45
  } = require('./launch-persistence');
46
- const { SessionState, probeSession } = require('./session-probe');
46
+ const {
47
+ SessionState,
48
+ pausedSessionError,
49
+ probeSession,
50
+ } = require('./session-probe');
47
51
  const { getDockerNetworks } = require('./docker-network-lifecycle');
48
52
  const {
49
53
  writeCommandHandoff,
@@ -217,6 +221,10 @@ function buildResumePlan(
217
221
  return { error: 'Only detached isolated executions can be resumed.' };
218
222
  }
219
223
 
224
+ if (probe.state === SessionState.PAUSED) {
225
+ return pausedSessionError('resume', record.uuid, sessionName);
226
+ }
227
+
220
228
  if (probe.alive) {
221
229
  return {
222
230
  error:
@@ -481,6 +489,13 @@ async function resumeExecution(store, identifier, deps = {}) {
481
489
  const runner = deps.runner || runCommand;
482
490
  const probeFn = deps.probe || ((r) => probeSession(r, runner));
483
491
  const probe = probeFn(record);
492
+ if (probe.state === SessionState.PAUSED) {
493
+ // Refused before any snapshot or resource change (issue #263).
494
+ return {
495
+ success: false,
496
+ ...pausedSessionError('resume', record.uuid, probe.sessionName),
497
+ };
498
+ }
484
499
  const opts = record.options || {};
485
500
  let resources;
486
501
  try {
@@ -663,7 +663,11 @@ class ExecutionStore {
663
663
  const staleRecords = [];
664
664
 
665
665
  for (const record of executingRecords) {
666
- if (require('./launch-owner').hasActiveLaunch(record)) {
666
+ if (
667
+ require('./launch-owner').hasActiveLaunch(record) ||
668
+ require('./execution-pause').isConfirmedPaused(record)
669
+ ) {
670
+ // A paused container is alive, just frozen (issue #263).
667
671
  continue;
668
672
  }
669
673
  let isStale = false;
@@ -13,9 +13,10 @@
13
13
  * keeps the rest of its line (issue #249, sensitive-data-sanitizer#45); the
14
14
  * pre-pass that did this here is gone (issue #253).
15
15
  *
16
- * The package depends on ^0.14.0, the current release flagged by the
17
- * dependency freshness check; the Rust installer and its version check
18
- * require the same release (rust/src/lib/log_sanitizer.rs).
16
+ * The package stays on ^0.14.0: 0.20.0 and later drop the bytes after a
17
+ * credential that is followed by a NUL byte (sensitive-data-sanitizer#180,
18
+ * the blocker in .github/dependency-blockers.json). The Rust installer and its
19
+ * version check require the same release (rust/src/lib/log_sanitizer.rs).
19
20
  */
20
21
 
21
22
  const fs = require('fs');
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Every option in this module addresses an execution that already exists in
5
5
  * the tracking store instead of launching a new one: --status, --list,
6
- * --upload-log, --stop, --terminate, --attach, --resume, --resume-all and
6
+ * --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume,
7
+ * --resume-all and
7
8
  * --cleanup, plus --alias and --rename. `dispatchQueryCommand` is the single
8
9
  * entry point the CLI calls before it starts interpreting the command line as
9
10
  * a command to run.
@@ -54,6 +55,28 @@ function handleControlQuery(store, identifier, action) {
54
55
  return reportResult(controlExecution(store, identifier, action));
55
56
  }
56
57
 
58
+ /**
59
+ * Handle --pause / --unpause (issue #263).
60
+ * @param {?object} store - Execution store
61
+ * @param {string} identifier - UUID or session name
62
+ * @param {string} action - pause or unpause
63
+ * @param {object} [options] - {outputFormat, pauseReason, pausedBy}
64
+ * @returns {number} Exit code
65
+ */
66
+ function handlePauseQuery(store, identifier, action, options = {}) {
67
+ const pause = require('./execution-pause');
68
+ const deps = {
69
+ outputFormat: options.outputFormat,
70
+ reason: options.pauseReason || null,
71
+ by: options.pausedBy || null,
72
+ };
73
+ return reportResult(
74
+ action === pause.PauseAction.PAUSE
75
+ ? pause.pauseExecution(store, identifier, deps)
76
+ : pause.unpauseExecution(store, identifier, deps)
77
+ );
78
+ }
79
+
57
80
  function handleAttachQuery(store, identifier, readOnly) {
58
81
  return reportResult(attachExecution(store, identifier, { readOnly }));
59
82
  }
@@ -155,6 +178,8 @@ function hasQueryMode(options) {
155
178
  hasValue(options.uploadLog) ||
156
179
  hasValue(options.stop) ||
157
180
  hasValue(options.terminate) ||
181
+ hasValue(options.pause) ||
182
+ hasValue(options.unpause) ||
158
183
  hasValue(options.attach) ||
159
184
  hasValue(options.resume) ||
160
185
  options.resumeAll === true ||
@@ -193,6 +218,12 @@ async function dispatchQueryCommand(options, context = {}) {
193
218
  ControlAction.TERMINATE
194
219
  );
195
220
  }
221
+ if (hasValue(options.pause)) {
222
+ return handlePauseQuery(store, options.pause, 'pause', options);
223
+ }
224
+ if (hasValue(options.unpause)) {
225
+ return handlePauseQuery(store, options.unpause, 'unpause', options);
226
+ }
196
227
  if (hasValue(options.attach)) {
197
228
  return handleAttachQuery(store, options.attach, options.readOnly === true);
198
229
  }
@@ -234,6 +265,7 @@ module.exports = {
234
265
  handleCleanup,
235
266
  handleControlQuery,
236
267
  handleListQuery,
268
+ handlePauseQuery,
237
269
  handleResumeAllQuery,
238
270
  handleResumeQuery,
239
271
  handleStatusQuery,
@@ -13,12 +13,16 @@ const { getDockerCommand } = require('./docker-cleanup');
13
13
  /**
14
14
  * States a tracked isolation session can be observed in.
15
15
  * - RUNNING: the session/container is alive and can be attached to
16
+ * - PAUSED: the container is frozen by `docker pause` (issue #263); it is
17
+ * alive (its process and memory are kept) but can be neither attached to nor
18
+ * resumed until `$ --unpause` thaws it
16
19
  * - STOPPED: the container still exists but is not running (resumable)
17
20
  * - MISSING: no trace of the session/container remains
18
21
  * - UNKNOWN: the backend cannot be probed locally (e.g. ssh)
19
22
  */
20
23
  const SessionState = {
21
24
  RUNNING: 'running',
25
+ PAUSED: 'paused',
22
26
  STOPPED: 'stopped',
23
27
  MISSING: 'missing',
24
28
  UNKNOWN: 'unknown',
@@ -39,6 +43,11 @@ function mapDockerStatusToState(status) {
39
43
  if (normalized === 'running' || normalized === 'restarting') {
40
44
  return SessionState.RUNNING;
41
45
  }
46
+ if (normalized === 'paused') {
47
+ // Paused is alive, not stopped: reading it as stopped made `--resume`
48
+ // plan a `docker start`/snapshot of a frozen container (issue #263).
49
+ return SessionState.PAUSED;
50
+ }
42
51
  return SessionState.STOPPED;
43
52
  }
44
53
 
@@ -109,13 +118,30 @@ function probeSession(record, runner = runCommand) {
109
118
  probe.state = result.success ? SessionState.RUNNING : SessionState.MISSING;
110
119
  }
111
120
 
112
- probe.alive = probe.state === SessionState.RUNNING;
121
+ probe.alive =
122
+ probe.state === SessionState.RUNNING || probe.state === SessionState.PAUSED;
113
123
  return probe;
114
124
  }
115
125
 
126
+ /**
127
+ * Refusal for a control that cannot act on a paused container (issue #263).
128
+ * @param {string} action - What was refused, e.g. "attach to" or "resume"
129
+ * @param {string} uuid - Execution UUID, for the suggested command
130
+ * @param {string} sessionName - Container name
131
+ * @returns {{error: string}} Error result
132
+ */
133
+ function pausedSessionError(action, uuid, sessionName) {
134
+ return {
135
+ error:
136
+ `Session "${sessionName}" is paused (frozen by docker pause), so start cannot ${action} it. ` +
137
+ `Use \`$ --unpause ${uuid}\` first to continue the same process, or \`$ --stop ${uuid}\` to end it.`,
138
+ };
139
+ }
140
+
116
141
  module.exports = {
117
142
  SessionState,
118
143
  mapDockerStatusToState,
119
144
  parseScreenSessionState,
145
+ pausedSessionError,
120
146
  probeSession,
121
147
  };
@@ -33,6 +33,7 @@ const {
33
33
  persistContainerRemoval,
34
34
  } = require('./container-removal');
35
35
  const { withDockerInspectBatch } = require('./docker-inspect-batch');
36
+ const pause = require('./execution-pause');
36
37
  const {
37
38
  backendExitCode,
38
39
  dockerProbeSpawnOptions,
@@ -460,8 +461,11 @@ function enrichDetachedStatus(record) {
460
461
  ? readAttemptLogTail(record, FATAL_MARKER_TAIL_BYTES)
461
462
  : null;
462
463
  const probe = dockerProbe();
463
- const resolved = attachContainerRemoval(
464
- resolveDetachedStatus(record, logTail, probe),
464
+ const resolved = pause.attachPauseState(
465
+ attachContainerRemoval(
466
+ resolveDetachedStatus(record, logTail, probe),
467
+ probe
468
+ ),
465
469
  probe
466
470
  );
467
471
  const enriched = attachMemoryExhaustion(
@@ -664,6 +668,7 @@ function formatRecordAsText(record) {
664
668
  if (obj.staleDetectedAt) {
665
669
  lines.push(`Stale Detected At: ${obj.staleDetectedAt}`);
666
670
  }
671
+ lines.push(...pause.pauseTextLines(obj));
667
672
  if (obj.containerState) {
668
673
  // The container no longer exists; `containerId` is history (issue #225).
669
674
  lines.push(`Container State: ${obj.containerState}`);
@@ -821,6 +826,7 @@ function queryStatus(store, identifier, outputFormat) {
821
826
  () => {
822
827
  const enrichedRecord = enrichDetachedStatus(record);
823
828
  persistContainerRemoval(store, record, enrichedRecord);
829
+ pause.persistPauseReconciliation(store, enrichedRecord);
824
830
  return attachProcessIds(enrichedRecord);
825
831
  }
826
832
  );
@@ -856,7 +862,11 @@ function listExecutions(store, outputFormat, options = {}) {
856
862
  // One docker inspect for every listed container (issue #243).
857
863
  const enriched = sortRecordsByStartTimeDesc(
858
864
  withDockerInspectBatch(all, dockerProbe(), () =>
859
- all.map(prepareRecordForList)
865
+ all.map((record) => {
866
+ const prepared = prepareRecordForList(record);
867
+ pause.persistPauseReconciliation(store, prepared);
868
+ return prepared;
869
+ })
860
870
  )
861
871
  );
862
872
  // --running reports the reconciled status, not the stored one: a record
@@ -29,7 +29,8 @@ const {
29
29
  *
30
30
  * @param {string} sessionName - Container name
31
31
  * @param {Object|null} [probe] - Receives `sessionNotFound` (issue #225)
32
- * @returns {{running: boolean, exitCode: number|null, oomKilled: boolean|null}|null} State,
32
+ * @returns {{running: boolean, exitCode: number|null, oomKilled: boolean|null,
33
+ * status: ?string, paused: boolean}|null} State,
33
34
  * or null when the container cannot be inspected (not found yet, removed, or docker error)
34
35
  */
35
36
  function inspectDockerState(sessionName, probe = null) {
@@ -37,7 +38,8 @@ function inspectDockerState(sessionName, probe = null) {
37
38
  getDockerCommand(),
38
39
  dockerProbeSpawnOptions(),
39
40
  '{{.State.Running}} {{.State.ExitCode}} {{.State.OOMKilled}} ' +
40
- '{{.State.StartedAt}} {{.State.FinishedAt}}',
41
+ '{{.State.StartedAt}} {{.State.FinishedAt}} ' +
42
+ '{{.State.Status}} {{.State.Paused}}',
41
43
  sessionName
42
44
  );
43
45
  if (probe) {
@@ -50,9 +52,15 @@ function inspectDockerState(sessionName, probe = null) {
50
52
  }
51
53
  // Parsed positionally and defensively: an older docker (or a stubbed one)
52
54
  // may answer with fewer fields than the template asks for.
53
- const [runningRaw, exitRaw, oomKilledRaw, startedRaw, finishedRaw] = stdout
54
- .trim()
55
- .split(/\s+/);
55
+ const [
56
+ runningRaw,
57
+ exitRaw,
58
+ oomKilledRaw,
59
+ startedRaw,
60
+ finishedRaw,
61
+ statusRaw,
62
+ pausedRaw,
63
+ ] = stdout.trim().split(/\s+/);
56
64
  const exitCode = Number.parseInt(exitRaw, 10);
57
65
  return {
58
66
  running: runningRaw === 'true',
@@ -61,6 +69,9 @@ function inspectDockerState(sessionName, probe = null) {
61
69
  oomKilledRaw === 'true' ? true : oomKilledRaw === 'false' ? false : null,
62
70
  startedAt: normalizeDockerTimestamp(startedRaw),
63
71
  finishedAt: normalizeDockerTimestamp(finishedRaw),
72
+ // A paused container is still `.State.Running` (issue #263).
73
+ status: statusRaw || null,
74
+ paused: pausedRaw === 'true' || statusRaw === 'paused',
64
75
  };
65
76
  }
66
77
 
@@ -99,6 +110,9 @@ function readDockerState(record, probe = null) {
99
110
  ) {
100
111
  return null;
101
112
  }
113
+ if (probe) {
114
+ probe.dockerState = state;
115
+ }
102
116
  return state;
103
117
  }
104
118
 
package/src/lib/usage.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Print usage information */
2
2
  function printUsage() {
3
- console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--running] [--output-format <fmt>] | $ --upload-log <id> | $ --stop <id> | $ --terminate <id> | $ --attach <id> [--read-only] | $ --resume <id> [-- <command>] | $ --resume-all | $ --alias <id> <name> | $ --rename <id> <name>
3
+ console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--running] [--output-format <fmt>] | $ --upload-log <id> | $ --stop <id> | $ --terminate <id> | $ --pause <id> | $ --unpause <id> | $ --attach <id> [--read-only] | $ --resume <id> [-- <command>] | $ --resume-all | $ --alias <id> <name> | $ --rename <id> <name>
4
4
 
5
5
  Options:
6
6
  --help, -h Show this usage and exit successfully
@@ -54,6 +54,11 @@ Options:
54
54
  --no-sanitize With --upload-log, explicitly upload without secret redaction
55
55
  --stop <id> Ask a detached isolated execution to stop gracefully
56
56
  --terminate <id> Terminate a detached isolated execution immediately
57
+ --pause <id> Freeze a detached Docker execution (docker pause): CPU is
58
+ released, memory stays allocated (--stop frees it)
59
+ --pause-reason <text> With --pause, record why the execution was frozen
60
+ --paused-by <name> With --pause, record who froze the execution
61
+ --unpause <id> Thaw a paused Docker execution; the same process continues
57
62
  --attach <id> Attach to a running detached isolated execution
58
63
  --read-only With --attach, follow output without sending input
59
64
  --resume <id> Restart a stopped detached execution in the same environment
@@ -93,6 +98,8 @@ Examples:
93
98
  $ --upload-log my-screen-session # Upload stored execution log
94
99
  $ --stop my-screen-session # Ask detached execution to stop gracefully
95
100
  $ --terminate my-screen-session # Terminate detached execution immediately
101
+ $ --pause my-docker-session --pause-reason 'rate limit' # Freeze, keep RAM
102
+ $ --unpause my-docker-session # Continue the same frozen process
96
103
  $ --list --running # Only executions that are still running
97
104
  $ --attach my-docker-session # Re-enter a running detached session
98
105
  $ --attach my-docker-session --read-only # Follow its output without stdin
@@ -142,7 +142,7 @@ describe('query mode exclusivity', () => {
142
142
  test(`rejects ${args.join(' ')}`, () => {
143
143
  assert.throws(() => {
144
144
  parse(args);
145
- }, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
145
+ }, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
146
146
  });
147
147
  }
148
148
  });
@@ -54,19 +54,19 @@ describe('control options', () => {
54
54
  it('should reject combining query and control modes', () => {
55
55
  assert.throws(() => {
56
56
  parseArgs(['--status', 'uuid-here', '--stop', 'my-session']);
57
- }, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
57
+ }, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
58
58
  });
59
59
 
60
60
  it('should reject combining upload-log with control modes', () => {
61
61
  assert.throws(() => {
62
62
  parseArgs(['--upload-log', 'uuid-here', '--terminate', 'my-session']);
63
- }, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
63
+ }, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
64
64
  });
65
65
 
66
66
  it('should reject output-format with control modes', () => {
67
67
  assert.throws(() => {
68
68
  parseArgs(['--stop', 'my-session', '--output-format', 'json']);
69
- }, /--output-format option is only valid with --status, --list, --resume, or --resume-all/);
69
+ }, /--output-format option is only valid with --status, --list, --resume, --resume-all, --pause, or --unpause/);
70
70
  });
71
71
 
72
72
  it('should default control options to null', () => {
@@ -859,7 +859,7 @@ describe('output-format option', () => {
859
859
  it('should throw error for output-format without status', () => {
860
860
  assert.throws(() => {
861
861
  parseArgs(['--output-format', 'json', '--', 'npm', 'test']);
862
- }, /--output-format option is only valid with --status, --list, --resume, or --resume-all/);
862
+ }, /--output-format option is only valid with --status, --list, --resume, --resume-all, --pause, or --unpause/);
863
863
  });
864
864
 
865
865
  it('should accept all valid output formats', () => {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Issue #263: runs one `pause-control` shared golden case
3
+ * (parity/fixtures/contracts.json) through the JavaScript `--pause` /
4
+ * `--unpause` control path. rust/tests/shared_golden.rs runs the same cases
5
+ * through the Rust path, so both runtimes are held to identical reports,
6
+ * errors, docker argv and persisted records.
7
+ *
8
+ * The in-memory docker follows the rules spelled out in
9
+ * `pause-control.docker` of the fixture file; keep it in step with
10
+ * `GoldenDocker` in rust/tests/shared_golden.rs.
11
+ */
12
+
13
+ const TIMESTAMP =
14
+ /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})/g;
15
+
16
+ /** Replace every ISO-8601 timestamp, which differs per run, with a marker. */
17
+ function normalize(value) {
18
+ if (typeof value === 'string') {
19
+ return value.replace(TIMESTAMP, '<timestamp>');
20
+ }
21
+ if (Array.isArray(value)) {
22
+ return value.map(normalize);
23
+ }
24
+ if (value && typeof value === 'object') {
25
+ return Object.fromEntries(
26
+ Object.entries(value).map(([key, entry]) => [key, normalize(entry)])
27
+ );
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function goldenDocker(initial, name) {
33
+ const docker = { state: initial, calls: [] };
34
+ const ok = (stdout = '') => ({
35
+ success: true,
36
+ stdout,
37
+ stderr: '',
38
+ status: 0,
39
+ error: null,
40
+ });
41
+ const fail = (stderr) => ({
42
+ success: false,
43
+ stdout: '',
44
+ stderr,
45
+ status: 1,
46
+ error: null,
47
+ });
48
+ docker.runner = (command, args, extra = {}) => {
49
+ docker.calls.push({ args: [...args], bounded: Boolean(extra.timeout) });
50
+ const verb = args[0];
51
+ if (docker.state === 'removed' && verb !== 'rm') {
52
+ return fail(`Error response from daemon: No such container: ${name}`);
53
+ }
54
+ const alive = docker.state === 'running' || docker.state === 'paused';
55
+ switch (verb) {
56
+ case 'inspect': {
57
+ const format = args[args.indexOf('-f') + 1];
58
+ if (format === '{{.State.Status}} {{.State.Paused}}') {
59
+ return ok(`${docker.state} ${docker.state === 'paused'}\n`);
60
+ }
61
+ if (format === '{{.State.Status}}') {
62
+ return ok(`${docker.state}\n`);
63
+ }
64
+ if (format === '{{.Id}} {{.State.Pid}}') {
65
+ return ok(`abc263def ${alive ? 4242 : 0}\n`);
66
+ }
67
+ return ok('');
68
+ }
69
+ case 'pause':
70
+ if (docker.state !== 'running') {
71
+ return fail(`container ${name} is not running`);
72
+ }
73
+ docker.state = 'paused';
74
+ return ok(`${name}\n`);
75
+ case 'unpause':
76
+ if (docker.state !== 'paused') {
77
+ return fail(`container ${name} is not paused`);
78
+ }
79
+ docker.state = 'running';
80
+ return ok(`${name}\n`);
81
+ default:
82
+ return ok('');
83
+ }
84
+ };
85
+ return docker;
86
+ }
87
+
88
+ /**
89
+ * Run one case and return its normalized observable result.
90
+ * @param {{ExecutionRecord: Function, pause: object}} modules - Code under test
91
+ * @param {object} store - An empty ExecutionStore
92
+ * @param {object} section - `pause-control` from the contracts file
93
+ * @param {object} fixture - One of `section.cases`
94
+ * @returns {object} The value `fixture.expected` must equal
95
+ */
96
+ function runPauseCase({ ExecutionRecord, pause }, store, section, fixture) {
97
+ const base = section.record;
98
+ store.save(
99
+ ExecutionRecord.fromObject({
100
+ ...base,
101
+ status: fixture.status || base.status,
102
+ exitCode:
103
+ fixture.exitCode === undefined ? base.exitCode : fixture.exitCode,
104
+ options: { ...base.options, ...(fixture.options || {}) },
105
+ })
106
+ );
107
+ const docker = goldenDocker(fixture.docker, base.options.sessionName);
108
+ const deps = {
109
+ runner: docker.runner,
110
+ outputFormat: fixture.outputFormat,
111
+ reason: fixture.reason,
112
+ by: fixture.by,
113
+ };
114
+ const result =
115
+ fixture.action === 'pause'
116
+ ? pause.pauseExecution(store, fixture.identifier, deps)
117
+ : pause.unpauseExecution(store, fixture.identifier, deps);
118
+ const saved = store.get(base.uuid).toObject();
119
+ let output = result.output === undefined ? null : result.output;
120
+ if (output !== null && fixture.outputFormat === 'json') {
121
+ output = JSON.parse(output);
122
+ }
123
+ return normalize({
124
+ success: result.success,
125
+ output,
126
+ error: result.error === undefined ? null : result.error,
127
+ dockerCalls: docker.calls.map((call) => call.args),
128
+ unboundedDockerCalls: docker.calls
129
+ .filter((call) => !call.bounded)
130
+ .map((call) => call.args),
131
+ dockerState: docker.state,
132
+ record: {
133
+ status: saved.status,
134
+ exitCode: saved.exitCode === undefined ? null : saved.exitCode,
135
+ containerState:
136
+ saved.containerState === undefined ? null : saved.containerState,
137
+ options: saved.options,
138
+ },
139
+ });
140
+ }
141
+
142
+ module.exports = { normalize, goldenDocker, runPauseCase };
@@ -82,6 +82,9 @@ const CONTAINERS = [
82
82
  '.State.OOMKilled': String(oom),
83
83
  '.State.StartedAt': startedAt || '0001-01-01T00:00:00Z',
84
84
  '.State.FinishedAt': finishedAt || '0001-01-01T00:00:00Z',
85
+ // Paused state for --status/--list (issue #263).
86
+ '.State.Status': running ? 'running' : 'exited',
87
+ '.State.Paused': 'false',
85
88
  }));
86
89
 
87
90
  /**
@@ -290,7 +293,7 @@ posix('issue #243: one docker inspect per query, same output', () => {
290
293
  });
291
294
 
292
295
  describe('issue #243: batched inspect answers', () => {
293
- const row = (name, id) => `/${name} ${id} 7 true 0 false s f`;
296
+ const row = (name, id) => `/${name} ${id} 7 true 0 false s f running false`;
294
297
 
295
298
  it('matches lines by name or ID prefix, not by order', () => {
296
299
  const answers = parseBatchInspect(