test: 测试矩阵扩至 161 项(深度审计 + 5 处缺陷修复)
新增测试矩阵(12 文件 / 161 cases): - rng 12 / data 18 / pcgen+cultivation 20 / economy 12 / combat+missions 24 - marriage+diplomacy 13 / events 12 / world+lifecycle 8(含镜像一致)/ save 4+5 / audit 8 审计带出并修复的真实缺陷: 1. Rng.pick 空数组未报错(防空白数组进而崩溃难查) 2. combatPowerOf 未应用功法悟道品阶战力加成(0.1.2 设计缺口) 3. 重伤者即使不代表伤状态仍按满速修炼(health 进修炼公式) 4. 寡妇/鳏夫无法再婚:候选列表放着人执行层却拒婚(spouseId 死旧契约) 5. isWidowed 参数契约陷阱:传 id 静默 false(升级接受 Character|Id 双签名)
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SaveSlot, SaveDbDriver } from '../src/renderer/game/storage/slots'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
|
||||
interface Row {
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
class MemoryDriver implements SaveDbDriver {
|
||||
tables = new Map<string, Map<string, Row>>()
|
||||
|
||||
async open(_name: string): Promise<void> {}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
|
||||
async run(sql: string, params: unknown[] = []): Promise<unknown> {
|
||||
const ins = /insert into (\w+)\s*\(([^)]+)\)\s*values\s*\(([^)]+)\)/i.exec(sql)
|
||||
if (ins) {
|
||||
const cols = ins[2].split(',').map((c) => c.trim())
|
||||
const row: Row = {}
|
||||
cols.forEach((c, i) => (row[c] = params[i]))
|
||||
const t = this.tables.get(ins[1]) ?? new Map<string, Row>()
|
||||
t.set(String(row['id'] ?? ins[1] + t.size), row)
|
||||
this.tables.set(ins[1], t)
|
||||
}
|
||||
const upd = /update\s+(\w+)\s+set/i.exec(sql)
|
||||
if (upd) {
|
||||
const setCol = /set\s+(\w+)\s*=\s*\?/i.exec(sql)![1]
|
||||
const setVal = params[0]
|
||||
const lit = /\bwhere\s+(\w+)\s*=\s*'([^']+)'/i.exec(sql)
|
||||
const ph = /where\s+(\w+)\s*=\s*\?/i.exec(sql)
|
||||
const col = (lit?.[1] ?? ph?.[1]) as string
|
||||
const val = (lit?.[2] as string | undefined) ?? String(params[1])
|
||||
const t = this.tables.get(upd[1])
|
||||
if (t) {
|
||||
for (const [k, row] of t.entries()) {
|
||||
if (String(row[col]) === val) t.set(k, { ...row, [setCol]: setVal })
|
||||
}
|
||||
}
|
||||
}
|
||||
const del = /delete from (\w+)\s+where\s+(\w+)\s*=\s*\?/i.exec(sql)
|
||||
if (del) {
|
||||
const t = this.tables.get(del[1])
|
||||
if (t) {
|
||||
for (const [k, row] of t.entries()) {
|
||||
if (String(row[del[2]]) === String(params[0])) t.delete(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async all<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
||||
const lower = sql.toLowerCase()
|
||||
const table = /from (\w+)/i.exec(sql)?.[1]
|
||||
if (!table) return []
|
||||
let rows = [...(this.tables.get(table)?.values() ?? [])].map((r) => ({ ...r }))
|
||||
if (/\bwhere\b/.test(lower)) {
|
||||
const m = /where\s+(\w+)\s*=\s*\?/i.exec(sql)
|
||||
if (m && params[0] !== undefined) {
|
||||
rows = rows.filter((r) => String(r[m[1]]) === String(params[0]))
|
||||
}
|
||||
}
|
||||
if (/\border by/i.test(lower)) {
|
||||
rows = rows.sort((a, b) => Number(b['year']) - Number(a['year']) || Number(b['month']) - Number(a['month']))
|
||||
}
|
||||
if (/select data/i.test(lower)) rows = rows.map((r) => ({ data: String(r['data']) }))
|
||||
if (/select value/i.test(lower)) rows = rows.map((r) => ({ value: String(r['value']) }))
|
||||
return rows as T[]
|
||||
}
|
||||
|
||||
async exec(sql: string): Promise<unknown> {
|
||||
const create = /create table (?:if not exists )?(\w+)/i.exec(sql)
|
||||
if (create && !this.tables.has(create[1])) this.tables.set(create[1], new Map())
|
||||
const del = /delete from (\w+)/i.exec(sql)
|
||||
if (del) this.tables.set(del[1], new Map())
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function world(seed: string) {
|
||||
return World.create({ seed, surname: '楚', familyName: '楚家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('SaveSlot 存储扩展', () => {
|
||||
it('snapshot 保留最近 12 份并淘汰最旧', async () => {
|
||||
const driver = new MemoryDriver()
|
||||
const file = new SaveSlot(9, driver)
|
||||
const w = world('st-9')
|
||||
for (let i = 0; i < 20; i++) {
|
||||
w.state.year = i + 1
|
||||
await file.saveState(w.state, 'x')
|
||||
}
|
||||
const snaps = await file.listSnapshots()
|
||||
expect(snaps.length).toBe(12)
|
||||
expect(snaps[0].year).toBe(20)
|
||||
})
|
||||
|
||||
it('wipe 后库中没有快照与 meta', async () => {
|
||||
const driver = new MemoryDriver()
|
||||
const file = new SaveSlot(10, driver)
|
||||
await file.saveState(world('st-10').state, 'x')
|
||||
await file.wipe()
|
||||
const state = await file.loadState()
|
||||
expect(state).toBeNull()
|
||||
})
|
||||
|
||||
it('非法 json 导入返回 false 且原数据不动', async () => {
|
||||
const driver = new MemoryDriver()
|
||||
const file = new SaveSlot(11, driver)
|
||||
await file.saveState(world('st-11').state, 'x')
|
||||
const ok = await file.importAll('not json at all')
|
||||
expect(ok).toBe(false)
|
||||
const still = await file.loadState()
|
||||
expect(still).toBeTruthy()
|
||||
})
|
||||
|
||||
it('export→import 完全恢复(含 rng 状态推进一致)', async () => {
|
||||
const driver = new MemoryDriver()
|
||||
const file = new SaveSlot(12, driver)
|
||||
const w = world('st-12')
|
||||
for (let i = 0; i < 10; i++) w.advanceMonth()
|
||||
w.syncRng()
|
||||
await file.saveState(w.state, 'y')
|
||||
const json = await file.exportAll()
|
||||
const d2 = new MemoryDriver()
|
||||
const f2 = new SaveSlot(12, d2)
|
||||
await f2.importAll(json)
|
||||
const restored = await f2.loadState()
|
||||
expect(restored?.totalTicks).toBe(10)
|
||||
// 此后推进与原始世界分叉前提一致(同 rng 同初始 = 同序列)
|
||||
const w2 = new World(restored!)
|
||||
w.advanceMonth()
|
||||
w2.advanceMonth()
|
||||
expect(w2.state.family.stones).toBe(w.state.family.stones)
|
||||
})
|
||||
})
|
||||
|
||||
// 修正语法:listSnapshots async
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user