일반 메시지 WALL_TIME 저장 회귀 수정
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra';
|
import { createGamePostgresConnector, persistMessageEnvelope, type GamePrismaClient } from '@sammo-ts/infra';
|
||||||
|
|
||||||
import { tombstoneMessages, tombstoneMessagesWithinDeleteWindow } from '../src/messages/store.js';
|
import { tombstoneMessages, tombstoneMessagesWithinDeleteWindow } from '../src/messages/store.js';
|
||||||
|
|
||||||
@@ -23,6 +23,51 @@ integration('message deletion tombstone persistence', () => {
|
|||||||
|
|
||||||
afterAll(async () => close?.());
|
afterAll(async () => close?.());
|
||||||
|
|
||||||
|
it('persists an ordinary wall-time envelope without creating a game action', async () => {
|
||||||
|
const rollback = new Error('rollback ordinary message envelope fixture');
|
||||||
|
await expect(
|
||||||
|
db.$transaction(async (transaction) => {
|
||||||
|
const target = {
|
||||||
|
generalId: 7,
|
||||||
|
generalName: '보낸이',
|
||||||
|
nationId: 0,
|
||||||
|
nationName: '재야',
|
||||||
|
color: '#000000',
|
||||||
|
icon: '',
|
||||||
|
};
|
||||||
|
const id = await persistMessageEnvelope(
|
||||||
|
transaction,
|
||||||
|
{
|
||||||
|
mailbox: 9999,
|
||||||
|
msgType: 'public',
|
||||||
|
srcId: target.generalId,
|
||||||
|
destId: 9999,
|
||||||
|
time: new Date('0200-01-01T00:00:00.000Z'),
|
||||||
|
validUntil: new Date('9999-12-31T00:00:00.000Z'),
|
||||||
|
payload: {
|
||||||
|
src: target,
|
||||||
|
dest: target,
|
||||||
|
text: '일반 메시지는 WALL_TIME envelope만 저장한다.',
|
||||||
|
option: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = await transaction.message.findUniqueOrThrow({
|
||||||
|
where: { id },
|
||||||
|
include: { action: true },
|
||||||
|
});
|
||||||
|
expect(message.createdAtWall).toBeInstanceOf(Date);
|
||||||
|
expect(message.deleteUntilWall.getTime() - message.createdAtWall.getTime()).toBe(5 * 60_000);
|
||||||
|
expect(message.occurredGameTick).toBeNull();
|
||||||
|
expect(message.action).toBeNull();
|
||||||
|
|
||||||
|
throw rollback;
|
||||||
|
})
|
||||||
|
).rejects.toBe(rollback);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps sender and receiver rows readable while replacing their bodies', async () => {
|
it('keeps sender and receiver rows readable while replacing their bodies', async () => {
|
||||||
const rollback = new Error('rollback message tombstone fixture');
|
const rollback = new Error('rollback message tombstone fixture');
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const persistMessageEnvelope = async (
|
|||||||
${gameContext?.clockRevision ?? 0n},
|
${gameContext?.clockRevision ?? 0n},
|
||||||
${gameContext?.deadlineGeneration ?? 0n}
|
${gameContext?.deadlineGeneration ?? 0n}
|
||||||
FROM inserted
|
FROM inserted
|
||||||
WHERE ${actionType} IS NOT NULL
|
WHERE CAST(${actionType} AS text) IS NOT NULL
|
||||||
RETURNING message_id
|
RETURNING message_id
|
||||||
)
|
)
|
||||||
SELECT id FROM inserted
|
SELECT id FROM inserted
|
||||||
|
|||||||
@@ -217,15 +217,15 @@ const status = async (): Promise<void> => {
|
|||||||
|
|
||||||
const prepareUsers = async (): Promise<void> => {
|
const prepareUsers = async (): Promise<void> => {
|
||||||
const state = await readState();
|
const state = await readState();
|
||||||
if (state.users?.length) throw new Error('Lifecycle users are already recorded.');
|
if ((state.users?.length ?? 0) > 10) throw new Error('Lifecycle state contains more than ten users.');
|
||||||
const browser = await chromium.launch({ headless: true });
|
const browser = await chromium.launch({ headless: true });
|
||||||
const runSlug = state.runId.replaceAll('-', '').slice(0, 8);
|
const runSlug = state.runId.replaceAll('-', '').slice(0, 8);
|
||||||
const password = `Live-${state.runId}`;
|
const password = `Live-${state.runId}`;
|
||||||
const users: NonNullable<State['users']> = [];
|
const users: NonNullable<State['users']> = [...(state.users ?? [])];
|
||||||
let browserErrors = 0;
|
let browserErrors = 0;
|
||||||
let applicationHttpErrors = 0;
|
let applicationHttpErrors = 0;
|
||||||
try {
|
try {
|
||||||
for (let index = 1; index <= 10; index += 1) {
|
for (let index = users.length + 1; index <= 10; index += 1) {
|
||||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
page.setDefaultTimeout(30_000);
|
page.setDefaultTimeout(30_000);
|
||||||
@@ -239,7 +239,7 @@ const prepareUsers = async (): Promise<void> => {
|
|||||||
});
|
});
|
||||||
const suffix = String(index).padStart(2, '0');
|
const suffix = String(index).padStart(2, '0');
|
||||||
const username = `live${runSlug}${suffix}`;
|
const username = `live${runSlug}${suffix}`;
|
||||||
const displayName = `통합사용자${suffix}`;
|
const displayName = `통합${runSlug.slice(0, 4)}${suffix}`;
|
||||||
const generalName = `통합장수${suffix}`;
|
const generalName = `통합장수${suffix}`;
|
||||||
await page.goto(`${webOrigin}/gateway/signup`, { waitUntil: 'networkidle' });
|
await page.goto(`${webOrigin}/gateway/signup`, { waitUntil: 'networkidle' });
|
||||||
await page.locator('#signup-username').fill(username);
|
await page.locator('#signup-username').fill(username);
|
||||||
@@ -249,12 +249,32 @@ const prepareUsers = async (): Promise<void> => {
|
|||||||
await page.locator('#signup-form input[type="checkbox"]').nth(0).check();
|
await page.locator('#signup-form input[type="checkbox"]').nth(0).check();
|
||||||
await page.locator('#signup-form input[type="checkbox"]').nth(1).check();
|
await page.locator('#signup-form input[type="checkbox"]').nth(1).check();
|
||||||
await page.getByRole('button', { name: '가입', exact: true }).click();
|
await page.getByRole('button', { name: '가입', exact: true }).click();
|
||||||
await page.waitForURL(/\/gateway\/lobby/u);
|
const registrationResult = await Promise.race([
|
||||||
|
page.waitForURL(/\/gateway\/lobby/u).then(() => 'registered' as const),
|
||||||
|
page
|
||||||
|
.locator('.signup-error')
|
||||||
|
.waitFor({ state: 'visible' })
|
||||||
|
.then(() => 'rejected' as const),
|
||||||
|
]);
|
||||||
|
if (registrationResult === 'rejected') {
|
||||||
|
const registrationError = (await page.locator('.signup-error').innerText()).trim();
|
||||||
|
if (!registrationError.includes('이미 사용')) {
|
||||||
|
throw new Error(`Gateway registration failed for viewer ${index}: ${registrationError}`);
|
||||||
|
}
|
||||||
|
await page.goto(`${webOrigin}/gateway/`, { waitUntil: 'networkidle' });
|
||||||
|
await page.locator('#username').fill(username);
|
||||||
|
await page.locator('#password').fill(password);
|
||||||
|
await page.getByRole('button', { name: '로그인', exact: true }).click();
|
||||||
|
await page.waitForURL(/\/gateway\/lobby/u);
|
||||||
|
}
|
||||||
const gatewayToken = await page.evaluate(() => window.localStorage.getItem('sammo-session-token'));
|
const gatewayToken = await page.evaluate(() => window.localStorage.getItem('sammo-session-token'));
|
||||||
if (!gatewayToken) throw new Error(`Gateway session was not persisted for viewer ${index}.`);
|
if (!gatewayToken) throw new Error(`Gateway session was not persisted for viewer ${index}.`);
|
||||||
|
|
||||||
const createButton = page.getByRole('button', { name: '장수생성', exact: true });
|
const createButton = page.getByRole('button', { name: '장수생성', exact: true });
|
||||||
await createButton.waitFor({ state: 'visible' });
|
await createButton.waitFor({ state: 'visible' }).catch(async () => {
|
||||||
|
await page.reload({ waitUntil: 'networkidle' });
|
||||||
|
await createButton.waitFor({ state: 'visible', timeout: 60_000 });
|
||||||
|
});
|
||||||
await createButton.click();
|
await createButton.click();
|
||||||
await page.waitForURL(/\/hwe\/join/u);
|
await page.waitForURL(/\/hwe\/join/u);
|
||||||
await page.getByLabel('장수명').fill(generalName);
|
await page.getByLabel('장수명').fill(generalName);
|
||||||
@@ -274,6 +294,8 @@ const prepareUsers = async (): Promise<void> => {
|
|||||||
await page.screenshot({ path: path.join(artifactDir, 'preopen-user-01-main.png'), fullPage: true });
|
await page.screenshot({ path: path.join(artifactDir, 'preopen-user-01-main.png'), fullPage: true });
|
||||||
}
|
}
|
||||||
users.push({ username, displayName, gatewayToken, gameToken, generalName });
|
users.push({ username, displayName, gatewayToken, gameToken, generalName });
|
||||||
|
state.users = users;
|
||||||
|
await writeState(state);
|
||||||
log('user-created', { index, username, displayName, generalName });
|
log('user-created', { index, username, displayName, generalName });
|
||||||
await context.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user