@things-factory/headless-twin 10.0.18 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +10 -1
- package/dist-server/engine/canonical-ingest.js +110 -33
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/event-time.d.ts +25 -0
- package/dist-server/engine/event-time.js +98 -0
- package/dist-server/engine/event-time.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +23 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/ingest-dedupe.d.ts +18 -6
- package/dist-server/engine/ingest-dedupe.js +201 -15
- package/dist-server/engine/ingest-dedupe.js.map +1 -1
- package/dist-server/engine/ingest-health.d.ts +60 -1
- package/dist-server/engine/ingest-health.js +43 -8
- package/dist-server/engine/ingest-health.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +19 -0
- package/dist-server/engine/kpi-fold.js +4 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.js +23 -3
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/oee-accumulator.d.ts +35 -2
- package/dist-server/engine/oee-accumulator.js +56 -4
- package/dist-server/engine/oee-accumulator.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +72 -3
- package/dist-server/engine/twin-engine.js +118 -5
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +21 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
- package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
- package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
- package/dist-server/migrations/index.js +5 -1
- package/dist-server/migrations/index.js.map +1 -1
- package/dist-server/routes.js +19 -16
- package/dist-server/routes.js.map +1 -1
- package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
- package/dist-server/service/actuation/actuation-advisor.js +53 -0
- package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
- package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
- package/dist-server/service/actuation/actuation-outcome.js +34 -0
- package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
- package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
- package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
- package/dist-server/service/actuation/actuation-rule.js +130 -0
- package/dist-server/service/actuation/actuation-rule.js.map +1 -0
- package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
- package/dist-server/service/actuation/approval-gateway.js +14 -0
- package/dist-server/service/actuation/approval-gateway.js.map +1 -0
- package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
- package/dist-server/service/actuation/catch-up-effects.js +120 -0
- package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
- package/dist-server/service/actuation/command-dispatcher.d.ts +230 -0
- package/dist-server/service/actuation/command-dispatcher.js +269 -0
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -0
- package/dist-server/service/actuation/command-store.d.ts +11 -0
- package/dist-server/service/actuation/command-store.js +219 -0
- package/dist-server/service/actuation/command-store.js.map +1 -0
- package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
- package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
- package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
- package/dist-server/service/actuation/effect-match.d.ts +53 -0
- package/dist-server/service/actuation/effect-match.js +65 -0
- package/dist-server/service/actuation/effect-match.js.map +1 -0
- package/dist-server/service/actuation/index.d.ts +25 -0
- package/dist-server/service/actuation/index.js +30 -0
- package/dist-server/service/actuation/index.js.map +1 -0
- package/dist-server/service/actuation/retry-plan.d.ts +63 -0
- package/dist-server/service/actuation/retry-plan.js +99 -0
- package/dist-server/service/actuation/retry-plan.js.map +1 -0
- package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
- package/dist-server/service/actuation/rule-evaluate.js +205 -0
- package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
- package/dist-server/service/actuation/rule-loop.d.ts +5 -0
- package/dist-server/service/actuation/rule-loop.js +156 -0
- package/dist-server/service/actuation/rule-loop.js.map +1 -0
- package/dist-server/service/actuation/rule-runner.d.ts +52 -0
- package/dist-server/service/actuation/rule-runner.js +135 -0
- package/dist-server/service/actuation/rule-runner.js.map +1 -0
- package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
- package/dist-server/service/actuation/rule-wiring.js +80 -0
- package/dist-server/service/actuation/rule-wiring.js.map +1 -0
- package/dist-server/service/actuation/settle-effects.d.ts +21 -0
- package/dist-server/service/actuation/settle-effects.js +101 -0
- package/dist-server/service/actuation/settle-effects.js.map +1 -0
- package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
- package/dist-server/service/actuation/sweep-dispatch.js +125 -0
- package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
- package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
- package/dist-server/service/actuation/twin-command-resolver.js +406 -0
- package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
- package/dist-server/service/actuation/twin-command.d.ts +111 -0
- package/dist-server/service/actuation/twin-command.js +170 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -0
- package/dist-server/service/actuation/twin-now.d.ts +41 -0
- package/dist-server/service/actuation/twin-now.js +65 -0
- package/dist-server/service/actuation/twin-now.js.map +1 -0
- package/dist-server/service/index.d.ts +5 -2
- package/dist-server/service/index.js +28 -17
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/reference/actuation-routing.d.ts +60 -0
- package/dist-server/service/reference/actuation-routing.js +126 -0
- package/dist-server/service/reference/actuation-routing.js.map +1 -0
- package/dist-server/service/reference/actuation-target.d.ts +65 -0
- package/dist-server/service/reference/actuation-target.js +94 -0
- package/dist-server/service/reference/actuation-target.js.map +1 -0
- package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
- package/dist-server/service/reference/connection-portability-resolver.js +181 -0
- package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
- package/dist-server/service/reference/connection-portability.d.ts +138 -0
- package/dist-server/service/reference/connection-portability.js +230 -0
- package/dist-server/service/reference/connection-portability.js.map +1 -0
- package/dist-server/service/reference/fill-cadence.d.ts +17 -0
- package/dist-server/service/reference/fill-cadence.js +31 -0
- package/dist-server/service/reference/fill-cadence.js.map +1 -0
- package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
- package/dist-server/service/reference/fill-dedupe.js +63 -0
- package/dist-server/service/reference/fill-dedupe.js.map +1 -0
- package/dist-server/service/reference/fill-loop.d.ts +35 -0
- package/dist-server/service/reference/fill-loop.js +187 -0
- package/dist-server/service/reference/fill-loop.js.map +1 -0
- package/dist-server/service/reference/fill-span.d.ts +10 -0
- package/dist-server/service/reference/fill-span.js +37 -0
- package/dist-server/service/reference/fill-span.js.map +1 -0
- package/dist-server/service/reference/hook-contract.d.ts +149 -22
- package/dist-server/service/reference/hook-contract.js +236 -23
- package/dist-server/service/reference/hook-contract.js.map +1 -1
- package/dist-server/service/reference/hook-rejected.d.ts +25 -0
- package/dist-server/service/reference/hook-rejected.js +48 -0
- package/dist-server/service/reference/hook-rejected.js.map +1 -0
- package/dist-server/service/reference/hook-store.d.ts +3 -0
- package/dist-server/service/reference/hook-store.js +28 -0
- package/dist-server/service/reference/hook-store.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +4 -1
- package/dist-server/service/reference/index.js +5 -1
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/live-cadence.d.ts +68 -0
- package/dist-server/service/reference/live-cadence.js +44 -0
- package/dist-server/service/reference/live-cadence.js.map +1 -0
- package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
- package/dist-server/service/reference/live-feed-lease.js +56 -0
- package/dist-server/service/reference/live-feed-lease.js.map +1 -0
- package/dist-server/service/reference/live-ingest.d.ts +21 -0
- package/dist-server/service/reference/live-ingest.js +66 -0
- package/dist-server/service/reference/live-ingest.js.map +1 -0
- package/dist-server/service/reference/reference-adapter.d.ts +355 -2
- package/dist-server/service/reference/reference-adapter.js +19 -1
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
- package/dist-server/service/reference/reference-fill-run.js +142 -0
- package/dist-server/service/reference/reference-fill-run.js.map +1 -0
- package/dist-server/service/reference/reference-fill.d.ts +90 -0
- package/dist-server/service/reference/reference-fill.js +141 -0
- package/dist-server/service/reference/reference-fill.js.map +1 -0
- package/dist-server/service/reference/reference-hook.d.ts +64 -6
- package/dist-server/service/reference/reference-hook.js +265 -27
- package/dist-server/service/reference/reference-hook.js.map +1 -1
- package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
- package/dist-server/service/reference/reference-link-groups.js +15 -0
- package/dist-server/service/reference/reference-link-groups.js.map +1 -0
- package/dist-server/service/reference/reference-links.d.ts +14 -0
- package/dist-server/service/reference/reference-links.js +73 -0
- package/dist-server/service/reference/reference-links.js.map +1 -0
- package/dist-server/service/reference/reference-live.d.ts +1 -0
- package/dist-server/service/reference/reference-live.js +32 -6
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +56 -2
- package/dist-server/service/reference/reference-master.js +33 -3
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +27 -0
- package/dist-server/service/reference/reference-resolver.js +79 -1
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
- package/dist-server/service/twin-event/twin-event-keys.js +36 -4
- package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.d.ts +1 -0
- package/dist-server/service/twin-event/twin-event.js +11 -2
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
- package/dist-server/service/twin-model/choose-recipe.js +33 -0
- package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
- package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
- package/dist-server/service/twin-model/external-resolver.js +109 -0
- package/dist-server/service/twin-model/external-resolver.js.map +1 -0
- package/dist-server/service/twin-model/project-structure.js +16 -3
- package/dist-server/service/twin-model/project-structure.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
- package/dist-server/service/twin-model/twin-equipment.js +10 -0
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
- package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space.d.ts +29 -0
- package/dist-server/service/twin-space/twin-space.js +13 -0
- package/dist-server/service/twin-space/twin-space.js.map +1 -1
- package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
- package/dist-server/service/twin-subject/event-subjects.js +8 -1
- package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
- package/dist-shared/entity-delta.js +1 -1
- package/dist-shared/entity-delta.js.map +1 -1
- package/package.json +7 -7
- package/server/engine/canonical-ingest.ts +119 -33
- package/server/engine/event-time.ts +96 -0
- package/server/engine/index.ts +20 -0
- package/server/engine/ingest-dedupe.ts +203 -27
- package/server/engine/ingest-health.ts +91 -6
- package/server/engine/kpi-fold.ts +24 -1
- package/server/engine/kpi-query.ts +23 -4
- package/server/engine/oee-accumulator.ts +80 -8
- package/server/engine/twin-engine.ts +119 -5
- package/server/index.ts +23 -0
- package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
- package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
- package/server/migrations/index.ts +5 -1
- package/server/routes.ts +19 -20
- package/server/service/actuation/actuation-advisor.ts +124 -0
- package/server/service/actuation/actuation-outcome.ts +97 -0
- package/server/service/actuation/actuation-rule-resolver.ts +147 -0
- package/server/service/actuation/actuation-rule.ts +139 -0
- package/server/service/actuation/approval-gateway.ts +44 -0
- package/server/service/actuation/catch-up-effects.ts +152 -0
- package/server/service/actuation/command-dispatcher.ts +511 -0
- package/server/service/actuation/command-store.ts +236 -0
- package/server/service/actuation/dispatch-after-approval.ts +71 -0
- package/server/service/actuation/effect-match.ts +124 -0
- package/server/service/actuation/index.ts +27 -0
- package/server/service/actuation/retry-plan.ts +145 -0
- package/server/service/actuation/rule-evaluate.ts +352 -0
- package/server/service/actuation/rule-loop.ts +160 -0
- package/server/service/actuation/rule-runner.ts +216 -0
- package/server/service/actuation/rule-wiring.ts +84 -0
- package/server/service/actuation/settle-effects.ts +115 -0
- package/server/service/actuation/sweep-dispatch.ts +179 -0
- package/server/service/actuation/twin-command-resolver.ts +387 -0
- package/server/service/actuation/twin-command.ts +232 -0
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +11 -0
- package/server/service/reference/actuation-routing.ts +165 -0
- package/server/service/reference/actuation-target.ts +163 -0
- package/server/service/reference/connection-portability-resolver.ts +171 -0
- package/server/service/reference/connection-portability.ts +285 -0
- package/server/service/reference/fill-cadence.ts +29 -0
- package/server/service/reference/fill-dedupe.ts +66 -0
- package/server/service/reference/fill-loop.ts +153 -0
- package/server/service/reference/fill-span.ts +31 -0
- package/server/service/reference/hook-contract.ts +280 -22
- package/server/service/reference/hook-rejected.ts +53 -0
- package/server/service/reference/hook-store.ts +28 -0
- package/server/service/reference/index.ts +5 -1
- package/server/service/reference/live-cadence.ts +129 -0
- package/server/service/reference/live-feed-lease.ts +56 -0
- package/server/service/reference/live-ingest.ts +94 -0
- package/server/service/reference/reference-adapter.ts +392 -5
- package/server/service/reference/reference-fill-run.ts +165 -0
- package/server/service/reference/reference-fill.ts +223 -0
- package/server/service/reference/reference-hook.ts +355 -32
- package/server/service/reference/reference-link-groups.ts +21 -0
- package/server/service/reference/reference-links.ts +77 -0
- package/server/service/reference/reference-live.ts +30 -5
- package/server/service/reference/reference-master.ts +90 -5
- package/server/service/reference/reference-resolver.ts +77 -2
- package/server/service/twin-event/twin-event-keys.ts +37 -4
- package/server/service/twin-event/twin-event.ts +18 -1
- package/server/service/twin-model/choose-recipe.ts +31 -0
- package/server/service/twin-model/external-resolver.ts +161 -0
- package/server/service/twin-model/project-structure.ts +16 -3
- package/server/service/twin-model/twin-equipment.ts +22 -0
- package/server/service/twin-model/twin-lineage-query.ts +82 -1
- package/server/service/twin-model/twin-model-item-query.ts +95 -4
- package/server/service/twin-model/twin-model-tree-query.ts +1 -1
- package/server/service/twin-space/twin-space.ts +39 -0
- package/server/service/twin-subject/event-subjects.ts +8 -1
- package/shared/entity-delta.ts +1 -1
- package/test/ack-shape-alignment.test.ts +97 -0
- package/test/actuation-approval-door.test.ts +421 -0
- package/test/actuation-dispatch.test.ts +209 -0
- package/test/actuation-effect.test.ts +162 -0
- package/test/actuation-outcome.test.ts +152 -0
- package/test/actuation-rule-runner.test.ts +281 -0
- package/test/actuation-rule.test.ts +266 -0
- package/test/actuation-seam.test.ts +249 -0
- package/test/actuation-settle.test.ts +164 -0
- package/test/actuation-target.test.ts +145 -0
- package/test/canonical-ingest-vocabularies.test.ts +49 -2
- package/test/capability-mapping.test.ts +1 -1
- package/test/choose-recipe.test.ts +117 -0
- package/test/command-store-writes-columns.test.ts +126 -0
- package/test/connection-portability-doors.test.ts +87 -0
- package/test/connector-capability-declaration.test.ts +54 -4
- package/test/declaration-reaches-model.test.ts +43 -0
- package/test/dedupe-transformation.test.ts +112 -0
- package/test/entity-delta.test.ts +1 -1
- package/test/event-subjects.test.ts +2 -1
- package/test/external-resolver.test.ts +171 -0
- package/test/fill-dedupe.test.ts +59 -0
- package/test/fill-loop.test.ts +35 -0
- package/test/first-envelope.test.ts +153 -0
- package/test/fold-shift-dst.test.ts +110 -0
- package/test/home-in-area.test.ts +90 -0
- package/test/hook-rejected-shape.test.ts +86 -0
- package/test/hook-response-sum.test.ts +193 -0
- package/test/hook-sequence.test.ts +333 -0
- package/test/ingest-dedupe.test.ts +56 -0
- package/test/ingest-expected-quiet.test.ts +111 -0
- package/test/ingest-idempotent.test.ts +79 -0
- package/test/ingest-reconcile-four-ways.test.ts +97 -0
- package/test/ingest-rules-header.test.ts +100 -0
- package/test/ingest-window-rolls-unobserved.test.ts +72 -0
- package/test/intake-mapping-parity.test.ts +175 -0
- package/test/live-cadence.test.ts +170 -0
- package/test/live-feed-lease.test.ts +63 -0
- package/test/live-mirror-parity.test.ts +1 -1
- package/test/oee-accumulator.test.ts +3 -3
- package/test/oee-site-calendar.test.ts +146 -0
- package/test/order-and-transaction-split.test.ts +107 -0
- package/test/reference-fill.test.ts +235 -0
- package/test/reference-hook.test.ts +114 -23
- package/test/reference-links.test.ts +56 -0
- package/test/retry-plan.test.ts +139 -0
- package/test/sweep-dispatch.test.ts +191 -0
- package/test/twin-event-keys.test.ts +20 -6
- package/test/twin-model-item-db.test.ts +6 -6
- package/test/twin-model-tree-db.test.ts +6 -6
- package/tsconfig.shared.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **연결 이관의 문 셋** — 몸통은 순수 함수이고, 이 검사는 **문이 그 규율을 지키는지** 본다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇을 옮기나 (사용자와 맞춘 개념, 2026-09-05) ────────────────────────
|
|
5
|
+
* 옮기는 것은 「붙는 법」이지 「붙은 결과」가 아니다. 마스터·사실·잰 값·비밀값은 안 옮긴다 —
|
|
6
|
+
* 앞의 셋은 다시 얻을 수 있고, 비밀값은 그 환경의 것이지 셋업이 아니다.
|
|
7
|
+
*
|
|
8
|
+
* ── 이 검사가 지키는 것 ────────────────────────────────────────────────────
|
|
9
|
+
* 문이 몸통의 판단을 **뒤집지 않는지**다. 몸통 시험 19개가 이미 「비밀값이 안 나간다」를 지키는데,
|
|
10
|
+
* 문이 그것을 되살려 쓰면 그 19개가 아무것도 못 막는다.
|
|
11
|
+
*/
|
|
12
|
+
import { test } from 'node:test'
|
|
13
|
+
import assert from 'node:assert/strict'
|
|
14
|
+
|
|
15
|
+
import { exportConnections, planImport, SECRET_KEYS } from '../server/service/reference/connection-portability.js'
|
|
16
|
+
|
|
17
|
+
const row = (over: any = {}) => ({
|
|
18
|
+
source: 'mes-miratek',
|
|
19
|
+
system: 'operato-plant',
|
|
20
|
+
siteName: '미라텍',
|
|
21
|
+
adapterType: 'operato-plant',
|
|
22
|
+
connectionConfig: { baseUrl: 'https://plant.example', siteId: 'mes-miratek', token: 'SECRET-VALUE', hookSecret: 'ALSO-SECRET' },
|
|
23
|
+
...over
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('★ 비밀값이 꾸러미에 안 나간다 — 이름만 나간다', () => {
|
|
27
|
+
const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
|
|
28
|
+
const text = JSON.stringify(b)
|
|
29
|
+
|
|
30
|
+
assert.ok(!text.includes('SECRET-VALUE'), '토큰이 꾸러미에 실렸다')
|
|
31
|
+
assert.ok(!text.includes('ALSO-SECRET'), '훅 비밀값이 꾸러미에 실렸다')
|
|
32
|
+
assert.deepEqual(b.connections[0].secretsRequired.sort(), ['hookSecret', 'token'], '무엇을 채워야 하는지 말하지 않는다')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('★ 선언은 나간다 — 그것이 옮기려는 것이다', () => {
|
|
36
|
+
const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
|
|
37
|
+
const c = b.connections[0]
|
|
38
|
+
|
|
39
|
+
assert.equal(c.adapterType, 'operato-plant')
|
|
40
|
+
assert.equal(c.config.baseUrl, 'https://plant.example')
|
|
41
|
+
assert.equal(c.config.siteId, 'mes-miratek')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('★ 새 비밀 칸도 기본으로 안 나간다 — 이름 규칙으로 거른다', () => {
|
|
45
|
+
/*
|
|
46
|
+
* 비밀 칸이 하나 늘 때 이 목록을 안 고치면 그 값이 파일로 나간다. 그래서 규칙이 이름이고,
|
|
47
|
+
* 그 규칙이 지금 무엇을 아는지 값으로 못 박는다.
|
|
48
|
+
*/
|
|
49
|
+
assert.ok(SECRET_KEYS.includes('token' as any))
|
|
50
|
+
assert.ok(SECRET_KEYS.includes('hookSecret' as any))
|
|
51
|
+
assert.ok(SECRET_KEYS.includes('intakeSecret' as any))
|
|
52
|
+
assert.ok(SECRET_KEYS.length >= 4, `비밀 칸 이름을 ${SECRET_KEYS.length}개만 안다`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('★ 없는 비밀값을 지어내지 않는다 — 이름을 대고 사람에게 넘긴다', () => {
|
|
56
|
+
const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
|
|
57
|
+
/* 이 환경에는 그 비밀값이 없다. */
|
|
58
|
+
const plan = planImport(b, [], () => false)
|
|
59
|
+
|
|
60
|
+
const needs = plan.needsHuman.find(n => n.source === 'mes-miratek')
|
|
61
|
+
assert.ok(needs, '사람이 채울 것을 말하지 않는다')
|
|
62
|
+
assert.deepEqual(needs!.keys.sort(), ['hookSecret', 'token'])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('★ 이미 있는 연결의 비밀값을 꾸러미가 안 지운다', () => {
|
|
66
|
+
const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
|
|
67
|
+
const existing = [{ source: 'mes-miratek', connectionConfig: { token: 'LIVE-TOKEN' } }]
|
|
68
|
+
const plan = planImport(b, existing, () => false)
|
|
69
|
+
|
|
70
|
+
const a = plan.actions.find(x => (x as any).connection?.source === 'mes-miratek') as any
|
|
71
|
+
assert.equal(a?.action, 'update', `갱신이 아니라 ${a?.action} 이다`)
|
|
72
|
+
assert.ok(a.keptSecrets?.includes('token'), '이미 있는 토큰을 지킨다고 말하지 않는다')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('★ 꾸러미가 아닌 것을 받으면 아무것도 하지 않는다', () => {
|
|
76
|
+
for (const bad of [null, {}, { kind: 'something-else' }, 'text']) {
|
|
77
|
+
const plan = planImport(bad, [], () => false)
|
|
78
|
+
assert.equal(plan.actions.length, 0, `${JSON.stringify(bad)} 로 무언가를 하려 했다`)
|
|
79
|
+
assert.ok(plan.errors.length, '왜 안 되는지 말하지 않는다')
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
|
|
84
|
+
const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
|
|
85
|
+
assert.equal(b.connections.length, 1, '꾸러미가 비었다 — 이 검사가 아무것도 안 잰다')
|
|
86
|
+
assert.ok(planImport(b, [], () => true).actions.length > 0, '계획이 비었다')
|
|
87
|
+
})
|
|
@@ -50,19 +50,69 @@ test('★ 라이브를 구현하고 선언하지 않으면 말해 준다 — 이
|
|
|
50
50
|
assert.ok(/live twin cannot be created/.test(said))
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
test('★
|
|
53
|
+
test('★ 구동을 구현하고 선언하지 않아도 말해 준다 — 같은 규율이다', () => {
|
|
54
54
|
const before = warnings.length
|
|
55
55
|
const a = adapter({ control: async () => ({ ok: true }) })
|
|
56
56
|
registerAdapterType(a)
|
|
57
57
|
assert.ok(warnings.slice(before).join('\n').includes('control'))
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
test('
|
|
60
|
+
test('★ 조치를 구현하고 선언하지 않아도 말해 준다 — 그 이름이 없어서 오늘 하루가 갔다', () => {
|
|
61
|
+
/*
|
|
62
|
+
* 이 자리에 `actuate` 가 없었다. 능력 축이 `live`·`control` 둘뿐이라 커넥터는 조치를 **선언할
|
|
63
|
+
* 방법이 없었고**, 등록 검사도 그것을 물을 수 없었다. MES 커넥터는 그 사이에 `actuate` 를 다
|
|
64
|
+
* 만들어 두었는데, 넘기는 자리는 「구현이 없습니다」로 답했다.
|
|
65
|
+
*
|
|
66
|
+
* 그 문장이 그럴듯해서 설계 문서와 다른 레인에까지 사실로 옮겨졌다. 오류가 아니라 **정직해 보이는
|
|
67
|
+
* 거절**이 원인이었다.
|
|
68
|
+
*/
|
|
61
69
|
const before = warnings.length
|
|
62
|
-
const a = adapter({
|
|
70
|
+
const a = adapter({ actuate: async () => ({ ok: true }) })
|
|
63
71
|
registerAdapterType(a)
|
|
72
|
+
|
|
64
73
|
const said = warnings.slice(before).join('\n')
|
|
65
|
-
assert.ok(said.includes('
|
|
74
|
+
assert.ok(said.includes('actuate'), '무엇이 빠졌는지 말한다')
|
|
75
|
+
assert.ok(said.includes(a.type), '어느 커넥터인지 말한다')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('여럿이 어긋나면 다 말한다 — 하나만 말하면 고친 뒤 또 막힌다', () => {
|
|
79
|
+
const before = warnings.length
|
|
80
|
+
const a = adapter({
|
|
81
|
+
openLiveFeed: () => () => {},
|
|
82
|
+
control: async () => ({ ok: true }),
|
|
83
|
+
actuate: async () => ({ ok: true })
|
|
84
|
+
})
|
|
85
|
+
registerAdapterType(a)
|
|
86
|
+
|
|
87
|
+
const said = warnings.slice(before).join('\n')
|
|
88
|
+
for (const name of ['live', 'control', 'actuate']) {
|
|
89
|
+
assert.ok(said.includes(name), `${name} 를 말하지 않았다`)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('★ 능력 이름이 늘면 검사도 함께 는다 — 표 하나에서 돈다', () => {
|
|
94
|
+
/*
|
|
95
|
+
* 예전에는 검사할 능력 셋이 코드 안에 손으로 적혀 있었다(`['live','control']`). 새 능력을 넣을 때
|
|
96
|
+
* 판정에는 넣고 검사에는 잊으면, **구현했는데 선언 안 한 것을 아무도 말해 주지 않는다** — 오늘 그
|
|
97
|
+
* 상태였다.
|
|
98
|
+
*
|
|
99
|
+
* 지금은 능력↔메서드 표 하나에서 돈다. 이 시험은 그 표가 실제로 셋을 다 덮는지 본다 — 넷째 능력을
|
|
100
|
+
* 넣고 표에 안 적으면 여기서 걸린다.
|
|
101
|
+
*/
|
|
102
|
+
const cases: [string, Record<string, unknown>][] = [
|
|
103
|
+
['live', { openLiveFeed: () => () => {} }],
|
|
104
|
+
['control', { control: async () => ({ ok: true }) }],
|
|
105
|
+
['actuate', { actuate: async () => ({ ok: true }) }]
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
for (const [name, impl] of cases) {
|
|
109
|
+
const before = warnings.length
|
|
110
|
+
registerAdapterType(adapter(impl))
|
|
111
|
+
assert.ok(
|
|
112
|
+
warnings.slice(before).join('\n').includes(name),
|
|
113
|
+
`${name} 를 구현하고 선언하지 않았는데 아무 말도 하지 않았다 — 검사 표에 그 이름이 없다`
|
|
114
|
+
)
|
|
115
|
+
}
|
|
66
116
|
})
|
|
67
117
|
|
|
68
118
|
test('선언과 구현이 맞으면 아무 말도 하지 않는다 — 소음이 되면 아무도 읽지 않는다', () => {
|
|
@@ -88,6 +88,9 @@ const VALUE: Record<string, unknown> = {
|
|
|
88
88
|
gs1Id: 'urn:epc:id:sgln:0614141.00001.0',
|
|
89
89
|
/* 설비의 정체성 — **GS1 키가 아닌 것이 정상이다**(실측: 인버터 하드웨어 식별자). */
|
|
90
90
|
identity: 'ES100000007d8d061d-1',
|
|
91
|
+
/* 설비 등급 소속 — 2026-09-04 에 이 칸이 접수면에 없어서 원본이 채운 등급 22대가 문턱에서
|
|
92
|
+
버려졌다(인티그레이션 레인 실측). 사람·자산은 받는데 설비만 없었다. */
|
|
93
|
+
equipmentClassIds: ['welding-robot'],
|
|
91
94
|
homeLocationId: 'cut-station',
|
|
92
95
|
mtbfMs: 600000,
|
|
93
96
|
mttrMs: 60000,
|
|
@@ -178,3 +181,43 @@ for (const c of CASES) {
|
|
|
178
181
|
)
|
|
179
182
|
})
|
|
180
183
|
}
|
|
184
|
+
|
|
185
|
+
/*
|
|
186
|
+
* ── 공간의 시각 기준도 모델까지 간다 (2026-09-07) ────────────────────────────
|
|
187
|
+
*
|
|
188
|
+
* 위 검사는 **자원**의 필드만 본다. `space` 는 안 보고 있었고, 그래서 실제로 하나가 새고 있었다 —
|
|
189
|
+
* `space.timezone` 이 오프셋으로만 풀려 모델에 실리고 **이름은 버려졌다.**
|
|
190
|
+
*
|
|
191
|
+
* 이름이 없으면 소비처가 순간마다 다시 풀 수 없다. 지난 구간을 교대별로 접을 때 그 구간 안의
|
|
192
|
+
* 일광절약시간 전환이 사실을 한 시간 밀어 **다른 교대로** 세게 하고, 오류는 나지 않는다.
|
|
193
|
+
* `America/Tijuana`(PST −480 / PDT −480 아닌 −420)가 이미 자료에 있다.
|
|
194
|
+
*
|
|
195
|
+
* 그리고 이 새는 자리를 잡은 것은 타입 검사도, 위 하네스도 아니라 **다른 레인의 물음**이었다
|
|
196
|
+
* ("자료를 채운 뒤 이미 세워진 모델이 따라가나"). 그래서 여기 검사로 남긴다.
|
|
197
|
+
*/
|
|
198
|
+
const withSpace = (space: Record<string, unknown>) => masterToTwin({ ...master(), space: { ...master().space, ...space } })
|
|
199
|
+
|
|
200
|
+
test('공간의 시간대 이름이 모델까지 간다 — 오프셋만 실으면 계절이 바뀔 때 틀린다', () => {
|
|
201
|
+
const { model } = withSpace({ timezone: 'America/Tijuana' })
|
|
202
|
+
|
|
203
|
+
assert.equal((model as any).timezone, 'America/Tijuana', '이름이 모델에 없다 — 소비처가 순간마다 풀 수 없다')
|
|
204
|
+
assert.equal(typeof (model as any).utcOffsetMinutes, 'number', '오프셋도 함께 실려야 한다 — 이름을 안 보는 소비처가 그 값으로 돈다')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('시간대를 선언하지 않으면 두 칸 다 만들지 않는다 — 0 은 「UTC 라고 선언했다」는 주장이다', () => {
|
|
208
|
+
const { model } = withSpace({})
|
|
209
|
+
|
|
210
|
+
assert.equal((model as any).timezone, undefined)
|
|
211
|
+
assert.equal((model as any).utcOffsetMinutes, undefined)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('풀리지 않는 이름은 싣지 않고 경고로 말한다 — 실어 두면 소비처가 믿고 매번 실패한다', () => {
|
|
215
|
+
const { model, warnings } = withSpace({ timezone: 'Mars/Olympus_Mons' })
|
|
216
|
+
|
|
217
|
+
assert.equal((model as any).timezone, undefined)
|
|
218
|
+
assert.equal((model as any).utcOffsetMinutes, undefined)
|
|
219
|
+
assert.ok(
|
|
220
|
+
(warnings ?? []).some((w: any) => JSON.stringify(w).includes('Mars/Olympus_Mons')),
|
|
221
|
+
'모르는 시간대를 조용히 버리지 않는다'
|
|
222
|
+
)
|
|
223
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 변환·담김의 정체가 **무엇이 무엇으로 되었나**로 갈리나.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 (2026-09-07) ────────────────────────────────────────────────
|
|
5
|
+
* 이 자리가 없어서 **사실 하나가 없어졌습니다.** 거절이 아니라 되풀이로 세어져서 경고도 없었습니다.
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* plant outbox seq 37 in LOT-CT-SIDE-001 24개 → out LOT-CT-MAIN-003 12개
|
|
9
|
+
* plant outbox seq 42 in LOT-CT-SIDE-001 18개 → out LOT-CT-MAIN-004 9개
|
|
10
|
+
* transformationID · bizStep · 초까지 전부 같다
|
|
11
|
+
* 저널에 남은 것 LOT-CT-MAIN-003 하나
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* `LOT-CT-MAIN-004` 를 어느 패널로 만들었는지 트윈이 답하지 못했습니다. 인티그레이션 레인이
|
|
15
|
+
* 원본과 저널의 건수를 맞춰 보다 찾았습니다 — 4건이 나갔고 3건이 앉았습니다.
|
|
16
|
+
*
|
|
17
|
+
* 아래 값은 그 두 봉투에서 온 것입니다. 지어낸 모양이 아닙니다.
|
|
18
|
+
*/
|
|
19
|
+
import test from 'node:test'
|
|
20
|
+
import assert from 'node:assert/strict'
|
|
21
|
+
|
|
22
|
+
import { factIdentity } from '../server/engine/ingest-dedupe.js'
|
|
23
|
+
|
|
24
|
+
const AT = '2026-09-05T05:00:00.000Z'
|
|
25
|
+
|
|
26
|
+
const transformation = (inQty: number, outClass: string, outQty: number) => ({
|
|
27
|
+
eventId: `evt-${outClass}-${outQty}`,
|
|
28
|
+
eventType: 'epcis.TransformationEvent',
|
|
29
|
+
eventTime: AT,
|
|
30
|
+
data: {
|
|
31
|
+
type: 'TransformationEvent',
|
|
32
|
+
bizStep: 'urn:epcglobal:cbv:bizstep:commissioning',
|
|
33
|
+
transformationID: 'WO-20260831-001',
|
|
34
|
+
inputQuantityList: [{ epcClass: 'LOT-CT-SIDE-001', quantity: inQty }],
|
|
35
|
+
outputQuantityList: [{ epcClass: outClass, quantity: outQty }]
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('산출 로트가 다르면 다른 사실이다 — 이것이 실제로 없어진 자리다', () => {
|
|
40
|
+
const a = factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12))
|
|
41
|
+
const b = factIdentity(transformation(18, 'LOT-CT-MAIN-004', 9))
|
|
42
|
+
|
|
43
|
+
assert.ok(a, '정체를 만들지 못하면 dedupe 밖이다')
|
|
44
|
+
assert.notEqual(a, b, '같은 오더·같은 공정·같은 초라도 산출이 다르면 다른 변환이다')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('똑같은 봉투가 두 번 오면 한 사실이다 — 갈라 놓기만 하면 되풀이를 못 막는다', () => {
|
|
48
|
+
assert.equal(
|
|
49
|
+
factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12)),
|
|
50
|
+
factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12))
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('산출 이름이 같고 수량만 다르면 다른 사실이다 — 분할이 그 모양이다', () => {
|
|
55
|
+
const a = factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12))
|
|
56
|
+
const b = factIdentity(transformation(24, 'LOT-CT-MAIN-003', 6))
|
|
57
|
+
|
|
58
|
+
assert.notEqual(a, b)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('입력 수량만 다르면 다른 사실이다 — 산출이 같아도 소비가 다르다', () => {
|
|
62
|
+
const a = factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12))
|
|
63
|
+
const b = factIdentity(transformation(18, 'LOT-CT-MAIN-003', 12))
|
|
64
|
+
|
|
65
|
+
assert.notEqual(a, b)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('오더가 다르면 다른 사실이다 — 같은 공정을 두 오더로 같은 초에 돌릴 수 있다', () => {
|
|
69
|
+
const withTx = (tx: string) => {
|
|
70
|
+
const e = transformation(24, 'LOT-CT-MAIN-003', 12) as any
|
|
71
|
+
e.data.bizTransactionList = [{ type: 'urn:epcglobal:cbv:btt:prodorder', bizTransaction: tx }]
|
|
72
|
+
return factIdentity(e)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
assert.notEqual(withTx('urn:x:bt:WO-A'), withTx('urn:x:bt:WO-B'))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('담김의 자식이 다르면 다른 사실이다 — 한 종류만 고치면 나머지에 같은 결함이 남는다', () => {
|
|
79
|
+
const aggregation = (child: string) => ({
|
|
80
|
+
eventId: `agg-${child}`,
|
|
81
|
+
eventType: 'epcis.AggregationEvent',
|
|
82
|
+
eventTime: AT,
|
|
83
|
+
data: {
|
|
84
|
+
type: 'AggregationEvent',
|
|
85
|
+
action: 'ADD',
|
|
86
|
+
bizStep: 'urn:epcglobal:cbv:bizstep:packing',
|
|
87
|
+
parentID: 'urn:epc:id:sscc:0614141.1234567890',
|
|
88
|
+
childQuantityList: [{ epcClass: child, quantity: 10 }]
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
assert.notEqual(factIdentity(aggregation('LOT-A')), factIdentity(aggregation('LOT-B')))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('수량 목록 전체를 본다 — 첫 원소만 보면 뒤가 다른 둘이 뭉친다', () => {
|
|
96
|
+
const twoLines = (second: string) => ({
|
|
97
|
+
eventId: `evt-${second}`,
|
|
98
|
+
eventType: 'epcis.TransformationEvent',
|
|
99
|
+
eventTime: AT,
|
|
100
|
+
data: {
|
|
101
|
+
type: 'TransformationEvent',
|
|
102
|
+
bizStep: 'urn:epcglobal:cbv:bizstep:commissioning',
|
|
103
|
+
transformationID: 'WO-20260831-001',
|
|
104
|
+
outputQuantityList: [
|
|
105
|
+
{ epcClass: 'LOT-SAME', quantity: 1 },
|
|
106
|
+
{ epcClass: second, quantity: 1 }
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
assert.notEqual(factIdentity(twoLines('LOT-X')), factIdentity(twoLines('LOT-Y')))
|
|
112
|
+
})
|
|
@@ -47,7 +47,7 @@ test('무버: OEE 는 정수 퍼센트로 정형화하고 원시 누적은 빼
|
|
|
47
47
|
id: 'cutter-1',
|
|
48
48
|
kind: 'cutter',
|
|
49
49
|
status: 'down',
|
|
50
|
-
oee: { overall: 0.8234, availability: 0.9, performance: 0.95, quality: 0.96, goodCount: 12,
|
|
50
|
+
oee: { overall: 0.8234, availability: 0.9, performance: 0.95, quality: 0.96, goodCount: 12, nonconformingCount: 3, idleMs: 99999 }
|
|
51
51
|
}
|
|
52
52
|
]
|
|
53
53
|
}, 't1')
|
|
@@ -91,7 +91,8 @@ test('★ 같은 대상을 두 번 적지 않는다 — 출발지와 도착지
|
|
|
91
91
|
})
|
|
92
92
|
|
|
93
93
|
test('종류 목록이 닫혀 있다 — 열어 두면 넣는 쪽마다 다른 이름을 쓴다', () => {
|
|
94
|
-
|
|
94
|
+
/* `transaction` 은 2026-09-05 에 늘었다 — 거래가 오더 칸에 섞여 들어오던 것을 갈랐다. */
|
|
95
|
+
assert.deepEqual([...SUBJECT_KINDS], ['item', 'lot', 'order', 'location', 'equipment', 'task', 'transaction'])
|
|
95
96
|
})
|
|
96
97
|
|
|
97
98
|
test('너무 긴 식별자는 적지 않는다 — 자르면 그 줄이 다른 대상을 가리킨다', () => {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **밖을 가리키는 관계를 푸는 자리** — 선언은 있고 코드가 0곳이던 이음새.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 있었나 (2026-09-05 실측) ────────────────────────────────────────
|
|
5
|
+
* 계약이 `target: { kind: 'external', entity: … }` 를 선언하고 주석에 「해소는 호스트가 한다」고
|
|
6
|
+
* 적어 두었다. 그런데 `headless-twin/server` 어디에도 그것을 다루는 코드가 없었다(0곳).
|
|
7
|
+
*
|
|
8
|
+
* `space.area` 가 도는 것처럼 보인 이유는 이음새가 있어서가 아니라 **호스트에서 그것이 평범한 FK
|
|
9
|
+
* 라서**다 — 선언이 길을 낸 것이 아니다.
|
|
10
|
+
*
|
|
11
|
+
* ── 왜 지금 채웠나 ─────────────────────────────────────────────────────────
|
|
12
|
+
* 아침에는 「아무도 안 막혔으니 급하지 않다」로 적었고 **그 판단이 규모를 작게 잡은 것**이었다.
|
|
13
|
+
*
|
|
14
|
+
* 360 의 소비처는 무한히 늘어난다(사용자 방향). 그러면 각 제품의 자기 저장소가 관계의 원천이 된다.
|
|
15
|
+
* 그때 선언은 「밖을 가리킨다」고 말하는데 푸는 길이 없으면 **제품마다 자기 길을 낸다** — 그것이
|
|
16
|
+
* 정확히 「무한히 갈린다」다.
|
|
17
|
+
*/
|
|
18
|
+
import { test } from 'node:test'
|
|
19
|
+
import assert from 'node:assert/strict'
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
clearExternalResolvers,
|
|
23
|
+
externalIncoming,
|
|
24
|
+
externalResolverEntities,
|
|
25
|
+
registerExternalResolver,
|
|
26
|
+
resolveExternal
|
|
27
|
+
} from '../server/service/twin-model/external-resolver.js'
|
|
28
|
+
|
|
29
|
+
const AREA = 'space.area'
|
|
30
|
+
|
|
31
|
+
/* ── 못 풀면 못 푼다고 말한다 ────────────────────────────────────────────── */
|
|
32
|
+
|
|
33
|
+
test('★ 꽂힌 것이 없으면 「해소기가 없다」고 말한다 — 「없다」가 아니다', async () => {
|
|
34
|
+
/*
|
|
35
|
+
* 링크를 지우면 화면은 「그런 관계가 없다」로 읽는다. 실제로는 「아무도 답할 수 없다」다.
|
|
36
|
+
* 사람이 할 일이 다르다 — 앞은 자료를 채울 일이고 뒤는 해소기를 꽂을 일이다.
|
|
37
|
+
*/
|
|
38
|
+
clearExternalResolvers()
|
|
39
|
+
const got = await resolveExternal(AREA, 'd1', ['LINE-CAM'])
|
|
40
|
+
|
|
41
|
+
assert.equal(got.unresolved, 'no-resolver')
|
|
42
|
+
assert.equal(got.refs, undefined)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('★ 해소기가 터져도 360 을 죽이지 않는다 — 알리고 못 풀었다고 한다', async () => {
|
|
46
|
+
clearExternalResolvers()
|
|
47
|
+
const said: string[] = []
|
|
48
|
+
registerExternalResolver({ entity: AREA, resolve: async () => { throw new Error('DB down') } })
|
|
49
|
+
|
|
50
|
+
const got = await resolveExternal(AREA, 'd1', ['X'], m => said.push(m))
|
|
51
|
+
|
|
52
|
+
assert.equal(got.unresolved, 'failed')
|
|
53
|
+
assert.ok(said.some(m => m.includes('DB down')), '사유를 삼켰다')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/* ── 풀면 무엇인지 말한다 ────────────────────────────────────────────────── */
|
|
57
|
+
|
|
58
|
+
test('★ 이름과 갈 곳을 낸다 — 막다른 링크를 만들지 않으려고', async () => {
|
|
59
|
+
clearExternalResolvers()
|
|
60
|
+
registerExternalResolver({
|
|
61
|
+
entity: AREA,
|
|
62
|
+
resolve: async (_d, ids) => ids.map(id => ({ id, name: `라인 ${id}`, href: `/areas/${id}` }))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const got = await resolveExternal(AREA, 'd1', ['LINE-CAM'])
|
|
66
|
+
assert.equal(got.refs?.[0].name, '라인 LINE-CAM')
|
|
67
|
+
assert.equal(got.refs?.[0].href, '/areas/LINE-CAM')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('★ 없는 id 는 없다고 말한다 — 답에서 빼지 않는다', async () => {
|
|
71
|
+
/* 빼면 화면은 그 참조가 원래 없었던 것으로 읽는다. 실제로는 가리키는데 대상이 없는 것이다. */
|
|
72
|
+
clearExternalResolvers()
|
|
73
|
+
registerExternalResolver({ entity: AREA, resolve: async (_d, ids) => ids.map(id => ({ id, missing: true })) })
|
|
74
|
+
|
|
75
|
+
const got = await resolveExternal(AREA, 'd1', ['사라진구역'])
|
|
76
|
+
assert.equal(got.refs?.length, 1)
|
|
77
|
+
assert.equal(got.refs?.[0].missing, true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
/* ── 들어오는 쪽 ─────────────────────────────────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
test('★ 제품이 자기 저장소의 관계를 싣는다 — 커널은 그 표를 모른다', async () => {
|
|
83
|
+
clearExternalResolvers()
|
|
84
|
+
registerExternalResolver({
|
|
85
|
+
entity: 'mes.records',
|
|
86
|
+
incoming: async () => [
|
|
87
|
+
{ via: 'mes.rel.report', from: 'productionReports', tense: 'past', ids: ['PR-1', 'PR-2'], total: 2 },
|
|
88
|
+
{ via: 'mes.rel.maintenance', from: 'maintenancePlans', tense: 'future', ids: ['MP-1'], total: 1 }
|
|
89
|
+
]
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const got = await externalIncoming('d1', 'mes-miratek', 'equipment', 'EQ-CT-MWELD-01')
|
|
93
|
+
|
|
94
|
+
assert.equal(got.length, 2)
|
|
95
|
+
assert.deepEqual(got.map(g => g.tense).sort(), ['future', 'past'], '시간축이 안 실렸다')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('★ 시간축이 실린다 — 「걸린 것이 열」이 두 뜻이 되는 자리', async () => {
|
|
99
|
+
/*
|
|
100
|
+
* 지난 일 아홉 · 앞으로 올 일 하나와 그 반대는 「이 설비를 세워도 되나」의 답이 정반대다.
|
|
101
|
+
* 묶음이 스스로 말해야 화면이 가를 수 있다.
|
|
102
|
+
*/
|
|
103
|
+
clearExternalResolvers()
|
|
104
|
+
registerExternalResolver({
|
|
105
|
+
entity: 'mes.records',
|
|
106
|
+
incoming: async () => [{ via: 'mes.rel.report', from: 'x', tense: 'past', ids: ['a'], total: 9 }]
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const got = await externalIncoming('d1', 'i', 'equipment', 'E-1')
|
|
110
|
+
assert.equal(got[0].tense, 'past')
|
|
111
|
+
assert.equal(got[0].total, 9, '전체 수를 안 말한다 — 잘린 목록만으로는 규모를 모른다')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('빈 묶음은 싣지 않는다 — 그 제품이 이 축을 안 보는 것이고 할 말이 없다', async () => {
|
|
115
|
+
clearExternalResolvers()
|
|
116
|
+
registerExternalResolver({
|
|
117
|
+
entity: 'mes.records',
|
|
118
|
+
incoming: async () => [{ via: 'x', from: 'y', ids: [], total: 0 }]
|
|
119
|
+
})
|
|
120
|
+
assert.deepEqual(await externalIncoming('d1', 'i', 'equipment', 'E-1'), [])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('★ 한 제품이 터져도 나머지는 실린다', async () => {
|
|
124
|
+
clearExternalResolvers()
|
|
125
|
+
const said: string[] = []
|
|
126
|
+
registerExternalResolver({ entity: 'broken', incoming: async () => { throw new Error('boom') } })
|
|
127
|
+
registerExternalResolver({
|
|
128
|
+
entity: 'ok',
|
|
129
|
+
incoming: async () => [{ via: 'v', from: 'f', ids: ['a'], total: 1 }]
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const got = await externalIncoming('d1', 'i', 'equipment', 'E-1', m => said.push(m))
|
|
133
|
+
assert.equal(got.length, 1, '하나가 터져서 나머지도 안 실렸다')
|
|
134
|
+
assert.ok(said.some(m => m.includes('boom')))
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
/* ── 꽂는 규율 ───────────────────────────────────────────────────────────── */
|
|
138
|
+
|
|
139
|
+
test('★ 같은 대상을 두 제품이 답하려 하면 알린다 — 조용히 덮지 않는다', async () => {
|
|
140
|
+
clearExternalResolvers()
|
|
141
|
+
const said: string[] = []
|
|
142
|
+
registerExternalResolver({ entity: AREA, resolve: async () => [] })
|
|
143
|
+
registerExternalResolver({ entity: AREA, resolve: async () => [] }, m => said.push(m))
|
|
144
|
+
|
|
145
|
+
assert.ok(said.some(m => m.includes(AREA)), '덮어쓴 것을 안 알렸다')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('entity 없는 해소기는 안 꽂는다 — 아무 관계에도 안 붙는다', async () => {
|
|
149
|
+
clearExternalResolvers()
|
|
150
|
+
const said: string[] = []
|
|
151
|
+
registerExternalResolver({ entity: ' ' } as any, m => said.push(m))
|
|
152
|
+
|
|
153
|
+
assert.deepEqual(externalResolverEntities(), [])
|
|
154
|
+
assert.ok(said.length, '조용히 넘겼다')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('★ 무엇을 풀 수 있나를 밖에서 잴 수 있다 — 「모른다」와 「없다」를 가르려고', async () => {
|
|
158
|
+
clearExternalResolvers()
|
|
159
|
+
registerExternalResolver({ entity: 'b', resolve: async () => [] })
|
|
160
|
+
registerExternalResolver({ entity: 'a', resolve: async () => [] })
|
|
161
|
+
|
|
162
|
+
assert.deepEqual(externalResolverEntities(), ['a', 'b'])
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', async () => {
|
|
166
|
+
clearExternalResolvers()
|
|
167
|
+
assert.deepEqual(externalResolverEntities(), [], '지우기가 안 듣는다')
|
|
168
|
+
registerExternalResolver({ entity: AREA, resolve: async (_d, ids) => ids.map(id => ({ id })) })
|
|
169
|
+
const got = await resolveExternal(AREA, 'd', ['X'])
|
|
170
|
+
assert.equal(got.refs?.length, 1, '꽂았는데 안 불린다')
|
|
171
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* backfill 이 이미 있는 사실을 다시 앉히지 않게 하는 부분.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 있었나 (2026-09-06 측정) ─────────────────────────────────────────
|
|
5
|
+
* 중복 판정 기억은 프로세스 안에만 있다. 재기동하면 비워지고, 그 뒤 backfill 을 돌리면 이미 저널에
|
|
6
|
+
* 있는 사실도 「처음 본 것」이 된다.
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* mes-line-a 를 재기동 뒤 backfill
|
|
10
|
+
* 저널 44 → 83건 · 서로 다른 것 39가지 · 중복 44가지
|
|
11
|
+
* 같은 프로세스에서 다시 돌리면 applied 0 (기억이 살아 있어서)
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* 저널 조회는 DB 를 물어야 해서 여기서 다루지 않는다. 확인하는 것은 **어느 구간을 읽을지 정하는
|
|
15
|
+
* 판정**이다 — 그것이 틀리면 조회가 엉뚱한 곳을 보고 기억이 안 찬다.
|
|
16
|
+
*/
|
|
17
|
+
import { test } from 'node:test'
|
|
18
|
+
import assert from 'node:assert/strict'
|
|
19
|
+
|
|
20
|
+
import { spanOf } from '../server/service/reference/fill-span.js'
|
|
21
|
+
|
|
22
|
+
const at = (s: string) => ({ subjectId: 'L', decision: 'scrap', at: s })
|
|
23
|
+
|
|
24
|
+
test('★ 레코드가 덮는 구간을 정한다', () => {
|
|
25
|
+
const span = spanOf([at('2026-09-01T00:00:00.000Z'), at('2026-09-03T12:00:00.000Z'), at('2026-09-02T00:00:00.000Z')])
|
|
26
|
+
assert.ok(span)
|
|
27
|
+
assert.equal(span!.from.toISOString(), '2026-08-31T23:59:59.000Z')
|
|
28
|
+
assert.equal(span!.to.toISOString(), '2026-09-03T12:00:01.000Z')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('★ 양쪽을 1초씩 넓힌다 — 좁게 읽으면 경계의 사실이 다시 앉는다', () => {
|
|
32
|
+
/*
|
|
33
|
+
* 저장된 시각은 밀리초까지 있고 경계에서 같은 값이 갈릴 수 있다. 넓게 읽는 것은 기억이 조금 더
|
|
34
|
+
* 차는 것뿐이라 손해가 없다.
|
|
35
|
+
*/
|
|
36
|
+
const one = '2026-09-01T00:00:00.000Z'
|
|
37
|
+
const span = spanOf([at(one)])!
|
|
38
|
+
assert.equal(span.to.getTime() - span.from.getTime(), 2000)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('★ 하나도 시각을 안 말하면 구간이 없다 — 저널 전체를 읽지 않는다', () => {
|
|
42
|
+
/*
|
|
43
|
+
* 구간을 못 정하면 채우지 않는다. 저널 전체를 읽는 쪽으로 넓히면 2백만 줄을 훑게 되고, 그것이
|
|
44
|
+
* 필요한 상황이면 조회가 아니라 정체 색인으로 풀어야 한다.
|
|
45
|
+
*/
|
|
46
|
+
assert.equal(spanOf([{ subjectId: 'L', decision: 'scrap' }]), undefined)
|
|
47
|
+
assert.equal(spanOf([]), undefined)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('시각을 말하는 것과 안 말하는 것이 섞이면 말하는 것으로 정한다', () => {
|
|
51
|
+
const span = spanOf([{ subjectId: 'A' }, at('2026-09-01T00:00:00.000Z'), { subjectId: 'B' }])
|
|
52
|
+
assert.ok(span, '한 줄이라도 말하면 구간이 있다')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('★ 이 테스트가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
|
|
56
|
+
const narrow = spanOf([at('2026-09-01T00:00:00.000Z')])!
|
|
57
|
+
const wide = spanOf([at('2026-09-01T00:00:00.000Z'), at('2026-09-10T00:00:00.000Z')])!
|
|
58
|
+
assert.ok(wide.to.getTime() > narrow.to.getTime(), '구간이 레코드를 안 따라간다')
|
|
59
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 빠진 구간을 **언제** 다시 받나 — 세 자리의 규율.
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* 1 재기동 직후 한 번 우리가 내려가 있는 동안 그쪽이 보낸 것
|
|
6
|
+
* 2 구멍을 본 그 자리 번호가 비었다는 것을 아는 가장 이른 순간
|
|
7
|
+
* 3 10분 그물 위 둘이 다 놓치는 경우
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* 여기서 확인하는 것은 **두 번 부르지 않는 규율**이다. 밀어 받는 쪽이 구멍 뒤의 봉투를 계속 보내면
|
|
11
|
+
* 그때마다 구멍 판정이 나고, 막지 않으면 봉투마다 backfill 이 돈다.
|
|
12
|
+
*
|
|
13
|
+
* 실제 조회는 DB 와 어댑터를 물어야 해서 여기서 다루지 않는다.
|
|
14
|
+
*/
|
|
15
|
+
import { test } from 'node:test'
|
|
16
|
+
import assert from 'node:assert/strict'
|
|
17
|
+
|
|
18
|
+
import { FILL_DEBOUNCE_MS, FILL_PERIOD_MS } from '../server/service/reference/fill-cadence.js'
|
|
19
|
+
|
|
20
|
+
test('★ 그물은 10분이다 — 30초가 아니다', () => {
|
|
21
|
+
/*
|
|
22
|
+
* 인티그레이션 레인이 측정으로 준 권고다. backfill 은 원본에 조회를 걸고 저널을 읽는다.
|
|
23
|
+
* 30초마다 돌면 얻는 것 없이 두 쪽을 계속 친다 — 놓친 구간은 10분 뒤에 받아도 사실이 달라지지 않는다.
|
|
24
|
+
*
|
|
25
|
+
* 규칙 주기(30초)에 얹지 않은 이유이기도 하다. 얹으면 간격이 그쪽에 묶이고, 나중에 규칙 주기를
|
|
26
|
+
* 바꾸면 이쪽이 소리 없이 따라간다.
|
|
27
|
+
*/
|
|
28
|
+
assert.equal(FILL_PERIOD_MS, 10 * 60_000)
|
|
29
|
+
assert.notEqual(FILL_PERIOD_MS, 30_000, '규칙 주기와 같아졌다 — 그러면 하나를 바꿀 때 둘이 움직인다')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('★ 같은 단위를 1분 안에 두 번 안 받는다 — 봉투마다 돌면 안 된다', () => {
|
|
33
|
+
assert.equal(FILL_DEBOUNCE_MS, 60_000)
|
|
34
|
+
assert.ok(FILL_DEBOUNCE_MS < FILL_PERIOD_MS, '막는 간격이 그물보다 길면 그물이 헛돈다')
|
|
35
|
+
})
|