/* * ★ **원천마다 시간의 결이 다르다** — 밤마다 나던 거짓 빨강의 나머지 반쪽. * * 판정은 이미 두 값을 받을 수 있다(`syncVerdictOf` 의 `expectedSilenceMs`·`quietNow`). 없던 것은 * **원천이 그것을 말할 자리**였다. 인티그레이션 레인이 다섯을 재 왔다. * * ppms 발전 일몰에 멈춘다 — 두 사이트가 같은 분 19:25 에 함께 * ppms 계측 밤에도 온다 · 15분마다 * ppms 가격 화·목만 거래된다 · 그 날 하루 한 번 * plant 근무시간 창 · 훅이라 「보내면 온다」 * chef 조업시간 창 · 같음 * * 셋이 다 다르다 — 해에 매인 것, 사람 근무에 매인 것, 거래일에 매인 것. 그런데 **두 축으로 * 덮인다**: 「지금이 활동 시각인가」와 「그 안에서 얼마나 뜸한가」. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { cadenceOf, ingestExpectation, type LiveCadence } from '../server/service/reference/live-cadence.js' const NOW = 1_800_000_000_000 /* ── 축을 찾는다 ──────────────────────────────────────────────────────────── */ test('★ 선언한 축을 찾는다', () => { const cadences: LiveCadence[] = [ { axis: 'equipment.status', windowKind: 'daylight' }, { axis: 'quality.output', expectedSilenceMs: 900_000 } ] assert.equal(cadenceOf(cadences, 'quality.output')?.expectedSilenceMs, 900_000) assert.equal(cadenceOf(cadences, 'equipment.status')?.windowKind, 'daylight') }) test('★ 선언하지 않은 축은 없다고 답한다 — 옛 거동으로 견주게', () => { assert.equal(cadenceOf([{ axis: 'quality.output' }], 'order.status'), undefined) assert.equal(cadenceOf(undefined, 'quality.output'), undefined) }) /* ── 판정에 넘길 두 값으로 옮긴다 ───────────────────────────────────────── */ test('★ 선언이 없으면 아무것도 답하지 않는다 — 「모른다」를 「안 와도 된다」로 접지 않는다', () => { /* * 여기서 `quietNow: true` 를 내면 **선언 안 한 원천이 전부 조용해진다.** 정말 끊긴 것도 * 조용해지고, 그러면 이 축을 만든 목적을 잃는다. */ assert.deepEqual(ingestExpectation(undefined, { nowMs: NOW }), {}) }) test('★ 15분마다 긷는 원천 — 주기만 넘긴다', () => { const out = ingestExpectation({ axis: 'quality.output', expectedSilenceMs: 900_000 }, { nowMs: NOW }) assert.equal(out.expectedSilenceMs, 900_000) assert.equal(out.quietNow, undefined, '창을 선언하지 않았는데 시각을 단언했다') }) test('0이나 음수 주기는 선언이 아니다', () => { for (const bad of [0, -1, NaN]) { const out = ingestExpectation({ axis: 'a', expectedSilenceMs: bad }, { nowMs: NOW }) assert.equal(out.expectedSilenceMs, undefined, `${bad} 를 주기로 받았다`) } }) /* ── 「보내면 온다」 ──────────────────────────────────────────────────────── */ test('★ 훅으로 미는 축은 침묵으로 판정하지 않는다 — plant 가 그렇다', () => { /* * 보낼 것이 없으면 안 보내는 것이 정상이다. 「주기가 없다」와 「모른다」를 가르는 값이다. */ const out = ingestExpectation({ axis: 'order.status', pushed: true }, { nowMs: NOW }) assert.equal(out.quietNow, true) }) test('★ 미는 축은 창 판정을 기다리지 않는다 — 판정 수단이 없어도 답한다', () => { const out = ingestExpectation( { axis: 'order.status', pushed: true, window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] }, { nowMs: NOW } /* inWindow 를 주지 않았다 */ ) assert.equal(out.quietNow, true) }) /* ── 창 ──────────────────────────────────────────────────────────────────── */ test('★ 활동 시각 안이면 조용할 때가 아니다', () => { const out = ingestExpectation( { axis: 'quality.output', window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] }, { nowMs: NOW, inWindow: () => true } ) assert.equal(out.quietNow, false) }) test('★ 활동 시각 밖이면 조용할 때다', () => { const out = ingestExpectation( { axis: 'quality.output', window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] }, { nowMs: NOW, inWindow: () => false } ) assert.equal(out.quietNow, true) }) test('★ 판정 수단을 안 받았으면 시각을 단언하지 않는다 — 지어낸 값을 만들지 않는다', () => { /* * 부르는 쪽이 달력 판정 함수를 주지 않았는데 우리가 「활동 시각이다」로 답하면, 창을 선언한 * 원천이 **창이 없는 것과 같아진다.** 그것은 조용한 실패다. */ const out = ingestExpectation( { axis: 'quality.output', window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] }, { nowMs: NOW } ) assert.equal(out.quietNow, undefined) }) test('빈 창은 창이 아니다', () => { assert.equal(ingestExpectation({ axis: 'a', window: [] }, { nowMs: NOW, inWindow: () => false }).quietNow, undefined) }) /* ── 두 축이 함께 온다 ──────────────────────────────────────────────────── */ test('★ 주기와 창을 같이 선언할 수 있다 — 인증서 가격이 그 모양이다', () => { /* * 화·목만 거래되고 그 날 하루 한 번 온다. 「매일 와야 한다」로 재면 주 5일 중 3일이 빨갛고, * 「주 2회」로 재면 이어 쓴 날을 못 본다. 두 축이면 둘 다 답한다. */ const out = ingestExpectation( { axis: 'quality.output', expectedSilenceMs: 26 * 3_600_000, window: [ { id: 'tue', entryType: 'production', startTime: '00:00', endTime: '23:59' }, { id: 'thu', entryType: 'production', startTime: '00:00', endTime: '23:59' } ] }, { nowMs: NOW, inWindow: () => false } ) assert.equal(out.expectedSilenceMs, 26 * 3_600_000) assert.equal(out.quietNow, true, '거래일이 아니면 조용한 것이 정상이다') }) /* ── 「해 있는 동안」은 호스트가 채운다 ──────────────────────────────────── */ test('★ 종류만 선언한 것으로는 창 판정을 하지 않는다 — 호스트가 채워야 한다', () => { /* * 커널은 일출·일몰을 계산하지 않는다(굴절·고위도·박명 정의가 다 선택이고, 정하면 방언이다). * 원천은 종류만 말하고, 좌표를 아는 호스트가 날마다 구체적인 구간을 채운다. * * 그러니 `windowKind` 만 있고 `window` 가 비어 있으면 **아직 채워지지 않은 것**이고, 그때 * 판정하지 않는다. `false` 로 접으면 태양광이 밤에도 활동 시각인 것이 된다. */ const out = ingestExpectation({ axis: 'equipment.status', windowKind: 'daylight' }, { nowMs: NOW, inWindow: () => true }) assert.equal(out.quietNow, undefined) }) test('★ 호스트가 채우면 그것으로 판정한다', () => { const out = ingestExpectation( { axis: 'equipment.status', windowKind: 'daylight', window: [{ id: 'today', entryType: 'production', startTime: '06:12', endTime: '19:25' }] }, { nowMs: NOW, inWindow: () => false } ) assert.equal(out.quietNow, true) })