@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.
Files changed (331) 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 +19 -16
  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 +230 -0
  58. package/dist-server/service/actuation/command-dispatcher.js +269 -0
  59. package/dist-server/service/actuation/command-dispatcher.js.map +1 -0
  60. package/dist-server/service/actuation/command-store.d.ts +11 -0
  61. package/dist-server/service/actuation/command-store.js +219 -0
  62. package/dist-server/service/actuation/command-store.js.map +1 -0
  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 +25 -0
  70. package/dist-server/service/actuation/index.js +30 -0
  71. package/dist-server/service/actuation/index.js.map +1 -0
  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 +111 -0
  97. package/dist-server/service/actuation/twin-command.js +170 -0
  98. package/dist-server/service/actuation/twin-command.js.map +1 -0
  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 +28 -17
  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 +149 -22
  130. package/dist-server/service/reference/hook-contract.js +236 -23
  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/hook-store.d.ts +3 -0
  136. package/dist-server/service/reference/hook-store.js +28 -0
  137. package/dist-server/service/reference/hook-store.js.map +1 -0
  138. package/dist-server/service/reference/index.d.ts +4 -1
  139. package/dist-server/service/reference/index.js +5 -1
  140. package/dist-server/service/reference/index.js.map +1 -1
  141. package/dist-server/service/reference/live-cadence.d.ts +68 -0
  142. package/dist-server/service/reference/live-cadence.js +44 -0
  143. package/dist-server/service/reference/live-cadence.js.map +1 -0
  144. package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
  145. package/dist-server/service/reference/live-feed-lease.js +56 -0
  146. package/dist-server/service/reference/live-feed-lease.js.map +1 -0
  147. package/dist-server/service/reference/live-ingest.d.ts +21 -0
  148. package/dist-server/service/reference/live-ingest.js +66 -0
  149. package/dist-server/service/reference/live-ingest.js.map +1 -0
  150. package/dist-server/service/reference/reference-adapter.d.ts +355 -2
  151. package/dist-server/service/reference/reference-adapter.js +19 -1
  152. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  153. package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
  154. package/dist-server/service/reference/reference-fill-run.js +142 -0
  155. package/dist-server/service/reference/reference-fill-run.js.map +1 -0
  156. package/dist-server/service/reference/reference-fill.d.ts +90 -0
  157. package/dist-server/service/reference/reference-fill.js +141 -0
  158. package/dist-server/service/reference/reference-fill.js.map +1 -0
  159. package/dist-server/service/reference/reference-hook.d.ts +64 -6
  160. package/dist-server/service/reference/reference-hook.js +265 -27
  161. package/dist-server/service/reference/reference-hook.js.map +1 -1
  162. package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
  163. package/dist-server/service/reference/reference-link-groups.js +15 -0
  164. package/dist-server/service/reference/reference-link-groups.js.map +1 -0
  165. package/dist-server/service/reference/reference-links.d.ts +14 -0
  166. package/dist-server/service/reference/reference-links.js +73 -0
  167. package/dist-server/service/reference/reference-links.js.map +1 -0
  168. package/dist-server/service/reference/reference-live.d.ts +1 -0
  169. package/dist-server/service/reference/reference-live.js +32 -6
  170. package/dist-server/service/reference/reference-live.js.map +1 -1
  171. package/dist-server/service/reference/reference-master.d.ts +56 -2
  172. package/dist-server/service/reference/reference-master.js +33 -3
  173. package/dist-server/service/reference/reference-master.js.map +1 -1
  174. package/dist-server/service/reference/reference-resolver.d.ts +27 -0
  175. package/dist-server/service/reference/reference-resolver.js +79 -1
  176. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  177. package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
  178. package/dist-server/service/twin-event/twin-event-keys.js +36 -4
  179. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  180. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  181. package/dist-server/service/twin-event/twin-event.js +11 -2
  182. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  183. package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
  184. package/dist-server/service/twin-model/choose-recipe.js +33 -0
  185. package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
  186. package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
  187. package/dist-server/service/twin-model/external-resolver.js +109 -0
  188. package/dist-server/service/twin-model/external-resolver.js.map +1 -0
  189. package/dist-server/service/twin-model/project-structure.js +16 -3
  190. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  191. package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
  192. package/dist-server/service/twin-model/twin-equipment.js +10 -0
  193. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  194. package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
  195. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  196. package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
  197. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  198. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  199. package/dist-server/service/twin-space/twin-space.d.ts +29 -0
  200. package/dist-server/service/twin-space/twin-space.js +13 -0
  201. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  202. package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
  203. package/dist-server/service/twin-subject/event-subjects.js +8 -1
  204. package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
  205. package/dist-shared/entity-delta.js +1 -1
  206. package/dist-shared/entity-delta.js.map +1 -1
  207. package/package.json +7 -7
  208. package/server/engine/canonical-ingest.ts +119 -33
  209. package/server/engine/event-time.ts +96 -0
  210. package/server/engine/index.ts +20 -0
  211. package/server/engine/ingest-dedupe.ts +203 -27
  212. package/server/engine/ingest-health.ts +91 -6
  213. package/server/engine/kpi-fold.ts +24 -1
  214. package/server/engine/kpi-query.ts +23 -4
  215. package/server/engine/oee-accumulator.ts +80 -8
  216. package/server/engine/twin-engine.ts +119 -5
  217. package/server/index.ts +23 -0
  218. package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
  219. package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
  220. package/server/migrations/index.ts +5 -1
  221. package/server/routes.ts +19 -20
  222. package/server/service/actuation/actuation-advisor.ts +124 -0
  223. package/server/service/actuation/actuation-outcome.ts +97 -0
  224. package/server/service/actuation/actuation-rule-resolver.ts +147 -0
  225. package/server/service/actuation/actuation-rule.ts +139 -0
  226. package/server/service/actuation/approval-gateway.ts +44 -0
  227. package/server/service/actuation/catch-up-effects.ts +152 -0
  228. package/server/service/actuation/command-dispatcher.ts +511 -0
  229. package/server/service/actuation/command-store.ts +236 -0
  230. package/server/service/actuation/dispatch-after-approval.ts +71 -0
  231. package/server/service/actuation/effect-match.ts +124 -0
  232. package/server/service/actuation/index.ts +27 -0
  233. package/server/service/actuation/retry-plan.ts +145 -0
  234. package/server/service/actuation/rule-evaluate.ts +352 -0
  235. package/server/service/actuation/rule-loop.ts +160 -0
  236. package/server/service/actuation/rule-runner.ts +216 -0
  237. package/server/service/actuation/rule-wiring.ts +84 -0
  238. package/server/service/actuation/settle-effects.ts +115 -0
  239. package/server/service/actuation/sweep-dispatch.ts +179 -0
  240. package/server/service/actuation/twin-command-resolver.ts +387 -0
  241. package/server/service/actuation/twin-command.ts +232 -0
  242. package/server/service/actuation/twin-now.ts +97 -0
  243. package/server/service/index.ts +11 -0
  244. package/server/service/reference/actuation-routing.ts +165 -0
  245. package/server/service/reference/actuation-target.ts +163 -0
  246. package/server/service/reference/connection-portability-resolver.ts +171 -0
  247. package/server/service/reference/connection-portability.ts +285 -0
  248. package/server/service/reference/fill-cadence.ts +29 -0
  249. package/server/service/reference/fill-dedupe.ts +66 -0
  250. package/server/service/reference/fill-loop.ts +153 -0
  251. package/server/service/reference/fill-span.ts +31 -0
  252. package/server/service/reference/hook-contract.ts +280 -22
  253. package/server/service/reference/hook-rejected.ts +53 -0
  254. package/server/service/reference/hook-store.ts +28 -0
  255. package/server/service/reference/index.ts +5 -1
  256. package/server/service/reference/live-cadence.ts +129 -0
  257. package/server/service/reference/live-feed-lease.ts +56 -0
  258. package/server/service/reference/live-ingest.ts +94 -0
  259. package/server/service/reference/reference-adapter.ts +392 -5
  260. package/server/service/reference/reference-fill-run.ts +165 -0
  261. package/server/service/reference/reference-fill.ts +223 -0
  262. package/server/service/reference/reference-hook.ts +355 -32
  263. package/server/service/reference/reference-link-groups.ts +21 -0
  264. package/server/service/reference/reference-links.ts +77 -0
  265. package/server/service/reference/reference-live.ts +30 -5
  266. package/server/service/reference/reference-master.ts +90 -5
  267. package/server/service/reference/reference-resolver.ts +77 -2
  268. package/server/service/twin-event/twin-event-keys.ts +37 -4
  269. package/server/service/twin-event/twin-event.ts +18 -1
  270. package/server/service/twin-model/choose-recipe.ts +31 -0
  271. package/server/service/twin-model/external-resolver.ts +161 -0
  272. package/server/service/twin-model/project-structure.ts +16 -3
  273. package/server/service/twin-model/twin-equipment.ts +22 -0
  274. package/server/service/twin-model/twin-lineage-query.ts +82 -1
  275. package/server/service/twin-model/twin-model-item-query.ts +95 -4
  276. package/server/service/twin-model/twin-model-tree-query.ts +1 -1
  277. package/server/service/twin-space/twin-space.ts +39 -0
  278. package/server/service/twin-subject/event-subjects.ts +8 -1
  279. package/shared/entity-delta.ts +1 -1
  280. package/test/ack-shape-alignment.test.ts +97 -0
  281. package/test/actuation-approval-door.test.ts +421 -0
  282. package/test/actuation-dispatch.test.ts +209 -0
  283. package/test/actuation-effect.test.ts +162 -0
  284. package/test/actuation-outcome.test.ts +152 -0
  285. package/test/actuation-rule-runner.test.ts +281 -0
  286. package/test/actuation-rule.test.ts +266 -0
  287. package/test/actuation-seam.test.ts +249 -0
  288. package/test/actuation-settle.test.ts +164 -0
  289. package/test/actuation-target.test.ts +145 -0
  290. package/test/canonical-ingest-vocabularies.test.ts +49 -2
  291. package/test/capability-mapping.test.ts +1 -1
  292. package/test/choose-recipe.test.ts +117 -0
  293. package/test/command-store-writes-columns.test.ts +126 -0
  294. package/test/connection-portability-doors.test.ts +87 -0
  295. package/test/connector-capability-declaration.test.ts +54 -4
  296. package/test/declaration-reaches-model.test.ts +43 -0
  297. package/test/dedupe-transformation.test.ts +112 -0
  298. package/test/entity-delta.test.ts +1 -1
  299. package/test/event-subjects.test.ts +2 -1
  300. package/test/external-resolver.test.ts +171 -0
  301. package/test/fill-dedupe.test.ts +59 -0
  302. package/test/fill-loop.test.ts +35 -0
  303. package/test/first-envelope.test.ts +153 -0
  304. package/test/fold-shift-dst.test.ts +110 -0
  305. package/test/home-in-area.test.ts +90 -0
  306. package/test/hook-rejected-shape.test.ts +86 -0
  307. package/test/hook-response-sum.test.ts +193 -0
  308. package/test/hook-sequence.test.ts +333 -0
  309. package/test/ingest-dedupe.test.ts +56 -0
  310. package/test/ingest-expected-quiet.test.ts +111 -0
  311. package/test/ingest-idempotent.test.ts +79 -0
  312. package/test/ingest-reconcile-four-ways.test.ts +97 -0
  313. package/test/ingest-rules-header.test.ts +100 -0
  314. package/test/ingest-window-rolls-unobserved.test.ts +72 -0
  315. package/test/intake-mapping-parity.test.ts +175 -0
  316. package/test/live-cadence.test.ts +170 -0
  317. package/test/live-feed-lease.test.ts +63 -0
  318. package/test/live-mirror-parity.test.ts +1 -1
  319. package/test/oee-accumulator.test.ts +3 -3
  320. package/test/oee-site-calendar.test.ts +146 -0
  321. package/test/order-and-transaction-split.test.ts +107 -0
  322. package/test/reference-fill.test.ts +235 -0
  323. package/test/reference-hook.test.ts +114 -23
  324. package/test/reference-links.test.ts +56 -0
  325. package/test/retry-plan.test.ts +139 -0
  326. package/test/sweep-dispatch.test.ts +191 -0
  327. package/test/twin-event-keys.test.ts +20 -6
  328. package/test/twin-model-item-db.test.ts +6 -6
  329. package/test/twin-model-tree-db.test.ts +6 -6
  330. package/tsconfig.shared.tsbuildinfo +1 -1
  331. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,63 @@
1
+ /*
2
+ * The name a live feed takes its lease under.
3
+ *
4
+ * This is worth a test because the failure it prevents is silent: two feeds
5
+ * sharing one name means one of them stops, and it stops looking like
6
+ * "someone else holds it" — the ordinary case on every instance but one, so
7
+ * nothing gets logged and nothing looks wrong.
8
+ */
9
+ import test from 'node:test'
10
+ import assert from 'node:assert/strict'
11
+
12
+ import { liveFeedLeaseName } from '../server/service/reference/live-feed-lease.js'
13
+
14
+ test('the domain is part of the name, so two tenants do not block each other', () => {
15
+ /*
16
+ * This is the whole reason the reference layer composes the name instead of
17
+ * the adapter: an adapter is handed cfg and site, and neither carries a
18
+ * domain, so a name built there would collide across tenants.
19
+ */
20
+ const a = liveFeedLeaseName('domain-a', 'mes-line-a', 'mes')
21
+ const b = liveFeedLeaseName('domain-b', 'mes-line-a', 'mes')
22
+
23
+ assert.notEqual(a, b)
24
+ assert.ok(a.includes('domain-a'))
25
+ })
26
+
27
+ test('the instance is part of the name, so two twins on one source do not block each other', () => {
28
+ const a = liveFeedLeaseName('d', 'mes-line-a', 'mes')
29
+ const b = liveFeedLeaseName('d', 'mes-miratek', 'mes')
30
+
31
+ assert.notEqual(a, b)
32
+ })
33
+
34
+ test('the same feed always gets the same name, so a second instance sees it as taken', () => {
35
+ assert.equal(liveFeedLeaseName('d', 'i', 's'), liveFeedLeaseName('d', 'i', 's'))
36
+ })
37
+
38
+ test('the name says it is a live feed, so a lease table read tells you the kind of task', () => {
39
+ assert.ok(liveFeedLeaseName('d', 'i', 's').startsWith('twin-live::'))
40
+ })
41
+
42
+ test('a missing source still names the feed — an instance has one feed', () => {
43
+ const named = liveFeedLeaseName('d', 'i')
44
+
45
+ assert.equal(named, 'twin-live::d::i')
46
+ assert.notEqual(named, liveFeedLeaseName('d', 'other'))
47
+ })
48
+
49
+ test('a blank source is the same as none rather than an empty segment', () => {
50
+ assert.equal(liveFeedLeaseName('d', 'i', ' '), liveFeedLeaseName('d', 'i'))
51
+ })
52
+
53
+ test('a missing domain or instance is refused, not filled in', () => {
54
+ /*
55
+ * Composing a name from a blank would put two feeds on one row. Throwing
56
+ * here surfaces it at startup; filling it in would surface it as a channel
57
+ * that quietly stopped weeks later.
58
+ */
59
+ assert.throws(() => liveFeedLeaseName('', 'i', 's'), /domainId/)
60
+ assert.throws(() => liveFeedLeaseName(' ', 'i', 's'), /domainId/)
61
+ assert.throws(() => liveFeedLeaseName('d', '', 's'), /instanceId/)
62
+ assert.throws(() => liveFeedLeaseName('d', undefined as any, 's'), /instanceId/)
63
+ })
@@ -213,7 +213,7 @@ test('★ OEE derive: 공유 공식 computeOee 가 공급된 카운터로 OEE
213
213
  */
214
214
  const planned = o.runMs + o.setupMs + o.downMs + o.delayMs
215
215
  const derived = computeOee(
216
- { runMs: o.runMs, setupMs: o.setupMs, downMs: o.downMs, goodCount: o.goodCount, scrapCount: o.scrapCount, metricsSinceMs: 0 },
216
+ { runMs: o.runMs, setupMs: o.setupMs, downMs: o.downMs, goodCount: o.goodCount, nonconformingCount: o.nonconformingCount, metricsSinceMs: 0 },
217
217
  planned
218
218
  )
219
219
  /* 세 비율은 근거가 없으면 없다 — 양쪽 다 없는 것도 재생산이다(계약 0.5.0). */
@@ -90,13 +90,13 @@ test('★ OEE 누적기: quality.output 델타 소비 → quality 정확 (good/s
90
90
  const ev = (ms: number, type: string, data: any): any => ({ eventId: 'x', eventType: type, eventTime: new Date(T0 + ms).toISOString(), tenantId: 't', data })
91
91
 
92
92
  acc.apply(ev(0, 'equipment.status', { moverId: 'w1', status: 'busy' }))
93
- acc.apply(ev(1000, 'quality.output', { moverId: 'w1', good: false, goodCount: 0, scrapCount: 1 }))
94
- acc.apply(ev(2000, 'quality.output', { moverId: 'w1', good: true, goodCount: 8, scrapCount: 2 })) // 누적값 최신화
93
+ acc.apply(ev(1000, 'quality.output', { moverId: 'w1', good: false, goodCount: 0, nonconformingCount: 1 }))
94
+ acc.apply(ev(2000, 'quality.output', { moverId: 'w1', good: true, goodCount: 8, nonconformingCount: 2 })) // 누적값 최신화
95
95
  acc.apply(ev(3000, 'equipment.status', { moverId: 'w1', status: 'idle' }))
96
96
 
97
97
  const c = acc.counters('w1', T0 + 4000)
98
98
  assert.equal(c.goodCount, 8, 'goodCount 추적(누적 최신)')
99
- assert.equal(c.scrapCount, 2, 'scrapCount 추적')
99
+ assert.equal(c.nonconformingCount, 2, 'nonconformingCount 추적')
100
100
  const oee = acc.oee('w1', T0 + 4000)
101
101
  assert.ok(Math.abs(oee.quality - 0.8) < 1e-9, 'quality = good/(good+scrap) = 0.8 (텔레메트리 없이 이벤트로 도출)')
102
102
  })
@@ -0,0 +1,146 @@
1
+ /*
2
+ * 현장의 조업 달력이 계획 조업 시간에 닿나.
3
+ *
4
+ * ── 왜 필요한가 (2026-09-07) ────────────────────────────────────────────────
5
+ * 계획 조업 시간(PBT)이 「재기 시작한 뒤 흐른 시간 전부」였다. 주말도 야간도 휴일도 계획 조업으로
6
+ * 세었다. [산술] 8시간 × 주5일이면 168시간 중 40시간이 조업이니 PBT 가 4.2배 크다.
7
+ *
8
+ * plant 레인이 같은 자리를 먼저 겪고 실측을 냈다 — 3교대를 물려받으면 `availability 0.4583`,
9
+ * 달력이 아예 없으면 수를 안 낸다. 트윈은 창 전체를 계획으로 쳐서 수를 냈다.
10
+ *
11
+ * ── 이 시험이 지키는 것은 「자리가 있고 부르는 곳이 있다」다 ─────────────────
12
+ * 오늘 이 저장소에서 그것을 여섯 번 놓쳤다. 값을 받을 칸을 만들고 아무도 채우지 않거나, 채우고
13
+ * 아무도 안 읽는 자리다. 그래서 여기서는 **누적기가 그 값을 실제로 쓰는지**를 본다 — 달력을 얹은
14
+ * 것과 안 얹은 것의 답이 달라야 한다.
15
+ */
16
+ import test from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+
19
+ import { OeeAccumulator } from '../server/engine/oee-accumulator.js'
20
+
21
+ const HOUR = 3_600_000
22
+ const MON = Date.UTC(2026, 8, 7, 0, 0, 0)
23
+
24
+ /** 08:00~16:00 만 조업 — 하루 열여섯 시간이 비근무다. */
25
+ const EIGHT_HOURS = [{ id: 'day', fromTime: '08:00', toTime: '16:00' }] as any[]
26
+
27
+ /** 여덟 시간 내내 돈 설비 한 대. */
28
+ function ranAllShift(calendar?: any[]) {
29
+ const acc = new OeeAccumulator()
30
+ acc.setWorkCalendar(calendar ? { workCalendar: calendar } : undefined)
31
+ /*
32
+ * 봉투의 `data` 에 `moverId` 다 — `payload.data` 도 `equipmentId` 도 아니다. 처음 그렇게 써서
33
+ * 누적기가 아무것도 안 받았고, 시험 넷이 빨개져서 알았다. 읽어서 고쳤다(`oee-accumulator.ts:111`).
34
+ */
35
+ acc.apply({
36
+ eventType: 'equipment.status',
37
+ eventTime: new Date(MON + 8 * HOUR).toISOString(),
38
+ data: { moverId: 'EQ-1', status: 'busy' }
39
+ } as any)
40
+
41
+ return acc.counters('EQ-1', MON + 24 * HOUR)
42
+ }
43
+
44
+ test('달력을 얹으면 계획 정지가 나온다 — 계측 창 안의 비근무만', () => {
45
+ /*
46
+ * ⚠ 하루가 아니다. 계측 창은 **첫 이벤트부터**이므로 08:00~24:00 의 열여섯 시간이고, 그중 조업이
47
+ * 08:00~16:00 이니 비근무가 여덟 시간이다.
48
+ *
49
+ * 처음 이 값을 열여섯으로 적었다 — 「하루 중 열여섯 시간이 비근무」라는 달력의 성질을 계측 창에
50
+ * 그대로 옮긴 것이다. 재보고 고쳤다. 계획 정지는 **달력의 성질이 아니라 이 창에서 잰 값**이다.
51
+ */
52
+ const c = ranAllShift(EIGHT_HOURS) as any
53
+
54
+ assert.equal(c.holdMs, 8 * HOUR)
55
+ assert.equal(c.metricsSinceMs, MON + 8 * HOUR, '창이 첫 이벤트부터라는 것이 위 계산의 전제다')
56
+ })
57
+
58
+ test('달력이 없으면 그 칸을 만들지 않는다 — 0 은 「쉬는 시간이 없다」는 주장이다', () => {
59
+ const c = ranAllShift() as any
60
+
61
+ assert.equal('holdMs' in c, false)
62
+ })
63
+
64
+ test('얹은 것과 안 얹은 것의 답이 다르다 — 같으면 그 값이 안 쓰이는 것이다', () => {
65
+ /*
66
+ * 이 시험이 이 파일의 요점이다. 오늘 제가 낸 결함 중 하나가 「값을 넣어 줄 사람이 있는지 아무것도
67
+ * 검사하지 않은 시험」이었다 — 고쳐도 통과하고 안 고쳐도 통과했다.
68
+ */
69
+ const withCal = ranAllShift(EIGHT_HOURS) as any
70
+ const without = ranAllShift() as any
71
+
72
+ assert.notEqual(withCal.holdMs, without.holdMs)
73
+ })
74
+
75
+ test('실 생산 시간은 달력과 무관하다 — APT 는 잰 값이다', () => {
76
+ /*
77
+ * 달력은 PBT 를 정하고 APT 는 안 건드린다. 휴일에 돌린 열한 시간이 사라지던 것이 그 반대 방향의
78
+ * 결함이었고, 그것은 자르는 쪽이었다.
79
+ */
80
+ const withCal = ranAllShift(EIGHT_HOURS)
81
+ const without = ranAllShift()
82
+
83
+ assert.equal(withCal.runMs, without.runMs)
84
+ assert.equal(withCal.runMs, 16 * HOUR, '08:00 에 busy 로 들어가 24:00 까지면 열여섯 시간이다')
85
+ })
86
+
87
+ test('잰 구간의 시작은 그대로다 — 그 칸에 파생값을 얹지 않는다', () => {
88
+ /*
89
+ * 같은 효과를 `metricsSinceMs` 를 당겨서 낼 수도 있는데, 그 칸은 「언제부터 쟀나」라는 사실이다.
90
+ * plant 레인이 그 방식으로 하고 있었고 `holdMs` 로 옮겼다 — 수는 자릿수까지 같았다.
91
+ */
92
+ const c = ranAllShift(EIGHT_HOURS)
93
+
94
+ assert.equal(c.metricsSinceMs, MON + 8 * HOUR)
95
+ })
96
+
97
+ /*
98
+ * ── 설비의 달력이 현장의 것을 이긴다 ────────────────────────────────────────
99
+ * 이 자리가 실제 자료를 살렸다. rosarito 트윈들이 설비 41대에 3교대 × 평일 달력을 이미 갖고 있었는데,
100
+ * 처음 배선할 때 **현장 달력만 읽었다.** 현장에 달력을 선언한 공간이 하나도 없어서, 있는 자료를 두고
101
+ * 계획 조업 시간이 창 전체로 남아 있었다.
102
+ *
103
+ * 사용자가 「모든 공간에 달력을 세우라」고 한 것이 그 말을 듣고 나온 지시였고, 재보니 세울 것이 아니라
104
+ * **읽을 것이 있었다.**
105
+ */
106
+ const ROUND_CLOCK = [{ id: 'always', fromTime: '00:00', toTime: '24:00' }] as any[]
107
+
108
+ function withModel(model: any) {
109
+ const acc = new OeeAccumulator()
110
+ acc.setWorkCalendar(model)
111
+ acc.apply({
112
+ eventType: 'equipment.status',
113
+ eventTime: new Date(MON + 8 * HOUR).toISOString(),
114
+ data: { moverId: 'EQ-1', status: 'busy' }
115
+ } as any)
116
+
117
+ return acc.counters('EQ-1', MON + 24 * HOUR) as any
118
+ }
119
+
120
+ test('설비가 자기 달력을 선언하면 그것을 쓴다 — 24시간 도는 로가 현장 근무조에 안 잘린다', () => {
121
+ const c = withModel({ workCalendar: EIGHT_HOURS, equipment: [{ id: 'EQ-1', workCalendar: ROUND_CLOCK }] })
122
+
123
+ assert.equal(c.holdMs, 0, '자기 달력이 종일 가동이면 계획 정지가 없다')
124
+ })
125
+
126
+ test('설비가 선언하지 않으면 현장의 달력으로 떨어진다', () => {
127
+ const c = withModel({ workCalendar: EIGHT_HOURS, equipment: [{ id: 'EQ-OTHER', workCalendar: ROUND_CLOCK }] })
128
+
129
+ assert.equal(c.holdMs, 8 * HOUR, '이 설비의 것이 없으니 현장 것을 쓴다')
130
+ })
131
+
132
+ test('현장이 없고 설비만 있어도 답한다 — 지금 실제 자료가 그 모양이다', () => {
133
+ /*
134
+ * rosarito 가 이렇다: 공간에 달력이 없고 설비 41대에 3교대가 있다. 현장 달력만 읽으면 이 자료가
135
+ * 통째로 안 쓰인다.
136
+ */
137
+ const c = withModel({ equipment: [{ id: 'EQ-1', workCalendar: EIGHT_HOURS }] })
138
+
139
+ assert.equal(c.holdMs, 8 * HOUR)
140
+ })
141
+
142
+ test('둘 다 없으면 그 칸을 만들지 않는다', () => {
143
+ const c = withModel({ equipment: [{ id: 'EQ-1' }] })
144
+
145
+ assert.equal('holdMs' in c, false)
146
+ })
@@ -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
+ })