unbound-cli 1.6.2 → 1.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbound-cli",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "CLI tool for Unbound - AI Gateway management",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ # WEB-4922 end-to-end verification: `sudo unbound nuke` removes /opt/unbound
3
+ # (plus newsyslog conf + log dir) on Linux when invoked as root.
4
+ #
5
+ # Runs ENTIRELY inside a fresh ubuntu:24.04 container — the host's filesystem
6
+ # is mounted read-only and the repo is copied into the container before
7
+ # `npm link`, so the host's real /opt/unbound (if any) is never touched.
8
+ #
9
+ # Usage: ./scripts/verify-nuke-ubuntu.sh
10
+ set -euo pipefail
11
+
12
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
13
+
14
+ if ! command -v docker >/dev/null 2>&1; then
15
+ echo "FAIL: docker not on PATH" >&2
16
+ exit 2
17
+ fi
18
+
19
+ echo "Running WEB-4922 nuke verification inside ubuntu:24.04..."
20
+ echo "Host repo (mounted read-only): $REPO_ROOT"
21
+ echo
22
+
23
+ docker run --rm \
24
+ -v "$REPO_ROOT:/repo:ro" \
25
+ ubuntu:24.04 bash -c '
26
+ set -euo pipefail
27
+
28
+ # Refuse to run on a host (e.g. if someone copy-pastes this heredoc into a
29
+ # terminal): /.dockerenv is created by the docker engine in every container
30
+ # but never exists on a real host.
31
+ [ -f /.dockerenv ] || { echo "FAIL: refusing to run outside a container" >&2; exit 99; }
32
+
33
+ echo "[setup] installing node + curl..."
34
+ export DEBIAN_FRONTEND=noninteractive
35
+ apt-get update -qq >/dev/null
36
+ apt-get install -y -qq curl ca-certificates >/dev/null
37
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1
38
+ apt-get install -y -qq nodejs >/dev/null
39
+
40
+ # Copy repo into a writable spot — host mount is read-only.
41
+ cp -R /repo /work
42
+ cd /work
43
+ echo "[setup] npm link..."
44
+ npm link --silent >/dev/null 2>&1
45
+
46
+ # Fake the pkg layout from setup/packaging/pkg/postinstall.
47
+ echo "[setup] faking /opt/unbound + newsyslog + log dir..."
48
+ mkdir -p /opt/unbound/0.1.5/unbound-hook
49
+ mkdir -p /opt/unbound/0.1.5/unbound-discovery
50
+ mkdir -p /opt/unbound/etc
51
+ echo "#!fake binary" > /opt/unbound/0.1.5/unbound-hook/unbound-hook
52
+ echo "#!fake binary" > /opt/unbound/0.1.5/unbound-discovery/unbound-discovery
53
+ echo "{}" > /opt/unbound/etc/discovery.json
54
+ ln -sfn /opt/unbound/0.1.5 /opt/unbound/current
55
+ mkdir -p /etc/newsyslog.d
56
+ echo "# rotate" > /etc/newsyslog.d/ai.getunbound.conf
57
+ mkdir -p /var/log/unbound
58
+ echo "old log" > /var/log/unbound/discovery.log
59
+
60
+ echo
61
+ echo "===== BEFORE ====="
62
+ ls -la /opt/unbound
63
+ ls -la /etc/newsyslog.d/ai.getunbound.conf
64
+ ls -la /var/log/unbound
65
+ echo
66
+
67
+ # EUID 0 inside the container, so hasRootPrivileges() returns true and the
68
+ # includeMdm branch (where removeBinaryInstall is called) runs.
69
+ # Per-tool MDM clears WILL fail (no real tool installs) — that is best-effort
70
+ # and must not abort the binary install removal.
71
+ echo "[run] unbound nuke --yes (per-tool clears may noisily fail; that is fine)"
72
+ unbound nuke --yes || true
73
+
74
+ echo
75
+ echo "===== AFTER ====="
76
+ ls -la /opt/unbound 2>/dev/null || echo "(gone) /opt/unbound"
77
+ ls -la /etc/newsyslog.d/ai.getunbound.conf 2>/dev/null || echo "(gone) /etc/newsyslog.d/ai.getunbound.conf"
78
+ ls -la /var/log/unbound 2>/dev/null || echo "(gone) /var/log/unbound"
79
+ echo
80
+
81
+ FAIL=0
82
+ [ ! -e /opt/unbound ] || { echo "FAIL: /opt/unbound survived"; FAIL=1; }
83
+ [ ! -e /etc/newsyslog.d/ai.getunbound.conf ] || { echo "FAIL: newsyslog conf survived"; FAIL=1; }
84
+ [ ! -e /var/log/unbound ] || { echo "FAIL: log dir survived"; FAIL=1; }
85
+ if [ "$FAIL" = "0" ]; then
86
+ echo "PASS: WEB-4922 Ubuntu container — all binary install artifacts removed"
87
+ exit 0
88
+ else
89
+ echo "FAIL: at least one artifact survived"
90
+ exit 1
91
+ fi
92
+ '
@@ -16,7 +16,7 @@ const DISCOVERY_EXIT_UNSUPPORTED_OS = 3;
16
16
  // indefinitely. Discovery enforces this itself — on expiry it releases its lock,
17
17
  // reports the run as failed, and exits non-zero. Kept in sync with
18
18
  // setup/mdm/onboard.py's DISCOVERY_TIMEOUT_SECONDS.
19
- const DISCOVERY_TIMEOUT_SECONDS = 5400;
19
+ const DISCOVERY_TIMEOUT_SECONDS = 9000;
20
20
 
21
21
  // Classifies a discovery subprocess exit code:
22
22
  // 'success' (scan ran), 'unsupported' (skipped on this OS), or 'failure'.
@@ -390,6 +390,85 @@ function clearUnboundEnvsEverywhere() {
390
390
  return cleared;
391
391
  }
392
392
 
393
+ // Layout written by the setup pkg's postinstall (setup repo @ 41ca7b2 —
394
+ // packaging/pkg/postinstall + pkg/ai.getunbound.discovery.plist). Hard-coded;
395
+ // never accept user input here. Frozen so a misbehaving test can't mutate the
396
+ // production paths for the rest of the process.
397
+ const BINARY_INSTALL_PATHS = Object.freeze({
398
+ optDir: '/opt/unbound',
399
+ daemonPlist: '/Library/LaunchDaemons/ai.getunbound.discovery.plist',
400
+ daemonLabel: 'ai.getunbound.discovery',
401
+ newsyslogConf: '/etc/newsyslog.d/ai.getunbound.conf',
402
+ logDir: '/var/log/unbound',
403
+ });
404
+
405
+ // Tears down the privileged binary install. Best-effort: a failure on any
406
+ // single path is logged via output.warn AND pushed onto the returned
407
+ // `failed` array, but never aborts the rest. Bootout the daemon FIRST so
408
+ // its supervisor doesn't relaunch mid-rm. Callers MUST gate this on root.
409
+ // Returns { removed, failed } — both labelled lists; nuke folds `failed`
410
+ // into its closing summary so a partial wipe doesn't print "success".
411
+ // Path overrides + `runLaunchctl` exist so tests stay hermetic — see
412
+ // test/setup-args.test.js. Safety net: any production-default path (or
413
+ // daemonLabel, when runLaunchctl is true) requires the caller to explicitly
414
+ // pass `_confirmProductionPaths:true` — dev machines often have a real
415
+ // install, and a forgotten override would wipe it. Production sets the
416
+ // flag; tests omit it and rely on the throw.
417
+ function removeBinaryInstall({
418
+ optDir = BINARY_INSTALL_PATHS.optDir,
419
+ daemonPlist = BINARY_INSTALL_PATHS.daemonPlist,
420
+ daemonLabel = BINARY_INSTALL_PATHS.daemonLabel,
421
+ newsyslogConf = BINARY_INSTALL_PATHS.newsyslogConf,
422
+ logDir = BINARY_INSTALL_PATHS.logDir,
423
+ runLaunchctl = process.platform === 'darwin',
424
+ _confirmProductionPaths = false,
425
+ } = {}) {
426
+ const usesProdDefaults =
427
+ optDir === BINARY_INSTALL_PATHS.optDir ||
428
+ daemonPlist === BINARY_INSTALL_PATHS.daemonPlist ||
429
+ newsyslogConf === BINARY_INSTALL_PATHS.newsyslogConf ||
430
+ logDir === BINARY_INSTALL_PATHS.logDir ||
431
+ (runLaunchctl && daemonLabel === BINARY_INSTALL_PATHS.daemonLabel);
432
+ if (usesProdDefaults && !_confirmProductionPaths) {
433
+ throw new Error('removeBinaryInstall: production-default path(s) used without _confirmProductionPaths:true — pass tmp overrides or set the flag explicitly');
434
+ }
435
+ const removed = [];
436
+ const failed = [];
437
+ if (runLaunchctl) {
438
+ // spawnSync does NOT throw on non-zero exit — it returns { status, error,
439
+ // stderr }. Treat "Could not find specified service" / "No such process"
440
+ // as the legitimate "daemon not loaded" case (first install, already
441
+ // booted out) and swallow it. Surface anything else so the operator knows
442
+ // the bootout-then-rm ordering may have lost the race.
443
+ const r = spawnSync('launchctl', ['bootout', `system/${daemonLabel}`], { stdio: 'pipe' });
444
+ const stderr = r.stderr ? r.stderr.toString().trim() : '';
445
+ if (r.error) {
446
+ output.warn(`nuke: launchctl bootout failed to spawn: ${r.error.message}`);
447
+ failed.push(`launchctl bootout ${daemonLabel}`);
448
+ } else if (r.status !== 0 && !/Could not find specified service|No such process/i.test(stderr)) {
449
+ output.warn(`nuke: launchctl bootout exited ${r.status}: ${stderr || '(no stderr)'}`);
450
+ failed.push(`launchctl bootout ${daemonLabel}`);
451
+ }
452
+ }
453
+ const targets = [
454
+ ['LaunchDaemon plist', daemonPlist],
455
+ ['newsyslog conf', newsyslogConf],
456
+ ['log dir', logDir],
457
+ ['install tree', optDir],
458
+ ];
459
+ for (const [label, p] of targets) {
460
+ try {
461
+ if (!fs.existsSync(p)) continue;
462
+ fs.rmSync(p, { recursive: true, force: true });
463
+ removed.push(`${label} (${p})`);
464
+ } catch (err) {
465
+ output.warn(`nuke: failed to remove ${label} (${p}): ${err.message}`);
466
+ failed.push(`${label} (${p})`);
467
+ }
468
+ }
469
+ return { removed, failed };
470
+ }
471
+
393
472
  /**
394
473
  * Returns true when the process has the privileges needed to touch system-level
395
474
  * (MDM) configuration. On Windows, `net session` succeeds only when elevated, so
@@ -956,9 +1035,21 @@ Examples:
956
1035
 
957
1036
  // MDM clears need root and touch all users — run them only when we have it.
958
1037
  let mdmFailed = [];
1038
+ let binaryFailed = [];
959
1039
  if (includeMdm) {
960
1040
  const mdmTools = Object.keys(MDM_TOOLS).map(name => ({ name, ...MDM_TOOLS[name] }));
961
1041
  mdmFailed = await clearToolsBestEffort('mdm', mdmTools, { mdm: true, backendUrl, gatewayUrl });
1042
+
1043
+ // Root-only by construction (gated on includeMdm). No-op when no binary
1044
+ // install is present (python-only setups) and on Windows.
1045
+ // `_confirmProductionPaths` is the explicit "this is the real call site"
1046
+ // ack — the function refuses production defaults without it as a guard
1047
+ // against tests forgetting to override paths.
1048
+ const { removed: binaryRemoved, failed: bf } = removeBinaryInstall({ _confirmProductionPaths: true });
1049
+ binaryFailed = bf;
1050
+ if (binaryRemoved.length) {
1051
+ output.info(`Removed binary install: ${binaryRemoved.join('; ')}.`);
1052
+ }
962
1053
  } else {
963
1054
  output.info('Skipped MDM (system-level) config — that needs root. Re-run with sudo to remove it too.');
964
1055
  }
@@ -975,12 +1066,12 @@ Examples:
975
1066
  output.success('Stored credentials and settings removed.');
976
1067
 
977
1068
  console.log('');
978
- const failed = [...userFailed, ...mdmFailed];
1069
+ const failed = [...userFailed, ...mdmFailed, ...binaryFailed];
979
1070
  const scope = includeMdm ? 'on this device' : 'for your user';
980
1071
  if (failed.length === 0) {
981
1072
  output.success(`Unbound removed ${scope}. The CLI is back to a fresh state — run "unbound login" to start over.`);
982
1073
  } else {
983
- output.warn(`Done ${scope}, but ${failed.length} tool clear(s) reported issues: ${failed.join(', ')}. Credentials were still removed.`);
1074
+ output.warn(`Done ${scope}, but ${failed.length} step(s) reported issues: ${failed.join(', ')}. Credentials were still removed.`);
984
1075
  }
985
1076
  } catch (err) {
986
1077
  output.error(err.message);
@@ -1049,4 +1140,6 @@ module.exports = {
1049
1140
  resolveSetupAllTools,
1050
1141
  clearUnboundEnvsEverywhere,
1051
1142
  NUKE_ENV_VARS,
1143
+ removeBinaryInstall,
1144
+ BINARY_INSTALL_PATHS,
1052
1145
  };
@@ -3,7 +3,7 @@ const assert = require('node:assert/strict');
3
3
  const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
- const { buildScriptArgs, scriptSupportsBackfill, resolveSetupAllTools, clearUnboundEnvsEverywhere, NUKE_ENV_VARS } = require('../src/commands/setup');
6
+ const { buildScriptArgs, scriptSupportsBackfill, resolveSetupAllTools, clearUnboundEnvsEverywhere, NUKE_ENV_VARS, removeBinaryInstall, BINARY_INSTALL_PATHS } = require('../src/commands/setup');
7
7
 
8
8
  // shellEscape single-quotes every value, so a real key surfaces as
9
9
  // --api-key '<key>' at the head of the argv tail.
@@ -219,3 +219,180 @@ if (process.platform !== 'win32') {
219
219
  }
220
220
  });
221
221
  }
222
+
223
+ // All assertions below route through path overrides into a tmp dir and force
224
+ // runLaunchctl:false — the real /opt/unbound + /Library/LaunchDaemons/... are
225
+ // never touched (the dev host has a real binary install).
226
+ function makeFakeBinaryInstall() {
227
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-bin-'));
228
+ const optDir = path.join(tmp, 'opt', 'unbound');
229
+ const versionDir = path.join(optDir, '0.1.5');
230
+ fs.mkdirSync(path.join(versionDir, 'unbound-hook'), { recursive: true });
231
+ fs.mkdirSync(path.join(versionDir, 'unbound-discovery'), { recursive: true });
232
+ fs.writeFileSync(path.join(versionDir, 'unbound-hook', 'unbound-hook'), '#!fake binary\n');
233
+ fs.writeFileSync(path.join(versionDir, 'unbound-discovery', 'unbound-discovery'), '#!fake binary\n');
234
+ fs.mkdirSync(path.join(optDir, 'etc'), { recursive: true });
235
+ fs.writeFileSync(path.join(optDir, 'etc', 'discovery.json'), '{}');
236
+ fs.symlinkSync(versionDir, path.join(optDir, 'current'));
237
+ const daemonPlist = path.join(tmp, 'Library', 'LaunchDaemons', 'ai.getunbound.discovery.plist');
238
+ fs.mkdirSync(path.dirname(daemonPlist), { recursive: true });
239
+ fs.writeFileSync(daemonPlist, '<plist/>');
240
+ const newsyslogConf = path.join(tmp, 'etc', 'newsyslog.d', 'ai.getunbound.conf');
241
+ fs.mkdirSync(path.dirname(newsyslogConf), { recursive: true });
242
+ fs.writeFileSync(newsyslogConf, '# rotate\n');
243
+ const logDir = path.join(tmp, 'var', 'log', 'unbound');
244
+ fs.mkdirSync(logDir, { recursive: true });
245
+ fs.writeFileSync(path.join(logDir, 'discovery.log'), 'old logs\n');
246
+ return { tmp, optDir, daemonPlist, newsyslogConf, logDir };
247
+ }
248
+
249
+ test('removeBinaryInstall: full install layout — wipes plist + newsyslog + logs + /opt/unbound', () => {
250
+ const fx = makeFakeBinaryInstall();
251
+ try {
252
+ const { removed, failed } = removeBinaryInstall({
253
+ optDir: fx.optDir,
254
+ daemonPlist: fx.daemonPlist,
255
+ newsyslogConf: fx.newsyslogConf,
256
+ logDir: fx.logDir,
257
+ runLaunchctl: false,
258
+ });
259
+ assert.equal(removed.length, 4, `expected 4 removed, got ${JSON.stringify(removed)}`);
260
+ assert.deepEqual(failed, [], `unexpected failures: ${JSON.stringify(failed)}`);
261
+ assert.ok(!fs.existsSync(fx.optDir), 'optDir survived');
262
+ assert.ok(!fs.existsSync(fx.daemonPlist), 'plist survived');
263
+ assert.ok(!fs.existsSync(fx.newsyslogConf), 'newsyslog survived');
264
+ assert.ok(!fs.existsSync(fx.logDir), 'logDir survived');
265
+ // Each label appears in the summary so `unbound nuke` output names what went.
266
+ for (const label of ['LaunchDaemon plist', 'newsyslog conf', 'log dir', 'install tree']) {
267
+ assert.ok(removed.some(s => s.startsWith(label)), `summary missing ${label}: ${removed.join('; ')}`);
268
+ }
269
+ } finally {
270
+ fs.rmSync(fx.tmp, { recursive: true, force: true });
271
+ }
272
+ });
273
+
274
+ test('removeBinaryInstall: nothing present (python-only install) — returns empty lists silently', () => {
275
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-bin-empty-'));
276
+ try {
277
+ const { removed, failed } = removeBinaryInstall({
278
+ optDir: path.join(tmp, 'opt', 'unbound'),
279
+ daemonPlist: path.join(tmp, 'Library', 'LaunchDaemons', 'ai.getunbound.discovery.plist'),
280
+ newsyslogConf: path.join(tmp, 'etc', 'newsyslog.d', 'ai.getunbound.conf'),
281
+ logDir: path.join(tmp, 'var', 'log', 'unbound'),
282
+ runLaunchctl: false,
283
+ });
284
+ assert.deepEqual(removed, []);
285
+ assert.deepEqual(failed, []);
286
+ } finally {
287
+ fs.rmSync(tmp, { recursive: true, force: true });
288
+ }
289
+ });
290
+
291
+ test('removeBinaryInstall: partial install (only /opt/unbound, no daemon/log) — removes what exists', () => {
292
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-bin-partial-'));
293
+ const optDir = path.join(tmp, 'opt', 'unbound');
294
+ fs.mkdirSync(optDir, { recursive: true });
295
+ fs.writeFileSync(path.join(optDir, 'marker'), 'x');
296
+ try {
297
+ const { removed, failed } = removeBinaryInstall({
298
+ optDir,
299
+ daemonPlist: path.join(tmp, 'Library', 'LaunchDaemons', 'ai.getunbound.discovery.plist'),
300
+ newsyslogConf: path.join(tmp, 'etc', 'newsyslog.d', 'ai.getunbound.conf'),
301
+ logDir: path.join(tmp, 'var', 'log', 'unbound'),
302
+ runLaunchctl: false,
303
+ });
304
+ assert.equal(removed.length, 1);
305
+ assert.ok(removed[0].startsWith('install tree'), removed[0]);
306
+ assert.deepEqual(failed, []);
307
+ assert.ok(!fs.existsSync(optDir));
308
+ } finally {
309
+ fs.rmSync(tmp, { recursive: true, force: true });
310
+ }
311
+ });
312
+
313
+ // Bugbot: when a removal step fails (e.g. plist locked by launchd), it must
314
+ // land in the returned `failed` list AND not appear in `removed`, so the
315
+ // outer nuke summary surfaces a partial wipe instead of printing "success".
316
+ test('removeBinaryInstall: rm failure surfaces in `failed`, not `removed`', () => {
317
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-bin-rmfail-'));
318
+ // Pass an existsSync-true path that fs.rmSync can't remove. The simplest
319
+ // way to force rmSync to throw is to point optDir at a regular file owned
320
+ // by another process — but cross-platform that's flaky. Instead, monkey-
321
+ // patch fs.rmSync just for this call to simulate the real-world EBUSY.
322
+ const optDir = path.join(tmp, 'opt', 'unbound');
323
+ fs.mkdirSync(optDir, { recursive: true });
324
+ const realRmSync = fs.rmSync;
325
+ fs.rmSync = (p, opts) => {
326
+ if (p === optDir) throw new Error('EBUSY: resource busy or locked');
327
+ return realRmSync(p, opts);
328
+ };
329
+ try {
330
+ const { removed, failed } = removeBinaryInstall({
331
+ optDir,
332
+ daemonPlist: path.join(tmp, 'plist'),
333
+ newsyslogConf: path.join(tmp, 'newsyslog'),
334
+ logDir: path.join(tmp, 'log'),
335
+ runLaunchctl: false,
336
+ });
337
+ assert.deepEqual(removed, [], 'install tree must not be in removed when rmSync threw');
338
+ assert.equal(failed.length, 1, `expected 1 failure, got ${JSON.stringify(failed)}`);
339
+ assert.ok(failed[0].startsWith('install tree'), failed[0]);
340
+ } finally {
341
+ fs.rmSync = realRmSync;
342
+ realRmSync(tmp, { recursive: true, force: true });
343
+ }
344
+ });
345
+
346
+ // Guards against accidental drift of the hard-coded paths.
347
+ test('BINARY_INSTALL_PATHS: defaults match setup/packaging/pkg/postinstall layout', () => {
348
+ assert.equal(BINARY_INSTALL_PATHS.optDir, '/opt/unbound');
349
+ assert.equal(BINARY_INSTALL_PATHS.daemonPlist, '/Library/LaunchDaemons/ai.getunbound.discovery.plist');
350
+ assert.equal(BINARY_INSTALL_PATHS.daemonLabel, 'ai.getunbound.discovery');
351
+ assert.equal(BINARY_INSTALL_PATHS.newsyslogConf, '/etc/newsyslog.d/ai.getunbound.conf');
352
+ assert.equal(BINARY_INSTALL_PATHS.logDir, '/var/log/unbound');
353
+ });
354
+
355
+ // Safety net: dev machines often have a real /opt/unbound install. Calling
356
+ // removeBinaryInstall with ANY production-default path requires the explicit
357
+ // `_confirmProductionPaths:true` ack, so a no-override test (or refactor
358
+ // drift) fails loudly instead of wiping the dev's real install. This works
359
+ // regardless of NODE_ENV — production sets the ack, tests don't.
360
+ test('removeBinaryInstall: refuses production-default paths without _confirmProductionPaths', () => {
361
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-bin-guard-'));
362
+ try {
363
+ assert.throws(() => removeBinaryInstall(),
364
+ /_confirmProductionPaths/, 'no-arg call must throw');
365
+ // Partial override (logDir left at production) must throw.
366
+ assert.throws(() => removeBinaryInstall({
367
+ optDir: path.join(tmp, 'opt', 'unbound'),
368
+ daemonPlist: path.join(tmp, 'plist'),
369
+ newsyslogConf: path.join(tmp, 'newsyslog'),
370
+ runLaunchctl: false,
371
+ }), /_confirmProductionPaths/, 'partial override leaving logDir defaulted must throw');
372
+ // Greptile P2: daemonLabel must be checked when runLaunchctl is true.
373
+ // All four paths overridden, but default daemonLabel + runLaunchctl:true
374
+ // → would call `launchctl bootout system/ai.getunbound.discovery` on the
375
+ // real host. Must throw.
376
+ assert.throws(() => removeBinaryInstall({
377
+ optDir: path.join(tmp, 'opt', 'unbound'),
378
+ daemonPlist: path.join(tmp, 'plist'),
379
+ newsyslogConf: path.join(tmp, 'newsyslog'),
380
+ logDir: path.join(tmp, 'log'),
381
+ runLaunchctl: true,
382
+ }), /_confirmProductionPaths/, 'default daemonLabel with runLaunchctl:true must throw');
383
+ // Full override with runLaunchctl:false does NOT throw (no prod surface).
384
+ assert.doesNotThrow(() => removeBinaryInstall({
385
+ optDir: path.join(tmp, 'opt', 'unbound'),
386
+ daemonPlist: path.join(tmp, 'plist'),
387
+ newsyslogConf: path.join(tmp, 'newsyslog'),
388
+ logDir: path.join(tmp, 'log'),
389
+ runLaunchctl: false,
390
+ }));
391
+ } finally {
392
+ fs.rmSync(tmp, { recursive: true, force: true });
393
+ }
394
+ });
395
+
396
+ test('BINARY_INSTALL_PATHS: is frozen', () => {
397
+ assert.ok(Object.isFrozen(BINARY_INSTALL_PATHS));
398
+ });