test(renown): pin the migration's backfill and constraint statement ordering

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-21 08:56:48 +02:00
parent 1a50e817f1
commit 3662029ec9
2 changed files with 28 additions and 15 deletions

View File

@@ -134,6 +134,7 @@ export class CreateRenownAndReputation1791000000000 implements MigrationInterfac
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX "IDX_turn_in_definitions_key"');
await queryRunner.query('DROP TABLE "turn_in_definitions"'); await queryRunner.query('DROP TABLE "turn_in_definitions"');
await queryRunner.query( await queryRunner.query(

View File

@@ -32,22 +32,29 @@ describe('CreateRenownAndReputation1791000000000', () => {
); );
}); });
it('clamps existing level into the 1-15 Renown range before dropping it', async () => { it('backfills renown from level BEFORE dropping the level column', async () => {
const up = await runUp(); const up = await runUp();
expect(up).toEqual( const backfillIndex = up.findIndex((sql) => sql.includes('LEAST(GREATEST("level", 1), 15)'));
expect.arrayContaining([expect.stringContaining('LEAST(GREATEST("level", 1), 15)')]), const dropLevelIndex = up.findIndex((sql) => sql.includes('DROP COLUMN "level"'));
);
expect(backfillIndex).toBeGreaterThanOrEqual(0);
expect(dropLevelIndex).toBeGreaterThanOrEqual(0);
// Reversing these two would silently discard every character's progression.
expect(backfillIndex).toBeLessThan(dropLevelIndex);
}); });
it('enforces the renown range at the database level', async () => { it('adds the renown range constraint only AFTER the backfill has populated valid values', async () => {
const up = await runUp(); const up = await runUp();
expect(up).toEqual( const backfillIndex = up.findIndex((sql) => sql.includes('LEAST(GREATEST("level", 1), 15)'));
expect.arrayContaining([ const constraintIndex = up.findIndex((sql) => sql.includes('CHK_characters_renown'));
expect.stringContaining('CHK_characters_renown'),
]), expect(backfillIndex).toBeGreaterThanOrEqual(0);
); expect(constraintIndex).toBeGreaterThanOrEqual(0);
// ADD CONSTRAINT validates the whole table; running it before the backfill
// would only pass by coincidence of the column DEFAULT being in range.
expect(backfillIndex).toBeLessThan(constraintIndex);
}); });
it('drops required_level from item_definitions', async () => { it('drops required_level from item_definitions', async () => {
@@ -125,10 +132,15 @@ describe('CreateRenownAndReputation1791000000000', () => {
expect.stringContaining('ALTER TABLE "characters" DROP COLUMN "renown"'), expect.stringContaining('ALTER TABLE "characters" DROP COLUMN "renown"'),
]), ]),
); );
// The dropped turn_in_definitions table must be the very first statement const turnInDropIndex = down.findIndex((sql) => sql.includes('DROP TABLE "turn_in_definitions"'));
// in down() -- it's the last thing up() created, and it has FK const reputationFactionsDropIndex = down.findIndex((sql) =>
// dependencies on reputation_factions/item_definitions that must be sql.includes('DROP TABLE "reputation_factions"'),
// gone before those tables can be touched. );
expect(down[0]).toContain('DROP TABLE "turn_in_definitions"');
expect(turnInDropIndex).toBeGreaterThanOrEqual(0);
expect(reputationFactionsDropIndex).toBeGreaterThanOrEqual(0);
// turn_in_definitions has FK dependencies on reputation_factions and
// item_definitions that must be gone before those tables are touched.
expect(turnInDropIndex).toBeLessThan(reputationFactionsDropIndex);
}); });
}); });