@dzhechkov/cloudru-hub 0.1.13 → 0.1.15
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 +30 -14
- package/CHANGELOG.md +20 -0
- package/LICENSE +4 -41
- package/README.md +42 -46
- package/THIRD_PARTY_NOTICES.md +5 -4
- package/data/tools-classification.json +1 -1
- package/data/tools-list-snapshot.json +1 -1
- package/docs/COST-COMPARE.md +21 -0
- package/docs/HERMES-CODEX.md +1 -1
- package/docs/LOCAL-TESTING.md +9 -9
- package/docs/cost-compare.example.json +84 -0
- package/package.json +7 -4
- package/sbom.json +53 -13
- package/src/cli.js +20 -9
- package/src/cost-compare.js +348 -0
- package/src/resolve.js +3 -3
- package/test/cost-compare.test.mjs +280 -0
- package/test/pack-holds.test.mjs +14 -16
- package/test/resolve.test.mjs +13 -6
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { after, test } from 'node:test';
|
|
8
|
+
|
|
9
|
+
const pkg = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const cli = join(pkg, 'bin/cloudru-hub.js');
|
|
11
|
+
const example = join(pkg, 'docs/cost-compare.example.json');
|
|
12
|
+
const exampleText = readFileSync(example, 'utf8');
|
|
13
|
+
const source = JSON.parse(exampleText);
|
|
14
|
+
const dirs = [];
|
|
15
|
+
after(() => { for (const dir of dirs) rmSync(dir, { recursive: true, force: true }); });
|
|
16
|
+
function directory() { const dir = mkdtempSync(join(tmpdir(), 'cloudru-cost-test-')); dirs.push(dir); return dir; }
|
|
17
|
+
function invoke(input, flags = [], env = {}) {
|
|
18
|
+
const dir = directory(), path = join(dir, 'input.json');
|
|
19
|
+
writeFileSync(path, JSON.stringify(input));
|
|
20
|
+
const result = spawnSync(process.execPath, [cli, 'cost-compare', '--input', path, '--json', ...flags], {
|
|
21
|
+
cwd: dir, env: { ...process.env, CLOUDRU_VM_BIN: join(dir, 'no-engine'), CLOUDRU_HUB_CONFIG: join(dir, 'no-config'), ...env },
|
|
22
|
+
encoding: 'utf8', timeout: 5000, maxBuffer: 1024 * 1024,
|
|
23
|
+
});
|
|
24
|
+
assert.ifError(result.error);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function invokeRaw(raw) {
|
|
28
|
+
const dir = directory(), path = join(dir, 'input.json');
|
|
29
|
+
writeFileSync(path, raw);
|
|
30
|
+
const result = spawnSync(process.execPath, [cli, 'cost-compare', '--input', path, '--json'], {
|
|
31
|
+
cwd: dir, encoding: 'utf8', timeout: 5000, maxBuffer: 1024 * 1024,
|
|
32
|
+
});
|
|
33
|
+
assert.ifError(result.error);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
function editRaw(from, to) {
|
|
37
|
+
assert.ok(exampleText.includes(from), `missing fixture anchor ${from}`);
|
|
38
|
+
return exampleText.replace(from, to);
|
|
39
|
+
}
|
|
40
|
+
function run(input) {
|
|
41
|
+
const result = invoke(input);
|
|
42
|
+
assert.equal(result.status, 0, result.stderr);
|
|
43
|
+
assert.equal(result.stderr, '');
|
|
44
|
+
return JSON.parse(result.stdout);
|
|
45
|
+
}
|
|
46
|
+
function reject(mutator, pattern) {
|
|
47
|
+
const input = structuredClone(source);
|
|
48
|
+
mutator(input);
|
|
49
|
+
const result = invoke(input);
|
|
50
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
51
|
+
assert.equal(result.stdout, '', 'invalid input must not emit partial success JSON');
|
|
52
|
+
assert.match(result.stderr, pattern);
|
|
53
|
+
}
|
|
54
|
+
function complete(input) {
|
|
55
|
+
input.equivalence = { status: 'assumed', assumptions: ['Only modeled infrastructure cost is compared; application equivalence is an operator assumption.'] };
|
|
56
|
+
for (const scenario of input.scenarios) {
|
|
57
|
+
scenario.resources.trafficGB = '7.5';
|
|
58
|
+
scenario.categories.traffic = { status: 'priced', items: [{
|
|
59
|
+
id: 'synthetic-traffic', description: 'Test-only invented traffic tariff', quantity: '7.5', unit: 'traffic-GB', rateRub: '0.12',
|
|
60
|
+
tariff: { url: 'https://example.invalid/test-only-rate', version: 'synthetic-test', asOf: '2026-10-21', vatBasis: 'included', pageOrRow: 'test fixture only' },
|
|
61
|
+
}] };
|
|
62
|
+
}
|
|
63
|
+
return input;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test('packaged official-rate example has independent 24h subtotals, capacity and no winner', () => {
|
|
67
|
+
const result = run(source);
|
|
68
|
+
assert.deepEqual(result.scenarios.map(item => item.knownSubtotalRubExact), ['251.6616', '466.1376']);
|
|
69
|
+
assert.deepEqual(result.scenarios.map(item => item.categories.storage.totalRubExact), ['22.8384', '30.4512']);
|
|
70
|
+
assert.deepEqual(result.scenarios.map(item => item.capacity.normalSlots), [6, 6]);
|
|
71
|
+
assert.deepEqual(result.scenarios.map(item => item.capacity.survivingSlots), [4, 4]);
|
|
72
|
+
assert.deepEqual(result.scenarios.map(item => item.capacity.feasible), [true, true]);
|
|
73
|
+
assert.deepEqual(result.scenarios.map(item => item.unpriced.map(entry => entry.category)), [['traffic'], ['traffic']]);
|
|
74
|
+
assert.equal(result.comparison.eligible, false);
|
|
75
|
+
assert.equal(result.comparison.winner, null);
|
|
76
|
+
assert.equal(result.comparison.savingsRubExact, null);
|
|
77
|
+
assert.match(result.scenarios[0].categories.compute.items[0].formula, /24 hours/);
|
|
78
|
+
assert.equal(result.scenarios[1].categories.compute.items[0].tariff.version, '260918');
|
|
79
|
+
assert.match(result.scenarios[0].categories.compute.items[0].formula, /3 instance × 2\.9707 RUB\/instance-hour × 24 hours/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('human output includes modeled formulas, tariff provenance, unknowns and assumptions', () => {
|
|
83
|
+
const dir = directory(), path = join(dir, 'input.json'); writeFileSync(path, JSON.stringify(source));
|
|
84
|
+
const result = spawnSync(process.execPath, [cli, 'cost-compare', '--input', path], { cwd: dir, encoding: 'utf8' });
|
|
85
|
+
assert.equal(result.status, 0, result.stderr);
|
|
86
|
+
assert.match(result.stdout, /3 instance × 2\.9707 RUB\/instance-hour × 24 hours/);
|
|
87
|
+
assert.match(result.stdout, /evolution-compute\.pdf version 261014/);
|
|
88
|
+
assert.match(result.stdout, /unpriced traffic/);
|
|
89
|
+
assert.match(result.stdout, /assumption:/);
|
|
90
|
+
assert.match(result.stdout, /No winner/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('fractional horizon keeps exact sums and rounds display only at output', () => {
|
|
94
|
+
const input = structuredClone(source); input.horizonHours = '1.5';
|
|
95
|
+
const result = run(input);
|
|
96
|
+
assert.deepEqual(result.scenarios.map(item => item.knownSubtotalRubExact), ['15.72885', '29.1336']);
|
|
97
|
+
assert.deepEqual(result.scenarios.map(item => item.categories.storage.totalRubExact), ['1.4274', '1.9032']);
|
|
98
|
+
assert.deepEqual(result.scenarios.map(item => item.knownSubtotalRubDisplay), ['15.73', '29.13']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('test-only traffic is charged once, then conditional lower modeled cost is identified', () => {
|
|
102
|
+
for (const [horizon, expected, savings] of [
|
|
103
|
+
['24', ['252.5616', '467.0376'], '214.476'],
|
|
104
|
+
['1.5', ['16.62885', '30.0336'], '13.40475'],
|
|
105
|
+
]) {
|
|
106
|
+
const input = complete(structuredClone(source)); input.horizonHours = horizon;
|
|
107
|
+
const result = run(input);
|
|
108
|
+
assert.deepEqual(result.scenarios.map(item => item.knownSubtotalRubExact), expected);
|
|
109
|
+
assert.deepEqual(result.scenarios.map(item => item.categories.traffic.totalRubExact), ['0.9', '0.9']);
|
|
110
|
+
assert.equal(result.comparison.eligible, true);
|
|
111
|
+
assert.equal(result.comparison.winner, 'vm');
|
|
112
|
+
assert.equal(result.comparison.savingsRubExact, savings);
|
|
113
|
+
assert.match(result.comparison.basis, /conditional/);
|
|
114
|
+
assert.deepEqual(result.equivalence.assumptions, input.equivalence.assumptions);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('aggregate CPU cannot conceal per-node packing failure', () => {
|
|
119
|
+
const input = complete(structuredClone(source));
|
|
120
|
+
input.workload.replicas = 4;
|
|
121
|
+
input.workload.cpuPerReplica = '1.5';
|
|
122
|
+
const result = run(input);
|
|
123
|
+
assert.deepEqual(result.scenarios.map(item => item.capacity.normalSlots), [3, 3]);
|
|
124
|
+
assert.deepEqual(result.scenarios.map(item => item.capacity.feasible), [false, false]);
|
|
125
|
+
assert.equal(result.comparison.winner, null);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('one-node failure capacity is distinct from normal capacity', () => {
|
|
129
|
+
const input = complete(structuredClone(source));
|
|
130
|
+
input.workload.replicas = 5;
|
|
131
|
+
input.workload.survivingReplicas = 5;
|
|
132
|
+
const failed = run(input);
|
|
133
|
+
assert.deepEqual(failed.scenarios.map(item => item.capacity.normalSlots), [6, 6]);
|
|
134
|
+
assert.deepEqual(failed.scenarios.map(item => item.capacity.survivingSlots), [4, 4]);
|
|
135
|
+
assert.equal(failed.comparison.winner, null);
|
|
136
|
+
input.workload.survivingReplicas = 4;
|
|
137
|
+
const fit = run(input);
|
|
138
|
+
assert.deepEqual(fit.scenarios.map(item => item.capacity.feasible), [true, true]);
|
|
139
|
+
assert.equal(fit.comparison.winner, 'vm');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('guaranteed CPU share, reservation and exact decimal floor determine per-node slots', () => {
|
|
143
|
+
const input = complete(structuredClone(source));
|
|
144
|
+
for (const scenario of input.scenarios) {
|
|
145
|
+
scenario.nodes.cpu = '4'; scenario.nodes.cpuShare = '0.5'; scenario.nodes.reservedCpu = '0.5';
|
|
146
|
+
}
|
|
147
|
+
input.workload.cpuPerReplica = '0.5';
|
|
148
|
+
const result = run(input);
|
|
149
|
+
assert.deepEqual(result.scenarios.map(item => item.capacity.perNodeCpuSlots), [3, 3]);
|
|
150
|
+
const tenth = complete(structuredClone(source));
|
|
151
|
+
for (const scenario of tenth.scenarios) { scenario.nodes.cpu = '0.3'; scenario.nodes.cpuShare = '1'; }
|
|
152
|
+
tenth.workload.cpuPerReplica = '0.1';
|
|
153
|
+
assert.deepEqual(run(tenth).scenarios.map(item => item.capacity.perNodeCpuSlots), [3, 3]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('a tie is explicit and carries no winner or savings', () => {
|
|
157
|
+
const input = complete(structuredClone(source));
|
|
158
|
+
const vm = input.scenarios[0];
|
|
159
|
+
const k8s = input.scenarios[1];
|
|
160
|
+
// Test-only unit rates: 66 VM hourly units versus 87 K8s hourly units,
|
|
161
|
+
// a 21 × 24 = 504 RUB difference, offset by 7.5 × (67.32 − 0.12).
|
|
162
|
+
vm.categories.compute.items[0].rateRub = '1';
|
|
163
|
+
vm.categories.storage.items[0].rateRub = '1';
|
|
164
|
+
vm.categories.publicIp.items[0].rateRub = '1';
|
|
165
|
+
k8s.categories.compute.items[0].rateRub = '1';
|
|
166
|
+
k8s.categories.controlPlane.items[0].rateRub = '1';
|
|
167
|
+
k8s.categories.storage.items[0].rateRub = '1';
|
|
168
|
+
k8s.categories.publicIp.items[0].rateRub = '1';
|
|
169
|
+
k8s.categories.snat.items[0].rateRub = '1';
|
|
170
|
+
// VM has 66 hourly units; K8s has 87. Add the same 7.5GB traffic to both; tie via VM traffic premium.
|
|
171
|
+
vm.categories.traffic.items[0].rateRub = '67.32';
|
|
172
|
+
k8s.categories.traffic.items[0].rateRub = '0.12';
|
|
173
|
+
const result = run(input);
|
|
174
|
+
assert.deepEqual(result.scenarios.map(item => item.knownSubtotalRubExact), ['2088.9', '2088.9']);
|
|
175
|
+
assert.equal(result.comparison.eligible, true);
|
|
176
|
+
assert.equal(result.comparison.tie, true);
|
|
177
|
+
assert.equal(result.comparison.winner, null);
|
|
178
|
+
assert.equal(result.comparison.savingsRubExact, null);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('unknown equivalence alone suppresses a winner even with complete prices', () => {
|
|
182
|
+
const input = complete(structuredClone(source));
|
|
183
|
+
input.equivalence = source.equivalence;
|
|
184
|
+
const result = run(input);
|
|
185
|
+
assert.deepEqual(result.scenarios.map(item => item.complete), [true, true]);
|
|
186
|
+
assert.equal(result.comparison.winner, null);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('raw flags reject duplicates, missing values, unknown options and positionals', () => {
|
|
190
|
+
const dir = directory(), path = join(dir, 'input.json'); writeFileSync(path, JSON.stringify(source));
|
|
191
|
+
for (const flags of [[], ['--input'], ['--input', path, '--input', path], ['--input', path, '--json', '--json'], ['--input', path, '--rate', '1'], ['--input', path, 'extra']]) {
|
|
192
|
+
const result = spawnSync(process.execPath, [cli, 'cost-compare', ...flags], { cwd: dir, encoding: 'utf8' });
|
|
193
|
+
assert.equal(result.status, 1, JSON.stringify(flags)); assert.equal(result.stdout, ''); assert.match(result.stderr, /cost-compare/);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('input must be a bounded regular file and cannot be a symlink', () => {
|
|
198
|
+
const dir = directory();
|
|
199
|
+
const large = join(dir, 'large.json'); writeFileSync(large, ' '.repeat(128 * 1024 + 1));
|
|
200
|
+
const link = join(dir, 'link.json'); symlinkSync(example, link);
|
|
201
|
+
for (const path of [large, link, '/dev/null']) {
|
|
202
|
+
const result = spawnSync(process.execPath, [cli, 'cost-compare', '--input', path, '--json'], { encoding: 'utf8' });
|
|
203
|
+
assert.equal(result.status, 1, path); assert.equal(result.stdout, ''); assert.match(result.stderr, /cost-compare/);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
for (const [name, raw, pattern] of [
|
|
208
|
+
['top-level duplicate horizon', editRaw('"horizonHours": "24"', '"horizonHours": "8760", "horizonHours": "24"'), /duplicate decoded object member/],
|
|
209
|
+
['nested duplicate CPU declaration', editRaw('"cpu": "2", "ramGB": "4"', '"cpu": "4", "cpu": "2", "ramGB": "4"'), /duplicate decoded object member/],
|
|
210
|
+
['sibling scenario duplicate id', editRaw('"id": "k8s",', '"id": "other", "id": "k8s",'), /duplicate decoded object member/],
|
|
211
|
+
['escaped-equivalent duplicate member', editRaw('"horizonHours": "24"', '"horizonHours": "8760", "horizon\\u0048ours": "24"'), /duplicate decoded object member/],
|
|
212
|
+
['rounded fractional replica token', JSON.stringify(source).replace('"replicas":3', '"replicas":3.0000000000000001'), /numeric tokens must be canonical/],
|
|
213
|
+
['unsafe integer replica token', JSON.stringify(source).replace('"replicas":3', '"replicas":9007199254740993'), /numeric tokens must be canonical/],
|
|
214
|
+
['infinite exponent token', JSON.stringify(source).replace('"replicas":3', '"replicas":1e309'), /numeric tokens must be canonical/],
|
|
215
|
+
['malformed exponent token', JSON.stringify(source).replace('"replicas":3', '"replicas":3e+'), /object member requires/],
|
|
216
|
+
['excessive JSON nesting', `${'['.repeat(65)}0${']'.repeat(65)}`, /nesting exceeds/],
|
|
217
|
+
['raw numeric value for decimal-string horizon', editRaw('"horizonHours": "24"', '"horizonHours": 24'), /horizonHours/],
|
|
218
|
+
]) test(`raw JSON rejects ${name} before accepting a result`, () => {
|
|
219
|
+
const result = invokeRaw(raw);
|
|
220
|
+
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
221
|
+
assert.equal(result.stdout, '');
|
|
222
|
+
assert.match(result.stderr, pattern);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('canonical safe integer tokens and quoted braces/commas remain accepted', () => {
|
|
226
|
+
const input = structuredClone(source);
|
|
227
|
+
input.workload.replicas = 3;
|
|
228
|
+
input.equivalence.reason = 'unknown: text contains "quotes", braces { } and comma, without object members';
|
|
229
|
+
const result = invokeRaw(JSON.stringify(input));
|
|
230
|
+
assert.equal(result.status, 0, result.stderr);
|
|
231
|
+
assert.equal(JSON.parse(result.stdout).workload.replicas, 3);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('CLI path cannot resolve an engine, spawn, access network or require credentials', () => {
|
|
235
|
+
const dir = directory(), preload = join(dir, 'forbid.cjs');
|
|
236
|
+
writeFileSync(preload, `
|
|
237
|
+
const Module = require('module'); const original = Module._load;
|
|
238
|
+
Module._load = function(request, parent, main) {
|
|
239
|
+
if (parent?.filename.endsWith('/src/cli.js') && ['./resolve','./classification','./install','./safe-mcp','./deploy-approval','./k8s-approval','./dialects'].includes(request)) throw Error('legacy engine/auth module load forbidden');
|
|
240
|
+
if (request === 'yaml') throw Error('external dependency load forbidden');
|
|
241
|
+
return original.apply(this, arguments);
|
|
242
|
+
};
|
|
243
|
+
const child = require('child_process'); for (const name of ['spawn','spawnSync','exec','execFile']) child[name] = () => { throw Error('spawn forbidden'); };
|
|
244
|
+
for (const name of ['http','https','net']) { const mod = require(name); for (const key of ['request','get','connect','createConnection']) if (key in mod) mod[key] = () => { throw Error('network forbidden'); }; }
|
|
245
|
+
global.fetch = () => { throw Error('fetch forbidden'); };
|
|
246
|
+
`);
|
|
247
|
+
const input = join(dir, 'input.json'); writeFileSync(input, JSON.stringify(source));
|
|
248
|
+
const result = spawnSync(process.execPath, ['--require', preload, cli, 'cost-compare', '--input', input, '--json'], {
|
|
249
|
+
cwd: dir, env: { PATH: '/nonexistent', CLOUDRU_VM_BIN: '/no-engine', CLOUDRU_HUB_CONFIG: '/no-config' }, encoding: 'utf8', timeout: 5000,
|
|
250
|
+
});
|
|
251
|
+
assert.ifError(result.error); assert.equal(result.status, 0, result.stderr);
|
|
252
|
+
assert.equal(JSON.parse(result.stdout).comparison.winner, null);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
for (const [label, mutate, pattern] of [
|
|
256
|
+
['unknown schema field', x => { x.hidden = true; }, /unknown field/],
|
|
257
|
+
['non-RUB currency', x => { x.currency = 'USD'; }, /currency/],
|
|
258
|
+
['different VAT basis', x => { x.vatBasis = 'excluded'; }, /vatBasis/],
|
|
259
|
+
['scientific rate', x => { x.scenarios[0].categories.compute.items[0].rateRub = '1e3'; }, /rateRub/],
|
|
260
|
+
['monthly unit', x => { x.scenarios[0].categories.compute.items[0].unit = 'instance-month'; }, /unit/],
|
|
261
|
+
['wrong unit category', x => { x.scenarios[0].categories.storage.items[0].unit = 'instance-hour'; }, /unit/],
|
|
262
|
+
['missing tariff version', x => { delete x.scenarios[0].categories.compute.items[0].tariff.version; }, /version/],
|
|
263
|
+
['HTTP tariff URL', x => { x.scenarios[0].categories.compute.items[0].tariff.url = 'http://example.invalid'; }, /url/],
|
|
264
|
+
['wrong tariff VAT basis', x => { x.scenarios[0].categories.compute.items[0].tariff.vatBasis = 'excluded'; }, /vatBasis/],
|
|
265
|
+
['unbound worker quantity', x => { x.scenarios[0].categories.compute.items[0].quantity = '2'; }, /quantities must sum/],
|
|
266
|
+
['unbound storage quantity', x => { x.scenarios[0].categories.storage.items[0].quantity = '59'; }, /quantities must sum/],
|
|
267
|
+
['unbound IP quantity', x => { x.scenarios[0].categories.publicIp.items[0].quantity = '2'; }, /quantities must sum/],
|
|
268
|
+
['noninteger service count', x => { x.scenarios[0].categories.compute.items[0].quantity = '3.5'; }, /integer service count/],
|
|
269
|
+
['missing K8s control plane', x => { x.scenarios[1].controlPlaneCount = 0; }, /controlPlaneCount/],
|
|
270
|
+
['unpriced compute with zero declaration', x => { x.scenarios[0].nodes.count = 0; }, /nodes.count/],
|
|
271
|
+
['not-used positive storage', x => { x.scenarios[0].categories.storage = { status: 'not-used', reason: 'omitted' }; }, /not-used requires/],
|
|
272
|
+
['unknown traffic silently zero', x => { x.scenarios[0].resources.trafficGB = '0'; }, /declared zero quantity/],
|
|
273
|
+
['priced unknown traffic', x => { x.scenarios[0].categories.traffic = { status: 'priced', items: [] }; }, /priced requires/],
|
|
274
|
+
['oversubscribed reservations', x => { x.scenarios[0].nodes.reservedCpu = '3'; }, /reservations exceed/],
|
|
275
|
+
['declared node failure exceeds topology', x => { x.workload.failedNodes = 4; }, /failedNodes/],
|
|
276
|
+
['bad CPU share', x => { x.scenarios[0].nodes.cpuShare = '1.1'; }, /cpuShare/],
|
|
277
|
+
['survival above replicas', x => { x.workload.survivingReplicas = 4; }, /survivingReplicas/],
|
|
278
|
+
['duplicate kinds', x => { x.scenarios[1].kind = 'vm'; x.scenarios[1].controlPlaneCount = 0; x.scenarios[1].categories.controlPlane = { status: 'not-used', reason: 'none' }; }, /one scenario of each kind/],
|
|
279
|
+
['missing equivalence assumption', x => { x.equivalence = { status: 'assumed', assumptions: [] }; }, /assumptions/],
|
|
280
|
+
]) test(`rejects ${label} without partial output`, () => reject(mutate, pattern));
|
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
|
@@ -13,6 +13,15 @@ const require = createRequire(import.meta.url);
|
|
|
13
13
|
const { resolveEngine, sha256File, platformKey } = require('../src/resolve.js');
|
|
14
14
|
const PKG = require('../package.json');
|
|
15
15
|
|
|
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
|
+
|
|
16
25
|
// Every scratch dir is removed at the end (after() + process exit) unless DZ_KEEP_TMP=1 —
|
|
17
26
|
// MEASURED 2026-09-20: 18 cloudru-resolve-* dirs per full sweep (backlog 7cb6f9b7).
|
|
18
27
|
const created = [];
|
|
@@ -91,19 +100,17 @@ test('config enginePath is used when env is unset', () => {
|
|
|
91
100
|
|
|
92
101
|
test('nothing resolvable → loud error naming all three paths and the platform-package status', () => {
|
|
93
102
|
const dir = tmp();
|
|
94
|
-
const r =
|
|
103
|
+
const r = resolveWithoutOptionalPlatform({ CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
|
|
95
104
|
assert.ok(r.error);
|
|
96
105
|
assert.match(r.error, /CLOUDRU_VM_BIN/);
|
|
97
106
|
assert.match(r.error, /enginePath/);
|
|
98
107
|
assert.match(r.error, /never bundled/);
|
|
99
|
-
|
|
100
|
-
// build — it is no longer a licence hold, and the error must say so honestly.
|
|
101
|
-
assert.match(r.error, /ADR-002 trusted-CI/);
|
|
108
|
+
assert.match(r.error, /optional dependencies/);
|
|
102
109
|
});
|
|
103
110
|
|
|
104
111
|
test('a dangling env path falls through to the error, not a crash', () => {
|
|
105
112
|
const dir = tmp();
|
|
106
|
-
const r =
|
|
113
|
+
const r = resolveWithoutOptionalPlatform({ CLOUDRU_VM_BIN: path.join(dir, 'nope'), CLOUDRU_HUB_CONFIG: path.join(dir, 'absent.json') });
|
|
107
114
|
assert.ok(r.error);
|
|
108
115
|
assert.match(r.error, /set, but not a file/);
|
|
109
116
|
});
|
|
@@ -118,6 +125,6 @@ test('sha256File matches node crypto', () => {
|
|
|
118
125
|
test('the pinned linux-x64 hash is the rebuilt private engine sha256', () => {
|
|
119
126
|
assert.equal(
|
|
120
127
|
PKG.cloudruHub.binaryHashes['linux-x64'],
|
|
121
|
-
'
|
|
128
|
+
'4463b09323c9dfe0c32d618d335734a947e7fab0a75d07a7bea089b97a97c9b6',
|
|
122
129
|
);
|
|
123
130
|
});
|