387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
597 lines
20 KiB
JavaScript
597 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import {
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const VERIFY_SCRIPT = 'scripts/ops/pingora-cutover-evidence-verify.mjs';
|
|
const failures = [];
|
|
const tmpRoot = mkdtempSync(
|
|
path.join(tmpdir(), 'genarrative-pingora-cutover-evidence-verify-'),
|
|
);
|
|
|
|
try {
|
|
main();
|
|
} finally {
|
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error('[check:pingora-cutover-evidence-verify] FAILED');
|
|
for (const failure of failures) {
|
|
console.error(`- ${failure}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('[check:pingora-cutover-evidence-verify] OK');
|
|
|
|
function main() {
|
|
assertScriptShape();
|
|
assertValidEvidenceBundleManifest();
|
|
assertValidCommandEvidenceManifest();
|
|
assertRequireSummaryOkPassesForOkManifest();
|
|
assertRequireSummaryOkFailsForCriticalManifest();
|
|
assertRequireSummaryOkFailsWhenSummaryMissing();
|
|
assertMissingSchemaVersionFails();
|
|
assertUnsupportedSchemaVersionFails();
|
|
assertMissingFileFails();
|
|
assertSizeMismatchFails();
|
|
assertSha256MismatchFails();
|
|
assertUnsafePathFails();
|
|
assertStringMetadataFailsExceptManifest();
|
|
assertExtraFileFailsByDefault();
|
|
assertExtraDirectoryFailsByDefault();
|
|
assertExtraSymlinkFailsByDefault();
|
|
assertAllowExtraFilesOverrideSucceeds();
|
|
assertRejectsRootPath();
|
|
assertRejectsEntryPathControlCharacters();
|
|
assertRejectsSymlinkBundleDir();
|
|
assertRejectsNonDirectoryBundleDir();
|
|
assertRejectsInvalidJson();
|
|
assertRejectsBothEntryModes();
|
|
assertRejectsRelativeEntryPath();
|
|
assertRejectsManifestFilePathControlCharacters();
|
|
}
|
|
|
|
function assertScriptShape() {
|
|
const content = readFileSync(VERIFY_SCRIPT, 'utf8');
|
|
assertIncludes(
|
|
content,
|
|
'createHash',
|
|
'证据校验脚本必须计算 sha256。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'sizeBytes',
|
|
'证据校验脚本必须校验 sizeBytes。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'证据目录不能是符号链接',
|
|
'证据校验脚本必须拒绝符号链接证据目录。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'不会修改证据目录',
|
|
'证据校验脚本 usage 必须说明只读边界。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'未登记',
|
|
'证据校验脚本必须说明默认拒绝未登记证据条目。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--allow-extra-files',
|
|
'证据校验脚本必须把未登记条目的例外开关显式化。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--require-summary-ok',
|
|
'证据校验脚本必须支持正式 runbook 要求 summary OK。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'manifest.summary.status is not OK',
|
|
'证据校验脚本必须在 summary 非 OK 时给出结构化失败原因。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'manifest.schemaVersion 必须是 1',
|
|
'证据校验脚本必须拒绝未知 manifest schemaVersion。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'manifest.files.${key}.path',
|
|
'证据校验脚本必须校验 manifest.files 路径安全。',
|
|
);
|
|
if (
|
|
content.includes('writeFile') ||
|
|
content.includes('chmod(') ||
|
|
content.includes("spawn(") ||
|
|
content.includes('execFile')
|
|
) {
|
|
failures.push('证据校验脚本必须保持只读,不能写文件或执行外部命令。');
|
|
}
|
|
}
|
|
|
|
function assertValidEvidenceBundleManifest() {
|
|
const fixture = prepareEvidenceBundleFixture('valid-bundle');
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 0, '合法证据包 manifest 应校验通过。');
|
|
if (result.status !== 0) {
|
|
return;
|
|
}
|
|
const output = parseJson(result.stdout, '合法证据包校验 stdout');
|
|
assertEqual(output.ok, true, '合法证据包 stdout 必须 ok=true。');
|
|
assertEqual(output.checkedCount, 4, '合法证据包必须校验四个元数据文件。');
|
|
assertEqual(output.failedCount, 0, '合法证据包不能有失败项。');
|
|
assertEqual(
|
|
output.files?.every((item) => item.status === 'OK'),
|
|
true,
|
|
'合法证据包每个文件都应 OK。',
|
|
);
|
|
}
|
|
|
|
function assertValidCommandEvidenceManifest() {
|
|
const fixture = prepareCommandEvidenceFixture('valid-command');
|
|
const result = runVerify(['--manifest', path.join(fixture.dir, 'manifest.json')]);
|
|
assertStatus(result, 0, '合法命令证据 manifest 应校验通过。');
|
|
if (result.status !== 0) {
|
|
return;
|
|
}
|
|
const output = parseJson(result.stdout, '合法命令证据校验 stdout');
|
|
assertEqual(output.ok, true, '合法命令证据 stdout 必须 ok=true。');
|
|
assertEqual(output.checkedCount, 3, '合法命令证据必须校验三个元数据文件。');
|
|
}
|
|
|
|
function assertRequireSummaryOkPassesForOkManifest() {
|
|
const fixture = prepareEvidenceBundleFixture('summary-ok');
|
|
const result = runVerify(['--bundle-dir', fixture.dir, '--require-summary-ok']);
|
|
assertStatus(result, 0, '要求 summary OK 且 manifest.summary.status=OK 时应通过。');
|
|
if (result.status !== 0) {
|
|
return;
|
|
}
|
|
const output = parseJson(result.stdout, 'summary OK 校验 stdout');
|
|
assertEqual(output.ok, true, 'summary OK 校验 stdout 必须 ok=true。');
|
|
assertEqual(
|
|
output.requireSummaryOk,
|
|
true,
|
|
'summary OK 校验 stdout 必须记录 requireSummaryOk=true。',
|
|
);
|
|
assertEqual(
|
|
output.summary?.status,
|
|
'OK',
|
|
'summary OK 校验 stdout 必须输出 summary.status=OK。',
|
|
);
|
|
}
|
|
|
|
function assertRequireSummaryOkFailsForCriticalManifest() {
|
|
const fixture = prepareEvidenceBundleFixture('summary-critical');
|
|
const manifestPath = path.join(fixture.dir, 'manifest.json');
|
|
const manifest = readJson(manifestPath);
|
|
manifest.summary.status = 'CRITICAL';
|
|
writeJson(manifestPath, manifest);
|
|
const result = runVerify(['--bundle-dir', fixture.dir, '--require-summary-ok']);
|
|
assertStatus(result, 1, '要求 summary OK 但 manifest.summary.status=CRITICAL 时必须失败。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'manifest.summary.status is not OK',
|
|
'summary 非 OK 失败必须写入 JSON reason。',
|
|
);
|
|
}
|
|
|
|
function assertRequireSummaryOkFailsWhenSummaryMissing() {
|
|
const fixture = prepareCommandEvidenceFixture('summary-missing');
|
|
const manifestPath = path.join(fixture.dir, 'manifest.json');
|
|
const manifest = readJson(manifestPath);
|
|
delete manifest.summary;
|
|
writeJson(manifestPath, manifest);
|
|
const result = runVerify(['--bundle-dir', fixture.dir, '--require-summary-ok']);
|
|
assertStatus(result, 1, '要求 summary OK 但 manifest 缺少 summary 时必须失败。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'manifest.summary.status is not OK',
|
|
'缺少 summary 失败必须写入 JSON reason。',
|
|
);
|
|
}
|
|
|
|
function assertMissingSchemaVersionFails() {
|
|
const fixture = prepareEvidenceBundleFixture('schema-version-missing');
|
|
const manifestPath = path.join(fixture.dir, 'manifest.json');
|
|
const manifest = readJson(manifestPath);
|
|
delete manifest.schemaVersion;
|
|
writeJson(manifestPath, manifest);
|
|
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, 'manifest 缺少 schemaVersion 时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'manifest.schemaVersion 必须是 1',
|
|
'缺少 manifest schemaVersion 必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertUnsupportedSchemaVersionFails() {
|
|
const fixture = prepareCommandEvidenceFixture('schema-version-unsupported');
|
|
const manifestPath = path.join(fixture.dir, 'manifest.json');
|
|
const manifest = readJson(manifestPath);
|
|
manifest.schemaVersion = 2;
|
|
writeJson(manifestPath, manifest);
|
|
|
|
const result = runVerify(['--manifest', manifestPath]);
|
|
assertStatus(result, 1, 'manifest schemaVersion 非 1 时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'manifest.schemaVersion 必须是 1',
|
|
'未知 manifest schemaVersion 必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertMissingFileFails() {
|
|
const fixture = prepareEvidenceBundleFixture('missing-file');
|
|
rmSync(path.join(fixture.dir, 'snapshot.stderr.txt'), { force: true });
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, 'manifest 指向缺失文件时必须失败。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'missing file',
|
|
'缺失文件失败必须写入 JSON reason。',
|
|
);
|
|
}
|
|
|
|
function assertSizeMismatchFails() {
|
|
const fixture = prepareEvidenceBundleFixture('size-mismatch');
|
|
const manifestPath = path.join(fixture.dir, 'manifest.json');
|
|
const manifest = readJson(manifestPath);
|
|
manifest.files.snapshot.sizeBytes += 1;
|
|
writeJson(manifestPath, manifest);
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, 'sizeBytes 不一致时必须失败。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'size mismatch',
|
|
'sizeBytes 不一致必须写入 JSON reason。',
|
|
);
|
|
}
|
|
|
|
function assertSha256MismatchFails() {
|
|
const fixture = prepareEvidenceBundleFixture('sha256-mismatch');
|
|
const manifestPath = path.join(fixture.dir, 'manifest.json');
|
|
const manifest = readJson(manifestPath);
|
|
manifest.files.snapshot.sha256 = '0'.repeat(64);
|
|
writeJson(manifestPath, manifest);
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, 'sha256 不一致时必须失败。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'sha256 mismatch',
|
|
'sha256 不一致必须写入 JSON reason。',
|
|
);
|
|
}
|
|
|
|
function assertUnsafePathFails() {
|
|
const fixture = prepareEvidenceBundleFixture('unsafe-path');
|
|
const manifestPath = path.join(fixture.dir, 'manifest.json');
|
|
const manifest = readJson(manifestPath);
|
|
manifest.files.snapshot.path = '../snapshot.json';
|
|
writeJson(manifestPath, manifest);
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, 'manifest 文件 path 逃逸证据目录时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'只能是证据目录内的安全文件名',
|
|
'unsafe path 失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertStringMetadataFailsExceptManifest() {
|
|
const fixture = prepareEvidenceBundleFixture('string-metadata');
|
|
const manifestPath = path.join(fixture.dir, 'manifest.json');
|
|
const manifest = readJson(manifestPath);
|
|
manifest.files.snapshot = 'snapshot.json';
|
|
writeJson(manifestPath, manifest);
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, '非 manifest 文件仍用字符串登记时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'必须使用 { path, sizeBytes, sha256 } 元数据对象',
|
|
'字符串 metadata 失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertExtraFileFailsByDefault() {
|
|
const fixture = prepareEvidenceBundleFixture('extra-file');
|
|
writeFileSync(path.join(fixture.dir, 'operator-note.txt'), 'manual note\n', 'utf8');
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, '证据目录混入未登记普通文件时默认必须失败。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'extra file not registered in manifest.files',
|
|
'未登记普通文件失败必须写入 JSON reason。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'operator-note.txt',
|
|
'未登记普通文件失败必须输出文件名。',
|
|
);
|
|
}
|
|
|
|
function assertExtraDirectoryFailsByDefault() {
|
|
const fixture = prepareEvidenceBundleFixture('extra-directory');
|
|
mkdirSync(path.join(fixture.dir, 'notes'), { recursive: true });
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, '证据目录混入未登记目录时默认必须失败。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'extra directory not registered in manifest.files',
|
|
'未登记目录失败必须写入 JSON reason。',
|
|
);
|
|
}
|
|
|
|
function assertExtraSymlinkFailsByDefault() {
|
|
const fixture = prepareEvidenceBundleFixture('extra-symlink');
|
|
writeFileSync(path.join(fixture.dir, 'target.txt'), 'target\n', 'utf8');
|
|
const manifest = readJson(path.join(fixture.dir, 'manifest.json'));
|
|
manifest.files.target = metadataFor(path.join(fixture.dir, 'target.txt'));
|
|
writeJson(path.join(fixture.dir, 'manifest.json'), manifest);
|
|
symlinkSync(path.join(fixture.dir, 'target.txt'), path.join(fixture.dir, 'target-link.txt'));
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, '证据目录混入未登记符号链接时默认必须失败。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'extra symlink not registered in manifest.files',
|
|
'未登记符号链接失败必须写入 JSON reason。',
|
|
);
|
|
}
|
|
|
|
function assertAllowExtraFilesOverrideSucceeds() {
|
|
const fixture = prepareEvidenceBundleFixture('allow-extra-file');
|
|
writeFileSync(path.join(fixture.dir, 'operator-note.txt'), 'manual note\n', 'utf8');
|
|
const result = runVerify(['--bundle-dir', fixture.dir, '--allow-extra-files']);
|
|
assertStatus(result, 0, '显式允许未登记文件时 verifier 应通过。');
|
|
if (result.status !== 0) {
|
|
return;
|
|
}
|
|
const output = parseJson(result.stdout, '允许未登记文件校验 stdout');
|
|
assertEqual(output.ok, true, '允许未登记文件时 stdout 必须 ok=true。');
|
|
assertEqual(
|
|
output.allowExtraFiles,
|
|
true,
|
|
'允许未登记文件时 stdout 必须记录 allowExtraFiles=true。',
|
|
);
|
|
assertEqual(
|
|
output.extraFiles?.length,
|
|
0,
|
|
'允许未登记文件时 stdout 不应输出失败 extraFiles。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsRootPath() {
|
|
const result = runVerify(['--bundle-dir', path.parse(process.cwd()).root]);
|
|
assertStatus(result, 1, '证据目录为文件系统根目录时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'不能是文件系统根目录',
|
|
'根目录失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsEntryPathControlCharacters() {
|
|
const fixture = prepareEvidenceBundleFixture('entry-control-character');
|
|
const result = runVerify(['--bundle-dir', `${fixture.dir}\n--allow-extra-files`]);
|
|
assertStatus(result, 1, '入口路径带换行控制字符时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'--bundle-dir 不能包含换行或 NUL 字符',
|
|
'入口路径控制字符失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsSymlinkBundleDir() {
|
|
const fixture = prepareEvidenceBundleFixture('symlink-target');
|
|
const linkPath = path.join(tmpRoot, 'symlink-bundle');
|
|
symlinkSync(fixture.dir, linkPath);
|
|
const result = runVerify(['--bundle-dir', linkPath]);
|
|
assertStatus(result, 1, '证据目录是符号链接时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'证据目录不能是符号链接',
|
|
'符号链接证据目录失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsNonDirectoryBundleDir() {
|
|
const filePath = path.join(tmpRoot, 'not-a-dir');
|
|
writeFileSync(filePath, 'not a dir\n', 'utf8');
|
|
const result = runVerify(['--bundle-dir', filePath]);
|
|
assertStatus(result, 1, '证据目录不是目录时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'证据目录必须是目录',
|
|
'非目录证据路径失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsInvalidJson() {
|
|
const dir = path.join(tmpRoot, 'invalid-json');
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(path.join(dir, 'manifest.json'), '{ not json\n', 'utf8');
|
|
const result = runVerify(['--bundle-dir', dir]);
|
|
assertStatus(result, 1, 'manifest JSON 非法时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'manifest 不是合法 JSON',
|
|
'非法 JSON 失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsBothEntryModes() {
|
|
const fixture = prepareEvidenceBundleFixture('both-entry-modes');
|
|
const result = runVerify([
|
|
'--bundle-dir',
|
|
fixture.dir,
|
|
'--manifest',
|
|
path.join(fixture.dir, 'manifest.json'),
|
|
]);
|
|
assertStatus(result, 1, '同时提供 manifest 和 bundle-dir 时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'只能二选一',
|
|
'入口参数互斥失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsRelativeEntryPath() {
|
|
const result = runVerify(['--bundle-dir', 'relative-evidence']);
|
|
assertStatus(result, 1, '入口路径是相对路径时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'必须是绝对路径',
|
|
'相对路径失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsManifestFilePathControlCharacters() {
|
|
const fixture = prepareEvidenceBundleFixture('metadata-control-character');
|
|
const manifestPath = path.join(fixture.dir, 'manifest.json');
|
|
const manifest = readJson(manifestPath);
|
|
manifest.files.snapshot.path = 'snapshot.json\noperator-note.txt';
|
|
writeJson(manifestPath, manifest);
|
|
const result = runVerify(['--bundle-dir', fixture.dir]);
|
|
assertStatus(result, 1, 'manifest 文件 path 带控制字符时必须失败。');
|
|
assertIncludes(
|
|
`${result.stdout}\n${result.stderr}`,
|
|
'只能是证据目录内的安全文件名',
|
|
'manifest 文件 path 控制字符失败必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function prepareEvidenceBundleFixture(name) {
|
|
const dir = path.join(tmpRoot, name);
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(path.join(dir, 'snapshot.json'), '{"ok":true}\n', 'utf8');
|
|
writeFileSync(path.join(dir, 'snapshot.stdout.txt'), '{"ok":true}\n', 'utf8');
|
|
writeFileSync(path.join(dir, 'snapshot.stderr.txt'), '', 'utf8');
|
|
writeFileSync(
|
|
path.join(dir, 'snapshot-command.json'),
|
|
'{"executable":"node"}\n',
|
|
'utf8',
|
|
);
|
|
writeJson(path.join(dir, 'manifest.json'), {
|
|
schemaVersion: 1,
|
|
summary: {
|
|
status: 'OK',
|
|
},
|
|
files: {
|
|
manifest: 'manifest.json',
|
|
snapshot: metadataFor(path.join(dir, 'snapshot.json')),
|
|
snapshotParseError: null,
|
|
snapshotStdout: metadataFor(path.join(dir, 'snapshot.stdout.txt')),
|
|
snapshotStderr: metadataFor(path.join(dir, 'snapshot.stderr.txt')),
|
|
snapshotCommand: metadataFor(path.join(dir, 'snapshot-command.json')),
|
|
directLive: null,
|
|
directLiveParseError: null,
|
|
directLiveStdout: null,
|
|
directLiveStderr: null,
|
|
directLiveCommand: null,
|
|
},
|
|
});
|
|
return { dir };
|
|
}
|
|
|
|
function prepareCommandEvidenceFixture(name) {
|
|
const dir = path.join(tmpRoot, name);
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(path.join(dir, 'command.stdout.txt'), 'ok\n', 'utf8');
|
|
writeFileSync(path.join(dir, 'command.stderr.txt'), '', 'utf8');
|
|
writeFileSync(
|
|
path.join(dir, 'command-record.json'),
|
|
'{"exitCode":0}\n',
|
|
'utf8',
|
|
);
|
|
writeJson(path.join(dir, 'manifest.json'), {
|
|
schemaVersion: 1,
|
|
summary: {
|
|
status: 'OK',
|
|
exitCode: 0,
|
|
},
|
|
files: {
|
|
manifest: 'manifest.json',
|
|
stdout: metadataFor(path.join(dir, 'command.stdout.txt')),
|
|
stderr: metadataFor(path.join(dir, 'command.stderr.txt')),
|
|
commandRecord: metadataFor(path.join(dir, 'command-record.json')),
|
|
},
|
|
});
|
|
return { dir };
|
|
}
|
|
|
|
function metadataFor(filePath) {
|
|
const content = readFileSync(filePath);
|
|
return {
|
|
path: path.basename(filePath),
|
|
sizeBytes: content.length,
|
|
sha256: createHash('sha256').update(content).digest('hex'),
|
|
};
|
|
}
|
|
|
|
function runVerify(args) {
|
|
return spawnSync('node', [VERIFY_SCRIPT, ...args], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
});
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
try {
|
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
} catch (error) {
|
|
failures.push(`${filePath} 不是合法 JSON: ${error.message}`);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function writeJson(filePath, value) {
|
|
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
}
|
|
|
|
function parseJson(raw, label) {
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch (error) {
|
|
failures.push(`${label} 不是合法 JSON: ${error.message}\n${raw}`);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function assertStatus(result, expected, reason) {
|
|
const actual = result.status ?? 0;
|
|
if (actual !== expected) {
|
|
failures.push(
|
|
`${reason} 预期退出码 ${expected},实际 ${actual}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertIncludes(content, needle, reason) {
|
|
if (!String(content).includes(needle)) {
|
|
failures.push(`${reason} 缺少: ${needle}`);
|
|
}
|
|
}
|
|
|
|
function assertEqual(actual, expected, reason) {
|
|
if (actual !== expected) {
|
|
failures.push(`${reason} 预期 ${JSON.stringify(expected)},实际 ${JSON.stringify(actual)}`);
|
|
}
|
|
}
|