| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- // 自动质检冒烟测试
- // ---------------------------------------------------------------------------
- // 运行:npm test 或 node test/smoke.mjs
- // 零依赖,仅用 node:assert 与 node:test。
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import path from 'node:path';
- import fs from 'node:fs';
- import { fileURLToPath } from 'node:url';
- import * as core from '../lib/index.mjs';
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
- const ROOT = path.resolve(__dirname, '..');
- // ============================================================
- // 1. ESM 入口导出完整性
- // ============================================================
- test('lib/index.mjs 导出全部公共接口', () => {
- const required = [
- 'VERSION',
- 'PLATFORM',
- 'ENDPOINTS',
- 'CREDENTIAL_CHAIN',
- 'TIERS',
- 'RUNTIMES',
- 'CHECKS',
- 'runChecks',
- 'summarize',
- 'validateName',
- 'validatePackageJson',
- 'validateManifest',
- 'validateFrontmatter',
- 'parseFrontmatter',
- 'resolveApiToken',
- 'bootstrap',
- 'INVENTORY',
- 'byTier',
- 'stats',
- 'publishPlan',
- ];
- for (const k of required) {
- assert.ok(k in core, `缺少导出:${k}`);
- }
- assert.equal(core.VERSION, '1.0.1');
- });
- test('ENDPOINTS 真值表结构合法', () => {
- for (const [key, e] of Object.entries(core.ENDPOINTS)) {
- assert.ok(e.url, `${key} 缺 url`);
- assert.ok(['live', 'planned', 'deprecated'].includes(e.status), `${key} status 非法:${e.status}`);
- assert.ok(e.auth, `${key} 缺 auth`);
- assert.ok(e.purpose, `${key} 缺 purpose`);
- }
- // 至少要有 live 端点,否则平台没得用
- assert.ok(core.endpointsByStatus('live').length >= 4, 'live 端点数量异常');
- });
- // ============================================================
- // 2. 命名校验
- // ============================================================
- test('validateName 接受合法名、拒绝非法名', () => {
- assert.equal(core.validateName('skill-my-thing').ok, true);
- assert.equal(core.validateName('fmode-image').ok, true);
- assert.equal(core.validateName('my-thing').ok, false);
- assert.equal(core.validateName('skill-My_Thing').ok, false);
- assert.equal(core.validateName('').ok, false);
- assert.equal(core.validateName(null).ok, false);
- });
- // ============================================================
- // 3. package.json 校验
- // ============================================================
- test('validatePackageJson 对合法包通过', () => {
- const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8'));
- const r = core.validatePackageJson(pkg);
- assert.equal(r.ok, true, `本仓库 package.json 应合法,实际错误:${r.errors.join(';')}`);
- });
- test('validatePackageJson 捕获缺失字段与 CJS 残留', () => {
- const bad = { name: 'skill-x', version: '1.0.0' };
- const r = core.validatePackageJson(bad);
- assert.equal(r.ok, false);
- assert.ok(r.errors.some((e) => e.includes('type')));
- assert.ok(r.errors.some((e) => e.includes('exports')));
- const cjs = {
- name: 'skill-x',
- version: '1.0.0',
- description: 'd',
- type: 'module',
- main: './lib/index.mjs',
- exports: { '.': { import: './lib/index.mjs', default: './lib/index.mjs' } },
- bin: { x: './bin/x.mjs' },
- files: [],
- license: 'MIT',
- require: './lib/index.cjs',
- };
- const r2 = core.validatePackageJson(cjs);
- assert.equal(r2.ok, false);
- assert.ok(r2.errors.some((e) => e.includes('require')));
- });
- // ============================================================
- // 4. frontmatter 解析(三种格式)
- // ============================================================
- test('parseFrontmatter 解析标量与行内数组', () => {
- const text = [
- '---',
- 'name: skill-demo',
- 'description: "一段描述"',
- 'version: 1.0.0',
- 'tags: [a, b, c]',
- '---',
- '',
- '# 标题',
- ].join('\n');
- const { data, body, raw } = core.parseFrontmatter(text);
- assert.equal(data.name, 'skill-demo');
- assert.equal(data.description, '一段描述');
- assert.deepEqual(data.tags, ['a', 'b', 'c']);
- assert.ok(raw !== null);
- assert.ok(body.includes('# 标题'));
- });
- test('validateFrontmatter 校验 hermes 与 skillhub 两种格式', () => {
- const hermes = '---\nname: skill-demo\ndescription: d\nversion: 1.0.0\ntags: [x]\n---\nbody';
- const h = core.validateFrontmatter(hermes, 'hermes');
- assert.equal(h.ok, true, h.errors.join(';'));
- const skillhub = '---\nslug: fmode-skill-demo\ndisplayName: skill-demo\nversion: 1.0.0\nsummary: s\nlicense: MIT\n---\nbody';
- const s = core.validateFrontmatter(skillhub, 'skillhub');
- assert.equal(s.ok, true, s.errors.join(';'));
- // 缺字段应失败
- const broken = '---\nname: skill-demo\n---\nbody';
- assert.equal(core.validateFrontmatter(broken, 'skillhub').ok, false);
- });
- test('本仓库 SKILL.md 满足 skillhub 格式', () => {
- const text = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
- const r = core.validateFrontmatter(text, 'skillhub');
- assert.equal(r.ok, true, `SKILL.md frontmatter 不合法:${r.errors.join(';')}`);
- });
- // ============================================================
- // 5. manifest 校验
- // ============================================================
- test('validateManifest 对本仓库清单通过', () => {
- const m = JSON.parse(fs.readFileSync(path.join(ROOT, 'skill-package-manifest.json'), 'utf-8'));
- const r = core.validateManifest(m);
- assert.equal(r.ok, true, r.errors.join(';'));
- });
- // ============================================================
- // 6. 清单数据完整性
- // ============================================================
- test('INVENTORY 覆盖三个分层且条目字段完整', () => {
- const s = core.stats();
- assert.ok(s.total >= 15, `清单条目过少:${s.total}`);
- assert.ok(s.byTier.system > 0 && s.byTier.service > 0 && s.byTier.application > 0);
- for (const sk of core.INVENTORY) {
- assert.ok(sk.name, '条目缺 name');
- assert.ok(sk.displayName, `${sk.name} 缺 displayName`);
- assert.ok(sk.summary, `${sk.name} 缺 summary`);
- assert.ok(Array.isArray(sk.platforms) && sk.platforms.length, `${sk.name} 缺 platforms`);
- assert.ok(core.TIERS[sk.tier], `${sk.name} tier 非法:${sk.tier}`);
- for (const p of sk.platforms) {
- assert.ok(core.CHANNELS[p], `${sk.name} 平台非法:${p}`);
- }
- // npm 渠道必须给出包名
- if (sk.platforms.includes('npm')) {
- assert.ok(sk.npmName, `${sk.name} 声明了 npm 渠道但缺 npmName`);
- }
- }
- });
- test('任务书点名的技能全部在清单中', () => {
- const expected = [
- 'skill-heterarchy',
- 'skill-multi-branch',
- 'skill-bypass-permission',
- 'skill-task-progress',
- 'plugin-wecom-fix',
- 'skill-agent-clone',
- 'skill-storage',
- 'skill-image',
- 'skill-vision',
- 'skill-listen',
- 'fmode-ffmpeg',
- 'fmode-qiwei',
- 'skill-study-report',
- 'skill-present',
- 'fmode-product-lab',
- ];
- const names = core.INVENTORY.map((s) => s.name);
- for (const e of expected) {
- assert.ok(names.includes(e), `清单缺少任务书点名的技能:${e}`);
- }
- });
- // ============================================================
- // 7. 分发计划
- // ============================================================
- test('publishPlan 覆盖四渠道且命令非空', () => {
- const plan = core.publishPlan('skill-demo', { dir: '.', changelog: 'test' });
- assert.equal(plan.length, 4);
- assert.deepEqual(plan.map((p) => p.channel), ['gogs', 'github', 'npm', 'skillhub']);
- for (const p of plan) {
- assert.ok(p.steps.length > 0, `${p.channel} 无步骤`);
- for (const s of p.steps) assert.equal(typeof s, 'string');
- }
- });
- // ============================================================
- // 8. 六项检查注册表
- // ============================================================
- test('CHECKS 恰好六项且 id 唯一', () => {
- assert.equal(core.CHECKS.length, 6);
- const ids = core.CHECKS.map((c) => c.id);
- assert.equal(new Set(ids).size, 6);
- assert.deepEqual(ids, ['functional', 'apiConnectivity', 'sop', 'dashboard', 'loop', 'multiRuntime']);
- });
- test('summarize 正确聚合(含 skip 语义)', () => {
- const s = core.summarize([
- { id: 'a', status: 'pass' },
- { id: 'b', status: 'pass' },
- { id: 'c', status: 'skip' },
- ]);
- assert.equal(s.total, 3);
- assert.equal(s.pass, 2);
- assert.equal(s.skip, 1);
- assert.equal(s.ok, false, '有 skip 时不应判定为完全通过');
- assert.equal(s.partial, true);
- assert.deepEqual(s.unverified, ['c']);
- const allPass = core.summarize([{ id: 'a', status: 'pass' }, { id: 'b', status: 'pass' }]);
- assert.equal(allPass.ok, true);
- assert.equal(allPass.partial, false);
- });
- // ============================================================
- // 9. 离线质检自跑(本仓库对自己做质检)
- // ============================================================
- test('runChecks --offline 在本仓库上可运行且无失败项', async () => {
- const report = await core.runChecks(ROOT, { offline: true, skipExec: false });
- assert.equal(report.results.length, 6);
- const failed = report.results.filter((r) => r.status === 'fail');
- assert.equal(
- failed.length,
- 0,
- `本仓库质检不应有失败项,实际:\n${failed.map((f) => `${f.id}: ${f.detail}`).join('\n')}`,
- );
- });
- test('runChecks 拒绝未知检查项', async () => {
- await assert.rejects(
- () => core.runChecks(ROOT, { only: ['nope'] }),
- /未知检查项/,
- );
- });
- // ============================================================
- // 10. 凭据解析(不联网,只验证形态与回落行为)
- // ============================================================
- test('validateToken 拒绝 sk-ant- 与非法形态', async () => {
- const { validateToken } = await import('../lib/bootstrap.mjs');
- assert.equal(validateToken('sk-ant-api03-xxxx').ok, false);
- assert.equal(validateToken('not-a-key').ok, false);
- assert.equal(validateToken('').ok, false);
- assert.equal(validateToken(null).ok, false);
- assert.equal(validateToken('sk-abcdefgh12345678').ok, true);
- });
- test('resolveFmodeDir 尊重 FMODE_HOME 覆盖', async () => {
- const { resolveFmodeDir } = await import('../lib/bootstrap.mjs');
- const prev = process.env.FMODE_HOME;
- process.env.FMODE_HOME = '/tmp/fmode-test-home';
- try {
- assert.equal(resolveFmodeDir(), '/tmp/fmode-test-home');
- } finally {
- if (prev === undefined) delete process.env.FMODE_HOME;
- else process.env.FMODE_HOME = prev;
- }
- });
- // ============================================================
- // 11. 浏览器 bundle 无 Node 依赖
- // ============================================================
- test('browser/index.mjs 不 import 任何 node: 内置模块', () => {
- const src = fs.readFileSync(path.join(ROOT, 'browser', 'index.mjs'), 'utf-8');
- const nodeImports = [...src.matchAll(/from\s+['"]node:[a-z_]+['"]/g)];
- assert.equal(nodeImports.length, 0, `browser bundle 违规引入:${nodeImports.map((m) => m[0]).join(', ')}`);
- });
- test('browser bundle 可独立 import 且导出核心常量', async () => {
- const b = await import('../browser/index.mjs');
- assert.equal(b.VERSION, '1.0.1');
- assert.ok(b.PLATFORM.apiBase.includes('fmode.cn'));
- assert.ok(Object.keys(b.ENDPOINTS).length > 0);
- assert.equal(typeof b.validatePackageJson, 'function');
- assert.equal(typeof b.checkApiConnectivity, 'function');
- });
- // ============================================================
- // 12. 脚手架模板完整性
- // ============================================================
- test('templates/skill-starter 脚手架文件齐全', () => {
- const tpl = path.join(ROOT, 'templates', 'skill-starter');
- assert.ok(fs.existsSync(tpl), '脚手架模板目录不存在');
- const required = [
- 'SKILL.md',
- 'package.json',
- 'lib/index.mjs',
- 'skill-package-manifest.json',
- 'README.md',
- 'LICENSE',
- ];
- for (const f of required) {
- assert.ok(fs.existsSync(path.join(tpl, f)), `脚手架缺少 ${f}`);
- }
- const binDir = path.join(tpl, 'bin');
- assert.ok(fs.existsSync(binDir), '脚手架缺少 bin/');
- const bins = fs.readdirSync(binDir).filter((f) => f.endsWith('.mjs'));
- assert.ok(bins.length > 0, '脚手架 bin/ 下无 .mjs 入口');
- const testDir = path.join(tpl, 'test');
- assert.ok(fs.existsSync(testDir), '脚手架缺少 test/');
- });
- test('脚手架占位符齐全(可被 init 替换)', () => {
- const tpl = path.join(ROOT, 'templates', 'skill-starter');
- const pkg = JSON.parse(fs.readFileSync(path.join(tpl, 'package.json'), 'utf-8'));
- assert.equal(pkg.name, '__SKILL_NAME__', 'package.json 应使用 __SKILL_NAME__ 占位符');
- assert.ok(pkg.type === 'module');
- assert.equal(pkg.main, './lib/index.mjs');
- assert.ok(pkg.bin['__CLI_NAME__'], 'bin 应使用 __CLI_NAME__ 占位符');
- });
|