/* * **선언한 설비 정체성이 투영까지 닿는가** — 선언에서 화면까지 걸어 본다. * * ── 무엇이 있었나 (2026-08-30) ──────────────────────────────────────────── * 투영이 `m.gs1Id` 를 읽고 있었는데 **선언에 그런 필드가 없었다.** 그래서 그 칸은 일곱 대 모두 영원히 * 비어 있었다. 오류는 나지 않았다 — `?? null` 이 늘 `null` 을 냈을 뿐이다. * * 값은 한 칸 옆에 있었다. 커넥터가 인버터의 하드웨어 식별자를 속성으로 이미 보내고 있었고 * (`properties.deviceId = ES100000007d8d061d-1`), 정체성 칸도 있었는데 **그 사이에 길이 없었다.** * * 이 저장소에서 가장 자주 나는 결함 부류다. 그래서 필드를 여는 것으로 끝내지 않고 **선언 → 투영**을 * 실제로 걸어 본다. 필드만 있고 길이 없으면 여기서 빨갛게 난다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' import type { TwinModelDef } from '@operato/ops-contract' const here = dirname(fileURLToPath(import.meta.url)) const src = (p: string) => readFileSync(join(here, '..', 'server', p), 'utf-8') test('★ 선언에 정체성 자리가 있다', () => { /* 타입이 받아들이면 선언에 자리가 있는 것이다 — 없으면 이 파일이 컴파일되지 않는다. */ const model: TwinModelDef = { locations: [{ id: 'incoming', type: 'incoming' }], equipment: [{ id: '001', kind: 'pv-array', homeLocation: 'incoming', identity: 'ES100000007d8d061d-1' }] } as any assert.equal((model.equipment?.[0] as any).identity, 'ES100000007d8d061d-1') }) test('★ 투영이 그 자리를 읽는다 — 선언에 없는 이름을 읽으면 칸이 영원히 빈다', () => { const projection = src('service/twin-model/project-structure.ts') assert.match( projection, /identity:\s*\(m as any\)\.identity/, '설비 투영이 선언의 identity 를 읽어야 한다. 다른 이름을 읽으면 그 칸은 늘 null 이다.' ) assert.doesNotMatch( projection, /gs1Id:\s*m\.gs1Id/, '설비 선언에 gs1Id 는 없다 — 읽으면 늘 null 이 된다(2026-08-30 실측: 일곱 대 모두 비어 있었다)' ) }) test('★ 저장 자리의 이름이 선언과 같다 — 두 벌이면 한쪽만 고쳐진다', () => { const entity = src('service/twin-model/twin-equipment.ts') assert.match(entity, /identity\?: string/, '설비 행에 identity 칸이 있어야 한다') assert.doesNotMatch(entity, /gs1Id\?: string/, '설비에서는 gs1Id 를 쓰지 않는다 — 정체성이 GS1 키가 아닌 것이 정상이다') }) test('자리(location)의 gs1Id 는 그대로 둔다 — 그쪽은 실제로 SGLN 이 온다', () => { const entity = src('service/twin-model/twin-location.ts') assert.match(entity, /gs1Id\?: string/, '자리는 GS1 키를 받는다 — 설비와 같은 이유로 지우면 안 된다') })