perf: 데몬 소유 명령의 중첩 transaction을 제거
This commit is contained in:
@@ -78,6 +78,8 @@ const buildContext = (options: {
|
||||
queryRaw?: (query: GamePrisma.Sql) => Promise<unknown>;
|
||||
isUnited?: number;
|
||||
isunited?: number;
|
||||
requestId?: string;
|
||||
transaction?: ReturnType<typeof vi.fn>;
|
||||
}) => {
|
||||
const auth = options.auth === undefined ? buildAuth() : options.auth;
|
||||
const general = options.general === undefined ? buildGeneral() : options.general;
|
||||
@@ -114,6 +116,7 @@ const buildContext = (options: {
|
||||
updatedAt: new Date('2026-07-26T00:00:00Z'),
|
||||
};
|
||||
const db = {
|
||||
...(options.transaction ? { $transaction: options.transaction } : {}),
|
||||
$queryRaw: queryRaw,
|
||||
general: {
|
||||
findFirst: vi.fn(async ({ where }: { where: { userId: string } }) =>
|
||||
@@ -154,6 +157,7 @@ const buildContext = (options: {
|
||||
battleSim: {} as GameApiContext['battleSim'],
|
||||
profile: { id: 'che', scenario: 'default', name: 'che:default' },
|
||||
auth,
|
||||
...(options.requestId ? { requestId: options.requestId } : {}),
|
||||
uploadDir: 'uploads',
|
||||
uploadPath: '/uploads',
|
||||
uploadPublicUrl: null,
|
||||
@@ -221,6 +225,34 @@ describe('auction router actor and permission boundaries', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('opens an auction without an API input-event transaction and preserves the ENGINE request identity', async () => {
|
||||
const transaction = vi.fn(async () => {
|
||||
throw new Error('API transaction must not run');
|
||||
});
|
||||
const fixture = buildContext({ requestId: 'http-auction-open', transaction });
|
||||
|
||||
await expect(
|
||||
appRouter.createCaller(fixture.context).auction.openBuyRice({
|
||||
amount: 1000,
|
||||
closeTurnCnt: 3,
|
||||
startBidAmount: 500,
|
||||
finishBidAmount: 2000,
|
||||
})
|
||||
).resolves.toMatchObject({ auctionId: 91 });
|
||||
|
||||
expect(transaction).not.toHaveBeenCalled();
|
||||
expect(fixture.requestCommand).toHaveBeenCalledWith({
|
||||
type: 'auctionOpen',
|
||||
requestId: 'http-auction-open:auction.openBuyRice:engine:0:auctionOpen',
|
||||
auctionType: 'BUY_RICE',
|
||||
generalId: 7,
|
||||
amount: 1000,
|
||||
closeTurnCnt: 3,
|
||||
startBidAmount: 500,
|
||||
finishBidAmount: 2000,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects auction mutations after unification before sending a daemon command', async () => {
|
||||
const fixture = buildContext({ isUnited: 0, isunited: 2 });
|
||||
const caller = appRouter.createCaller(fixture.context);
|
||||
|
||||
@@ -85,6 +85,8 @@ const createContext = (options: {
|
||||
troopLeaderAction?: string | null;
|
||||
refreshScore?: number;
|
||||
refreshScoreTotal?: number;
|
||||
requestId?: string;
|
||||
transaction?: ReturnType<typeof vi.fn>;
|
||||
}) => {
|
||||
const me = options.me === undefined ? buildGeneral() : options.me;
|
||||
const targets = options.targets ?? (me ? [me] : []);
|
||||
@@ -94,6 +96,7 @@ const createContext = (options: {
|
||||
async ({ where }: { where: { id: number } }) => targets.find((general) => general.id === where.id) ?? null
|
||||
);
|
||||
const db = {
|
||||
...(options.transaction ? { $transaction: options.transaction } : {}),
|
||||
general: {
|
||||
findFirst: vi.fn(async () => me),
|
||||
findUnique: generalFindUnique,
|
||||
@@ -190,6 +193,7 @@ const createContext = (options: {
|
||||
battleSim: {} as GameApiContext['battleSim'],
|
||||
profile: { id: 'che', scenario: 'default', name: 'che:default' },
|
||||
auth,
|
||||
...(options.requestId ? { requestId: options.requestId } : {}),
|
||||
uploadDir: 'uploads',
|
||||
uploadPath: '/uploads',
|
||||
uploadPublicUrl: null,
|
||||
@@ -446,6 +450,29 @@ describe('in-game my information ownership', () => {
|
||||
expect(fixture.db.general.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends settings directly to ENGINE without creating an API input event', async () => {
|
||||
const transaction = vi.fn(async () => {
|
||||
throw new Error('API transaction must not run');
|
||||
});
|
||||
const requestCommand = vi.fn(async () => ({ type: 'setMySetting', ok: true, generalId: 7 }));
|
||||
const fixture = createContext({
|
||||
requestId: 'http-general-setting',
|
||||
transaction,
|
||||
requestCommand,
|
||||
});
|
||||
|
||||
await expect(
|
||||
appRouter.createCaller(fixture.context).general.setMySetting({ tnmt: 1 })
|
||||
).resolves.toEqual({ ok: true });
|
||||
expect(transaction).not.toHaveBeenCalled();
|
||||
expect(requestCommand).toHaveBeenCalledWith({
|
||||
type: 'setMySetting',
|
||||
requestId: 'http-general-setting:general.setMySetting:engine:0:setMySetting',
|
||||
generalId: 7,
|
||||
settings: { tnmt: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the authenticated user for both the page and its logs without accepting a target general id', async () => {
|
||||
const otherUser = buildGeneral({ id: 8, userId: 'user-8', name: '타유저' });
|
||||
const fixture = createContext({ targets: [buildGeneral(), otherUser] });
|
||||
|
||||
@@ -68,11 +68,14 @@ const createContext = (
|
||||
me?: GeneralRow;
|
||||
db?: Record<string, unknown>;
|
||||
requestCommand?: ReturnType<typeof vi.fn>;
|
||||
requestId?: string;
|
||||
transaction?: ReturnType<typeof vi.fn>;
|
||||
} = {}
|
||||
): GameApiContext => {
|
||||
const requestCommand = options.requestCommand ?? vi.fn();
|
||||
const redisClient = { get: async () => null, set: async () => null };
|
||||
const db = {
|
||||
...(options.transaction ? { $transaction: options.transaction } : {}),
|
||||
general: { findFirst: vi.fn(async () => options.me ?? baseGeneral) },
|
||||
...options.db,
|
||||
};
|
||||
@@ -83,6 +86,7 @@ const createContext = (
|
||||
battleSim: {} as GameApiContext['battleSim'],
|
||||
profile: { id: 'che', scenario: 'default', name: 'che:default' },
|
||||
auth,
|
||||
...(options.requestId ? { requestId: options.requestId } : {}),
|
||||
uploadDir: 'uploads',
|
||||
uploadPath: '/uploads',
|
||||
uploadPublicUrl: null,
|
||||
@@ -139,6 +143,25 @@ describe('nation personnel router', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps nation personnel commands out of the API transaction and gives ENGINE a stable request id', async () => {
|
||||
const transaction = vi.fn(async () => {
|
||||
throw new Error('API transaction must not run');
|
||||
});
|
||||
const requestCommand = vi.fn(async () => ({ type: 'kick', ok: true, generalId: 22 }));
|
||||
const caller = appRouter.createCaller(
|
||||
createContext({ requestId: 'http-nation-kick', transaction, requestCommand })
|
||||
);
|
||||
|
||||
await expect(caller.nation.kick({ destGeneralId: 8 })).resolves.toEqual({ ok: true });
|
||||
expect(transaction).not.toHaveBeenCalled();
|
||||
expect(requestCommand).toHaveBeenCalledWith({
|
||||
type: 'kick',
|
||||
requestId: 'http-nation-kick:nation.kick:engine:0:kick',
|
||||
generalId: 22,
|
||||
destGeneralId: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects oversized and duplicate permission selections before daemon dispatch', async () => {
|
||||
const requestCommand = vi.fn();
|
||||
const caller = appRouter.createCaller(createContext({ requestCommand }));
|
||||
|
||||
@@ -75,11 +75,14 @@ const buildContext = (options: {
|
||||
troop?: { troopLeaderId: number; nationId: number; name: string } | null;
|
||||
nationMeta?: Record<string, unknown>;
|
||||
auth?: GameSessionTokenPayload | null;
|
||||
requestId?: string;
|
||||
transaction?: ReturnType<typeof vi.fn>;
|
||||
result: Awaited<ReturnType<TurnDaemonTransport['requestCommand']>>;
|
||||
}) => {
|
||||
const me = options.me ?? buildGeneral();
|
||||
const requestCommand = vi.fn(async () => options.result);
|
||||
const db = {
|
||||
...(options.transaction ? { $transaction: options.transaction } : {}),
|
||||
general: {
|
||||
findFirst: vi.fn(async ({ where }: { where: { userId: string } }) =>
|
||||
me.userId === where.userId ? me : null
|
||||
@@ -123,6 +126,7 @@ const buildContext = (options: {
|
||||
battleSim: {} as GameApiContext['battleSim'],
|
||||
profile: { id: 'che', scenario: 'default', name: 'che:default' },
|
||||
auth: options.auth === undefined ? auth : options.auth,
|
||||
...(options.requestId ? { requestId: options.requestId } : {}),
|
||||
uploadDir: 'uploads',
|
||||
uploadPath: '/uploads',
|
||||
uploadPublicUrl: null,
|
||||
@@ -189,6 +193,28 @@ describe('troop router permissions and mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches a stable ENGINE request without opening an API input-event transaction', async () => {
|
||||
const transaction = vi.fn(async () => {
|
||||
throw new Error('API transaction must not run');
|
||||
});
|
||||
const { context, requestCommand } = buildContext({
|
||||
requestId: 'http-troop-create',
|
||||
transaction,
|
||||
result: { type: 'troopCreate', ok: true, generalId: 1, troopId: 1, troopName: '백마대' },
|
||||
});
|
||||
|
||||
await expect(appRouter.createCaller(context).troop.create({ troopName: '백마대' })).resolves.toMatchObject({
|
||||
ok: true,
|
||||
});
|
||||
expect(transaction).not.toHaveBeenCalled();
|
||||
expect(requestCommand).toHaveBeenCalledWith({
|
||||
type: 'troopCreate',
|
||||
requestId: 'http-troop-create:troop.create:engine:0:troopCreate',
|
||||
generalId: 1,
|
||||
troopName: '백마대',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects troop creation before daemon dispatch when already assigned or the name is blank', async () => {
|
||||
const assigned = buildContext({
|
||||
me: buildGeneral({ troopId: 9 }),
|
||||
|
||||
Reference in New Issue
Block a user