Files
ProxMenux/tests/test_audit_policy.cjs
T
MacRimiandClaude Opus 5 da8a480eff Add audit and reports page, and a change journal
ProxMenux modifies the host: it rewrites configuration files, installs packages, enables services. Until now nobody could say afterwards what had changed, and showing the script does not answer that question — a four-hundred-line function may alter two values, and the reader has no way to know which two. This adds the two halves of an answer.

The change journal records what ProxMenux does as it does it. Eleven bash primitives capture the previous state, apply the change and record it in the same step, writing to a spool that the Monitor reads back. One hundred and thirteen functions across twenty-five scripts are instrumented, covering post-install, shared storage, security tooling, container conversions, disk operations and the PVE 8 to 9 upgrade path. The page shows the difference — rotate 7 becoming rotate 14 — and never the script. Restore and backup scripts are deliberately left out: a restore puts the host back to a state some other script already recorded.

The Audit and reports page answers the other half: what state is this host in, regardless of who put it there. Forty-three checks across seven areas read the host and classify each result as critical, warning, observation, conformant, unverified or not applicable, with the evidence they read attached to each one. A declared policy lets the reader say what this particular host is expected to do — which guests must have a backup, which storages are essential — so the report judges the host against its own intent rather than a generic template. An inventory records the hardware, network and guest topology behind those readings, a comparison shows what moved between two runs, and six report profiles produce a printable document scoped to what the reader needs. Everything is available in the eight supported languages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 21:06:04 +02:00

123 lines
6.9 KiB
JavaScript

// Component-state regression tests with isolated hooks and a mocked API.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const app = path.resolve(__dirname, '../AppImage');
const ts = require(path.join(app, 'node_modules/typescript'));
const messages = require(path.join(app, 'messages/en/common.json'));
const t = (key, params = {}) => key.split('.').reduce((v, k) => v?.[k], messages)
.replace(/\{(\w+)\}/g, (_, k) => params[k] ?? `{${k}}`);
let cursor = 0, states = [], effects = [], initialized = false, submitted, rejectSave = false;
const snapshot = { guests: {}, storages: {}, thresholds: {},
defaults: { backup: 'required', autostart: 'not_required', storage_role: 'essential', recovery_objective_hours: 48 } };
const hooks = {
useState(initial) { const i = cursor++; if (!(i in states)) states[i] = initial;
return [states[i], v => { states[i] = typeof v === 'function' ? v(states[i]) : v; }]; },
useMemo: fn => fn(), useCallback: fn => fn,
useEffect(fn) { if (!initialized) effects.push(fn); },
};
const jsx = (type, props) => ({ type, props: props || {} });
const api = async (url, options) => {
if (options) {
submitted = JSON.parse(options.body);
if (rejectSave) throw Object.assign(new Error('conflict'), { status: 409 });
return { success: true, summary: { revision: 'second' } };
}
if (url.includes('inventory')) return { inventory: { sections: {
guests: [{ vmid: 100, name: 'fixture', type: 'lxc' }], storages: [{ id: 'pbs', type: 'pbs' }],
} } };
return { success: true, policy: snapshot, summary: { revision: 'first' }, vocabulary: {
expectations: ['required', 'not_required', 'unspecified'], roles: ['essential', 'optional', 'unspecified'],
thresholds: { storage_usage_percent: 90 },
} };
};
const mod = { exports: {} };
const source = fs.readFileSync(path.join(app, 'components/audit-policy.tsx'), 'utf8');
const js = ts.transpileModule(source, { compilerOptions: {
module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, jsx: ts.JsxEmit.ReactJSX,
} }).outputText;
new Function('require', 'module', 'exports', js)(name => {
if (name === 'react') return hooks;
if (name === 'react/jsx-runtime') return { jsx, jsxs: jsx };
if (name.endsWith('api-config')) return { fetchApi: api };
if (name.endsWith('provider')) return { useT: () => t };
return new Proxy({}, { get: (_, name) => String(name) });
}, mod, mod.exports);
function render() { cursor = 0; const tree = mod.exports.AuditPolicy(); initialized = true; return tree; }
function nodes(tree, type) {
if (!tree || typeof tree !== 'object') return [];
if (Array.isArray(tree)) return tree.flatMap(x => nodes(x, type));
if (typeof tree.type === 'function') return nodes(tree.type(tree.props), type);
return [...(tree.type === type ? [tree] : []), ...nodes(tree.props?.children, type)];
}
const change = (node, value) => node.props.onChange({ target: { value } });
// The dropdowns are the shared Select, which reports a value rather than
// an event. Unresolved imports come back as their own name, so the
// element type is the component's name.
const choose = (node, value) => node.props.onValueChange(value);
const tick = () => new Promise(resolve => setImmediate(resolve));
(async () => {
render(); effects.forEach(fn => fn()); await tick();
// The form is locked until the reader says they are editing it.
let tree = render();
assert.equal(nodes(tree, 'fieldset')[0].props.disabled, true,
'The declaration is editable before anyone asked to edit it');
const editButton = nodes(tree, 'button').find(
b => JSON.stringify(b.props.children).includes(messages.actions.edit));
assert(editButton, 'No edit button to unlock the declaration');
// A disabled fieldset disables every control it holds, so the button that
// leaves that state cannot live inside it.
assert(!nodes(nodes(tree, 'fieldset')[0], 'button').includes(editButton),
'The edit button sits inside the fieldset it unlocks, so it is never clickable');
// The dropdown governs its own opening, so the fieldset does not reach it.
assert(nodes(tree, 'Select').every(sel => sel.props.disabled === true),
'A locked declaration still opens its dropdowns');
editButton.props.onClick(); tree = render();
assert.equal(nodes(tree, 'fieldset')[0].props.disabled, false);
assert(nodes(tree, 'Select').every(sel => sel.props.disabled === false),
'Editing does not unlock the dropdowns');
let select = nodes(tree, 'Select');
assert.equal(select[0].props.value, 'inherit');
const inherited = (value) => messages.audit.policy.inherit.replace('{value}', value);
assert.equal(nodes(select[0], 'SelectItem')[0].props.children, inherited('Required'));
assert.equal(nodes(select[2], 'SelectItem')[0].props.children, inherited('Essential'));
choose(select[0], 'unspecified'); choose(select[2], 'unspecified');
tree = render();
assert.equal(nodes(tree, 'Select')[0].props.value, 'unspecified');
nodes(tree, 'form')[0].props.onSubmit({ preventDefault() {} }); await tick();
assert.equal(submitted.expected_revision, 'first');
assert.equal(submitted.guests['100'].backup, 'unspecified');
assert.equal(submitted.storages.pbs.role, 'unspecified');
tree = render(); choose(nodes(tree, 'Select')[0], 'inherit');
tree = render();
const inputs = nodes(tree, 'input');
assert.equal(inputs[0].props.placeholder, '48');
assert.equal(inputs[1].props.max, 100);
change(inputs[1], '-1'); tree = render();
assert.equal(nodes(tree, 'input')[1].props.value, -1, 'Invalid value is not silently cleared');
change(nodes(tree, 'input')[1], ''); tree = render();
rejectSave = true;
nodes(tree, 'form')[0].props.onSubmit({ preventDefault() {} }); await tick(); tree = render();
assert.equal(submitted.expected_revision, 'second');
assert.equal(submitted.guests['100'], undefined);
assert.equal(nodes(tree, 'fieldset')[0].props.disabled, true);
assert(JSON.stringify(tree).includes(messages.audit.policy.conflict));
assert(JSON.stringify(tree).includes(messages.audit.policy.reload));
// With nothing declared site-wide there is no value to name, and the
// explicit option that would say the same thing is not offered twice.
Object.assign(snapshot.defaults, { backup: undefined, autostart: undefined,
storage_role: undefined });
cursor = 0; states = []; effects = []; initialized = false;
render(); effects.forEach(fn => fn()); await tick(); tree = render();
select = nodes(tree, 'Select');
const first = nodes(select[0], 'SelectItem');
assert.equal(first[0].props.children, messages.audit.policy.inheritUnset,
'The default option names a value nobody declared');
assert(!first.slice(1).some(i => i.props.value === 'unspecified'),
'The dropdown offers the same outcome twice');
console.log('Policy UI: inheritance, explicit unspecified, numeric constraints, revision and conflict tests passed');
})().catch(error => { console.error(error); process.exitCode = 1; });