Backend CI / type-check, build, tests (push) Successful in 1m0s
Co-authored-by: Cursor <cursoragent@cursor.com>
98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
/**
|
|
* Two independent SQLite connections on one file — one writer wins, no corrupt cash.
|
|
* Run: npm run test:dual-connection
|
|
*/
|
|
import assert from 'node:assert/strict'
|
|
import { mkdtempSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { Worker } from 'node:worker_threads'
|
|
import Database from 'better-sqlite3'
|
|
import { runSchemaMigrations } from '../src/database/migration.service'
|
|
import { createDefaultUserState } from '../src/shared/catalog/default-user-state'
|
|
|
|
const testDir = mkdtempSync(join(tmpdir(), 'gold-land-dual-sqlite-'))
|
|
const dbPath = join(testDir, 'dual.db')
|
|
const userId = 'dual-user'
|
|
|
|
function seedUser(db: Database.Database) {
|
|
const state = createDefaultUserState(userId)
|
|
state.cash = 1000
|
|
state.goldGrams = 0
|
|
db.prepare('INSERT INTO users (id, state_json, updated_at) VALUES (?, ?, ?)').run(
|
|
userId,
|
|
JSON.stringify(state),
|
|
new Date().toISOString(),
|
|
)
|
|
}
|
|
|
|
function readCash(): number {
|
|
const db = new Database(dbPath, { readonly: true })
|
|
const row = db.prepare('SELECT state_json FROM users WHERE id = ?').get(userId) as {
|
|
state_json: string
|
|
}
|
|
db.close()
|
|
return (JSON.parse(row.state_json) as { cash: number }).cash
|
|
}
|
|
|
|
function spendInWorker(amount: number): Promise<'ok' | 'fail'> {
|
|
return new Promise((resolve, reject) => {
|
|
const worker = new Worker(
|
|
`
|
|
const Database = require('better-sqlite3');
|
|
const { parentPort, workerData } = require('worker_threads');
|
|
const db = new Database(workerData.dbPath);
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('busy_timeout = 5000');
|
|
const spend = db.transaction(() => {
|
|
const row = db.prepare('SELECT state_json FROM users WHERE id = ?').get(workerData.userId);
|
|
const state = JSON.parse(row.state_json);
|
|
if (state.cash < workerData.amount) return false;
|
|
state.cash -= workerData.amount;
|
|
db.prepare('UPDATE users SET state_json = ?, updated_at = ? WHERE id = ?').run(
|
|
JSON.stringify(state),
|
|
new Date().toISOString(),
|
|
workerData.userId,
|
|
);
|
|
return true;
|
|
}).immediate;
|
|
parentPort.postMessage(spend() ? 'ok' : 'fail');
|
|
db.close();
|
|
`,
|
|
{ eval: true, workerData: { dbPath, userId, amount } },
|
|
)
|
|
worker.on('message', (msg) => resolve(msg as 'ok' | 'fail'))
|
|
worker.on('error', reject)
|
|
})
|
|
}
|
|
|
|
async function runTests() {
|
|
const bootstrap = new Database(dbPath)
|
|
bootstrap.pragma('journal_mode = WAL')
|
|
runSchemaMigrations(bootstrap)
|
|
seedUser(bootstrap)
|
|
bootstrap.close()
|
|
|
|
const [a, b] = await Promise.all([spendInWorker(700), spendInWorker(700)])
|
|
|
|
const outcomes = [a, b]
|
|
assert.equal(outcomes.filter((x) => x === 'ok').length, 1)
|
|
assert.equal(outcomes.filter((x) => x === 'fail').length, 1)
|
|
|
|
const finalCash = readCash()
|
|
assert.equal(finalCash, 300)
|
|
assert.ok(finalCash >= 0)
|
|
|
|
try {
|
|
rmSync(testDir, { recursive: true, force: true })
|
|
} catch {
|
|
// ignore
|
|
}
|
|
console.log('dual-connection tests passed')
|
|
}
|
|
|
|
runTests().catch((error) => {
|
|
console.error(error)
|
|
process.exit(1)
|
|
})
|