smoke.js 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const os = require('os');
  5. const { pathToFileURL } = require('url');
  6. const ROOT = path.resolve(__dirname, '..');
  7. const SKILL_DIR = path.join(ROOT, 'skills', 'fmode-vision');
  8. function fail(msg) { console.error('SMOKE FAIL: ' + msg); process.exit(1); }
  9. const required = [
  10. 'SKILL.md',
  11. 'scripts/vision-client.mjs',
  12. 'scripts/prompts/room-measurement.mjs'
  13. ];
  14. for (const rel of required) {
  15. if (!fs.existsSync(path.join(SKILL_DIR, rel))) fail('missing ' + rel);
  16. }
  17. (async () => {
  18. const mod = await import(pathToFileURL(path.join(SKILL_DIR, 'scripts', 'vision-client.mjs')).href);
  19. for (const fn of ['resolveApiToken', 'callVisionAPI', 'callMultiPass', 'extractJSON', 'detectHostVisionModel', 'resolveVisionModel', 'analyze']) {
  20. if (typeof mod[fn] !== 'function') fail('export ' + fn + ' is not a function');
  21. }
  22. const { parsed } = mod.extractJSON('text {"a":1} tail');
  23. if (!parsed || parsed.a !== 1) fail('extractJSON did not parse JSON');
  24. // 默认模型:glm-5.3-flash(无宿主配置时回落)
  25. const cleanEnv = { ...process.env };
  26. delete cleanEnv.FMODE_VISION_MODEL;
  27. const savedEnv = process.env;
  28. for (const k of Object.keys(cleanEnv)) if (k === 'FMODE_VISION_MODEL') delete process.env[k];
  29. const fallback = mod.resolveVisionModel();
  30. if (fallback.provider !== 'fmode' || fallback.model !== 'glm-5.3-flash') {
  31. fail('fallback model should be fmode/glm-5.3-flash, got ' + JSON.stringify(fallback));
  32. }
  33. // 宿主探测:FMODE_VISION_MODEL 命中 → host
  34. process.env.FMODE_VISION_MODEL = 'claude-sonnet-4-5';
  35. const host = mod.resolveVisionModel();
  36. if (host.provider !== 'host' || host.model !== 'claude-sonnet-4-5') {
  37. fail('FMODE_VISION_MODEL should resolve to host, got ' + JSON.stringify(host));
  38. }
  39. delete process.env.FMODE_VISION_MODEL;
  40. // analyze() 宿主会话内 → 返回 instruction,不调 API
  41. if (process.env.CLAUDECODE || process.env.CODEX_SANDBOX || process.env.CODEX_HOME) {
  42. process.env.FMODE_VISION_MODEL = 'claude-opus-4-1';
  43. const r = await mod.analyze({ imagePath: '/nonexistent.jpg', systemPrompt: 's', userPrompt: 'u' });
  44. if (r.provider !== 'host' || !r.instruction) fail('analyze() host path did not return instruction');
  45. delete process.env.FMODE_VISION_MODEL;
  46. }
  47. // Token 加载链:隔离 HOME 下无任何 token → 应抛错并列出四级来源
  48. const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'fmode-vision-smoke-'));
  49. const oldHome = process.env.HOME;
  50. process.env.HOME = tmpHome;
  51. const savedToken = process.env.ANTHROPIC_AUTH_TOKEN;
  52. delete process.env.ANTHROPIC_AUTH_TOKEN;
  53. const savedBase = process.env.ANTHROPIC_BASE_URL;
  54. delete process.env.ANTHROPIC_BASE_URL;
  55. const savedFmodeToken = process.env.FMODE_API_TOKEN;
  56. delete process.env.FMODE_API_TOKEN;
  57. try {
  58. mod.resolveApiToken(tmpHome);
  59. fail('resolveApiToken should throw when no token source exists');
  60. } catch (e) {
  61. if (!/FMODE_API_TOKEN/.test(e.message)) fail('error message should mention FMODE_API_TOKEN');
  62. } finally {
  63. process.env.HOME = oldHome;
  64. if (savedToken) process.env.ANTHROPIC_AUTH_TOKEN = savedToken;
  65. if (savedBase) process.env.ANTHROPIC_BASE_URL = savedBase;
  66. if (savedFmodeToken) process.env.FMODE_API_TOKEN = savedFmodeToken;
  67. fs.rmSync(tmpHome, { recursive: true, force: true });
  68. }
  69. console.log('SMOKE OK: fmode-vision package structure + module exports + model selection + token chain verified');
  70. })().catch(e => fail(e.message));