test: 조건부 통합 fixture의 시계와 소유권을 격리한다
This commit is contained in:
@@ -34,6 +34,7 @@ liveDescribe('auction worker durable recovery', () => {
|
||||
const createdRequestPrefixes: string[] = [];
|
||||
const createdWorldIds: number[] = [];
|
||||
const createdGeneralIds: number[] = [];
|
||||
const lifecycleUserIds = ['auction-durable-host', 'auction-durable-bidder'];
|
||||
|
||||
beforeAll(async () => {
|
||||
await connector.connect();
|
||||
@@ -56,6 +57,8 @@ liveDescribe('auction worker durable recovery', () => {
|
||||
await connector.prisma.worldState.deleteMany({ where: { id: { in: createdWorldIds } } });
|
||||
}
|
||||
if (createdGeneralIds.length > 0) {
|
||||
await connector.prisma.message.deleteMany({ where: { mailbox: { in: createdGeneralIds } } });
|
||||
await connector.prisma.webPushOutbox.deleteMany({ where: { userIds: { hasSome: lifecycleUserIds } } });
|
||||
await connector.prisma.logEntry.deleteMany({ where: { generalId: { in: createdGeneralIds } } });
|
||||
await connector.prisma.general.deleteMany({ where: { id: { in: createdGeneralIds } } });
|
||||
}
|
||||
@@ -366,6 +369,8 @@ liveDescribe('auction worker durable recovery', () => {
|
||||
};
|
||||
|
||||
const lifecycleGeneralIds = [8_032, 8_033];
|
||||
await connector.prisma.message.deleteMany({ where: { mailbox: { in: lifecycleGeneralIds } } });
|
||||
await connector.prisma.webPushOutbox.deleteMany({ where: { userIds: { hasSome: lifecycleUserIds } } });
|
||||
await connector.prisma.logEntry.deleteMany({ where: { generalId: { in: lifecycleGeneralIds } } });
|
||||
await connector.prisma.general.deleteMany({ where: { id: { in: lifecycleGeneralIds } } });
|
||||
await connector.prisma.worldState.deleteMany({ where: { id: worldId } });
|
||||
|
||||
@@ -472,7 +472,9 @@ integration('scenario 903 select pool through the durable turn daemon', () => {
|
||||
}, 30_000);
|
||||
|
||||
it('keeps a stable ENGINE event for retries and rejects reservation bypasses', async () => {
|
||||
const logicalNowMs = runtime!.world.getGameNow(new Date()).getTime();
|
||||
const logicalNow = runtime!.world.getGameNow(new Date());
|
||||
const logicalNowMs = logicalNow.getTime();
|
||||
const logicalNowTick = runtime!.world.dateToGameTick(logicalNow);
|
||||
const reservation = await appRouter
|
||||
.createCaller(buildContext('select-pool-other-reserve', otherAuth))
|
||||
.join.getSelectionPool();
|
||||
@@ -488,7 +490,10 @@ integration('scenario 903 select pool through the durable turn daemon', () => {
|
||||
|
||||
await db.selectPoolEntry.update({
|
||||
where: { uniqueName: candidate.uniqueName },
|
||||
data: { reservedUntil: new Date(logicalNowMs - 60_000) },
|
||||
data: {
|
||||
reservedUntil: new Date(logicalNowMs - 60_000),
|
||||
reservedUntilTick: BigInt(logicalNowTick - 1),
|
||||
},
|
||||
});
|
||||
await expect(
|
||||
appRouter.createCaller(buildContext('select-pool-expired-token', otherAuth)).join.selectPoolGeneral({
|
||||
|
||||
@@ -560,12 +560,6 @@ integration('immediate general action persistence', () => {
|
||||
generalId,
|
||||
text: expect.stringContaining('낙양'),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
scope: 'NATION',
|
||||
category: 'HISTORY',
|
||||
nationId: existingNationId + 1,
|
||||
text: expect.stringContaining('통합장수'),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
scope: 'SYSTEM',
|
||||
category: 'SUMMARY',
|
||||
@@ -578,6 +572,17 @@ integration('immediate general action persistence', () => {
|
||||
}),
|
||||
])
|
||||
);
|
||||
// Ref creates this action logger while the actor is still unaffiliated,
|
||||
// so its queued national history is discarded when nationID is 0.
|
||||
await expect(
|
||||
db.logEntry.count({
|
||||
where: {
|
||||
scope: 'NATION',
|
||||
category: 'HISTORY',
|
||||
nationId: existingNationId + 1,
|
||||
},
|
||||
})
|
||||
).resolves.toBe(0);
|
||||
|
||||
const reloaded = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
|
||||
expect(reloaded.snapshot.generals.find((entry) => entry.id === generalId)).toMatchObject({
|
||||
|
||||
@@ -502,7 +502,6 @@ integration('RaiseInvader database persistence', () => {
|
||||
diplomacyCountsPerNation: [2],
|
||||
diplomacyStates: ['1:24'],
|
||||
isunited: 1,
|
||||
isUnited: 1,
|
||||
blockChangeScout: false,
|
||||
});
|
||||
}
|
||||
@@ -633,7 +632,6 @@ integration('RaiseInvader database persistence', () => {
|
||||
result: 'Deleted',
|
||||
endingEventPresent: false,
|
||||
isunited: 3,
|
||||
isUnited: 3,
|
||||
refreshLimit: 300,
|
||||
logs: [
|
||||
'<C>●</>200년 4월:<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!',
|
||||
@@ -644,5 +642,5 @@ integration('RaiseInvader database persistence', () => {
|
||||
} finally {
|
||||
await dbHooks.close();
|
||||
}
|
||||
});
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
@@ -43,8 +43,8 @@ integration('select pool release during general deletion', () => {
|
||||
'release-candidate',
|
||||
'claim-candidate',
|
||||
'conflict-candidate',
|
||||
'early-protected-candidate',
|
||||
'later-free-candidate',
|
||||
'early-protected',
|
||||
'later-free',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -203,8 +203,8 @@ integration('select pool release during general deletion', () => {
|
||||
'release-candidate',
|
||||
'claim-candidate',
|
||||
'conflict-candidate',
|
||||
'early-protected-candidate',
|
||||
'later-free-candidate',
|
||||
'early-protected',
|
||||
'later-free',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -366,12 +366,12 @@ integration('select pool release during general deletion', () => {
|
||||
await db.selectPoolEntry.createMany({
|
||||
data: [
|
||||
{
|
||||
uniqueName: 'early-protected-candidate',
|
||||
uniqueName: 'early-protected',
|
||||
ownerUserId: 'protected-user',
|
||||
generalId: null,
|
||||
reservedUntil,
|
||||
info: {
|
||||
uniqueName: 'early-protected-candidate',
|
||||
uniqueName: 'early-protected',
|
||||
generalName: '보호후보',
|
||||
leadership: 70,
|
||||
strength: 80,
|
||||
@@ -383,12 +383,12 @@ integration('select pool release during general deletion', () => {
|
||||
} as GamePrisma.InputJsonValue,
|
||||
},
|
||||
{
|
||||
uniqueName: 'later-free-candidate',
|
||||
uniqueName: 'later-free',
|
||||
ownerUserId: null,
|
||||
generalId: null,
|
||||
reservedUntil: null,
|
||||
info: {
|
||||
uniqueName: 'later-free-candidate',
|
||||
uniqueName: 'later-free',
|
||||
generalName: '후행후보',
|
||||
leadership: 70,
|
||||
strength: 80,
|
||||
@@ -406,8 +406,8 @@ integration('select pool release during general deletion', () => {
|
||||
schedule: { entries: [{ startMinute: 0, tickMinutes: 5 }] },
|
||||
});
|
||||
const entries = world.listGeneralPoolEntries()!;
|
||||
const protectedCandidate = entries.find((entry) => entry.uniqueName === 'early-protected-candidate')!.candidate;
|
||||
const laterCandidate = entries.find((entry) => entry.uniqueName === 'later-free-candidate')!.candidate;
|
||||
const protectedCandidate = entries.find((entry) => entry.uniqueName === 'early-protected')!.candidate;
|
||||
const laterCandidate = entries.find((entry) => entry.uniqueName === 'later-free')!.candidate;
|
||||
const template = world.getGeneralById(generalId)!;
|
||||
expect(
|
||||
world.addGeneral({
|
||||
@@ -446,7 +446,7 @@ integration('select pool release during general deletion', () => {
|
||||
durationMs: 0,
|
||||
partial: false,
|
||||
})
|
||||
).rejects.toThrow('select_pool 후보를 점유하지 못했습니다: early-protected-candidate');
|
||||
).rejects.toThrow('select_pool 후보를 점유하지 못했습니다: early-protected');
|
||||
} finally {
|
||||
await hooks.close();
|
||||
}
|
||||
@@ -454,14 +454,14 @@ integration('select pool release during general deletion', () => {
|
||||
await expect(db.general.findUnique({ where: { id: protectedGeneralId } })).resolves.toBeNull();
|
||||
await expect(db.general.findUnique({ where: { id: laterGeneralId } })).resolves.toBeNull();
|
||||
await expect(
|
||||
db.selectPoolEntry.findUniqueOrThrow({ where: { uniqueName: 'early-protected-candidate' } })
|
||||
db.selectPoolEntry.findUniqueOrThrow({ where: { uniqueName: 'early-protected' } })
|
||||
).resolves.toMatchObject({
|
||||
generalId: null,
|
||||
ownerUserId: 'protected-user',
|
||||
reservedUntil,
|
||||
});
|
||||
await expect(
|
||||
db.selectPoolEntry.findUniqueOrThrow({ where: { uniqueName: 'later-free-candidate' } })
|
||||
db.selectPoolEntry.findUniqueOrThrow({ where: { uniqueName: 'later-free' } })
|
||||
).resolves.toMatchObject({ generalId: null, ownerUserId: null, reservedUntil: null });
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { PnpmBuildRunner } from '../src/orchestrator/buildRunner.js';
|
||||
import { buildProfileMigrationPreflightCommand } from '../src/orchestrator/gatewayOrchestrator.js';
|
||||
|
||||
const utcDatabaseUrl = process.env.PROFILE_MIGRATION_UTC_DATABASE_URL?.trim();
|
||||
|
||||
describe('profile migration timezone preflight', () => {
|
||||
it.skipIf(!utcDatabaseUrl)('rejects an actual database role whose default session timezone is UTC', async () => {
|
||||
if (!utcDatabaseUrl) throw new Error('PROFILE_MIGRATION_UTC_DATABASE_URL is required');
|
||||
const path = process.env.PATH;
|
||||
if (!path) throw new Error('PATH is required');
|
||||
const command = buildProfileMigrationPreflightCommand(process.cwd(), utcDatabaseUrl, { PATH: path });
|
||||
|
||||
const result = await new PnpmBuildRunner().run([command]);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.output).toContain('database session TimeZone does not match the required migration contract');
|
||||
expect(result.output).not.toContain(utcDatabaseUrl);
|
||||
const password = decodeURIComponent(new URL(utcDatabaseUrl).password);
|
||||
if (password) expect(result.output).not.toContain(password);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user