merge: 최신 main을 사용자 군주 자율행동 브랜치에 통합한다
This commit is contained in:
@@ -226,8 +226,22 @@ const buildNationSnapshot = async (ctx: GameApiContext) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildLogs = async (ctx: GameApiContext, year: number, month: number) => {
|
const readGlobalActionLogs = async (ctx: GameApiContext, year: number, month: number) => {
|
||||||
const [historyLogs, actionLogs] = await Promise.all([
|
const actionLogs = await ctx.db.logEntry.findMany({
|
||||||
|
where: {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: { in: [LogCategory.SUMMARY, LogCategory.ACTION] },
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
},
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return actionLogs.map((entry) => entry.text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readLogs = async (ctx: GameApiContext, year: number, month: number) => {
|
||||||
|
const [historyLogs, globalAction] = await Promise.all([
|
||||||
ctx.db.logEntry.findMany({
|
ctx.db.logEntry.findMany({
|
||||||
where: {
|
where: {
|
||||||
scope: LogScope.SYSTEM,
|
scope: LogScope.SYSTEM,
|
||||||
@@ -237,20 +251,16 @@ const buildLogs = async (ctx: GameApiContext, year: number, month: number) => {
|
|||||||
},
|
},
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
}),
|
}),
|
||||||
ctx.db.logEntry.findMany({
|
readGlobalActionLogs(ctx, year, month),
|
||||||
where: {
|
|
||||||
scope: LogScope.SYSTEM,
|
|
||||||
category: LogCategory.ACTION,
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
},
|
|
||||||
orderBy: { id: 'desc' },
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const globalHistory = historyLogs.map((entry) => entry.text);
|
const globalHistory = historyLogs.map((entry) => entry.text);
|
||||||
const globalAction = actionLogs.map((entry) => entry.text);
|
|
||||||
|
|
||||||
|
return { globalHistory, globalAction };
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLogs = async (ctx: GameApiContext, year: number, month: number) => {
|
||||||
|
const { globalHistory, globalAction } = await readLogs(ctx, year, month);
|
||||||
return {
|
return {
|
||||||
globalHistory: globalHistory.length ? globalHistory : [`<C>●</>${month}월: 기록 없음`],
|
globalHistory: globalHistory.length ? globalHistory : [`<C>●</>${month}월: 기록 없음`],
|
||||||
globalAction: globalAction.length ? globalAction : [`<C>●</>${month}월: 기록 없음`],
|
globalAction: globalAction.length ? globalAction : [`<C>●</>${month}월: 기록 없음`],
|
||||||
@@ -387,7 +397,13 @@ export const yearbookRouter = router({
|
|||||||
const map = asRecord(row.map) as BaseMapResult;
|
const map = asRecord(row.map) as BaseMapResult;
|
||||||
const nations = parseYearbookNations(row.nations);
|
const nations = parseYearbookNations(row.nations);
|
||||||
const globalHistory = normalizeArchivedLogs(row.globalHistory, input.month);
|
const globalHistory = normalizeArchivedLogs(row.globalHistory, input.month);
|
||||||
const globalAction = normalizeArchivedLogs(row.globalAction, input.month);
|
let globalAction = normalizeArchivedLogs(row.globalAction, input.month);
|
||||||
|
if (target.isCurrentProfile) {
|
||||||
|
const liveGlobalAction = await readGlobalActionLogs(ctx, input.year, input.month);
|
||||||
|
if (liveGlobalAction.length) {
|
||||||
|
globalAction = liveGlobalAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
const data = {
|
const data = {
|
||||||
year: input.year,
|
year: input.year,
|
||||||
month: input.month,
|
month: input.month,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
|
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
|
||||||
import type { RedisConnector } from '@sammo-ts/infra';
|
import type { RedisConnector } from '@sammo-ts/infra';
|
||||||
|
import { LogCategory } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js';
|
import { RedisAccessTokenStore } from '../src/auth/accessTokenStore.js';
|
||||||
import { InMemoryFlushStore } from '../src/auth/flushStore.js';
|
import { InMemoryFlushStore } from '../src/auth/flushStore.js';
|
||||||
@@ -92,17 +93,49 @@ const authFor = (userId: string): GameSessionTokenPayload => ({
|
|||||||
|
|
||||||
const buildContext = (
|
const buildContext = (
|
||||||
auth: GameSessionTokenPayload | null,
|
auth: GameSessionTokenPayload | null,
|
||||||
options: { hasGeneral?: boolean; worldMeta?: unknown } = {}
|
options: {
|
||||||
|
hasGeneral?: boolean;
|
||||||
|
worldMeta?: unknown;
|
||||||
|
liveLogs?: { history: string[]; action: string[] };
|
||||||
|
} = {}
|
||||||
): GameApiContext => {
|
): GameApiContext => {
|
||||||
const db = {
|
const db = {
|
||||||
|
$queryRaw: async () => [],
|
||||||
general: {
|
general: {
|
||||||
findFirst: async ({ where }: { where: { userId: string } }) =>
|
findFirst: async ({ where }: { where: { userId: string } }) =>
|
||||||
options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId },
|
options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId },
|
||||||
|
findMany: async () => [],
|
||||||
|
},
|
||||||
|
city: {
|
||||||
|
findMany: async () => [],
|
||||||
|
},
|
||||||
|
nation: {
|
||||||
|
findMany: async () => [],
|
||||||
|
},
|
||||||
|
logEntry: {
|
||||||
|
findMany: async ({ where }: { where: { category: unknown } }) => {
|
||||||
|
if (where.category === LogCategory.HISTORY) {
|
||||||
|
return (options.liveLogs?.history ?? []).map((text) => ({ text }));
|
||||||
|
}
|
||||||
|
const categories =
|
||||||
|
typeof where.category === 'object' && where.category !== null && 'in' in where.category
|
||||||
|
? (where.category as { in: unknown }).in
|
||||||
|
: null;
|
||||||
|
if (
|
||||||
|
Array.isArray(categories) &&
|
||||||
|
categories.includes(LogCategory.SUMMARY) &&
|
||||||
|
categories.includes(LogCategory.ACTION)
|
||||||
|
) {
|
||||||
|
return (options.liveLogs?.action ?? []).map((text) => ({ text }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
worldState: {
|
worldState: {
|
||||||
findFirst: async () => ({
|
findFirst: async () => ({
|
||||||
currentYear: 220,
|
currentYear: 220,
|
||||||
currentMonth: 1,
|
currentMonth: 1,
|
||||||
|
config: {},
|
||||||
meta: options.worldMeta ?? { serverId: currentServerId },
|
meta: options.worldMeta ?? { serverId: currentServerId },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -247,4 +280,50 @@ describe('historical yearbook access from dynasty', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recovers an already archived current-generation month from retained summary logs', async () => {
|
||||||
|
const caller = appRouter.createCaller(
|
||||||
|
buildContext(authFor('owner-a'), {
|
||||||
|
liveLogs: {
|
||||||
|
history: ['복구한 현재 기수 과거 정세'],
|
||||||
|
action: ['복구한 현재 기수 장수 동향'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await caller.yearbook.getHistory({
|
||||||
|
serverID: currentServerId,
|
||||||
|
year: 219,
|
||||||
|
month: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
notModified: false,
|
||||||
|
data: {
|
||||||
|
globalHistory: ['저장된 현재 기수 과거 기록'],
|
||||||
|
globalAction: ['복구한 현재 기수 장수 동향'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns summary and compatible action logs as the live month general trend', async () => {
|
||||||
|
const caller = appRouter.createCaller(
|
||||||
|
buildContext(authFor('owner-a'), {
|
||||||
|
liveLogs: {
|
||||||
|
history: ['현재 천하 동향'],
|
||||||
|
action: ['최신 호환 행동', '최신 장수 동향'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await caller.yearbook.getHistory({ year: 220, month: 1 });
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
notModified: false,
|
||||||
|
data: {
|
||||||
|
globalHistory: ['현재 천하 동향'],
|
||||||
|
globalAction: ['최신 호환 행동', '최신 장수 동향'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const persistYearbookSnapshot = async (
|
|||||||
transaction.logEntry.findMany({
|
transaction.logEntry.findMany({
|
||||||
where: {
|
where: {
|
||||||
scope: LogScope.SYSTEM,
|
scope: LogScope.SYSTEM,
|
||||||
category: LogCategory.ACTION,
|
category: { in: [LogCategory.SUMMARY, LogCategory.ACTION] },
|
||||||
year: snapshot.year,
|
year: snapshot.year,
|
||||||
month: snapshot.month,
|
month: snapshot.month,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const cityIds = [991_201, 991_202, 991_203, 991_204, 991_205, 991_206, 991_207];
|
|||||||
const nationId = 991_201;
|
const nationId = 991_201;
|
||||||
const yearbookProfile = 'monthly-boundary-pre-persistence';
|
const yearbookProfile = 'monthly-boundary-pre-persistence';
|
||||||
const yearbookServerId = 'monthly-boundary-generation-20260731';
|
const yearbookServerId = 'monthly-boundary-generation-20260731';
|
||||||
const archivedLogTexts = ['월경계 과거 정세', '월경계 과거 행동'];
|
const archivedLogTexts = ['월경계 과거 정세', '월경계 과거 장수 동향', '월경계 과거 호환 행동'];
|
||||||
|
|
||||||
integration('monthly pre-update persistence', () => {
|
integration('monthly pre-update persistence', () => {
|
||||||
let db: GamePrismaClient;
|
let db: GamePrismaClient;
|
||||||
@@ -78,11 +78,18 @@ integration('monthly pre-update persistence', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
scope: LogScope.SYSTEM,
|
scope: LogScope.SYSTEM,
|
||||||
category: LogCategory.ACTION,
|
category: LogCategory.SUMMARY,
|
||||||
year: 200,
|
year: 200,
|
||||||
month: 12,
|
month: 12,
|
||||||
text: archivedLogTexts[1]!,
|
text: archivedLogTexts[1]!,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
year: 200,
|
||||||
|
month: 12,
|
||||||
|
text: archivedLogTexts[2]!,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
await db.city.createMany({
|
await db.city.createMany({
|
||||||
@@ -256,7 +263,7 @@ integration('monthly pre-update persistence', () => {
|
|||||||
);
|
);
|
||||||
expect(cityIds.map((id) => yearbookStates.get(id))).toEqual([31, 32, 33, 34, 41, 42, 43]);
|
expect(cityIds.map((id) => yearbookStates.get(id))).toEqual([31, 32, 33, 34, 41, 42, 43]);
|
||||||
expect(yearbookRow.globalHistory).toEqual([archivedLogTexts[0]]);
|
expect(yearbookRow.globalHistory).toEqual([archivedLogTexts[0]]);
|
||||||
expect(yearbookRow.globalAction).toEqual([archivedLogTexts[1]]);
|
expect(yearbookRow.globalAction).toEqual([archivedLogTexts[2], archivedLogTexts[1]]);
|
||||||
expect(await db.yearbookHistory.count({ where: { profileName: yearbookProfile } })).toBe(0);
|
expect(await db.yearbookHistory.count({ where: { profileName: yearbookProfile } })).toBe(0);
|
||||||
} finally {
|
} finally {
|
||||||
await hooks.close();
|
await hooks.close();
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type { GamePrisma } from '@sammo-ts/infra';
|
||||||
|
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
import { persistYearbookSnapshot } from '../src/turn/yearbookPersistence.js';
|
||||||
|
|
||||||
|
describe('persistYearbookSnapshot', () => {
|
||||||
|
it('archives canonical summary logs and compatible action logs together in descending ID order', async () => {
|
||||||
|
const findMany = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce([{ text: '천하 동향' }])
|
||||||
|
.mockResolvedValueOnce([{ text: '호환 행동' }, { text: '장수 동향' }]);
|
||||||
|
const upsert = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const transaction = {
|
||||||
|
logEntry: { findMany },
|
||||||
|
yearbookHistory: { upsert },
|
||||||
|
} as unknown as GamePrisma.TransactionClient;
|
||||||
|
|
||||||
|
await persistYearbookSnapshot(transaction, {
|
||||||
|
serverId: 'hwe:generation',
|
||||||
|
sourceId: 0,
|
||||||
|
year: 195,
|
||||||
|
month: 1,
|
||||||
|
map: { year: 195, month: 1 },
|
||||||
|
nations: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findMany).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
expect.objectContaining({
|
||||||
|
where: {
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: { in: [LogCategory.SUMMARY, LogCategory.ACTION] },
|
||||||
|
year: 195,
|
||||||
|
month: 1,
|
||||||
|
},
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(upsert).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
create: expect.objectContaining({ globalAction: ['호환 행동', '장수 동향'] }),
|
||||||
|
update: expect.objectContaining({ globalAction: ['호환 행동', '장수 동향'] }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user