smoke.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  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. const fs = require('fs');
  14. const path = require('path');
  15. const os = require('os');
  16. const { pathToFileURL } = require('url');
  17. const ROOT = path.resolve(__dirname, '..');
  18. const SKILL_DIR = path.join(ROOT, 'skills', 'skill-image');
  19. function fail(msg) { console.error('SMOKE FAIL: ' + msg); process.exit(1); }
  20. // 1. 包结构
  21. for (const rel of ['SKILL.md']) {
  22. if (!fs.existsSync(path.join(SKILL_DIR, rel))) fail('missing skills/skill-image/' + rel);
  23. }
  24. for (const rel of ['bin/skill-image.js', 'lib/skill-image.mjs', 'skill-package-manifest.json', '.claude-plugin/plugin.json']) {
  25. if (!fs.existsSync(path.join(ROOT, rel))) fail('missing ' + rel);
  26. }
  27. // 2. 元数据一致性:包名/版本/bin 三处对齐
  28. const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
  29. const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'skill-package-manifest.json'), 'utf8'));
  30. const plugin = JSON.parse(fs.readFileSync(path.join(ROOT, '.claude-plugin', 'plugin.json'), 'utf8'));
  31. if (pkg.name !== 'skill-image') fail('package.json name should be skill-image, got ' + pkg.name);
  32. if (manifest.name !== 'skill-image') fail('manifest name should be skill-image, got ' + manifest.name);
  33. if (plugin.name !== 'skill-image') fail('plugin.json name should be skill-image, got ' + plugin.name);
  34. if (manifest.version !== pkg.version) fail(`version mismatch: manifest ${manifest.version} vs package ${pkg.version}`);
  35. if (plugin.version !== pkg.version) fail(`version mismatch: plugin ${plugin.version} vs package ${pkg.version}`);
  36. if (!pkg.bin || !pkg.bin['skill-image']) fail('package.json bin should expose skill-image');
  37. // 3. 无残留旧名(模式由片段拼出,避免本文件自匹配)
  38. const OLD_NAME = ['fmode', 'image'].join('-');
  39. const OLD_RE = new RegExp(OLD_NAME);
  40. const stale = [];
  41. (function walk(dir) {
  42. for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
  43. if (entry.name === '.git' || entry.name === 'node_modules') continue;
  44. const full = path.join(dir, entry.name);
  45. if (entry.isDirectory()) { walk(full); continue; }
  46. if (!/\.(js|mjs|json|md)$/.test(entry.name)) continue;
  47. const text = fs.readFileSync(full, 'utf8');
  48. if (OLD_RE.test(text)) stale.push(path.relative(ROOT, full));
  49. }
  50. })(ROOT);
  51. if (stale.length) fail(`stale "${OLD_NAME}" references in: ` + stale.join(', '));
  52. (async () => {
  53. // 4. 模块导出
  54. const mod = await import(pathToFileURL(path.join(ROOT, 'lib', 'skill-image.mjs')).href);
  55. for (const fn of ['getApiKey', 'callImageAPI', 'gen', 'genArch', 'genApp', 'genExplode', 'genProduct', 'genScene', 'genDetail', 'genSlide', 'batch']) {
  56. if (typeof mod[fn] !== 'function') fail('export ' + fn + ' is not a function');
  57. }
  58. // 5. 模式注册表完整性
  59. const expected = ['--arch', '--app', '--explode', '--product', '--scene', '--detail', '--slide'];
  60. for (const mode of expected) {
  61. if (!mod.MODES[mode]) fail('missing mode ' + mode);
  62. for (const key of ['prefix', 'suffix', 'size', 'cost', 'desc']) {
  63. if (mod.MODES[mode][key] === undefined) fail(`mode ${mode} missing ${key}`);
  64. }
  65. }
  66. if (Object.keys(mod.MODES).length !== expected.length) {
  67. fail(`expected ${expected.length} modes, got ${Object.keys(mod.MODES).length}`);
  68. }
  69. // 6. 未知模式应抛错
  70. try {
  71. await mod.gen('--nope', 'x', 'smoke');
  72. fail('gen() should reject an unknown mode');
  73. } catch (e) {
  74. if (!/Unknown mode/.test(e.message)) fail('unexpected error for unknown mode: ' + e.message);
  75. }
  76. // 7. Key 解析链:环境变量优先,无任何来源时应抛错并指明设置方式
  77. const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-image-smoke-'));
  78. const oldHome = process.env.HOME;
  79. const savedKey = process.env.FMODE_API_KEY;
  80. const savedAuth = process.env.ANTHROPIC_AUTH_TOKEN;
  81. process.env.HOME = tmpHome;
  82. delete process.env.FMODE_API_KEY;
  83. delete process.env.ANTHROPIC_AUTH_TOKEN;
  84. try {
  85. try {
  86. mod.getApiKey();
  87. fail('getApiKey should throw when no key source exists');
  88. } catch (e) {
  89. if (!/FMODE_API_KEY/.test(e.message)) fail('error message should mention FMODE_API_KEY');
  90. }
  91. // 环境变量命中时应直接返回
  92. process.env.FMODE_API_KEY = 'sk-smoke-test-key-0123456789';
  93. if (mod.getApiKey() !== 'sk-smoke-test-key-0123456789') fail('getApiKey should prefer FMODE_API_KEY env');
  94. } finally {
  95. process.env.HOME = oldHome;
  96. if (savedKey) process.env.FMODE_API_KEY = savedKey; else delete process.env.FMODE_API_KEY;
  97. if (savedAuth) process.env.ANTHROPIC_AUTH_TOKEN = savedAuth;
  98. fs.rmSync(tmpHome, { recursive: true, force: true });
  99. }
  100. console.log('SMOKE OK: skill-image package structure + metadata + module exports + mode registry + key chain verified');
  101. })().catch(e => fail(e.message));