unbound-cli 1.16.18 → 1.16.19

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.16.18",
3
+ "version": "1.16.19",
4
4
  "description": "CLI tool for Unbound - AI Gateway management",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -36,6 +36,7 @@ const SETUP_TOOLS = [
36
36
  { label: 'Gemini CLI', value: 'gemini', script: 'gemini-cli/gateway/setup.py' },
37
37
  { label: 'Codex \u2014 subscription (hooks)', value: 'codex-sub', script: 'codex/hooks/setup.py', group: 'codex' },
38
38
  { label: 'Codex \u2014 gateway (gateway)', value: 'codex-gw', script: 'codex/gateway/setup.py', group: 'codex' },
39
+ { label: 'Pi Coding Agent', value: 'pi', script: 'pi/setup.py' },
39
40
  ];
40
41
 
41
42
  // Labels drop the `(subscription)` suffix on the default mode and keep
@@ -49,6 +50,7 @@ const MDM_TOOLS = {
49
50
  'gemini-cli': { label: 'Gemini CLI', script: 'gemini-cli/gateway/mdm/setup.py' },
50
51
  'codex-subscription': { label: 'Codex', script: 'codex/hooks/mdm/setup.py' },
51
52
  'codex-gateway': { label: 'Codex (gateway)', script: 'codex/gateway/mdm/setup.py' },
53
+ 'pi': { label: 'Pi Coding Agent', script: 'pi/mdm/setup.py' },
52
54
  };
53
55
 
54
56
  // Default MDM tools for `sudo unbound onboard` (subscription mode for Claude Code/Codex since only one can be active)
@@ -73,6 +75,7 @@ const SETUP_TOOL_MAP = {
73
75
  'gemini-cli': { label: 'Gemini CLI', script: 'gemini-cli/gateway/setup.py' },
74
76
  'codex-subscription': { label: 'Codex', script: 'codex/hooks/setup.py' },
75
77
  'codex-gateway': { label: 'Codex (gateway)', script: 'codex/gateway/setup.py' },
78
+ 'pi': { label: 'Pi Coding Agent', script: 'pi/setup.py' },
76
79
  };
77
80
 
78
81
  /**
@@ -763,6 +766,7 @@ Available tools:
763
766
  claude-code Claude Code (use --subscription or --gateway)
764
767
  gemini-cli Gemini CLI
765
768
  codex Codex (use --subscription or --gateway)
769
+ pi Pi Coding Agent
766
770
  roo-code Roo Code (shows config values)
767
771
  cline Cline (shows config values)
768
772
  kilo-code Kilo Code (shows config values)
@@ -782,6 +786,7 @@ Examples:
782
786
  $ unbound setup claude-code --gateway Claude Code gateway mode
783
787
  $ unbound setup claude-code --subscription Claude Code hooks only
784
788
  $ unbound setup codex --gateway Codex gateway mode
789
+ $ unbound setup pi Set up Pi Coding Agent
785
790
 
786
791
  Install the default bundle (Cursor + Copilot + Augment + Claude Code hooks + Codex hooks):
787
792
  $ unbound setup --all Set up the default bundle
@@ -1341,6 +1346,8 @@ module.exports = {
1341
1346
  hasRootPrivileges,
1342
1347
  ALL_TOOLS,
1343
1348
  MDM_ALL_TOOLS,
1349
+ SETUP_TOOL_MAP,
1350
+ MDM_TOOLS,
1344
1351
  buildScriptArgs,
1345
1352
  configDirForScript,
1346
1353
  scriptSupportsBackfill,
@@ -0,0 +1,206 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const {
6
+ SETUP_TOOL_MAP,
7
+ MDM_TOOLS,
8
+ ALL_TOOLS,
9
+ MDM_ALL_TOOLS,
10
+ resolveSetupAllTools,
11
+ buildScriptArgs,
12
+ configDirForScript,
13
+ scriptSupportsBackfill,
14
+ } = require('../src/commands/setup');
15
+
16
+ const SETUP_SOURCE_PATH = path.join(__dirname, '..', 'src', 'commands', 'setup.js');
17
+ const SETUP_SOURCE = fs.readFileSync(SETUP_SOURCE_PATH, 'utf8');
18
+
19
+ // The 10-00 harness in test/setup-tables.test.js is a test file, not a module: it has
20
+ // no module.exports, and requiring it would re-run its five tests inside this file.
21
+ // The five-check shape below is therefore the same contract re-stated locally rather
22
+ // than imported — test/setup-tables.test.js stays byte-identical.
23
+ function sourceArrayBlock(name) {
24
+ const start = SETUP_SOURCE.indexOf(`const ${name} = [`);
25
+ assert.notEqual(start, -1, `${name} not found in setup.js`);
26
+ const end = SETUP_SOURCE.indexOf('];', start);
27
+ assert.notEqual(end, -1, `${name} is not terminated`);
28
+ return SETUP_SOURCE.slice(start, end);
29
+ }
30
+
31
+ // The "Available tools:" block ends at the first blank line, before the
32
+ // "For multi-tool setup" section that lists the explicit mode names.
33
+ function helpToolLines() {
34
+ const start = SETUP_SOURCE.indexOf('Available tools:');
35
+ assert.notEqual(start, -1, 'the help text has no "Available tools:" block');
36
+ const block = SETUP_SOURCE.slice(start).split('\n\n')[0];
37
+ return block.split('\n').slice(1).filter((line) => line.trim());
38
+ }
39
+
40
+ const HELP_TOOL_LINES = helpToolLines();
41
+ const HELP_TOOL_NAMES = HELP_TOOL_LINES.map((line) => line.trim().split(/\s+/)[0]);
42
+ const PICKER_BLOCK = sourceArrayBlock('SETUP_TOOLS');
43
+
44
+ // SETUP_ALL_TOOLS and MDM_SETUP_ALL_TOOLS are deliberately NOT exported — keeping them
45
+ // unexported is what holds this plan's setup.js diff at five lines — so their membership
46
+ // is read off the array literals, the same seam 10-00 already uses for the help text.
47
+ function sourceStringListBlock(name) {
48
+ const match = SETUP_SOURCE.match(new RegExp(`const ${name} = \\[([^\\]]*)\\]`));
49
+ assert.ok(match, `${name} not found as an array literal in setup.js`);
50
+ return match[1];
51
+ }
52
+
53
+ const BUNDLES = {
54
+ // exported, asserted through the real object
55
+ ALL_TOOLS: (name) => ALL_TOOLS.includes(name),
56
+ MDM_ALL_TOOLS: (name) => MDM_ALL_TOOLS.includes(name),
57
+ // unexported, asserted through the source literal
58
+ SETUP_ALL_TOOLS: (name) => sourceStringListBlock('SETUP_ALL_TOOLS').includes(`'${name}'`),
59
+ MDM_SETUP_ALL_TOOLS: (name) => sourceStringListBlock('MDM_SETUP_ALL_TOOLS').includes(`'${name}'`),
60
+ };
61
+
62
+ function checkToolWired(name, spec = {}) {
63
+ const mdmKey = spec.mdmKey || name;
64
+ const entry = SETUP_TOOL_MAP[name];
65
+ const mdmEntry = MDM_TOOLS[mdmKey];
66
+ return {
67
+ inSetupMap: Boolean(entry) && entry.script === spec.script,
68
+ inMdmTools: Boolean(mdmEntry) && mdmEntry.script === spec.mdmScript,
69
+ inPicker: PICKER_BLOCK.includes(`value: '${name}'`),
70
+ inHelp: HELP_TOOL_NAMES.includes(name),
71
+ inSetupAllClear: resolveSetupAllTools(true).includes(name),
72
+ bundles: Object.keys(BUNDLES).filter((bundle) => BUNDLES[bundle](name)),
73
+ };
74
+ }
75
+
76
+ function assertToolWired(name, spec) {
77
+ const got = checkToolWired(name, spec);
78
+ assert.ok(got.inSetupMap, `${name}: SETUP_TOOL_MAP entry missing or script !== ${spec.script}`);
79
+ assert.ok(got.inMdmTools, `${name}: MDM_TOOLS entry missing or script !== ${spec.mdmScript}`);
80
+ assert.ok(got.inPicker, `${name}: absent from the interactive SETUP_TOOLS picker`);
81
+ assert.ok(got.inHelp, `${name}: absent from the "Available tools:" help block`);
82
+ assert.ok(got.inSetupAllClear, `${name}: absent from resolveSetupAllTools(true)`);
83
+ assert.deepEqual(got.bundles.sort(), [...spec.bundles].sort(), `${name}: bundle membership drifted`);
84
+ return got;
85
+ }
86
+
87
+ const PI_SPEC = {
88
+ script: 'pi/setup.py',
89
+ mdmScript: 'pi/mdm/setup.py',
90
+ bundles: [],
91
+ };
92
+
93
+ // INST-01/INST-02: `unbound setup pi` and `sudo unbound setup pi` resolve to the two
94
+ // Python installers through the dispatcher every other tool already uses.
95
+ test('pi is wired across the four setup tables and in no default bundle', () => {
96
+ const got = assertToolWired('pi', PI_SPEC);
97
+ assert.deepEqual(got.bundles, []);
98
+ assert.equal(SETUP_TOOL_MAP.pi.label, 'Pi Coding Agent');
99
+ assert.equal(SETUP_TOOL_MAP.pi.script, 'pi/setup.py');
100
+ assert.equal(MDM_TOOLS.pi.label, 'Pi Coding Agent');
101
+ assert.equal(MDM_TOOLS.pi.script, 'pi/mdm/setup.py');
102
+ });
103
+
104
+ // No `group` key: pi has a single mode, so the picker must not offer it as a
105
+ // subscription/gateway pair the way claude-code and codex are grouped.
106
+ test('the interactive picker entry for pi has a script, a label and no group', () => {
107
+ const line = PICKER_BLOCK.split('\n').find((l) => l.includes("value: 'pi'"));
108
+ assert.ok(line, `no SETUP_TOOLS entry with value: 'pi'\n${PICKER_BLOCK}`);
109
+ assert.ok(line.includes("label: 'Pi Coding Agent'"), line);
110
+ assert.ok(line.includes("script: 'pi/setup.py'"), line);
111
+ assert.ok(!line.includes('group:'), `pi has one mode, so the picker entry must carry no group: ${line}`);
112
+ });
113
+
114
+ // `unbound setup --all` and `unbound onboard` print the bundles verbatim, so pi must
115
+ // stay out of all four — but `--all --clear` must still sweep it, because
116
+ // resolveSetupAllTools(true) returns every SETUP_TOOL_MAP key.
117
+ test('pi is absent from each of the four default bundles, asserted one by one', () => {
118
+ assert.equal(ALL_TOOLS.includes('pi'), false, 'pi leaked into ALL_TOOLS');
119
+ assert.equal(MDM_ALL_TOOLS.includes('pi'), false, 'pi leaked into MDM_ALL_TOOLS');
120
+ assert.equal(sourceStringListBlock('SETUP_ALL_TOOLS').includes("'pi'"), false,
121
+ 'pi leaked into the SETUP_ALL_TOOLS literal');
122
+ assert.equal(sourceStringListBlock('MDM_SETUP_ALL_TOOLS').includes("'pi'"), false,
123
+ 'pi leaked into the MDM_SETUP_ALL_TOOLS literal');
124
+ // the negative half: the regex reader is not vacuously returning nothing
125
+ assert.equal(sourceStringListBlock('SETUP_ALL_TOOLS').includes("'cursor'"), true);
126
+ assert.equal(sourceStringListBlock('MDM_SETUP_ALL_TOOLS').includes("'cursor'"), true);
127
+ });
128
+
129
+ test('resolveSetupAllTools sweeps pi on --all --clear but never installs it via --all', () => {
130
+ assert.ok(resolveSetupAllTools(true).includes('pi'), resolveSetupAllTools(true));
131
+ assert.equal(resolveSetupAllTools(false).includes('pi'), false, resolveSetupAllTools(false));
132
+ });
133
+
134
+ test('the help text lists pi as "Pi Coding Agent" and shows an `unbound setup pi` example', () => {
135
+ const piHelpLine = HELP_TOOL_LINES.find((l) => l.trim().startsWith('pi '));
136
+ assert.ok(piHelpLine, `no pi line in the "Available tools:" block\n${HELP_TOOL_LINES.join('\n')}`);
137
+ assert.match(piHelpLine, /^\s*pi\s+Pi Coding Agent\s*$/);
138
+ const examples = SETUP_SOURCE.slice(SETUP_SOURCE.indexOf('Examples:'));
139
+ assert.match(examples, /\$ unbound setup pi\s+Set up Pi Coding Agent/);
140
+ });
141
+
142
+ // buildScriptArgs takes no tool argument (setup.js:256), so pi adds no argv behaviour.
143
+ // This is a cross-check of the generic flag shape — test/setup-args.test.js:126-137
144
+ // already owns the primary coverage — asserted once against pi's own call shape.
145
+ test('buildScriptArgs stays tool-agnostic: --domain non-MDM, --frontend-url under MDM, --clear keyless', () => {
146
+ const user = buildScriptArgs('k-123', { backendUrl: 'https://b', gatewayUrl: 'https://g', frontendUrl: 'https://f' });
147
+ assert.match(user, /--api-key /);
148
+ assert.match(user, /--domain /);
149
+ assert.ok(!user.includes('--frontend-url'), user);
150
+
151
+ const mdm = buildScriptArgs('k-123', { frontendUrl: 'https://f', mdm: true });
152
+ assert.match(mdm, /--frontend-url /);
153
+ assert.ok(!mdm.includes('--domain'), mdm);
154
+
155
+ // --clear runs no auth in the Python installers, so no key is present and the
156
+ // literal string "undefined" must never reach the argv.
157
+ const cleared = buildScriptArgs(undefined, { clear: true, backendUrl: 'https://b' });
158
+ assert.match(cleared, /--clear/);
159
+ assert.ok(!cleared.includes('--api-key'), cleared);
160
+ assert.ok(!cleared.includes('undefined'), cleared);
161
+ });
162
+
163
+ // configDirForScript returns undefined for any script outside this set, and also for a
164
+ // member when CLAUDE_CONFIG_DIR is unset — so set membership, not the return value, is
165
+ // the positive control that keeps the pi assertion from passing vacuously.
166
+ const CLAUDE_CONFIG_DIR_SOURCE = SETUP_SOURCE.slice(
167
+ SETUP_SOURCE.indexOf('const CLAUDE_CONFIG_DIR_SCRIPTS'),
168
+ SETUP_SOURCE.indexOf(']);', SETUP_SOURCE.indexOf('const CLAUDE_CONFIG_DIR_SCRIPTS')),
169
+ );
170
+
171
+ test('pi honours neither CLAUDE_CONFIG_DIR nor --backfill', () => {
172
+ assert.ok(!configDirForScript('pi/setup.py'), String(configDirForScript('pi/setup.py')));
173
+ assert.equal(scriptSupportsBackfill('pi/setup.py'), false);
174
+ assert.equal(scriptSupportsBackfill('pi/mdm/setup.py'), false);
175
+ // positive controls
176
+ assert.equal(scriptSupportsBackfill('claude-code/hooks/setup.py'), true);
177
+ assert.ok(CLAUDE_CONFIG_DIR_SOURCE.includes("'claude-code/hooks/setup.py'"),
178
+ 'the CLAUDE_CONFIG_DIR set no longer lists the Claude hooks installer');
179
+ assert.ok(!CLAUDE_CONFIG_DIR_SOURCE.includes("'pi/setup.py'"),
180
+ 'pi must not be added to CLAUDE_CONFIG_DIR_SCRIPTS');
181
+ });
182
+
183
+ // T-10-01: integrity is verified once, in pi/setup.py, against the committed sidecar.
184
+ // A second hash path in JavaScript could diverge from the Python one.
185
+ test('the CLI gains no hashing path — integrity stays in Python', () => {
186
+ assert.equal(/createHash|sha256/i.test(SETUP_SOURCE), false,
187
+ 'setup.js must contain no hashing: downloadToFile checks status, redirects, wx-open and length only');
188
+ });
189
+
190
+ // T-10-13: the release-version bump is validated by .github/workflows/version-check.yml,
191
+ // which diffs package.json against a real checkout of `main` on every PR. It is
192
+ // deliberately NOT asserted here: this suite has to pass on any checkout, and a
193
+ // version assertion would couple it to mutable Git state — it would fail on `main`
194
+ // immediately after this PR merges (ours would then equal main's) and would need
195
+ // editing on every future bump. Release state belongs in the release workflow.
196
+
197
+ // The Edit-tool hazard from 10-VALIDATION: setup.js:33-38 holds four real two-character
198
+ // — escapes. An edit near them must not normalise them into literal em-dashes.
199
+ // Scoped to the picker block on purpose: literal em-dashes are used freely in setup.js
200
+ // comments, so only the four string-literal escapes inside SETUP_TOOLS are asserted.
201
+ test('the four \\u2014 escapes in the picker labels survive byte-for-byte', () => {
202
+ assert.equal((SETUP_SOURCE.match(/\\u2014/g) || []).length, 4, 'the \\u2014 escape count changed');
203
+ assert.equal((PICKER_BLOCK.match(/\\u2014/g) || []).length, 4, 'a picker label escape was lost');
204
+ assert.equal(PICKER_BLOCK.includes('—'), false,
205
+ 'a literal em-dash appeared inside SETUP_TOOLS: an escape was normalised');
206
+ });
@@ -0,0 +1,132 @@
1
+ const { test } = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const {
6
+ SETUP_TOOL_MAP,
7
+ MDM_TOOLS,
8
+ ALL_TOOLS,
9
+ MDM_ALL_TOOLS,
10
+ resolveSetupAllTools,
11
+ } = require('../src/commands/setup');
12
+
13
+ const SETUP_SOURCE_PATH = path.join(__dirname, '..', 'src', 'commands', 'setup.js');
14
+ const SETUP_SOURCE = fs.readFileSync(SETUP_SOURCE_PATH, 'utf8');
15
+
16
+ // SETUP_TOOLS (the interactive picker) and MDM_SETUP_ALL_TOOLS are not exported and
17
+ // the help text is built inline by addHelpText, so all three are read from source.
18
+ function sourceArrayBlock(name) {
19
+ const start = SETUP_SOURCE.indexOf(`const ${name} = [`);
20
+ assert.notEqual(start, -1, `${name} not found in setup.js`);
21
+ const end = SETUP_SOURCE.indexOf('];', start);
22
+ assert.notEqual(end, -1, `${name} is not terminated`);
23
+ return SETUP_SOURCE.slice(start, end);
24
+ }
25
+
26
+ // The "Available tools:" block ends at the first blank line, before the
27
+ // "For multi-tool setup" section that lists the explicit mode names.
28
+ function helpToolLines() {
29
+ const start = SETUP_SOURCE.indexOf('Available tools:');
30
+ assert.notEqual(start, -1, 'the help text has no "Available tools:" block');
31
+ const block = SETUP_SOURCE.slice(start).split('\n\n')[0];
32
+ return block.split('\n').slice(1).filter((line) => line.trim());
33
+ }
34
+
35
+ const HELP_TOOL_NAMES = helpToolLines().map((line) => line.trim().split(/\s+/)[0]);
36
+ const PICKER_BLOCK = sourceArrayBlock('SETUP_TOOLS');
37
+ const MDM_SETUP_ALL_TOOLS_BLOCK = sourceArrayBlock('MDM_SETUP_ALL_TOOLS');
38
+
39
+ // Every bundle a tool can be in. `pi` must stay out of all four (RESEARCH A1) while
40
+ // still appearing in resolveSetupAllTools(true), so membership is asserted by name.
41
+ const BUNDLES = {
42
+ SETUP_ALL_TOOLS: (name) => resolveSetupAllTools(false).includes(name),
43
+ ALL_TOOLS: (name) => ALL_TOOLS.includes(name),
44
+ MDM_ALL_TOOLS: (name) => MDM_ALL_TOOLS.includes(name),
45
+ MDM_SETUP_ALL_TOOLS: (name) => MDM_SETUP_ALL_TOOLS_BLOCK.includes(`'${name}'`),
46
+ };
47
+
48
+ // The five wiring facts a `unbound setup <tool>` entry needs, as booleans, so the
49
+ // negative case can prove each one individually rather than trusting a thrown error.
50
+ function checkToolWired(name, spec = {}) {
51
+ const mdmKey = spec.mdmKey || name;
52
+ const entry = SETUP_TOOL_MAP[name];
53
+ const mdmEntry = MDM_TOOLS[mdmKey];
54
+ return {
55
+ inSetupMap: Boolean(entry) && entry.script === spec.script,
56
+ inMdmTools: Boolean(mdmEntry) && mdmEntry.script === spec.mdmScript,
57
+ inPicker: PICKER_BLOCK.includes(`value: '${name}'`),
58
+ inHelp: HELP_TOOL_NAMES.includes(name),
59
+ inSetupAllClear: resolveSetupAllTools(true).includes(name),
60
+ bundles: Object.keys(BUNDLES).filter((bundle) => BUNDLES[bundle](name)),
61
+ };
62
+ }
63
+
64
+ function assertToolWired(name, spec) {
65
+ const got = checkToolWired(name, spec);
66
+ assert.ok(got.inSetupMap, `${name}: SETUP_TOOL_MAP entry missing or script !== ${spec.script}`);
67
+ assert.ok(got.inMdmTools, `${name}: MDM_TOOLS entry missing or script !== ${spec.mdmScript}`);
68
+ assert.ok(got.inPicker, `${name}: absent from the interactive SETUP_TOOLS picker`);
69
+ assert.ok(got.inHelp, `${name}: absent from the "Available tools:" help block`);
70
+ assert.ok(got.inSetupAllClear, `${name}: absent from resolveSetupAllTools(true)`);
71
+ assert.deepEqual(got.bundles.sort(), [...spec.bundles].sort(),
72
+ `${name}: bundle membership drifted`);
73
+ return got;
74
+ }
75
+
76
+ // The maps have to be reachable at all before any assertion about them means anything.
77
+ test('setup.js exports SETUP_TOOL_MAP and MDM_TOOLS as plain objects', () => {
78
+ assert.equal(typeof SETUP_TOOL_MAP, 'object');
79
+ assert.equal(typeof MDM_TOOLS, 'object');
80
+ assert.ok(Object.keys(SETUP_TOOL_MAP).length >= 8, Object.keys(SETUP_TOOL_MAP));
81
+ assert.ok(Object.keys(MDM_TOOLS).length >= 8, Object.keys(MDM_TOOLS));
82
+ });
83
+
84
+ // Proves the five-check assertion against a tool that is already fully wired.
85
+ test('assertToolWired: augment is wired across all four tables', () => {
86
+ const got = assertToolWired('augment', {
87
+ script: 'augment/hooks/setup.py',
88
+ mdmScript: 'augment/hooks/mdm/setup.py',
89
+ bundles: ['SETUP_ALL_TOOLS', 'ALL_TOOLS', 'MDM_ALL_TOOLS', 'MDM_SETUP_ALL_TOOLS'],
90
+ });
91
+ assert.equal(SETUP_TOOL_MAP.augment.label, 'Augment Code');
92
+ assert.equal(got.bundles.length, 4);
93
+ });
94
+
95
+ // The regression half: a tool in SETUP_TOOL_MAP is in `--all --clear` for free,
96
+ // because resolveSetupAllTools(true) returns Object.keys(SETUP_TOOL_MAP).
97
+ test('resolveSetupAllTools: clear covers every SETUP_TOOL_MAP key, install covers the bundle only', () => {
98
+ assert.deepEqual(resolveSetupAllTools(true).sort(), Object.keys(SETUP_TOOL_MAP).sort());
99
+ const install = resolveSetupAllTools(false);
100
+ assert.ok(install.length < resolveSetupAllTools(true).length, install);
101
+ for (const name of install) {
102
+ assert.ok(SETUP_TOOL_MAP[name], `${name} is in the install bundle but not in SETUP_TOOL_MAP`);
103
+ }
104
+ });
105
+
106
+ // Without this the five checks could all pass vacuously on a typo'd tool name.
107
+ test('assertToolWired: a made-up tool fails every one of the five checks', () => {
108
+ const got = checkToolWired('definitely-not-a-tool', {
109
+ script: 'definitely-not-a-tool/setup.py',
110
+ mdmScript: 'definitely-not-a-tool/mdm/setup.py',
111
+ });
112
+ assert.equal(got.inSetupMap, false);
113
+ assert.equal(got.inMdmTools, false);
114
+ assert.equal(got.inPicker, false);
115
+ assert.equal(got.inHelp, false);
116
+ assert.equal(got.inSetupAllClear, false);
117
+ assert.deepEqual(got.bundles, []);
118
+ assert.throws(() => assertToolWired('definitely-not-a-tool', {
119
+ script: 'definitely-not-a-tool/setup.py',
120
+ mdmScript: 'definitely-not-a-tool/mdm/setup.py',
121
+ bundles: [],
122
+ }), /SETUP_TOOL_MAP entry missing/);
123
+ });
124
+
125
+ // The source-read helpers are the fragile half of this file: if a rename made either
126
+ // block unfindable, every membership check would silently report false.
127
+ test('the source-read blocks are real and non-empty', () => {
128
+ assert.ok(PICKER_BLOCK.includes("value: 'cursor'"), 'SETUP_TOOLS block did not parse');
129
+ assert.ok(MDM_SETUP_ALL_TOOLS_BLOCK.includes("'cursor'"), 'MDM_SETUP_ALL_TOOLS did not parse');
130
+ assert.ok(HELP_TOOL_NAMES.length >= 10, HELP_TOOL_NAMES);
131
+ assert.ok(HELP_TOOL_NAMES.includes('cursor'), HELP_TOOL_NAMES);
132
+ });