play.wenpai.net/scripts/validate-plugins
elementary-qa 36b7ad5288
All checks were successful
Deploy play.wenpai.net / deploy (push) Successful in 3s
feat: add 7-layer test infrastructure for play.wenpai.net
- Schema validation (validate-plugins): field/type/enum/blueprint existence checks
- Deep verification (play-verify): admin notice bar, plugin activation, JS error capture
- Performance baseline (play-verify): navigation + playground load timing
- Visual regression (play-visual): 3 viewports, ImageMagick pixel diff
- Cross-browser (play-verify): --browser firefox/webkit, --mobile viewport
- Accessibility (play-a11y): axe-core WCAG 2 AA audit
- Error monitoring (play-smoke): blueprint URL reachability for all zip references
- Fix: color contrast on featured card (opacity 0.45→0.7) for WCAG AA compliance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:51:08 +08:00

139 lines
4.3 KiB
JavaScript
Executable file

#!/usr/bin/env node
// validate-plugins — plugins.json schema 校验 + 蓝图文件存在性检查
// 用法: validate-plugins [--fix-hints]
const fs = require('fs');
const path = require('path');
const PLAY_DIR = path.join(process.env.HOME, 'play');
const PLUGINS_PATH = path.join(PLAY_DIR, 'plugins.json');
const SCHEMA_PATH = path.join(PLAY_DIR, 'plugins.schema.json');
const RED = '\x1b[31m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', NC = '\x1b[0m';
let errors = 0, warnings = 0;
function fail(msg) { console.error(` ${RED}[✗]${NC} ${msg}`); errors++; }
function pass(msg) { console.log(` ${GREEN}[✓]${NC} ${msg}`); }
function warn(msg) { console.log(` ${YELLOW}[!]${NC} ${msg}`); warnings++; }
// --- 1. 文件可读性 ---
console.log('\n=== plugins.json Schema 校验 ===\n');
let plugins, schema;
try {
plugins = JSON.parse(fs.readFileSync(PLUGINS_PATH, 'utf8'));
pass('plugins.json 可读且为合法 JSON');
} catch (e) {
fail(`plugins.json 读取/解析失败: ${e.message}`);
process.exit(1);
}
try {
schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
pass('plugins.schema.json 可读');
} catch (e) {
fail(`plugins.schema.json 读取失败: ${e.message}`);
process.exit(1);
}
// --- 2. 结构校验 ---
if (!Array.isArray(plugins)) {
fail('plugins.json 顶层必须是数组');
process.exit(1);
}
pass(`${plugins.length} 个条目`);
const requiredFields = schema.items.required;
const properties = schema.items.properties;
const seenIds = new Set();
for (const [i, item] of plugins.entries()) {
const label = `[${i}] ${item.id || '(无id)'}`;
// 必填字段
for (const field of requiredFields) {
if (item[field] === undefined || item[field] === null) {
fail(`${label}: 缺少必填字段 "${field}"`);
}
}
// 额外字段
const allowed = Object.keys(properties);
for (const key of Object.keys(item)) {
if (!allowed.includes(key)) {
fail(`${label}: 未知字段 "${key}"`);
}
}
// ID 唯一性
if (item.id) {
if (seenIds.has(item.id)) {
fail(`${label}: ID 重复`);
}
seenIds.add(item.id);
// ID 格式
if (properties.id.pattern && !new RegExp(properties.id.pattern).test(item.id)) {
fail(`${label}: ID 格式不合法 (应为 ${properties.id.pattern})`);
}
}
// 枚举校验
if (item.icon && properties.icon.enum && !properties.icon.enum.includes(item.icon)) {
fail(`${label}: icon "${item.icon}" 不在允许列表中`);
}
if (item.group && properties.group.enum && !properties.group.enum.includes(item.group)) {
fail(`${label}: group "${item.group}" 不在允许列表中`);
}
// 颜色格式
if (item.color && !(/^#[0-9a-fA-F]{6}$/).test(item.color)) {
fail(`${label}: color "${item.color}" 格式不合法`);
}
// 蓝图路径格式 + 文件存在性
if (item.blueprint) {
if (!(/^\/blueprints\/[a-z0-9-]+\.json$/).test(item.blueprint)) {
fail(`${label}: blueprint 路径格式不合法 "${item.blueprint}"`);
}
const bpFile = path.join(PLAY_DIR, item.blueprint.slice(1));
if (!fs.existsSync(bpFile)) {
fail(`${label}: 蓝图文件不存在 ${bpFile}`);
} else {
// 蓝图 JSON 合法性
try {
JSON.parse(fs.readFileSync(bpFile, 'utf8'));
} catch (e) {
fail(`${label}: 蓝图文件 JSON 解析失败`);
}
}
}
// 描述长度
if (item.desc) {
if (item.desc.length < 10) warn(`${label}: desc 过短 (${item.desc.length} 字符)`);
if (item.desc.length > 200) warn(`${label}: desc 过长 (${item.desc.length} 字符)`);
}
// repo URL 格式(非空时)
if (item.repo && item.repo.length > 0) {
if (!item.repo.startsWith('http://') && !item.repo.startsWith('https://')) {
fail(`${label}: repo URL 格式不合法`);
}
}
}
// --- 3. 蓝图反向检查:有蓝图文件但不在 plugins.json 中 ---
const bpDir = path.join(PLAY_DIR, 'blueprints');
if (fs.existsSync(bpDir)) {
const bpFiles = fs.readdirSync(bpDir).filter(f => f.endsWith('.json'));
const registeredBps = new Set(plugins.map(p => p.blueprint.replace('/blueprints/', '')));
for (const f of bpFiles) {
if (!registeredBps.has(f)) {
warn(`蓝图文件 ${f} 未在 plugins.json 中注册`);
}
}
}
// --- 汇总 ---
console.log(`\n=== 结果: ${errors} 错误, ${warnings} 警告 ===`);
process.exit(errors > 0 ? 1 : 0);