@celilo/e2e 0.20.3 → 0.20.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.
@@ -17,7 +17,7 @@
17
17
  # generated HERE, at docker-build time, because dpkg-scanpackages /
18
18
  # apt-ftparchive aren't available on the macOS build host.
19
19
 
20
- FROM debian:trixie-slim
20
+ FROM debian:trixie-slim@sha256:a99cfc517144bc59b1978475ec53b46ecabec7e43635402ee5b77cc54cd1b20a
21
21
 
22
22
  RUN apt-get update \
23
23
  && apt-get install -y --no-install-recommends dpkg-dev apt-utils python3 \
@@ -1,4 +1,4 @@
1
- FROM cznic/knot
1
+ FROM cznic/knot@sha256:166fd8956f1a69eeb6558c28f65cedb4ea112d4689317f4a50c2f2787ee6a622
2
2
 
3
3
  RUN apt-get update && apt-get install -y \
4
4
  iproute2 \
@@ -1,4 +1,4 @@
1
- FROM debian:bookworm-slim
1
+ FROM debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251
2
2
 
3
3
  # Caddy + tools needed by entrypoint.sh. iproute2 provides `ip` so the
4
4
  # entrypoint can rewrite the default route through fw-ext (same pattern
@@ -13,7 +13,7 @@
13
13
  #
14
14
  # No systemd: sshd + apache are all this host runs, so a two-process entrypoint
15
15
  # is enough and boots in a fraction of the time.
16
- FROM ubuntu:22.04
16
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
17
17
 
18
18
  ENV DEBIAN_FRONTEND=noninteractive
19
19
 
@@ -1,4 +1,4 @@
1
- FROM ubuntu:22.04
1
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
2
2
 
3
3
  ENV DEBIAN_FRONTEND=noninteractive
4
4
 
@@ -1,4 +1,4 @@
1
- FROM ubuntu:22.04
1
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
2
2
 
3
3
  # Make apt resilient to flaky upstream (no apt-cacher-ng masking transient errors)
4
4
  COPY config/apt/99retries.conf /etc/apt/apt.conf.d/99retries.conf
@@ -1,4 +1,4 @@
1
- FROM ubuntu:22.04
1
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
2
2
 
3
3
  RUN apt-get update && apt-get install -y \
4
4
  squid-openssl \
@@ -1,4 +1,4 @@
1
- FROM oven/bun:latest
1
+ FROM oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895
2
2
 
3
3
  RUN apt-get update && apt-get install -y \
4
4
  openssh-server \
@@ -1,4 +1,4 @@
1
- FROM ubuntu:22.04
1
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
2
2
 
3
3
  RUN apt-get update && apt-get install -y \
4
4
  unbound \
@@ -1,4 +1,4 @@
1
- FROM oven/bun:latest
1
+ FROM oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895
2
2
 
3
3
  # iproute2 so the entrypoint can see its own addressing when debugging; the
4
4
  # service itself needs no routing changes (it sits on internet-external with
@@ -1,4 +1,4 @@
1
- FROM oven/bun:latest
1
+ FROM oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895
2
2
 
3
3
  # Caddy (for ACME/TLS termination) + tools needed by entrypoint.sh.
4
4
  # iproute2 provides `ip` so the entrypoint can rewrite the default route
@@ -1,4 +1,4 @@
1
- FROM oven/bun:latest
1
+ FROM oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895
2
2
 
3
3
  # This Dockerfile produces a *vanilla* management image — bun + unzip +
4
4
  # bunfig, no celilo. `cele2e build-infra` runs install.sh inside a
@@ -17,14 +17,14 @@
17
17
  # well-known minioadmin/minioadmin; the migration test passes them to
18
18
  # `celilo storage add s3 --access-key-id ... --secret-access-key ...`.
19
19
 
20
- FROM minio/mc:latest AS mc
20
+ FROM quay.io/minio/mc:latest@sha256:a7fe349ef4bd8521fb8497f55c6042871b2ae640607cf99d9bede5e9bdf11727 AS mc
21
21
 
22
22
  # Static busybox purely for its `ip` applet: the MinIO image has no iproute2
23
23
  # and no package manager worth invoking, but it still needs the ISP route
24
24
  # every host on internet-external carries (see minio-startup.sh).
25
- FROM busybox:stable-musl AS busybox
25
+ FROM busybox:stable-musl@sha256:3c6ae8008e2c2eedd141725c30b20d9c36b026eb796688f88205845ef17aa213 AS busybox
26
26
 
27
- FROM minio/minio:latest
27
+ FROM quay.io/minio/minio:latest@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e
28
28
 
29
29
  COPY --from=busybox /bin/busybox /usr/local/bin/busybox
30
30
 
@@ -1,4 +1,4 @@
1
- FROM cznic/knot
1
+ FROM cznic/knot@sha256:166fd8956f1a69eeb6558c28f65cedb4ea112d4689317f4a50c2f2787ee6a622
2
2
 
3
3
  RUN apt-get update && apt-get install -y \
4
4
  iproute2 \
@@ -1,4 +1,4 @@
1
- FROM oven/bun:1
1
+ FROM oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895
2
2
 
3
3
  WORKDIR /app
4
4
 
@@ -6,7 +6,7 @@
6
6
  # Its faithfulness as a vantage comes from the routing profile installed at startup
7
7
  # (see config/routing/observer-setup.sh), driven by the OBSERVER_* env the compose
8
8
  # generator sets per vantage.
9
- FROM ubuntu:22.04
9
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
10
10
 
11
11
  ENV DEBIAN_FRONTEND=noninteractive
12
12
 
@@ -1,6 +1,6 @@
1
- FROM ghcr.io/letsencrypt/pebble:latest AS pebble
1
+ FROM ghcr.io/letsencrypt/pebble:latest@sha256:ddf230642b1a584f519f32e347de1b05a6e4c1f6c35c1863b33effeab5f78199 AS pebble
2
2
 
3
- FROM ubuntu:22.04
3
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
4
4
 
5
5
  RUN apt-get update && apt-get install -y \
6
6
  iproute2 \
@@ -1,4 +1,4 @@
1
- FROM oven/bun:1
1
+ FROM oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895
2
2
 
3
3
  WORKDIR /app
4
4
 
@@ -1,4 +1,4 @@
1
- FROM oven/bun:1
1
+ FROM oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895
2
2
 
3
3
  WORKDIR /app
4
4
 
@@ -1,4 +1,4 @@
1
- FROM ubuntu:22.04
1
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
2
2
 
3
3
  RUN apt-get update && apt-get install -y \
4
4
  unbound \
@@ -1,4 +1,4 @@
1
- FROM ubuntu:22.04
1
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
2
2
 
3
3
  RUN apt-get update && apt-get install -y \
4
4
  iproute2 \
@@ -24,7 +24,7 @@
24
24
 
25
25
  ARG SIGNAL_CLI_VERSION=0.14.6
26
26
 
27
- FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS fetch
27
+ FROM --platform=$BUILDPLATFORM debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251 AS fetch
28
28
  ARG SIGNAL_CLI_VERSION
29
29
  RUN apt-get update \
30
30
  && apt-get install -y --no-install-recommends curl ca-certificates \
@@ -44,7 +44,7 @@ RUN curl -fsSL \
44
44
  && mkdir -p /out/schemas \
45
45
  && tar -xzf /tmp/schemas.tar.gz -C /out/schemas
46
46
 
47
- FROM --platform=linux/amd64 eclipse-temurin:25-jre
47
+ FROM --platform=linux/amd64 eclipse-temurin:25-jre@sha256:bb036ed6cfdc57e3da7c22634d15f1b840d2caf76183861c80e81ca4b5104abb
48
48
  COPY --from=fetch /out/signal-cli /opt/signal-cli
49
49
  COPY --from=fetch /out/schemas /opt/signal-cli-schemas
50
50
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  ARG SIGNAL_CLI_VERSION=0.14.6
15
15
 
16
- FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS fetch
16
+ FROM --platform=$BUILDPLATFORM debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251 AS fetch
17
17
  ARG SIGNAL_CLI_VERSION
18
18
  RUN apt-get update \
19
19
  && apt-get install -y --no-install-recommends curl ca-certificates \
@@ -31,7 +31,7 @@ RUN mkdir -p "/out/v${SIGNAL_CLI_VERSION}" \
31
31
  "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}.tar.gz" \
32
32
  -o "/out/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}.tar.gz"
33
33
 
34
- FROM nginx:alpine
34
+ FROM nginx:alpine@sha256:62ff2089abf5a9ed33bd232895bef5e22f7bb4b200675cec49a5ebc48e3d4ac8
35
35
  COPY --from=fetch /out /usr/share/nginx/html
36
36
 
37
37
  # Route the customer's public prefix via the ISP edge, like every other host on
@@ -6,7 +6,7 @@
6
6
  # arrives: it can queue an inbound reply, fail a recipient inside a success,
7
7
  # and revoke the device link on demand — none of which the real unlinked
8
8
  # daemon can be made to do.
9
- FROM oven/bun:1
9
+ FROM oven/bun:1.4.2@sha256:9114c058aeae42162ee16dd5084b95fe9473970bb6bcb5b232ab1630f0546895
10
10
 
11
11
  WORKDIR /app
12
12
  COPY simulators/signal-cli/server.ts ./server.ts
@@ -11,7 +11,7 @@
11
11
  # internet (split-horizon, namecheap publication, etc.)
12
12
  # The startup script handles both based on the VANTAGE env var passed
13
13
  # at `docker run` time.
14
- FROM ubuntu:22.04
14
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
15
15
 
16
16
  RUN apt-get update && apt-get install -y \
17
17
  dante-server \
@@ -1,4 +1,4 @@
1
- FROM ubuntu:22.04
1
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
2
2
 
3
3
  ENV DEBIAN_FRONTEND=noninteractive
4
4
 
@@ -2,7 +2,7 @@
2
2
  # Used for app-zone machines that run Docker Compose workloads (e.g. authentik).
3
3
  # Docker itself is baked in; application images are pulled at deploy time by each module.
4
4
 
5
- FROM ubuntu:22.04
5
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
6
6
 
7
7
  ENV DEBIAN_FRONTEND=noninteractive
8
8
 
@@ -2,7 +2,7 @@
2
2
  # Based on target-machine; Technitium is installed at image build time so the
3
3
  # Ansible role finds it already present and skips the download step.
4
4
 
5
- FROM ubuntu:22.04
5
+ FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02
6
6
 
7
7
  ENV DEBIAN_FRONTEND=noninteractive
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/e2e",
3
- "version": "0.20.3",
3
+ "version": "0.20.5",
4
4
  "description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -38,9 +38,9 @@
38
38
  "README.md"
39
39
  ],
40
40
  "dependencies": {
41
- "@celilo/capabilities": "^5.0.0",
41
+ "@celilo/capabilities": "^5.1.1",
42
42
  "@celilo/cli-display": "^0.2.0",
43
- "@celilo/event-bus": "^0.6.0",
43
+ "@celilo/event-bus": "^0.7.0",
44
44
  "@celilo/terraform-fake": "^0.3.1",
45
45
  "yaml": "^2.8.0",
46
46
  "zod": "^3.24.1"
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # Authenticate the builder to Docker Hub before any e2e image build.
3
+ #
4
+ # celilo#1376: the e2e base images are digest-pinned, which makes a pull
5
+ # REPRODUCIBLE — it does not make it ALLOWED. Docker Hub meters anonymous pulls
6
+ # per source IP, and once the builder's share is spent every pull fails at
7
+ # `load metadata` with:
8
+ #
9
+ # ERROR: pull access denied ... insufficient_scope: authorization failed
10
+ #
11
+ # which reads like a missing image rather than a spent quota. An authenticated
12
+ # pull draws on the account's much larger allowance instead.
13
+ #
14
+ # No credentials configured (a fork, a local run) => stay anonymous and say so,
15
+ # rather than failing the build for a quota that may well not be spent.
16
+ set -eu
17
+
18
+ if [ -z "${DOCKERHUB_USERNAME:-}" ] || [ -z "${DOCKERHUB_TOKEN:-}" ]; then
19
+ echo "DOCKERHUB_USERNAME/DOCKERHUB_TOKEN unset — pulling Docker Hub anonymously."
20
+ echo "Anonymous pulls share this host's IP quota; a spent quota fails as"
21
+ echo "'insufficient_scope: authorization failed' at 'load metadata' (celilo#1376)."
22
+ exit 0
23
+ fi
24
+
25
+ printf '%s' "${DOCKERHUB_TOKEN}" |
26
+ docker login --username "${DOCKERHUB_USERNAME}" --password-stdin
@@ -47,6 +47,7 @@ function healthyProbe(overrides: Partial<DoctorProbe> = {}): DoctorProbe {
47
47
  imageExists: () => true,
48
48
  imageCmd: () => [EXPECTED_MANAGEMENT_CMD],
49
49
  imageLabel: () => null,
50
+ celiloVersion: () => '1.2.3',
50
51
  staleContainers: () => [],
51
52
  reclaimableImageBytes: () => 0,
52
53
  leakedStacks: () => [],
@@ -103,6 +104,22 @@ describe('management image (problem 1: the bake fails as an SSH error)', () => {
103
104
  expect(check.remedy).toBe('cele2e build-infra');
104
105
  });
105
106
 
107
+ test('a hollow image — starts fine, no celilo inside — FAILS', () => {
108
+ // celilo#1390: doctor said "2 baked images present, both running
109
+ // /startup.sh" about an image with no celilo binary at all.
110
+ const check = checkManagementImage(healthyProbe({ celiloVersion: () => null }));
111
+ expect(check.status).toBe('fail');
112
+ expect(check.detail).toContain('HOLLOW');
113
+ expect(check.detail).toContain('127');
114
+ expect(check.remedy).toBe('cele2e build-infra');
115
+ });
116
+
117
+ test('a passing check reports the version it found, so a stale bake is visible too', () => {
118
+ const check = checkManagementImage(healthyProbe({ celiloVersion: () => '0.42.1' }));
119
+ expect(check.status).toBe('ok');
120
+ expect(check.detail).toContain('0.42.1');
121
+ });
122
+
106
123
  test('both images present and correctly baked passes', () => {
107
124
  const check = checkManagementImage(healthyProbe());
108
125
  expect(check.status).toBe('ok');
package/src/doctor.ts CHANGED
@@ -105,6 +105,12 @@ export interface DoctorProbe {
105
105
  imageCmd(ref: string): string[] | null;
106
106
  /** One label off the image, or null when the image or the label is absent. */
107
107
  imageLabel(ref: string, label: string): string | null;
108
+ /**
109
+ * `celilo --version` run INSIDE the image, or null when the binary is absent
110
+ * or does not run. This is the only check that opens the image up; every
111
+ * other one reasons about metadata, which is how a hollow image passed.
112
+ */
113
+ celiloVersion(ref: string): string | null;
108
114
  /** Names of leftover `celilo-e2e-*` containers. */
109
115
  staleContainers(): string[];
110
116
  /** Bytes docker reports as reclaimable image space. */
@@ -228,10 +234,24 @@ export function checkManagementImage(probe: DoctorProbe): DoctorCheck {
228
234
  remedy: 'cele2e build-infra',
229
235
  };
230
236
  }
237
+ // The contract every stage depends on: a celilo binary that actually runs.
238
+ // A failed bake (celilo#1365, the install.sh fetch timing out) leaves the
239
+ // image present, correctly Cmd'd and completely hollow, and the suite then
240
+ // dies at stage 1 with `celilo --version exited 127` — which reads as a test
241
+ // defect rather than the bake failure it is.
242
+ const version = probe.celiloVersion(MANAGEMENT_LATEST);
243
+ if (!version) {
244
+ return {
245
+ name: 'management-image',
246
+ status: 'fail',
247
+ detail: `${MANAGEMENT_LATEST} is HOLLOW — it starts, but \`celilo\` is not on PATH inside it, so every stage dies with "celilo --version exited 127: command not found". The bake failed and left the image behind`,
248
+ remedy: 'cele2e build-infra',
249
+ };
250
+ }
231
251
  return {
232
252
  name: 'management-image',
233
253
  status: 'ok',
234
- detail: `${BAKED_MANAGEMENT_IMAGES.length} baked images present, both running ${EXPECTED_MANAGEMENT_CMD}`,
254
+ detail: `${BAKED_MANAGEMENT_IMAGES.length} baked images present, both running ${EXPECTED_MANAGEMENT_CMD}; celilo ${version} inside ${MANAGEMENT_LATEST}`,
235
255
  };
236
256
  }
237
257
 
@@ -565,6 +585,22 @@ export function createDockerProbe(): DoctorProbe {
565
585
  return null;
566
586
  }
567
587
  },
588
+ celiloVersion(ref) {
589
+ try {
590
+ const out = docker([
591
+ 'run',
592
+ '--rm',
593
+ '--entrypoint',
594
+ 'sh',
595
+ ref,
596
+ '-c',
597
+ 'command -v celilo >/dev/null 2>&1 && celilo --version',
598
+ ]);
599
+ return out === '' ? null : out.split('\n').pop()?.trim() || null;
600
+ } catch {
601
+ return null;
602
+ }
603
+ },
568
604
  staleContainers() {
569
605
  try {
570
606
  return docker(['ps', '-a', '--filter', 'name=celilo-e2e', '--format', '{{.Names}}'])
@@ -113,6 +113,42 @@ describe('evaluateHostVm', () => {
113
113
  expect(evaluateHostVm(vm({ cpus: 16 }), M1_PRO, budget).problems[0]).toContain('16 CPUs');
114
114
  });
115
115
 
116
+ test('half the policy CPUs is flagged — the measured case that read as a match', () => {
117
+ const verdict = evaluateHostVm(vm({ cpus: 4 }), M1_PRO, budget);
118
+ expect(verdict.problems).toHaveLength(1);
119
+ expect(verdict.problems[0]).toContain('4 CPUs');
120
+ // colima resizes CPUs on a restart, so this must not demand the destructive path.
121
+ expect(verdict.needsRecreate).toBe(false);
122
+ });
123
+
124
+ test('memory below the policy is flagged', () => {
125
+ const verdict = evaluateHostVm(vm({ memoryGiB: 4 }), M1_PRO, budget);
126
+ expect(verdict.problems).toHaveLength(1);
127
+ expect(verdict.problems[0]).toContain('4 GiB');
128
+ expect(verdict.needsRecreate).toBe(false);
129
+ });
130
+
131
+ /**
132
+ * The property that actually failed in celilo#1395: `budget.cpus` was
133
+ * computed, printed inside another rule's message, and never compared. A
134
+ * per-rule test cannot express that — it can only break rules that exist.
135
+ * So drive every budget field away from the budget and require a sentence
136
+ * back. A field used only in a template string produces none.
137
+ */
138
+ test('every field of the budget is compared, not merely printed', () => {
139
+ const diverge: Record<keyof typeof budget, Partial<HostVmFacts>> = {
140
+ cpus: { cpus: budget.cpus - 1 },
141
+ memoryGiB: { memoryGiB: budget.memoryGiB - 1 },
142
+ mountType: { mountType: 'sshfs' },
143
+ };
144
+ for (const [field, override] of Object.entries(diverge)) {
145
+ expect(
146
+ evaluateHostVm(vm(override), M1_PRO, budget).problems,
147
+ `budget.${field} diverged but no rule fired`,
148
+ ).not.toEqual([]);
149
+ }
150
+ });
151
+
116
152
  test('several problems are all reported, not just the first', () => {
117
153
  const verdict = evaluateHostVm(vm({ memoryGiB: 24, mountType: 'sshfs', cpus: 16 }), M1_PRO, {
118
154
  ...budget,
package/src/host-vm.ts CHANGED
@@ -221,10 +221,20 @@ export function evaluateHostVm(
221
221
  // colima discards a mount-type change on an existing VM.
222
222
  needsRecreate = true;
223
223
  }
224
+ if (facts.memoryGiB < budget.memoryGiB) {
225
+ problems.push(
226
+ `VM memory is ${facts.memoryGiB} GiB, below the ${budget.memoryGiB} GiB this host can afford it — the guest has less page cache than the rig's images need, so reads that should be cached hit the disk.`,
227
+ );
228
+ }
224
229
  if (facts.cpus > host.cpus) {
225
230
  problems.push(
226
231
  `VM is configured with ${facts.cpus} CPUs but the host has ${host.cpus}. Recommended: ${budget.cpus}.`,
227
232
  );
233
+ } else if (facts.cpus < budget.cpus) {
234
+ // colima resizes CPUs on a restart, so this does NOT need a recreate.
235
+ problems.push(
236
+ `VM has ${facts.cpus} CPUs against a policy of ${budget.cpus} on a ${host.cpus}-core host — a suite runs on the cores the VM was given, not the ones the host has. Recommended: ${budget.cpus}.`,
237
+ );
228
238
  }
229
239
  return { problems, needsRecreate };
230
240
  }