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; nation: { count(): Promise; }; city: { count(): Promise; findUnique(args: { where: { id: number } }): Promise<{ population: number; agriculture: number; commerce: number; security: number; trust: number; defence: number; wall: number; } | null>; }; general: { count(): Promise; findFirst(): Promise<{ age: number; startAge: number; meta: unknown } | null>; }; diplomacy: { count(): Promise; findFirst(args: { where: { srcNationId: number; destNationId: number }; }): Promise<{ stateCode: number; term: number } | null>; }; event: { count(): Promise; }; worldState: { findFirst(): Promise<{ config: unknown; meta: unknown; tickSeconds: number; currentYear: number; currentMonth: number; } | null>; }; }; const requiredTables = [ 'world_state', 'nation', 'city', 'general', 'diplomacy', 'troop', 'event', ]; const hasRequiredTables = async (prisma: ScenarioSeederPrismaClient, schemaName: string): Promise => { 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 => { 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 [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(generalCount).toBeGreaterThan(0); const seededGeneral = await prisma.general.findFirst(); expect(seededGeneral?.startAge).toBe(seededGeneral?.age); 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: true, 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; expect(config.extendedGeneral).toBe(false); expect(config.joinMode).toBe('full'); const meta = (worldState.meta ?? {}) as Record; const autorun = (meta.autorun_user ?? {}) as Record; const autorunOptions = (autorun.options ?? {}) as Record; expect(autorunOptions.develop).toBe(true); } finally { await connector.disconnect(); } }); });