- A file with no `schema` key is read as schema 1, but every pier must have a `ground` object, so a renamed field is refused instead of loading as empty. - `schema` null or any other number is still refused. - The project file keeps the normalised model and checks it with its own check (checkStoredGroundModel), so a saved model reopens. - Temporary: remove the no-schema branch once examples/out is regenerated. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
118 lines
5.1 KiB
TypeScript
118 lines
5.1 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
||
import { describe, expect, it } from 'vitest';
|
||
import { groundElFor, parseGroundModel } from '../src/core/ground-model';
|
||
import { changedKeys, overrideKeys, revertDraft, type PierValues } from '../src/core/changes';
|
||
import { parseProject, serializeProject, SCHEMA_VERSION, type ProjectFile } from '../src/core/project';
|
||
import { loadP1 } from './helpers';
|
||
|
||
/** The shape written by RAG/pierzainer_ground (schema 1). */
|
||
const SAMPLE = JSON.stringify({
|
||
schema: 1,
|
||
generated: '2026-10-23',
|
||
coord: '측량 X(북) · Y(동), m. 치수 mm',
|
||
piers: [
|
||
{ id: 'P1', ground: { center: 29.355, excel: 28.931, used: 29.355, source: '계산' }, footing: { cover: 0.972 } },
|
||
{ id: 'P2', ground: { center: null, excel: null, used: null, source: '없음' } },
|
||
],
|
||
});
|
||
|
||
describe('지반 모델 읽기 (ground_model.json, schema 1)', () => {
|
||
it('reads the pier-centre ground EL and the excel value', () => {
|
||
const m = parseGroundModel(SAMPLE);
|
||
expect(m.piers[0]).toEqual({ id: 'P1', centerEl: 29.355, excelEl: 28.931 });
|
||
expect(groundElFor(m, 'P1')).toBe(29.355);
|
||
});
|
||
|
||
it('a pier outside the terrain has no value, and an unknown pier has none either', () => {
|
||
const m = parseGroundModel(SAMPLE);
|
||
expect(groundElFor(m, 'P2')).toBeUndefined();
|
||
expect(groundElFor(m, 'P9')).toBeUndefined();
|
||
});
|
||
|
||
it('refuses another schema number instead of guessing the layout', () => {
|
||
expect(() => parseGroundModel(SAMPLE.replace('"schema":1', '"schema":2'))).toThrow('형식 번호 2');
|
||
});
|
||
|
||
it('refuses a file without a pier list or without pier names', () => {
|
||
expect(() => parseGroundModel('{"schema": 1}')).toThrow('교각 목록');
|
||
expect(() => parseGroundModel('{"schema": 1, "piers": [{"ground": {}}]}')).toThrow('이름(id)');
|
||
});
|
||
|
||
it('refuses text that is not JSON', () => {
|
||
expect(() => parseGroundModel('not json')).toThrow('JSON으로 읽히지 않습니다');
|
||
});
|
||
});
|
||
|
||
describe('번호 없는 옛 지반 모델 (B′: 임시 경로)', () => {
|
||
// examples/out/ground_model.json as written before the schema number existed (P1–P6, same values as the report).
|
||
const LEGACY = readFileSync(new URL('./fixtures/ground-model-legacy.json', import.meta.url), 'utf-8');
|
||
|
||
it('reads the unversioned file and reports it as schema 1', () => {
|
||
const m = parseGroundModel(LEGACY);
|
||
expect(m.schema).toBe(1);
|
||
expect(m.piers.map((p) => p.id)).toEqual(['P1', 'P2', 'P3', 'P4', 'P5', 'P6']);
|
||
expect(groundElFor(m, 'P1')).toBe(29.355);
|
||
expect(m.piers[0].excelEl).toBe(28.931);
|
||
});
|
||
|
||
it('refuses a pier that has no ground object (a renamed field must not load as empty values)', () => {
|
||
const data = JSON.parse(LEGACY);
|
||
delete data.piers[1].ground;
|
||
expect(() => parseGroundModel(JSON.stringify(data))).toThrow('P2: 지반 값(ground)이 없습니다');
|
||
});
|
||
|
||
it('refuses a null schema number instead of treating it as missing', () => {
|
||
expect(() => parseGroundModel(LEGACY.replace('{', '{"schema": null,'))).toThrow('형식 번호 null');
|
||
});
|
||
});
|
||
|
||
describe('지반고 적용 (사용자가 누를 때만, 변경으로 기록)', () => {
|
||
const base = loadP1();
|
||
const pier: PierValues = { defaults: base, input: base, draft: { ...base, groundEl: 29.355 } };
|
||
|
||
it('an applied value counts as an override and can be reverted to the excel value', () => {
|
||
expect(overrideKeys(pier)).toContain('groundEl');
|
||
expect(revertDraft(pier, 'groundEl').groundEl).toBe(base.groundEl);
|
||
});
|
||
|
||
it('the draft and the generated input are compared, so the change waits for regeneration', () => {
|
||
expect(changedKeys(pier.input, pier.draft)).toEqual(['groundEl']);
|
||
});
|
||
});
|
||
|
||
describe('프로젝트 파일의 지반 모델', () => {
|
||
function project(extra: Partial<ProjectFile>): ProjectFile {
|
||
const p1 = loadP1();
|
||
return {
|
||
schemaVersion: SCHEMA_VERSION,
|
||
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
|
||
source: { fileName: 'x.xlsx', sheets: [] },
|
||
piers: [{ id: 'P1', input: p1 }],
|
||
history: [],
|
||
...extra,
|
||
};
|
||
}
|
||
|
||
it('keeps the imported ground model across save and open', () => {
|
||
const m = parseGroundModel(SAMPLE);
|
||
const reopened = parseProject(serializeProject(project({ groundModel: m })));
|
||
expect(reopened.groundModel).toEqual(m);
|
||
});
|
||
|
||
it('refuses a damaged ground model inside the project file', () => {
|
||
const text = serializeProject(project({})).replace('"history": []', '"history": [], "groundModel": {"schema": 1}');
|
||
expect(() => parseProject(text)).toThrow('교각 목록');
|
||
});
|
||
|
||
it('refuses a stored model whose pier values are not numbers or null', () => {
|
||
const m = parseGroundModel(SAMPLE);
|
||
const bad = { ...m, piers: [{ id: 'P1', centerEl: '29.3', excelEl: null }] };
|
||
const text = serializeProject(project({})).replace('"history": []', `"history": [], "groundModel": ${JSON.stringify(bad)}`);
|
||
expect(() => parseProject(text)).toThrow('P1의 값이 맞지 않습니다');
|
||
});
|
||
|
||
it('opens an older file that has no ground model', () => {
|
||
expect(parseProject(serializeProject(project({}))).groundModel).toBeUndefined();
|
||
});
|
||
});
|