merge: 최신 main의 내부 예약 명령 경계를 반영한다
# Conflicts: # app/game-api/src/router/turns/index.ts
This commit is contained in:
@@ -99,6 +99,31 @@ describe('turn command argument input', () => {
|
||||
await expect(parseReservedTurnArgs('general', 'che_포상', {})).rejects.toThrow('Unknown general turn command');
|
||||
});
|
||||
|
||||
it('rejects internal general commands before parsing their arguments or scenario overrides', async () => {
|
||||
await expect(parseReservedTurnArgs('general', 'che_NPC능동', {})).rejects.toThrow(
|
||||
'Unknown general turn command: che_NPC능동'
|
||||
);
|
||||
await expect(parseReservedTurnArgs('general', 'che_방랑', {})).rejects.toThrow(
|
||||
'Unknown general turn command: che_방랑'
|
||||
);
|
||||
await expect(
|
||||
parseReservedTurnArgs('general', 'che_등용수락', { destNationId: 1, destGeneralId: 2 })
|
||||
).rejects.toThrow('Unknown general turn command: che_등용수락');
|
||||
|
||||
await expect(
|
||||
parseReservedTurnArgs(
|
||||
'general',
|
||||
'che_NPC능동',
|
||||
{ optionText: '순간이동', destCityId: 1 },
|
||||
{
|
||||
availableGeneralCommand: {
|
||||
내부: ['휴식', 'che_NPC능동'],
|
||||
},
|
||||
}
|
||||
)
|
||||
).rejects.toThrow('Unknown scenario general command key: che_NPC능동');
|
||||
});
|
||||
|
||||
it('accepts and rejects reserved commands from the real 904/905/910/912 world config', async () => {
|
||||
const scenarioConsts = Object.fromEntries(
|
||||
await Promise.all(
|
||||
|
||||
@@ -39,8 +39,12 @@ const buildContext = (
|
||||
nation: {
|
||||
count: vi.fn(async () => 0),
|
||||
},
|
||||
turnDaemonLease: {
|
||||
findUnique: vi.fn(async () => ({ leaseUntil: new Date('2099-01-01T00:00:00.000Z') })),
|
||||
},
|
||||
} as unknown as DatabaseClient,
|
||||
}) as GameApiContext;
|
||||
profileStatusSource: { get: vi.fn(async () => 'RUNNING' as const) },
|
||||
}) as unknown as GameApiContext;
|
||||
|
||||
describe('lobby season state', () => {
|
||||
it.each([0, 1, 2, 3])('returns legacy isunited state %i', async (isunited) => {
|
||||
@@ -70,9 +74,28 @@ describe('lobby season state', () => {
|
||||
expect(result.clockMode).toBe('manual');
|
||||
expect(result.clockRunning).toBe(false);
|
||||
expect(result.clockStartsAt).toBeNull();
|
||||
expect(result.turnEngineRunning).toBe(true);
|
||||
expect(new Date(result.serverWallTime).getTime()).not.toBeNaN();
|
||||
});
|
||||
|
||||
it('projects the explicit Gateway turn-running capability independently of the game clock mode', async () => {
|
||||
const context = buildContext(
|
||||
{},
|
||||
{
|
||||
baseTime: new Date('2026-08-15T00:00:00.000Z'),
|
||||
tick: 72_000_000n,
|
||||
mode: 'realtime',
|
||||
wallAnchor: new Date('2026-08-15T00:00:00.000Z'),
|
||||
}
|
||||
);
|
||||
context.profileStatusSource = { get: vi.fn(async () => 'PAUSED' as const) };
|
||||
|
||||
const result = await appRouter.createCaller(context).lobby.info();
|
||||
|
||||
expect(result.clockRunning).toBe(true);
|
||||
expect(result.turnEngineRunning).toBe(false);
|
||||
});
|
||||
|
||||
it('exposes the future realtime wall anchor without advancing the preopen clock', async () => {
|
||||
const wallAnchor = new Date('2099-08-21T11:00:00.000Z');
|
||||
const result = await appRouter
|
||||
|
||||
@@ -1440,7 +1440,12 @@ describe('appRouter', () => {
|
||||
const generalWrites: unknown[] = [];
|
||||
const nationWrites: unknown[] = [];
|
||||
const caller = appRouter.createCaller(
|
||||
buildContext({ general, generalTurnWrites: generalWrites, nationTurnWrites: nationWrites })
|
||||
buildContext({
|
||||
state: buildWorldState(),
|
||||
general,
|
||||
generalTurnWrites: generalWrites,
|
||||
nationTurnWrites: nationWrites,
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -1461,6 +1466,18 @@ describe('appRouter', () => {
|
||||
expectedRevision: 0,
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
||||
await expect(
|
||||
caller.turns.reserved.setGeneral({
|
||||
generalId: 14,
|
||||
turnIndex: 0,
|
||||
action: 'che_NPC능동',
|
||||
args: {},
|
||||
expectedRevision: 0,
|
||||
})
|
||||
).rejects.toMatchObject({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Unknown general turn command: che_NPC능동',
|
||||
});
|
||||
|
||||
expect(generalWrites).toHaveLength(0);
|
||||
expect(nationWrites).toHaveLength(0);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { CachedTurnEngineStatus, loadTurnEngineRunning } from '../src/services/turnEngineStatus.js';
|
||||
|
||||
describe('turn engine status projection', () => {
|
||||
it('maps Gateway profile capabilities and keeps unavailable status unknown', async () => {
|
||||
const activeLease = {
|
||||
turnDaemonLease: {
|
||||
findUnique: vi.fn(async () => ({ leaseUntil: new Date('2026-08-24T00:01:00.000Z') })),
|
||||
},
|
||||
};
|
||||
const now = new Date('2026-08-24T00:00:00.000Z');
|
||||
await expect(loadTurnEngineRunning({ get: async () => 'RUNNING' }, activeLease, 'che:default', now)).resolves.toBe(
|
||||
true
|
||||
);
|
||||
await expect(loadTurnEngineRunning({ get: async () => 'PREOPEN' }, activeLease, 'che:default', now)).resolves.toBe(
|
||||
false
|
||||
);
|
||||
await expect(loadTurnEngineRunning({ get: async () => 'PAUSED' }, activeLease, 'che:default', now)).resolves.toBe(
|
||||
false
|
||||
);
|
||||
await expect(loadTurnEngineRunning({ get: async () => null }, activeLease, 'che:default', now)).resolves.toBeNull();
|
||||
await expect(
|
||||
loadTurnEngineRunning(
|
||||
{ get: async () => Promise.reject(new Error('gateway unavailable')) },
|
||||
activeLease,
|
||||
'che:default',
|
||||
now
|
||||
)
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('marks a RUNNING profile stopped when its daemon lease is missing or expired', async () => {
|
||||
const source = { get: async () => 'RUNNING' as const };
|
||||
const now = new Date('2026-08-24T00:00:00.000Z');
|
||||
await expect(
|
||||
loadTurnEngineRunning(
|
||||
source,
|
||||
{ turnDaemonLease: { findUnique: async () => null } },
|
||||
'che:default',
|
||||
now
|
||||
)
|
||||
).resolves.toBe(false);
|
||||
await expect(
|
||||
loadTurnEngineRunning(
|
||||
source,
|
||||
{
|
||||
turnDaemonLease: {
|
||||
findUnique: async () => ({ leaseUntil: new Date('2026-08-23T23:59:59.999Z') }),
|
||||
},
|
||||
},
|
||||
'che:default',
|
||||
now
|
||||
)
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('coalesces concurrent heartbeat reads and refreshes after the bounded cache window', async () => {
|
||||
let now = 1_000;
|
||||
const get = vi.fn(async () => 'RUNNING' as const);
|
||||
const findUnique = vi.fn(async () => ({ leaseUntil: new Date('2099-01-01T00:00:00.000Z') }));
|
||||
const cache = new CachedTurnEngineStatus(
|
||||
{ get },
|
||||
{ turnDaemonLease: { findUnique } },
|
||||
'che:default',
|
||||
2_000,
|
||||
() => now
|
||||
);
|
||||
|
||||
await expect(Promise.all([cache.get(), cache.get()])).resolves.toEqual([true, true]);
|
||||
expect(get).toHaveBeenCalledTimes(1);
|
||||
now += 1_999;
|
||||
await expect(cache.get()).resolves.toBe(true);
|
||||
expect(get).toHaveBeenCalledTimes(1);
|
||||
now += 1;
|
||||
await expect(cache.get()).resolves.toBe(true);
|
||||
expect(get).toHaveBeenCalledTimes(2);
|
||||
expect(findUnique).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user