875 lines
37 KiB
TypeScript
875 lines
37 KiB
TypeScript
import { createGamePostgresConnector } from '@sammo-ts/infra';
|
|
import { describe, expect, test } from 'vitest';
|
|
import { resolveDatabaseUrl } from '../src/scenario/databaseUrl.js';
|
|
import { loadScenarioDefinitionById } from '../src/scenario/scenarioLoader.js';
|
|
import { seedScenarioToDatabase } from '../src/scenario/scenarioSeeder.js';
|
|
|
|
const scenarioId = 1010;
|
|
const schema = process.env.POSTGRES_SCHEMA ?? 'public';
|
|
process.env.POSTGRES_SCHEMA = schema;
|
|
const databaseUrl = await resolveDatabaseUrl({ schema });
|
|
|
|
type ScenarioSeederPrismaClient = {
|
|
$queryRawUnsafe(query: string): Promise<unknown>;
|
|
nation: {
|
|
count(): Promise<number>;
|
|
};
|
|
city: {
|
|
count(): Promise<number>;
|
|
findUnique(args: { where: { id: number } }): Promise<{
|
|
population: number;
|
|
agriculture: number;
|
|
commerce: number;
|
|
security: number;
|
|
trust: number;
|
|
defence: number;
|
|
wall: number;
|
|
} | null>;
|
|
};
|
|
general: {
|
|
count(): Promise<number>;
|
|
findFirst(): Promise<{ age: number; startAge: number; meta: unknown } | null>;
|
|
};
|
|
selectPoolEntry: {
|
|
count(): Promise<number>;
|
|
findFirst(args: { orderBy: { id: 'asc' | 'desc' } }): Promise<{ uniqueName: string; info: unknown } | null>;
|
|
};
|
|
diplomacy: {
|
|
count(): Promise<number>;
|
|
findFirst(args: {
|
|
where: { srcNationId: number; destNationId: number };
|
|
}): Promise<{ stateCode: number; term: number } | null>;
|
|
};
|
|
event: {
|
|
count(): Promise<number>;
|
|
};
|
|
worldState: {
|
|
findFirst(): Promise<{
|
|
config: unknown;
|
|
meta: unknown;
|
|
tickSeconds: number;
|
|
currentYear: number;
|
|
currentMonth: number;
|
|
} | null>;
|
|
};
|
|
gameHistory: {
|
|
findUnique(args: { where: { serverId: string } }): Promise<{ env: unknown } | null>;
|
|
deleteMany(args: { where: { serverId: string } }): Promise<{ count: number }>;
|
|
};
|
|
};
|
|
|
|
const requiredTables = ['world_state', 'nation', 'city', 'general', 'diplomacy', 'troop', 'event'];
|
|
|
|
const hasRequiredTables = async (prisma: ScenarioSeederPrismaClient, schemaName: string): Promise<boolean> => {
|
|
for (const table of requiredTables) {
|
|
const result = (await prisma.$queryRawUnsafe(
|
|
`SELECT to_regclass('${schemaName}.${table}')::text as regclass`
|
|
)) as Array<{ regclass: string | null }>;
|
|
if (!Array.isArray(result) || result.length === 0 || result[0]?.regclass === null) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const canConnectToDatabase = async (url: string): Promise<boolean> => {
|
|
const connector = createGamePostgresConnector({ url });
|
|
try {
|
|
await connector.connect();
|
|
const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient;
|
|
await prisma.$queryRawUnsafe('SELECT 1');
|
|
if (!(await hasRequiredTables(prisma, schema))) {
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
await connector.disconnect();
|
|
}
|
|
};
|
|
|
|
const canRun = await canConnectToDatabase(databaseUrl);
|
|
const describeDb = describe.runIf(canRun);
|
|
|
|
describeDb('scenario database seed', () => {
|
|
test('writes scenario data into tables', async () => {
|
|
const { seed } = await seedScenarioToDatabase({
|
|
scenarioId,
|
|
databaseUrl,
|
|
});
|
|
|
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
|
await connector.connect();
|
|
try {
|
|
const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient;
|
|
const worldState = await prisma.worldState.findFirst();
|
|
const [nationCount, cityCount, generalCount, diplomacyCount, eventCount] = await Promise.all([
|
|
prisma.nation.count(),
|
|
prisma.city.count(),
|
|
prisma.general.count(),
|
|
prisma.diplomacy.count(),
|
|
prisma.event.count(),
|
|
]);
|
|
|
|
expect(nationCount).toBe(seed.nations.length);
|
|
expect(cityCount).toBe(seed.cities.length);
|
|
expect(generalCount).toBe(seed.generals.length);
|
|
expect(diplomacyCount).toBe(seed.nations.length * Math.max(0, seed.nations.length - 1));
|
|
expect(eventCount).toBe(seed.events.length);
|
|
expect(worldState?.config).toMatchObject({ tournamentTrig: true });
|
|
expect(generalCount).toBeGreaterThan(0);
|
|
const seededGeneral = await prisma.general.findFirst();
|
|
expect(seededGeneral?.startAge).toBe(20);
|
|
expect(seededGeneral?.meta).toEqual(
|
|
expect.objectContaining({
|
|
specage: expect.any(Number),
|
|
specage2: expect.any(Number),
|
|
})
|
|
);
|
|
|
|
const freeCity = seed.cities.find((city) => city.nationId === 0);
|
|
const occupiedCity = seed.cities.find((city) => city.nationId !== 0);
|
|
expect(freeCity).toMatchObject({
|
|
population: freeCity ? Math.round(freeCity.populationMax * 0.7) : undefined,
|
|
agriculture: freeCity ? Math.round(freeCity.agricultureMax * 0.7) : undefined,
|
|
commerce: freeCity ? Math.round(freeCity.commerceMax * 0.7) : undefined,
|
|
security: freeCity ? Math.round(freeCity.securityMax * 0.7) : undefined,
|
|
trust: 80,
|
|
});
|
|
expect(occupiedCity).toMatchObject({
|
|
population: occupiedCity ? Math.round(occupiedCity.populationMax * 0.7) : undefined,
|
|
defence: occupiedCity ? Math.round(occupiedCity.defenceMax * 0.7) : undefined,
|
|
wall: occupiedCity ? Math.round(occupiedCity.wallMax * 0.7) : undefined,
|
|
trust: 80,
|
|
});
|
|
for (const city of [freeCity, occupiedCity]) {
|
|
expect(city).toBeDefined();
|
|
if (!city) {
|
|
continue;
|
|
}
|
|
expect(await prisma.city.findUnique({ where: { id: city.id } })).toMatchObject({
|
|
population: city.population,
|
|
agriculture: city.agriculture,
|
|
commerce: city.commerce,
|
|
security: city.security,
|
|
trust: city.trust,
|
|
defence: city.defence,
|
|
wall: city.wall,
|
|
});
|
|
}
|
|
|
|
if (seed.diplomacy.length > 0) {
|
|
const sample = seed.diplomacy[0];
|
|
const row = await prisma.diplomacy.findFirst({
|
|
where: {
|
|
srcNationId: sample.fromNationId,
|
|
destNationId: sample.toNationId,
|
|
},
|
|
});
|
|
expect(row).not.toBeNull();
|
|
if (row) {
|
|
expect(row.stateCode).toBe(sample.state);
|
|
expect(row.term).toBe(sample.durationMonths);
|
|
}
|
|
const reverse = await prisma.diplomacy.findFirst({
|
|
where: {
|
|
srcNationId: sample.toNationId,
|
|
destNationId: sample.fromNationId,
|
|
},
|
|
});
|
|
expect(reverse).not.toBeNull();
|
|
if (reverse) {
|
|
expect(reverse.stateCode).toBe(sample.state);
|
|
expect(reverse.term).toBe(sample.durationMonths);
|
|
}
|
|
}
|
|
} finally {
|
|
await connector.disconnect();
|
|
}
|
|
});
|
|
|
|
test('applies install options to world state', async () => {
|
|
const scenario = await loadScenarioDefinitionById(scenarioId);
|
|
const { seed } = await seedScenarioToDatabase({
|
|
scenarioId,
|
|
databaseUrl,
|
|
now: new Date('2030-01-01T00:00:00Z'),
|
|
installOptions: {
|
|
turnTermMinutes: 60,
|
|
sync: false,
|
|
fiction: 1,
|
|
extend: false,
|
|
blockGeneralCreate: 2,
|
|
npcMode: 0,
|
|
showImgLevel: 3,
|
|
tournamentTrig: false,
|
|
joinMode: 'full',
|
|
autorunUser: {
|
|
limitMinutes: 60,
|
|
options: {
|
|
develop: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Future-born entries are converted to delayed events, so the raw
|
|
// scenario array length is not the installed general count.
|
|
expect(seed.generals.length).toBeGreaterThan(0);
|
|
expect(seed.generals.length).toBeLessThan(
|
|
scenario.generals.length + scenario.generalsNeutral.length + scenario.generalsEx.length
|
|
);
|
|
|
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
|
await connector.connect();
|
|
try {
|
|
const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient;
|
|
const worldState = await prisma.worldState.findFirst();
|
|
expect(worldState).not.toBeNull();
|
|
if (!worldState) {
|
|
return;
|
|
}
|
|
expect(worldState.tickSeconds).toBe(3600);
|
|
expect(worldState.currentMonth).toBe(1);
|
|
|
|
const config = (worldState.config ?? {}) as Record<string, unknown>;
|
|
expect(config.extendedGeneral).toBe(false);
|
|
expect(config.joinMode).toBe('full');
|
|
expect(config.tournamentTrig).toBe(false);
|
|
|
|
const meta = (worldState.meta ?? {}) as Record<string, unknown>;
|
|
expect(meta.develcost).toBe(
|
|
(worldState.currentYear - (scenario.startYear ?? worldState.currentYear) + 10) * 2
|
|
);
|
|
expect(meta.killturn).toBe(80);
|
|
const autorun = (meta.autorun_user ?? {}) as Record<string, unknown>;
|
|
const autorunOptions = (autorun.options ?? {}) as Record<string, unknown>;
|
|
expect(autorunOptions.develop).toBe(true);
|
|
} finally {
|
|
await connector.disconnect();
|
|
}
|
|
});
|
|
|
|
test('persists a tracked scenario effect in the world configuration', async () => {
|
|
await seedScenarioToDatabase({
|
|
scenarioId: 906,
|
|
databaseUrl,
|
|
});
|
|
|
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
|
await connector.connect();
|
|
try {
|
|
const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient;
|
|
const worldState = await prisma.worldState.findFirst();
|
|
const config = (worldState?.config ?? {}) as Record<string, unknown>;
|
|
const environment = (config.environment ?? {}) as Record<string, unknown>;
|
|
expect(environment.scenarioEffect).toBe('event_StrongAttacker');
|
|
} finally {
|
|
await connector.disconnect();
|
|
}
|
|
});
|
|
|
|
test('seeds the exact scenario 903 UnderS30 selection pool', async () => {
|
|
await seedScenarioToDatabase({
|
|
scenarioId: 903,
|
|
databaseUrl,
|
|
});
|
|
|
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
|
await connector.connect();
|
|
try {
|
|
const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient;
|
|
expect(await prisma.selectPoolEntry.count()).toBe(1844);
|
|
expect(await prisma.selectPoolEntry.findFirst({ orderBy: { id: 'asc' } })).toMatchObject({
|
|
uniqueName: '⑨탈곡기',
|
|
info: {
|
|
generalName: '⑨탈곡기',
|
|
specialDomestic: 'che_event_징병',
|
|
},
|
|
});
|
|
expect(await prisma.selectPoolEntry.findFirst({ orderBy: { id: 'desc' } })).toMatchObject({
|
|
uniqueName: '④야부키 나코',
|
|
});
|
|
} finally {
|
|
await connector.disconnect();
|
|
}
|
|
});
|
|
|
|
test('clears general lifecycle rows before reusing general ids on reseed', async () => {
|
|
await seedScenarioToDatabase({
|
|
scenarioId: 903,
|
|
databaseUrl,
|
|
});
|
|
|
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
|
await connector.connect();
|
|
try {
|
|
const prisma = connector.prisma;
|
|
const generalId = 990_903;
|
|
const createSelectedGeneral = async (): Promise<void> => {
|
|
await prisma.general.create({
|
|
data: {
|
|
id: generalId,
|
|
userId: 'scenario-reseed-user',
|
|
name: '재설치선택장수',
|
|
turnTime: new Date('2026-07-30T12:00:00.000Z'),
|
|
},
|
|
});
|
|
};
|
|
const createLifecycleRows = async (): Promise<void> => {
|
|
await prisma.generalTurn.create({
|
|
data: {
|
|
generalId,
|
|
turnIdx: 0,
|
|
actionCode: '휴식',
|
|
},
|
|
});
|
|
await prisma.generalTurnRevision.create({
|
|
data: {
|
|
generalId,
|
|
revision: 7,
|
|
},
|
|
});
|
|
await prisma.rankData.create({
|
|
data: {
|
|
nationId: 0,
|
|
generalId,
|
|
type: 'experience',
|
|
value: 123,
|
|
},
|
|
});
|
|
await prisma.generalAccessLog.create({
|
|
data: {
|
|
generalId,
|
|
userId: 'scenario-reseed-user',
|
|
},
|
|
});
|
|
};
|
|
const expectLifecycleRows = async (count: number): Promise<void> => {
|
|
await expect(
|
|
Promise.all([
|
|
prisma.generalTurn.count({ where: { generalId } }),
|
|
prisma.generalTurnRevision.count({ where: { generalId } }),
|
|
prisma.rankData.count({ where: { generalId } }),
|
|
prisma.generalAccessLog.count({ where: { generalId } }),
|
|
])
|
|
).resolves.toEqual([count, count, count, count]);
|
|
};
|
|
|
|
await createSelectedGeneral();
|
|
await createLifecycleRows();
|
|
await expectLifecycleRows(1);
|
|
|
|
await seedScenarioToDatabase({
|
|
scenarioId: 903,
|
|
databaseUrl,
|
|
});
|
|
await expectLifecycleRows(0);
|
|
await expect(prisma.general.findUnique({ where: { id: generalId } })).resolves.toBeNull();
|
|
|
|
await createSelectedGeneral();
|
|
await createLifecycleRows();
|
|
await expectLifecycleRows(1);
|
|
|
|
await seedScenarioToDatabase({
|
|
scenarioId: 903,
|
|
databaseUrl,
|
|
});
|
|
await expectLifecycleRows(0);
|
|
} finally {
|
|
await connector.disconnect();
|
|
}
|
|
});
|
|
|
|
test('persists a private hidden seed without copying it into game history', async () => {
|
|
const envName = 'INTEGRATION_WORLD_SEED';
|
|
const originalSeed = process.env[envName];
|
|
const serverId = 'scenario-seeder-hidden-seed-test';
|
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
|
try {
|
|
process.env[envName] = 'scenario-seeder-explicit-hidden-seed';
|
|
await seedScenarioToDatabase({
|
|
scenarioId: 903,
|
|
databaseUrl,
|
|
installOptions: { serverId },
|
|
});
|
|
|
|
await connector.connect();
|
|
const prisma = connector.prisma as unknown as ScenarioSeederPrismaClient;
|
|
const explicitWorld = await prisma.worldState.findFirst();
|
|
expect(explicitWorld?.meta).toMatchObject({
|
|
hiddenSeed: 'scenario-seeder-explicit-hidden-seed',
|
|
});
|
|
const explicitHistory = await prisma.gameHistory.findUnique({ where: { serverId } });
|
|
expect((explicitHistory?.env as { meta?: Record<string, unknown> })?.meta).not.toHaveProperty('hiddenSeed');
|
|
|
|
delete process.env[envName];
|
|
await seedScenarioToDatabase({
|
|
scenarioId: 903,
|
|
databaseUrl,
|
|
installOptions: { serverId },
|
|
});
|
|
const randomWorld = await prisma.worldState.findFirst();
|
|
const randomSeed = (randomWorld?.meta as Record<string, unknown>)?.hiddenSeed;
|
|
expect(randomSeed).toMatch(/^[0-9a-f]{32}$/);
|
|
expect(randomSeed).not.toBe('scenario-seeder-explicit-hidden-seed');
|
|
const randomHistory = await prisma.gameHistory.findUnique({ where: { serverId } });
|
|
expect((randomHistory?.env as { meta?: Record<string, unknown> })?.meta).not.toHaveProperty('hiddenSeed');
|
|
await prisma.gameHistory.deleteMany({ where: { serverId } });
|
|
} finally {
|
|
if (originalSeed === undefined) {
|
|
delete process.env[envName];
|
|
} else {
|
|
process.env[envName] = originalSeed;
|
|
}
|
|
await connector.disconnect();
|
|
}
|
|
});
|
|
|
|
test('serializes and skips the same committed install generation inside the seed transaction', async () => {
|
|
const envName = 'INTEGRATION_WORLD_SEED';
|
|
const originalSeed = process.env[envName];
|
|
const serverId = 'scenario-seeder-idempotent-generation';
|
|
const installOperationId = 'scenario-seeder-idempotent-operation';
|
|
const installCommitSha = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
|
try {
|
|
process.env[envName] = 'scenario-seeder-idempotent-hidden-seed';
|
|
const results = await Promise.all([
|
|
seedScenarioToDatabase({
|
|
scenarioId: 1010,
|
|
databaseUrl,
|
|
installOptions: { serverId, installOperationId, installCommitSha },
|
|
}),
|
|
seedScenarioToDatabase({
|
|
scenarioId: 1010,
|
|
databaseUrl,
|
|
installOptions: { serverId, installOperationId, installCommitSha },
|
|
}),
|
|
]);
|
|
expect(results.map(({ applied }) => applied).sort()).toEqual([false, true]);
|
|
|
|
await connector.connect();
|
|
const worldBeforeMismatch = await connector.prisma.worldState.findFirstOrThrow();
|
|
expect(worldBeforeMismatch.meta).toMatchObject({
|
|
hiddenSeed: 'scenario-seeder-idempotent-hidden-seed',
|
|
installOperationId,
|
|
installCommitSha,
|
|
});
|
|
await expect(
|
|
seedScenarioToDatabase({
|
|
scenarioId: 903,
|
|
databaseUrl,
|
|
installOptions: {
|
|
serverId,
|
|
installOperationId,
|
|
installCommitSha: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
},
|
|
})
|
|
).rejects.toThrow('belongs to a different source commit');
|
|
await expect(connector.prisma.worldState.findFirstOrThrow()).resolves.toEqual(worldBeforeMismatch);
|
|
await expect(connector.prisma.gameHistory.count({ where: { serverId } })).resolves.toBe(1);
|
|
} finally {
|
|
if (originalSeed === undefined) {
|
|
delete process.env[envName];
|
|
} else {
|
|
process.env[envName] = originalSeed;
|
|
}
|
|
await connector.prisma.gameHistory.deleteMany({ where: { serverId } });
|
|
await connector.disconnect();
|
|
}
|
|
});
|
|
|
|
test('rolls back the complete season replacement when the final seed hook fails', async () => {
|
|
const envName = 'INTEGRATION_WORLD_SEED';
|
|
const originalSeed = process.env[envName];
|
|
const baselineServerId = 'scenario-seeder-rollback-baseline';
|
|
const failedServerId = 'scenario-seeder-rollback-failed';
|
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
|
try {
|
|
process.env[envName] = 'scenario-seeder-rollback-hidden-seed';
|
|
await seedScenarioToDatabase({
|
|
scenarioId: 1010,
|
|
databaseUrl,
|
|
now: new Date('2031-01-01T00:00:00.000Z'),
|
|
installOptions: {
|
|
serverId: baselineServerId,
|
|
installOperationId: 'baseline-operation',
|
|
},
|
|
});
|
|
|
|
await connector.connect();
|
|
const prisma = connector.prisma;
|
|
const readSnapshot = async () => ({
|
|
world: await prisma.worldState.findMany({ orderBy: { id: 'asc' } }),
|
|
nations: await prisma.nation.findMany({ orderBy: { id: 'asc' } }),
|
|
cities: await prisma.city.findMany({ orderBy: { id: 'asc' } }),
|
|
generals: await prisma.general.findMany({ orderBy: { id: 'asc' } }),
|
|
troops: await prisma.troop.findMany({ orderBy: { troopLeaderId: 'asc' } }),
|
|
diplomacy: await prisma.diplomacy.findMany({ orderBy: { id: 'asc' } }),
|
|
events: await prisma.event.findMany({ orderBy: { id: 'asc' } }),
|
|
history: await prisma.gameHistory.findMany({ orderBy: { id: 'asc' } }),
|
|
});
|
|
const before = await readSnapshot();
|
|
|
|
await expect(
|
|
seedScenarioToDatabase({
|
|
scenarioId: 903,
|
|
databaseUrl,
|
|
now: new Date('2032-01-01T00:00:00.000Z'),
|
|
installOptions: {
|
|
serverId: baselineServerId,
|
|
installOperationId: 'colliding-operation',
|
|
},
|
|
})
|
|
).rejects.toThrow('Game history serverId collision');
|
|
expect(await readSnapshot()).toEqual(before);
|
|
|
|
await expect(
|
|
seedScenarioToDatabase({
|
|
scenarioId: 903,
|
|
databaseUrl,
|
|
now: new Date('2032-02-02T00:00:00.000Z'),
|
|
installOptions: {
|
|
serverId: failedServerId,
|
|
installOperationId: 'failed-operation',
|
|
},
|
|
onBeforeCommit: async () => {
|
|
throw new Error('injected final seed failure');
|
|
},
|
|
})
|
|
).rejects.toThrow('injected final seed failure');
|
|
|
|
expect(await readSnapshot()).toEqual(before);
|
|
await expect(prisma.gameHistory.findUnique({ where: { serverId: failedServerId } })).resolves.toBeNull();
|
|
} finally {
|
|
if (originalSeed === undefined) {
|
|
delete process.env[envName];
|
|
} else {
|
|
process.env[envName] = originalSeed;
|
|
}
|
|
await connector.prisma.gameHistory.deleteMany({
|
|
where: { serverId: { in: [baselineServerId, failedServerId] } },
|
|
});
|
|
await connector.disconnect();
|
|
}
|
|
});
|
|
|
|
test('clears current-season services while preserving archive and diagnostic data', async () => {
|
|
const marker = 'scenario-reset-boundary';
|
|
const serverId = 'rst-boundary-server';
|
|
const connector = createGamePostgresConnector({ url: databaseUrl });
|
|
await seedScenarioToDatabase({ scenarioId: 1010, databaseUrl });
|
|
await connector.connect();
|
|
const prisma = connector.prisma;
|
|
try {
|
|
const world = await prisma.worldState.findFirstOrThrow();
|
|
const general = await prisma.general.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
|
const nation = await prisma.nation.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
|
const trafficPeriod = await prisma.trafficPeriod.create({
|
|
data: {
|
|
worldStateId: world.id,
|
|
year: 999,
|
|
month: 12,
|
|
startedAt: new Date('2033-01-01T00:00:00.000Z'),
|
|
lastRefresh: new Date('2033-01-01T00:00:00.000Z'),
|
|
},
|
|
});
|
|
await prisma.trafficPeriodGeneral.create({
|
|
data: {
|
|
periodId: trafficPeriod.id,
|
|
generalId: general.id,
|
|
refresh: 1,
|
|
lastRefresh: new Date('2033-01-01T00:00:00.000Z'),
|
|
},
|
|
});
|
|
await prisma.inputEvent.create({
|
|
data: { requestId: marker, target: 'API', eventType: marker },
|
|
});
|
|
await prisma.turnDaemonLease.create({
|
|
data: {
|
|
profile: marker,
|
|
ownerId: marker,
|
|
leaseUntil: new Date('2033-01-01T00:00:00.000Z'),
|
|
},
|
|
});
|
|
await prisma.npcSelectionToken.create({
|
|
data: {
|
|
ownerUserId: marker,
|
|
validUntil: new Date('2033-01-01T00:00:00.000Z'),
|
|
pickMoreFrom: new Date('2033-01-01T00:00:00.000Z'),
|
|
pickResult: {},
|
|
nonce: 1,
|
|
},
|
|
});
|
|
await prisma.messageReadState.create({ data: { generalId: general.id } });
|
|
await prisma.message.create({
|
|
data: {
|
|
mailbox: general.id,
|
|
type: marker,
|
|
src: general.id,
|
|
dest: general.id,
|
|
time: new Date('2033-01-01T00:00:00.000Z'),
|
|
validUntil: new Date('2033-02-01T00:00:00.000Z'),
|
|
message: { marker },
|
|
},
|
|
});
|
|
await prisma.nationTurn.create({
|
|
data: { nationId: nation.id, officerLevel: 12, turnIdx: 0, actionCode: marker },
|
|
});
|
|
await prisma.nationTurnRevision.create({
|
|
data: { nationId: nation.id, officerLevel: 12, revision: 1 },
|
|
});
|
|
await prisma.diplomacyLetter.create({
|
|
data: {
|
|
srcNationId: nation.id,
|
|
destNationId: 0,
|
|
state: 'PROPOSED',
|
|
textBrief: marker,
|
|
textDetail: marker,
|
|
srcSignerId: general.id,
|
|
},
|
|
});
|
|
const auction = await prisma.auction.create({
|
|
data: {
|
|
type: 'UNIQUE_ITEM',
|
|
targetCode: marker,
|
|
hostGeneralId: general.id,
|
|
status: 'OPEN',
|
|
closeAt: new Date('2033-01-01T00:00:00.000Z'),
|
|
},
|
|
});
|
|
await prisma.auctionBid.create({
|
|
data: {
|
|
auctionId: auction.id,
|
|
generalId: general.id,
|
|
amount: 1,
|
|
eventId: marker,
|
|
eventAt: new Date('2033-01-01T00:00:00.000Z'),
|
|
},
|
|
});
|
|
const bettingId = 990_731;
|
|
await prisma.nationBetting.create({
|
|
data: {
|
|
id: bettingId,
|
|
name: marker,
|
|
selectCount: 1,
|
|
openYearMonth: 99901,
|
|
closeYearMonth: 99902,
|
|
candidates: [{ id: nation.id }],
|
|
},
|
|
});
|
|
await prisma.nationBet.create({
|
|
data: {
|
|
bettingId,
|
|
generalId: general.id,
|
|
userId: marker,
|
|
selection: [nation.id],
|
|
selectionKey: String(nation.id),
|
|
amount: 1,
|
|
},
|
|
});
|
|
const post = await prisma.boardPost.create({
|
|
data: {
|
|
nationId: nation.id,
|
|
authorGeneralId: general.id,
|
|
authorName: marker,
|
|
title: marker,
|
|
contentHtml: marker,
|
|
},
|
|
});
|
|
await prisma.boardComment.create({
|
|
data: {
|
|
postId: post.id,
|
|
nationId: nation.id,
|
|
authorGeneralId: general.id,
|
|
authorName: marker,
|
|
contentText: marker,
|
|
},
|
|
});
|
|
const poll = await prisma.votePoll.create({
|
|
data: {
|
|
title: marker,
|
|
options: [marker],
|
|
revealMode: 'never',
|
|
openerGeneralId: general.id,
|
|
openerName: marker,
|
|
},
|
|
});
|
|
await prisma.vote.create({
|
|
data: { voteId: poll.id, generalId: general.id, nationId: nation.id, selection: [0] },
|
|
});
|
|
await prisma.voteComment.create({
|
|
data: {
|
|
voteId: poll.id,
|
|
generalId: general.id,
|
|
nationId: nation.id,
|
|
generalName: marker,
|
|
nationName: marker,
|
|
text: marker,
|
|
},
|
|
});
|
|
await prisma.logEntry.create({
|
|
data: { scope: 'SYSTEM', category: 'HISTORY', year: 999, month: 12, text: marker },
|
|
});
|
|
|
|
await prisma.errorLog.create({ data: { category: marker, message: marker } });
|
|
await prisma.inheritancePoint.create({ data: { userId: marker, key: marker, value: 1 } });
|
|
await prisma.inheritanceLog.create({
|
|
data: { userId: marker, serverId, year: 999, month: 12, text: marker },
|
|
});
|
|
await prisma.inheritanceResult.create({
|
|
data: { serverId, owner: marker, generalId: general.id, year: 999, month: 12, value: { marker } },
|
|
});
|
|
await prisma.inheritanceUserState.create({ data: { userId: marker, meta: { marker } } });
|
|
await prisma.gameHistory.create({
|
|
data: {
|
|
serverId,
|
|
date: new Date('2033-01-01T00:00:00.000Z'),
|
|
season: 1,
|
|
scenario: 1010,
|
|
scenarioName: marker,
|
|
env: { marker },
|
|
},
|
|
});
|
|
await prisma.oldNation.create({ data: { serverId, nation: 1, sourceId: 1, data: { marker } } });
|
|
await prisma.oldGeneral.create({
|
|
data: {
|
|
serverId,
|
|
generalNo: general.id,
|
|
owner: marker,
|
|
name: marker,
|
|
lastYearMonth: 99912,
|
|
turnTime: new Date('2033-01-01T00:00:00.000Z'),
|
|
data: { marker },
|
|
},
|
|
});
|
|
await prisma.emperor.create({ data: { serverId, name: marker, history: { marker }, aux: { marker } } });
|
|
await prisma.unificationFinalization.create({
|
|
data: {
|
|
generationKey: `unification:${serverId}`,
|
|
serverId,
|
|
profileName: marker,
|
|
winnerNation: 1,
|
|
year: 999,
|
|
month: 12,
|
|
completedAt: new Date('2033-01-01T00:00:00.000Z'),
|
|
},
|
|
});
|
|
await prisma.yearbookHistory.create({
|
|
data: {
|
|
profileName: marker,
|
|
year: 999,
|
|
month: 12,
|
|
map: { marker },
|
|
nations: [{ marker }],
|
|
globalHistory: [`${marker}-history`],
|
|
globalAction: [`${marker}-action`],
|
|
hash: `${marker}-hash`,
|
|
},
|
|
});
|
|
await prisma.legacyGameStorage.create({
|
|
data: { sourceId: 990_731, namespace: marker, key: marker, value: {}, scope: marker },
|
|
});
|
|
await prisma.hallOfFame.create({
|
|
data: {
|
|
serverId,
|
|
season: 1,
|
|
scenario: 1010,
|
|
generalNo: general.id,
|
|
type: 'reset-boundary',
|
|
value: 1,
|
|
},
|
|
});
|
|
|
|
await seedScenarioToDatabase({ scenarioId: 903, databaseUrl });
|
|
|
|
await expect(
|
|
Promise.all([
|
|
prisma.inputEvent.count({ where: { requestId: marker } }),
|
|
prisma.turnDaemonLease.count({ where: { profile: marker } }),
|
|
prisma.npcSelectionToken.count({ where: { ownerUserId: marker } }),
|
|
prisma.trafficPeriod.count({ where: { id: trafficPeriod.id } }),
|
|
prisma.message.count({ where: { type: marker } }),
|
|
prisma.nationTurn.count({ where: { actionCode: marker } }),
|
|
prisma.diplomacyLetter.count({ where: { textBrief: marker } }),
|
|
prisma.auction.count({ where: { targetCode: marker } }),
|
|
prisma.nationBetting.count({ where: { id: bettingId } }),
|
|
prisma.boardPost.count({ where: { title: marker } }),
|
|
prisma.votePoll.count({ where: { title: marker } }),
|
|
prisma.logEntry.count({ where: { text: marker } }),
|
|
])
|
|
).resolves.toEqual(Array.from({ length: 12 }, () => 0));
|
|
await expect(
|
|
Promise.all([
|
|
prisma.errorLog.count({ where: { category: marker } }),
|
|
prisma.inheritancePoint.count({ where: { userId: marker } }),
|
|
prisma.inheritanceLog.count({ where: { userId: marker } }),
|
|
prisma.inheritanceResult.count({ where: { owner: marker } }),
|
|
prisma.inheritanceUserState.count({ where: { userId: marker } }),
|
|
prisma.gameHistory.count({ where: { serverId } }),
|
|
prisma.oldNation.count({ where: { serverId } }),
|
|
prisma.oldGeneral.count({ where: { serverId } }),
|
|
prisma.emperor.count({ where: { serverId } }),
|
|
prisma.unificationFinalization.count({ where: { serverId } }),
|
|
prisma.yearbookHistory.count({ where: { profileName: marker } }),
|
|
prisma.legacyGameStorage.count({ where: { namespace: marker } }),
|
|
prisma.hallOfFame.count({ where: { serverId } }),
|
|
])
|
|
).resolves.toEqual(Array.from({ length: 13 }, () => 1));
|
|
await expect(
|
|
prisma.yearbookHistory.findUniqueOrThrow({
|
|
where: {
|
|
profileName_year_month_sourceId: {
|
|
profileName: marker,
|
|
year: 999,
|
|
month: 12,
|
|
sourceId: 0,
|
|
},
|
|
},
|
|
})
|
|
).resolves.toMatchObject({
|
|
map: { marker },
|
|
nations: [{ marker }],
|
|
globalHistory: [`${marker}-history`],
|
|
globalAction: [`${marker}-action`],
|
|
hash: `${marker}-hash`,
|
|
});
|
|
} finally {
|
|
await prisma.errorLog.deleteMany({ where: { category: marker } });
|
|
await prisma.inheritancePoint.deleteMany({ where: { userId: marker } });
|
|
await prisma.inheritanceLog.deleteMany({ where: { userId: marker } });
|
|
await prisma.inheritanceResult.deleteMany({ where: { owner: marker } });
|
|
await prisma.inheritanceUserState.deleteMany({ where: { userId: marker } });
|
|
await prisma.oldNation.deleteMany({ where: { serverId } });
|
|
await prisma.oldGeneral.deleteMany({ where: { serverId } });
|
|
await prisma.emperor.deleteMany({ where: { serverId } });
|
|
await prisma.unificationFinalization.deleteMany({ where: { serverId } });
|
|
await prisma.gameHistory.deleteMany({ where: { serverId } });
|
|
await prisma.yearbookHistory.deleteMany({ where: { profileName: marker } });
|
|
await prisma.legacyGameStorage.deleteMany({ where: { namespace: marker } });
|
|
await prisma.hallOfFame.deleteMany({ where: { serverId } });
|
|
await connector.disconnect();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('tracked scenario composition', () => {
|
|
test('loads the shared event and buyable unique extensions', async () => {
|
|
const standard = await loadScenarioDefinitionById(1010);
|
|
const expanded = await loadScenarioDefinitionById(2141);
|
|
const mixedConst = await loadScenarioDefinitionById(2904);
|
|
|
|
expect(standard.events).toHaveLength(1);
|
|
expect(standard.initialEvents).toHaveLength(1);
|
|
expect(expanded.events).toHaveLength(7);
|
|
expect(expanded.initialEvents).toHaveLength(1);
|
|
expect(expanded.config.const.availableSpecialWar).toHaveLength(20);
|
|
expect(Object.keys(expanded.config.const.allItems as object)).toEqual(['horse', 'weapon', 'book', 'item']);
|
|
expect(mixedConst.config.const.scenarioEffect).toBe('event_StrongAttacker');
|
|
expect(mixedConst.config.const.availableSpecialWar).toEqual(expanded.config.const.availableSpecialWar);
|
|
expect(mixedConst.config.const.allItems).toEqual(expanded.config.const.allItems);
|
|
});
|
|
});
|