@dzhechkov/cloudru-hub 0.1.12 → 0.1.14
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/.dz-manifest.json +62 -18
- package/CHANGELOG.md +89 -0
- package/LICENSE +4 -41
- package/README.md +67 -58
- package/THIRD_PARTY_NOTICES.md +5 -4
- package/bin/cloudru-hub.js +0 -0
- package/data/tools-classification.json +9 -9
- package/data/tools-list-snapshot.json +6 -6
- package/docs/HERMES-CODEX.md +34 -0
- package/docs/LOCAL-TESTING.md +14 -22
- package/package.json +16 -10
- package/sbom.json +127 -17
- package/src/cli.js +65 -17
- package/src/credentials.js +61 -0
- package/src/deploy-approval.js +120 -0
- package/src/install.js +246 -27
- package/src/k8s-approval.js +208 -0
- package/src/probe-child.js +38 -0
- package/src/resolve.js +3 -3
- package/src/safe-mcp.js +268 -0
- package/src/toolsets.js +58 -0
- package/templates/.claude/hooks/cloudru-ssh-guard.cjs +0 -0
- package/test/billing-credentials.test.mjs +113 -0
- package/test/classification.test.mjs +23 -32
- package/test/deploy-approval.test.mjs +154 -0
- package/test/dialects.test.mjs +11 -1
- package/test/install.test.mjs +235 -11
- package/test/k8s-approval.test.mjs +125 -0
- package/test/mutation-registry.json +6 -6
- package/test/pack-holds.test.mjs +14 -16
- package/test/resolve.test.mjs +58 -9
- package/test/safe-mcp.test.mjs +162 -0
package/test/install.test.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Installer tests — ADR-005 tiering + ADR-004 brake emission with EXECUTED veto.
|
|
2
|
-
import { test } from 'node:test';
|
|
2
|
+
import { after, test } from 'node:test';
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
@@ -7,10 +7,21 @@ import path from 'node:path';
|
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
|
+
const YAML = require('yaml');
|
|
10
11
|
const { install, mergeSettings, claudeSettingsFragment, probeBrake, HOOK_TEMPLATE, TIERS } = require('../src/install.js');
|
|
11
12
|
const { permissionRules } = require('../src/classification.js');
|
|
13
|
+
const pinnedEngine = { path: '/verified/cloudru-vm', verified: true, sha256: require('../data/tools-classification.json').engineSha256 };
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
// Every scratch dir is removed at the end (after() + process exit) unless DZ_KEEP_TMP=1 —
|
|
16
|
+
// MEASURED 2026-09-20: 42 cloudru-hub-test-* dirs per full sweep (backlog 7cb6f9b7).
|
|
17
|
+
const created = [];
|
|
18
|
+
const removeCreated = () => {
|
|
19
|
+
if (process.env.DZ_KEEP_TMP === '1') return;
|
|
20
|
+
for (const d of created.splice(0)) fs.rmSync(d, { recursive: true, force: true });
|
|
21
|
+
};
|
|
22
|
+
after(removeCreated);
|
|
23
|
+
process.on('exit', removeCreated);
|
|
24
|
+
const tmp = () => { const d = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-hub-test-')); created.push(d); return d; };
|
|
14
25
|
|
|
15
26
|
test('claude-code install emits .mcp.json + permission brake + hook, and the veto is EXECUTED', () => {
|
|
16
27
|
const dir = tmp();
|
|
@@ -80,7 +91,7 @@ test('install is idempotent — second run does not duplicate rules or hooks', (
|
|
|
80
91
|
});
|
|
81
92
|
|
|
82
93
|
test('DEGRADED targets are refused with the ADR-005 reason — never a fake install', () => {
|
|
83
|
-
for (const target of ['openclaude', '
|
|
94
|
+
for (const target of ['openclaude', 'cursor', 'windsurf', 'copilot']) {
|
|
84
95
|
const dir = tmp();
|
|
85
96
|
const res = install(target, dir);
|
|
86
97
|
assert.equal(res.code, 2, target);
|
|
@@ -104,22 +115,235 @@ test('POINTER-ONLY targets get the ≤2048-char pointer with the mandatory no-ve
|
|
|
104
115
|
}
|
|
105
116
|
});
|
|
106
117
|
|
|
107
|
-
test('codex
|
|
118
|
+
test('codex installs a project-scoped safe server and preserves unrelated TOML', () => {
|
|
108
119
|
const dir = tmp();
|
|
120
|
+
fs.mkdirSync(path.join(dir, '.codex'));
|
|
121
|
+
fs.writeFileSync(path.join(dir, '.codex', 'config.toml'), 'model = "other"\n');
|
|
109
122
|
const res = install('codex', dir);
|
|
110
|
-
assert.equal(res.code,
|
|
111
|
-
const out =
|
|
112
|
-
assert.
|
|
113
|
-
assert.
|
|
114
|
-
assert.
|
|
115
|
-
assert.
|
|
123
|
+
assert.equal(res.code, 0, res.lines.join('\n'));
|
|
124
|
+
const out = fs.readFileSync(path.join(dir, '.codex', 'config.toml'), 'utf8');
|
|
125
|
+
assert.match(out, /model = "other"/);
|
|
126
|
+
assert.match(out, /\[mcp_servers\.cloudru_hub\]/);
|
|
127
|
+
assert.match(out, /mcp-safe/);
|
|
128
|
+
assert.match(out, /enabled_tools = .*"vm_list"/);
|
|
129
|
+
assert.match(out, /--toolsets","inventory"/);
|
|
130
|
+
assert.doesNotMatch(out, /"deploy"/);
|
|
131
|
+
assert.match(out, /default_tools_approval_mode = "prompt"/);
|
|
132
|
+
assert.equal(install('codex', dir).code, 0, 'same install is idempotent');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('OpenCode and Pi install a safe MCP server without replacing unrelated JSON', () => {
|
|
136
|
+
for (const target of ['opencode', 'pi']) {
|
|
137
|
+
const dir = tmp();
|
|
138
|
+
const file = path.join(dir, target === 'pi' ? '.pi/mcp.json' : 'opencode.json');
|
|
139
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
140
|
+
fs.writeFileSync(file, JSON.stringify(target === 'pi' ? { mcpServers: { other: { command: 'test' } }, theme: 'x' } : { mcp: { other: { type: 'remote', url: 'https://example.com/mcp' } }, theme: 'x' }));
|
|
141
|
+
assert.equal(install(target, dir).code, 0);
|
|
142
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
143
|
+
const servers = target === 'pi' ? data.mcpServers : data.mcp;
|
|
144
|
+
assert.ok(servers.other);
|
|
145
|
+
assert.equal(data.theme, 'x');
|
|
146
|
+
assert.match(JSON.stringify(servers.cloudru_hub), /mcp-safe/);
|
|
147
|
+
assert.equal(install(target, dir).code, 0);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('installers refuse a competing raw Cloud.ru server', () => {
|
|
152
|
+
for (const target of ['codex', 'opencode', 'pi']) {
|
|
153
|
+
const dir = tmp();
|
|
154
|
+
if (target === 'codex') {
|
|
155
|
+
fs.mkdirSync(path.join(dir, '.codex'));
|
|
156
|
+
fs.writeFileSync(path.join(dir, '.codex/config.toml'), '[mcp_servers.cloudru-vm]\ncommand = "raw"\n');
|
|
157
|
+
} else {
|
|
158
|
+
const file = path.join(dir, target === 'pi' ? '.pi/mcp.json' : 'opencode.json');
|
|
159
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
160
|
+
fs.writeFileSync(file, JSON.stringify(target === 'pi' ? { mcpServers: { 'cloudru-vm': {} } } : { mcp: { 'cloudru-vm': {} } }));
|
|
161
|
+
}
|
|
162
|
+
assert.equal(install(target, dir).code, 2, target);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('Codex refuses quoted and inline TOML MCP declarations before changing the file', () => {
|
|
167
|
+
const variants = [
|
|
168
|
+
'[mcp_servers."cloudru-vm"]\ncommand = "raw"\n',
|
|
169
|
+
'[mcp_servers."cloudru_hub"]\ncommand = "raw"\n',
|
|
170
|
+
'mcp_servers = { cloudru_hub = { command = "raw" } }\n',
|
|
171
|
+
'["\\u006dcp_servers"."cloudru-vm"]\ncommand = "raw"\n',
|
|
172
|
+
];
|
|
173
|
+
for (const source of variants) {
|
|
174
|
+
const dir = tmp();
|
|
175
|
+
const file = path.join(dir, '.codex/config.toml');
|
|
176
|
+
fs.mkdirSync(path.dirname(file));
|
|
177
|
+
fs.writeFileSync(file, source);
|
|
178
|
+
const res = install('codex', dir);
|
|
179
|
+
assert.equal(res.code, 2, source);
|
|
180
|
+
assert.equal(fs.readFileSync(file, 'utf8'), source);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('Hermes installs a guarded nonempty include list without changing unrelated YAML', () => {
|
|
185
|
+
const dir = tmp();
|
|
186
|
+
const file = path.join(dir, 'config.yaml');
|
|
187
|
+
const source = 'plugins:\n enabled: true\nmcp_servers:\n github:\n command: "other"\nagent:\n model: "test"\n';
|
|
188
|
+
fs.writeFileSync(file, source);
|
|
189
|
+
const res = install('hermes', dir, { hermesHome: dir, resolvedEngine: pinnedEngine });
|
|
190
|
+
assert.equal(res.code, 0, res.lines.join('\n'));
|
|
191
|
+
const out = fs.readFileSync(file, 'utf8');
|
|
192
|
+
assert.deepEqual(Object.keys(YAML.parse(out).mcp_servers).sort(), ['cloudru_hub', 'github']);
|
|
193
|
+
assert.match(out, /mcp-safe/);
|
|
194
|
+
assert.match(out, /vm_list/);
|
|
195
|
+
assert.match(out, /--toolsets/);
|
|
196
|
+
assert.match(out, /include: \[/);
|
|
197
|
+
assert.equal(YAML.parse(out).mcp_servers.cloudru_hub.env.CLOUDRU_VM_BIN, pinnedEngine.path);
|
|
198
|
+
assert.doesNotMatch(out, /"deploy"/);
|
|
199
|
+
assert.match(out, /plugins:\n enabled: true/);
|
|
200
|
+
assert.match(out, / github:\n command: "other"/);
|
|
201
|
+
assert.match(out, /agent:\n model: "test"/);
|
|
202
|
+
assert.equal(install('hermes', dir, { hermesHome: dir, resolvedEngine: pinnedEngine }).code, 0);
|
|
203
|
+
assert.equal(fs.readFileSync(file, 'utf8'), out, 'second install is idempotent');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('Hermes keeps valid comments and refuses a flow-root document', () => {
|
|
207
|
+
const commented = tmp();
|
|
208
|
+
const file = path.join(commented, 'config.yaml');
|
|
209
|
+
fs.writeFileSync(file, 'mcp_servers:\n other:\n command: harmless\n # explanatory comment\n');
|
|
210
|
+
assert.equal(install('hermes', commented, { hermesHome: commented, resolvedEngine: pinnedEngine }).code, 0);
|
|
211
|
+
assert.equal(YAML.parse(fs.readFileSync(file, 'utf8')).mcp_servers.other.command, 'harmless');
|
|
212
|
+
assert.equal(YAML.parse(fs.readFileSync(file, 'utf8')).mcp_servers.cloudru_hub.tools.include.length, 11);
|
|
213
|
+
|
|
214
|
+
const flow = tmp();
|
|
215
|
+
const flowFile = path.join(flow, 'config.yaml');
|
|
216
|
+
fs.writeFileSync(flowFile, '{ model: harmless }\n');
|
|
217
|
+
assert.equal(install('hermes', flow, { hermesHome: flow }).code, 2);
|
|
218
|
+
assert.equal(fs.readFileSync(flowFile, 'utf8'), '{ model: harmless }\n');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('Hermes refuses missing or hash-mismatched engine without writing config', () => {
|
|
222
|
+
for (const resolvedEngine of [
|
|
223
|
+
{ error: 'engine absent' },
|
|
224
|
+
{ ...pinnedEngine, sha256: '0'.repeat(64) },
|
|
225
|
+
{ ...pinnedEngine, verified: false },
|
|
226
|
+
]) {
|
|
227
|
+
const dir = tmp();
|
|
228
|
+
const file = path.join(dir, 'config.yaml');
|
|
229
|
+
fs.writeFileSync(file, 'agent:\n model: test\n');
|
|
230
|
+
const res = install('hermes', dir, { hermesHome: dir, resolvedEngine });
|
|
231
|
+
assert.equal(res.code, 2);
|
|
232
|
+
assert.equal(fs.readFileSync(file, 'utf8'), 'agent:\n model: test\n');
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('Hermes refuses raw Cloud.ru entries and complex YAML without writing', () => {
|
|
237
|
+
for (const source of [
|
|
238
|
+
'mcp_servers:\n cloudru-vm:\n command: raw\n',
|
|
239
|
+
'mcp_servers: {cloudru_hub: {command: raw}}\n',
|
|
240
|
+
'mcp_servers: &servers\n github: {}\n',
|
|
241
|
+
'mcp_servers:\n other:\n command: harmless\n# comment\n cloudru-vm:\n command: raw\n',
|
|
242
|
+
'mcp_servers:\n other:\n command: harmless\n"mcp_servers":\n cloudru-vm:\n command: raw\n',
|
|
243
|
+
]) {
|
|
244
|
+
const dir = tmp();
|
|
245
|
+
const file = path.join(dir, 'config.yaml');
|
|
246
|
+
fs.writeFileSync(file, source);
|
|
247
|
+
assert.equal(install('hermes', dir, { hermesHome: dir }).code, 2, source);
|
|
248
|
+
assert.equal(fs.readFileSync(file, 'utf8'), source);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('toolset selection validates before install and limits Codex enabled_tools', () => {
|
|
253
|
+
const dir = tmp();
|
|
254
|
+
const invalid = install('codex', dir, { toolsets: 'inventory,all' });
|
|
255
|
+
assert.equal(invalid.code, 1);
|
|
256
|
+
assert.deepEqual(fs.readdirSync(dir), []);
|
|
257
|
+
const ok = install('codex', dir, { toolsets: 'billing' });
|
|
258
|
+
assert.equal(ok.code, 0);
|
|
259
|
+
const config = fs.readFileSync(path.join(dir, '.codex/config.toml'), 'utf8');
|
|
260
|
+
assert.match(config, /billing_balance/);
|
|
261
|
+
assert.doesNotMatch(config, /vm_list/);
|
|
262
|
+
assert.match(config, /CLOUDRU_HUB_CREDENTIALS/);
|
|
263
|
+
assert.match(config, /CLOUDRU_AGREEMENT_ID/);
|
|
264
|
+
assert.match(config, /CLOUDRU_KEY_ID/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('vm-deploy installs guarded tools and long-call timeouts for Codex, Hermes, OpenCode and Pi', () => {
|
|
268
|
+
const codexDir = tmp();
|
|
269
|
+
assert.equal(install('codex', codexDir, { toolsets: 'vm-deploy' }).code, 0);
|
|
270
|
+
const config = fs.readFileSync(path.join(codexDir, '.codex/config.toml'), 'utf8');
|
|
271
|
+
for (const name of ['deploy_plan', 'deploy_apply', 'vm_exec']) assert.match(config, new RegExp(`"${name}"`));
|
|
272
|
+
for (const name of ['destroy', 'secret_value', 'stack_apply']) assert.doesNotMatch(config, new RegExp(`"${name}"`));
|
|
273
|
+
assert.match(config, /--toolsets","vm-deploy"/);
|
|
274
|
+
assert.match(config, /tool_timeout_sec = 1200/);
|
|
275
|
+
assert.match(config, /env_vars = \["CLOUDRU_VM_BIN","CLOUDRU_HUB_CREDENTIALS","CLOUDRU_VM_HOME","CLOUDRU_HUB_APPROVALS"\]/);
|
|
276
|
+
assert.match(config, /default_tools_approval_mode = "auto"/);
|
|
277
|
+
|
|
278
|
+
const hermesDir = tmp();
|
|
279
|
+
assert.equal(install('hermes', hermesDir, { hermesHome: hermesDir, toolsets: 'vm-deploy', resolvedEngine: pinnedEngine }).code, 0);
|
|
280
|
+
const yaml = YAML.parse(fs.readFileSync(path.join(hermesDir, 'config.yaml'), 'utf8'));
|
|
281
|
+
assert.ok(yaml.mcp_servers.cloudru_hub.tools.include.includes('deploy_apply'));
|
|
282
|
+
assert.ok(!yaml.mcp_servers.cloudru_hub.tools.include.includes('destroy'));
|
|
283
|
+
assert.equal(yaml.mcp_servers.cloudru_hub.timeout, 1200);
|
|
284
|
+
|
|
285
|
+
const opencodeDir = tmp();
|
|
286
|
+
assert.equal(install('opencode', opencodeDir, { toolsets: 'vm-deploy' }).code, 0);
|
|
287
|
+
const opencode = JSON.parse(fs.readFileSync(path.join(opencodeDir, 'opencode.json'), 'utf8'));
|
|
288
|
+
assert.equal(opencode.mcp.cloudru_hub.timeout, 1_200_000, 'OpenCode uses milliseconds');
|
|
289
|
+
assert.equal(install('opencode', opencodeDir, { toolsets: 'vm-deploy' }).code, 0);
|
|
290
|
+
|
|
291
|
+
const piDir = tmp();
|
|
292
|
+
assert.equal(install('pi', piDir, { toolsets: 'vm-deploy' }).code, 0);
|
|
293
|
+
const pi = JSON.parse(fs.readFileSync(path.join(piDir, '.pi/mcp.json'), 'utf8'));
|
|
294
|
+
assert.equal(pi.mcpServers.cloudru_hub.timeout, 1200, 'Pi uses seconds');
|
|
295
|
+
assert.equal(install('pi', piDir, { toolsets: 'vm-deploy' }).code, 0);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('k8s-deploy installs the bounded profile across five hosts and refuses a raw Claude server', () => {
|
|
299
|
+
const codexDir = tmp();
|
|
300
|
+
assert.equal(install('codex', codexDir, { toolsets: 'k8s-deploy' }).code, 0);
|
|
301
|
+
const codex = fs.readFileSync(path.join(codexDir, '.codex/config.toml'), 'utf8');
|
|
302
|
+
assert.match(codex, /k8s-deploy/);
|
|
303
|
+
assert.match(codex, /tool_timeout_sec = 1200/);
|
|
304
|
+
assert.doesNotMatch(codex, /k8s_create/);
|
|
305
|
+
|
|
306
|
+
const hermesDir = tmp();
|
|
307
|
+
assert.equal(install('hermes', hermesDir, { toolsets: 'k8s-deploy', hermesHome: hermesDir, resolvedEngine: pinnedEngine }).code, 0);
|
|
308
|
+
const hermes = YAML.parse(fs.readFileSync(path.join(hermesDir, 'config.yaml'), 'utf8'));
|
|
309
|
+
assert.deepEqual(hermes.mcp_servers.cloudru_hub.tools.include, ['k8s_apply', 'k8s_get', 'k8s_kubectl', 'k8s_list', 'k8s_nodepools', 'k8s_options']);
|
|
310
|
+
assert.equal(hermes.mcp_servers.cloudru_hub.timeout, 1200);
|
|
311
|
+
|
|
312
|
+
const opencodeDir = tmp();
|
|
313
|
+
assert.equal(install('opencode', opencodeDir, { toolsets: 'k8s-deploy' }).code, 0);
|
|
314
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(opencodeDir, 'opencode.json'), 'utf8')).mcp.cloudru_hub.timeout, 1_200_000);
|
|
315
|
+
|
|
316
|
+
const piDir = tmp();
|
|
317
|
+
assert.equal(install('pi', piDir, { toolsets: 'k8s-deploy' }).code, 0);
|
|
318
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(piDir, '.pi/mcp.json'), 'utf8')).mcpServers.cloudru_hub.timeout, 1200);
|
|
319
|
+
|
|
320
|
+
const claudeDir = tmp();
|
|
321
|
+
assert.equal(install('claude-code', claudeDir, { toolsets: 'k8s-deploy' }).code, 0);
|
|
322
|
+
const mcp = JSON.parse(fs.readFileSync(path.join(claudeDir, '.mcp.json'), 'utf8'));
|
|
323
|
+
assert.ok(mcp.mcpServers.cloudru_hub.args.includes('k8s-deploy'));
|
|
324
|
+
assert.equal(mcp.mcpServers['cloudru-vm'], undefined);
|
|
325
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(claudeDir, '.claude/settings.json'), 'utf8')).enabledMcpjsonServers, ['cloudru_hub']);
|
|
326
|
+
assert.equal(install('claude-code', claudeDir, { toolsets: 'k8s-deploy' }).code, 0);
|
|
327
|
+
assert.equal(install('claude-code', claudeDir, { toolsets: 'k8s-deploy', npx: true }).code, 2);
|
|
328
|
+
const rawDir = tmp();
|
|
329
|
+
fs.writeFileSync(path.join(rawDir, '.mcp.json'), JSON.stringify({ mcpServers: { 'cloudru-vm': { command: 'unsafe' } } }));
|
|
330
|
+
assert.equal(install('claude-code', rawDir, { toolsets: 'k8s-deploy' }).code, 2);
|
|
331
|
+
const invalidMcp = tmp();
|
|
332
|
+
fs.writeFileSync(path.join(invalidMcp, '.mcp.json'), '[]\n');
|
|
333
|
+
assert.equal(install('claude-code', invalidMcp, { toolsets: 'k8s-deploy' }).code, 2);
|
|
334
|
+
assert.equal(fs.readFileSync(path.join(invalidMcp, '.mcp.json'), 'utf8'), '[]\n');
|
|
335
|
+
const invalidSettings = tmp();
|
|
336
|
+
fs.mkdirSync(path.join(invalidSettings, '.claude'));
|
|
337
|
+
fs.writeFileSync(path.join(invalidSettings, '.claude/settings.json'), '[]\n');
|
|
338
|
+
assert.equal(install('claude-code', invalidSettings, { toolsets: 'k8s-deploy' }).code, 2);
|
|
339
|
+
assert.equal(fs.readFileSync(path.join(invalidSettings, '.claude/settings.json'), 'utf8'), '[]\n');
|
|
116
340
|
});
|
|
117
341
|
|
|
118
342
|
test('unknown target is an error listing the known ones', () => {
|
|
119
343
|
const res = install('emacs', tmp());
|
|
120
344
|
assert.equal(res.code, 1);
|
|
121
345
|
assert.ok(res.lines[0].includes('unknown target'));
|
|
122
|
-
assert.equal(Object.keys(TIERS).length,
|
|
346
|
+
assert.equal(Object.keys(TIERS).length, 11);
|
|
123
347
|
});
|
|
124
348
|
|
|
125
349
|
test('probeBrake discriminates: a hook that stops vetoing FAILS the probe (broken-brake control)', () => {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { after, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const YAML = require('yaml');
|
|
11
|
+
const cls = require('../data/tools-classification.json');
|
|
12
|
+
const { selectedToolNames } = require('../src/toolsets.js');
|
|
13
|
+
const { PILOT_IMAGE, validateManifest, issueK8sApproval, consumeK8sApproval, k8sSession } = require('../src/k8s-approval.js');
|
|
14
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-k8s-approval-'));
|
|
15
|
+
after(() => fs.rmSync(scratch, { recursive: true, force: true }));
|
|
16
|
+
const clusterId = '11111111-2222-4333-8444-555555555555';
|
|
17
|
+
const projectId = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee';
|
|
18
|
+
const credentials = path.join(scratch, 'credentials.json');
|
|
19
|
+
fs.writeFileSync(credentials, JSON.stringify({ key_id: 'test-only', secret: 'test-only', project_id: projectId }), { mode: 0o600 });
|
|
20
|
+
const env = { ...process.env, CLOUDRU_HUB_CREDENTIALS: credentials, CLOUDRU_HUB_APPROVALS: path.join(scratch, 'approvals') };
|
|
21
|
+
|
|
22
|
+
function docs() {
|
|
23
|
+
const metadata = (name) => ({ name, namespace: 'snake-pilot' });
|
|
24
|
+
const app = 'snake-game';
|
|
25
|
+
return [
|
|
26
|
+
{ apiVersion: 'v1', kind: 'ConfigMap', metadata: metadata('snake-game-html'), data: { 'index.html': '<!doctype html><title>Snake</title>' } },
|
|
27
|
+
{ apiVersion: 'apps/v1', kind: 'Deployment', metadata: metadata(app), spec: {
|
|
28
|
+
replicas: 1, selector: { matchLabels: { app } }, template: { metadata: { labels: { app } }, spec: {
|
|
29
|
+
automountServiceAccountToken: false, securityContext: { runAsNonRoot: true },
|
|
30
|
+
containers: [{ name: app, image: PILOT_IMAGE, command: ['httpd'], args: ['-f', '-p', '8080', '-h', '/www'],
|
|
31
|
+
ports: [{ containerPort: 8080 }], readinessProbe: { httpGet: { path: '/', port: 8080 } },
|
|
32
|
+
resources: { requests: { cpu: '50m', memory: '32Mi' }, limits: { cpu: '100m', memory: '64Mi' } },
|
|
33
|
+
securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, runAsUser: 10001, runAsGroup: 10001, capabilities: { drop: ['ALL'] } },
|
|
34
|
+
volumeMounts: [{ name: 'html', mountPath: '/www', readOnly: true }] }],
|
|
35
|
+
volumes: [{ name: 'html', configMap: { name: 'snake-game-html' } }],
|
|
36
|
+
} } } },
|
|
37
|
+
{ apiVersion: 'v1', kind: 'Service', metadata: metadata(app), spec: { type: 'ClusterIP', selector: { app }, ports: [{ port: 80, targetPort: 8080 }] } },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
const serialize = (resources) => resources.map((r) => YAML.stringify(r)).join('---\n');
|
|
41
|
+
const manifest = serialize(docs());
|
|
42
|
+
const digest = require('crypto').createHash('sha256').update(manifest).digest('hex');
|
|
43
|
+
const manifestFile = path.join(scratch, 'snake.yaml');
|
|
44
|
+
fs.writeFileSync(manifestFile, manifest);
|
|
45
|
+
|
|
46
|
+
test('bounded Kubernetes manifest accepts exactly the reviewed pod-native resources', () => {
|
|
47
|
+
assert.deepEqual(validateManifest(manifest, 'snake-pilot').resources.map((r) => r.kind), ['ConfigMap', 'Deployment', 'Service']);
|
|
48
|
+
assert.deepEqual(selectedToolNames('k8s-deploy'), ['k8s_apply', 'k8s_get', 'k8s_kubectl', 'k8s_list', 'k8s_nodepools', 'k8s_options']);
|
|
49
|
+
for (const name of selectedToolNames('k8s-deploy')) {
|
|
50
|
+
assert.equal(cls.tools[name].permission, ['k8s_apply', 'k8s_kubectl'].includes(name) ? 'ask' : 'allow');
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('unexpected mutators, exposure, privilege, image and namespace are rejected', () => {
|
|
55
|
+
const mutations = [
|
|
56
|
+
(r) => { r[1].spec.template.spec.volumes.push({ name: 'host', hostPath: { path: '/etc' } }); },
|
|
57
|
+
(r) => { r[1].spec.template.spec.containers[0].securityContext.privileged = true; },
|
|
58
|
+
(r) => { r[1].spec.template.spec.containers[0].command = ['sh']; },
|
|
59
|
+
(r) => { r[1].spec.template.spec.containers[0].image = 'docker.io/library/busybox@sha256:' + 'f'.repeat(64); },
|
|
60
|
+
(r) => { r[2].spec.type = 'LoadBalancer'; },
|
|
61
|
+
(r) => { r[0].metadata.namespace = 'default'; },
|
|
62
|
+
(r) => { r.push({ apiVersion: 'v1', kind: 'Secret', metadata: { name: 's', namespace: 'snake-pilot' } }); },
|
|
63
|
+
];
|
|
64
|
+
for (const mutate of mutations) {
|
|
65
|
+
const r = docs(); mutate(r);
|
|
66
|
+
assert.throws(() => validateManifest(serialize(r), 'snake-pilot'));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('digest, project, expiry and single-use approval bind the exact manifest', () => {
|
|
71
|
+
assert.throws(() => issueK8sApproval(clusterId, 'snake-pilot', manifestFile, '0'.repeat(64), env), /digest differs/);
|
|
72
|
+
const a = issueK8sApproval(clusterId, 'snake-pilot', manifestFile, digest, env, 1000);
|
|
73
|
+
assert.equal(a.manifestSha256, digest);
|
|
74
|
+
const used = consumeK8sApproval(a.approvalId, env, 1100);
|
|
75
|
+
assert.equal(used.manifest, manifest);
|
|
76
|
+
assert.equal(k8sSession(a.approvalId, env, 1200).namespace, 'snake-pilot');
|
|
77
|
+
assert.throws(() => consumeK8sApproval(a.approvalId, env, 1200));
|
|
78
|
+
const b = issueK8sApproval(clusterId, 'snake-pilot', manifestFile, digest, env, 2000);
|
|
79
|
+
const otherCredentials = path.join(scratch, 'other.json');
|
|
80
|
+
fs.writeFileSync(otherCredentials, JSON.stringify({ key_id: 'test-other', secret: 'test-other', project_id: 'ffffffff-eeee-4ddd-8ccc-bbbbbbbbbbbb' }), { mode: 0o600 });
|
|
81
|
+
assert.throws(() => consumeK8sApproval(b.approvalId, { ...env, CLOUDRU_HUB_CREDENTIALS: otherCredentials }, 2100), /does not match/);
|
|
82
|
+
assert.throws(() => consumeK8sApproval(b.approvalId, env, 2000 + 10 * 60_000));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('MCP proxy forwards only locally approved apply and four fixed verification queries', () => {
|
|
86
|
+
const a = issueK8sApproval(clusterId, 'snake-pilot', manifestFile, digest, env);
|
|
87
|
+
const engine = path.join(scratch, 'fake-engine');
|
|
88
|
+
const log = path.join(scratch, 'forwarded.jsonl');
|
|
89
|
+
fs.writeFileSync(engine, `#!/usr/bin/env node
|
|
90
|
+
const fs=require('fs');let b='';process.stdin.setEncoding('utf8');process.stdin.on('data',x=>{b+=x;let i;while((i=b.indexOf('\\n'))>=0){const line=b.slice(0,i);b=b.slice(i+1);if(!line)continue;const m=JSON.parse(line);fs.appendFileSync(process.env.FAKE_LOG,JSON.stringify(m)+'\\n');if(!Object.hasOwn(m,'id'))continue;let result={};if(m.method==='tools/list')result={tools:[{name:'k8s_apply'},{name:'k8s_kubectl'},{name:'k8s_create'}]};else result={content:[{type:'text',text:'{"status":"applied"}'}]};process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:m.id,result})+'\\n');}});
|
|
91
|
+
`);
|
|
92
|
+
fs.chmodSync(engine, 0o755);
|
|
93
|
+
const script = `const {runSafeMcp}=require(${JSON.stringify(path.resolve(import.meta.dirname, '../src/safe-mcp.js'))});runSafeMcp({toolsets:'k8s-deploy',resolved:{path:${JSON.stringify(engine)},verified:true,sha256:${JSON.stringify(cls.engineSha256)}}});`;
|
|
94
|
+
const msg = (id, name, args) => JSON.stringify({ jsonrpc: '2.0', id, method: 'tools/call', params: { name, arguments: args } });
|
|
95
|
+
const lines = [
|
|
96
|
+
JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
|
|
97
|
+
msg(2, 'k8s_apply', { approval_id: a.approvalId, cluster_id: 'wrong', confirmed: true }),
|
|
98
|
+
msg(3, 'k8s_apply', { approval_id: a.approvalId }),
|
|
99
|
+
msg(4, 'k8s_apply', { approval_id: a.approvalId }),
|
|
100
|
+
msg(5, 'k8s_kubectl', { approval_id: a.approvalId, query: 'delete' }),
|
|
101
|
+
msg(6, 'k8s_kubectl', { approval_id: a.approvalId, query: 'pods', args: 'delete pods -A' }),
|
|
102
|
+
msg(7, 'k8s_kubectl', { approval_id: a.approvalId, query: 'pods' }),
|
|
103
|
+
msg(8, 'k8s_kubectl', { approval_id: a.approvalId, query: 'deployment' }),
|
|
104
|
+
msg(9, 'k8s_kubectl', { approval_id: a.approvalId, query: 'service' }),
|
|
105
|
+
msg(10, 'k8s_kubectl', { approval_id: a.approvalId, query: 'rollout' }),
|
|
106
|
+
msg(11, 'k8s_create', { confirmed: true }),
|
|
107
|
+
msg(12, 'k8s_apply', { approval_id: a.approvalId, create_only: false }),
|
|
108
|
+
];
|
|
109
|
+
fs.writeFileSync(log, '');
|
|
110
|
+
const r = spawnSync(process.execPath, ['-e', script], { input: lines.join('\n') + '\n', encoding: 'utf8', timeout: 5000,
|
|
111
|
+
env: { ...env, FAKE_LOG: log } });
|
|
112
|
+
assert.equal(r.status, 0, r.stderr);
|
|
113
|
+
const responses = r.stdout.trim().split('\n').map(JSON.parse);
|
|
114
|
+
const byId = Object.fromEntries(responses.map((x) => [x.id, x]));
|
|
115
|
+
assert.deepEqual(byId[1].result.tools.map((x) => x.name), ['k8s_apply', 'k8s_kubectl']);
|
|
116
|
+
assert.deepEqual(Object.keys(byId[1].result.tools[0].inputSchema.properties), ['approval_id']);
|
|
117
|
+
for (const id of [2, 4, 5, 6, 11, 12]) assert.ok(byId[id].error, `request ${id} rejected`);
|
|
118
|
+
const forwarded = fs.readFileSync(log, 'utf8').trim().split('\n').map(JSON.parse).filter((x) => x.method === 'tools/call');
|
|
119
|
+
assert.deepEqual(forwarded.map((x) => x.params.name), ['k8s_apply', 'k8s_kubectl', 'k8s_kubectl', 'k8s_kubectl', 'k8s_kubectl']);
|
|
120
|
+
assert.deepEqual(forwarded[0].params.arguments, { cluster_id: clusterId, namespace: 'snake-pilot', manifest, create_only: true, confirmed: true });
|
|
121
|
+
assert.deepEqual(forwarded[1].params.arguments, { cluster_id: clusterId, args: '-n snake-pilot get pods -l app=snake-game -o json' });
|
|
122
|
+
assert.deepEqual(forwarded[2].params.arguments, { cluster_id: clusterId, args: '-n snake-pilot get deployment snake-game -o json' });
|
|
123
|
+
assert.deepEqual(forwarded[3].params.arguments, { cluster_id: clusterId, args: '-n snake-pilot get service snake-game -o json' });
|
|
124
|
+
assert.deepEqual(forwarded[4].params.arguments, { cluster_id: clusterId, args: '-n snake-pilot rollout status deployment/snake-game --timeout=90s' });
|
|
125
|
+
});
|
|
@@ -74,18 +74,18 @@
|
|
|
74
74
|
"minFailing": 1
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
|
-
"id": "
|
|
78
|
-
"property": "
|
|
77
|
+
"id": "stack-status-remains-observational",
|
|
78
|
+
"property": "After the engine fix, stack_status reads persisted job state without advancing steps; stack_apply remains the explicitly gated mutation. Reclassifying status as ask loses the read-only contract and must fail the golden test.",
|
|
79
79
|
"file": "data/tools-classification.json",
|
|
80
80
|
"mutation": {
|
|
81
|
-
"find": "\"stack_status\": {\n \"effect\": \"
|
|
82
|
-
"replace": "\"stack_status\": {\n \"effect\": \"
|
|
81
|
+
"find": "\"stack_status\": {\n \"effect\": \"read-only\",\n \"permission\": \"allow\",",
|
|
82
|
+
"replace": "\"stack_status\": {\n \"effect\": \"read-only\",\n \"permission\": \"ask\","
|
|
83
83
|
},
|
|
84
|
-
"minFailing":
|
|
84
|
+
"minFailing": 1
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
"id": "brake-logs-stays-ask",
|
|
88
|
-
"property": "
|
|
88
|
+
"property": "The pinned engine quotes logs.service before remote execution, but logs still reaches SSH/compose and can disclose sensitive output. Flipping its classification from ask to allow violates the reviewed shell-reach boundary.",
|
|
89
89
|
"file": "data/tools-classification.json",
|
|
90
90
|
"mutation": {
|
|
91
91
|
"find": "\"logs\": {\n \"effect\": \"shell-reach\",\n \"permission\": \"ask\",",
|
package/test/pack-holds.test.mjs
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
// The iron guardrails as layer-1 tests (feature hermes-claude-adaptation)
|
|
2
|
-
// ADR-001 AM-
|
|
3
|
-
// (
|
|
4
|
-
// SATISFIED. The hold-era protections (private:true, PENDING placeholder, EPRIVATE probe)
|
|
1
|
+
// The iron guardrails as layer-1 tests (feature hermes-claude-adaptation).
|
|
2
|
+
// ADR-001 AM-3 (2026-10-20) corrects the historical engine attribution.
|
|
3
|
+
// The hold-era protections (private:true, PENDING placeholder, EPRIVATE probe)
|
|
5
4
|
// retired; what must now hold forever:
|
|
6
|
-
// 1. LICENSE
|
|
7
|
-
// and
|
|
5
|
+
// 1. LICENSE links the corrected provenance record, credits API validation,
|
|
6
|
+
// and does not assign Go source authorship to the API tester.
|
|
8
7
|
// 2. The `licenseHold` trigger field STAYS so the dz guard `licence-hold` rule keeps
|
|
9
8
|
// verifying LICENSE/NOTICES/SPDX on every publish (a satisfied hold passes with it).
|
|
10
|
-
// 3. package.json.license matches the SPDX id of the actual LICENSE text
|
|
11
|
-
// Confirmation), and covers ONLY the owner's code.
|
|
9
|
+
// 3. package.json.license matches the SPDX id of the actual LICENSE text.
|
|
12
10
|
// 4. ZERO upstream engine bytes in the tarball — unchanged.
|
|
13
11
|
// Discrimination: test/mutation-registry.json flips each protection and requires red here.
|
|
14
12
|
import { test } from 'node:test';
|
|
@@ -24,22 +22,22 @@ const license = fs.readFileSync(path.join(pkgDir, 'LICENSE'), 'utf8');
|
|
|
24
22
|
|
|
25
23
|
const PENDING_MARKER = '<!-- PENDING:';
|
|
26
24
|
|
|
27
|
-
test('LICENSE
|
|
25
|
+
test('LICENSE preserves MIT text and links the corrected provenance record', () => {
|
|
28
26
|
assert.ok(license.length > 400);
|
|
29
27
|
assert.ok(license.includes('MIT License'), 'owner code is MIT');
|
|
30
28
|
assert.ok(license.includes('Dmitry Zhechkov'), 'owner copyright line');
|
|
31
29
|
assert.ok(!license.includes(PENDING_MARKER), 'the PENDING placeholder must be gone post-grant');
|
|
32
|
-
assert.match(license, /
|
|
33
|
-
assert.match(license, /
|
|
34
|
-
assert.match(license, /owner-attested/, 'honesty: the approval is stated as owner-attested, not as a written artifact we do not hold');
|
|
30
|
+
assert.match(license, /Grant-Confirmation:\s*https?:\/\/\S+/, 'the provenance record link is present (dz guard licence-hold requires it)');
|
|
31
|
+
assert.match(license, /corrected attribution record/, 'the superseding attribution correction is linked');
|
|
35
32
|
assert.ok(!/Hermes author(.|\n){0,120}hereby grants/i.test(license), 'no fabricated grant wording');
|
|
36
33
|
assert.ok(!/hereby grants/i.test(license), 'no grant-deed language fabricated on anyone’s behalf');
|
|
37
34
|
});
|
|
38
35
|
|
|
39
|
-
test('LICENSE
|
|
40
|
-
assert.ok(license.includes('Тимур'), 'the
|
|
41
|
-
assert.match(license, /
|
|
42
|
-
assert.
|
|
36
|
+
test('LICENSE credits Timur for API validation', () => {
|
|
37
|
+
assert.ok(license.includes('Тимур'), 'the API tester is credited by name');
|
|
38
|
+
assert.match(license, /API testing/, 'the contribution is specified');
|
|
39
|
+
assert.doesNotMatch(license, /(?:Timur|Тимур).{0,100}(?:author|authored)/i, 'no unsupported source authorship claim');
|
|
40
|
+
assert.match(license, /ZERO bytes of the Go engine/, 'the launcher still contains no engine bytes');
|
|
43
41
|
});
|
|
44
42
|
|
|
45
43
|
test('the licenseHold trigger for the dz guard rule STAYS declared post-grant (satisfied hold passes with it in place)', () => {
|
package/test/resolve.test.mjs
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
// Engine resolution tests — ADR-002: runtime resolution, sha256 pin, loud esbuild-style error.
|
|
2
|
-
import { test } from 'node:test';
|
|
2
|
+
import { after, test } from 'node:test';
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import crypto from 'node:crypto';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
8
9
|
import { createRequire } from 'node:module';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
9
11
|
|
|
10
12
|
const require = createRequire(import.meta.url);
|
|
11
13
|
const { resolveEngine, sha256File, platformKey } = require('../src/resolve.js');
|
|
12
14
|
const PKG = require('../package.json');
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
function resolveWithoutOptionalPlatform(env) {
|
|
17
|
+
const packages = PKG.cloudruHub.platformPackages;
|
|
18
|
+
const key = platformKey();
|
|
19
|
+
const original = packages[key];
|
|
20
|
+
delete packages[key];
|
|
21
|
+
try { return resolveEngine(env); }
|
|
22
|
+
finally { if (original !== undefined) packages[key] = original; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Every scratch dir is removed at the end (after() + process exit) unless DZ_KEEP_TMP=1 —
|
|
26
|
+
// MEASURED 2026-09-20: 18 cloudru-resolve-* dirs per full sweep (backlog 7cb6f9b7).
|
|
27
|
+
const created = [];
|
|
28
|
+
const removeCreated = () => {
|
|
29
|
+
if (process.env.DZ_KEEP_TMP === '1') return;
|
|
30
|
+
for (const d of created.splice(0)) fs.rmSync(d, { recursive: true, force: true });
|
|
31
|
+
};
|
|
32
|
+
after(removeCreated);
|
|
33
|
+
process.on('exit', removeCreated);
|
|
34
|
+
const tmp = () => { const d = fs.mkdtempSync(path.join(os.tmpdir(), 'cloudru-resolve-')); created.push(d); return d; };
|
|
15
35
|
|
|
16
36
|
test('env CLOUDRU_VM_BIN wins and is hashed; a non-pinned file reports verified:false but resolves', () => {
|
|
17
37
|
const dir = tmp();
|
|
@@ -24,6 +44,37 @@ test('env CLOUDRU_VM_BIN wins and is hashed; a non-pinned file reports verified:
|
|
|
24
44
|
assert.equal(r.verified, false);
|
|
25
45
|
});
|
|
26
46
|
|
|
47
|
+
test('mcp refuses a non-pinned engine before executing it', () => {
|
|
48
|
+
const dir = tmp();
|
|
49
|
+
const marker = path.join(dir, 'executed');
|
|
50
|
+
const fake = path.join(dir, 'cloudru-vm');
|
|
51
|
+
fs.writeFileSync(fake, `#!/bin/sh\ntouch "${marker}"\n`);
|
|
52
|
+
fs.chmodSync(fake, 0o755);
|
|
53
|
+
const result = spawnSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), '../bin/cloudru-hub.js'), 'mcp'], {
|
|
54
|
+
encoding: 'utf8', timeout: 5000,
|
|
55
|
+
env: { ...process.env, CLOUDRU_VM_BIN: fake, CLOUDRU_HUB_CONFIG: path.join(dir, 'none.json') },
|
|
56
|
+
});
|
|
57
|
+
assert.equal(result.status, 1);
|
|
58
|
+
assert.match(result.stderr, /does not match pinned .*refusing to launch/);
|
|
59
|
+
assert.equal(fs.existsSync(marker), false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('self-test stops on a non-pinned engine before version or MCP probe', () => {
|
|
63
|
+
const dir = tmp();
|
|
64
|
+
const marker = path.join(dir, 'executed');
|
|
65
|
+
const fake = path.join(dir, 'cloudru-vm');
|
|
66
|
+
fs.writeFileSync(fake, `#!/bin/sh\ntouch "${marker}"\n`);
|
|
67
|
+
fs.chmodSync(fake, 0o755);
|
|
68
|
+
const result = spawnSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), '../bin/cloudru-hub.js'), 'self-test', '--json'], {
|
|
69
|
+
encoding: 'utf8', timeout: 5000,
|
|
70
|
+
env: { ...process.env, CLOUDRU_VM_BIN: fake, CLOUDRU_HUB_CONFIG: path.join(dir, 'none.json') },
|
|
71
|
+
});
|
|
72
|
+
assert.equal(result.status, 1);
|
|
73
|
+
const report = JSON.parse(result.stdout);
|
|
74
|
+
assert.deepEqual(report.steps.map((step) => [step.name, step.ok]), [['resolve', true], ['sha256', false]]);
|
|
75
|
+
assert.equal(fs.existsSync(marker), false);
|
|
76
|
+
});
|
|
77
|
+
|
|
27
78
|
test('a file matching the pinned baseline hash reports verified:true', () => {
|
|
28
79
|
// Only meaningful on linux-x64 (the single pinned platform, ADR-002 point 4).
|
|
29
80
|
const key = platformKey();
|
|
@@ -49,19 +100,17 @@ test('config enginePath is used when env is unset', () => {
|
|
|
49
100
|
|
|
50
101
|
test('nothing resolvable → loud error naming all three paths and the platform-package status', () => {
|
|
51
102
|
const dir = tmp();
|
|
52
|
-
const r =
|
|
103
|
+
const r = resolveWithoutOptionalPlatform({ CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
|
|
53
104
|
assert.ok(r.error);
|
|
54
105
|
assert.match(r.error, /CLOUDRU_VM_BIN/);
|
|
55
106
|
assert.match(r.error, /enginePath/);
|
|
56
107
|
assert.match(r.error, /never bundled/);
|
|
57
|
-
|
|
58
|
-
// build — it is no longer a licence hold, and the error must say so honestly.
|
|
59
|
-
assert.match(r.error, /ADR-002 trusted-CI/);
|
|
108
|
+
assert.match(r.error, /optional dependencies/);
|
|
60
109
|
});
|
|
61
110
|
|
|
62
111
|
test('a dangling env path falls through to the error, not a crash', () => {
|
|
63
112
|
const dir = tmp();
|
|
64
|
-
const r =
|
|
113
|
+
const r = resolveWithoutOptionalPlatform({ CLOUDRU_VM_BIN: path.join(dir, 'nope'), CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
|
|
65
114
|
assert.ok(r.error);
|
|
66
115
|
assert.match(r.error, /set, but not a file/);
|
|
67
116
|
});
|
|
@@ -73,9 +122,9 @@ test('sha256File matches node crypto', () => {
|
|
|
73
122
|
assert.equal(sha256File(f), crypto.createHash('sha256').update('abc').digest('hex'));
|
|
74
123
|
});
|
|
75
124
|
|
|
76
|
-
test('the pinned linux-x64 hash is the
|
|
125
|
+
test('the pinned linux-x64 hash is the rebuilt private engine sha256', () => {
|
|
77
126
|
assert.equal(
|
|
78
127
|
PKG.cloudruHub.binaryHashes['linux-x64'],
|
|
79
|
-
'
|
|
128
|
+
'4463b09323c9dfe0c32d618d335734a947e7fab0a75d07a7bea089b97a97c9b6',
|
|
80
129
|
);
|
|
81
130
|
});
|