Files
shd.build/database/schema.test.ts

266 lines
9.2 KiB
TypeScript

import { connect, type Database } from '@tursodatabase/database';
import { readFile } from 'node:fs/promises';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
let database: Database;
const applySchema = async (target: Database) => {
const schema = await readFile(new URL('./schema.sql', import.meta.url), 'utf8');
for (const statement of schema
.split(';')
.map((value) => value.trim())
.filter(Boolean)) {
await target.exec(statement);
}
};
const insertSource = async (
target: Database,
id: string,
ownerId: string,
fingerprint: string,
) => {
await (
await target.prepare(
`INSERT INTO mechanic_authored_sources(
id,owner_type,owner_id,template,authored_contract_version,source_fingerprint,
talent_scope,talent_has_tier_ii,created_at,updated_at
) VALUES(?,?,?,?,?,?,?,?,?,?)`,
)
).run(
id,
'talent',
ownerId,
'Gain {1}',
1,
fingerprint,
'standard',
1,
'2026-08-27T00:00:00.000Z',
'2026-08-27T00:00:00.000Z',
);
};
describe('mechanic persistence schema', () => {
beforeEach(async () => {
database = await connect(':memory:');
await database.exec('PRAGMA foreign_keys=ON');
await applySchema(database);
});
afterEach(async () => {
await database.close();
});
it('installs the complete schema with clean foreign keys', async () => {
const rows = await (
await database.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'mechanic_%' ORDER BY name",
)
).all();
expect(rows.map((row) => row.name)).toEqual([
'mechanic_authored_slots',
'mechanic_authored_sources',
'mechanic_authored_variants',
'mechanic_model_revision_clauses',
'mechanic_model_revision_outcomes',
'mechanic_model_revision_pools',
'mechanic_model_revisions',
'mechanic_model_selections',
'mechanic_public_variants',
]);
expect(await (await database.prepare('PRAGMA foreign_key_check')).all()).toEqual([]);
});
it('enforces owner metadata, ordered authored children, and source cascades', async () => {
const fingerprint = 'a'.repeat(64);
await insertSource(database, 'source-1', 'talent-1', fingerprint);
await expect(
insertSource(database, 'source-2', 'talent-1', fingerprint),
).rejects.toThrow();
await expect(
database.exec(
`INSERT INTO mechanic_authored_sources(
id,owner_type,owner_id,template,authored_contract_version,source_fingerprint,
specialization_rank_description,specialization_effect_id,specialization_effect_template,
created_at,updated_at
) VALUES('source-3','talent','talent-3','Gain',1,'${fingerprint}',
'rank','effect','template','now','now')`,
),
).rejects.toThrow();
await (
await database.prepare(
`INSERT INTO mechanic_authored_variants(source_id,variant_key,materialized,sort_order)
VALUES('source-1','I','Gain 1',0),('source-1','II','Gain 2',1)`,
)
).run();
await (
await database.prepare(
`INSERT INTO mechanic_authored_slots(
source_id,variant_key,slot_key,display_value,sort_order
) VALUES('source-1','I','1','1',0)`,
)
).run();
await expect(
database.exec(
`INSERT INTO mechanic_authored_slots(
source_id,variant_key,slot_key,display_value,sort_order
) VALUES('source-1','missing','1','1',0)`,
),
).rejects.toThrow();
await expect(
database.exec(
`INSERT INTO mechanic_authored_slots(
source_id,variant_key,slot_key,display_value,sort_order
) VALUES('source-1','II','1','10% Weapon Damage',0)`,
),
).rejects.toThrow();
await database.exec("DELETE FROM mechanic_authored_sources WHERE id='source-1'");
for (const table of [
'mechanic_authored_variants',
'mechanic_authored_slots',
]) {
expect(
await (
await database.prepare(`SELECT COUNT(*) AS count FROM ${table}`)
).get(),
).toMatchObject({ count: 0 });
}
});
it('allows deliberate duplicate graphs while proving selection ownership', async () => {
const sourceFingerprint = 'a'.repeat(64);
const programHash = 'b'.repeat(64);
await insertSource(database, 'source-1', 'talent-1', sourceFingerprint);
await (
await database.prepare(
`INSERT INTO mechanic_authored_variants(source_id,variant_key,materialized,sort_order)
VALUES('source-1','I','Gain 1',0)`,
)
).run();
await (
await database.prepare(
`INSERT INTO mechanic_model_revisions(
id,source_id,revision_number,source_fingerprint,mechanic_contract_version,
program_hash,simulation_readiness,scenario_inputs_json,source_variants_json,
program_name,origin,created_at
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`,
)
).run(
'revision-1',
'source-1',
1,
sourceFingerprint,
1,
programHash,
'ready',
'[]',
'["I"]',
'Model',
'manual',
'now',
);
await (
await database.prepare(
`INSERT INTO mechanic_model_revisions(
id,source_id,revision_number,source_fingerprint,mechanic_contract_version,
program_hash,simulation_readiness,scenario_inputs_json,source_variants_json,
program_name,origin,created_at
) SELECT 'revision-2',source_id,2,source_fingerprint,mechanic_contract_version,
program_hash,simulation_readiness,scenario_inputs_json,source_variants_json,
program_name,origin,created_at FROM mechanic_model_revisions WHERE id='revision-1'`,
)
).run();
await expect(
database.exec(
`INSERT INTO mechanic_model_revisions(
id,source_id,revision_number,source_fingerprint,mechanic_contract_version,
program_hash,simulation_readiness,scenario_inputs_json,source_variants_json,
program_name,origin,created_at
) SELECT 'revision-3',source_id,1,source_fingerprint,mechanic_contract_version,
program_hash,simulation_readiness,scenario_inputs_json,source_variants_json,
program_name,origin,created_at FROM mechanic_model_revisions WHERE id='revision-1'`,
),
).rejects.toThrow();
await (
await database.prepare(
`INSERT INTO mechanic_model_selections(source_id,revision_id,updated_at)
VALUES('source-1','revision-2','now')`,
)
).run();
await expect(
database.exec(
`INSERT INTO mechanic_model_selections(source_id,revision_id,updated_at)
VALUES('source-1','not-this-source','now')`,
),
).rejects.toThrow();
await (
await database.prepare(
`INSERT INTO mechanic_model_revision_pools(
id,revision_id,pool_key,label,state_kind,state_scope,initial_value,
minimum_value,maximum_value,best_case_value,duration_seconds,
duration_variants_json,sort_order
) VALUES('pool-1','revision-1','stacks','Stacks','stack','agent',0,0,5,5,NULL,NULL,0)`,
)
).run();
await (
await database.prepare(
`INSERT INTO mechanic_model_revision_clauses(
id,revision_id,clause_key,label,event_kind,recipe_key,conditions_json,
reducer_json,weight_expression_json,mutations_json,cooldown_seconds,
proc_chance_percent,requires_assigned_ability_active,sort_order
) VALUES('clause-1','revision-1','on-hit','On hit','weapon_hit',NULL,NULL,
'{"kind":"none"}',NULL,'[]',NULL,25,NULL,0)`,
)
).run();
await (
await database.prepare(
`INSERT INTO mechanic_model_revision_outcomes(
id,revision_id,clause_id,state_pool_id,effect_key,variant_key,label,
outcome_kind,effect_scope,value,value_expression_json,value_basis,
resource_kind,delivery_mode,delivery_duration_seconds,application_phase,
combat_timing,stat_attribute_id,per_stack,min_state_value,max_state_value,
targeting_json,probability_percent,sort_order
) VALUES('outcome-1','revision-1','clause-1','pool-1','mark','I','Mark',
'marker','self',1,NULL,'flat',NULL,'instant',NULL,'while_active','current',
NULL,0,NULL,NULL,NULL,NULL,0)`,
)
).run();
await (
await database.prepare(
`INSERT INTO mechanic_public_variants(
source_id,variant_key,selected_revision_id,current_source_fingerprint,
authored_contract_version,mechanic_contract_version,status,behavior_text,
display_source,updated_at
) VALUES('source-1','I','revision-1',?,1,1,'modeled','Modeled behavior','model','now')`,
)
).run(sourceFingerprint);
await database.exec("DELETE FROM mechanic_authored_sources WHERE id='source-1'");
for (const table of [
'mechanic_authored_variants',
'mechanic_authored_slots',
'mechanic_model_revisions',
'mechanic_model_revision_pools',
'mechanic_model_revision_clauses',
'mechanic_model_revision_outcomes',
'mechanic_model_selections',
'mechanic_public_variants',
]) {
expect(
await (
await database.prepare(`SELECT COUNT(*) AS count FROM ${table}`)
).get(),
).toMatchObject({ count: 0 });
}
});
});