fix: 특수 유저 커맨드의 Ref 호환 경계를 보강한다

수뇌 국가 설정과 NPC 정책을 actor-bound ENGINE mutation으로 옮기고, 추방·등용·점령·멸망·아이템 폐기의 특수 분기를 Ref와 맞춘다.

요청 ID를 사용자·프로필별로 격리하고 토너먼트 손상 projection을 fail-closed하며 실제 DB 및 Ref 차등 회귀를 보강한다.
This commit is contained in:
2026-08-24 12:10:57 +00:00
parent 5389f8ed94
commit 630bc29100
74 changed files with 3893 additions and 1141 deletions
@@ -595,6 +595,7 @@ describe('in-game my information ownership', () => {
await caller.general.setMySetting({ tnmt: 1, defence_train: 999 });
expect(requestCommand).toHaveBeenCalledWith({
type: 'setMySetting',
userId: 'user-7',
generalId: 7,
settings: { tnmt: 1, defence_train: 999 },
});
@@ -622,18 +623,51 @@ describe('in-game my information ownership', () => {
requestCommand,
});
await expect(
appRouter.createCaller(fixture.context).general.setMySetting({ tnmt: 1 })
).resolves.toEqual({ ok: true });
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',
userId: 'user-7',
generalId: 7,
settings: { tnmt: 1 },
});
});
it.each(['horse', 'weapon', 'book', 'item'] as const)(
'dispatches the authenticated dropItem command for the %s slot',
async (itemType) => {
const requestCommand = vi.fn(async () => ({
type: 'dropItem' as const,
ok: true as const,
generalId: 7,
}));
const fixture = createContext({ requestCommand });
await expect(appRouter.createCaller(fixture.context).general.dropItem({ itemType })).resolves.toEqual({
ok: true,
});
expect(requestCommand).toHaveBeenCalledWith({
type: 'dropItem',
userId: 'user-7',
generalId: 7,
itemType,
});
}
);
it('rejects an unknown dropItem slot before dispatching it to ENGINE', async () => {
const requestCommand = vi.fn(async () => ({ type: 'dropItem' as const, ok: true as const, generalId: 7 }));
const fixture = createContext({ requestCommand });
await expect(
appRouter.createCaller(fixture.context).general.dropItem({ itemType: 'armor' as never })
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
expect(requestCommand).not.toHaveBeenCalled();
});
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] });