- execute-command/upload/download accept host/port/username/password inline to open an ephemeral connection for a single call - add redactSecret/redactSecrets to mask credentials in logs and errors - logger log()/handleError() accept optional secrets[] for redaction - add SSH_CONFIG_MISSING error code and --password-from-env CLI flag - dynamic-mode startup: server runs with no static config
160 lines
5.0 KiB
JavaScript
160 lines
5.0 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert';
|
|
import { collectSystemStatus } from '../build/utils/status-collector.js';
|
|
|
|
const PROBE_PATTERN = /printf '\\n(__MCP_FIELD_\w+_)(\w+)\\n'/g;
|
|
|
|
/** Field names in the order the script probes them, plus the shared marker. */
|
|
function readProbes(script) {
|
|
const probes = [...script.matchAll(PROBE_PATTERN)];
|
|
return {
|
|
marker: probes[0]?.[1],
|
|
fields: probes.map((probe) => probe[2]),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Stand-in for the remote shell: emits the marker line for every probe the
|
|
* script declares, followed by whatever `values` supplies for it.
|
|
*/
|
|
function fakeRemote(values, { lineEnding = '\n' } = {}) {
|
|
return (script) => {
|
|
const { marker, fields } = readProbes(script);
|
|
const output = fields
|
|
.map((field) => `${marker}${field}${lineEnding}${values[field] ?? ''}`)
|
|
.join(lineEnding);
|
|
return Promise.resolve(lineEnding + output);
|
|
};
|
|
}
|
|
|
|
describe('status collector', () => {
|
|
it('所有探针合并为一条单行命令', async () => {
|
|
const scripts = [];
|
|
await collectSystemStatus((script) => {
|
|
scripts.push(script);
|
|
return Promise.resolve('');
|
|
}, 'dev');
|
|
|
|
assert.strictEqual(scripts.length, 1);
|
|
// 多行脚本会被 commandTemplate 的引号包裹破坏
|
|
assert.ok(!scripts[0].includes('\n'));
|
|
// 每个探针都被隔离,单个失败不影响其余,且整体以成功退出
|
|
assert.ok(scripts[0].endsWith('; true'));
|
|
|
|
const { fields } = readProbes(scripts[0]);
|
|
assert.ok(fields.includes('hostname'));
|
|
assert.ok(fields.includes('servicesInstalled'));
|
|
assert.strictEqual(new Set(fields).size, fields.length);
|
|
});
|
|
|
|
it('按 marker 还原各字段', async () => {
|
|
const status = await collectSystemStatus(
|
|
fakeRemote({
|
|
hostname: 'web-01',
|
|
osName: 'Linux',
|
|
kernelVersion: '6.1.0',
|
|
memory: 'free:2.1G total:7.7G',
|
|
processes: '181',
|
|
threads: '901',
|
|
}),
|
|
'dev',
|
|
);
|
|
|
|
assert.strictEqual(status.reachable, true);
|
|
assert.strictEqual(status.hostname, 'web-01');
|
|
assert.strictEqual(status.osName, 'Linux');
|
|
assert.strictEqual(status.kernelVersion, '6.1.0');
|
|
assert.deepStrictEqual(status.memory, { free: '2.1G', total: '7.7G' });
|
|
// 解析时会各减去一行表头
|
|
assert.deepStrictEqual(status.processes, { running: 180, threads: 900 });
|
|
});
|
|
|
|
// 多行字段是 CRLF 归一化真正起作用的地方:逐行拆分的值不会再单独 trim,
|
|
// 残留的 \r 会直接进入结果。
|
|
it('pty 的 CRLF 输出同样能解析', async () => {
|
|
const status = await collectSystemStatus(
|
|
fakeRemote(
|
|
{
|
|
hostname: 'web-02',
|
|
ipAddresses: '10.0.0.7\r\n192.168.1.5',
|
|
drives: '/dev/sda1|50G|20G|30G|40%|/',
|
|
},
|
|
{ lineEnding: '\r\n' },
|
|
),
|
|
'dev',
|
|
);
|
|
|
|
assert.strictEqual(status.hostname, 'web-02');
|
|
assert.deepStrictEqual(status.ipAddresses, ['10.0.0.7', '192.168.1.5']);
|
|
assert.deepStrictEqual(status.drives, [
|
|
{
|
|
device: '/dev/sda1',
|
|
total: '50G',
|
|
used: '20G',
|
|
free: '30G',
|
|
usagePercent: '40%',
|
|
mountPoint: '/',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('空探针不影响其它字段', async () => {
|
|
const status = await collectSystemStatus(
|
|
fakeRemote({ hostname: 'web-03', ipAddresses: '', osName: 'Linux' }),
|
|
'dev',
|
|
);
|
|
|
|
assert.strictEqual(status.hostname, 'web-03');
|
|
assert.strictEqual(status.osName, 'Linux');
|
|
assert.strictEqual(status.ipAddresses, undefined);
|
|
});
|
|
|
|
// 命令白名单会拒绝这条探针脚本;那种情况下字段留空即可,
|
|
// 不能把主机报成不可达。
|
|
it('命令被拒绝时字段留空但仍视为可达', async () => {
|
|
const status = await collectSystemStatus(
|
|
() => Promise.reject(new Error('Command validation failed')),
|
|
'dev',
|
|
);
|
|
|
|
assert.strictEqual(status.reachable, true);
|
|
assert.strictEqual(status.hostname, undefined);
|
|
assert.ok(status.lastUpdated);
|
|
});
|
|
|
|
it('先逐条授权探针,再只批量执行允许的命令', async () => {
|
|
const scripts = [];
|
|
const recordingRemote = fakeRemote({ hostname: 'allowed-host' });
|
|
const status = await collectSystemStatus(
|
|
(script) => {
|
|
scripts.push(script);
|
|
return recordingRemote(script);
|
|
},
|
|
'dev',
|
|
(command) => command === 'hostname',
|
|
);
|
|
|
|
assert.strictEqual(status.hostname, 'allowed-host');
|
|
assert.strictEqual(scripts.length, 1);
|
|
assert.match(scripts[0], /hostname/);
|
|
assert.ok(!scripts[0].includes('uname -s'));
|
|
assert.ok(!scripts[0].includes('cat /etc/os-release'));
|
|
});
|
|
|
|
it('没有获准探针时不执行远端脚本', async () => {
|
|
let calls = 0;
|
|
const status = await collectSystemStatus(
|
|
async () => {
|
|
calls += 1;
|
|
return '';
|
|
},
|
|
'dev',
|
|
() => false,
|
|
);
|
|
|
|
assert.strictEqual(calls, 0);
|
|
assert.strictEqual(status.reachable, true);
|
|
assert.strictEqual(status.hostname, undefined);
|
|
});
|
|
});
|