mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
652 lines
19 KiB
JavaScript
652 lines
19 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { spawn, spawnSync } = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
buildServiceStartReport,
|
|
installContract,
|
|
normalizeServiceStartManifest,
|
|
normalizeSession,
|
|
parseArguments,
|
|
parseOpenRcState,
|
|
parseSystemdShow,
|
|
parseWrapperObservations,
|
|
renderEvidenceWrapper,
|
|
validateServiceStartReport,
|
|
} = require('../../scripts/ql3-physical-edge-service-start.cjs');
|
|
const {
|
|
canonicalDigest,
|
|
} = require('../../scripts/ql3-physical-edge-evidence.cjs');
|
|
|
|
const packages = [
|
|
'@qinglong/local-admin',
|
|
'@qinglong/local-application',
|
|
'@qinglong/local-command-file',
|
|
'@qinglong/local-execution',
|
|
'@qinglong/local-process',
|
|
'@qinglong/local-secret',
|
|
'@qinglong/local-sqlite',
|
|
'@qinglong/runtime-core',
|
|
'croner',
|
|
'semver',
|
|
];
|
|
|
|
function artifact() {
|
|
return {
|
|
artifactSha256: 'a'.repeat(64),
|
|
artifactMetadataSha256: 'b'.repeat(64),
|
|
artifactFiles: 627,
|
|
artifactBytes: 5_045_360,
|
|
entrypointSha256: 'c'.repeat(64),
|
|
packages,
|
|
};
|
|
}
|
|
|
|
function manifest(overrides = {}) {
|
|
return normalizeServiceStartManifest({
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_service_start_candidate',
|
|
profile: 'edge',
|
|
deviceId: 'router-a1',
|
|
serviceManager: 'systemd',
|
|
expectedArchitecture: 'arm64',
|
|
expectedFilesystem: 'ext4',
|
|
expectedArtifactSha256: 'a'.repeat(64),
|
|
expectedArtifactFiles: 627,
|
|
expectedArtifactBytes: 5_045_360,
|
|
expectedNodeSha256: 'd'.repeat(64),
|
|
maximumBootToActiveMs: 180_000,
|
|
maximumServiceStartBootAgeMs: 60_000,
|
|
maximumServiceStartToActiveMs: 30_000,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function environment(bootId) {
|
|
return {
|
|
platform: 'linux',
|
|
architecture: 'arm64',
|
|
bootId,
|
|
bootAgeMs: 15_000,
|
|
dataFilesystem: 'ext4',
|
|
nodeExecutable: '/usr/bin/node',
|
|
nodeSha256: 'd'.repeat(64),
|
|
nodeVersion: 'v24.18.0',
|
|
virtualizationIndicators: [],
|
|
};
|
|
}
|
|
|
|
function sessionFixture() {
|
|
const sessionId = '019f0000-0000-4000-8000-000000000020';
|
|
const dataPath = '/mnt/ql3-evidence';
|
|
const deploymentRoot = `${dataPath}/.ql3-service-start-deployment-${sessionId}`;
|
|
const toolRoot = `${deploymentRoot}/physical-service-start`;
|
|
const serviceName = 'qinglong3-physical-019f0000';
|
|
const body = {
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_service_start_session',
|
|
sessionId,
|
|
manifestDigest: canonicalDigest(manifest()),
|
|
uid: 1000,
|
|
preparedAt: '2026-07-29T08:00:00.000Z',
|
|
artifact: artifact(),
|
|
environment: environment('019f0000-0000-4000-8000-000000000001'),
|
|
paths: {
|
|
dataPath,
|
|
toolRoot,
|
|
deploymentRoot,
|
|
artifactRoot: '/opt/qinglong3-release',
|
|
applicationEntrypoint:
|
|
'/opt/qinglong3-release/node_modules/@qinglong/local-application/dist/cli.js',
|
|
applicationConfig: `${deploymentRoot}/local-application.json`,
|
|
wrapper: `${toolRoot}/boot-probe.sh`,
|
|
wrapperStartRecord: `${toolRoot}/wrapper-start.record`,
|
|
nodeRecord: `${toolRoot}/node.record`,
|
|
activeRecord: `${toolRoot}/active.record`,
|
|
eventLog: `${toolRoot}/events.jsonl`,
|
|
stderrLog: `${toolRoot}/stderr.log`,
|
|
fifo: `${toolRoot}/events.fifo`,
|
|
},
|
|
service: {
|
|
kind: 'systemd',
|
|
serviceName,
|
|
managerExecutable: '/usr/bin/systemctl',
|
|
managerSha256: 'e'.repeat(64),
|
|
enableExecutable: '/usr/bin/systemctl',
|
|
enableSha256: 'e'.repeat(64),
|
|
descriptorSource: `${deploymentRoot}/service/qinglong3.service`,
|
|
descriptorDestination: `/etc/systemd/system/${serviceName}.service`,
|
|
descriptorMode: 0o644,
|
|
descriptorSha256: 'f'.repeat(64),
|
|
wrapperSha256: '1'.repeat(64),
|
|
installArguments: [
|
|
'-o',
|
|
'root',
|
|
'-g',
|
|
'root',
|
|
'-m',
|
|
'644',
|
|
`${deploymentRoot}/service/qinglong3.service`,
|
|
`/etc/systemd/system/${serviceName}.service`,
|
|
],
|
|
enableArguments: ['enable', serviceName],
|
|
},
|
|
};
|
|
return normalizeSession({ ...body, sha256: canonicalDigest(body) });
|
|
}
|
|
|
|
function reportFixture(overrides = {}) {
|
|
const session = sessionFixture();
|
|
const after = environment('019f0000-0000-4000-8000-000000000002');
|
|
return buildServiceStartReport({
|
|
manifest: manifest(),
|
|
session,
|
|
observed: {
|
|
after,
|
|
artifact: artifact(),
|
|
process: {
|
|
bootId: after.bootId,
|
|
wrapperPid: 101,
|
|
wrapperStartTicks: 1200,
|
|
nodePid: 102,
|
|
nodeStartTicks: 1201,
|
|
},
|
|
service: {
|
|
kind: 'systemd',
|
|
serviceName: session.service.serviceName,
|
|
managerExecutable: '/usr/bin/systemctl',
|
|
managerSha256: 'e'.repeat(64),
|
|
descriptorSha256: 'f'.repeat(64),
|
|
mainPid: 101,
|
|
},
|
|
},
|
|
measurements: {
|
|
serviceStartBootAgeMs: 12_000,
|
|
activeBootAgeMs: 13_250,
|
|
bootToActiveMs: 13_250,
|
|
serviceStartToActiveMs: 1_250,
|
|
activeEventOrdinal: 2,
|
|
},
|
|
outcomes: {
|
|
activeEventCount: 1,
|
|
aiStatus: 'deployment_excluded',
|
|
descriptorInstalled: true,
|
|
serviceActive: true,
|
|
serviceEnabled: true,
|
|
wrapperProcessIdentityMatched: true,
|
|
nodeProcessIdentityMatched: true,
|
|
stderrBytes: 0,
|
|
},
|
|
generatedAt: '2026-07-29T08:01:00.000Z',
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
test('normalizes bounded systemd and OpenRC boot budgets', () => {
|
|
assert.equal(manifest().serviceManager, 'systemd');
|
|
assert.equal(manifest({ serviceManager: 'openrc' }).serviceManager, 'openrc');
|
|
assert.throws(
|
|
() => manifest({ maximumBootToActiveMs: 601_000 }),
|
|
/measurement budget/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
manifest({
|
|
maximumBootToActiveMs: 20_000,
|
|
maximumServiceStartToActiveMs: 30_000,
|
|
}),
|
|
/measurement budget/,
|
|
);
|
|
});
|
|
|
|
test('requires phase-specific absolute paths', () => {
|
|
assert.deepEqual(
|
|
parseArguments([
|
|
'inspect',
|
|
'--artifact-root=/opt/qinglong3-release',
|
|
'--json',
|
|
]),
|
|
{
|
|
phase: 'inspect',
|
|
artifactRoot: '/opt/qinglong3-release',
|
|
json: true,
|
|
},
|
|
);
|
|
assert.equal(
|
|
parseArguments([
|
|
'prepare',
|
|
'--manifest=/mnt/data/manifest.json',
|
|
'--data-path=/mnt/data',
|
|
'--artifact-root=/opt/qinglong3-release',
|
|
'--session=/mnt/data/session.json',
|
|
]).phase,
|
|
'prepare',
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseArguments([
|
|
'resume',
|
|
'--manifest=manifest.json',
|
|
'--session=/mnt/data/session.json',
|
|
'--output=/mnt/data/report.json',
|
|
]),
|
|
/manifestPath must be absolute/,
|
|
);
|
|
});
|
|
|
|
test('renders a pre-Node POSIX anchor without service mutation authority', () => {
|
|
const session = sessionFixture();
|
|
const source = renderEvidenceWrapper({
|
|
applicationEntrypoint: session.paths.applicationEntrypoint,
|
|
nodeExecutable: session.environment.nodeExecutable,
|
|
paths: session.paths,
|
|
});
|
|
assert.ok(
|
|
source.indexOf('read -r service_start_uptime') <
|
|
source.indexOf('"$expected_node" "$entrypoint"'),
|
|
);
|
|
assert.match(source, /set -C/);
|
|
assert.match(source, /event_ordinal/);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/\b(?:reboot|poweroff|shutdown|systemctl|rc-service|rc-update|sudo)\b/,
|
|
);
|
|
assert.deepEqual(
|
|
installContract(
|
|
'systemd',
|
|
session.service.serviceName,
|
|
session.service.descriptorSource,
|
|
).enableArguments,
|
|
['enable', session.service.serviceName],
|
|
);
|
|
});
|
|
|
|
test('binds one official active event to wrapper monotonic records', () => {
|
|
const result = parseWrapperObservations({
|
|
startContents:
|
|
'schema=qinglong/physical-edge-service-wrapper-start@v1\n' +
|
|
'boot_id=019f0000-0000-4000-8000-000000000002\n' +
|
|
'service_start_uptime=12.00\n' +
|
|
'wrapper_pid=101\n',
|
|
nodeContents:
|
|
'schema=qinglong/physical-edge-service-node@v1\nnode_pid=102\n',
|
|
activeContents:
|
|
'schema=qinglong/physical-edge-service-active@v1\n' +
|
|
'boot_id=019f0000-0000-4000-8000-000000000002\n' +
|
|
'event_ordinal=2\n' +
|
|
'active_uptime=13.25\n',
|
|
eventContents:
|
|
'{"schemaVersion":1,"component":"qinglong3-local-application","level":"info","event":"starting","profile":"edge"}\n' +
|
|
'{"schemaVersion":1,"component":"qinglong3-local-application","level":"info","event":"active","profile":"edge","aiStatus":"deployment_excluded"}\n',
|
|
});
|
|
assert.equal(result.serviceStartToActiveMs, 1250);
|
|
assert.equal(result.activeEventOrdinal, 2);
|
|
assert.throws(
|
|
() =>
|
|
parseWrapperObservations({
|
|
startContents:
|
|
'schema=qinglong/physical-edge-service-wrapper-start@v1\n' +
|
|
'boot_id=019f0000-0000-4000-8000-000000000002\n' +
|
|
'service_start_uptime=14.00\n' +
|
|
'wrapper_pid=101\n',
|
|
nodeContents:
|
|
'schema=qinglong/physical-edge-service-node@v1\nnode_pid=102\n',
|
|
activeContents:
|
|
'schema=qinglong/physical-edge-service-active@v1\n' +
|
|
'boot_id=019f0000-0000-4000-8000-000000000002\n' +
|
|
'event_ordinal=1\n' +
|
|
'active_uptime=13.25\n',
|
|
eventContents:
|
|
'{"schemaVersion":1,"component":"qinglong3-local-application","level":"info","event":"active","profile":"edge","aiStatus":"deployment_excluded"}\n',
|
|
}),
|
|
/monotonic ordering/,
|
|
);
|
|
});
|
|
|
|
test('parses exact systemd and OpenRC active-enabled state', () => {
|
|
assert.deepEqual(
|
|
parseSystemdShow(
|
|
'LoadState=loaded\n' +
|
|
'ActiveState=active\n' +
|
|
'SubState=running\n' +
|
|
'UnitFileState=enabled\n' +
|
|
'FragmentPath=/etc/systemd/system/qinglong3-physical-019f0000.service\n' +
|
|
'MainPID=101\n',
|
|
),
|
|
{
|
|
active: true,
|
|
enabled: true,
|
|
fragmentPath: '/etc/systemd/system/qinglong3-physical-019f0000.service',
|
|
mainPid: 101,
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
parseOpenRcState(
|
|
0,
|
|
' qinglong3-physical-019f0000 | default\n',
|
|
'qinglong3-physical-019f0000',
|
|
),
|
|
{ active: true, enabled: true, mainPid: null },
|
|
);
|
|
assert.equal(
|
|
parseOpenRcState(
|
|
3,
|
|
' qinglong3-physical-019f0000 | default\n',
|
|
'qinglong3-physical-019f0000',
|
|
).active,
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('normalizes a digest-bound install session', () => {
|
|
const session = sessionFixture();
|
|
assert.equal(session.service.descriptorMode, 0o644);
|
|
assert.equal(
|
|
session.service.descriptorDestination,
|
|
'/etc/systemd/system/qinglong3-physical-019f0000.service',
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
normalizeSession({
|
|
...session,
|
|
service: { ...session.service, descriptorMode: 0o666 },
|
|
}),
|
|
/session is invalid or drifted/,
|
|
);
|
|
});
|
|
|
|
test('accepts a bounded different-boot service report and rejects drift', () => {
|
|
const report = reportFixture();
|
|
assert.equal(report.qualification.passed, true);
|
|
assert.deepEqual(
|
|
validateServiceStartReport(
|
|
report,
|
|
manifest(),
|
|
environment('019f0000-0000-4000-8000-000000000002'),
|
|
),
|
|
[],
|
|
);
|
|
assert.equal(
|
|
reportFixture({
|
|
measurements: {
|
|
serviceStartBootAgeMs: 12_000,
|
|
activeBootAgeMs: 181_000,
|
|
bootToActiveMs: 181_000,
|
|
serviceStartToActiveMs: 169_000,
|
|
activeEventOrdinal: 2,
|
|
},
|
|
}).qualification.passed,
|
|
false,
|
|
);
|
|
assert.notDeepEqual(
|
|
validateServiceStartReport(
|
|
report,
|
|
manifest(),
|
|
environment('019f0000-0000-4000-8000-000000000099'),
|
|
),
|
|
[],
|
|
);
|
|
});
|
|
|
|
test('recomputes qualification instead of trusting a digest-valid passed flag', () => {
|
|
const invalid = reportFixture({
|
|
measurements: {
|
|
serviceStartBootAgeMs: 12_000,
|
|
activeBootAgeMs: 181_000,
|
|
bootToActiveMs: 181_000,
|
|
serviceStartToActiveMs: 169_000,
|
|
activeEventOrdinal: 2,
|
|
},
|
|
});
|
|
const { sha256: ignored, ...body } = invalid;
|
|
const widenedBody = {
|
|
...body,
|
|
qualification: {
|
|
...body.qualification,
|
|
passed: true,
|
|
violations: [],
|
|
},
|
|
};
|
|
const widened = {
|
|
...widenedBody,
|
|
sha256: canonicalDigest(widenedBody),
|
|
};
|
|
assert.notDeepEqual(
|
|
validateServiceStartReport(
|
|
widened,
|
|
manifest(),
|
|
environment('019f0000-0000-4000-8000-000000000002'),
|
|
),
|
|
[],
|
|
);
|
|
});
|
|
|
|
test('renders POSIX shell syntax with order-independent active predicates', () => {
|
|
const session = sessionFixture();
|
|
const source = renderEvidenceWrapper({
|
|
applicationEntrypoint: session.paths.applicationEntrypoint,
|
|
nodeExecutable: session.environment.nodeExecutable,
|
|
paths: session.paths,
|
|
});
|
|
const syntax = spawnSync('/bin/sh', ['-n'], {
|
|
input: source,
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(syntax.status, 0, syntax.stderr);
|
|
for (const predicate of [
|
|
'"component":"qinglong3-local-application"',
|
|
'"event":"active"',
|
|
'"profile":"edge"',
|
|
'"aiStatus":"deployment_excluded"',
|
|
]) {
|
|
assert.ok(source.includes(predicate));
|
|
}
|
|
assert.equal((source.match(/active_candidate=0/g) ?? []).length, 4);
|
|
});
|
|
|
|
test('uses the recorded active uptime while rejecting a forged package closure', () => {
|
|
const baseline = reportFixture();
|
|
const delayed = reportFixture({
|
|
observed: {
|
|
...baseline.observed,
|
|
after: { ...baseline.observed.after, bootAgeMs: 900_000 },
|
|
},
|
|
});
|
|
assert.equal(delayed.qualification.passed, true);
|
|
|
|
const { sha256: ignored, ...body } = baseline;
|
|
const forgedBody = {
|
|
...body,
|
|
observed: {
|
|
...body.observed,
|
|
artifact: {
|
|
...body.observed.artifact,
|
|
packages: [...packages.slice(0, -1), 'unexpected'],
|
|
},
|
|
},
|
|
};
|
|
const forged = { ...forgedBody, sha256: canonicalDigest(forgedBody) };
|
|
assert.notDeepEqual(
|
|
validateServiceStartReport(
|
|
forged,
|
|
manifest(),
|
|
environment('019f0000-0000-4000-8000-000000000002'),
|
|
),
|
|
[],
|
|
);
|
|
});
|
|
|
|
test('separates OpenRC status and runlevel enable executables', () => {
|
|
const systemd = sessionFixture();
|
|
const descriptorSource = `${systemd.paths.deploymentRoot}/service/qinglong3.openrc`;
|
|
const descriptorDestination = `/etc/init.d/${systemd.service.serviceName}`;
|
|
const { sha256: ignored, ...sessionBody } = systemd;
|
|
const openRcBody = {
|
|
...sessionBody,
|
|
service: {
|
|
...systemd.service,
|
|
kind: 'openrc',
|
|
managerExecutable: '/sbin/rc-service',
|
|
managerSha256: '2'.repeat(64),
|
|
enableExecutable: '/sbin/rc-update',
|
|
enableSha256: '3'.repeat(64),
|
|
descriptorSource,
|
|
descriptorDestination,
|
|
descriptorMode: 0o755,
|
|
installArguments: [
|
|
'-o',
|
|
'root',
|
|
'-g',
|
|
'root',
|
|
'-m',
|
|
'755',
|
|
descriptorSource,
|
|
descriptorDestination,
|
|
],
|
|
enableArguments: ['add', systemd.service.serviceName, 'default'],
|
|
},
|
|
};
|
|
assert.equal(
|
|
normalizeSession({
|
|
...openRcBody,
|
|
sha256: canonicalDigest(openRcBody),
|
|
}).service.enableExecutable,
|
|
'/sbin/rc-update',
|
|
);
|
|
const invalidBody = {
|
|
...openRcBody,
|
|
service: {
|
|
...openRcBody.service,
|
|
enableExecutable: '/sbin/rc-service',
|
|
},
|
|
};
|
|
assert.throws(
|
|
() =>
|
|
normalizeSession({
|
|
...invalidBody,
|
|
sha256: canonicalDigest(invalidBody),
|
|
}),
|
|
/session is invalid or drifted/,
|
|
);
|
|
});
|
|
|
|
test('rejects container or VM indicators before physical qualification', () => {
|
|
const baseline = reportFixture();
|
|
const virtual = reportFixture({
|
|
observed: {
|
|
...baseline.observed,
|
|
after: {
|
|
...baseline.observed.after,
|
|
virtualizationIndicators: ['PID 1 cgroup'],
|
|
},
|
|
},
|
|
});
|
|
assert.equal(virtual.qualification.passed, false);
|
|
assert.match(
|
|
virtual.qualification.violations.join('; '),
|
|
/device, Node or boot environment/,
|
|
);
|
|
});
|
|
|
|
test(
|
|
'runs the POSIX wrapper through a real Linux FIFO and Node child',
|
|
{ skip: process.platform !== 'linux' },
|
|
async (t) => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-service-wrapper-'));
|
|
fs.chmodSync(root, 0o700);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const paths = {
|
|
applicationConfig: path.join(root, 'local-application.json'),
|
|
fifo: path.join(root, 'events.fifo'),
|
|
wrapper: path.join(root, 'boot-probe.sh'),
|
|
wrapperStartRecord: path.join(root, 'wrapper-start.record'),
|
|
nodeRecord: path.join(root, 'node.record'),
|
|
activeRecord: path.join(root, 'active.record'),
|
|
eventLog: path.join(root, 'events.jsonl'),
|
|
stderrLog: path.join(root, 'stderr.log'),
|
|
};
|
|
const entrypoint = path.join(root, 'application.cjs');
|
|
fs.writeFileSync(paths.applicationConfig, '{}\n', { mode: 0o600 });
|
|
fs.writeFileSync(
|
|
entrypoint,
|
|
`'use strict';
|
|
const emit = (event) => process.stdout.write(JSON.stringify({
|
|
schemaVersion: 1,
|
|
component: 'qinglong3-local-application',
|
|
instanceId: 'physical-service-test',
|
|
profile: 'edge',
|
|
level: 'info',
|
|
...event,
|
|
}) + '\\n');
|
|
process.once('SIGTERM', () => {
|
|
emit({ event: 'shutdown_requested', signal: 'SIGTERM' });
|
|
emit({ event: 'stopped', stopResult: 'stopped' });
|
|
process.exit(0);
|
|
});
|
|
emit({ event: 'starting' });
|
|
emit({ event: 'active', aiStatus: 'deployment_excluded' });
|
|
setInterval(() => {}, 1000);
|
|
`,
|
|
{ mode: 0o600 },
|
|
);
|
|
const fifo = spawnSync('/usr/bin/mkfifo', ['-m', '600', paths.fifo], {
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(fifo.status, 0, fifo.stderr);
|
|
const wrapper = renderEvidenceWrapper({
|
|
applicationEntrypoint: entrypoint,
|
|
nodeExecutable: process.execPath,
|
|
paths,
|
|
});
|
|
fs.writeFileSync(paths.wrapper, wrapper, { mode: 0o700 });
|
|
const child = spawn(
|
|
'/bin/sh',
|
|
[paths.wrapper, process.execPath, '--config', paths.applicationConfig],
|
|
{ stdio: ['ignore', 'ignore', 'pipe'] },
|
|
);
|
|
t.after(() => {
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill('SIGKILL');
|
|
}
|
|
});
|
|
const exitPromise = new Promise((resolve) => {
|
|
child.once('exit', (code, signal) => resolve({ code, signal }));
|
|
});
|
|
let wrapperStderr = '';
|
|
child.stderr.setEncoding('utf8');
|
|
child.stderr.on('data', (chunk) => {
|
|
wrapperStderr += chunk;
|
|
});
|
|
const deadline = Date.now() + 5_000;
|
|
while (!fs.existsSync(paths.activeRecord) && Date.now() < deadline) {
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
assert.equal(fs.existsSync(paths.activeRecord), true);
|
|
const observation = parseWrapperObservations({
|
|
startContents: fs.readFileSync(paths.wrapperStartRecord, 'utf8'),
|
|
nodeContents: fs.readFileSync(paths.nodeRecord, 'utf8'),
|
|
activeContents: fs.readFileSync(paths.activeRecord, 'utf8'),
|
|
eventContents: fs.readFileSync(paths.eventLog, 'utf8'),
|
|
});
|
|
assert.equal(observation.activeEventCount, 1);
|
|
assert.equal(observation.activeEvent.aiStatus, 'deployment_excluded');
|
|
child.kill('SIGTERM');
|
|
const outcome = await Promise.race([
|
|
exitPromise,
|
|
new Promise((_, reject) => {
|
|
const timer = setTimeout(
|
|
() => reject(new Error('wrapper did not exit after SIGTERM')),
|
|
5_000,
|
|
);
|
|
timer.unref();
|
|
}),
|
|
]);
|
|
assert.deepEqual(outcome, { code: 0, signal: null });
|
|
assert.equal(wrapperStderr, '');
|
|
assert.equal(fs.readFileSync(paths.stderrLog, 'utf8'), '');
|
|
},
|
|
);
|