@cat-factory/executor-harness 1.145.1 → 1.147.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.
@@ -6,9 +6,10 @@
6
6
  import { execFile } from 'node:child_process'
7
7
  import { promisify } from 'node:util'
8
8
  import type { AgentInfraSpec, InfraSetupRecord, ServiceInfraSpec } from './job.js'
9
+ import type { DockerWorkload } from './docker-capability.js'
9
10
  import {
10
11
  type DockerProbe,
11
- probeDockerServing,
12
+ probeLiveDockerCapability,
12
13
  readDockerStatus,
13
14
  resolveDockerVerdict,
14
15
  } from './docker-status.js'
@@ -26,19 +27,22 @@ const exec = promisify(execFile)
26
27
  * still run unit-level tests and report what it could. A no-op for ephemeral / no-infra /
27
28
  * no-compose-path runs.
28
29
  *
29
- * A CONFIRMED absence of a Docker daemon short-circuits it: the container's own probe
30
- * ({@link readDockerStatus}, recorded by `entrypoint.sh`) already knows there is nothing to
31
- * talk to, so running compose against it would only turn a fact this container holds into a
32
- * connection error the agent has to interpret. The record then carries `dockerAvailable: false`
33
- * and the stated cause, which is what makes the Tester step say why it ran no infra instead of
34
- * looking like a Tester that simply chose not to. Anything OTHER than a confirmed absence
35
- * attempts as before (`DockerStatus.available` in docker-status.ts states why "undecided" is its
36
- * own value).
30
+ * A CONFIRMED absence of a USABLE Docker daemon short-circuits it: the container already knows
31
+ * compose cannot work, so running it would only turn a fact this container holds into an error
32
+ * the agent has to interpret. The record then carries the stated cause plus the two facts that
33
+ * decide where a human should look (`dockerAvailable`: was anything answering, `dockerWorkload`:
34
+ * what a container did on it), which is what makes the Tester step say why it ran no infra
35
+ * instead of looking like a Tester that simply chose not to. Anything OTHER than a confirmed
36
+ * negative attempts as before (`DockerStatus.available` in docker-status.ts states why
37
+ * "undecided" is its own value).
37
38
  *
38
- * "Confirmed", not merely recorded: {@link resolveDockerVerdict} re-checks a recorded absence
39
- * against a live daemon first, so a warm-pool container whose sidecar came up late is not
40
- * latched into refusing infra that works. `probe` is that check, injected so the unit suite can
41
- * state both answers on a machine that has its own daemon either way.
39
+ * "Confirmed", not merely recorded: {@link resolveDockerVerdict} re-checks the boot record
40
+ * against a live daemon first, so a warm-pool container whose sidecar came up late is not latched
41
+ * into refusing infra that works. It re-checks a recorded PRESENCE too, by running an actual
42
+ * container: a rootless daemon in a sandbox answers `docker version` while being unable to mount
43
+ * an image, and compose against that one died on a mount error inside the very mechanism that
44
+ * exists to explain why infra did not come up. `probe` is that check, injected so the unit suite
45
+ * can state every answer on a machine that has its own daemon either way.
42
46
  *
43
47
  * Whether it succeeds or fails, the (redacted, bounded) command output is captured into a
44
48
  * {@link InfraSetupRecord} returned alongside the prompt `note`, so the backend can surface
@@ -54,27 +58,43 @@ export async function standUpInfra(
54
58
  infra: ServiceInfraSpec,
55
59
  signal: AbortSignal | undefined,
56
60
  logger: Logger,
57
- probe: DockerProbe = probeDockerServing,
61
+ probe: DockerProbe = probeLiveDockerCapability,
58
62
  ): Promise<{ started: boolean; note?: string; record?: InfraSetupRecord }> {
59
63
  if (infra.environment !== 'local' || infra.noInfraDependencies || !infra.composePath) {
60
64
  return { started: false }
61
65
  }
62
66
  const startedAt = Date.now()
63
67
  const recorded = await readDockerStatus()
64
- const docker = await resolveDockerVerdict(recorded, probe)
68
+ const docker = await resolveDockerVerdict(recorded, {
69
+ probe,
70
+ ...(signal ? { signal } : {}),
71
+ logger,
72
+ })
65
73
  if (docker.refusal) {
66
74
  const note = `the dependencies could not be started: ${docker.refusal}`
67
- logger.warn('agent(explore): infra stand-up refused, no docker daemon', {
75
+ logger.warn('agent(explore): infra stand-up refused, no usable docker daemon', {
68
76
  composePath: infra.composePath,
69
77
  dockerSource: recorded.source,
70
78
  dockerReason: recorded.reason,
79
+ // What the LIVE check found, which is the only place the second refusal cause exists: the
80
+ // boot record's own words for a daemon that answers and cannot run anything are `serving`
81
+ // and nothing else, so a log line carrying the record alone describes the wrong failure.
82
+ dockerWorkload: docker.workload?.status ?? 'unmeasured',
83
+ ...(docker.workload?.status === 'unusable' ? { dockerDetail: docker.workload.detail } : {}),
71
84
  })
72
85
  return {
73
86
  started: false,
74
87
  note,
75
88
  record: {
76
89
  started: false,
77
- dockerAvailable: false,
90
+ // NOT a flat `false`. Two refusals reach this branch and they have opposite fixes: with
91
+ // nothing answering, the executor image or the sandbox running it is what to go and look
92
+ // at; with a daemon that answers and cannot run a container, that daemon is up and an
93
+ // operator sent to restart it finds nothing wrong. `dockerAvailable` answers only the
94
+ // first question and `dockerWorkload` the second, so neither has to carry the other's
95
+ // fact (the same rule the compose-failure branch below states for its own `false`).
96
+ dockerAvailable: docker.daemon === true,
97
+ ...workloadRecord(docker.workload),
78
98
  composePath: infra.composePath,
79
99
  at: Date.now(),
80
100
  durationMs: Date.now() - startedAt,
@@ -97,6 +117,7 @@ export async function standUpInfra(
97
117
  record: {
98
118
  started: true,
99
119
  dockerAvailable: true,
120
+ ...workloadRecord(docker.workload),
100
121
  composePath: infra.composePath,
101
122
  at: Date.now(),
102
123
  durationMs: Date.now() - startedAt,
@@ -116,12 +137,13 @@ export async function standUpInfra(
116
137
  note,
117
138
  record: {
118
139
  started: false,
119
- // A compose failure with a REACHABLE daemon: the two `false`s above and here are
120
- // different diagnoses (nothing to talk to vs the stack itself did not come up), and
121
- // only stating both keeps the second from being read as the first. Read off the
122
- // RESOLVED verdict, so a container whose daemon came up after boot claims the daemon it
123
- // actually reached rather than the one its boot record still denies.
124
- ...(docker.available === true ? { dockerAvailable: true } : {}),
140
+ // A compose failure with a REACHABLE daemon, which is a third diagnosis again: the stack
141
+ // itself did not come up. Read off the RESOLVED verdict, so a container whose daemon came
142
+ // up after boot claims the daemon it actually reached rather than the one its boot record
143
+ // still denies, and OMITTED rather than `false` when nothing answered the live check,
144
+ // because the boot record's word for that is a hypothesis and not a measurement.
145
+ ...(docker.daemon === true ? { dockerAvailable: true } : {}),
146
+ ...workloadRecord(docker.workload),
125
147
  composePath: infra.composePath,
126
148
  at: Date.now(),
127
149
  durationMs: Date.now() - startedAt,
@@ -132,6 +154,22 @@ export async function standUpInfra(
132
154
  }
133
155
  }
134
156
 
157
+ /**
158
+ * What the live check measured, for the record the Tester step shows.
159
+ *
160
+ * Its own field beside `dockerAvailable` because the two answer different questions and only one
161
+ * of them has a boolean's worth of answers: a daemon either answered or it did not, while what a
162
+ * container DID on it is `usable`, `unusable`, or a check that could not be carried out. Absent
163
+ * when nothing was measured at all (an undecided boot record probes nothing), which is not the
164
+ * same as a check that ran and could not tell.
165
+ */
166
+ function workloadRecord(workload: DockerWorkload | undefined): {
167
+ dockerWorkload?: InfraSetupRecord['dockerWorkload']
168
+ } {
169
+ if (!workload) return {}
170
+ return { dockerWorkload: workload.status === 'unknown' ? 'undetermined' : workload.status }
171
+ }
172
+
135
173
  /**
136
174
  * Stand the run's infra up and return a single cleanup handle, dispatching on the spec's
137
175
  * `kind`: the frontend UI-test flow (`kind: 'frontend'`) builds/serves the app + WireMock as
package/src/job.ts CHANGED
@@ -955,6 +955,16 @@ export interface InfraSetupRecord {
955
955
  * mistake that let a daemon-less image read as an ordinary infra failure for months.
956
956
  */
957
957
  dockerAvailable?: boolean
958
+ /**
959
+ * What a real container DID on that daemon, when the platform measured it.
960
+ *
961
+ * The third diagnosis, and the one `dockerAvailable` structurally cannot carry: a rootless
962
+ * daemon nested in a sandbox answers throughout while unable to mount any image layer, so it is
963
+ * `dockerAvailable: true` and no stack can come up on it (issue #2120). Reporting that as an
964
+ * absent daemon sends a human to restart one that is already up. `undetermined` is a check that
965
+ * ran and could not tell; ABSENT means nothing was measured at all.
966
+ */
967
+ dockerWorkload?: 'usable' | 'unusable' | 'undetermined'
958
968
  /** The repo-relative compose file that was stood up. */
959
969
  composePath?: string
960
970
  /** Epoch ms the stand-up attempt finished. */