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 +1 -1
- package/src/commands/setup.js +7 -0
- package/test/setup-pi.test.js +206 -0
- package/test/setup-tables.test.js +132 -0
package/package.json
CHANGED
package/src/commands/setup.js
CHANGED
|
@@ -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
|
+
});
|