package-smoke-test.js 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. 'use strict';
  2. const assert = require('assert/strict');
  3. const fs = require('fs');
  4. const path = require('path');
  5. const { spawnSync } = require('child_process');
  6. const ROOT = path.resolve(__dirname, '..');
  7. function readJson(relativePath) {
  8. return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), 'utf8').replace(/^\uFEFF/, ''));
  9. }
  10. function runNpmPackDryRun() {
  11. const command = process.platform === 'win32' ? 'cmd.exe' : 'npm';
  12. const args = process.platform === 'win32'
  13. ? ['/d', '/s', '/c', 'npm', 'pack', '--dry-run', '--json']
  14. : ['pack', '--dry-run', '--json'];
  15. const result = spawnSync(command, args, { cwd: ROOT, encoding: 'utf8' });
  16. if (result.status !== 0) throw new Error('npm pack --dry-run 执行失败');
  17. return JSON.parse(result.stdout)[0];
  18. }
  19. function main() {
  20. const pkg = readJson('package.json');
  21. const lock = readJson('package-lock.json');
  22. const manifest = readJson('skill-package-manifest.json');
  23. const plugin = readJson('.claude-plugin/plugin.json');
  24. const serverSource = fs.readFileSync(path.join(ROOT, 'mcp', 'src', 'server.js'), 'utf8');
  25. const serverVersion = serverSource.match(/new McpServer\([\s\S]*?version:\s*['"]([^'"]+)/)?.[1];
  26. const toolNames = [...serverSource.matchAll(/registerTool\(\s*['"]([^'"]+)['"]/g)].map(match => match[1]);
  27. const skillDirs = fs.readdirSync(path.join(ROOT, 'skills'), { withFileTypes: true })
  28. .filter(entry => entry.isDirectory() && fs.existsSync(path.join(ROOT, 'skills', entry.name, 'SKILL.md')))
  29. .map(entry => entry.name)
  30. .sort();
  31. assert.equal(pkg.version, lock.version);
  32. assert.equal(pkg.version, lock.packages?.['']?.version);
  33. assert.equal(pkg.version, manifest.version);
  34. assert.equal(pkg.version, plugin.version);
  35. assert.equal(pkg.version, serverVersion);
  36. assert.equal(pkg.engines?.node, '>=22.5.0');
  37. assert.equal(new Set(toolNames).size, toolNames.length, 'MCP 工具名不能重复');
  38. assert.equal(toolNames.length, manifest.mcpToolCount, 'MCP 工具数量与 manifest 不一致');
  39. assert.deepEqual(skillDirs, [...manifest.skills].sort(), 'Skill 目录与 manifest 不一致');
  40. const pack = runNpmPackDryRun();
  41. const packedPaths = new Set(pack.files.map(item => item.path.replace(/\\/g, '/')));
  42. for (const required of [
  43. '.claude-plugin/plugin.json',
  44. '.env.example',
  45. 'install.js',
  46. 'mcp/src/core/runtime-context.js',
  47. 'mcp/src/core/startup-summary.js',
  48. 'mcp/src/server.js',
  49. 'mcp/src/tools/qiwei-agent-control-run.js',
  50. 'scripts/agent-console-smoke-test.js',
  51. 'scripts/preview-dashboard.js',
  52. 'scripts/startup-preview-smoke-test.js',
  53. 'skill-package-manifest.json',
  54. 'THIRD_PARTY_NOTICES.md',
  55. ]) {
  56. assert(packedPaths.has(required), `npm 包缺少 ${required}`);
  57. }
  58. const forbidden = [...packedPaths].filter(item =>
  59. /(^|\/)(?:node_modules|outputs?|\.playwright-cli|coverage|dist)(\/|$)/i.test(item) ||
  60. /(^|\/)\.env\.local$/i.test(item) ||
  61. /\.(?:db|sqlite|sqlite3|log|tgz|zip)$/i.test(item)
  62. );
  63. assert.deepEqual(forbidden, [], `npm 包包含本地运行文件:${forbidden.join(', ')}`);
  64. process.stdout.write(`${JSON.stringify({
  65. status: 'ok',
  66. version: pkg.version,
  67. skillCount: skillDirs.length,
  68. mcpToolCount: toolNames.length,
  69. packageEntries: pack.entryCount,
  70. packageSize: pack.size,
  71. unpackedSize: pack.unpackedSize,
  72. forbiddenFiles: forbidden.length,
  73. }, null, 2)}\n`);
  74. }
  75. try { main(); }
  76. catch (error) {
  77. process.stderr.write(`${error.message}\n`);
  78. process.exit(1);
  79. }