feat: 응답 가능한 서신과 턴 시간 호환 이관
등용장과 이민족 선택 응답을 turn daemon transaction으로 연결하고 Ref의 통일 이후 상태 전이와 수신자별 메시지 저장 규칙을 보존한다.\n\n유산 턴 시간 변경을 nextTurnTimeBase 기반 결정적 계산으로 바로잡고 API, 엔진, Chromium 회귀를 추가한다.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveResetTurnTimeBase } from '../src/router/inherit/index.js';
|
||||
|
||||
describe('inherit reset turn time Ref compatibility', () => {
|
||||
it('matches the Ref PHP deterministic seed, offset, and displayed minute', () => {
|
||||
const result = resolveResetTurnTimeBase({
|
||||
hiddenSeed: 'hidden-seed',
|
||||
userId: 'user-7',
|
||||
previousTurnTimeBase: 123_456,
|
||||
tickSeconds: 600,
|
||||
});
|
||||
|
||||
expect(result.nextTurnTimeBase).toBeCloseTo(302.5143852464758, 12);
|
||||
expect(result.nextTurnTimeLabel).toBe('00:05');
|
||||
});
|
||||
|
||||
it('uses the prior pending base as the next deterministic seed input', () => {
|
||||
const first = resolveResetTurnTimeBase({
|
||||
hiddenSeed: 'hidden-seed',
|
||||
userId: 'user-7',
|
||||
previousTurnTimeBase: 123_456,
|
||||
tickSeconds: 600,
|
||||
});
|
||||
const second = resolveResetTurnTimeBase({
|
||||
hiddenSeed: 'hidden-seed',
|
||||
userId: 'user-7',
|
||||
previousTurnTimeBase: first.nextTurnTimeBase,
|
||||
tickSeconds: 600,
|
||||
});
|
||||
|
||||
expect(second.nextTurnTimeBase).not.toBe(first.nextTurnTimeBase);
|
||||
expect(second.nextTurnTimeBase).toBeGreaterThanOrEqual(0);
|
||||
expect(second.nextTurnTimeBase).toBeLessThan(600);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { InMemoryFlushStore } from '../src/auth/flushStore.js';
|
||||
import type { DatabaseClient, GameApiContext, GeneralRow } from '../src/context.js';
|
||||
import type { TurnDaemonTransport } from '../src/daemon/transport.js';
|
||||
import { appRouter } from '../src/router.js';
|
||||
import { resolveResetTurnTimeBase } from '../src/router/inherit/index.js';
|
||||
|
||||
const buildGeneral = (overrides: Partial<GeneralRow> = {}): GeneralRow => ({
|
||||
id: 7,
|
||||
@@ -278,6 +279,43 @@ describe('inherit router actor and permission boundaries', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('queues Ref-compatible nextTurnTimeBase without moving the current scheduled turn', async () => {
|
||||
const fixture = buildContext({
|
||||
inheritancePoint: 2_000,
|
||||
general: buildGeneral({ meta: { nextTurnTimeBase: 123_456 } }),
|
||||
});
|
||||
const expected = resolveResetTurnTimeBase({
|
||||
hiddenSeed: 'test-seed',
|
||||
userId: 'user-1',
|
||||
previousTurnTimeBase: 123_456,
|
||||
tickSeconds: worldState.tickSeconds,
|
||||
});
|
||||
|
||||
await expect(appRouter.createCaller(fixture.context).inherit.resetTurnTime()).resolves.toEqual({
|
||||
ok: true,
|
||||
...expected,
|
||||
});
|
||||
expect(fixture.requestCommand).toHaveBeenCalledWith({
|
||||
type: 'patchGeneral',
|
||||
generalId: 7,
|
||||
patch: {
|
||||
meta: {
|
||||
nextTurnTimeBase: expected.nextTurnTimeBase,
|
||||
inheritResetTurnTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(fixture.pointUpsert).toHaveBeenCalledWith(expect.objectContaining({ update: { value: 1_000 } }));
|
||||
expect(fixture.logCreate).toHaveBeenCalledWith({
|
||||
data: {
|
||||
userId: 'user-1',
|
||||
year: 200,
|
||||
month: 4,
|
||||
text: `1000 포인트로 턴 시간을 바꾸어 다다음 턴부터 ${expected.nextTurnTimeLabel} 적용`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('reveals a target owner to the caller without using the caller general id from input', async () => {
|
||||
const fixture = buildContext({ inheritancePoint: 1500 });
|
||||
|
||||
|
||||
@@ -565,6 +565,70 @@ describe('messages router missing-flow compatibility', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['scout', 'raiseInvader'] as const)(
|
||||
'dispatches a private %s response through the durable engine command',
|
||||
async (action) => {
|
||||
const messageRow = {
|
||||
id: 29,
|
||||
mailbox: general.id,
|
||||
type: 'private',
|
||||
src: 8,
|
||||
dest: general.id,
|
||||
time: new Date(),
|
||||
valid_until: new Date('9999-12-31T00:00:00Z'),
|
||||
message: {
|
||||
src: {
|
||||
generalId: 8,
|
||||
generalName: '제안자',
|
||||
nationId: 2,
|
||||
nationName: '촉',
|
||||
color: '#000',
|
||||
icon: '',
|
||||
},
|
||||
dest: {
|
||||
generalId: general.id,
|
||||
generalName: general.name,
|
||||
nationId: general.nationId,
|
||||
nationName: '위',
|
||||
color: '#fff',
|
||||
icon: '',
|
||||
},
|
||||
text: '응답할 메시지',
|
||||
option: { action },
|
||||
},
|
||||
};
|
||||
const requestCommand = vi.fn(async () => ({
|
||||
type: 'messageRespond' as const,
|
||||
ok: true,
|
||||
generalId: general.id,
|
||||
messageId: messageRow.id,
|
||||
action,
|
||||
reason: 'success',
|
||||
}));
|
||||
const { caller } = buildContext(
|
||||
{ $queryRaw: vi.fn(async () => [messageRow]) },
|
||||
{ turnDaemon: { requestCommand } }
|
||||
);
|
||||
|
||||
const result = await caller.messages.respond({
|
||||
generalId: general.id,
|
||||
messageId: messageRow.id,
|
||||
response: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ result: true, reason: 'success' });
|
||||
expect(requestCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'messageRespond',
|
||||
userId: auth.user.id,
|
||||
generalId: general.id,
|
||||
messageId: messageRow.id,
|
||||
response: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const buildDiplomaticContext = (options?: {
|
||||
action?: 'noAggression' | 'cancelNA' | 'stopWar';
|
||||
actorNationId?: number;
|
||||
@@ -634,10 +698,10 @@ describe('messages router missing-flow compatibility', () => {
|
||||
let insertedId = 100;
|
||||
const queryRaw = vi.fn(async () => {
|
||||
rawCall += 1;
|
||||
if (rawCall === 1) {
|
||||
if (rawCall <= 2) {
|
||||
return [messageRow];
|
||||
}
|
||||
if (rawCall >= 4) {
|
||||
if (rawCall >= 5) {
|
||||
insertedId += 1;
|
||||
return [{ id: insertedId }];
|
||||
}
|
||||
@@ -693,84 +757,87 @@ describe('messages router missing-flow compatibility', () => {
|
||||
const messageUpdateMany = vi.fn(async () => ({ count: 1 }));
|
||||
const cityUpdate = vi.fn(async () => ({}));
|
||||
const changeJournal = new ChangeJournal();
|
||||
const { caller } = buildContext({
|
||||
general: {
|
||||
findUnique: vi.fn(async ({ where }: { where: { id: number } }) =>
|
||||
where.id === actor.id ? actor : where.id === proposer.id ? proposer : null
|
||||
),
|
||||
findMany: vi.fn(async () => []),
|
||||
const { caller } = buildContext(
|
||||
{
|
||||
general: {
|
||||
findUnique: vi.fn(async ({ where }: { where: { id: number } }) =>
|
||||
where.id === actor.id ? actor : where.id === proposer.id ? proposer : null
|
||||
),
|
||||
findMany: vi.fn(async () => []),
|
||||
},
|
||||
nation: {
|
||||
findUnique: vi.fn(async ({ where }: { where: { id: number } }) =>
|
||||
where.id === actorNationId
|
||||
? {
|
||||
id: actorNationId,
|
||||
name: '위',
|
||||
color: '#fff',
|
||||
capitalCityId: 10,
|
||||
chiefGeneralId: actor.id,
|
||||
gold: 1000,
|
||||
rice: 1000,
|
||||
tech: 0,
|
||||
level: 1,
|
||||
typeCode: 'test',
|
||||
meta: {},
|
||||
}
|
||||
: where.id === proposerNationId
|
||||
? {
|
||||
id: proposerNationId,
|
||||
name: '촉',
|
||||
color: '#000',
|
||||
capitalCityId: 20,
|
||||
chiefGeneralId: proposer.id,
|
||||
gold: 1000,
|
||||
rice: 1000,
|
||||
tech: 0,
|
||||
level: 1,
|
||||
typeCode: 'test',
|
||||
meta: { recv_assist: { [`n${actorNationId}`]: [actorNationId, 50] } },
|
||||
}
|
||||
: null
|
||||
),
|
||||
findMany: vi.fn(async () => []),
|
||||
update: nationUpdate,
|
||||
},
|
||||
city: {
|
||||
findUnique: vi.fn(async () => ({
|
||||
id: 10,
|
||||
nationId: actorNationId,
|
||||
supplyState: 1,
|
||||
})),
|
||||
findMany: vi.fn(async () => options?.cities ?? []),
|
||||
update: cityUpdate,
|
||||
},
|
||||
diplomacy: {
|
||||
findUnique: vi.fn(
|
||||
async ({
|
||||
where,
|
||||
}: {
|
||||
where: { srcNationId_destNationId: { srcNationId: number; destNationId: number } };
|
||||
}) =>
|
||||
diplomacyRows.find(
|
||||
(row) =>
|
||||
row.srcNationId === where.srcNationId_destNationId.srcNationId &&
|
||||
row.destNationId === where.srcNationId_destNationId.destNationId
|
||||
) ?? null
|
||||
),
|
||||
findMany: vi.fn(async () => diplomacyRows),
|
||||
update: diplomacyUpdate,
|
||||
},
|
||||
worldState: {
|
||||
findFirst: vi.fn(async () => ({
|
||||
currentYear: 200,
|
||||
currentMonth: 3,
|
||||
config: { environment: { mapName: 'che' } },
|
||||
})),
|
||||
},
|
||||
logEntry: { createMany: logCreateMany },
|
||||
message: { updateMany: messageUpdateMany },
|
||||
$queryRaw: queryRaw,
|
||||
},
|
||||
nation: {
|
||||
findUnique: vi.fn(async ({ where }: { where: { id: number } }) =>
|
||||
where.id === actorNationId
|
||||
? {
|
||||
id: actorNationId,
|
||||
name: '위',
|
||||
color: '#fff',
|
||||
capitalCityId: 10,
|
||||
chiefGeneralId: actor.id,
|
||||
gold: 1000,
|
||||
rice: 1000,
|
||||
tech: 0,
|
||||
level: 1,
|
||||
typeCode: 'test',
|
||||
meta: {},
|
||||
}
|
||||
: where.id === proposerNationId
|
||||
? {
|
||||
id: proposerNationId,
|
||||
name: '촉',
|
||||
color: '#000',
|
||||
capitalCityId: 20,
|
||||
chiefGeneralId: proposer.id,
|
||||
gold: 1000,
|
||||
rice: 1000,
|
||||
tech: 0,
|
||||
level: 1,
|
||||
typeCode: 'test',
|
||||
meta: { recv_assist: { [`n${actorNationId}`]: [actorNationId, 50] } },
|
||||
}
|
||||
: null
|
||||
),
|
||||
findMany: vi.fn(async () => []),
|
||||
update: nationUpdate,
|
||||
},
|
||||
city: {
|
||||
findUnique: vi.fn(async () => ({
|
||||
id: 10,
|
||||
nationId: actorNationId,
|
||||
supplyState: 1,
|
||||
})),
|
||||
findMany: vi.fn(async () => options?.cities ?? []),
|
||||
update: cityUpdate,
|
||||
},
|
||||
diplomacy: {
|
||||
findUnique: vi.fn(
|
||||
async ({
|
||||
where,
|
||||
}: {
|
||||
where: { srcNationId_destNationId: { srcNationId: number; destNationId: number } };
|
||||
}) =>
|
||||
diplomacyRows.find(
|
||||
(row) =>
|
||||
row.srcNationId === where.srcNationId_destNationId.srcNationId &&
|
||||
row.destNationId === where.srcNationId_destNationId.destNationId
|
||||
) ?? null
|
||||
),
|
||||
findMany: vi.fn(async () => diplomacyRows),
|
||||
update: diplomacyUpdate,
|
||||
},
|
||||
worldState: {
|
||||
findFirst: vi.fn(async () => ({
|
||||
currentYear: 200,
|
||||
currentMonth: 3,
|
||||
config: { environment: { mapName: 'che' } },
|
||||
})),
|
||||
},
|
||||
logEntry: { createMany: logCreateMany },
|
||||
message: { updateMany: messageUpdateMany },
|
||||
$queryRaw: queryRaw,
|
||||
}, { changeJournal });
|
||||
{ changeJournal }
|
||||
);
|
||||
return {
|
||||
caller,
|
||||
actor,
|
||||
@@ -811,7 +878,7 @@ describe('messages router missing-flow compatibility', () => {
|
||||
where: { id: { in: [31] } },
|
||||
data: { validUntil: expect.any(Date) },
|
||||
});
|
||||
expect(setup.queryRaw).toHaveBeenCalledTimes(8);
|
||||
expect(setup.queryRaw).toHaveBeenCalledTimes(9);
|
||||
});
|
||||
|
||||
it('declines a diplomatic prompt without changing diplomacy', async () => {
|
||||
|
||||
Reference in New Issue
Block a user