smoke.mjs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. #!/usr/bin/env node
  2. // Copyright (c) 未来飞马
  3. //
  4. // This Source Code Form is subject to the terms of the Mozilla Public
  5. // License, v. 2.0. If a copy of the MPL was not distributed with this
  6. // file, You can obtain one at https://mozilla.org/MPL/2.0/.
  7. //
  8. // Trademark Notice:
  9. // The MPL-2.0 license grants copyright permissions for source code only.
  10. // It does NOT grant any rights to use trademarks including "未来飞马",
  11. // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步".
  12. // Any use of these trademarks requires separate written permission.
  13. /**
  14. * 冒烟检查:不联网、不调模型、不写仓库。
  15. *
  16. * 检查项:
  17. * 1. 必需文件齐全(含标签字典可解析)
  18. * 2. 运行器自检(office 解包 + 分组 + 打标 + 合规 + reorder)
  19. * 3. 单元测试(node --test)
  20. * 4. 未把 token / 私钥 / 内部地址写进包内任何文件
  21. */
  22. import fs from 'node:fs';
  23. import path from 'node:path';
  24. import { spawnSync } from 'node:child_process';
  25. import { fileURLToPath } from 'node:url';
  26. const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  27. const failures = [];
  28. const checks = [];
  29. function record(name, ok, detail) {
  30. checks.push({ name, ok, detail: String(detail ?? '') });
  31. if (!ok) failures.push(name);
  32. }
  33. // 1. 必需文件
  34. const REQUIRED = [
  35. 'SKILL.md', 'README.md', 'package.json', 'skill-package-manifest.json',
  36. 'references/tag-dictionary.json',
  37. 'scripts/case-intake.mjs', 'scripts/office_extract.py', 'scripts/vision-bridge.mjs',
  38. 'scripts/listen-bridge.mjs', 'scripts/cloud-client.mjs', 'scripts/grouping.mjs',
  39. 'scripts/classify.mjs', 'scripts/image-size.mjs', 'scripts/lib.mjs',
  40. 'bin/skill-case-get.js',
  41. ];
  42. {
  43. const missing = REQUIRED.filter((rel) => !fs.existsSync(path.join(ROOT, rel)));
  44. record('必需文件齐全', missing.length === 0, missing.length ? `缺少:${missing.join(', ')}` : `${REQUIRED.length} 个文件`);
  45. }
  46. // 2. 标签字典可解析
  47. {
  48. try {
  49. const dict = JSON.parse(fs.readFileSync(path.join(ROOT, 'references', 'tag-dictionary.json'), 'utf8'));
  50. const dims = Object.keys(dict.dimensions || {});
  51. record('标签字典可解析', dims.length >= 8, `维度:${dims.length} 个(${dims.join(', ')})`);
  52. } catch (error) {
  53. record('标签字典可解析', false, error.message);
  54. }
  55. }
  56. // 3. 运行器自检
  57. {
  58. const run = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'case-intake.mjs'), 'selftest'], { encoding: 'utf8' });
  59. record('case-intake selftest', run.status === 0, run.stdout ? run.stdout.slice(0, 200) : run.stderr);
  60. }
  61. // 4. 单元测试
  62. {
  63. const run = spawnSync(process.execPath, ['--test', path.join(ROOT, 'scripts', 'tests')], { encoding: 'utf8' });
  64. // node --test 传目录在部分版本上会当模块解析;退回 glob
  65. const globRun = run.status === 0 ? run : spawnSync(
  66. process.execPath,
  67. ['--test', path.join(ROOT, 'scripts', 'tests', 'grouping.test.mjs'), path.join(ROOT, 'scripts', 'tests', 'classify.test.mjs')],
  68. { encoding: 'utf8' },
  69. );
  70. const output = `${globRun.stdout || ''}${globRun.stderr || ''}`;
  71. const passMatch = output.match(/# pass (\d+)/) || output.match(/ℹ pass (\d+)/);
  72. const failMatch = output.match(/# fail (\d+)/) || output.match(/ℹ fail (\d+)/);
  73. record('单元测试', globRun.status === 0, `pass=${passMatch ? passMatch[1] : '?'} fail=${failMatch ? failMatch[1] : '?'}`);
  74. }
  75. // 5. 包内不得出现凭据 / 内部地址
  76. {
  77. const FORBIDDEN = [
  78. { label: 'internal-address', re: new RegExp(`\\b${['local', 'host'].join('')}\\b`, 'i') },
  79. { label: 'loopback', re: /\b(127\.0\.0\.1|0\.0\.0\.0)\b/ },
  80. { label: 'parse-session-token', re: /\br:[a-f0-9]{20,}\b/i },
  81. { label: 'api-key', re: /\bsk-[a-zA-Z0-9-]{20,}\b/ },
  82. { label: 'mongo-uri', re: /mongodb(\+srv)?:\/\//i },
  83. ];
  84. const BINARY = new Set(['.png', '.jpg', '.jpeg', '.gif', '.zip', '.pdf']);
  85. // 本文件自身就写着这些正则(和仓库 self-check.mjs 一样自跳过),否则会自己命中自己
  86. const SELF = new Set([path.resolve(fileURLToPath(import.meta.url))]);
  87. const offenders = [];
  88. const walk = (dir) => {
  89. for (const entry of fs.readdirSync(dir)) {
  90. if (['node_modules', 'outputs', '.git', '.claude', 'case-get-outputs'].includes(entry)) continue;
  91. const full = path.join(dir, entry);
  92. const stat = fs.statSync(full);
  93. if (stat.isDirectory()) { walk(full); continue; }
  94. if (SELF.has(path.resolve(full))) continue;
  95. if (BINARY.has(path.extname(full).toLowerCase())) continue;
  96. let content = '';
  97. try { content = fs.readFileSync(full, 'utf8'); } catch { continue; }
  98. for (const item of FORBIDDEN) {
  99. if (item.re.test(content)) offenders.push(`${path.relative(ROOT, full)}:${item.label}`);
  100. }
  101. }
  102. };
  103. walk(ROOT);
  104. record('包内无凭据 / 无内网地址', offenders.length === 0, offenders.length ? offenders.join(', ') : '干净');
  105. }
  106. // 输出
  107. const passed = checks.filter((c) => c.ok).length;
  108. for (const check of checks) {
  109. process.stdout.write(`${check.ok ? '✔' : '✖'} ${check.name} — ${check.detail}\n`);
  110. }
  111. process.stdout.write(`\n冒烟检查:${passed}/${checks.length} 通过\n`);
  112. process.exit(failures.length ? 1 : 0);