@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
|
@@ -12,7 +12,18 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { test } from 'node:test'
|
|
14
14
|
import assert from 'node:assert/strict'
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
import { WEBHOOK_STATUS, webhookSenderAction } from '@operato/ops-contract'
|
|
17
|
+
import { signWebhook, WEBHOOK_HEADER } from '@operato/ops-contract/webhook'
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
authorizeHook,
|
|
21
|
+
producedInstance,
|
|
22
|
+
secretFromHeaders,
|
|
23
|
+
secretMatches,
|
|
24
|
+
seqOf,
|
|
25
|
+
withSeq
|
|
26
|
+
} from '../server/service/reference/hook-contract.js'
|
|
16
27
|
|
|
17
28
|
test('★ 비밀값을 두 가지 방식으로 받는다 — 밀어 주는 쪽의 관습이 갈린다', () => {
|
|
18
29
|
assert.equal(secretFromHeaders({ 'x-twin-hook-secret': 'abc' }), 'abc')
|
|
@@ -54,33 +65,113 @@ test('★ 그 연결이 만든 트윈만 받는다 — 남의 트윈에 밀어
|
|
|
54
65
|
assert.equal(producedInstance({}, 'twin-a'), undefined)
|
|
55
66
|
})
|
|
56
67
|
|
|
57
|
-
test('★ 응답
|
|
68
|
+
test('★ 응답 코드 표가 계약에 있다 — 여기에 두 번째 벌을 만들지 않는다', () => {
|
|
58
69
|
/*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
70
|
+
* 예전에는 이 표가 훅 규약 파일에 있었고 보내는 쪽(operato-plant)에도 있었다. 그래서 같은 409 를 양쪽이
|
|
71
|
+
* 다른 뜻으로 썼다 — 여기서는 「트윈이 안 돈다」, 저쪽에서는 「번호가 비었다」였다.
|
|
72
|
+
*
|
|
73
|
+
* 값을 여기서 다시 단정하지 않는다. 계약이 정본이고 그 시험은 계약 쪽에 있다. 여기서 지키는 것은
|
|
74
|
+
* **이 파일이 계약을 쓰고 있는가**다.
|
|
62
75
|
*/
|
|
63
|
-
assert.equal(
|
|
64
|
-
assert.equal(
|
|
65
|
-
assert.equal(HOOK_STATUS.badSecret, 401)
|
|
66
|
-
assert.equal(HOOK_STATUS.unknownTarget, 404)
|
|
67
|
-
assert.equal(HOOK_STATUS.notLive, 409)
|
|
68
|
-
assert.equal(HOOK_STATUS.failed, 500)
|
|
69
|
-
assert.equal(HOOK_STATUS.unsupported, 501)
|
|
70
|
-
|
|
71
|
-
/* 재시도해도 같은 것에 5xx 를 주면 안 된다 — 그쪽이 영원히 다시 보낸다. */
|
|
72
|
-
for (const code of [HOOK_STATUS.badPayload, HOOK_STATUS.badSecret, HOOK_STATUS.unknownTarget, HOOK_STATUS.notLive]) {
|
|
73
|
-
assert.ok(code < 500, `${code} 는 4xx 여야 한다`)
|
|
74
|
-
}
|
|
75
|
-
/* 우리 쪽 문제에는 5xx 를 준다 — 그쪽이 다시 보내야 그 사실이 살아남는다. */
|
|
76
|
-
assert.ok(HOOK_STATUS.failed >= 500)
|
|
76
|
+
assert.equal(WEBHOOK_STATUS.sequenceGap, 409, '409 는 번호가 비었다는 뜻이다')
|
|
77
|
+
assert.equal(WEBHOOK_STATUS.notLive, 503, '트윈이 안 도는 것은 503 이다')
|
|
77
78
|
})
|
|
78
79
|
|
|
79
80
|
test('★ 트윈이 돌지 않으면 200 을 주지 않는다 — 그쪽이 그 사실을 버린다', () => {
|
|
80
81
|
/*
|
|
81
|
-
* 200 은 「받아서 반영했다」는
|
|
82
|
-
*
|
|
82
|
+
* 200 은 「받아서 반영했다」는 뜻이다. 트윈이 실시간으로 돌지 않아 한 건도 반영되지 않았는데 200 을
|
|
83
|
+
* 주면, 밀어 주는 쪽은 전달된 줄 알고 그 사실을 버린다.
|
|
84
|
+
*
|
|
85
|
+
* 그렇다고 4xx 를 주면 「보낸 요청이 잘못됐다」는 뜻이라 그쪽이 포기할 근거가 된다. 트윈은 누가
|
|
86
|
+
* 띄우면 다시 도므로 들고 기다리는 것이 맞다.
|
|
87
|
+
*/
|
|
88
|
+
assert.notEqual(WEBHOOK_STATUS.notLive, WEBHOOK_STATUS.ok)
|
|
89
|
+
assert.equal(webhookSenderAction(WEBHOOK_STATUS.notLive), 'wait')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
/* ── 인증 — 비밀값과 서명 ─────────────────────────────────────────────────── */
|
|
93
|
+
|
|
94
|
+
const SECRET = 'shared-secret'
|
|
95
|
+
const NOW = Date.parse('2026-08-31T02:00:00.000Z')
|
|
96
|
+
const TS = '2026-08-31T02:00:00.000Z'
|
|
97
|
+
const BODY = '{"batch":[{"seq":1,"scope":"EVENTS","record":{}}]}'
|
|
98
|
+
|
|
99
|
+
test('★ 서명이 오면 서명을 본다', () => {
|
|
100
|
+
const headers = {
|
|
101
|
+
[WEBHOOK_HEADER.timestamp]: TS,
|
|
102
|
+
[WEBHOOK_HEADER.signature]: signWebhook(SECRET, TS, BODY)
|
|
103
|
+
}
|
|
104
|
+
assert.deepEqual(authorizeHook({ headers, rawBody: BODY, secret: SECRET, nowMs: NOW }), {
|
|
105
|
+
ok: true,
|
|
106
|
+
method: 'signature'
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('★ 서명이 없으면 비밀값을 본다 — 이미 붙어 있는 커넥터가 그 방식이다', () => {
|
|
111
|
+
const got = authorizeHook({ headers: { 'x-twin-hook-secret': SECRET }, rawBody: BODY, secret: SECRET, nowMs: NOW })
|
|
112
|
+
assert.deepEqual(got, { ok: true, method: 'secret' })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('★ 서명이 왔는데 받은 바이트가 없으면 거절한다 — 확인할 수 없는 것을 통과시키지 않는다', () => {
|
|
116
|
+
/*
|
|
117
|
+
* 파싱한 객체를 다시 문자열로 만들어 확인하고 싶어지는데, 키 순서와 공백이 달라져 어차피 맞지 않는다.
|
|
118
|
+
* 맞지 않는 것을 통과시키면 서명이 있으나 마나 한 것이 된다.
|
|
83
119
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
const headers = {
|
|
121
|
+
[WEBHOOK_HEADER.timestamp]: TS,
|
|
122
|
+
[WEBHOOK_HEADER.signature]: signWebhook(SECRET, TS, BODY)
|
|
123
|
+
}
|
|
124
|
+
const got = authorizeHook({ headers, rawBody: undefined, secret: SECRET, nowMs: NOW })
|
|
125
|
+
assert.equal(got.ok, false)
|
|
126
|
+
assert.match((got as any).reason, /raw body/)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('★ 서명이 왔으면 비밀값 헤더로 넘어가지 않는다 — 맞는 비밀값을 함께 실어도 서명이 틀리면 거절이다', () => {
|
|
130
|
+
const headers = {
|
|
131
|
+
'x-twin-hook-secret': SECRET,
|
|
132
|
+
[WEBHOOK_HEADER.timestamp]: TS,
|
|
133
|
+
[WEBHOOK_HEADER.signature]: 'deadbeef'
|
|
134
|
+
}
|
|
135
|
+
const got = authorizeHook({ headers, rawBody: BODY, secret: SECRET, nowMs: NOW })
|
|
136
|
+
assert.equal(got.ok, false)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('거절 사유가 무엇을 고칠지 말한다', () => {
|
|
140
|
+
const stale = new Date(NOW - 10 * 60_000).toISOString()
|
|
141
|
+
const headers = {
|
|
142
|
+
[WEBHOOK_HEADER.timestamp]: stale,
|
|
143
|
+
[WEBHOOK_HEADER.signature]: signWebhook(SECRET, stale, BODY)
|
|
144
|
+
}
|
|
145
|
+
const got = authorizeHook({ headers, rawBody: BODY, secret: SECRET, nowMs: NOW })
|
|
146
|
+
assert.deepEqual(got, { ok: false, reason: 'signature expired' })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
/* ── 커서에 번호를 적는 자리 ──────────────────────────────────────────────── */
|
|
150
|
+
|
|
151
|
+
test('★ 번호는 폴링이 쓰는 커서와 같은 자리에 산다 — 진행 위치를 두 벌로 두지 않는다', () => {
|
|
152
|
+
const before = { streams: { EVENTS: { since: '2026-08-30T00:00:00.000Z', seen: ['a'] } }, firstAttachedAt: 'x' }
|
|
153
|
+
const after = withSeq(before, 'EVENTS', 918342)
|
|
154
|
+
|
|
155
|
+
assert.equal(seqOf(after, 'EVENTS'), 918342)
|
|
156
|
+
assert.equal((after as any).streams.EVENTS.since, before.streams.EVENTS.since, 'since 를 지우지 않는다')
|
|
157
|
+
assert.deepEqual((after as any).streams.EVENTS.seen, ['a'], 'seen 을 지우지 않는다')
|
|
158
|
+
assert.equal((after as any).firstAttachedAt, 'x', '커서의 다른 항목을 지우지 않는다')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('★ 다른 흐름의 번호를 건드리지 않는다 — 한 단위를 올리면서 다른 단위를 되돌리면 그 구간이 다시 온다', () => {
|
|
162
|
+
const before = { streams: { EVENTS: { seq: 10 }, MASTER: { seq: 4 } } }
|
|
163
|
+
const after = withSeq(before, 'EVENTS', 11)
|
|
164
|
+
assert.equal(seqOf(after, 'EVENTS'), 11)
|
|
165
|
+
assert.equal(seqOf(after, 'MASTER'), 4)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('번호가 없는 흐름은 「모른다」다 — 0 으로 메우지 않는다', () => {
|
|
169
|
+
assert.equal(seqOf(undefined, 'EVENTS'), undefined)
|
|
170
|
+
assert.equal(seqOf({}, 'EVENTS'), undefined)
|
|
171
|
+
assert.equal(seqOf({ streams: {} }, 'EVENTS'), undefined)
|
|
172
|
+
assert.equal(seqOf({ streams: { EVENTS: { since: 'x' } } }, 'EVENTS'), undefined)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('커서가 없던 연결에도 번호를 적을 수 있다', () => {
|
|
176
|
+
assert.equal(seqOf(withSeq(undefined, 'EVENTS', 1), 'EVENTS'), 1)
|
|
86
177
|
})
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ **그물이 실제로 줍는가** — 배선.
|
|
3
|
+
*
|
|
4
|
+
* 판단은 순수 함수가 지킨다(`retry-plan.test.ts`). 여기서 보는 것은 **그 답이 실제 경로에 서
|
|
5
|
+
* 있는가**다: 표를 읽었나, 어댑터를 불렀나, 시도 횟수와 다음 시각을 적었나, 한 조치가 터져도
|
|
6
|
+
* 다음을 보나.
|
|
7
|
+
*
|
|
8
|
+
* 이 저장소에서 가장 자주 나는 결함이 「자리는 있고 길이 없다」이고, 오늘 하루 그 부류를 여러 번
|
|
9
|
+
* 고쳤다. 그물이 그 부류로 나면 **승인된 조치가 조용히 안 나간다.**
|
|
10
|
+
*/
|
|
11
|
+
import { test } from 'node:test'
|
|
12
|
+
import assert from 'node:assert/strict'
|
|
13
|
+
|
|
14
|
+
import { sweepPendingDispatch } from '../server/service/actuation/sweep-dispatch.js'
|
|
15
|
+
import type { CommandStore, StoredCommand } from '../server/service/actuation/command-dispatcher.js'
|
|
16
|
+
|
|
17
|
+
const DOMAIN = 'd-1'
|
|
18
|
+
const AT = '2026-09-04T02:00:00.000Z'
|
|
19
|
+
|
|
20
|
+
const cmd = (over: Partial<StoredCommand> = {}): StoredCommand =>
|
|
21
|
+
({
|
|
22
|
+
id: 'cmd-1',
|
|
23
|
+
instanceId: 'twin-a',
|
|
24
|
+
kind: 'production.release',
|
|
25
|
+
origin: 'rule',
|
|
26
|
+
state: 'approved',
|
|
27
|
+
reversible: false,
|
|
28
|
+
/* 되돌릴 수 없는 조치는 **승인 기록**이 있어야 넘어간다(§`asApproved`) — 그 모양이 이것이다. */
|
|
29
|
+
approval: { by: 'lead', at: AT },
|
|
30
|
+
...over
|
|
31
|
+
}) as StoredCommand
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 표 하나와 가짜 커넥터 — `save` 가 무엇을 적었는지 본다.
|
|
35
|
+
*
|
|
36
|
+
* 의존을 인자로 받는 서명이라 여기서 다 흉내낼 수 있다. 직접 부르는 서명이었을 때는 이 시험이
|
|
37
|
+
* DB·모듈 부팅을 끌어와 아예 뜨지 않았다 — 그것이 이 파일을 그 모양으로 바꾼 이유다.
|
|
38
|
+
*/
|
|
39
|
+
function bench(rows: StoredCommand[], over: Record<string, unknown> = {}) {
|
|
40
|
+
const table = new Map(rows.map(r => [r.id, { ...r }]))
|
|
41
|
+
const saves: StoredCommand[] = []
|
|
42
|
+
const store: CommandStore = {
|
|
43
|
+
load: async (_d, id) => (table.has(id) ? { ...table.get(id)! } : null),
|
|
44
|
+
save: async (_d, c) => {
|
|
45
|
+
saves.push({ ...c })
|
|
46
|
+
table.set(c.id, { ...c })
|
|
47
|
+
},
|
|
48
|
+
pendingDispatch: async () => [...table.values()]
|
|
49
|
+
} as any
|
|
50
|
+
|
|
51
|
+
const calls: string[] = []
|
|
52
|
+
const deps = {
|
|
53
|
+
store,
|
|
54
|
+
/* 어댑터가 늘 받아 준다 — 「집었나」를 보는 시험이라 저쪽 사정은 다른 파일이 지킨다. */
|
|
55
|
+
actuate: async (c: any) => {
|
|
56
|
+
calls.push(c.id)
|
|
57
|
+
return { ok: true, ref: `WO-${c.id}` }
|
|
58
|
+
},
|
|
59
|
+
idempotent: async () => false,
|
|
60
|
+
...over
|
|
61
|
+
} as any
|
|
62
|
+
return { store, table, saves, deps, calls }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ── 첫 시도를 줍는다 ─────────────────────────────────────────────────────── */
|
|
66
|
+
|
|
67
|
+
test('★ 승인만 되고 안 나간 것을 줍는다 — 승인 직후 발신이 놓친 그 창이다', async () => {
|
|
68
|
+
const { table, deps, calls } = bench([cmd()])
|
|
69
|
+
|
|
70
|
+
const report = await sweepPendingDispatch(DOMAIN, deps)
|
|
71
|
+
/* 어댑터가 없으므로 `routeActuation` 이 거절한다 — 그래도 **집었다**는 것이 이 시험의 주제다. */
|
|
72
|
+
assert.equal(report.seen, 1)
|
|
73
|
+
assert.equal(table.get('cmd-1')!.state !== 'approved', true, '집지 않아 상태가 그대로다')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('★ 시도 횟수와 다음 시각을 적는다 — 메모리에 두면 재기동에 사라진다', async () => {
|
|
77
|
+
const { table, deps, calls } = bench([cmd()])
|
|
78
|
+
|
|
79
|
+
await sweepPendingDispatch(DOMAIN, deps)
|
|
80
|
+
const after = table.get('cmd-1')!
|
|
81
|
+
|
|
82
|
+
assert.equal(after.attempts, 1, '첫 시도가 1로 적혀야 한다')
|
|
83
|
+
assert.ok(after.nextAttemptAt, '다음 시각이 없으면 물러섬이 재기동에 사라진다')
|
|
84
|
+
assert.ok(Date.parse(after.nextAttemptAt!) > Date.now(), '다음 시각이 과거다')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
/* ── 집지 않는 것들 ───────────────────────────────────────────────────────── */
|
|
88
|
+
|
|
89
|
+
test('★ 종착은 집지 않는다', async () => {
|
|
90
|
+
const { saves, deps } = bench([cmd({ state: 'acked', attempts: 1 })])
|
|
91
|
+
|
|
92
|
+
const report = await sweepPendingDispatch(DOMAIN, deps)
|
|
93
|
+
|
|
94
|
+
assert.equal(report.dispatched, 0)
|
|
95
|
+
assert.equal(report.skipped['not-dispatchable'], 1)
|
|
96
|
+
assert.deepEqual(saves, [], '집지 않았는데 표를 고쳤다')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('★ 어댑터가 다시 될 일인지 말하지 않았으면 집지 않는다 — 목록에 「모름」으로 남는다', async () => {
|
|
100
|
+
const { deps, store } = bench([cmd({ state: 'failed', attempts: 1, error: '저쪽이 안 받았습니다' })])
|
|
101
|
+
|
|
102
|
+
const report = await sweepPendingDispatch(DOMAIN, deps)
|
|
103
|
+
|
|
104
|
+
assert.equal(report.skipped['outlook-unknown'], 1)
|
|
105
|
+
assert.equal(report.dispatched, 0)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('★ 멱등을 선언하지 않은 커넥터는 재시도하지 않는다', async () => {
|
|
109
|
+
const { deps, store } = bench([cmd({ state: 'failed', attempts: 1, retry: 'again' })])
|
|
110
|
+
|
|
111
|
+
const report = await sweepPendingDispatch(DOMAIN, deps)
|
|
112
|
+
|
|
113
|
+
assert.equal(report.skipped['not-idempotent'], 1)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('★ 영원히 안 될 것은 집지 않는다', async () => {
|
|
117
|
+
const { deps, store } = bench([cmd({ state: 'failed', attempts: 1, retry: 'never' })])
|
|
118
|
+
|
|
119
|
+
assert.equal((await sweepPendingDispatch(DOMAIN, deps)).skipped['never'], 1)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('★ 물러섬이 안 끝났으면 기다린다', async () => {
|
|
123
|
+
const later = new Date(Date.now() + 300_000).toISOString()
|
|
124
|
+
const { saves, deps } = bench([cmd({ state: 'failed', attempts: 1, retry: 'again', nextAttemptAt: later })])
|
|
125
|
+
|
|
126
|
+
const report = await sweepPendingDispatch(DOMAIN, deps)
|
|
127
|
+
|
|
128
|
+
/* 멱등 선언이 없어 그쪽에서 먼저 걸린다 — 어느 이유든 **집지 않았고 표를 안 고쳤다**가 핵심이다. */
|
|
129
|
+
assert.equal(report.dispatched, 0)
|
|
130
|
+
assert.deepEqual(saves, [])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
/* ── 저장소가 못 내면 아무것도 하지 않는다 ──────────────────────────────── */
|
|
134
|
+
|
|
135
|
+
test('★ 대기 줄을 못 내는 저장소에서는 아무것도 하지 않는다 — 옛 거동이 깨지지 않게', async () => {
|
|
136
|
+
const deps = { store: { load: async () => null, save: async () => {} }, actuate: async () => ({ ok: true }), idempotent: async () => false } as any
|
|
137
|
+
|
|
138
|
+
const report = await sweepPendingDispatch(DOMAIN, deps)
|
|
139
|
+
|
|
140
|
+
assert.deepEqual(report, { seen: 0, dispatched: 0, skipped: {} })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('★ 표 읽기가 터져도 그물이 죽지 않는다', async () => {
|
|
144
|
+
const deps = {
|
|
145
|
+
store: {
|
|
146
|
+
load: async () => null,
|
|
147
|
+
save: async () => {},
|
|
148
|
+
pendingDispatch: async () => {
|
|
149
|
+
throw new Error('DB 가 없다')
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
actuate: async () => ({ ok: true }),
|
|
153
|
+
idempotent: async () => false
|
|
154
|
+
} as any
|
|
155
|
+
|
|
156
|
+
const report = await sweepPendingDispatch(DOMAIN, deps)
|
|
157
|
+
assert.equal(report.seen, 0)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
/* ── 한 조치가 터져도 다음을 본다 ───────────────────────────────────────── */
|
|
161
|
+
|
|
162
|
+
test('★ 한 조치에서 터져도 다음 조치를 본다 — 그물이 한 건에 멈추면 뒤가 다 막힌다', async () => {
|
|
163
|
+
const { deps, store } = bench([cmd({ id: 'a' }), cmd({ id: 'b' })])
|
|
164
|
+
const original = store.save
|
|
165
|
+
let first = true
|
|
166
|
+
;(store as any).save = async (d: string, c: StoredCommand) => {
|
|
167
|
+
if (first) {
|
|
168
|
+
first = false
|
|
169
|
+
throw new Error('첫 저장이 터졌다')
|
|
170
|
+
}
|
|
171
|
+
return original!(d, c)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const report = await sweepPendingDispatch(DOMAIN, deps)
|
|
175
|
+
|
|
176
|
+
assert.equal(report.seen, 2, '둘 다 읽었다')
|
|
177
|
+
/* 하나가 터졌어도 바퀴가 끝났다 — 던지면 이 단정에 오지 못한다. */
|
|
178
|
+
assert.ok(report.dispatched + Object.values(report.skipped).reduce((a, b) => a + b, 0) >= 1)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
/* ── 한 바퀴 상한 ─────────────────────────────────────────────────────────── */
|
|
182
|
+
|
|
183
|
+
test('★ 대기 줄이 커도 한 주기를 독차지하지 않는다', async () => {
|
|
184
|
+
const many = Array.from({ length: 120 }, (_, i) => cmd({ id: `c-${i}`, state: 'acked', attempts: 1 }))
|
|
185
|
+
const { deps, store } = bench(many)
|
|
186
|
+
|
|
187
|
+
const report = await sweepPendingDispatch(DOMAIN, deps)
|
|
188
|
+
|
|
189
|
+
assert.equal(report.seen, 120, '표는 다 읽는다')
|
|
190
|
+
assert.equal(report.skipped['not-dispatchable'], 50, '한 바퀴에 보는 것은 상한까지다')
|
|
191
|
+
})
|
|
@@ -15,7 +15,7 @@ import assert from 'node:assert/strict'
|
|
|
15
15
|
import { createRequire } from 'node:module'
|
|
16
16
|
|
|
17
17
|
const req = createRequire(import.meta.url)
|
|
18
|
-
const { bizStepOf, epcOf, orderOf, locationOf, equipmentIdOf, twinEventKeys } = req('../dist-server/service/twin-event/twin-event-keys.js')
|
|
18
|
+
const { bizStepOf, epcOf, orderOf, bizTransactionOf, locationOf, equipmentIdOf, twinEventKeys } = req('../dist-server/service/twin-event/twin-event-keys.js')
|
|
19
19
|
|
|
20
20
|
/** EPCIS ObjectEvent 한 건(입고) — 실제 인제스트가 싣는 모양. */
|
|
21
21
|
const EPCIS_OBJECT = {
|
|
@@ -31,12 +31,18 @@ const EPCIS_OBJECT = {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
test('EPCIS 이벤트에서
|
|
34
|
+
test('EPCIS 이벤트에서 축이 모두 뽑히고, 거래는 거래 칸에 든다', () => {
|
|
35
35
|
const k = twinEventKeys(EPCIS_OBJECT)
|
|
36
36
|
assert.equal(k.bizStep, 'receiving')
|
|
37
37
|
assert.equal(k.epc, 'urn:epc:id:sgtin:0614141.107346.2018')
|
|
38
|
-
assert.equal(k.orderId, 'urn:epc:id:gdti:0614141.402.2')
|
|
39
38
|
assert.equal(k.locationId, 'urn:epc:id:sgln:0614141.00777.DOCK-3')
|
|
39
|
+
/*
|
|
40
|
+
* ── 이 기대가 옮겨졌다 (2026-09-05 사용자 결정) ────────────────────────────
|
|
41
|
+
* 전에는 이 GDTI 가 `orderId` 로 뽑혔다. 그래서 한 오더의 이력이 색인 안에서 두 쪽으로
|
|
42
|
+
* 갈라졌다 — 운영 델타는 `order-1`, EPCIS 는 GDTI. 이제 칸이 둘이다.
|
|
43
|
+
*/
|
|
44
|
+
assert.equal(k.bizTransaction, 'urn:epc:id:gdti:0614141.402.2')
|
|
45
|
+
assert.equal(k.orderId, undefined, '거래 식별자가 오더 칸에 앉았다')
|
|
40
46
|
})
|
|
41
47
|
|
|
42
48
|
test('식별자는 끝마디가 아니라 전체를 저장한다 — 부분일치 검색이 양방향으로 성립해야 한다', () => {
|
|
@@ -86,8 +92,16 @@ test('★ 운영 델타의 오더는 `orderId` 다 — 이 시험이 결함을
|
|
|
86
92
|
orderOf({ data: { orderId: 'WO-1', bizTransactionList: [{ bizTransaction: 'urn:epc:id:gdti:0614141.402.2' }] } }),
|
|
87
93
|
'WO-1'
|
|
88
94
|
)
|
|
89
|
-
/*
|
|
90
|
-
|
|
95
|
+
/*
|
|
96
|
+
* ── 이 줄이 뒤집혔다 (2026-09-05 사용자 결정) ──────────────────────────────
|
|
97
|
+
* 전에는 오더를 말하지 않는 EPCIS 가 **거래로 내려갔다.** 그래서 한 칸에 두 낱말이 들었고,
|
|
98
|
+
* 한 오더의 이력이 색인 안에서 두 쪽으로 갈라졌다(실측: 어휘가 섞인 행 10,004개).
|
|
99
|
+
*
|
|
100
|
+
* 바로 위 줄이 그 위험을 이미 알고 있었다 — 「거래와 오더는 같은 것이 아닐 수 있다」. 알면서
|
|
101
|
+
* 같은 칸에 넣고 있었다. 이제 갈라 든다.
|
|
102
|
+
*/
|
|
103
|
+
assert.equal(orderOf({ data: { bizTransactionList: [{ bizTransaction: 'PO-9' }] } }), undefined)
|
|
104
|
+
assert.equal(bizTransactionOf({ data: { bizTransactionList: [{ bizTransaction: 'PO-9' }] } }), 'PO-9')
|
|
91
105
|
})
|
|
92
106
|
|
|
93
107
|
test('★ 승격 키는 **커널이 실제로 내는 델타 모양**으로 검사한다', () => {
|
|
@@ -156,7 +170,7 @@ test('상한을 넘는 값은 오류 없이 사라지지 않는다 — 잘리되
|
|
|
156
170
|
})
|
|
157
171
|
|
|
158
172
|
test('빈 봉투에도 터지지 않는다', () => {
|
|
159
|
-
const empty = { action: undefined, bizStep: undefined, epc: undefined, orderId: undefined, locationId: undefined, moverId: undefined }
|
|
173
|
+
const empty = { action: undefined, bizStep: undefined, epc: undefined, orderId: undefined, bizTransaction: undefined, locationId: undefined, moverId: undefined }
|
|
160
174
|
assert.deepEqual(twinEventKeys(undefined), empty)
|
|
161
175
|
assert.deepEqual(twinEventKeys({}), empty)
|
|
162
176
|
})
|
|
@@ -153,13 +153,13 @@ test('이어진 것은 양방향이고 선언된 관계만 따른다', async ()
|
|
|
153
153
|
const loc = await q.twinModelItem(INSTANCE, 'locations', 'weld-station', ctx)
|
|
154
154
|
|
|
155
155
|
/* 들어오는 쪽 — 이 자리를 기준으로 삼는 설비 둘. 선언(`equipment.homeLocation`)이 알려 준다. */
|
|
156
|
-
const home = loc.links.incoming.find((l: any) => l.via === '
|
|
156
|
+
const home = loc.links.incoming.find((l: any) => l.via === 'ops.rel.home' && l.from === 'equipment')
|
|
157
157
|
assert.ok(home, '나를 가리키는 관계가 보여야 한 걸음씩 걸을 수 있다')
|
|
158
158
|
assert.deepEqual(home.ids.sort(), ['welder-1', 'welder-2'])
|
|
159
159
|
assert.equal(home.total, 2)
|
|
160
160
|
|
|
161
161
|
/* 나가는 쪽 — 자리는 구역을 가리킨다. 대상이 **축 밖**(공간의 구역)이라는 사실을 그대로 낸다. */
|
|
162
|
-
const area = loc.links.out.find((l: any) => l.via === '
|
|
162
|
+
const area = loc.links.out.find((l: any) => l.via === 'ops.rel.area')
|
|
163
163
|
assert.ok(area, '커널이 "밖" 이라 부른 참조도 이어진 것으로 낸다')
|
|
164
164
|
assert.equal(area.target.kind, 'external', '갈 곳이 다르므로 화면이 링크를 만들지 말지 알 수 있어야 한다')
|
|
165
165
|
})
|
|
@@ -168,13 +168,13 @@ test('문서에만 사는 항목의 관계도 선언대로 이어진다', async
|
|
|
168
168
|
const recipe = await q.twinModelItem(INSTANCE, 'recipes', 'frame-recipe', ctx)
|
|
169
169
|
assert.equal(recipe.provenance.basis, 'document')
|
|
170
170
|
|
|
171
|
-
const route = recipe.links.out.find((l: any) => l.via === '
|
|
171
|
+
const route = recipe.links.out.find((l: any) => l.via === 'ops.rel.route')
|
|
172
172
|
assert.deepEqual(route.ids, ['frame-route'])
|
|
173
173
|
|
|
174
174
|
/* `inputs[].material` — 배열을 지나는 경로가 선언의 문법이고, 그것을 따르는 것이 선언을 존중하는 것이다. */
|
|
175
|
-
const input = recipe.links.out.find((l: any) => l.via === '
|
|
175
|
+
const input = recipe.links.out.find((l: any) => l.via === 'ops.rel.input')
|
|
176
176
|
assert.deepEqual(input.ids, ['steel'])
|
|
177
|
-
const output = recipe.links.out.find((l: any) => l.via === '
|
|
177
|
+
const output = recipe.links.out.find((l: any) => l.via === 'ops.rel.output')
|
|
178
178
|
assert.deepEqual(output.ids, ['frame'])
|
|
179
179
|
})
|
|
180
180
|
|
|
@@ -216,7 +216,7 @@ test('지도의 선이 진단이 된다 — 이어진 것뿐 아니라 **끊긴
|
|
|
216
216
|
await TwinEngine.ingestMaster(domainId, bad, 'reset')
|
|
217
217
|
|
|
218
218
|
const sum = await new TwinModelQuery().twinModelSummary(INSTANCE, ctx)
|
|
219
|
-
const home = sum.edges.find((e: any) => e.from === 'equipment' && e.to === 'locations' && e.via === '
|
|
219
|
+
const home = sum.edges.find((e: any) => e.from === 'equipment' && e.to === 'locations' && e.via === 'ops.rel.home')
|
|
220
220
|
assert.ok(home, '설비→자리 선이 있어야 한다')
|
|
221
221
|
assert.equal(home.linked, 3, '제대로 이어진 설비 셋')
|
|
222
222
|
assert.equal(home.dangling, 1, '없는 자리를 가리키는 설비 하나 — 가장 나쁜 부류이므로 따로 센다')
|