test(clock): exercise suspended lifecycle actions
This commit is contained in:
@@ -18,10 +18,26 @@ integration('database command queue', () => {
|
||||
const cleanupFixtures = async (): Promise<void> => {
|
||||
await db.inputEvent.deleteMany({ where: { requestId: { startsWith: 'integration:engine:' } } });
|
||||
await db.clockProjectionOutbox.deleteMany({
|
||||
where: { suspensionId: { in: ['integration-queue-revision-8-9', 'integration-unification-wait'] } },
|
||||
where: {
|
||||
suspensionId: {
|
||||
in: [
|
||||
'integration-queue-revision-8-9',
|
||||
'integration-maintenance-suspension',
|
||||
'integration-unification-wait',
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await db.clockSuspension.deleteMany({
|
||||
where: { id: { in: ['integration-queue-revision-8-9', 'integration-unification-wait'] } },
|
||||
where: {
|
||||
id: {
|
||||
in: [
|
||||
'integration-queue-revision-8-9',
|
||||
'integration-maintenance-suspension',
|
||||
'integration-unification-wait',
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await db.message.deleteMany({ where: { mailbox: 991_199 } });
|
||||
await db.worldState.deleteMany({
|
||||
@@ -524,6 +540,116 @@ integration('database command queue', () => {
|
||||
await expect(new DatabaseTurnDaemonCommandQueue(db).drain()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('dequeues fenced immediate user mutations during a maintenance suspension', async () => {
|
||||
const world = await db.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
||||
await db.worldState.update({
|
||||
where: { id: world.id },
|
||||
data: { clockPhase: 'SUSPENDED', clockRevision: 23n, deadlineGeneration: 9n, clockTick: 777n },
|
||||
});
|
||||
await db.clockSuspension.create({
|
||||
data: {
|
||||
id: 'integration-maintenance-suspension',
|
||||
worldStateId: world.id,
|
||||
source: 'MAINTENANCE',
|
||||
policy: 'EXACT',
|
||||
status: 'SUSPENDED',
|
||||
sourceRevision: 23n,
|
||||
targetRevision: 24n,
|
||||
cutTick: 777n,
|
||||
cutWallAt: new Date(),
|
||||
rateTicksPerSecond: 60_000,
|
||||
},
|
||||
});
|
||||
const commands: TurnDaemonCommand[] = [
|
||||
{
|
||||
type: 'inheritanceAction',
|
||||
requestId: 'integration:engine:suspended-inheritance',
|
||||
userId: 'user-7',
|
||||
input: { action: 'buyHiddenBuff', buffType: 'warAvoidRatio', level: 1 },
|
||||
},
|
||||
{
|
||||
type: 'dropItem',
|
||||
requestId: 'integration:engine:suspended-drop-item',
|
||||
userId: 'user-7',
|
||||
generalId: 7,
|
||||
itemType: 'weapon',
|
||||
},
|
||||
{
|
||||
type: 'changePermission',
|
||||
requestId: 'integration:engine:suspended-permission',
|
||||
userId: 'user-7',
|
||||
generalId: 7,
|
||||
isAmbassador: true,
|
||||
targetGeneralIds: [8],
|
||||
},
|
||||
{
|
||||
type: 'appoint',
|
||||
requestId: 'integration:engine:suspended-appoint',
|
||||
userId: 'user-7',
|
||||
generalId: 7,
|
||||
destGeneralId: 8,
|
||||
destCityId: 1,
|
||||
officerLevel: 2,
|
||||
},
|
||||
{
|
||||
type: 'setNationSetting',
|
||||
requestId: 'integration:engine:suspended-nation-setting',
|
||||
userId: 'user-7',
|
||||
generalId: 7,
|
||||
nationId: 1,
|
||||
mutation: { kind: 'rate', amount: 20 },
|
||||
},
|
||||
{
|
||||
type: 'setNpcPolicy',
|
||||
requestId: 'integration:engine:suspended-npc-policy',
|
||||
userId: 'user-7',
|
||||
generalId: 7,
|
||||
nationId: 1,
|
||||
expectedUpdatedAt: null,
|
||||
mutation: { kind: 'nationPriority', priority: ['develop'] },
|
||||
},
|
||||
];
|
||||
await db.inputEvent.createMany({
|
||||
data: commands.map((command) => ({
|
||||
requestId: command.requestId!,
|
||||
target: 'ENGINE' as const,
|
||||
eventType: command.type,
|
||||
actorUserId: 'userId' in command ? command.userId : null,
|
||||
payload: command as GamePrisma.InputJsonValue,
|
||||
})),
|
||||
});
|
||||
const blockedRequestId = 'integration:engine:suspended-vacation-still-gated';
|
||||
await db.inputEvent.create({
|
||||
data: {
|
||||
requestId: blockedRequestId,
|
||||
target: 'ENGINE',
|
||||
eventType: 'vacation',
|
||||
actorUserId: 'user-7',
|
||||
payload: {
|
||||
type: 'vacation',
|
||||
requestId: blockedRequestId,
|
||||
userId: 'user-7',
|
||||
generalId: 7,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const claimed = await new DatabaseTurnDaemonCommandQueue(db).drain();
|
||||
expect(claimed.map(({ type }) => type)).toEqual(commands.map(({ type }) => type));
|
||||
for (const command of commands) {
|
||||
await expect(db.inputEvent.findUniqueOrThrow({ where: { requestId: command.requestId! } })).resolves.toMatchObject({
|
||||
status: 'PROCESSING',
|
||||
processingGameTick: 777n,
|
||||
processingClockRevision: 23n,
|
||||
processingDeadlineGeneration: 9n,
|
||||
});
|
||||
}
|
||||
await expect(db.inputEvent.findUniqueOrThrow({ where: { requestId: blockedRequestId } })).resolves.toMatchObject({
|
||||
status: 'PENDING',
|
||||
processingClockRevision: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('dequeues only the invader decision while an UNIFICATION_WAIT suspension is active', async () => {
|
||||
const existingWorld = await db.worldState.findFirst({ orderBy: { id: 'asc' } });
|
||||
const world = existingWorld
|
||||
|
||||
@@ -3,6 +3,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { createGatewayPostgresConnector, type GatewayPrismaClient } from '@sammo-ts/infra';
|
||||
|
||||
import { createGatewayAdminActionConsumer } from '../src/turn/gatewayAdminActions.js';
|
||||
import { createGatewayProfileGate } from '../src/turn/gatewayProfileGate.js';
|
||||
|
||||
const databaseUrl = process.env.GATEWAY_RUNTIME_ACTION_DATABASE_URL;
|
||||
const integration = describe.skipIf(!databaseUrl);
|
||||
@@ -109,4 +110,35 @@ integration('gateway runtime action consumer', () => {
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
expect(onActionApplied).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not overwrite a terminal operator status while reporting a daemon error', async () => {
|
||||
const gate = await createGatewayProfileGate({
|
||||
databaseUrl: databaseUrl!,
|
||||
gatewayDatabaseUrl: databaseUrl!,
|
||||
profileName,
|
||||
});
|
||||
try {
|
||||
await db.gatewayProfile.update({
|
||||
where: { profileName },
|
||||
data: { status: 'RUNNING', lastError: null },
|
||||
});
|
||||
await gate.markPaused(new Error('running failure'));
|
||||
expect(await db.gatewayProfile.findUniqueOrThrow({ where: { profileName } })).toMatchObject({
|
||||
status: 'PAUSED',
|
||||
lastError: 'running failure',
|
||||
});
|
||||
|
||||
await db.gatewayProfile.update({
|
||||
where: { profileName },
|
||||
data: { status: 'STOPPED', lastError: null },
|
||||
});
|
||||
await gate.markPaused(new Error('late shutdown failure'));
|
||||
expect(await db.gatewayProfile.findUniqueOrThrow({ where: { profileName } })).toMatchObject({
|
||||
status: 'STOPPED',
|
||||
lastError: null,
|
||||
});
|
||||
} finally {
|
||||
await gate.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user