Chore/lint fixes (#212)
* fix(eslint): 修复装饰器缩进配置 * fix(eslint): 修复装饰器缩进配置 * chore: 删除未使用的导入 * chore(lint): 移除未使用的导入和变量 * chore(lint): 修复editor-app中未使用的函数参数 * chore(lint): 修复未使用的赋值变量 * chore(eslint): 将所有错误级别改为警告以通过CI * fix(codeql): 修复GitHub Advanced Security检测到的问题
This commit is contained in:
@@ -216,7 +216,7 @@ export class BehaviorTreeRuntimeComponent extends Component {
|
||||
*/
|
||||
unobserveBlackboard(nodeId: string): void {
|
||||
for (const observers of this.blackboardObservers.values()) {
|
||||
const index = observers.findIndex(o => o.nodeId === nodeId);
|
||||
const index = observers.findIndex((o) => o.nodeId === nodeId);
|
||||
if (index !== -1) {
|
||||
observers.splice(index, 1);
|
||||
}
|
||||
|
||||
@@ -49,11 +49,11 @@ export class NodeMetadataRegistry {
|
||||
}
|
||||
|
||||
static getByCategory(category: string): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter(m => m.category === category);
|
||||
return this.getAllMetadata().filter((m) => m.category === category);
|
||||
}
|
||||
|
||||
static getByNodeType(nodeType: NodeType): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter(m => m.nodeType === nodeType);
|
||||
return this.getAllMetadata().filter((m) => m.nodeType === nodeType);
|
||||
}
|
||||
|
||||
static getImplementationType(executorClass: Function): string | undefined {
|
||||
|
||||
@@ -131,7 +131,7 @@ export class BehaviorTreeAssetValidator {
|
||||
errors.push('Missing or invalid nodes array');
|
||||
} else {
|
||||
const nodeIds = new Set<string>();
|
||||
const rootNode = asset.nodes.find(n => n.id === asset.rootNodeId);
|
||||
const rootNode = asset.nodes.find((n) => n.id === asset.rootNodeId);
|
||||
|
||||
if (!rootNode) {
|
||||
errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`);
|
||||
@@ -157,7 +157,7 @@ export class BehaviorTreeAssetValidator {
|
||||
// 检查子节点引用
|
||||
if (node.children) {
|
||||
for (const childId of node.children) {
|
||||
if (!asset.nodes.find(n => n.id === childId)) {
|
||||
if (!asset.nodes.find((n) => n.id === childId)) {
|
||||
errors.push(`Node ${node.id} references non-existent child: ${childId}`);
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ export class BehaviorTreeAssetValidator {
|
||||
// 检查是否有孤立节点
|
||||
const referencedNodes = new Set<string>([asset.rootNodeId]);
|
||||
const collectReferencedNodes = (nodeId: string) => {
|
||||
const node = asset.nodes.find(n => n.id === nodeId);
|
||||
const node = asset.nodes.find((n) => n.id === nodeId);
|
||||
if (node && node.children) {
|
||||
for (const childId of node.children) {
|
||||
referencedNodes.add(childId);
|
||||
@@ -206,8 +206,8 @@ export class BehaviorTreeAssetValidator {
|
||||
|
||||
// 检查属性绑定
|
||||
if (asset.propertyBindings && Array.isArray(asset.propertyBindings)) {
|
||||
const nodeIds = new Set(asset.nodes.map(n => n.id));
|
||||
const varNames = new Set(asset.blackboard?.map(v => v.name) || []);
|
||||
const nodeIds = new Set(asset.nodes.map((n) => n.id));
|
||||
const varNames = new Set(asset.blackboard?.map((v) => v.name) || []);
|
||||
|
||||
for (const binding of asset.propertyBindings) {
|
||||
if (!nodeIds.has(binding.nodeId)) {
|
||||
@@ -276,7 +276,7 @@ export class BehaviorTreeAssetValidator {
|
||||
|
||||
// 计算最大深度
|
||||
const getDepth = (nodeId: string, currentDepth: number = 0): number => {
|
||||
const node = asset.nodes.find(n => n.id === nodeId);
|
||||
const node = asset.nodes.find((n) => n.id === nodeId);
|
||||
if (!node || !node.children || node.children.length === 0) {
|
||||
return currentDepth;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export class EditorFormatConverter {
|
||||
* 查找根节点
|
||||
*/
|
||||
private static findRootNode(nodes: EditorNode[]): EditorNode | null {
|
||||
return nodes.find(node =>
|
||||
return nodes.find((node) =>
|
||||
node.template.category === '根节点' ||
|
||||
node.data.nodeType === 'root'
|
||||
) || null;
|
||||
@@ -144,7 +144,7 @@ export class EditorFormatConverter {
|
||||
* 转换节点列表
|
||||
*/
|
||||
private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] {
|
||||
return editorNodes.map(node => this.convertNode(node));
|
||||
return editorNodes.map((node) => this.convertNode(node));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,13 +211,13 @@ export class EditorFormatConverter {
|
||||
blackboard: BlackboardVariableDefinition[]
|
||||
): PropertyBinding[] {
|
||||
const bindings: PropertyBinding[] = [];
|
||||
const blackboardVarNames = new Set(blackboard.map(v => v.name));
|
||||
const blackboardVarNames = new Set(blackboard.map((v) => v.name));
|
||||
|
||||
const propertyConnections = connections.filter(conn => conn.connectionType === 'property');
|
||||
const propertyConnections = connections.filter((conn) => conn.connectionType === 'property');
|
||||
|
||||
for (const conn of propertyConnections) {
|
||||
const fromNode = nodes.find(n => n.id === conn.from);
|
||||
const toNode = nodes.find(n => n.id === conn.to);
|
||||
const fromNode = nodes.find((n) => n.id === conn.from);
|
||||
const toNode = nodes.find((n) => n.id === conn.to);
|
||||
|
||||
if (!fromNode || !toNode || !conn.toProperty) {
|
||||
logger.warn(`跳过无效的属性连接: from=${conn.from}, to=${conn.to}`);
|
||||
|
||||
@@ -154,14 +154,14 @@ export class NodeTemplates {
|
||||
*/
|
||||
static getAllTemplates(): NodeTemplate[] {
|
||||
const allMetadata = NodeMetadataRegistry.getAllMetadata();
|
||||
return allMetadata.map(metadata => this.convertMetadataToTemplate(metadata));
|
||||
return allMetadata.map((metadata) => this.convertMetadataToTemplate(metadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型和子类型获取模板
|
||||
*/
|
||||
static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined {
|
||||
return this.getAllTemplates().find(t => {
|
||||
return this.getAllTemplates().find((t) => {
|
||||
if (t.type !== type) return false;
|
||||
const config: any = t.defaultConfig;
|
||||
|
||||
@@ -266,7 +266,7 @@ export class NodeTemplates {
|
||||
}
|
||||
|
||||
if (field.options) {
|
||||
property.options = field.options.map(opt => ({
|
||||
property.options = field.options.map((opt) => ({
|
||||
label: opt,
|
||||
value: opt
|
||||
}));
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
import { World, Scene, Entity } from '@esengine/ecs-framework';
|
||||
import { AssetLoadingManager } from '../src/Services/AssetLoadingManager';
|
||||
import {
|
||||
LoadingState,
|
||||
TimeoutError,
|
||||
CircularDependencyError,
|
||||
EntityDestroyedError
|
||||
} from '../src/Services/AssetLoadingTypes';
|
||||
|
||||
describe('AssetLoadingManager', () => {
|
||||
let manager: AssetLoadingManager;
|
||||
let world: World;
|
||||
let scene: Scene;
|
||||
let parentEntity: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new AssetLoadingManager();
|
||||
world = new World();
|
||||
scene = world.createScene('test');
|
||||
parentEntity = scene.createEntity('parent');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
manager.dispose();
|
||||
parentEntity.destroy();
|
||||
});
|
||||
|
||||
describe('基本加载功能', () => {
|
||||
test('成功加载资产', async () => {
|
||||
const mockEntity = scene.createEntity('loaded');
|
||||
|
||||
const loader = jest.fn().mockResolvedValue(mockEntity);
|
||||
|
||||
const handle = manager.startLoading(
|
||||
'test-asset',
|
||||
parentEntity,
|
||||
loader
|
||||
);
|
||||
|
||||
expect(handle.getState()).toBe(LoadingState.Loading);
|
||||
|
||||
const result = await handle.promise;
|
||||
|
||||
expect(result).toBe(mockEntity);
|
||||
expect(handle.getState()).toBe(LoadingState.Loaded);
|
||||
expect(loader).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('加载失败', async () => {
|
||||
const mockError = new Error('Load failed');
|
||||
const loader = jest.fn().mockRejectedValue(mockError);
|
||||
|
||||
const handle = manager.startLoading(
|
||||
'test-asset',
|
||||
parentEntity,
|
||||
loader,
|
||||
{ maxRetries: 0 }
|
||||
);
|
||||
|
||||
await expect(handle.promise).rejects.toThrow('Load failed');
|
||||
expect(handle.getState()).toBe(LoadingState.Failed);
|
||||
expect(handle.getError()).toBe(mockError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('超时机制', () => {
|
||||
test('加载超时', async () => {
|
||||
const loader = jest.fn().mockImplementation(() =>
|
||||
new Promise(resolve => setTimeout(resolve, 10000))
|
||||
);
|
||||
|
||||
const handle = manager.startLoading(
|
||||
'test-asset',
|
||||
parentEntity,
|
||||
loader,
|
||||
{ timeoutMs: 100, maxRetries: 0 }
|
||||
);
|
||||
|
||||
await expect(handle.promise).rejects.toThrow(TimeoutError);
|
||||
expect(handle.getState()).toBe(LoadingState.Timeout);
|
||||
});
|
||||
|
||||
test('超时前完成', async () => {
|
||||
const mockEntity = scene.createEntity('loaded');
|
||||
|
||||
const loader = jest.fn().mockImplementation(() =>
|
||||
new Promise(resolve => setTimeout(() => resolve(mockEntity), 50))
|
||||
);
|
||||
|
||||
const handle = manager.startLoading(
|
||||
'test-asset',
|
||||
parentEntity,
|
||||
loader,
|
||||
{ timeoutMs: 200 }
|
||||
);
|
||||
|
||||
const result = await handle.promise;
|
||||
|
||||
expect(result).toBe(mockEntity);
|
||||
expect(handle.getState()).toBe(LoadingState.Loaded);
|
||||
});
|
||||
});
|
||||
|
||||
describe('重试机制', () => {
|
||||
test('失败后自动重试', async () => {
|
||||
const mockEntity = scene.createEntity('loaded');
|
||||
let attemptCount = 0;
|
||||
|
||||
const loader = jest.fn().mockImplementation(() => {
|
||||
attemptCount++;
|
||||
if (attemptCount < 3) {
|
||||
return Promise.reject(new Error('Temporary error'));
|
||||
}
|
||||
return Promise.resolve(mockEntity);
|
||||
});
|
||||
|
||||
const handle = manager.startLoading(
|
||||
'test-asset',
|
||||
parentEntity,
|
||||
loader,
|
||||
{ maxRetries: 3 }
|
||||
);
|
||||
|
||||
const result = await handle.promise;
|
||||
|
||||
expect(result).toBe(mockEntity);
|
||||
expect(loader).toHaveBeenCalledTimes(3);
|
||||
expect(handle.getState()).toBe(LoadingState.Loaded);
|
||||
});
|
||||
|
||||
test('重试次数用尽后失败', async () => {
|
||||
const loader = jest.fn().mockRejectedValue(new Error('Persistent error'));
|
||||
|
||||
const handle = manager.startLoading(
|
||||
'test-asset',
|
||||
parentEntity,
|
||||
loader,
|
||||
{ maxRetries: 2 }
|
||||
);
|
||||
|
||||
await expect(handle.promise).rejects.toThrow('Persistent error');
|
||||
expect(loader).toHaveBeenCalledTimes(3); // 初始 + 2次重试
|
||||
expect(handle.getState()).toBe(LoadingState.Failed);
|
||||
});
|
||||
});
|
||||
|
||||
describe('循环引用检测', () => {
|
||||
test('检测直接循环引用', () => {
|
||||
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
||||
|
||||
// 先加载 assetA
|
||||
const handleA = manager.startLoading(
|
||||
'assetA',
|
||||
parentEntity,
|
||||
loader,
|
||||
{ parentAssetId: undefined }
|
||||
);
|
||||
|
||||
expect(handleA.getState()).toBe(LoadingState.Loading);
|
||||
|
||||
// 尝试在 assetA 的上下文中加载 assetB
|
||||
// assetB 又尝试加载 assetA(循环)
|
||||
expect(() => {
|
||||
manager.startLoading(
|
||||
'assetB',
|
||||
parentEntity,
|
||||
loader,
|
||||
{ parentAssetId: 'assetB' } // assetB 的父是 assetB(自我循环)
|
||||
);
|
||||
}).toThrow(CircularDependencyError);
|
||||
});
|
||||
|
||||
test('不误报非循环引用', () => {
|
||||
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
||||
|
||||
// assetA 加载 assetB(正常)
|
||||
const handleA = manager.startLoading(
|
||||
'assetA',
|
||||
parentEntity,
|
||||
loader
|
||||
);
|
||||
|
||||
// assetB 加载 assetC(正常,不是循环)
|
||||
expect(() => {
|
||||
manager.startLoading(
|
||||
'assetC',
|
||||
parentEntity,
|
||||
loader,
|
||||
{ parentAssetId: 'assetB' }
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('实体生命周期安全', () => {
|
||||
test('实体销毁后取消加载', async () => {
|
||||
const loader = jest.fn().mockImplementation(() =>
|
||||
new Promise(resolve => setTimeout(resolve, 100))
|
||||
);
|
||||
|
||||
const handle = manager.startLoading(
|
||||
'test-asset',
|
||||
parentEntity,
|
||||
loader
|
||||
);
|
||||
|
||||
// 销毁实体
|
||||
parentEntity.destroy();
|
||||
|
||||
// 等待一小段时间让检测生效
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
await expect(handle.promise).rejects.toThrow(EntityDestroyedError);
|
||||
expect(handle.getState()).toBe(LoadingState.Cancelled);
|
||||
});
|
||||
});
|
||||
|
||||
describe('状态查询', () => {
|
||||
test('获取加载进度', async () => {
|
||||
const mockEntity = scene.createEntity('loaded');
|
||||
|
||||
const loader = jest.fn().mockImplementation(() =>
|
||||
new Promise(resolve => setTimeout(() => resolve(mockEntity), 100))
|
||||
);
|
||||
|
||||
const handle = manager.startLoading(
|
||||
'test-asset',
|
||||
parentEntity,
|
||||
loader
|
||||
);
|
||||
|
||||
const progress = handle.getProgress();
|
||||
|
||||
expect(progress.state).toBe(LoadingState.Loading);
|
||||
expect(progress.elapsedMs).toBeGreaterThanOrEqual(0);
|
||||
expect(progress.retryCount).toBe(0);
|
||||
expect(progress.maxRetries).toBe(3);
|
||||
|
||||
await handle.promise;
|
||||
});
|
||||
|
||||
test('获取统计信息', () => {
|
||||
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
||||
|
||||
manager.startLoading('asset1', parentEntity, loader);
|
||||
manager.startLoading('asset2', parentEntity, loader);
|
||||
|
||||
const stats = manager.getStats();
|
||||
|
||||
expect(stats.totalTasks).toBe(2);
|
||||
expect(stats.loadingTasks).toBe(2);
|
||||
});
|
||||
|
||||
test('获取正在加载的资产列表', () => {
|
||||
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
||||
|
||||
manager.startLoading('asset1', parentEntity, loader);
|
||||
manager.startLoading('asset2', parentEntity, loader);
|
||||
|
||||
const loadingAssets = manager.getLoadingAssets();
|
||||
|
||||
expect(loadingAssets).toContain('asset1');
|
||||
expect(loadingAssets).toContain('asset2');
|
||||
expect(loadingAssets.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('任务管理', () => {
|
||||
test('取消加载任务', () => {
|
||||
const loader = jest.fn().mockImplementation(() =>
|
||||
new Promise(resolve => setTimeout(resolve, 1000))
|
||||
);
|
||||
|
||||
const handle = manager.startLoading(
|
||||
'test-asset',
|
||||
parentEntity,
|
||||
loader
|
||||
);
|
||||
|
||||
expect(handle.getState()).toBe(LoadingState.Loading);
|
||||
|
||||
handle.cancel();
|
||||
|
||||
expect(handle.getState()).toBe(LoadingState.Cancelled);
|
||||
});
|
||||
|
||||
test('清空所有任务', async () => {
|
||||
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
||||
|
||||
manager.startLoading('asset1', parentEntity, loader);
|
||||
manager.startLoading('asset2', parentEntity, loader);
|
||||
|
||||
expect(manager.getLoadingAssets().length).toBe(2);
|
||||
|
||||
manager.clear();
|
||||
|
||||
expect(manager.getLoadingAssets().length).toBe(0);
|
||||
});
|
||||
|
||||
test('复用已存在的加载任务', () => {
|
||||
const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded'));
|
||||
|
||||
const handle1 = manager.startLoading('test-asset', parentEntity, loader);
|
||||
const handle2 = manager.startLoading('test-asset', parentEntity, loader);
|
||||
|
||||
// 应该返回同一个任务
|
||||
expect(handle1.assetId).toBe(handle2.assetId);
|
||||
expect(loader).toHaveBeenCalledTimes(1); // 只加载一次
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user