@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.
Files changed (327) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +10 -1
  2. package/dist-server/engine/canonical-ingest.js +110 -33
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/event-time.d.ts +25 -0
  5. package/dist-server/engine/event-time.js +98 -0
  6. package/dist-server/engine/event-time.js.map +1 -0
  7. package/dist-server/engine/index.d.ts +2 -0
  8. package/dist-server/engine/index.js +23 -0
  9. package/dist-server/engine/index.js.map +1 -1
  10. package/dist-server/engine/ingest-dedupe.d.ts +18 -6
  11. package/dist-server/engine/ingest-dedupe.js +201 -15
  12. package/dist-server/engine/ingest-dedupe.js.map +1 -1
  13. package/dist-server/engine/ingest-health.d.ts +60 -1
  14. package/dist-server/engine/ingest-health.js +43 -8
  15. package/dist-server/engine/ingest-health.js.map +1 -1
  16. package/dist-server/engine/kpi-fold.d.ts +19 -0
  17. package/dist-server/engine/kpi-fold.js +4 -1
  18. package/dist-server/engine/kpi-fold.js.map +1 -1
  19. package/dist-server/engine/kpi-query.js +23 -3
  20. package/dist-server/engine/kpi-query.js.map +1 -1
  21. package/dist-server/engine/oee-accumulator.d.ts +35 -2
  22. package/dist-server/engine/oee-accumulator.js +56 -4
  23. package/dist-server/engine/oee-accumulator.js.map +1 -1
  24. package/dist-server/engine/twin-engine.d.ts +72 -3
  25. package/dist-server/engine/twin-engine.js +118 -5
  26. package/dist-server/engine/twin-engine.js.map +1 -1
  27. package/dist-server/index.js +21 -0
  28. package/dist-server/index.js.map +1 -1
  29. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
  30. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
  31. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
  32. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
  33. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
  34. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
  35. package/dist-server/migrations/index.js +5 -1
  36. package/dist-server/migrations/index.js.map +1 -1
  37. package/dist-server/routes.js +12 -20
  38. package/dist-server/routes.js.map +1 -1
  39. package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
  40. package/dist-server/service/actuation/actuation-advisor.js +53 -0
  41. package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
  42. package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
  43. package/dist-server/service/actuation/actuation-outcome.js +34 -0
  44. package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
  45. package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
  46. package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
  47. package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
  48. package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
  49. package/dist-server/service/actuation/actuation-rule.js +130 -0
  50. package/dist-server/service/actuation/actuation-rule.js.map +1 -0
  51. package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
  52. package/dist-server/service/actuation/approval-gateway.js +14 -0
  53. package/dist-server/service/actuation/approval-gateway.js.map +1 -0
  54. package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
  55. package/dist-server/service/actuation/catch-up-effects.js +120 -0
  56. package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
  57. package/dist-server/service/actuation/command-dispatcher.d.ts +206 -8
  58. package/dist-server/service/actuation/command-dispatcher.js +191 -14
  59. package/dist-server/service/actuation/command-dispatcher.js.map +1 -1
  60. package/dist-server/service/actuation/command-store.d.ts +10 -2
  61. package/dist-server/service/actuation/command-store.js +168 -5
  62. package/dist-server/service/actuation/command-store.js.map +1 -1
  63. package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
  64. package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
  65. package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
  66. package/dist-server/service/actuation/effect-match.d.ts +53 -0
  67. package/dist-server/service/actuation/effect-match.js +65 -0
  68. package/dist-server/service/actuation/effect-match.js.map +1 -0
  69. package/dist-server/service/actuation/index.d.ts +21 -2
  70. package/dist-server/service/actuation/index.js +21 -2
  71. package/dist-server/service/actuation/index.js.map +1 -1
  72. package/dist-server/service/actuation/retry-plan.d.ts +63 -0
  73. package/dist-server/service/actuation/retry-plan.js +99 -0
  74. package/dist-server/service/actuation/retry-plan.js.map +1 -0
  75. package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
  76. package/dist-server/service/actuation/rule-evaluate.js +205 -0
  77. package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
  78. package/dist-server/service/actuation/rule-loop.d.ts +5 -0
  79. package/dist-server/service/actuation/rule-loop.js +156 -0
  80. package/dist-server/service/actuation/rule-loop.js.map +1 -0
  81. package/dist-server/service/actuation/rule-runner.d.ts +52 -0
  82. package/dist-server/service/actuation/rule-runner.js +135 -0
  83. package/dist-server/service/actuation/rule-runner.js.map +1 -0
  84. package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
  85. package/dist-server/service/actuation/rule-wiring.js +80 -0
  86. package/dist-server/service/actuation/rule-wiring.js.map +1 -0
  87. package/dist-server/service/actuation/settle-effects.d.ts +21 -0
  88. package/dist-server/service/actuation/settle-effects.js +101 -0
  89. package/dist-server/service/actuation/settle-effects.js.map +1 -0
  90. package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
  91. package/dist-server/service/actuation/sweep-dispatch.js +125 -0
  92. package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
  93. package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
  94. package/dist-server/service/actuation/twin-command-resolver.js +406 -0
  95. package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
  96. package/dist-server/service/actuation/twin-command.d.ts +81 -0
  97. package/dist-server/service/actuation/twin-command.js +60 -0
  98. package/dist-server/service/actuation/twin-command.js.map +1 -1
  99. package/dist-server/service/actuation/twin-now.d.ts +41 -0
  100. package/dist-server/service/actuation/twin-now.js +65 -0
  101. package/dist-server/service/actuation/twin-now.js.map +1 -0
  102. package/dist-server/service/index.d.ts +5 -2
  103. package/dist-server/service/index.js +8 -0
  104. package/dist-server/service/index.js.map +1 -1
  105. package/dist-server/service/reference/actuation-routing.d.ts +60 -0
  106. package/dist-server/service/reference/actuation-routing.js +126 -0
  107. package/dist-server/service/reference/actuation-routing.js.map +1 -0
  108. package/dist-server/service/reference/actuation-target.d.ts +65 -0
  109. package/dist-server/service/reference/actuation-target.js +94 -0
  110. package/dist-server/service/reference/actuation-target.js.map +1 -0
  111. package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
  112. package/dist-server/service/reference/connection-portability-resolver.js +181 -0
  113. package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
  114. package/dist-server/service/reference/connection-portability.d.ts +138 -0
  115. package/dist-server/service/reference/connection-portability.js +230 -0
  116. package/dist-server/service/reference/connection-portability.js.map +1 -0
  117. package/dist-server/service/reference/fill-cadence.d.ts +17 -0
  118. package/dist-server/service/reference/fill-cadence.js +31 -0
  119. package/dist-server/service/reference/fill-cadence.js.map +1 -0
  120. package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
  121. package/dist-server/service/reference/fill-dedupe.js +63 -0
  122. package/dist-server/service/reference/fill-dedupe.js.map +1 -0
  123. package/dist-server/service/reference/fill-loop.d.ts +35 -0
  124. package/dist-server/service/reference/fill-loop.js +187 -0
  125. package/dist-server/service/reference/fill-loop.js.map +1 -0
  126. package/dist-server/service/reference/fill-span.d.ts +10 -0
  127. package/dist-server/service/reference/fill-span.js +37 -0
  128. package/dist-server/service/reference/fill-span.js.map +1 -0
  129. package/dist-server/service/reference/hook-contract.d.ts +88 -4
  130. package/dist-server/service/reference/hook-contract.js +105 -4
  131. package/dist-server/service/reference/hook-contract.js.map +1 -1
  132. package/dist-server/service/reference/hook-rejected.d.ts +25 -0
  133. package/dist-server/service/reference/hook-rejected.js +48 -0
  134. package/dist-server/service/reference/hook-rejected.js.map +1 -0
  135. package/dist-server/service/reference/index.d.ts +4 -1
  136. package/dist-server/service/reference/index.js +5 -1
  137. package/dist-server/service/reference/index.js.map +1 -1
  138. package/dist-server/service/reference/live-cadence.d.ts +68 -0
  139. package/dist-server/service/reference/live-cadence.js +44 -0
  140. package/dist-server/service/reference/live-cadence.js.map +1 -0
  141. package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
  142. package/dist-server/service/reference/live-feed-lease.js +56 -0
  143. package/dist-server/service/reference/live-feed-lease.js.map +1 -0
  144. package/dist-server/service/reference/live-ingest.d.ts +21 -0
  145. package/dist-server/service/reference/live-ingest.js +66 -0
  146. package/dist-server/service/reference/live-ingest.js.map +1 -0
  147. package/dist-server/service/reference/reference-adapter.d.ts +291 -2
  148. package/dist-server/service/reference/reference-adapter.js +19 -1
  149. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  150. package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
  151. package/dist-server/service/reference/reference-fill-run.js +142 -0
  152. package/dist-server/service/reference/reference-fill-run.js.map +1 -0
  153. package/dist-server/service/reference/reference-fill.d.ts +90 -0
  154. package/dist-server/service/reference/reference-fill.js +141 -0
  155. package/dist-server/service/reference/reference-fill.js.map +1 -0
  156. package/dist-server/service/reference/reference-hook.d.ts +22 -0
  157. package/dist-server/service/reference/reference-hook.js +135 -55
  158. package/dist-server/service/reference/reference-hook.js.map +1 -1
  159. package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
  160. package/dist-server/service/reference/reference-link-groups.js +15 -0
  161. package/dist-server/service/reference/reference-link-groups.js.map +1 -0
  162. package/dist-server/service/reference/reference-links.d.ts +14 -0
  163. package/dist-server/service/reference/reference-links.js +73 -0
  164. package/dist-server/service/reference/reference-links.js.map +1 -0
  165. package/dist-server/service/reference/reference-live.d.ts +1 -0
  166. package/dist-server/service/reference/reference-live.js +32 -6
  167. package/dist-server/service/reference/reference-live.js.map +1 -1
  168. package/dist-server/service/reference/reference-master.d.ts +56 -2
  169. package/dist-server/service/reference/reference-master.js +33 -3
  170. package/dist-server/service/reference/reference-master.js.map +1 -1
  171. package/dist-server/service/reference/reference-resolver.d.ts +27 -0
  172. package/dist-server/service/reference/reference-resolver.js +79 -1
  173. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  174. package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
  175. package/dist-server/service/twin-event/twin-event-keys.js +36 -4
  176. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  177. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  178. package/dist-server/service/twin-event/twin-event.js +11 -2
  179. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  180. package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
  181. package/dist-server/service/twin-model/choose-recipe.js +33 -0
  182. package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
  183. package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
  184. package/dist-server/service/twin-model/external-resolver.js +109 -0
  185. package/dist-server/service/twin-model/external-resolver.js.map +1 -0
  186. package/dist-server/service/twin-model/project-structure.js +16 -3
  187. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  188. package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
  189. package/dist-server/service/twin-model/twin-equipment.js +10 -0
  190. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  191. package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
  192. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  193. package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
  194. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  195. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  196. package/dist-server/service/twin-space/twin-space.d.ts +29 -0
  197. package/dist-server/service/twin-space/twin-space.js +13 -0
  198. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  199. package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
  200. package/dist-server/service/twin-subject/event-subjects.js +8 -1
  201. package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
  202. package/dist-shared/entity-delta.js +1 -1
  203. package/dist-shared/entity-delta.js.map +1 -1
  204. package/package.json +7 -7
  205. package/server/engine/canonical-ingest.ts +119 -33
  206. package/server/engine/event-time.ts +96 -0
  207. package/server/engine/index.ts +20 -0
  208. package/server/engine/ingest-dedupe.ts +203 -27
  209. package/server/engine/ingest-health.ts +91 -6
  210. package/server/engine/kpi-fold.ts +24 -1
  211. package/server/engine/kpi-query.ts +23 -4
  212. package/server/engine/oee-accumulator.ts +80 -8
  213. package/server/engine/twin-engine.ts +119 -5
  214. package/server/index.ts +23 -0
  215. package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
  216. package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
  217. package/server/migrations/index.ts +5 -1
  218. package/server/routes.ts +12 -24
  219. package/server/service/actuation/actuation-advisor.ts +124 -0
  220. package/server/service/actuation/actuation-outcome.ts +97 -0
  221. package/server/service/actuation/actuation-rule-resolver.ts +147 -0
  222. package/server/service/actuation/actuation-rule.ts +139 -0
  223. package/server/service/actuation/approval-gateway.ts +44 -0
  224. package/server/service/actuation/catch-up-effects.ts +152 -0
  225. package/server/service/actuation/command-dispatcher.ts +401 -20
  226. package/server/service/actuation/command-store.ts +182 -8
  227. package/server/service/actuation/dispatch-after-approval.ts +71 -0
  228. package/server/service/actuation/effect-match.ts +124 -0
  229. package/server/service/actuation/index.ts +21 -2
  230. package/server/service/actuation/retry-plan.ts +145 -0
  231. package/server/service/actuation/rule-evaluate.ts +352 -0
  232. package/server/service/actuation/rule-loop.ts +160 -0
  233. package/server/service/actuation/rule-runner.ts +216 -0
  234. package/server/service/actuation/rule-wiring.ts +84 -0
  235. package/server/service/actuation/settle-effects.ts +115 -0
  236. package/server/service/actuation/sweep-dispatch.ts +179 -0
  237. package/server/service/actuation/twin-command-resolver.ts +387 -0
  238. package/server/service/actuation/twin-command.ts +128 -1
  239. package/server/service/actuation/twin-now.ts +97 -0
  240. package/server/service/index.ts +9 -1
  241. package/server/service/reference/actuation-routing.ts +165 -0
  242. package/server/service/reference/actuation-target.ts +163 -0
  243. package/server/service/reference/connection-portability-resolver.ts +171 -0
  244. package/server/service/reference/connection-portability.ts +285 -0
  245. package/server/service/reference/fill-cadence.ts +29 -0
  246. package/server/service/reference/fill-dedupe.ts +66 -0
  247. package/server/service/reference/fill-loop.ts +153 -0
  248. package/server/service/reference/fill-span.ts +31 -0
  249. package/server/service/reference/hook-contract.ts +185 -5
  250. package/server/service/reference/hook-rejected.ts +53 -0
  251. package/server/service/reference/index.ts +5 -1
  252. package/server/service/reference/live-cadence.ts +129 -0
  253. package/server/service/reference/live-feed-lease.ts +56 -0
  254. package/server/service/reference/live-ingest.ts +94 -0
  255. package/server/service/reference/reference-adapter.ts +327 -5
  256. package/server/service/reference/reference-fill-run.ts +165 -0
  257. package/server/service/reference/reference-fill.ts +223 -0
  258. package/server/service/reference/reference-hook.ts +162 -56
  259. package/server/service/reference/reference-link-groups.ts +21 -0
  260. package/server/service/reference/reference-links.ts +77 -0
  261. package/server/service/reference/reference-live.ts +30 -5
  262. package/server/service/reference/reference-master.ts +90 -5
  263. package/server/service/reference/reference-resolver.ts +77 -2
  264. package/server/service/twin-event/twin-event-keys.ts +37 -4
  265. package/server/service/twin-event/twin-event.ts +18 -1
  266. package/server/service/twin-model/choose-recipe.ts +31 -0
  267. package/server/service/twin-model/external-resolver.ts +161 -0
  268. package/server/service/twin-model/project-structure.ts +16 -3
  269. package/server/service/twin-model/twin-equipment.ts +22 -0
  270. package/server/service/twin-model/twin-lineage-query.ts +82 -1
  271. package/server/service/twin-model/twin-model-item-query.ts +95 -4
  272. package/server/service/twin-model/twin-model-tree-query.ts +1 -1
  273. package/server/service/twin-space/twin-space.ts +39 -0
  274. package/server/service/twin-subject/event-subjects.ts +8 -1
  275. package/shared/entity-delta.ts +1 -1
  276. package/test/ack-shape-alignment.test.ts +97 -0
  277. package/test/actuation-approval-door.test.ts +421 -0
  278. package/test/actuation-dispatch.test.ts +16 -3
  279. package/test/actuation-effect.test.ts +162 -0
  280. package/test/actuation-outcome.test.ts +152 -0
  281. package/test/actuation-rule-runner.test.ts +281 -0
  282. package/test/actuation-rule.test.ts +266 -0
  283. package/test/actuation-seam.test.ts +249 -0
  284. package/test/actuation-settle.test.ts +164 -0
  285. package/test/actuation-target.test.ts +145 -0
  286. package/test/canonical-ingest-vocabularies.test.ts +49 -2
  287. package/test/capability-mapping.test.ts +1 -1
  288. package/test/choose-recipe.test.ts +117 -0
  289. package/test/command-store-writes-columns.test.ts +126 -0
  290. package/test/connection-portability-doors.test.ts +87 -0
  291. package/test/connector-capability-declaration.test.ts +54 -4
  292. package/test/declaration-reaches-model.test.ts +43 -0
  293. package/test/dedupe-transformation.test.ts +112 -0
  294. package/test/entity-delta.test.ts +1 -1
  295. package/test/event-subjects.test.ts +2 -1
  296. package/test/external-resolver.test.ts +171 -0
  297. package/test/fill-dedupe.test.ts +59 -0
  298. package/test/fill-loop.test.ts +35 -0
  299. package/test/first-envelope.test.ts +153 -0
  300. package/test/fold-shift-dst.test.ts +110 -0
  301. package/test/home-in-area.test.ts +90 -0
  302. package/test/hook-rejected-shape.test.ts +86 -0
  303. package/test/hook-response-sum.test.ts +193 -0
  304. package/test/hook-sequence.test.ts +1 -1
  305. package/test/ingest-dedupe.test.ts +56 -0
  306. package/test/ingest-expected-quiet.test.ts +111 -0
  307. package/test/ingest-idempotent.test.ts +79 -0
  308. package/test/ingest-reconcile-four-ways.test.ts +97 -0
  309. package/test/ingest-rules-header.test.ts +100 -0
  310. package/test/ingest-window-rolls-unobserved.test.ts +72 -0
  311. package/test/intake-mapping-parity.test.ts +175 -0
  312. package/test/live-cadence.test.ts +170 -0
  313. package/test/live-feed-lease.test.ts +63 -0
  314. package/test/live-mirror-parity.test.ts +1 -1
  315. package/test/oee-accumulator.test.ts +3 -3
  316. package/test/oee-site-calendar.test.ts +146 -0
  317. package/test/order-and-transaction-split.test.ts +107 -0
  318. package/test/reference-fill.test.ts +235 -0
  319. package/test/reference-hook.test.ts +1 -1
  320. package/test/reference-links.test.ts +56 -0
  321. package/test/retry-plan.test.ts +139 -0
  322. package/test/sweep-dispatch.test.ts +191 -0
  323. package/test/twin-event-keys.test.ts +20 -6
  324. package/test/twin-model-item-db.test.ts +6 -6
  325. package/test/twin-model-tree-db.test.ts +6 -6
  326. package/tsconfig.shared.tsbuildinfo +1 -1
  327. 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-mes)에도 있었다. 그래서 같은 409 를 양쪽이
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
+ })