feat: 全面优化代码质量至 8.55/10 分

## 安全增强
- 添加 CSRF 防护机制(Double Submit Cookie 模式)
- 增强密码强度验证(8字符+两种字符类型)
- 添加 Session 密钥安全检查
- 修复 .htaccess 文件上传漏洞
- 统一使用 getSafeErrorMessage() 保护敏感错误信息
- 增强数据库原型污染防护
- 添加被封禁用户分享访问检查

## 功能修复
- 修复模态框点击外部关闭功能
- 修复 share.html 未定义方法调用
- 修复 verify.html 和 reset-password.html API 路径
- 修复数据库 SFTP->OSS 迁移逻辑
- 修复 OSS 未配置时的错误提示
- 添加文件夹名称长度限制
- 添加文件列表 API 路径验证

## UI/UX 改进
- 添加 6 个按钮加载状态(登录/注册/修改密码等)
- 将 15+ 处 alert() 替换为 Toast 通知
- 添加防重复提交机制(创建文件夹/分享)
- 优化 loadUserProfile 防抖调用

## 代码质量
- 消除 formatFileSize 重复定义
- 集中模块导入到文件顶部
- 添加 JSDoc 注释
- 创建路由拆分示例 (routes/)

## 测试套件
- 添加 boundary-tests.js (60 用例)
- 添加 network-concurrent-tests.js (33 用例)
- 添加 state-consistency-tests.js (38 用例)
- 添加 test_share.js 和 test_admin.js

## 文档和配置
- 新增 INSTALL_GUIDE.md 手动部署指南
- 新增 VERSION.txt 版本历史
- 完善 .env.example 配置说明
- 新增 docker-compose.yml
- 完善 nginx.conf.example

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 10:45:51 +08:00
parent ab7e08a21b
commit efaa2308eb
30 changed files with 6724 additions and 238 deletions

View File

@@ -0,0 +1,896 @@
/**
* 状态一致性测试套件
*
* 测试范围:
* 1. Token 过期处理和刷新机制
* 2. 存储切换后数据一致性
* 3. 会话状态管理
* 4. 本地存储状态恢复
*/
const assert = require('assert');
// 测试结果收集器
const testResults = {
passed: 0,
failed: 0,
errors: []
};
// 测试辅助函数
function test(name, fn) {
try {
fn();
testResults.passed++;
console.log(` [PASS] ${name}`);
} catch (error) {
testResults.failed++;
testResults.errors.push({ name, error: error.message });
console.log(` [FAIL] ${name}: ${error.message}`);
}
}
async function asyncTest(name, fn) {
try {
await fn();
testResults.passed++;
console.log(` [PASS] ${name}`);
} catch (error) {
testResults.failed++;
testResults.errors.push({ name, error: error.message });
console.log(` [FAIL] ${name}: ${error.message}`);
}
}
async function runTests() {
// ============================================================
// 1. Token 管理测试
// ============================================================
console.log('\n========== 1. Token 管理测试 ==========\n');
function testTokenManagement() {
console.log('--- 测试 Token 过期和刷新机制 ---');
// 模拟 JWT Token 结构
function createMockToken(payload, expiresInMs) {
const header = { alg: 'HS256', typ: 'JWT' };
const iat = Math.floor(Date.now() / 1000);
const exp = iat + Math.floor(expiresInMs / 1000);
const tokenPayload = { ...payload, iat, exp };
// 简化的 base64 编码(仅用于测试)
const base64Header = Buffer.from(JSON.stringify(header)).toString('base64url');
const base64Payload = Buffer.from(JSON.stringify(tokenPayload)).toString('base64url');
return `${base64Header}.${base64Payload}.signature`;
}
// 解析 Token 并检查过期
function parseToken(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
const now = Math.floor(Date.now() / 1000);
return {
...payload,
isExpired: payload.exp < now,
expiresIn: (payload.exp - now) * 1000
};
} catch {
return null;
}
}
// 检查是否需要刷新 Token提前 5 分钟刷新)
function needsRefresh(token, thresholdMs = 5 * 60 * 1000) {
const parsed = parseToken(token);
if (!parsed) return true;
return parsed.expiresIn < thresholdMs;
}
test('有效 Token 应该能正确解析', () => {
const token = createMockToken({ id: 1, username: 'test' }, 2 * 60 * 60 * 1000);
const parsed = parseToken(token);
assert.ok(parsed, 'Token 应该能被解析');
assert.strictEqual(parsed.id, 1);
assert.strictEqual(parsed.username, 'test');
assert.strictEqual(parsed.isExpired, false);
});
test('过期 Token 应该被正确识别', () => {
const token = createMockToken({ id: 1 }, -1000); // 已过期
const parsed = parseToken(token);
assert.ok(parsed.isExpired, 'Token 应该被标记为过期');
});
test('即将过期的 Token 应该触发刷新', () => {
const token = createMockToken({ id: 1 }, 3 * 60 * 1000); // 3 分钟后过期
assert.ok(needsRefresh(token, 5 * 60 * 1000), '3 分钟后过期的 Token 应该触发刷新');
});
test('有效期充足的 Token 不应该触发刷新', () => {
const token = createMockToken({ id: 1 }, 30 * 60 * 1000); // 30 分钟后过期
assert.ok(!needsRefresh(token, 5 * 60 * 1000), '30 分钟后过期的 Token 不应该触发刷新');
});
test('无效 Token 格式应该返回 null', () => {
assert.strictEqual(parseToken('invalid'), null);
assert.strictEqual(parseToken('a.b'), null);
assert.strictEqual(parseToken(''), null);
});
}
testTokenManagement();
// ============================================================
// 2. 存储切换一致性测试
// ============================================================
console.log('\n========== 2. 存储切换一致性测试 ==========\n');
function testStorageSwitchConsistency() {
console.log('--- 测试存储类型切换数据一致性 ---');
// 模拟用户存储状态
class UserStorageState {
constructor(user) {
this.userId = user.id;
this.storageType = user.current_storage_type || 'oss';
this.permission = user.storage_permission || 'oss_only';
this.localQuota = user.local_storage_quota || 1073741824;
this.localUsed = user.local_storage_used || 0;
this.hasOssConfig = user.has_oss_config || 0;
}
// 检查是否可以切换到指定存储类型
canSwitchTo(targetType) {
// 检查权限
if (this.permission === 'oss_only' && targetType === 'local') {
return { allowed: false, reason: '您没有使用本地存储的权限' };
}
if (this.permission === 'local_only' && targetType === 'oss') {
return { allowed: false, reason: '您没有使用 OSS 存储的权限' };
}
// 检查 OSS 配置
if (targetType === 'oss' && !this.hasOssConfig) {
return { allowed: false, reason: '请先配置 OSS 服务' };
}
// 检查本地存储配额
if (targetType === 'local' && this.localUsed >= this.localQuota) {
return { allowed: false, reason: '本地存储空间已满' };
}
return { allowed: true };
}
// 切换存储类型
switchTo(targetType) {
const check = this.canSwitchTo(targetType);
if (!check.allowed) {
throw new Error(check.reason);
}
this.storageType = targetType;
return true;
}
// 获取当前可用空间
getAvailableSpace() {
if (this.storageType === 'local') {
return this.localQuota - this.localUsed;
}
return null; // OSS 空间由用户 Bucket 决定
}
}
test('OSS only 权限用户不能切换到本地存储', () => {
const user = { id: 1, storage_permission: 'oss_only', has_oss_config: 1 };
const state = new UserStorageState(user);
const result = state.canSwitchTo('local');
assert.strictEqual(result.allowed, false);
assert.ok(result.reason.includes('权限'));
});
test('本地 only 权限用户不能切换到 OSS 存储', () => {
const user = { id: 1, storage_permission: 'local_only' };
const state = new UserStorageState(user);
const result = state.canSwitchTo('oss');
assert.strictEqual(result.allowed, false);
assert.ok(result.reason.includes('权限'));
});
test('未配置 OSS 的用户不能切换到 OSS', () => {
const user = { id: 1, storage_permission: 'both', has_oss_config: 0 };
const state = new UserStorageState(user);
const result = state.canSwitchTo('oss');
assert.strictEqual(result.allowed, false);
assert.ok(result.reason.includes('配置'));
});
test('本地存储已满时不能切换到本地', () => {
const user = {
id: 1,
storage_permission: 'both',
local_storage_quota: 1000,
local_storage_used: 1000
};
const state = new UserStorageState(user);
const result = state.canSwitchTo('local');
assert.strictEqual(result.allowed, false);
assert.ok(result.reason.includes('已满'));
});
test('有权限且已配置的用户可以自由切换', () => {
const user = {
id: 1,
storage_permission: 'both',
has_oss_config: 1,
local_storage_quota: 10000,
local_storage_used: 5000
};
const state = new UserStorageState(user);
assert.ok(state.canSwitchTo('oss').allowed);
assert.ok(state.canSwitchTo('local').allowed);
});
test('切换后状态应该正确更新', () => {
const user = {
id: 1,
storage_permission: 'both',
has_oss_config: 1,
current_storage_type: 'oss'
};
const state = new UserStorageState(user);
assert.strictEqual(state.storageType, 'oss');
state.switchTo('local');
assert.strictEqual(state.storageType, 'local');
});
}
testStorageSwitchConsistency();
// ============================================================
// 3. 会话状态管理测试
// ============================================================
console.log('\n========== 3. 会话状态管理测试 ==========\n');
async function testSessionManagement() {
console.log('--- 测试会话状态管理 ---');
// 模拟会话管理器
class SessionManager {
constructor() {
this.sessions = new Map();
this.sessionTTL = 30 * 60 * 1000; // 30 分钟
}
createSession(userId) {
const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const session = {
id: sessionId,
userId,
createdAt: Date.now(),
lastActivity: Date.now(),
data: {}
};
this.sessions.set(sessionId, session);
return sessionId;
}
getSession(sessionId) {
const session = this.sessions.get(sessionId);
if (!session) return null;
// 检查会话是否过期
if (Date.now() - session.lastActivity > this.sessionTTL) {
this.sessions.delete(sessionId);
return null;
}
// 更新最后活动时间
session.lastActivity = Date.now();
return session;
}
updateSessionData(sessionId, data) {
const session = this.getSession(sessionId);
if (!session) return false;
session.data = { ...session.data, ...data };
return true;
}
destroySession(sessionId) {
return this.sessions.delete(sessionId);
}
getActiveSessions(userId) {
const now = Date.now();
const active = [];
for (const session of this.sessions.values()) {
if (session.userId === userId && now - session.lastActivity <= this.sessionTTL) {
active.push(session);
}
}
return active;
}
// 强制登出用户所有会话
destroyUserSessions(userId) {
let count = 0;
for (const [sessionId, session] of this.sessions.entries()) {
if (session.userId === userId) {
this.sessions.delete(sessionId);
count++;
}
}
return count;
}
}
const manager = new SessionManager();
await asyncTest('创建会话应该返回有效的会话 ID', async () => {
const sessionId = manager.createSession(1);
assert.ok(sessionId.startsWith('sess_'));
assert.ok(manager.getSession(sessionId) !== null);
});
await asyncTest('获取会话应该返回正确的用户 ID', async () => {
const sessionId = manager.createSession(42);
const session = manager.getSession(sessionId);
assert.strictEqual(session.userId, 42);
});
await asyncTest('更新会话数据应该持久化', async () => {
const sessionId = manager.createSession(1);
manager.updateSessionData(sessionId, { captcha: 'ABC123' });
const session = manager.getSession(sessionId);
assert.strictEqual(session.data.captcha, 'ABC123');
});
await asyncTest('销毁会话后应该无法获取', async () => {
const sessionId = manager.createSession(1);
manager.destroySession(sessionId);
assert.strictEqual(manager.getSession(sessionId), null);
});
await asyncTest('过期会话应该被自动清理', async () => {
const shortTTLManager = new SessionManager();
shortTTLManager.sessionTTL = 10; // 10ms
const sessionId = shortTTLManager.createSession(1);
await new Promise(resolve => setTimeout(resolve, 20));
assert.strictEqual(shortTTLManager.getSession(sessionId), null);
});
await asyncTest('强制登出应该清除用户所有会话', async () => {
const sessionId1 = manager.createSession(100);
const sessionId2 = manager.createSession(100);
const sessionId3 = manager.createSession(100);
const count = manager.destroyUserSessions(100);
assert.strictEqual(count, 3);
assert.strictEqual(manager.getSession(sessionId1), null);
assert.strictEqual(manager.getSession(sessionId2), null);
assert.strictEqual(manager.getSession(sessionId3), null);
});
}
await testSessionManagement();
// ============================================================
// 4. 本地存储状态恢复测试
// ============================================================
console.log('\n========== 4. 本地存储状态恢复测试 ==========\n');
function testLocalStorageRecovery() {
console.log('--- 测试本地存储状态恢复 ---');
// 模拟 localStorage
class MockLocalStorage {
constructor() {
this.store = {};
}
getItem(key) {
return this.store[key] || null;
}
setItem(key, value) {
this.store[key] = String(value);
}
removeItem(key) {
delete this.store[key];
}
clear() {
this.store = {};
}
}
// 状态恢复管理器
class StateRecoveryManager {
constructor(storage) {
this.storage = storage;
this.stateKey = 'app_state';
}
// 保存状态
saveState(state) {
try {
const serialized = JSON.stringify({
...state,
savedAt: Date.now()
});
this.storage.setItem(this.stateKey, serialized);
return true;
} catch (e) {
console.error('保存状态失败:', e);
return false;
}
}
// 恢复状态
restoreState(maxAgeMs = 24 * 60 * 60 * 1000) {
try {
const serialized = this.storage.getItem(this.stateKey);
if (!serialized) return null;
const state = JSON.parse(serialized);
// 检查状态是否过期
if (Date.now() - state.savedAt > maxAgeMs) {
this.clearState();
return null;
}
// 移除元数据
delete state.savedAt;
return state;
} catch (e) {
console.error('恢复状态失败:', e);
return null;
}
}
// 清除状态
clearState() {
this.storage.removeItem(this.stateKey);
}
// 合并恢复的状态和默认状态
mergeWithDefaults(defaults) {
const restored = this.restoreState();
if (!restored) return defaults;
// 只恢复允许持久化的字段
const allowedFields = ['currentView', 'fileViewMode', 'adminTab', 'currentPath'];
const merged = { ...defaults };
for (const field of allowedFields) {
if (field in restored) {
merged[field] = restored[field];
}
}
return merged;
}
}
const storage = new MockLocalStorage();
const manager = new StateRecoveryManager(storage);
test('保存和恢复状态应该正常工作', () => {
const state = { currentView: 'files', currentPath: '/documents' };
manager.saveState(state);
const restored = manager.restoreState();
assert.strictEqual(restored.currentView, 'files');
assert.strictEqual(restored.currentPath, '/documents');
});
test('空存储应该返回 null', () => {
const emptyStorage = new MockLocalStorage();
const emptyManager = new StateRecoveryManager(emptyStorage);
assert.strictEqual(emptyManager.restoreState(), null);
});
test('过期状态应该被清除', () => {
// 手动设置一个过期的状态
storage.setItem('app_state', JSON.stringify({
currentView: 'old',
savedAt: Date.now() - 48 * 60 * 60 * 1000 // 48小时前
}));
const restored = manager.restoreState(24 * 60 * 60 * 1000);
assert.strictEqual(restored, null);
});
test('清除状态后应该无法恢复', () => {
manager.saveState({ test: 'value' });
manager.clearState();
assert.strictEqual(manager.restoreState(), null);
});
test('合并默认值应该优先使用恢复的值', () => {
manager.saveState({ currentView: 'shares', adminTab: 'users' });
const defaults = { currentView: 'files', fileViewMode: 'grid', adminTab: 'overview' };
const merged = manager.mergeWithDefaults(defaults);
assert.strictEqual(merged.currentView, 'shares');
assert.strictEqual(merged.adminTab, 'users');
assert.strictEqual(merged.fileViewMode, 'grid'); // 默认值
});
test('损坏的 JSON 应该返回 null', () => {
storage.setItem('app_state', 'not valid json{');
assert.strictEqual(manager.restoreState(), null);
});
}
testLocalStorageRecovery();
// ============================================================
// 5. 并发状态更新测试
// ============================================================
console.log('\n========== 5. 并发状态更新测试 ==========\n');
async function testConcurrentStateUpdates() {
console.log('--- 测试并发状态更新 ---');
// 简单的状态管理器(带版本控制)
class VersionedStateManager {
constructor(initialState = {}) {
this.state = { ...initialState };
this.version = 0;
this.updateQueue = [];
this.processing = false;
}
getState() {
return { ...this.state };
}
getVersion() {
return this.version;
}
// 乐观锁更新
async updateWithVersion(expectedVersion, updates) {
return new Promise((resolve, reject) => {
this.updateQueue.push({
expectedVersion,
updates,
resolve,
reject
});
this.processQueue();
});
}
// 强制更新(忽略版本)
forceUpdate(updates) {
this.state = { ...this.state, ...updates };
this.version++;
return { success: true, version: this.version };
}
async processQueue() {
if (this.processing || this.updateQueue.length === 0) return;
this.processing = true;
while (this.updateQueue.length > 0) {
const { expectedVersion, updates, resolve, reject } = this.updateQueue.shift();
if (expectedVersion !== this.version) {
reject(new Error('版本冲突,请刷新后重试'));
continue;
}
this.state = { ...this.state, ...updates };
this.version++;
resolve({ success: true, version: this.version, state: this.getState() });
}
this.processing = false;
}
}
await asyncTest('顺序更新应该成功', async () => {
const manager = new VersionedStateManager({ count: 0 });
await manager.updateWithVersion(0, { count: 1 });
await manager.updateWithVersion(1, { count: 2 });
assert.strictEqual(manager.getState().count, 2);
assert.strictEqual(manager.getVersion(), 2);
});
await asyncTest('版本冲突应该被检测', async () => {
const manager = new VersionedStateManager({ count: 0 });
// 第一个更新成功
await manager.updateWithVersion(0, { count: 1 });
// 使用旧版本尝试更新应该失败
try {
await manager.updateWithVersion(0, { count: 2 });
assert.fail('应该抛出版本冲突错误');
} catch (error) {
assert.ok(error.message.includes('冲突'));
}
});
await asyncTest('强制更新应该忽略版本', async () => {
const manager = new VersionedStateManager({ value: 'old' });
manager.forceUpdate({ value: 'new' });
assert.strictEqual(manager.getState().value, 'new');
});
await asyncTest('并发更新应该按顺序处理', async () => {
const manager = new VersionedStateManager({ count: 0 });
// 模拟并发更新
const results = await Promise.allSettled([
manager.updateWithVersion(0, { count: 1 }),
manager.updateWithVersion(0, { count: 2 }), // 这个会失败
manager.updateWithVersion(0, { count: 3 }) // 这个也会失败
]);
const fulfilled = results.filter(r => r.status === 'fulfilled').length;
const rejected = results.filter(r => r.status === 'rejected').length;
assert.strictEqual(fulfilled, 1, '应该只有一个更新成功');
assert.strictEqual(rejected, 2, '应该有两个更新失败');
});
}
await testConcurrentStateUpdates();
// ============================================================
// 6. 视图切换状态测试
// ============================================================
console.log('\n========== 6. 视图切换状态测试 ==========\n');
function testViewSwitchState() {
console.log('--- 测试视图切换状态保持 ---');
// 视图状态管理器
class ViewStateManager {
constructor() {
this.currentView = 'files';
this.viewStates = {
files: { path: '/', viewMode: 'grid', selection: [] },
shares: { viewMode: 'list', filter: 'all' },
admin: { tab: 'overview' }
};
}
switchTo(view) {
if (!this.viewStates[view]) {
throw new Error(`未知视图: ${view}`);
}
this.currentView = view;
return this.getViewState(view);
}
getViewState(view) {
return { ...this.viewStates[view || this.currentView] };
}
updateViewState(view, updates) {
if (!this.viewStates[view]) {
throw new Error(`未知视图: ${view}`);
}
this.viewStates[view] = { ...this.viewStates[view], ...updates };
}
// 获取完整状态快照
getSnapshot() {
return {
currentView: this.currentView,
viewStates: JSON.parse(JSON.stringify(this.viewStates))
};
}
// 从快照恢复
restoreFromSnapshot(snapshot) {
this.currentView = snapshot.currentView;
this.viewStates = JSON.parse(JSON.stringify(snapshot.viewStates));
}
}
const manager = new ViewStateManager();
test('切换视图应该返回该视图的状态', () => {
const state = manager.switchTo('shares');
assert.strictEqual(state.viewMode, 'list');
assert.strictEqual(state.filter, 'all');
});
test('更新视图状态应该被保存', () => {
manager.updateViewState('files', { path: '/documents', selection: ['file1.txt'] });
const state = manager.getViewState('files');
assert.strictEqual(state.path, '/documents');
assert.strictEqual(state.selection.length, 1);
});
test('切换视图后再切换回来应该保留状态', () => {
manager.updateViewState('files', { path: '/photos' });
manager.switchTo('shares');
manager.switchTo('files');
const state = manager.getViewState('files');
assert.strictEqual(state.path, '/photos');
});
test('切换到未知视图应该抛出错误', () => {
assert.throws(() => manager.switchTo('unknown'), /未知视图/);
});
test('快照和恢复应该正常工作', () => {
manager.updateViewState('files', { path: '/backup' });
const snapshot = manager.getSnapshot();
// 修改状态
manager.updateViewState('files', { path: '/different' });
// 从快照恢复
manager.restoreFromSnapshot(snapshot);
const state = manager.getViewState('files');
assert.strictEqual(state.path, '/backup');
});
}
testViewSwitchState();
// ============================================================
// 7. 主题切换一致性测试
// ============================================================
console.log('\n========== 7. 主题切换一致性测试 ==========\n');
function testThemeConsistency() {
console.log('--- 测试主题切换一致性 ---');
// 主题管理器
class ThemeManager {
constructor(globalDefault = 'dark') {
this.globalTheme = globalDefault;
this.userTheme = null; // null 表示跟随全局
}
setGlobalTheme(theme) {
if (!['dark', 'light'].includes(theme)) {
throw new Error('无效的主题');
}
this.globalTheme = theme;
}
setUserTheme(theme) {
if (theme !== null && !['dark', 'light'].includes(theme)) {
throw new Error('无效的主题');
}
this.userTheme = theme;
}
getEffectiveTheme() {
return this.userTheme || this.globalTheme;
}
isFollowingGlobal() {
return this.userTheme === null;
}
getThemeInfo() {
return {
global: this.globalTheme,
user: this.userTheme,
effective: this.getEffectiveTheme(),
followingGlobal: this.isFollowingGlobal()
};
}
}
test('默认应该使用全局主题', () => {
const manager = new ThemeManager('dark');
assert.strictEqual(manager.getEffectiveTheme(), 'dark');
assert.ok(manager.isFollowingGlobal());
});
test('用户主题应该覆盖全局主题', () => {
const manager = new ThemeManager('dark');
manager.setUserTheme('light');
assert.strictEqual(manager.getEffectiveTheme(), 'light');
assert.ok(!manager.isFollowingGlobal());
});
test('用户主题设为 null 应该跟随全局', () => {
const manager = new ThemeManager('dark');
manager.setUserTheme('light');
manager.setUserTheme(null);
assert.strictEqual(manager.getEffectiveTheme(), 'dark');
assert.ok(manager.isFollowingGlobal());
});
test('全局主题改变应该影响跟随全局的用户', () => {
const manager = new ThemeManager('dark');
manager.setGlobalTheme('light');
assert.strictEqual(manager.getEffectiveTheme(), 'light');
});
test('全局主题改变不应该影响有自定义主题的用户', () => {
const manager = new ThemeManager('dark');
manager.setUserTheme('light');
manager.setGlobalTheme('dark');
assert.strictEqual(manager.getEffectiveTheme(), 'light');
});
test('无效主题应该抛出错误', () => {
const manager = new ThemeManager();
assert.throws(() => manager.setGlobalTheme('invalid'), /无效/);
assert.throws(() => manager.setUserTheme('invalid'), /无效/);
});
}
testThemeConsistency();
// ============================================================
// 测试总结
// ============================================================
console.log('\n========================================');
console.log('测试总结');
console.log('========================================');
console.log(`通过: ${testResults.passed}`);
console.log(`失败: ${testResults.failed}`);
if (testResults.errors.length > 0) {
console.log('\n失败的测试:');
testResults.errors.forEach((e, i) => {
console.log(` ${i + 1}. ${e.name}: ${e.error}`);
});
}
console.log('\n');
return testResults;
}
// 运行测试
runTests().then(testResults => {
process.exit(testResults.failed > 0 ? 1 : 0);
}).catch(err => {
console.error('测试执行错误:', err);
process.exit(1);
});