- A file without a schema number is refused again; the example output in RAG/pierzainer_ground now has schema 2, so the temporary path is not needed. - The legacy fixture is removed; tests cover the missing and null schema numbers and a pier without a ground object. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
154 lines
6.4 KiB
TypeScript
154 lines
6.4 KiB
TypeScript
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":3'))).toThrow('형식 번호 3');
|
||
});
|
||
|
||
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으로 읽히지 않습니다');
|
||
});
|
||
});
|
||
|
||
const LEGACY_SAMPLE = JSON.stringify({ schema: 1, piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }] });
|
||
|
||
describe('지형 격자 (schema 2)', () => {
|
||
const WITH_GRID = JSON.stringify({
|
||
schema: 2,
|
||
generated: '2026-10-23',
|
||
piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }],
|
||
terrain_grid: { origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [28, null, 29, 30] },
|
||
});
|
||
|
||
it('reads the terrain grid as terrainGrid', () => {
|
||
const m = parseGroundModel(WITH_GRID);
|
||
expect(m.schema).toBe(2);
|
||
expect(m.terrainGrid).toEqual({ origin: [259815.676, 179088.094], step: 5, nx: 2, ny: 2, z: [28, null, 29, 30] });
|
||
});
|
||
|
||
it('a schema 1 file has no terrain grid', () => {
|
||
expect(parseGroundModel(LEGACY_SAMPLE).terrainGrid).toBeUndefined();
|
||
});
|
||
|
||
it('refuses a grid whose value count is not nx × ny', () => {
|
||
expect(() => parseGroundModel(WITH_GRID.replace('"z":[28,null,29,30]', '"z":[28,29]'))).toThrow('nx × ny');
|
||
});
|
||
|
||
it('refuses a grid value that is not a number or null', () => {
|
||
expect(() => parseGroundModel(WITH_GRID.replace('"z":[28,null,29,30]', '"z":[28,"x",29,30]'))).toThrow('숫자가 아닌');
|
||
});
|
||
|
||
it('refuses an unknown schema number', () => {
|
||
expect(() => parseGroundModel(WITH_GRID.replace('"schema":2', '"schema":3'))).toThrow('형식 번호 3');
|
||
});
|
||
|
||
it('keeps the grid in a saved project', () => {
|
||
const m = parseGroundModel(WITH_GRID);
|
||
const text = serializeProject({
|
||
schemaVersion: SCHEMA_VERSION,
|
||
project: { name: 'x', bridge: '운곡교', client: '', author: '', crs: 'KATEC', memo: '' },
|
||
source: { fileName: 'x.xlsx', sheets: [] },
|
||
piers: [{ id: 'P1', input: loadP1() }],
|
||
history: [],
|
||
groundModel: m,
|
||
});
|
||
expect(parseProject(text).groundModel?.terrainGrid).toEqual(m.terrainGrid);
|
||
});
|
||
});
|
||
|
||
describe('형식 번호 검사', () => {
|
||
it('refuses a file with no schema number (the B′ fallback is removed)', () => {
|
||
expect(() => parseGroundModel(JSON.stringify({ piers: [{ id: 'P1', ground: { center: 29.355, excel: 28.931 } }] }))).toThrow('형식 번호 undefined');
|
||
});
|
||
|
||
it('refuses a null schema number', () => {
|
||
expect(() => parseGroundModel('{"schema": null, "piers": []}')).toThrow('형식 번호 null');
|
||
});
|
||
|
||
it('refuses a pier that has no ground object', () => {
|
||
expect(() => parseGroundModel(JSON.stringify({ schema: 1, piers: [{ id: 'P2' }] }))).toThrow('P2: 지반 값(ground)이 없습니다');
|
||
});
|
||
});
|
||
|
||
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();
|
||
});
|
||
});
|