From 2cb9bba3ce14364635149b2cf035e8678a5bdb76 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 14 Sep 2026 16:56:17 +0800 Subject: [PATCH] test(06-08): cover FlagSystem, FaceManager handlers and RoleFaceBinder (stage 2) - add system.test.ts covering the full FlagSystem public surface and warn code 111 - add faceManager.test.ts covering FaceManager registry plus Dir4/Dir8 handlers - add face.test.ts covering RoleFaceBinder malloc/bind/query and error codes 43/44 --- .../data-base/src/flag/system.test.ts | 171 +++++++++++++++++ .../data-common/src/common/face.test.ts | 122 ++++++++++++ .../src/common/faceManager.test.ts | 178 ++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 packages-user/data-base/src/flag/system.test.ts create mode 100644 packages-user/data-common/src/common/face.test.ts create mode 100644 packages-user/data-common/src/common/faceManager.test.ts diff --git a/packages-user/data-base/src/flag/system.test.ts b/packages-user/data-base/src/flag/system.test.ts new file mode 100644 index 0000000..225ca86 --- /dev/null +++ b/packages-user/data-base/src/flag/system.test.ts @@ -0,0 +1,171 @@ +// 测试 data-base 全局 Flag 系统的公开接口行为(存读档归 06-09,此处不测) +import { beforeAll, describe, expect, it, vi } from 'vitest'; + +vi.hoisted(() => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + Map.prototype.getOrInsertComputed ??= function ( + this: Map, + key: K, + callback: (key: K) => V + ): V { + const existing = this.get(key); + if (existing !== undefined) return existing; + const value = callback(key); + this.set(key, value); + return value; + }; + Map.prototype.getOrInsert ??= function ( + this: Map, + key: K, + defaultValue: V + ): V { + const existing = this.get(key); + if (existing !== undefined) return existing; + this.set(key, defaultValue); + return defaultValue; + }; +}); + +interface TestModules { + FlagSystem: typeof import('./system').FlagSystem; + logger: typeof import('@motajs/common').logger; +} + +let modules: TestModules; + +beforeAll(async () => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + const systemModule = await import('./system'); + const commonModule = await import('@motajs/common'); + modules = { + FlagSystem: systemModule.FlagSystem, + logger: commonModule.logger + }; +}); + +/** 创建一个 Flag 系统及其中一个已插入字段,便于直接操作字段对象 */ +function createField(key: PropertyKey, value: T) { + const system = new modules.FlagSystem(); + return { system, field: system.insertField(key, value) }; +} + +describe('FlagSystem field container', () => { + // 验证新系统的字段初始未占用,插入字段后被占用 + it('tracks field occupancy', () => { + const system = new modules.FlagSystem(); + expect(system.occupied('score')).toBe(false); + + system.insertField('score', 1); + + expect(system.occupied('score')).toBe(true); + }); + + // 验证 insertField 返回的字段对象支持取值、赋值与结构化克隆 + it('returns a field exposing get, set and toStructured', () => { + const { field } = createField('nested', { list: [1, 2] }); + + expect(field.get()).toEqual({ list: [1, 2] }); + + field.set({ list: [3] }); + + expect(field.get()).toEqual({ list: [3] }); + const structured = field.toStructured(); + expect(structured).toEqual({ list: [3] }); + expect(structured).not.toBe(field.get()); + }); + + // 验证 getField 对未知字段返回 null,getOrInsert 插入默认值且二次调用返回同一实例 + it('gets by key and reuses the same field instance', () => { + const system = new modules.FlagSystem(); + expect(system.getField('missing')).toBeNull(); + + const first = system.getOrInsert('count', 5); + const second = system.getOrInsert('count', 99); + + expect(first).toBe(second); + expect(first.get()).toBe(5); + expect(system.getField('count')).toBe(first); + }); + + // 验证 getOrInsertComputed 把 key 传给默认值函数且仅在字段缺失时执行一次 + it('computes the default only when the field is missing', () => { + const system = new modules.FlagSystem(); + let calls = 0; + const compute = (key: string) => { + calls++; + return `value:${key}`; + }; + + const field = system.getOrInsertComputed('name', compute); + + expect(field.get()).toBe('value:name'); + expect(system.getOrInsertComputed('name', compute)).toBe(field); + expect(calls).toBe(1); + }); + + // 验证 deleteField 移除字段并恢复未占用状态 + it('deletes a field', () => { + const system = new modules.FlagSystem(); + system.insertField('temp', 1); + + system.deleteField('temp'); + + expect(system.occupied('temp')).toBe(false); + expect(system.getField('temp')).toBeNull(); + expect(system.getFieldValue('temp')).toBeUndefined(); + }); +}); + +describe('FlagSystem value accessors', () => { + // 验证 setFieldValue 覆盖已有值并创建缺失字段 + it('sets values creating or overriding fields', () => { + const system = new modules.FlagSystem(); + system.setFieldValue('score', 1); + system.setFieldValue('score', 9); + + expect(system.getFieldValue('score')).toBe(9); + expect(system.occupied('score')).toBe(true); + }); + + // 验证 addFieldValue 对数值字段累加 + it('accumulates numeric field values', () => { + const system = new modules.FlagSystem(); + system.addFieldValue('score', 4); + system.addFieldValue('score', 6); + + expect(system.getFieldValue('score')).toBe(10); + }); + + // 验证对非数值字段调用 addFieldValue 时经 logger 观测告警码 111 + it('warns code 111 when adding to a non-numeric field', () => { + const system = new modules.FlagSystem(); + system.setFieldValue('name', 'hero'); + + const result = modules.logger.catch(() => + system.addFieldValue('name', 1) + ); + + expect(result.info.map(info => info.code)).toContain(111); + expect(system.getFieldValue('name')).toBe('hero'); + }); + + // 验证 getFieldValue 对未知字段返回 undefined + it('returns undefined for an unknown value', () => { + const system = new modules.FlagSystem(); + + expect(system.getFieldValue('missing')).toBeUndefined(); + }); + + // 验证 getFieldValueDefaults 返回默认值并以该值插入字段 + it('returns and inserts the default value', () => { + const system = new modules.FlagSystem(); + + const value = system.getFieldValueDefaults('lives', 3); + + expect(value).toBe(3); + expect(system.occupied('lives')).toBe(true); + expect(system.getFieldValue('lives')).toBe(3); + }); +}); diff --git a/packages-user/data-common/src/common/face.test.ts b/packages-user/data-common/src/common/face.test.ts new file mode 100644 index 0000000..44184c1 --- /dev/null +++ b/packages-user/data-common/src/common/face.test.ts @@ -0,0 +1,122 @@ +// 测试 L0 RoleFaceBinder 的朝向分配、绑定与查询(含码 43/44 告警) +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { FaceDirection } from './types'; + +vi.hoisted(() => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); +}); + +interface TestModules { + RoleFaceBinder: typeof import('./face').RoleFaceBinder; + logger: typeof import('@motajs/common').logger; +} + +let modules: TestModules; + +beforeAll(async () => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + const faceModule = await import('./face'); + const commonModule = await import('@motajs/common'); + modules = { + RoleFaceBinder: faceModule.RoleFaceBinder, + logger: commonModule.logger + }; +}); + +/** 创建一个朝向绑定器实例 */ +function createBinder() { + return new modules.RoleFaceBinder(); +} + +describe('RoleFaceBinder malloc and bind', () => { + // 验证 malloc 建立主朝向后可查询主朝向及该朝向的图块 + it('registers the main face of a block', () => { + const binder = createBinder(); + binder.malloc(1, FaceDirection.Down); + + expect(binder.getMainFace(1)).toEqual({ + identifier: 1, + face: FaceDirection.Down + }); + expect(binder.getFaceDirection(1)).toBe(FaceDirection.Down); + expect(binder.getFaceOf(1, FaceDirection.Down)).toEqual({ + identifier: 1, + face: FaceDirection.Down + }); + }); + + // 验证 bind 将另一朝向图块绑定到主图块并共享朝向映射 + it('binds a face block to its main block', () => { + const binder = createBinder(); + binder.malloc(1, FaceDirection.Down); + binder.bind(2, 1, FaceDirection.Up); + + expect(binder.getMainFace(2)).toEqual({ + identifier: 2, + face: FaceDirection.Down + }); + expect(binder.getFaceDirection(2)).toBe(FaceDirection.Up); + expect(binder.getFaceOf(1, FaceDirection.Up)).toEqual({ + identifier: 2, + face: FaceDirection.Up + }); + expect(binder.getFaceOf(2, FaceDirection.Down)).toEqual({ + identifier: 1, + face: FaceDirection.Down + }); + }); + + // 验证绑定未知主图块时经 logger 观测错误码 43 且不建立绑定 + it('warns code 43 for an unknown main block', () => { + const binder = createBinder(); + + const result = modules.logger.catch(() => + binder.bind(2, 99, FaceDirection.Up) + ); + + expect(result.info.map(info => info.code)).toContain(43); + expect(binder.getMainFace(2)).toBeNull(); + expect(binder.getFaceDirection(2)).toBeUndefined(); + }); + + // 验证绑定与主朝向相同朝向时经 logger 观测错误码 44 且不建立绑定 + it('warns code 44 when binding the main direction', () => { + const binder = createBinder(); + binder.malloc(1, FaceDirection.Down); + + const result = modules.logger.catch(() => + binder.bind(2, 1, FaceDirection.Down) + ); + + expect(result.info.map(info => info.code)).toContain(44); + expect(binder.getMainFace(2)).toBeNull(); + expect(binder.getFaceDirection(2)).toBeUndefined(); + }); +}); + +describe('RoleFaceBinder queries', () => { + // 验证 getFaceOf 对未绑定朝向与未知图块均返回 null + it('returns null for an unbound face or an unknown block', () => { + const binder = createBinder(); + binder.malloc(1, FaceDirection.Down); + + expect(binder.getFaceOf(1, FaceDirection.Up)).toBeNull(); + expect(binder.getFaceOf(99, FaceDirection.Down)).toBeNull(); + }); + + // 验证 getFaceDirection 对未注册图块返回 undefined + it('returns undefined direction for an unknown block', () => { + const binder = createBinder(); + + expect(binder.getFaceDirection(42)).toBeUndefined(); + }); + + // 验证 getMainFace 对未注册图块返回 null + it('returns null main face for an unknown block', () => { + const binder = createBinder(); + + expect(binder.getMainFace(42)).toBeNull(); + }); +}); diff --git a/packages-user/data-common/src/common/faceManager.test.ts b/packages-user/data-common/src/common/faceManager.test.ts new file mode 100644 index 0000000..063f98e --- /dev/null +++ b/packages-user/data-common/src/common/faceManager.test.ts @@ -0,0 +1,178 @@ +// 测试 L0 FaceManager 注册表与内建 4/8 方向处理器的方向语义 +import { describe, expect, it } from 'vitest'; +import { FaceDirection } from './types'; +import { Dir4FaceHandler, Dir8FaceHandler, FaceManager } from './faceManager'; + +/** 创建内建的 4 方向与 8 方向处理器 */ +function createHandler() { + return { + dir4: new Dir4FaceHandler(), + dir8: new Dir8FaceHandler() + }; +} + +describe('FaceManager registry', () => { + // 验证按数字 key 与字符串 id 注册后都能查回同一处理器 + it('returns the registered handler by group and id', () => { + const manager = new FaceManager(); + const { dir4, dir8 } = createHandler(); + manager.register(1, dir8); + manager.registerById('four', dir4); + + expect(manager.get(1)).toBe(dir8); + expect(manager.getById('four')).toBe(dir4); + }); + + // 验证查询未注册的 key 或 id 时返回 null + it('returns null for an unknown group and id', () => { + const manager = new FaceManager(); + + expect(manager.get(9)).toBeNull(); + expect(manager.getById('missing')).toBeNull(); + }); +}); + +describe('Dir8FaceHandler', () => { + // 验证 degrade 对任意输入原样透传 + it('passes degrade through unchanged', () => { + const { dir8 } = createHandler(); + + expect(dir8.degrade(FaceDirection.LeftUp)).toBe(FaceDirection.LeftUp); + expect(dir8.degrade(123)).toBe(123); + }); + + // 验证 movement 对全部八个方向及未知返回单步偏移量 + it('returns the single-step offset for every direction', () => { + const { dir8 } = createHandler(); + + expect(dir8.movement(FaceDirection.Left)).toEqual({ x: -1, y: 0 }); + expect(dir8.movement(FaceDirection.Up)).toEqual({ x: 0, y: -1 }); + expect(dir8.movement(FaceDirection.Right)).toEqual({ x: 1, y: 0 }); + expect(dir8.movement(FaceDirection.Down)).toEqual({ x: 0, y: 1 }); + expect(dir8.movement(FaceDirection.LeftUp)).toEqual({ x: -1, y: -1 }); + expect(dir8.movement(FaceDirection.RightUp)).toEqual({ x: 1, y: -1 }); + expect(dir8.movement(FaceDirection.LeftDown)).toEqual({ x: -1, y: 1 }); + expect(dir8.movement(FaceDirection.RightDown)).toEqual({ x: 1, y: 1 }); + expect(dir8.movement(FaceDirection.Unknown)).toEqual({ x: 0, y: 0 }); + }); + + // 验证越界方向输入返回零偏移量 + it('returns the zero offset for an out-of-range direction', () => { + const { dir8 } = createHandler(); + + expect(dir8.movement(999)).toEqual({ x: 0, y: 0 }); + }); + + // 验证 move 按步数缩放偏移量,负步数得到反向位移 + it('scales the offset by the step count including negatives', () => { + const { dir8 } = createHandler(); + + expect(dir8.move(FaceDirection.Right, 3)).toEqual({ x: 3, y: 0 }); + expect(dir8.move(FaceDirection.RightUp, 2)).toEqual({ x: 2, y: -2 }); + expect(dir8.move(FaceDirection.RightUp, -2)).toEqual({ x: -2, y: 2 }); + }); + + // 验证 opposite 返回反方向,未知朝向返回未知 + it('returns the opposite direction including unknown', () => { + const { dir8 } = createHandler(); + + expect(dir8.opposite(FaceDirection.Up)).toBe(FaceDirection.Down); + expect(dir8.opposite(FaceDirection.Left)).toBe(FaceDirection.Right); + expect(dir8.opposite(FaceDirection.LeftUp)).toBe( + FaceDirection.RightDown + ); + expect(dir8.opposite(FaceDirection.RightUp)).toBe( + FaceDirection.LeftDown + ); + expect(dir8.opposite(FaceDirection.Unknown)).toBe( + FaceDirection.Unknown + ); + }); + + // 验证 next 默认顺时针、传入参数时逆时针,未知朝向透传 + it('rotates clockwise by default and anticlockwise on request', () => { + const { dir8 } = createHandler(); + + expect(dir8.next(FaceDirection.Up)).toBe(FaceDirection.RightUp); + expect(dir8.next(FaceDirection.LeftUp)).toBe(FaceDirection.Up); + expect(dir8.next(FaceDirection.Up, true)).toBe(FaceDirection.LeftUp); + expect(dir8.next(FaceDirection.LeftUp, true)).toBe(FaceDirection.Left); + expect(dir8.next(FaceDirection.Unknown)).toBe(FaceDirection.Unknown); + expect(dir8.next(FaceDirection.Unknown, true)).toBe( + FaceDirection.Unknown + ); + }); + + // 验证 mapDirection 与 mapMovement 迭代全部九个方向且未知为零偏移 + it('maps every direction together with its movement', () => { + const { dir8 } = createHandler(); + + expect([...dir8.mapDirection()]).toEqual([ + FaceDirection.Unknown, + FaceDirection.Left, + FaceDirection.Up, + FaceDirection.Right, + FaceDirection.Down, + FaceDirection.LeftUp, + FaceDirection.RightUp, + FaceDirection.LeftDown, + FaceDirection.RightDown + ]); + + const movements = [...dir8.mapMovement()]; + expect(movements).toHaveLength(9); + expect(movements[0]).toEqual([FaceDirection.Unknown, { x: 0, y: 0 }]); + expect(movements[8]).toEqual([FaceDirection.RightDown, { x: 1, y: 1 }]); + }); +}); + +describe('Dir4FaceHandler', () => { + // 验证 degrade 把对角降级为对应正交方向,其余非法输入降级为未知 + it('degrades diagonals into orthogonal and the rest into unknown', () => { + const { dir4 } = createHandler(); + + expect(dir4.degrade(FaceDirection.LeftUp)).toBe(FaceDirection.Left); + expect(dir4.degrade(FaceDirection.LeftDown)).toBe(FaceDirection.Left); + expect(dir4.degrade(FaceDirection.RightUp)).toBe(FaceDirection.Right); + expect(dir4.degrade(FaceDirection.RightDown)).toBe(FaceDirection.Right); + expect(dir4.degrade(FaceDirection.Left)).toBe(FaceDirection.Left); + expect(dir4.degrade(FaceDirection.Up)).toBe(FaceDirection.Up); + expect(dir4.degrade(FaceDirection.Unknown)).toBe(FaceDirection.Unknown); + expect(dir4.degrade(999)).toBe(FaceDirection.Unknown); + }); + + // 验证 movement、move、opposite 与 next 在四方向集合内正确工作 + it('computes movement, opposite and rotation on the four-way set', () => { + const { dir4 } = createHandler(); + + expect(dir4.movement(FaceDirection.Left)).toEqual({ x: -1, y: 0 }); + expect(dir4.movement(FaceDirection.RightUp)).toEqual({ x: 1, y: 0 }); + expect(dir4.movement(999)).toEqual({ x: 0, y: 0 }); + expect(dir4.move(FaceDirection.Down, 2)).toEqual({ x: 0, y: 2 }); + expect(dir4.move(FaceDirection.Right, 3)).toEqual({ x: 3, y: 0 }); + expect(dir4.opposite(FaceDirection.Up)).toBe(FaceDirection.Down); + expect(dir4.opposite(FaceDirection.Unknown)).toBe( + FaceDirection.Unknown + ); + expect(dir4.next(FaceDirection.Up)).toBe(FaceDirection.Right); + expect(dir4.next(FaceDirection.Up, true)).toBe(FaceDirection.Left); + expect(dir4.next(FaceDirection.Unknown)).toBe(FaceDirection.Unknown); + }); + + // 验证 mapDirection 与 mapMovement 只迭代四个正交方向与未知 + it('maps only the four-way set plus unknown', () => { + const { dir4 } = createHandler(); + + expect([...dir4.mapDirection()]).toEqual([ + FaceDirection.Unknown, + FaceDirection.Left, + FaceDirection.Up, + FaceDirection.Right, + FaceDirection.Down + ]); + + const movements = [...dir4.mapMovement()]; + expect(movements).toHaveLength(5); + expect(movements[0]).toEqual([FaceDirection.Unknown, { x: 0, y: 0 }]); + }); +});