import { describe, expect, it } from 'vitest';
import { Camera } from '../../render/Camera';
import { EventBus } from '../../core/EventBus';
import { EngineAgent } from '../EngineAgent';
import type { AgentHost, SceneAgent, SnapshotLayer } from '../types';
/**
* Фейковый хост: без Pixi/канваса. stepTick двигает «время» и вызывает
* sceneAgent; renderFrame только помечается.
*/
function fakeHost(): AgentHost & { renders: number } {
let ticks = 0;
const events = new EventBus();
return {
events,
scenes: { current: undefined, transitioning: false } as never,
input: {
getPointer: () => ({ x: 0, y: 0, down: false, justPressed: false, downTicks: 0 })
} as never,
camera: new Camera(480, 270),
virtualWidth: 480,
virtualHeight: 270,
fixedStep: 1 / 60,
get tickCount() {
return ticks;
},
stepTick() {
return ++ticks;
},
stepTicks(n = 1) {
for (let i = 0; i < n; i++) this.stepTick();
return ticks;
},
renderFrame() {
this.renders++;
},
renders: 0
};
}
function fakeScene(): SceneAgent {
return {
agentSnapshot: () => ({ area: 'test', hp: 5 }),
agentInvariants: () => [{ id: 'warn-x', severity: 'warn', message: 'подозрительно' }],
agentCommand: (name) => (name === 'ping' ? 'pong' : null)
};
}
describe('EngineAgent — снапшот и шаги', () => {
it('снапшот содержит движковый слой + слой сцены', () => {
const host = fakeHost();
const agent = new EngineAgent(host, () => fakeScene());
const s = agent.snapshot() as SnapshotLayer;
expect(s.tick).toBe(0);
expect(s.fixedStep).toBeCloseTo(1 / 60, 6);
expect(s.transitioning).toBe(false);
expect(s.camera).toBeTruthy();
expect(s.pointer).toBeTruthy();
expect(s.area).toBe('test');
expect(s.hp).toBe(5);
});
it('без сцены снапшот всё равно валиден', () => {
const agent = new EngineAgent(fakeHost(), () => null);
const s = agent.snapshot();
expect(s.tick).toBe(0);
expect(s.area).toBeUndefined();
});
});
describe('EngineAgent — инварианты', () => {
it('мержит движковые и сценические инварианты', () => {
const host = fakeHost();
host.camera.x = NaN; // движковый инвариант pos-nan
const agent = new EngineAgent(host, () => fakeScene());
const invs = agent.invariants();
expect(invs.map((i) => i.id)).toEqual(['pos-nan', 'warn-x']);
});
});
describe('EngineAgent — шаги и команды', () => {
it('step крутит ровно n тиков', () => {
const host = fakeHost();
const agent = new EngineAgent(host, () => fakeScene());
expect(agent.step(10).tick).toBe(10);
expect(host.tickCount).toBe(10);
});
it('waitFor завершается по предикату и по лимиту', async () => {
const host = fakeHost();
const agent = new EngineAgent(host, () => fakeScene());
const ok = await agent.waitFor((s) => (s.tick as number) >= 5, { timeoutTicks: 100 });
expect(ok.ok).toBe(true);
expect(ok.ticks).toBe(5);
const fail = await agent.waitFor(() => false, { timeoutTicks: 3 });
expect(fail.ok).toBe(false);
expect(fail.ticks).toBe(3);
});
it('command проходит в сцену, неизвестная — null', () => {
const agent = new EngineAgent(fakeHost(), () => fakeScene());
expect(agent.command('ping')).toBe('pong');
expect(agent.command('unknown')).toBeNull();
});
it('ошибка в сцене не вылетает, а попадает в слой { error }', () => {
const bad: SceneAgent = {
agentSnapshot: () => {
throw new Error('бум');
},
agentInvariants: () => {
throw new Error('бум');
}
};
const agent = new EngineAgent(fakeHost(), () => bad);
const s = agent.snapshot() as SnapshotLayer;
expect(String(s.error)).toContain('бум');
expect(agent.invariants()).toEqual([]);
});
});