Files
yourpart3/backend/tools/characterDevelopment.test.mjs
Torsten Schulz (local) 8e8e92aa62
All checks were successful
Deploy to production / deploy (push) Successful in 2m26s
Mache Charakterentwicklungsmigration wiederholbar
2026-11-04 14:37:11 +01:00

176 lines
11 KiB
JavaScript

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('repeating the migration preserves all existing character data', async () => {
const [saved] = await db.query('SELECT * FROM falukant_data.character ORDER BY id');
await migration.up(qi);
const [repeated] = await db.query('SELECT * FROM falukant_data.character ORDER BY id');
assert.deepEqual(repeated, saved);
});
for (const bothColumns of [true, false]) {
await t.test(`manually prepared schema with ${bothColumns ? 'both columns' : 'one column'}`, async () => {
await migration.down(qi);
await db.query(`ALTER TABLE falukant_data.character ADD COLUMN puberty_start_age SMALLINT`);
if (bothColumns) {
await db.query(`ALTER TABLE falukant_data.character ADD COLUMN fertile_from_age SMALLINT;
UPDATE falukant_data.character SET fertile_from_age = 13 WHERE id = 1`);
}
await db.query(`UPDATE falukant_data.character SET puberty_start_age = 10 WHERE id = 1`);
await migration.up(qi);
const [characters] = await db.query('SELECT * FROM falukant_data.character ORDER BY id');
assert.equal(characters[0].puberty_start_age, 10);
if (bothColumns) assert.equal(characters[0].fertile_from_age, 13);
assert.deepEqual(characters[0].pregnancy_due_at, before[0].pregnancy_due_at);
assert.ok(characters.every(c => c.fertile_from_age > c.puberty_start_age));
});
}
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();
}
});