mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
635 lines
18 KiB
JavaScript
635 lines
18 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
buildDirectServiceStartReport,
|
|
normalizeDirectServiceStartManifest,
|
|
normalizeSession,
|
|
parseStartupReceipt,
|
|
} = require('../../scripts/ql3-physical-edge-direct-service-start.cjs');
|
|
const {
|
|
buildEvidenceReport,
|
|
canonicalDigest,
|
|
normalizeManifest,
|
|
writeNoReplace,
|
|
} = require('../../scripts/ql3-physical-edge-evidence.cjs');
|
|
const {
|
|
buildFinalReport,
|
|
buildSigningPayload,
|
|
parseArguments,
|
|
validatePhysicalEvidenceReport,
|
|
} = require('../../scripts/ql3-physical-edge-release-evidence.cjs');
|
|
const {
|
|
runBenchmark: runPluginPackageRecoveryEdgeBenchmark,
|
|
} = require('../../scripts/ql3-plugin-package-recovery-edge-benchmark.cjs');
|
|
|
|
const SCRIPT_PATH = path.resolve(
|
|
__dirname,
|
|
'../../scripts/ql3-physical-edge-release-evidence.cjs',
|
|
);
|
|
const REPOSITORY = 'https://github.com/whyour/qinglong.git';
|
|
const REVISION = 'a'.repeat(40);
|
|
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: '1'.repeat(64),
|
|
artifactMetadataSha256: '2'.repeat(64),
|
|
artifactFiles: 627,
|
|
artifactBytes: 5_045_360,
|
|
entrypointSha256: '3'.repeat(64),
|
|
packages: PACKAGES,
|
|
};
|
|
}
|
|
|
|
function directManifest() {
|
|
return normalizeDirectServiceStartManifest({
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_direct_service_start_candidate',
|
|
profile: 'edge',
|
|
deviceId: 'router-a1',
|
|
serviceManager: 'systemd',
|
|
expectedArchitecture: 'arm64',
|
|
expectedFilesystem: 'ext4',
|
|
expectedArtifactSha256: '1'.repeat(64),
|
|
expectedArtifactFiles: 627,
|
|
expectedArtifactBytes: 5_045_360,
|
|
expectedNodeSha256: '4'.repeat(64),
|
|
maximumBootToActiveMs: 180_000,
|
|
maximumServiceStartBootAgeMs: 60_000,
|
|
maximumServiceStartToActiveMs: 30_000,
|
|
});
|
|
}
|
|
|
|
function physicalManifest() {
|
|
return normalizeManifest({
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_candidate',
|
|
profile: 'edge',
|
|
deviceId: 'router-a1',
|
|
deviceModel: 'Example Router A1',
|
|
soc: 'Example SoC',
|
|
storageMedium: 'emmc',
|
|
expectedArchitecture: 'arm64',
|
|
memoryBytes: {
|
|
minimum: 128 * 1024 * 1024,
|
|
maximum: 512 * 1024 * 1024,
|
|
},
|
|
expectedFilesystem: 'ext4',
|
|
});
|
|
}
|
|
|
|
function environment(bootId) {
|
|
return {
|
|
platform: 'linux',
|
|
architecture: 'arm64',
|
|
bootId,
|
|
bootAgeMs: 15_000,
|
|
dataFilesystem: 'ext4',
|
|
nodeExecutable: '/usr/bin/node',
|
|
nodeSha256: '4'.repeat(64),
|
|
nodeVersion: 'v24.18.0',
|
|
virtualizationIndicators: [],
|
|
};
|
|
}
|
|
|
|
function physicalObserved(bootId) {
|
|
return {
|
|
platform: 'linux',
|
|
architecture: 'arm64',
|
|
node: 'v24.18.0',
|
|
bootId,
|
|
kernel: '6.6.0',
|
|
distribution: { id: 'openwrt', versionId: '24.10' },
|
|
libc: 'musl-ld-musl-aarch64.so.1',
|
|
cpuModel: 'Example CPU',
|
|
cpuCount: 1,
|
|
totalMemoryBytes: 256 * 1024 * 1024,
|
|
observedModel: 'Example Router A1',
|
|
dataPath: '/mnt/ql3-evidence',
|
|
dataFilesystem: 'ext4',
|
|
dataMountOptions: ['rw', 'noatime'],
|
|
dataBytes: 4 * 1024 * 1024 * 1024,
|
|
dataAvailableBytes: 2 * 1024 * 1024 * 1024,
|
|
virtualizationIndicators: [],
|
|
};
|
|
}
|
|
|
|
function edgeWorkloadReport() {
|
|
return {
|
|
schemaVersion: 1,
|
|
profile: 'edge',
|
|
generatedAt: '2026-08-14T08:01:30.000Z',
|
|
host: {
|
|
platform: 'linux',
|
|
architecture: 'arm64',
|
|
node: 'v24.18.0',
|
|
cpuCount: 1,
|
|
totalMemoryBytes: 256 * 1024 * 1024,
|
|
},
|
|
moduleLoad: {
|
|
rssBeforeBytes: 50_000_000,
|
|
rssAfterBytes: 55_000_000,
|
|
rssDeltaBytes: 5_000_000,
|
|
},
|
|
cases: [
|
|
{
|
|
name: 'single_noop',
|
|
durationMs: 15,
|
|
baselineRssBytes: 55_000_000,
|
|
peakRssBytes: 56_000_000,
|
|
peakRssDeltaBytes: 1_000_000,
|
|
outcome: 'succeeded',
|
|
exitCode: 0,
|
|
},
|
|
{
|
|
name: 'stdout_10000_lines',
|
|
durationMs: 45,
|
|
baselineRssBytes: 56_000_000,
|
|
peakRssBytes: 58_000_000,
|
|
peakRssDeltaBytes: 2_000_000,
|
|
outcome: 'succeeded',
|
|
exitCode: 0,
|
|
output: { bytes: 270_000, lines: 10_000, writes: 20 },
|
|
},
|
|
],
|
|
cancellation: {
|
|
durationMs: 20,
|
|
outcome: 'cancelled',
|
|
termSignalSent: true,
|
|
killSignalSent: false,
|
|
},
|
|
gates: {
|
|
maxRssDeltaMb: 96,
|
|
maxCancelMs: 5000,
|
|
passed: true,
|
|
violations: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function sqliteWorkloadReport() {
|
|
return {
|
|
node: 'v24.18.0',
|
|
arch: 'arm64',
|
|
platform: 'linux',
|
|
iterations: 250,
|
|
batchSize: 10,
|
|
journalMode: 'delete',
|
|
synchronous: 'full',
|
|
transactionMs: { p50: 0.1, p95: 0.2, p99: 0.3, max: 0.4 },
|
|
maxBatchStallMs: 2,
|
|
rssDeltaMb: 1,
|
|
databaseBytes: 16_384,
|
|
integrityCheck: 'ok',
|
|
};
|
|
}
|
|
|
|
function directSession() {
|
|
const manifest = directManifest();
|
|
const sessionId = '019f0000-0000-4000-8000-000000000030';
|
|
const dataPath = '/mnt/ql3-evidence';
|
|
const deploymentRoot = `${dataPath}/.ql3-direct-service-start-deployment-${sessionId}`;
|
|
const descriptorSource = `${deploymentRoot}/service/qinglong3.service`;
|
|
const body = {
|
|
schemaVersion: 1,
|
|
evidenceClass: 'physical_edge_direct_service_start_session',
|
|
sessionId,
|
|
manifestDigest: canonicalDigest(manifest),
|
|
uid: 1000,
|
|
preparedAt: '2026-08-14T08:00:00.000Z',
|
|
artifact: artifact(),
|
|
environment: environment('019f0000-0000-4000-8000-000000000001'),
|
|
bridge: {
|
|
actionId: sessionId,
|
|
controllerRoot: '/var/lib/qinglong3-service-bridge',
|
|
intentDigest: '5'.repeat(64),
|
|
intentPath: `${deploymentRoot}/service/service-manager-intents/${sessionId}.json`,
|
|
outcomePath: `${deploymentRoot}/service/service-manager-outcomes/${sessionId}.json`,
|
|
},
|
|
paths: {
|
|
dataPath,
|
|
deploymentRoot,
|
|
artifactRoot: '/opt/qinglong3-release',
|
|
applicationEntrypoint:
|
|
'/opt/qinglong3-release/node_modules/@qinglong/local-application/dist/cli.js',
|
|
applicationConfig: `${deploymentRoot}/local-application.json`,
|
|
startupReceipt: `${deploymentRoot}/local-application.json.active.json`,
|
|
},
|
|
service: {
|
|
kind: 'systemd',
|
|
serviceName: 'qinglong3',
|
|
managerExecutable: '/usr/bin/systemctl',
|
|
managerSha256: '6'.repeat(64),
|
|
enableExecutable: '/usr/bin/systemctl',
|
|
enableSha256: '6'.repeat(64),
|
|
supervisorExecutable: null,
|
|
supervisorSha256: null,
|
|
descriptorSource,
|
|
descriptorDestination: '/etc/systemd/system/qinglong3.service',
|
|
descriptorMode: 0o644,
|
|
descriptorSha256: '7'.repeat(64),
|
|
applicationConfigSha256: '8'.repeat(64),
|
|
installArguments: [
|
|
'-o',
|
|
'root',
|
|
'-g',
|
|
'root',
|
|
'-m',
|
|
'644',
|
|
descriptorSource,
|
|
'/etc/systemd/system/qinglong3.service',
|
|
],
|
|
enableArguments: ['enable', 'qinglong3'],
|
|
},
|
|
};
|
|
return normalizeSession({ ...body, sha256: canonicalDigest(body) });
|
|
}
|
|
|
|
function startupReceipt() {
|
|
const body = {
|
|
schemaVersion: 1,
|
|
schema: 'qinglong/local-application-startup-receipt@v1',
|
|
instanceId: 'physical-direct-019f0000',
|
|
profile: 'edge',
|
|
aiStatus: 'deployment_excluded',
|
|
bootId: '019f0000-0000-4000-8000-000000000002',
|
|
activeBootAgeMs: 13_250,
|
|
processId: 101,
|
|
processStartTicks: '1200',
|
|
nodeExecutable: '/usr/bin/node',
|
|
nodeVersion: 'v24.18.0',
|
|
};
|
|
const sha256 = crypto
|
|
.createHash('sha256')
|
|
.update('qinglong.local-application-startup-receipt.v1\0', 'utf8')
|
|
.update(JSON.stringify(body), 'utf8')
|
|
.digest('hex');
|
|
return parseStartupReceipt(JSON.stringify({ ...body, sha256 }));
|
|
}
|
|
|
|
function directReport() {
|
|
const session = directSession();
|
|
const receipt = startupReceipt();
|
|
const after = environment(receipt.bootId);
|
|
return buildDirectServiceStartReport({
|
|
manifest: directManifest(),
|
|
session,
|
|
observed: {
|
|
after,
|
|
artifact: artifact(),
|
|
bridge: {
|
|
actionId: session.bridge.actionId,
|
|
intentDigest: session.bridge.intentDigest,
|
|
outcomeDigest: '9'.repeat(64),
|
|
observationDigest: 'a'.repeat(64),
|
|
state: 'active',
|
|
},
|
|
process: {
|
|
bootId: after.bootId,
|
|
nodePid: 101,
|
|
nodeParentPid: 1,
|
|
nodeStartTicks: 1200,
|
|
clockTicksPerSecond: 100,
|
|
},
|
|
receipt,
|
|
service: {
|
|
kind: 'systemd',
|
|
serviceName: 'qinglong3',
|
|
managerExecutable: '/usr/bin/systemctl',
|
|
managerSha256: '6'.repeat(64),
|
|
descriptorSha256: '7'.repeat(64),
|
|
mainPid: 101,
|
|
mainStartMonotonicUs: 12_000_000,
|
|
},
|
|
},
|
|
measurements: {
|
|
serviceStartBootAgeMs: 12_000,
|
|
activeBootAgeMs: 13_250,
|
|
bootToActiveMs: 13_250,
|
|
serviceStartToActiveMs: 1_250,
|
|
},
|
|
outcomes: {
|
|
aiStatus: 'deployment_excluded',
|
|
descriptorInstalled: true,
|
|
initSupervisionMatched: true,
|
|
managerStartMonotonicMatched: true,
|
|
nodeProcessIdentityMatched: true,
|
|
ownerBridgeOutcomeVerified: true,
|
|
serviceActive: true,
|
|
serviceEnabled: true,
|
|
startupReceiptValidated: true,
|
|
},
|
|
generatedAt: '2026-08-14T08:01:00.000Z',
|
|
});
|
|
}
|
|
|
|
async function physicalReport() {
|
|
const plugin = await runPluginPackageRecoveryEdgeBenchmark({
|
|
maxDatabaseGrowthBytes: 4 * 1024 * 1024,
|
|
maxDurationMs: 10_000,
|
|
maxRssDeltaBytes: 96 * 1024 * 1024,
|
|
});
|
|
const direct = directReport();
|
|
const observed = physicalObserved(direct.observed.after.bootId);
|
|
return buildEvidenceReport({
|
|
manifest: physicalManifest(),
|
|
observed,
|
|
workloads: [
|
|
{
|
|
name: 'edge-executor',
|
|
report: edgeWorkloadReport(),
|
|
},
|
|
{
|
|
name: 'node-sqlite-on-device-storage',
|
|
report: sqliteWorkloadReport(),
|
|
},
|
|
{
|
|
name: 'plugin-package-failed-upgrade',
|
|
report: {
|
|
...plugin,
|
|
identity: {
|
|
...plugin.identity,
|
|
platform: 'linux',
|
|
architecture: 'arm64',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
supplementalEvidence: [direct],
|
|
generatedAt: '2026-08-14T08:02:00.000Z',
|
|
});
|
|
}
|
|
|
|
function writeFile(filePath, contents, mode) {
|
|
fs.writeFileSync(filePath, contents, { mode });
|
|
fs.chmodSync(filePath, mode);
|
|
}
|
|
|
|
test('requires exact phase-specific source and authority inputs', () => {
|
|
assert.equal(
|
|
parseArguments([
|
|
'prepare',
|
|
'--physical-report=/tmp/physical.json',
|
|
'--release-archive=/tmp/release.tar.gz',
|
|
`--repository=${REPOSITORY}`,
|
|
`--revision=${REVISION}`,
|
|
'--payload=/tmp/payload.json',
|
|
]).phase,
|
|
'prepare',
|
|
);
|
|
assert.equal(
|
|
parseArguments([
|
|
'finalize',
|
|
'--physical-report=/tmp/physical.json',
|
|
'--release-archive=/tmp/release.tar.gz',
|
|
'--payload=/tmp/payload.json',
|
|
'--signature=/tmp/payload.sig',
|
|
'--trusted-public-key=/tmp/release.pub',
|
|
`--expected-repository=${REPOSITORY}`,
|
|
`--expected-revision=${REVISION}`,
|
|
'--output=/tmp/release-evidence.json',
|
|
]).phase,
|
|
'finalize',
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseArguments([
|
|
'prepare',
|
|
'--physical-report=physical.json',
|
|
'--release-archive=/tmp/release.tar.gz',
|
|
`--repository=${REPOSITORY}`,
|
|
`--revision=${REVISION}`,
|
|
'--payload=/tmp/payload.json',
|
|
]),
|
|
/physicalReportPath must be absolute/,
|
|
);
|
|
});
|
|
|
|
test('reconstructs a passed physical report and retains unsupported status', async () => {
|
|
const report = await physicalReport();
|
|
assert.equal(report.qualification.physicalCandidate, true);
|
|
assert.deepEqual(validatePhysicalEvidenceReport(report), []);
|
|
const minimalForgery = buildEvidenceReport({
|
|
manifest: report.manifest,
|
|
observed: report.observed,
|
|
workloads: [
|
|
{
|
|
name: 'edge-executor',
|
|
report: {
|
|
schemaVersion: 1,
|
|
profile: 'edge',
|
|
host: {
|
|
platform: 'linux',
|
|
architecture: 'arm64',
|
|
},
|
|
gates: { passed: true, violations: [] },
|
|
},
|
|
},
|
|
report.workloads[1],
|
|
report.workloads[2],
|
|
],
|
|
supplementalEvidence: report.supplementalEvidence,
|
|
generatedAt: report.generatedAt,
|
|
});
|
|
assert.equal(minimalForgery.qualification.physicalCandidate, true);
|
|
assert.match(
|
|
validatePhysicalEvidenceReport(minimalForgery).join('; '),
|
|
/not an exact recorder result/,
|
|
);
|
|
const withoutDirect = buildEvidenceReport({
|
|
manifest: report.manifest,
|
|
observed: report.observed,
|
|
workloads: report.workloads,
|
|
generatedAt: report.generatedAt,
|
|
});
|
|
assert.match(
|
|
validatePhysicalEvidenceReport(withoutDirect).join('; '),
|
|
/direct release service start evidence is required/,
|
|
);
|
|
const payload = buildSigningPayload({
|
|
physicalReport: report,
|
|
releaseArchive: { sha256: 'b'.repeat(64), bytes: 1024 },
|
|
repository: REPOSITORY,
|
|
revision: REVISION,
|
|
signedAt: '2026-08-14T08:03:00.000Z',
|
|
});
|
|
const final = buildFinalReport({
|
|
physicalReport: report,
|
|
payload,
|
|
signature: Buffer.alloc(64, 1),
|
|
fingerprint: 'c'.repeat(64),
|
|
});
|
|
assert.equal(final.supported, false);
|
|
assert.equal(
|
|
final.qualification.remainingRequiredEvidence.includes(
|
|
'release_archive_signature',
|
|
),
|
|
false,
|
|
);
|
|
assert.ok(
|
|
final.qualification.remainingRequiredEvidence.includes(
|
|
'power_loss_restart',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('prepares and verifies one externally signed release binding', async (t) => {
|
|
const directory = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-release-evidence-')),
|
|
);
|
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
const paths = {
|
|
physical: path.join(directory, 'physical.json'),
|
|
archive: path.join(directory, 'release.tar.gz'),
|
|
payload: path.join(directory, 'payload.json'),
|
|
signature: path.join(directory, 'payload.sig'),
|
|
publicKey: path.join(directory, 'release.pub'),
|
|
output: path.join(directory, 'release-evidence.json'),
|
|
mismatchOutput: path.join(directory, 'mismatch-evidence.json'),
|
|
tamperedOutput: path.join(directory, 'tampered-evidence.json'),
|
|
};
|
|
writeNoReplace(
|
|
paths.physical,
|
|
`${JSON.stringify(await physicalReport(), null, 2)}\n`,
|
|
);
|
|
writeFile(paths.archive, Buffer.from('bounded release archive'), 0o444);
|
|
|
|
const prepared = spawnSync(
|
|
process.execPath,
|
|
[
|
|
SCRIPT_PATH,
|
|
'prepare',
|
|
`--physical-report=${paths.physical}`,
|
|
`--release-archive=${paths.archive}`,
|
|
`--repository=${REPOSITORY}`,
|
|
`--revision=${REVISION}`,
|
|
`--payload=${paths.payload}`,
|
|
'--json',
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(prepared.status, 0, prepared.stderr);
|
|
assert.equal(fs.statSync(paths.payload).mode & 0o777, 0o600);
|
|
assert.equal(fs.readFileSync(paths.payload, 'utf8').endsWith('\n'), false);
|
|
|
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
|
|
const signature = crypto.sign(
|
|
null,
|
|
fs.readFileSync(paths.payload),
|
|
privateKey,
|
|
);
|
|
writeFile(paths.signature, signature, 0o600);
|
|
writeFile(
|
|
paths.publicKey,
|
|
publicKey.export({ type: 'spki', format: 'pem' }),
|
|
0o444,
|
|
);
|
|
const finalized = spawnSync(
|
|
process.execPath,
|
|
[
|
|
SCRIPT_PATH,
|
|
'finalize',
|
|
`--physical-report=${paths.physical}`,
|
|
`--release-archive=${paths.archive}`,
|
|
`--payload=${paths.payload}`,
|
|
`--signature=${paths.signature}`,
|
|
`--trusted-public-key=${paths.publicKey}`,
|
|
`--expected-repository=${REPOSITORY}`,
|
|
`--expected-revision=${REVISION}`,
|
|
`--output=${paths.output}`,
|
|
'--json',
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(finalized.status, 0, finalized.stderr);
|
|
const report = JSON.parse(fs.readFileSync(paths.output, 'utf8'));
|
|
assert.equal(report.supported, false);
|
|
assert.equal(report.qualification.passed, true);
|
|
assert.equal(report.payload.release.archiveBytes, 23);
|
|
assert.ok(
|
|
report.qualification.collectedEvidence.includes(
|
|
'release_archive_signature_or_attestation',
|
|
),
|
|
);
|
|
assert.match(report.trust.publicKeyFingerprintSha256, /^[a-f0-9]{64}$/);
|
|
|
|
const sourceMismatch = spawnSync(
|
|
process.execPath,
|
|
[
|
|
SCRIPT_PATH,
|
|
'finalize',
|
|
`--physical-report=${paths.physical}`,
|
|
`--release-archive=${paths.archive}`,
|
|
`--payload=${paths.payload}`,
|
|
`--signature=${paths.signature}`,
|
|
`--trusted-public-key=${paths.publicKey}`,
|
|
`--expected-repository=${REPOSITORY}`,
|
|
`--expected-revision=${'b'.repeat(40)}`,
|
|
`--output=${paths.mismatchOutput}`,
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.notEqual(sourceMismatch.status, 0);
|
|
assert.match(sourceMismatch.stderr, /payload did not match/);
|
|
assert.equal(fs.existsSync(paths.mismatchOutput), false);
|
|
|
|
const noReplace = spawnSync(
|
|
process.execPath,
|
|
[
|
|
SCRIPT_PATH,
|
|
'finalize',
|
|
`--physical-report=${paths.physical}`,
|
|
`--release-archive=${paths.archive}`,
|
|
`--payload=${paths.payload}`,
|
|
`--signature=${paths.signature}`,
|
|
`--trusted-public-key=${paths.publicKey}`,
|
|
`--expected-repository=${REPOSITORY}`,
|
|
`--expected-revision=${REVISION}`,
|
|
`--output=${paths.output}`,
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.notEqual(noReplace.status, 0);
|
|
assert.match(noReplace.stderr, /EEXIST/);
|
|
|
|
fs.chmodSync(paths.archive, 0o644);
|
|
fs.appendFileSync(paths.archive, 'tampered');
|
|
fs.chmodSync(paths.archive, 0o444);
|
|
const tampered = spawnSync(
|
|
process.execPath,
|
|
[
|
|
SCRIPT_PATH,
|
|
'finalize',
|
|
`--physical-report=${paths.physical}`,
|
|
`--release-archive=${paths.archive}`,
|
|
`--payload=${paths.payload}`,
|
|
`--signature=${paths.signature}`,
|
|
`--trusted-public-key=${paths.publicKey}`,
|
|
`--expected-repository=${REPOSITORY}`,
|
|
`--expected-revision=${REVISION}`,
|
|
`--output=${paths.tamperedOutput}`,
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.notEqual(tampered.status, 0);
|
|
assert.match(tampered.stderr, /payload did not match/);
|
|
assert.equal(fs.existsSync(paths.tamperedOutput), false);
|
|
});
|