@things-factory/headless-twin 10.0.19 → 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 +12 -20
- 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 +206 -8
- package/dist-server/service/actuation/command-dispatcher.js +191 -14
- package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
- package/dist-server/service/actuation/command-store.d.ts +10 -2
- package/dist-server/service/actuation/command-store.js +168 -5
- package/dist-server/service/actuation/command-store.js.map +1 -1
- 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 +21 -2
- package/dist-server/service/actuation/index.js +21 -2
- package/dist-server/service/actuation/index.js.map +1 -1
- 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 +81 -0
- package/dist-server/service/actuation/twin-command.js +60 -0
- package/dist-server/service/actuation/twin-command.js.map +1 -1
- 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 +8 -0
- 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 +88 -4
- package/dist-server/service/reference/hook-contract.js +105 -4
- 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/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 +291 -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 +22 -0
- package/dist-server/service/reference/reference-hook.js +135 -55
- 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 +12 -24
- 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 +401 -20
- package/server/service/actuation/command-store.ts +182 -8
- 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 +21 -2
- 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 +128 -1
- package/server/service/actuation/twin-now.ts +97 -0
- package/server/service/index.ts +9 -1
- 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 +185 -5
- package/server/service/reference/hook-rejected.ts +53 -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 +327 -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 +162 -56
- 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 +16 -3
- 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 +1 -1
- 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 +1 -1
- 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,107 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **오더 식별자와 거래 식별자는 다른 칸에 든다** — 한 칸에 두 낱말이 들어가던 자리.
|
|
3
|
+
*
|
|
4
|
+
* ── 실측 (2026-09-05) ──────────────────────────────────────────────────────
|
|
5
|
+
* 승격 칸 `order_id` 하나가 두 낱말을 담고 있었다.
|
|
6
|
+
*
|
|
7
|
+
* order.status order-1 내부 오더 id
|
|
8
|
+
* epcis.TransformationEvent urn:epc:id:gdti:9521321.403.1 GS1 거래 식별자
|
|
9
|
+
*
|
|
10
|
+
* 한 오더의 이력이 **색인 안에서 두 쪽으로 갈라진다.** 어느 이름으로 물어도 절반만 나오고,
|
|
11
|
+
* 오류는 나지 않는다. 어휘가 섞인 행 10,004개.
|
|
12
|
+
*
|
|
13
|
+
* 그 갈라짐이 막은 것은 「이 개체를 어느 레시피로 만들었나」다 — 답은 오더 사건에 이미 있는데
|
|
14
|
+
* (`recipeKey`, 실측 328,021/328,021), 변환 사건에서 그 오더로 건너갈 색인이 없었다.
|
|
15
|
+
*
|
|
16
|
+
* ── 왜 이 검사인가 ────────────────────────────────────────────────────────
|
|
17
|
+
* 「합치면 편하다」는 힘이 계속 작용한다 — 값을 채우는 관점에서는 한 칸이 간단해 보이고, 실제로
|
|
18
|
+
* 그 이유로 합쳐져 있었다(옛 주석이 「오더를 스스로 말하는 사건은 그 말을 그대로 쓴다」고 적고
|
|
19
|
+
* 있었다). 그러니 갈라 둔 것을 **값으로** 못 박는다.
|
|
20
|
+
*/
|
|
21
|
+
import { test } from 'node:test'
|
|
22
|
+
import assert from 'node:assert/strict'
|
|
23
|
+
|
|
24
|
+
import { bizTransactionOf, orderOf, twinEventKeys } from '../server/service/twin-event/twin-event-keys.js'
|
|
25
|
+
import { SUBJECT_KINDS, subjectsOf } from '../server/service/twin-subject/event-subjects.js'
|
|
26
|
+
|
|
27
|
+
const GDTI = 'urn:epc:id:gdti:9521321.403.1'
|
|
28
|
+
|
|
29
|
+
/** 실측한 변환 사건의 모양 — GDTI 만 있고 오더 id 는 없다. */
|
|
30
|
+
const transformation = {
|
|
31
|
+
eventType: 'epcis.TransformationEvent',
|
|
32
|
+
data: {
|
|
33
|
+
type: 'TransformationEvent',
|
|
34
|
+
inputEPCList: ['urn:epc:id:sgtin:9521321.011111.1'],
|
|
35
|
+
outputEPCList: ['urn:epc:id:sgtin:9521321.033333.1'],
|
|
36
|
+
bizTransactionList: [{ type: 'urn:epcglobal:cbv:btt:prodorder', bizTransaction: GDTI }]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 실측한 오더 사건의 모양 — 둘 다 든다. 이것이 다리다. */
|
|
41
|
+
const orderStatus = {
|
|
42
|
+
eventType: 'order.status',
|
|
43
|
+
data: { orderId: 'order-1', status: 'created', recipeKey: 'trailer', bizTransaction: GDTI }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ── 갈라져 있나 ─────────────────────────────────────────────────────────── */
|
|
47
|
+
|
|
48
|
+
test('★ 변환 사건의 GDTI 는 거래 칸에 든다 — 오더 칸에 앉지 않는다', () => {
|
|
49
|
+
assert.equal(bizTransactionOf(transformation), GDTI)
|
|
50
|
+
assert.equal(orderOf(transformation), undefined, 'GDTI 가 오더 식별자 자리에 앉았다')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('★ 오더 사건은 두 칸을 다 든다 — 이것이 EPCIS 에서 오더로 건너가는 다리다', () => {
|
|
54
|
+
const keys = twinEventKeys(orderStatus)
|
|
55
|
+
assert.equal(keys.orderId, 'order-1')
|
|
56
|
+
assert.equal(keys.bizTransaction, GDTI)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('★ 두 사건이 같은 거래 값으로 이어진다 — 문자열까지 같아야 색인이 잇는다', () => {
|
|
60
|
+
/*
|
|
61
|
+
* 이 검사가 이 변경의 목적이다. 값이 한 글자라도 다르면 색인 조회가 0을 내고, 그 0은
|
|
62
|
+
* 「그런 오더가 없다」와 구별되지 않는다.
|
|
63
|
+
*/
|
|
64
|
+
assert.equal(twinEventKeys(transformation).bizTransaction, twinEventKeys(orderStatus).bizTransaction)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('★ 오더 사건에서 레시피를 읽을 수 있다 — 짐작하지 않아도 되는 근거', () => {
|
|
68
|
+
/* 자재로 되짚어 첫 레시피를 고르던 자리가 이 값으로 대체된다. */
|
|
69
|
+
assert.equal((orderStatus.data as any).recipeKey, 'trailer')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/* ── 합쳐지지 않았나 ──────────────────────────────────────────────────────── */
|
|
73
|
+
|
|
74
|
+
test('★ 오더 칸이 거래로 되돌아가지 않는다 — 옛 되돌림(fallback)이 살아나면 빨강', () => {
|
|
75
|
+
/*
|
|
76
|
+
* 옛 코드: `d.orderId ?? d.bizTransactionList?.[0]?.bizTransaction`.
|
|
77
|
+
* 그 한 줄이 돌아오면 아래가 GDTI 를 내고, 두 낱말이 다시 한 칸에 든다.
|
|
78
|
+
*/
|
|
79
|
+
const onlyTx = { eventType: 'epcis.ObjectEvent', data: { bizTransactionList: [{ bizTransaction: GDTI }] } }
|
|
80
|
+
assert.equal(orderOf(onlyTx), undefined, '오더 칸이 거래 값으로 되돌아간다')
|
|
81
|
+
assert.equal(bizTransactionOf(onlyTx), GDTI)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('★ 주체 표도 갈라 적는다 — 거래가 오더 이름으로 앉지 않는다', () => {
|
|
85
|
+
const subjects = subjectsOf(transformation)
|
|
86
|
+
const kinds = (id: string) => subjects.filter(s => s.id === id).map(s => s.kind)
|
|
87
|
+
|
|
88
|
+
assert.deepEqual(kinds(GDTI), ['transaction'], `GDTI 가 붙은 종류가 다르다: ${JSON.stringify(subjects)}`)
|
|
89
|
+
assert.ok(SUBJECT_KINDS.includes('transaction' as any), '거래 종류가 목록에 없다')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('★ 오더 사건은 두 주체를 다 낸다', () => {
|
|
93
|
+
const subjects = subjectsOf(orderStatus)
|
|
94
|
+
const has = (kind: string, id: string) => subjects.some(s => s.kind === kind && s.id === id)
|
|
95
|
+
|
|
96
|
+
assert.ok(has('order', 'order-1'), `오더 주체가 없다: ${JSON.stringify(subjects)}`)
|
|
97
|
+
assert.ok(has('transaction', GDTI), `거래 주체가 없다: ${JSON.stringify(subjects)}`)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
/* ── 검사가 실제로 무언가를 재나 ─────────────────────────────────────────── */
|
|
101
|
+
|
|
102
|
+
test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
|
|
103
|
+
const keys = twinEventKeys(orderStatus)
|
|
104
|
+
assert.ok(Object.values(keys).filter(v => v !== undefined).length >= 3, `승격 키를 ${JSON.stringify(keys)} 만 뽑았다`)
|
|
105
|
+
assert.ok(subjectsOf(orderStatus).length >= 2, '주체가 둘도 안 나왔다')
|
|
106
|
+
assert.ok(subjectsOf(transformation).length >= 1, '변환 사건에서 주체가 하나도 안 나왔다')
|
|
107
|
+
})
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **놓친 구간을 backfill 해 채운다** — 그 길이 없었다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 있었나 (2026-08-31, 인티그레이션 레인 지적) ────────────────────
|
|
5
|
+
* 번호로 「빠졌다」를 알아채게 만들었는데, 알아챈 뒤 **그 사이를 채우는 길이 없었다.** 설계가 적어 둔
|
|
6
|
+
* 길이고 양쪽이 다 읽었는데 만들지 않았다.
|
|
7
|
+
*
|
|
8
|
+
* ── 되풀이해 요청하는 경로에서 무너지는 것 ─────────────────────────────────────
|
|
9
|
+
* 행복한 길은 짧다. 무너지는 것은 **끝나지 않는 것**이다.
|
|
10
|
+
*
|
|
11
|
+
* 빈 답에 물어본 번호를 그대로 돌려주는 원본 → 무한히 요청한다
|
|
12
|
+
* 구멍 뒤를 이어 받는다 → 커서가 그 번호를 지나쳐 적히고 빠진 사실은 안 온다
|
|
13
|
+
* 상한에 닿았는데 조용히 끝낸다 → 사람은 다 받은 줄로 읽는다
|
|
14
|
+
*
|
|
15
|
+
* 셋 다 오류가 나지 않는다. 그래서 시험이 셋을 만든다.
|
|
16
|
+
*/
|
|
17
|
+
import { test } from 'node:test'
|
|
18
|
+
import assert from 'node:assert/strict'
|
|
19
|
+
|
|
20
|
+
import { fillSince } from '../server/service/reference/reference-fill.js'
|
|
21
|
+
import { pushCursorKey, seqOf } from '../server/service/reference/hook-contract.js'
|
|
22
|
+
import type { InboundBatch } from '../server/service/reference/reference-adapter.js'
|
|
23
|
+
|
|
24
|
+
const CFG = { baseUrl: 'x' }
|
|
25
|
+
const SITE = { siteId: 's-1' }
|
|
26
|
+
const SCOPE = 'EVENTS'
|
|
27
|
+
const KEY = pushCursorKey('twin-a', SCOPE)
|
|
28
|
+
|
|
29
|
+
/** 번호가 붙은 봉투 한 쪽. */
|
|
30
|
+
const page = (seqs: number[], scope: string = SCOPE): InboundBatch => ({
|
|
31
|
+
scope,
|
|
32
|
+
items: seqs.map(seq => ({ seq, record: { moverId: `eq-${seq}`, kind: 'press', status: 'idle' } }))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/** 저장본 하나 — 프로세스가 아니라 여기 산다. */
|
|
36
|
+
function harness(initial?: unknown) {
|
|
37
|
+
const state = { cursor: initial as unknown, saves: 0, ingested: [] as unknown[][] }
|
|
38
|
+
return {
|
|
39
|
+
state,
|
|
40
|
+
saveCursor: async (c: Record<string, unknown>) => {
|
|
41
|
+
state.saves++
|
|
42
|
+
state.cursor = JSON.parse(JSON.stringify(c))
|
|
43
|
+
},
|
|
44
|
+
ingest: (records: unknown[]) => {
|
|
45
|
+
state.ingested.push(records)
|
|
46
|
+
return { applied: records.length, duplicates: 0, rejected: [] }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const run = (h: ReturnType<typeof harness>, fetch: any, maxPages?: number) =>
|
|
52
|
+
fillSince({
|
|
53
|
+
cfg: CFG,
|
|
54
|
+
site: SITE,
|
|
55
|
+
scope: SCOPE,
|
|
56
|
+
cursorKey: KEY,
|
|
57
|
+
cursor: h.state.cursor,
|
|
58
|
+
fetch,
|
|
59
|
+
ingest: h.ingest,
|
|
60
|
+
saveCursor: h.saveCursor,
|
|
61
|
+
...(maxPages ? { maxPages } : {})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
/* ── 채운다 ───────────────────────────────────────────────────────────────── */
|
|
65
|
+
|
|
66
|
+
test('★ 커서 다음부터 요청하고 받은 데까지 커서를 올린다', async () => {
|
|
67
|
+
const h = harness({ streams: { [KEY]: { seq: 4 } } })
|
|
68
|
+
const asked: number[] = []
|
|
69
|
+
const out = await run(h, async (_c: any, _s: any, _sc: string, since: number) => {
|
|
70
|
+
asked.push(since)
|
|
71
|
+
return since === 4 ? page([5, 6]) : page([])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
assert.deepEqual(asked, [4, 6], '받은 데부터 다시 요청한다')
|
|
75
|
+
assert.equal(out.applied, 2)
|
|
76
|
+
assert.equal(out.stopped, 'done')
|
|
77
|
+
assert.equal(seqOf(h.state.cursor, KEY), 6)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('★ 커서가 없으면 0부터 요청한다. 처음 채우는 것이다', async () => {
|
|
81
|
+
const h = harness()
|
|
82
|
+
const asked: number[] = []
|
|
83
|
+
await run(h, async (_c: any, _s: any, _sc: string, since: number) => {
|
|
84
|
+
asked.push(since)
|
|
85
|
+
return since === 0 ? page([1, 2, 3]) : page([])
|
|
86
|
+
})
|
|
87
|
+
assert.equal(asked[0], 0)
|
|
88
|
+
assert.equal(seqOf(h.state.cursor, KEY), 3)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('여러 쪽을 이어 받는다', async () => {
|
|
92
|
+
const h = harness()
|
|
93
|
+
const out = await run(h, async (_c: any, _s: any, _sc: string, since: number) => {
|
|
94
|
+
if (since === 0) return page([1, 2])
|
|
95
|
+
if (since === 2) return page([3, 4])
|
|
96
|
+
return page([])
|
|
97
|
+
})
|
|
98
|
+
assert.equal(out.applied, 4)
|
|
99
|
+
assert.equal(out.pages >= 2, true)
|
|
100
|
+
assert.equal(seqOf(h.state.cursor, KEY), 4)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/* ── 끝나지 않는 것을 막는다 ──────────────────────────────────────────────── */
|
|
104
|
+
|
|
105
|
+
test('★ 번호가 나아가지 않으면 멈춘다 — 같은 것을 다시 주는 원본에 무한히 요청하지 않는다', async () => {
|
|
106
|
+
/*
|
|
107
|
+
* 아웃박스 계약이 「빈 묶음에는 물어본 번호를 그대로 돌려준다」이므로, 나아감을 보지 않고 요청하면
|
|
108
|
+
* 영원히 돈다. 실제로 그 계약을 쓰는 상대가 있다.
|
|
109
|
+
*/
|
|
110
|
+
const h = harness({ streams: { [KEY]: { seq: 4 } } })
|
|
111
|
+
let calls = 0
|
|
112
|
+
const out = await run(h, async () => {
|
|
113
|
+
calls++
|
|
114
|
+
/* 이미 받은 번호만 돌려준다 — 커서가 나아가지 않는다. */
|
|
115
|
+
return page([3, 4])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
assert.equal(out.stopped, 'no-progress')
|
|
119
|
+
assert.ok(calls <= 2, `무한히 요청했다 — ${calls}번`)
|
|
120
|
+
assert.match(String(out.detail), /나아가지 않았다/)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('★ 빈 답은 끝이고 오류가 아니다', async () => {
|
|
124
|
+
const h = harness({ streams: { [KEY]: { seq: 9 } } })
|
|
125
|
+
const out = await run(h, async () => page([]))
|
|
126
|
+
assert.equal(out.stopped, 'done')
|
|
127
|
+
assert.equal(out.applied, 0)
|
|
128
|
+
assert.equal(out.detail, undefined)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
/* ── 구멍 ─────────────────────────────────────────────────────────────────── */
|
|
132
|
+
|
|
133
|
+
test('★ 구멍이 있으면 그 앞까지만 넣고 멈춘다 — 이어 받으면 빠진 사실이 다시 오지 않는다', async () => {
|
|
134
|
+
const h = harness({ streams: { [KEY]: { seq: 4 } } })
|
|
135
|
+
const out = await run(h, async (_c: any, _s: any, _sc: string, since: number) =>
|
|
136
|
+
since === 4 ? page([5, 6, 9, 10]) : page([])
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
assert.equal(out.stopped, 'gap')
|
|
140
|
+
assert.equal(out.applied, 2, '5·6 만 넣는다')
|
|
141
|
+
assert.equal(seqOf(h.state.cursor, KEY), 6, '커서가 구멍을 지나치지 않는다')
|
|
142
|
+
assert.match(String(out.detail), /6 다음이 7/)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('★ 첫 봉투부터 구멍이면 아무것도 넣지 않고 커서도 그대로다', async () => {
|
|
146
|
+
const h = harness({ streams: { [KEY]: { seq: 4 } } })
|
|
147
|
+
const out = await run(h, async () => page([9, 10]))
|
|
148
|
+
|
|
149
|
+
assert.equal(out.stopped, 'gap')
|
|
150
|
+
assert.equal(out.applied, 0)
|
|
151
|
+
assert.equal(seqOf(h.state.cursor, KEY), 4)
|
|
152
|
+
assert.equal(h.state.ingested.length, 0, '넣을 것이 없는데 유입을 불렀다')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
/* ── 상한 ─────────────────────────────────────────────────────────────────── */
|
|
156
|
+
|
|
157
|
+
test('★ 쪽 상한에 닿으면 말한다 — 조용히 자르면 사람은 다 받은 줄로 읽는다', async () => {
|
|
158
|
+
const h = harness()
|
|
159
|
+
let n = 0
|
|
160
|
+
const out = await run(h, async () => page([++n]), 3)
|
|
161
|
+
|
|
162
|
+
assert.equal(out.stopped, 'page-limit')
|
|
163
|
+
assert.equal(out.pages, 3)
|
|
164
|
+
assert.match(String(out.detail), /남은 것이 있을 수 있다/)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
/* ── 거절 ─────────────────────────────────────────────────────────────────── */
|
|
168
|
+
|
|
169
|
+
test('★ backfill 이 실패하면 이유를 남기고 멈춘다 — 커서는 받은 데까지다', async () => {
|
|
170
|
+
const h = harness({ streams: { [KEY]: { seq: 4 } } })
|
|
171
|
+
const out = await run(h, async (_c: any, _s: any, _sc: string, since: number) => {
|
|
172
|
+
if (since === 4) return page([5])
|
|
173
|
+
throw new Error('MES 가 응답하지 않는다')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
assert.equal(out.stopped, 'refused')
|
|
177
|
+
assert.match(String(out.detail), /응답하지 않는다/)
|
|
178
|
+
assert.equal(seqOf(h.state.cursor, KEY), 5, '받은 것은 지킨다')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('★ 번호가 섞인 쪽은 받지 않는다 — 밀어 받는 길과 같은 검사를 지난다', async () => {
|
|
182
|
+
const h = harness()
|
|
183
|
+
const out = await run(h, async () => ({
|
|
184
|
+
scope: SCOPE,
|
|
185
|
+
items: [{ seq: 1, record: {} }, { record: {} }]
|
|
186
|
+
}))
|
|
187
|
+
|
|
188
|
+
assert.equal(out.stopped, 'refused')
|
|
189
|
+
assert.match(String(out.detail), /mixed batch/)
|
|
190
|
+
assert.equal(h.state.ingested.length, 0)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('★ 커서 열쇠에 트윈이 들어간다 — 한 연결의 두 공장이 계수기를 함께 쓰지 않는다', async () => {
|
|
194
|
+
const other = pushCursorKey('twin-b', SCOPE)
|
|
195
|
+
const h = harness({ streams: { [KEY]: { seq: 4 }, [other]: { seq: 100 } } })
|
|
196
|
+
await run(h, async (_c: any, _s: any, _sc: string, since: number) => (since === 4 ? page([5]) : page([])))
|
|
197
|
+
|
|
198
|
+
assert.equal(seqOf(h.state.cursor, KEY), 5)
|
|
199
|
+
assert.equal(seqOf(h.state.cursor, other), 100, '다른 공장의 번호를 건드렸다')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
/* ── 원본이 가진 번호 범위 ──────────────────────────────────────────────────── */
|
|
203
|
+
|
|
204
|
+
test('★ 원본이 말한 번호 범위를 그대로 낸다 — 커서와 견주려면 이 값이 있어야 한다', async () => {
|
|
205
|
+
const got = await fillSince({
|
|
206
|
+
cfg: {},
|
|
207
|
+
site: { siteId: 's' },
|
|
208
|
+
scope: 'EVENTS',
|
|
209
|
+
cursorKey: 'i::EVENTS',
|
|
210
|
+
cursor: {},
|
|
211
|
+
fetch: async () => ({ items: [{ seq: 1, record: {} }], scope: 'EVENTS', sourceRange: { first: 1, last: 20 } }),
|
|
212
|
+
ingest: () => ({ applied: 1, duplicates: 0, rejected: [] }),
|
|
213
|
+
saveCursor: async () => {}
|
|
214
|
+
})
|
|
215
|
+
assert.deepEqual(got.sourceRange, { first: 1, last: 20 })
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('★ 원본이 안 말하면 그 칸을 안 낸다 — 0 으로 채우면 「다 받았다」가 된다', async () => {
|
|
219
|
+
/*
|
|
220
|
+
* 0 을 넣으면 「원본이 1번부터 다 갖고 있다」로 읽히고, 그것이 거짓이면 「우리가 다 받았다」는
|
|
221
|
+
* 잘못된 결론이 나온다. 견주는 쪽은 「맞다」가 아니라 「모른다」로 답해야 한다.
|
|
222
|
+
*/
|
|
223
|
+
const got = await fillSince({
|
|
224
|
+
cfg: {},
|
|
225
|
+
site: { siteId: 's' },
|
|
226
|
+
scope: 'EVENTS',
|
|
227
|
+
cursorKey: 'i::EVENTS',
|
|
228
|
+
cursor: {},
|
|
229
|
+
fetch: async () => ({ items: [{ seq: 1, record: {} }], scope: 'EVENTS' }),
|
|
230
|
+
ingest: () => ({ applied: 1, duplicates: 0, rejected: [] }),
|
|
231
|
+
saveCursor: async () => {}
|
|
232
|
+
})
|
|
233
|
+
assert.equal(got.sourceRange, undefined)
|
|
234
|
+
assert.equal('sourceRange' in got, false, '없는 것을 undefined 로 실으면 화면이 칸을 그린다')
|
|
235
|
+
})
|
|
@@ -67,7 +67,7 @@ test('★ 그 연결이 만든 트윈만 받는다 — 남의 트윈에 밀어
|
|
|
67
67
|
|
|
68
68
|
test('★ 응답 코드 표가 계약에 있다 — 여기에 두 번째 벌을 만들지 않는다', () => {
|
|
69
69
|
/*
|
|
70
|
-
* 예전에는 이 표가 훅 규약 파일에 있었고 보내는 쪽(operato-
|
|
70
|
+
* 예전에는 이 표가 훅 규약 파일에 있었고 보내는 쪽(operato-plant)에도 있었다. 그래서 같은 409 를 양쪽이
|
|
71
71
|
* 다른 뜻으로 썼다 — 여기서는 「트윈이 안 돈다」, 저쪽에서는 「번호가 비었다」였다.
|
|
72
72
|
*
|
|
73
73
|
* 값을 여기서 다시 단정하지 않는다. 계약이 정본이고 그 시험은 계약 쪽에 있다. 여기서 지키는 것은
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 원본에만 있는 관계를 화면에 낼 수 있는지 판정하는 부분.
|
|
3
|
+
*
|
|
4
|
+
* 조회 자체는 DB 와 어댑터를 물어야 돌아가므로 테스트가 다루지 않는다. 여기서 확인하는 것은
|
|
5
|
+
* 원본이 준 값을 그대로 믿지 않는다는 것이다.
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
|
|
10
|
+
import { usableLinkGroups } from '../server/service/reference/reference-link-groups.js'
|
|
11
|
+
|
|
12
|
+
test('정상 그룹은 그대로 남는다', () => {
|
|
13
|
+
const got = usableLinkGroups([
|
|
14
|
+
{ via: 'mes.rel.report', from: 'productionReports', tense: 'past', ids: ['PR-1'], total: 2 }
|
|
15
|
+
])
|
|
16
|
+
assert.equal(got.length, 1)
|
|
17
|
+
assert.equal(got[0].tense, 'past')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('★ 전체 수가 없는 그룹은 버린다 — 화면이 목록 길이를 전체 수로 쓰게 된다', () => {
|
|
21
|
+
/*
|
|
22
|
+
* 원본이 20개만 보내고 전체 수를 빼면, 실제 200건이 화면에 20건으로 표시된다. 오류가 나지
|
|
23
|
+
* 않으므로 아무도 알아채지 못한다.
|
|
24
|
+
*/
|
|
25
|
+
const got = usableLinkGroups([{ via: 'mes.rel.report', from: 'x', ids: ['a', 'b'] } as any])
|
|
26
|
+
assert.equal(got.length, 0)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('전체 수가 0 인 그룹은 버린다 — 원본이 그 축을 취급하지 않는다는 뜻이다', () => {
|
|
30
|
+
assert.equal(usableLinkGroups([{ via: 'v', from: 'f', ids: [], total: 0 }]).length, 0)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('이름이 없는 그룹은 버린다 — 화면에 이름 없는 줄이 된다', () => {
|
|
34
|
+
const got = usableLinkGroups([
|
|
35
|
+
{ via: '', from: 'f', ids: ['a'], total: 1 },
|
|
36
|
+
{ via: ' ', from: 'f', ids: ['a'], total: 1 },
|
|
37
|
+
{ from: 'f', ids: ['a'], total: 1 } as any
|
|
38
|
+
])
|
|
39
|
+
assert.equal(got.length, 0)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('원본이 아무것도 안 주거나 목록에 빈 값이 섞여도 견딘다', () => {
|
|
43
|
+
assert.deepEqual(usableLinkGroups(undefined), [])
|
|
44
|
+
assert.deepEqual(usableLinkGroups(null), [])
|
|
45
|
+
assert.equal(usableLinkGroups([null as any, undefined as any]).length, 0)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('★ 이 테스트가 실제로 무언가를 확인한다 — 통과와 탈락이 둘 다 있어야 한다', () => {
|
|
49
|
+
const got = usableLinkGroups([
|
|
50
|
+
{ via: 'ok', from: 'f', ids: ['a'], total: 1 },
|
|
51
|
+
{ via: 'no-total', from: 'f', ids: ['a'] } as any,
|
|
52
|
+
{ via: '', from: 'f', ids: ['a'], total: 5 }
|
|
53
|
+
])
|
|
54
|
+
assert.equal(got.length, 1, '통과한 것이 하나여야 한다')
|
|
55
|
+
assert.equal(got[0].via, 'ok')
|
|
56
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **다시 넘길 것인가, 언제인가** — 일꾼의 판단 전부.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 이 판단이 필요한가 (2026-09-04) ─────────────────────────────────────
|
|
5
|
+
* 승인 직후에 한 번 보내는 것만 있었다. 그래서 **커밋과 발신 사이에 프로세스가 죽으면** 그
|
|
6
|
+
* 조치는 `approved` 로 남고 스스로 나가지 않았다. 그리고 실패한 것을 다시 보내는 것은 사람이
|
|
7
|
+
* 시켜야 했다.
|
|
8
|
+
*
|
|
9
|
+
* ── 실패가 다 같지 않다 (2026-09-02 실측) ──────────────────────────────────
|
|
10
|
+
* 저쪽 인증 정보가 없어 503 이었던 것(고치면 됨)과 `ITEM-1` 이 400 이었던 것(고쳐도 이 조치로는
|
|
11
|
+
* 안 됨)이 같은 모양이었다. 다 다시 하면 없는 자재를 끝없이 두드리고, 다 포기하면 저쪽을 고쳐
|
|
12
|
+
* 놓고도 안 나간다.
|
|
13
|
+
*/
|
|
14
|
+
import { test } from 'node:test'
|
|
15
|
+
import assert from 'node:assert/strict'
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
AFTER_FIX_PERIOD_MS,
|
|
19
|
+
AGAIN_BACKOFF_MS,
|
|
20
|
+
retryDecisionOf
|
|
21
|
+
} from '../server/service/actuation/retry-plan.js'
|
|
22
|
+
|
|
23
|
+
const NOW = 1_800_000_000_000
|
|
24
|
+
const at = (over: Record<string, unknown> = {}) =>
|
|
25
|
+
retryDecisionOf({ state: 'failed', dispatchable: true, nowMs: NOW, ...over } as any)
|
|
26
|
+
|
|
27
|
+
/* ── 첫 시도 — 승인 직후 발신이 놓친 것을 줍는다 ─────────────────────────── */
|
|
28
|
+
|
|
29
|
+
test('★ 승인됐는데 한 번도 안 나간 것은 바로 집는다 — 그 창이 이 일꾼의 존재 이유다', () => {
|
|
30
|
+
const d = at({ state: 'approved', attempts: 0 })
|
|
31
|
+
|
|
32
|
+
assert.equal(d.take, true)
|
|
33
|
+
assert.equal(d.attempt, 1)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('★ 첫 시도는 멱등을 묻지 않는다 — 한 번뿐이라 두 건이 설 수 없다', () => {
|
|
37
|
+
assert.equal(at({ state: 'approved', attempts: 0, idempotent: false }).take, true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('★ 첫 시도는 어댑터의 답을 묻지 않는다 — 아직 아무 답도 하지 않았다', () => {
|
|
41
|
+
assert.equal(at({ state: 'approved' }).take, true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/* ── 재시도 — 모르면 넘기지 않는다 ──────────────────────────────────────── */
|
|
45
|
+
|
|
46
|
+
test('★ 어댑터가 다시 될 일인지 말하지 않았으면 집지 않는다 — 「never」로 접지도 않는다', () => {
|
|
47
|
+
/*
|
|
48
|
+
* 모르는 것을 `never` 로 접으면 고칠 수 있는 것을 사람이 포기한다. 그래서 집지 않고 목록에
|
|
49
|
+
* 「모름」으로 남긴다 — 사람이 보고 정한다.
|
|
50
|
+
*/
|
|
51
|
+
const d = at({ attempts: 1, idempotent: true })
|
|
52
|
+
|
|
53
|
+
assert.equal(d.take, false)
|
|
54
|
+
assert.equal(d.skip, 'outlook-unknown')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('★ 멱등을 선언하지 않은 커넥터는 재시도하지 않는다 — 현장에 지시가 두 건 서면 안 된다', () => {
|
|
58
|
+
const d = at({ attempts: 1, retry: 'again' })
|
|
59
|
+
|
|
60
|
+
assert.equal(d.take, false)
|
|
61
|
+
assert.equal(d.skip, 'not-idempotent')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('★ 영원히 안 될 것은 집지 않는다 — 사람이 새 조치를 내야 한다', () => {
|
|
65
|
+
const d = at({ attempts: 1, retry: 'never', idempotent: true })
|
|
66
|
+
|
|
67
|
+
assert.equal(d.take, false)
|
|
68
|
+
assert.equal(d.skip, 'never', '우리 쪽에 고칠 것이 없다는 뜻이다')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
/* ── `again` — 짧게 세 번, 그다음 접는다 ────────────────────────────────── */
|
|
72
|
+
|
|
73
|
+
test('★ 물러서면서 세 번 한다', () => {
|
|
74
|
+
for (const [i, expected] of AGAIN_BACKOFF_MS.entries()) {
|
|
75
|
+
const d = at({ attempts: i, retry: 'again', idempotent: true, state: i === 0 ? 'approved' : 'failed' })
|
|
76
|
+
|
|
77
|
+
assert.equal(d.take, true, `${i + 1}번째를 안 했다`)
|
|
78
|
+
assert.equal(d.attempt, i + 1)
|
|
79
|
+
assert.equal(Date.parse(d.nextAttemptAt!) - NOW, expected, `${i + 1}번째 물러섬이 다르다`)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('★ 세 번을 다 쓰면 접는다 — 끝없이 두드리는 것은 소음이다', () => {
|
|
84
|
+
const d = at({ attempts: AGAIN_BACKOFF_MS.length, retry: 'again', idempotent: true })
|
|
85
|
+
|
|
86
|
+
assert.equal(d.take, false)
|
|
87
|
+
assert.equal(d.skip, 'attempts-spent')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
/* ── `after-fix` — 접지 않는다. 이 파일에서 가장 중요한 것 ──────────────── */
|
|
91
|
+
|
|
92
|
+
test('★ 설정 문제는 횟수로 접지 않는다 — 고쳐 놓고도 손으로 눌러야 하면 안 된다', () => {
|
|
93
|
+
/*
|
|
94
|
+
* 2026-09-02 사고가 그 모양이었다: 저쪽이 고쳐진 뒤 사람이 손으로 다시 넘겨서 나갔다.
|
|
95
|
+
* `after-fix` 는 재시도가 아니라 **기다리는 것**이다.
|
|
96
|
+
*/
|
|
97
|
+
for (const attempts of [1, 3, 10, 100]) {
|
|
98
|
+
const d = at({ attempts, retry: 'after-fix', idempotent: true })
|
|
99
|
+
|
|
100
|
+
assert.equal(d.take, true, `${attempts}번째에 접었다`)
|
|
101
|
+
assert.equal(Date.parse(d.nextAttemptAt!) - NOW, AFTER_FIX_PERIOD_MS, '간격이 늘어났다')
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
/* ── 물러섬을 표에서 읽는다 ─────────────────────────────────────────────── */
|
|
106
|
+
|
|
107
|
+
test('★ 아직 이르면 기다린다', () => {
|
|
108
|
+
const d = at({ attempts: 1, retry: 'again', idempotent: true, nextAttemptAt: new Date(NOW + 10_000).toISOString() })
|
|
109
|
+
|
|
110
|
+
assert.equal(d.take, false)
|
|
111
|
+
assert.equal(d.skip, 'backing-off')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('★ 때가 되면 집는다', () => {
|
|
115
|
+
const d = at({ attempts: 1, retry: 'again', idempotent: true, nextAttemptAt: new Date(NOW - 1).toISOString() })
|
|
116
|
+
|
|
117
|
+
assert.equal(d.take, true)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('★ 읽을 수 없는 시각으로 막지 않는다 — 막으면 그 조치가 표에 갇힌다', () => {
|
|
121
|
+
const d = at({ attempts: 1, retry: 'again', idempotent: true, nextAttemptAt: '언젠가' })
|
|
122
|
+
|
|
123
|
+
assert.equal(d.take, true)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/* ── 넘길 수 없는 상태 ──────────────────────────────────────────────────── */
|
|
127
|
+
|
|
128
|
+
test('★ 종착이면 아무것도 하지 않는다', () => {
|
|
129
|
+
const d = at({ state: 'acked', dispatchable: false, attempts: 1, retry: 'again', idempotent: true })
|
|
130
|
+
|
|
131
|
+
assert.equal(d.take, false)
|
|
132
|
+
assert.equal(d.skip, 'not-dispatchable')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('넘길 수 없으면 다른 어떤 값도 그것을 뒤집지 않는다', () => {
|
|
136
|
+
for (const retry of ['again', 'after-fix', 'never', undefined]) {
|
|
137
|
+
assert.equal(at({ dispatchable: false, attempts: 0, retry, idempotent: true }).skip, 'not-dispatchable')
|
|
138
|
+
}
|
|
139
|
+
})
|