diff --git a/packages-user/data-state/src/event/__test__/event.test.ts b/packages-user/data-state/src/event/__test__/event.test.ts index f12336b..40b7009 100644 --- a/packages-user/data-state/src/event/__test__/event.test.ts +++ b/packages-user/data-state/src/event/__test__/event.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import { readFileSync } from 'node:fs'; import { WebLoadStarter } from '@motajs/loader'; +import { logger } from '@motajs/common'; import { Statement, StatementType } from '@motajs/anon-tokyo'; import { FaceDirection, @@ -155,6 +156,20 @@ describe('event built-ins', () => { expect(fixture.layer.getBlock(1, 0)).toBe(2); }); + // 验证 setBlock 遇到未注册图块时告警码 1001 并跳过 + it('warns code 1001 when setBlock receives an unknown tile', async () => { + const fixture = createFixture(); + const warn = vi.spyOn(logger, 'warn'); + + await invokeBuiltin( + getRegistration('setBlock'), + { x: 1, y: 0, tile: 'missing' }, + fixture.env + ); + + expect(warn).toHaveBeenCalledWith(1001); + }); + // 验证真实注册项移动动态图块并按 safe 分支还原 it('moves a dynamic block and respects safe static transfer', async () => { const fixture = createFixture(); diff --git a/packages-user/data-state/src/loader/__test__/loader.test.ts b/packages-user/data-state/src/loader/__test__/loader.test.ts new file mode 100644 index 0000000..de31b7f --- /dev/null +++ b/packages-user/data-state/src/loader/__test__/loader.test.ts @@ -0,0 +1,176 @@ +// 测试 data-state loader:PrefixedJSONCProcessor 解析、DefaultDataLoaderHook 核心配置守卫与 MotaDataLoader 公开面 +import { describe, expect, it, vi } from 'vitest'; +import { ILoadTask, LoadDataType } from '@motajs/loader'; +import { logger } from '@motajs/common'; +import { PrefixedJSONCProcessor } from '../jsoncProcessor'; +import { DefaultDataLoaderHook } from '../hook'; +import { MotaDataLoader } from '../loader'; + +/** 与生产 loader 一致的 jsonc 前缀分隔串 */ +const PREFIX = '// --- SYSTEM PREFIX END --- //'; + +/** 构造一个仅暴露 url 的最小加载任务桩 */ +function createTaskStub(url: string): ILoadTask { + return { url } as never; +} + +/** 构造一个按任务 url 返回带前缀 JSON 的加载启动器桩 */ +function createStarter() { + return { + async start(request: { url: string }) { + const identifier = request.url.split('/').pop(); + return { + url: request.url, + body: null, + headers: { get: () => null }, + arrayBuffer: async () => new ArrayBuffer(0), + blob: async () => new Blob(), + bytes: async () => new Uint8Array(), + json: async () => ({}), + text: async () => `${PREFIX}${JSON.stringify({ identifier })}` + }; + } + }; +} + +/** 构造一个按需迭代任务 start() 的加载管理器桩 */ +function createManager() { + const tasks: ILoadTask[] = []; + return { + addTask(task: ILoadTask): void { + tasks.push(task); + }, + async *load(): AsyncIterable { + for (const task of [...tasks]) { + for await (const loaded of task.start()) { + yield loaded; + } + } + } + }; +} + +describe('PrefixedJSONCProcessor', () => { + // 验证携带合法前缀的响应会被解析为对象 + it('parses the payload after the prefix split', async () => { + const processor = new PrefixedJSONCProcessor<{ a: number }>(PREFIX); + + await expect( + processor.process(`${PREFIX}{"a":1}`, createTaskStub('x')) + ).resolves.toEqual({ a: 1 }); + }); + + // 验证响应缺少前缀分隔串时告警码 66 并返回空对象 + it('warns code 66 when the prefix split string is missing', () => { + const processor = new PrefixedJSONCProcessor(PREFIX); + const result = logger.catch(() => + processor.process('no prefix at all', createTaskStub('core.jsonc')) + ); + + expect(result.info.map(info => info.code)).toContain(66); + }); + + // 验证前缀后的 JSON 无法解析时告警码 67 并返回空对象 + it('warns code 67 when the split payload is not valid json', () => { + const processor = new PrefixedJSONCProcessor(PREFIX); + const result = logger.catch(() => + processor.process( + `${PREFIX}{not json`, + createTaskStub('core.jsonc') + ) + ); + + expect(result.info.map(info => info.code)).toContain(67); + }); +}); + +describe('DefaultDataLoaderHook', () => { + // 验证核心配置缺失时告警码 69 且不追加任何额外配置 + it('warns code 69 when the core config is missing', async () => { + const hook = new DefaultDataLoaderHook(); + const loader = { getConfig: () => null } as never; + const result = logger.catch(() => hook.onCoreConfigLoaded(loader)); + + await expect(result.ret).resolves.toBeUndefined(); + expect(result.info.map(info => info.code)).toContain(69); + }); + + // 验证核心配置存在时按目录与默认值追加全部额外配置 + it('adds every extra config declared by the core config', async () => { + const hook = new DefaultDataLoaderHook(); + const addExtraConfig = vi.fn(); + const coreConfig = { + content: { + enemyDir: 'enemy', + itemDir: 'item', + mapListDir: 'map-list', + tileDir: 'tile', + defaults: { + enemyDefault: 'enemy-default', + itemDefault: 'item-default', + mapDefault: 'map-default', + tileDefault: 'tile-default' + } + } + }; + const loader = { + getConfig: (id: string) => (id === 'core' ? coreConfig : null), + addExtraConfig + } as never; + + await hook.onCoreConfigLoaded(loader); + + expect(addExtraConfig).toHaveBeenCalledWith('enemy', 'enemy'); + expect(addExtraConfig).toHaveBeenCalledWith('item', 'item'); + expect(addExtraConfig).toHaveBeenCalledWith('map-list', 'map-list'); + expect(addExtraConfig).toHaveBeenCalledWith('tile', 'tile'); + expect(addExtraConfig).toHaveBeenCalledWith( + 'enemy-default', + 'enemy-default' + ); + expect(addExtraConfig).toHaveBeenCalledWith( + 'item-default', + 'item-default' + ); + expect(addExtraConfig).toHaveBeenCalledWith( + 'map-default', + 'map-default' + ); + expect(addExtraConfig).toHaveBeenCalledWith( + 'tile-default', + 'tile-default' + ); + }); +}); + +describe('MotaDataLoader', () => { + // 验证加载器依次加载核心与额外配置、兑现 loaded 并暴露已加载配置 + it('loads core and extra configs, exposes them and resolves loaded', async () => { + const loader = new MotaDataLoader( + createManager() as never, + createStarter() as never + ); + const coreHook = vi.fn(async () => {}); + const extraHook = vi.fn(async () => {}); + loader.addHook({ + onCoreConfigLoaded: coreHook, + onExtraConfigLoaded: extraHook + }); + loader.addCoreConfig('core', 'core.jsonc'); + loader.addExtraConfig('enemy', 'enemy.jsonc'); + loader.addExtraConfig('item', 'item.jsonc'); + + for await (const loaded of loader.start()) { + expect(typeof loaded).toBe('number'); + } + await loader.loaded(); + + expect(coreHook).toHaveBeenCalledWith(loader); + expect(extraHook).toHaveBeenCalledWith(loader); + expect(loader.getConfig('enemy')).toEqual({ + identifier: 'enemy.jsonc' + }); + expect(loader.getConfig('item')).toEqual({ identifier: 'item.jsonc' }); + expect(loader.getConfig('missing')).toBeNull(); + }); +}); diff --git a/packages-user/data-system/src/event/__test__/eventDispatch.test.ts b/packages-user/data-system/src/event/__test__/eventDispatch.test.ts index abd5ae4..ef999a8 100644 --- a/packages-user/data-system/src/event/__test__/eventDispatch.test.ts +++ b/packages-user/data-system/src/event/__test__/eventDispatch.test.ts @@ -16,6 +16,7 @@ import { type IGameEventInvocation } from '../types'; import { AnonTokyoInterpreter } from '@motajs/anon-tokyo'; +import { logger } from '@motajs/common'; import { IStateBase } from '@user/data-base'; vi.hoisted(() => { @@ -524,6 +525,37 @@ describe('event execute modes and reductions', () => { ).resolves.toEqual([true]); expect(calls).toEqual(['valid']); }); + + // 验证事件 id 不在事件存储时告警码 171 并跳过该事件 + it('warns code 171 for an event id missing from the store', async () => { + const fixture = createFixture(); + const warn = vi.spyOn(logger, 'warn'); + + await fixture.executor.execute( + [invocation('missing', modules.EventTrigger.OnEnter)], + { custom: {} } + ); + + expect(warn).toHaveBeenCalledWith(171, 'missing'); + }); + + // 验证折叠时事件返回非布尔值告警码 172 并按短路语义继续 + it('warns code 172 for a non-boolean event result during reduction', async () => { + const fixture = createFixture(); + const warn = vi.spyOn(logger, 'warn'); + fixture.events.set('non-bool', { + trigger: modules.EventTrigger.OnEnter, + execute: async () => 5 + }); + fixture.executor.setReduce(modules.EventReduceMode.OrReduce); + + await fixture.executor.execute( + [invocation('non-bool', modules.EventTrigger.OnEnter)], + { custom: {} } + ); + + expect(warn).toHaveBeenCalledWith(172, '5'); + }); }); describe('enter leave hit trigger hooks', () => { diff --git a/packages-user/data-system/src/path/__test__/system.test.ts b/packages-user/data-system/src/path/__test__/system.test.ts index 9b066a2..3de58f6 100644 --- a/packages-user/data-system/src/path/__test__/system.test.ts +++ b/packages-user/data-system/src/path/__test__/system.test.ts @@ -610,4 +610,16 @@ describe('pathfinding system', () => { await result!.controller.onEnd; expect(result!.controller.done).toBe(true); }); + + // 验证向求解器绑定属于其他 CoreState 的图层时告警码 182 并不建立绑定 + it('warns code 182 when binding a layer from another CoreState', () => { + const a = createSystem([1, 1, 1], 3); + const b = createSystem([1, 1, 1], 3); + + const result = modules.logger.catch(() => + a.system.finder.useMapLayer(b.layer) + ); + + expect(result.info.map(info => info.code)).toContain(182); + }); });