feat(config-editor): TsConfigFile load/read/patch/add/delete/save (entry-level AST patch, .bak + syntax check)
26 IO tests green: serializer, parser (speed/enumRef/raw), TsConfigFile round-trip (load/patch/add/delete/save preserving symbolic expressions).
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, copyFileSync, mkdtempSync, readdirSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { TsConfigFile } from '../src/io/TsConfigFile';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const fixture = join(here, 'fixtures', 'heroSet.sample.ts');
|
||||
|
||||
test('load + getKeys', () => {
|
||||
const f = new TsConfigFile(fixture, 'HeroInfo');
|
||||
f.load();
|
||||
assert.deepEqual(f.getKeys(), ['5011', '6001']);
|
||||
});
|
||||
test('read returns structured value with speed/enumRef preserved', () => {
|
||||
const f = new TsConfigFile(fixture, 'HeroInfo');
|
||||
f.load();
|
||||
const v = f.read('5011')!;
|
||||
assert.equal(v.kind, 'obj');
|
||||
assert.deepEqual(v.props['fac'], { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' });
|
||||
const skill = v.props['skills'].props['6001'];
|
||||
assert.deepEqual(skill.props['cd'], { kind: 'speed', level: 'Slow3' });
|
||||
});
|
||||
test('getText after load equals original file (no mutation on load)', () => {
|
||||
const f = new TsConfigFile(fixture, 'HeroInfo');
|
||||
f.load();
|
||||
assert.equal(f.getText(), readFileSync(fixture, 'utf8'));
|
||||
});
|
||||
test('read missing key returns null', () => {
|
||||
const f = new TsConfigFile(fixture, 'HeroInfo');
|
||||
f.load();
|
||||
assert.equal(f.read('9999'), null);
|
||||
});
|
||||
|
||||
function withTempFixture(): { dir: string; file: string } {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'phcfg-'));
|
||||
const file = join(dir, 'heroSet.sample.ts');
|
||||
copyFileSync(fixture, file);
|
||||
return { dir, file };
|
||||
}
|
||||
|
||||
test('patch updates one entry; reload reads new value; other entries intact', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
const v = f.read('5011')!;
|
||||
v.props['ap'] = { kind: 'num', value: 99 };
|
||||
f.patch('5011', v);
|
||||
assert.equal(f.isDirty(), true);
|
||||
assert.equal(f.save().ok, true);
|
||||
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
|
||||
assert.deepEqual(f2.read('5011')!.props['ap'], { kind: 'num', value: 99 });
|
||||
assert.equal(f2.read('6001')!.props['name'].value, '兽人战士'); // 另一条未破坏
|
||||
});
|
||||
|
||||
test('patch preserves speed + enumRef on reload', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
const v = f.read('5011')!;
|
||||
v.props['hp'] = { kind: 'num', value: 500 };
|
||||
f.patch('5011', v); f.save();
|
||||
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
|
||||
const again = f2.read('5011')!;
|
||||
assert.deepEqual(again.props['fac'], { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' });
|
||||
assert.deepEqual(again.props['skills'].props['6001'].props['cd'], { kind: 'speed', level: 'Slow3' });
|
||||
});
|
||||
|
||||
test('add appends entry readable after save+reload', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
f.add('5099', { kind: 'obj', props: { uuid: { kind: 'num', value: 5099 }, name: { kind: 'str', value: '新英雄' } } });
|
||||
f.save();
|
||||
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
|
||||
assert.ok(f2.getKeys().includes('5099'));
|
||||
assert.equal(f2.read('5099')!.props['name'].value, '新英雄');
|
||||
});
|
||||
|
||||
test('delete removes entry', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
f.delete('6001'); f.save();
|
||||
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
|
||||
assert.ok(!f2.getKeys().includes('6001'));
|
||||
assert.ok(f2.getKeys().includes('5011'));
|
||||
});
|
||||
|
||||
test('save writes a .bak backup of the pre-save file', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
const v = f.read('5011')!; v.props['hp'] = { kind: 'num', value: 1 }; f.patch('5011', v);
|
||||
f.save();
|
||||
assert.equal(readFileSync(file + '.bak', 'utf8'), readFileSync(fixture, 'utf8'));
|
||||
});
|
||||
|
||||
test('save with no edits is a no-op (ok, no .bak written)', () => {
|
||||
const { file, dir } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
assert.equal(f.save().ok, true);
|
||||
assert.equal(readdirSync(dir).some(p => p.endsWith('.bak')), false);
|
||||
});
|
||||
Reference in New Issue
Block a user