mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,651 @@
|
||||
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'), '');
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user