From 05d8ebe3d32329d0f4179189d7cbfbf6e6a85346 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Sun, 28 Dec 2025 11:14:34 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20ManualClock=20=EB=B0=8F=20StepClock=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=EC=99=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=9E=91=EC=84=B1=20feat:=20vitest?= =?UTF-8?q?=EB=A5=BC=20devDependencies=EC=97=90=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95=20docs:=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=20=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=8B=9C=EA=B3=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-engine/package.json | 5 +- app/game-engine/src/lifecycle/clock.ts | 61 ++++++++++++++++++++++ app/game-engine/test/clock.test.ts | 44 ++++++++++++++++ app/game-engine/vitest.config.ts | 9 ++++ docs/architecture/turn-daemon-lifecycle.md | 7 +++ pnpm-lock.yaml | 6 ++- 6 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 app/game-engine/test/clock.test.ts create mode 100644 app/game-engine/vitest.config.ts diff --git a/app/game-engine/package.json b/app/game-engine/package.json index 03c957f..7dc04d5 100644 --- a/app/game-engine/package.json +++ b/app/game-engine/package.json @@ -7,6 +7,9 @@ "build": "tsc -p tsconfig.json", "dev": "node -e \"console.log('dev not configured')\"", "lint": "node -e \"console.log('lint not configured')\"", - "test": "node -e \"console.log('test not configured')\"" + "test": "vitest run --config vitest.config.ts" + }, + "devDependencies": { + "vitest": "^4.0.16" } } diff --git a/app/game-engine/src/lifecycle/clock.ts b/app/game-engine/src/lifecycle/clock.ts index 02d4395..ce2c6b1 100644 --- a/app/game-engine/src/lifecycle/clock.ts +++ b/app/game-engine/src/lifecycle/clock.ts @@ -13,3 +13,64 @@ export class SystemClock implements Clock { await new Promise((resolve) => setTimeout(resolve, ms)); } } + +export class ManualClock implements Clock { + // 테스트에서 시간을 직접 이동시키는 수동 시계. + private currentMs: number; + + constructor(initialMs = 0) { + this.currentMs = initialMs; + } + + nowMs(): number { + return this.currentMs; + } + + async sleepMs(ms: number): Promise { + if (ms <= 0) { + return; + } + this.currentMs += ms; + } + + advanceMs(ms: number): void { + if (ms <= 0) { + return; + } + this.currentMs += ms; + } + + setMs(ms: number): void { + this.currentMs = ms; + } +} + +export class StepClock implements Clock { + // 호출마다 일정 간격씩 시간이 진행되는 시계. + private currentMs: number; + private readonly stepMs: number; + + constructor(stepMs: number, initialMs = 0) { + if (stepMs <= 0) { + throw new Error('stepMs must be positive'); + } + this.stepMs = stepMs; + this.currentMs = initialMs; + } + + nowMs(): number { + this.currentMs += this.stepMs; + return this.currentMs; + } + + async sleepMs(ms: number): Promise { + if (ms <= 0) { + return; + } + this.currentMs += ms; + } + + setMs(ms: number): void { + this.currentMs = ms; + } +} diff --git a/app/game-engine/test/clock.test.ts b/app/game-engine/test/clock.test.ts new file mode 100644 index 0000000..0df36ec --- /dev/null +++ b/app/game-engine/test/clock.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { ManualClock, StepClock } from '../src/lifecycle/clock.js'; + +describe('ManualClock', () => { + it('returns current time without advancing', () => { + const clock = new ManualClock(1000); + expect(clock.nowMs()).toBe(1000); + expect(clock.nowMs()).toBe(1000); + }); + + it('advances with sleep and manual advance', async () => { + const clock = new ManualClock(0); + await clock.sleepMs(250); + expect(clock.nowMs()).toBe(250); + clock.advanceMs(750); + expect(clock.nowMs()).toBe(1000); + }); + + it('can set time explicitly', () => { + const clock = new ManualClock(10); + clock.setMs(5000); + expect(clock.nowMs()).toBe(5000); + }); +}); + +describe('StepClock', () => { + it('advances on each nowMs call', () => { + const clock = new StepClock(100, 0); + expect(clock.nowMs()).toBe(100); + expect(clock.nowMs()).toBe(200); + expect(clock.nowMs()).toBe(300); + }); + + it('advances with sleep', async () => { + const clock = new StepClock(50, 1000); + await clock.sleepMs(200); + expect(clock.nowMs()).toBe(1250); + }); + + it('rejects non-positive step', () => { + expect(() => new StepClock(0)).toThrow('stepMs must be positive'); + expect(() => new StepClock(-5)).toThrow('stepMs must be positive'); + }); +}); diff --git a/app/game-engine/vitest.config.ts b/app/game-engine/vitest.config.ts new file mode 100644 index 0000000..5c63f1e --- /dev/null +++ b/app/game-engine/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + include: ['test/**/*.test.ts'], + }, +}); diff --git a/docs/architecture/turn-daemon-lifecycle.md b/docs/architecture/turn-daemon-lifecycle.md index 02e5ca5..b713d0e 100644 --- a/docs/architecture/turn-daemon-lifecycle.md +++ b/docs/architecture/turn-daemon-lifecycle.md @@ -185,6 +185,13 @@ export type DaemonEvent = - On shutdown, stop accepting new triggers, finish the current run, flush, then exit cleanly. +## Testing with a Controlled Clock + +- Turn daemon tests should use a controllable `Clock` implementation (예: `ManualClock`) to + advance time deterministically without relying on wall-clock time. +- 기준 시간은 DB에 저장된 `game_env.turntime`을 우선으로 삼고, 테스트에서도 동일한 기준을 + 사용해 스케줄 계산과 체크포인트 동작을 검증한다. + ## TypeScript Sketch (Draft) ```ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fd5c41..01ce7c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,11 @@ importers: app/game-api: {} - app/game-engine: {} + app/game-engine: + devDependencies: + vitest: + specifier: ^4.0.16 + version: 4.0.16(@types/node@20.19.27) app/game-frontend: {}