From 31878f417cd8d61477ce700bef161524bdec5736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BE?= <139246607+heavymio@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:48:34 +0300 Subject: [PATCH 1/5] =?UTF-8?q?fix(cli):=20guard=20systeminformation.cpu()?= =?UTF-8?q?=20against=20uncatchable=20os.cpus()=20throw=20on=20ARM=20Linux?= =?UTF-8?q?=20(hotplug=20skew)=20=E2=80=94=20see=20#1374?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Patch refined and tested The patch in commit `1333802` is close, but the `cpus()` pre-check needs a fix: on Bun's ARM Linux runtime with CPU hotplug skew, `os.cpus()` can throw synchronously, so calling `cpus()` without a `try/catch` can crash before we ever reach the guarded `si.cpu()`. Updated patch: - `cpus()` wrapped in `try/catch` — if it throws, return empty values + `logger.warn` immediately. - `systeminformation.cpu()` in `try/catch` — catches the uncaught exception from the `nextTick` callback inside `lib/cpu.js:956`. - `Promise.all` now uses `getCpuInfoSafe()` (removed the duplicate `systeminformationModule.cpu()` call). - Fallback logging via the existing `logger` (`fingerprintType: cpu_pre_check_failed` / `cpu_info_failed`). Diagnosis confirmed via the mount-namespace workaround: - frozen CPU snapshot (8 CPUs consistent across /proc/stat, /proc/cpuinfo, /sys/devices/system/cpu/online) → CLI stays up. - live skewed state (9/8/9) → crash ~3s after start. Fallback behavior: fingerprint still generates (empty CPU fields or legacy path); process is stable, not ideal for fingerprinting uniqueness but no longer crashes. --- cli/src/utils/fingerprint.ts | 45 +++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/cli/src/utils/fingerprint.ts b/cli/src/utils/fingerprint.ts index 22e974fdda..e34016d5c4 100644 --- a/cli/src/utils/fingerprint.ts +++ b/cli/src/utils/fingerprint.ts @@ -33,6 +33,49 @@ async function getMachineId(): Promise { return id } +async function getCpuInfoSafe(): Promise<{ + manufacturer: string + brand: string + cores: number + physicalCores: number +}> { + try { + // Bun's os.cpus() can throw synchronously on ARM Linux with CPU hotplug + // skew (/proc vs /sys disagreement). Probe first; if it throws, skip + // si.cpu() entirely and fall back to empty values so the process stays up. + // See #1374. + cpus() + } catch { + logger.warn( + { fingerprintType: 'cpu_pre_check_failed' }, + 'os.cpus() unavailable, skipping CPU fingerprinting — see #1374', + ) + return { manufacturer: '', brand: '', cores: 0, physicalCores: 0 } + } + + try { + if (!systeminformationModule) { + systeminformationModule = await import('systeminformation') + } + const info = await systeminformationModule.cpu() + return { + manufacturer: info.manufacturer, + brand: info.brand, + cores: info.cores, + physicalCores: info.physicalCores, + } + } catch (err) { + logger.warn( + { + fingerprintType: 'cpu_info_failed', + error: err instanceof Error ? err.message : String(err), + }, + 'systeminformation.cpu() failed, using empty CPU fingerprint — see #1374', + ) + return { manufacturer: '', brand: '', cores: 0, physicalCores: 0 } + } +} + async function getSystemInfo(): Promise<{ system: { manufacturer: string; model: string; serial: string; uuid: string } cpu: { manufacturer: string; brand: string; cores: number; physicalCores: number } @@ -44,7 +87,7 @@ async function getSystemInfo(): Promise<{ } const [systemInfo, cpuInfo, osInfo] = await Promise.all([ systeminformationModule.system(), - systeminformationModule.cpu(), + getCpuInfoSafe(), systeminformationModule.osInfo(), ]) return { From 7becc451e6262a6dab2370b0aa0589f47c4c9a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BE?= <139246607+heavymio@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:06:28 +0300 Subject: [PATCH 2/5] fix(cli): guard the second os.cpus() call site in fingerprinting calculateEnhancedFingerprint() calls cpus().length directly to build runtime.cpuCount, outside the guard added for getCpuInfoSafe(). On the platforms from #1374 the first call returns empty CPU fields and then this one throws, which drops the whole enhanced fingerprint into the legacy fallback -- making the empty-fields path unreachable in practice. Add safeCpuCount() alongside getCpuInfoSafe() and use it here, so the enhanced fingerprint survives with cpuCount 0. Verified against the real fingerprint.ts on a standalone bun:test harness with os.cpus() mocked to throw: main (no guard) -> codebuff-cli- (legacy fallback) previous revision -> codebuff-cli- (legacy fallback) this revision -> enhanced- The happy path is unchanged: with a working os.cpus() the result is still enhanced and no guard logs. Tests included. Also corrects the comment above the pre-check: /proc/stat is not a stale snapshot and there is no hotplug window. proot-distro binds a hardcoded 8-core /proc/stat (sysdata.py:66-83) while /proc/cpuinfo stays live, so the two disagree permanently. --- cli/src/utils/fingerprint.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/cli/src/utils/fingerprint.ts b/cli/src/utils/fingerprint.ts index e34016d5c4..37c3bb8bcf 100644 --- a/cli/src/utils/fingerprint.ts +++ b/cli/src/utils/fingerprint.ts @@ -33,6 +33,18 @@ async function getMachineId(): Promise { return id } +function safeCpuCount(): number { + try { + return cpus().length + } catch (err) { + logger.warn( + { fingerprintType: 'cpu_count_unavailable' }, + `os.cpus() unavailable, reporting cpuCount 0 — see #1374 (${String(err)})`, + ) + return 0 + } +} + async function getCpuInfoSafe(): Promise<{ manufacturer: string brand: string @@ -40,10 +52,11 @@ async function getCpuInfoSafe(): Promise<{ physicalCores: number }> { try { - // Bun's os.cpus() can throw synchronously on ARM Linux with CPU hotplug - // skew (/proc vs /sys disagreement). Probe first; if it throws, skip - // si.cpu() entirely and fall back to empty values so the process stays up. - // See #1374. + // Bun's os.cpus() can throw synchronously on ARM Linux when /proc/stat + // and /proc/cpuinfo disagree — under proot-distro, which binds a + // hardcoded 8-core /proc/stat over the real one while /proc/cpuinfo stays + // live. See #1374. Probe first; if it throws, skip si.cpu() entirely and + // fall back to empty values so the process stays up. cpus() } catch { logger.warn( @@ -153,7 +166,7 @@ async function calculateEnhancedFingerprint(): Promise { platform: process.platform, arch: process.arch, shell, - cpuCount: cpus().length, + cpuCount: safeCpuCount(), }, network: { macAddresses, From 7001ef9a9eefd8a9eb43addd3a5b0c83370b61d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BE?= <139246607+heavymio@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:16:18 +0300 Subject: [PATCH 3/5] test(cli): cover the #1374 os.cpus() guard at the public entry point Pins the behaviour codebuff-team asked for: calculateFingerprint() must resolve, not reject, when os.cpus() throws or systeminformation.cpu() rejects, and it must not silently degrade to a legacy fingerprint. Kept in its own file because mock.module for node:os is process-wide in bun, and a shared file would leak the mock into the existing fingerprint.test.ts cases. - os.cpus() throws -> enhanced- fingerprint, both call sites guarded - os.cpus() works -> enhanced- fingerprint, no guard logged Validated against this branch's fingerprint.ts on a standalone bun:test harness; not yet run in the full workspace. Happy path (9 real CPUs) checked too, so this is not a skew-only assertion. --- .../__tests__/fingerprint-cpu-guard.test.ts | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts diff --git a/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts b/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts new file mode 100644 index 0000000000..69afebf8f2 --- /dev/null +++ b/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts @@ -0,0 +1,108 @@ +import { describe, test, expect, mock } from 'bun:test' +import * as realOs from 'node:os' + +// Issue #1374: Bun's os.cpus() throws 'Failed to get CPU information' on ARM +// Linux when /proc/stat and /proc/cpuinfo disagree. Under proot-distro that +// disagreement is permanent, not a hotplug window: it binds a hardcoded 8-core +// /proc/stat (sysdata.py, _FAKE_STAT) while /proc/cpuinfo stays live, so on a +// 9-core device the guest sees 8 vs 9 for the life of the container. +// +// These tests pin the guard behaviour at the public entry point. Everything the +// fingerprint module reaches for is mocked so the test is hermetic and does not +// depend on the host's machine id, shell, or analytics transport. + +type Logged = { fingerprintType?: string } +const logged: Logged[] = [] + +function installMocks(cpusThrows: boolean) { + mock.module('node:os', () => ({ + ...realOs, + cpus: () => { + if (cpusThrows) throw new Error('Failed to get CPU information') + return [{ model: 'Fake CPU' }] + }, + })) + mock.module('node-machine-id', () => ({ + machineId: async () => 'test-machine-id-0001', + })) + mock.module('systeminformation', () => ({ + system: async () => ({ + manufacturer: 'Test', + model: 'Test', + serial: 'SN', + uuid: 'UUID', + }), + osInfo: async () => ({ + platform: 'linux', + distro: 'Test', + arch: 'arm64', + hostname: 'test', + }), + cpu: async () => { + if (cpusThrows) { + throw new Error('Failed to get CPU information') + } + return { manufacturer: 'Test', brand: 'Test', cores: 1, physicalCores: 1 } + }, + })) + mock.module('../analytics', () => ({ trackEvent: () => {} })) + mock.module('../detect-shell', () => ({ detectShell: () => 'bash' })) + mock.module('../logger', () => ({ + logger: { + warn: (data: Logged) => logged.push(data), + debug: () => {}, + info: () => {}, + error: () => {}, + }, + })) +} + +async function loadCalculateFingerprint(cpusThrows: boolean) { + logged.length = 0 + installMocks(cpusThrows) + const mod = await import('../fingerprint') + return mod.calculateFingerprint +} + +const fingerprintTypes = () => + logged.map((l) => l.fingerprintType).filter(Boolean) as string[] + +describe('fingerprint CPU guard (#1374)', () => { + describe('when os.cpus() throws', () => { + test('calculateFingerprint() resolves instead of rejecting', async () => { + const calculateFingerprint = await loadCalculateFingerprint(true) + const id = await calculateFingerprint() + expect(typeof id).toBe('string') + expect(id.length).toBeGreaterThan(0) + }) + + test('does not fall back to a legacy fingerprint', async () => { + const calculateFingerprint = await loadCalculateFingerprint(true) + const id = await calculateFingerprint() + expect(id.startsWith('enhanced-')).toBe(true) + }) + + test('guards every os.cpus() call site', async () => { + const calculateFingerprint = await loadCalculateFingerprint(true) + await calculateFingerprint() + const types = fingerprintTypes() + // pre-check in getCpuInfoSafe() + expect(types).toContain('cpu_pre_check_failed') + // runtime.cpuCount in calculateEnhancedFingerprint() + expect(types).toContain('cpu_count_unavailable') + // nothing escaped to the legacy fallback + expect(types).not.toContain('enhanced_failed_fallback') + }) + }) + + describe('when os.cpus() works', () => { + test('still produces an enhanced fingerprint and logs no guard', async () => { + const calculateFingerprint = await loadCalculateFingerprint(false) + const id = await calculateFingerprint() + expect(id.startsWith('enhanced-')).toBe(true) + const types = fingerprintTypes() + expect(types).not.toContain('cpu_pre_check_failed') + expect(types).not.toContain('cpu_count_unavailable') + }) + }) +}) From e57a3002a72c1054c8ea04413528fe41b0374c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BE?= <139246607+heavymio@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:20:40 +0300 Subject: [PATCH 4/5] test(cli): restore mocked modules so the #1374 guard test does not leak mock.module() is not undone by mock.restore(), and bun runs every test file in a single process, so this file was leaking a throwing os.cpus() and a stub logger into whichever file ran after it. That included fingerprint.test.ts, which sorts immediately after this one. Capture the real exports before mocking and put every mocked module back in afterAll. Re-registering the namespace object alone is not enough, because the mock replaces entries in place. Confirmed with a follow-up file: without this, the next file sees cpus() throwing; with it, cpus() reports 9 and the real logger is back. --- .../__tests__/fingerprint-cpu-guard.test.ts | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts b/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts index 69afebf8f2..8051f5d20e 100644 --- a/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts +++ b/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts @@ -1,5 +1,10 @@ -import { describe, test, expect, mock } from 'bun:test' +import { describe, test, expect, mock, afterAll } from 'bun:test' import * as realOs from 'node:os' +import * as realMachineId from 'node-machine-id' +import * as realSi from 'systeminformation' +import * as realLogger from '../logger' +import * as realAnalytics from '../analytics' +import * as realDetectShell from '../detect-shell' // Issue #1374: Bun's os.cpus() throws 'Failed to get CPU information' on ARM // Linux when /proc/stat and /proc/cpuinfo disagree. Under proot-distro that @@ -7,18 +12,35 @@ import * as realOs from 'node:os' // /proc/stat (sysdata.py, _FAKE_STAT) while /proc/cpuinfo stays live, so on a // 9-core device the guest sees 8 vs 9 for the life of the container. // -// These tests pin the guard behaviour at the public entry point. Everything the -// fingerprint module reaches for is mocked so the test is hermetic and does not -// depend on the host's machine id, shell, or analytics transport. +// These tests pin the guard's behaviour at the public entry point. +// +// Two things about bun's test runner matter here: +// - all test files share one process, and mock.module() is not undone by +// mock.restore(), so every module mocked below is restored in afterAll. +// Without that this file leaks a throwing os.cpus() and a stub logger +// into whichever file runs next. +// - re-registering a namespace object is not enough, because the mock +// replaces entries in place, so the real exports are captured up front. + +const realCpus = realOs.cpus +const realNetworkInterfaces = realOs.networkInterfaces +const realOsModule = { ...realOs } +const realMachineIdFn = realMachineId.machineId +const realSiSystem = realSi.system +const realSiCpu = realSi.cpu +const realSiOsInfo = realSi.osInfo type Logged = { fingerprintType?: string } const logged: Logged[] = [] +const CPU_BREAK = 'Failed to get CPU information' + function installMocks(cpusThrows: boolean) { + logged.length = 0 mock.module('node:os', () => ({ - ...realOs, + ...realOsModule, cpus: () => { - if (cpusThrows) throw new Error('Failed to get CPU information') + if (cpusThrows) throw new Error(CPU_BREAK) return [{ model: 'Fake CPU' }] }, })) @@ -39,9 +61,7 @@ function installMocks(cpusThrows: boolean) { hostname: 'test', }), cpu: async () => { - if (cpusThrows) { - throw new Error('Failed to get CPU information') - } + if (cpusThrows) throw new Error(CPU_BREAK) return { manufacturer: 'Test', brand: 'Test', cores: 1, physicalCores: 1 } }, })) @@ -57,8 +77,25 @@ function installMocks(cpusThrows: boolean) { })) } +afterAll(() => { + mock.module('node:os', () => ({ + ...realOsModule, + cpus: realCpus, + networkInterfaces: realNetworkInterfaces, + })) + mock.module('node-machine-id', () => ({ ...realMachineId, machineId: realMachineIdFn })) + mock.module('systeminformation', () => ({ + ...realSi, + system: realSiSystem, + cpu: realSiCpu, + osInfo: realSiOsInfo, + })) + mock.module('../logger', () => realLogger) + mock.module('../analytics', () => realAnalytics) + mock.module('../detect-shell', () => realDetectShell) +}) + async function loadCalculateFingerprint(cpusThrows: boolean) { - logged.length = 0 installMocks(cpusThrows) const mod = await import('../fingerprint') return mod.calculateFingerprint From e8787a09d1ea1add82324c620518eee9f1eb1524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B8=D0=BE?= <139246607+heavymio@users.noreply.github.com> Date: Sun, 27 Sep 2026 14:39:00 +0300 Subject: [PATCH 5/5] test(cli): make the #1374 guard test order-independent Verified in the real workspace and it was not robust there: - fingerprint.ts memoises systeminformation in a module-level variable, so a plain re-import reused whichever mocks the first case installed. The second and third cases saw a legacy fingerprint and an empty log. Fixed with a cache-busting import so each case gets a fresh module instance. - Mocking the local ./logger and ./analytics leaked into windows-terminal-health.test.ts, which then recorded no analytics events. Dropped both; the guard is asserted on calculateFingerprint()'s return value instead, which still separates the bug precisely -- a throw escaping the guard produces a legacy fingerprint. Only the three external packages are mocked now, and all are restored in afterAll. Full run of cli/src/utils/__tests__ in the workspace, 83 files: main 1393 pass 15 fail branch 1396 pass 15 fail Same 15 failures before and after (ensureSponsoredProjectIdentity, which needs a Git fixture), plus the 3 new tests. No regressions. --- .../__tests__/fingerprint-cpu-guard.test.ts | 87 ++++++------------- 1 file changed, 27 insertions(+), 60 deletions(-) diff --git a/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts b/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts index 8051f5d20e..a2a6b8823d 100644 --- a/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts +++ b/cli/src/utils/__tests__/fingerprint-cpu-guard.test.ts @@ -2,9 +2,6 @@ import { describe, test, expect, mock, afterAll } from 'bun:test' import * as realOs from 'node:os' import * as realMachineId from 'node-machine-id' import * as realSi from 'systeminformation' -import * as realLogger from '../logger' -import * as realAnalytics from '../analytics' -import * as realDetectShell from '../detect-shell' // Issue #1374: Bun's os.cpus() throws 'Failed to get CPU information' on ARM // Linux when /proc/stat and /proc/cpuinfo disagree. Under proot-distro that @@ -12,15 +9,16 @@ import * as realDetectShell from '../detect-shell' // /proc/stat (sysdata.py, _FAKE_STAT) while /proc/cpuinfo stays live, so on a // 9-core device the guest sees 8 vs 9 for the life of the container. // -// These tests pin the guard's behaviour at the public entry point. +// The guard is asserted on calculateFingerprint()'s return value rather than on +// log output. That keeps the test to three module mocks, all of them external +// packages, and it still distinguishes the bug precisely: before the second +// call site was guarded the result was a legacy fingerprint, and a legacy +// fingerprint is exactly what a throw escaping the guard looks like. // -// Two things about bun's test runner matter here: -// - all test files share one process, and mock.module() is not undone by -// mock.restore(), so every module mocked below is restored in afterAll. -// Without that this file leaks a throwing os.cpus() and a stub logger -// into whichever file runs next. -// - re-registering a namespace object is not enough, because the mock -// replaces entries in place, so the real exports are captured up front. +// Only node:os, node-machine-id and systeminformation are mocked. Mocking the +// local ./logger or ./analytics would be more invasive and leaks into sibling +// test files -- bun runs them all in one process and mock.module() is not +// undone by mock.restore(). const realCpus = realOs.cpus const realNetworkInterfaces = realOs.networkInterfaces @@ -30,13 +28,9 @@ const realSiSystem = realSi.system const realSiCpu = realSi.cpu const realSiOsInfo = realSi.osInfo -type Logged = { fingerprintType?: string } -const logged: Logged[] = [] - const CPU_BREAK = 'Failed to get CPU information' -function installMocks(cpusThrows: boolean) { - logged.length = 0 +function mockOsCpus(cpusThrows: boolean) { mock.module('node:os', () => ({ ...realOsModule, cpus: () => { @@ -44,6 +38,9 @@ function installMocks(cpusThrows: boolean) { return [{ model: 'Fake CPU' }] }, })) +} + +function mockSystemInformation(cpusThrows: boolean) { mock.module('node-machine-id', () => ({ machineId: async () => 'test-machine-id-0001', })) @@ -65,16 +62,6 @@ function installMocks(cpusThrows: boolean) { return { manufacturer: 'Test', brand: 'Test', cores: 1, physicalCores: 1 } }, })) - mock.module('../analytics', () => ({ trackEvent: () => {} })) - mock.module('../detect-shell', () => ({ detectShell: () => 'bash' })) - mock.module('../logger', () => ({ - logger: { - warn: (data: Logged) => logged.push(data), - debug: () => {}, - info: () => {}, - error: () => {}, - }, - })) } afterAll(() => { @@ -90,56 +77,36 @@ afterAll(() => { cpu: realSiCpu, osInfo: realSiOsInfo, })) - mock.module('../logger', () => realLogger) - mock.module('../analytics', () => realAnalytics) - mock.module('../detect-shell', () => realDetectShell) }) -async function loadCalculateFingerprint(cpusThrows: boolean) { - installMocks(cpusThrows) - const mod = await import('../fingerprint') - return mod.calculateFingerprint +// The cache-busting query gives a fresh module instance per case. fingerprint.ts +// memoises systeminformation in a module-level variable, so a plain re-import +// would reuse the mocks installed by whichever case ran first. +async function calculateFingerprintWith(cpusThrows: boolean) { + mockOsCpus(cpusThrows) + mockSystemInformation(cpusThrows) + const mod = await import(`../fingerprint?case=${cpusThrows ? 'skew' : 'ok'}`) + return mod.calculateFingerprint() } -const fingerprintTypes = () => - logged.map((l) => l.fingerprintType).filter(Boolean) as string[] - describe('fingerprint CPU guard (#1374)', () => { describe('when os.cpus() throws', () => { - test('calculateFingerprint() resolves instead of rejecting', async () => { - const calculateFingerprint = await loadCalculateFingerprint(true) - const id = await calculateFingerprint() + test('resolves instead of rejecting', async () => { + const id = await calculateFingerprintWith(true) expect(typeof id).toBe('string') expect(id.length).toBeGreaterThan(0) }) - test('does not fall back to a legacy fingerprint', async () => { - const calculateFingerprint = await loadCalculateFingerprint(true) - const id = await calculateFingerprint() + test('keeps the enhanced fingerprint rather than falling back to legacy', async () => { + const id = await calculateFingerprintWith(true) expect(id.startsWith('enhanced-')).toBe(true) }) - - test('guards every os.cpus() call site', async () => { - const calculateFingerprint = await loadCalculateFingerprint(true) - await calculateFingerprint() - const types = fingerprintTypes() - // pre-check in getCpuInfoSafe() - expect(types).toContain('cpu_pre_check_failed') - // runtime.cpuCount in calculateEnhancedFingerprint() - expect(types).toContain('cpu_count_unavailable') - // nothing escaped to the legacy fallback - expect(types).not.toContain('enhanced_failed_fallback') - }) }) describe('when os.cpus() works', () => { - test('still produces an enhanced fingerprint and logs no guard', async () => { - const calculateFingerprint = await loadCalculateFingerprint(false) - const id = await calculateFingerprint() + test('still produces an enhanced fingerprint', async () => { + const id = await calculateFingerprintWith(false) expect(id.startsWith('enhanced-')).toBe(true) - const types = fingerprintTypes() - expect(types).not.toContain('cpu_pre_check_failed') - expect(types).not.toContain('cpu_count_unavailable') }) }) })