import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; import { Sequelize } from 'sequelize'; import migration from '../migrations-active/20261104000000-add-character-development-ages.cjs'; import { getCharacterDevelopment, hasReachedFertility } from '../utils/falukant/characterDevelopment.js'; test('development follows each character, independently of the marriage age', () => { const early = { pubertyStartAge: 10, fertileFromAge: 13 }; const late = { pubertyStartAge: 13, fertileFromAge: 16 }; assert.equal(hasReachedFertility(early, 12), false); assert.equal(hasReachedFertility(early, 13), true); assert.equal(hasReachedFertility(late, 14), false); assert.equal(hasReachedFertility(late, 16), true); assert.deepEqual(getCharacterDevelopment(early, 11), { ...early, hasReachedPuberty: true, hasReachedFertility: false, }); for (const missing of [null, {}, { fertileFromAge: null }, { fertileFromAge: 0 }]) { assert.equal(hasReachedFertility(missing, 30), false); } assert.equal(getCharacterDevelopment(early, null).hasReachedPuberty, false); }); // Only an explicitly provided, disposable test database may be used. // DAEMON_SQL_PATH additionally exercises the actual Rust SQL against PostgreSQL. test('database migration, persistence and daemon conception paths', { skip: !process.env.TEST_DATABASE_URL, }, async (t) => { const db = new Sequelize(process.env.TEST_DATABASE_URL, { logging: false }); try { const [[{ name }]] = await db.query('SELECT current_database() AS name'); assert.equal(name, 'yourpart_development_test', 'Use the isolated development test database'); await db.query(` DROP SCHEMA IF EXISTS falukant_data CASCADE; DROP SCHEMA IF EXISTS falukant_type CASCADE; CREATE SCHEMA falukant_data; CREATE SCHEMA falukant_type; CREATE TABLE falukant_data.character ( id SERIAL PRIMARY KEY, gender TEXT, birthdate TIMESTAMPTZ DEFAULT NOW(), user_id INT, title_of_nobility INT DEFAULT 1, last_name INT DEFAULT 1, region_id INT DEFAULT 1, health INT DEFAULT 100, pregnancy_due_at TIMESTAMPTZ, pregnancy_father_character_id INT, updated_at TIMESTAMPTZ DEFAULT NOW() ); INSERT INTO falukant_data.character (gender, pregnancy_due_at) VALUES ('female', NOW() + INTERVAL '1 hour'), ('male', NULL); `); const qi = db.getQueryInterface(); await migration.up(qi); const [before] = await db.query('SELECT * FROM falukant_data.character ORDER BY id'); assert.ok(before[0].pregnancy_due_at); assert.equal(before[0].puberty_start_age >= 8, true); assert.equal(before[0].fertile_from_age > before[0].puberty_start_age, true); await db.query("UPDATE falukant_data.character SET health = 99, puberty_start_age = NULL, fertile_from_age = NULL"); const [after] = await db.query('SELECT * FROM falukant_data.character ORDER BY id'); assert.deepEqual(after.map(c => [c.puberty_start_age, c.fertile_from_age]), before.map(c => [c.puberty_start_age, c.fertile_from_age])); assert.deepEqual(after[0].pregnancy_due_at, before[0].pregnancy_due_at); // Raw inserts represent newborns/NPCs created outside Sequelize. await db.query(`INSERT INTO falukant_data.character (gender) SELECT CASE WHEN i % 2 = 0 THEN 'female' ELSE 'male' END FROM generate_series(1, 1000) i`); const [[stats]] = await db.query(`SELECT count(DISTINCT puberty_start_age)::int AS starts, count(DISTINCT fertile_from_age)::int AS fertile, count(*) FILTER (WHERE fertile_from_age <= puberty_start_age)::int AS invalid FROM falukant_data.character`); assert.ok(stats.starts > 1 && stats.fertile > 1); assert.equal(stats.invalid, 0); await assert.rejects(db.query(`INSERT INTO falukant_data.character (puberty_start_age, fertile_from_age) VALUES (12, 11)`)); await t.test('daemon checks both parents and preserves already scheduled births', { skip: !process.env.DAEMON_SQL_PATH, }, async () => { const source = await readFile(process.env.DAEMON_SQL_PATH, 'utf8'); const sql = (name) => { const match = source.match(new RegExp(`pub const ${name}: &str = r#"([\\s\\S]*?)"#;`)); assert.ok(match, name); // Remove chance from these eligibility tests only. return match[1].replaceAll('random()', '0.0'); }; await db.query(` TRUNCATE falukant_data.character RESTART IDENTITY; CREATE TABLE falukant_type.relationship (id INT PRIMARY KEY, tr TEXT); INSERT INTO falukant_type.relationship VALUES (1, 'married'), (2, 'lover'); CREATE TABLE falukant_data.relationship ( id INT PRIMARY KEY, character1_id INT, character2_id INT, relationship_type_id INT, marriage_pregnancy_due_at TIMESTAMPTZ ); CREATE TABLE falukant_data.falukant_user (id INT PRIMARY KEY, user_id INT); CREATE TABLE falukant_data.child_relation (mother_character_id INT, created_at TIMESTAMPTZ); CREATE TABLE falukant_data.relationship_state ( relationship_id INT, active BOOLEAN, affection INT, maintenance_level INT, last_monthly_processed_at TIMESTAMPTZ, lover_role TEXT, visibility INT ); INSERT INTO falukant_data.character (gender, birthdate, puberty_start_age, fertile_from_age) VALUES ('female', CURRENT_DATE - INTERVAL '13 days', 10, 13), ('male', CURRENT_DATE - INTERVAL '13 days', 10, 13); INSERT INTO falukant_data.relationship VALUES (1, 1, 2, 1, NULL); INSERT INTO falukant_data.relationship_state VALUES (1, true, 80, 50, NOW(), 'lover', 20); `); for (const [queryName, type] of [ ['QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE', 1], ['QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES', 1], ['QUERY_GET_LOVER_PREGNANCY_CANDIDATES', 2], ]) { for (const [motherAge, fatherAge, eligible] of [[13, 13, true], [12, 13, false], [13, 12, false]]) { await db.query(`UPDATE falukant_data.character SET birthdate = CURRENT_DATE - (CASE WHEN id = 1 THEN :motherAge ELSE :fatherAge END) * INTERVAL '1 day', pregnancy_due_at = NULL; UPDATE falukant_data.relationship SET relationship_type_id = :type, marriage_pregnancy_due_at = NULL`, { replacements: { motherAge, fatherAge, type }, }); const [rows] = await db.query(sql(queryName)); if (queryName === 'QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE') { const [[relationship]] = await db.query('SELECT marriage_pregnancy_due_at FROM falukant_data.relationship'); assert.equal(relationship.marriage_pregnancy_due_at != null, eligible, `${queryName}: ${motherAge}/${fatherAge}`); } else { assert.equal(rows.length > 0, eligible, `${queryName}: ${motherAge}/${fatherAge}`); } } } await db.query(`UPDATE falukant_data.relationship SET marriage_pregnancy_due_at = NULL`); for (const [motherAge, fatherAge, eligible] of [[12, 13, false], [13, 12, false], [13, 13, true]]) { await db.query(`UPDATE falukant_data.character SET birthdate = CURRENT_DATE - (CASE WHEN id = 1 THEN :motherAge ELSE :fatherAge END) * INTERVAL '1 day', pregnancy_due_at = NULL`, { replacements: { motherAge, fatherAge } }); const [rows] = await db.query(sql('QUERY_SET_CHARACTER_PREGNANCY_FROM_LOVER'), { bind: [1, 2] }); assert.equal(rows.length > 0, eligible); } // A backfilled threshold must not cancel an existing marriage pregnancy. await db.query(`UPDATE falukant_data.character SET fertile_from_age = 17, pregnancy_due_at = NULL; UPDATE falukant_data.relationship SET relationship_type_id = 1, marriage_pregnancy_due_at = NOW() - INTERVAL '1 hour'`); const [births] = await db.query(sql('QUERY_GET_MARRIAGE_BIRTH_DELIVERIES')); assert.equal(births.length, 1); }); await migration.down(qi); const [[columns]] = await db.query(`SELECT count(*)::int AS count FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name IN ('puberty_start_age', 'fertile_from_age')`); assert.equal(columns.count, 0); } finally { await db.close(); } });