@hexabot-ai/types 3.0.0-alpha.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 (294) hide show
  1. package/LICENSE.md +120 -0
  2. package/README.md +172 -0
  3. package/dist/cjs/analytics/index.js +18 -0
  4. package/dist/cjs/analytics/integration-health.js +30 -0
  5. package/dist/cjs/analytics/stats.js +32 -0
  6. package/dist/cjs/attachment/attachment.js +53 -0
  7. package/dist/cjs/attachment/index.js +16 -0
  8. package/dist/cjs/audit/audit-log.js +33 -0
  9. package/dist/cjs/audit/index.js +11 -0
  10. package/dist/cjs/channel/channel-metadata.js +15 -0
  11. package/dist/cjs/channel/index.js +15 -0
  12. package/dist/cjs/channel/source-full.js +16 -0
  13. package/dist/cjs/channel/source.js +25 -0
  14. package/dist/cjs/chat/attachment.js +32 -0
  15. package/dist/cjs/chat/button.js +70 -0
  16. package/dist/cjs/chat/index.js +87 -0
  17. package/dist/cjs/chat/label-group.js +22 -0
  18. package/dist/cjs/chat/label.js +37 -0
  19. package/dist/cjs/chat/message-contract.js +183 -0
  20. package/dist/cjs/chat/message.js +107 -0
  21. package/dist/cjs/chat/options.js +122 -0
  22. package/dist/cjs/chat/quick-reply.js +35 -0
  23. package/dist/cjs/chat/subscriber.js +36 -0
  24. package/dist/cjs/chat/thread.js +36 -0
  25. package/dist/cjs/cms/content-type.js +17 -0
  26. package/dist/cjs/cms/content.js +29 -0
  27. package/dist/cjs/cms/index.js +21 -0
  28. package/dist/cjs/cms/menu.js +36 -0
  29. package/dist/cjs/dummy/dummy.js +18 -0
  30. package/dist/cjs/dummy/index.js +12 -0
  31. package/dist/cjs/i18n/index.js +16 -0
  32. package/dist/cjs/i18n/language.js +19 -0
  33. package/dist/cjs/i18n/translation.js +17 -0
  34. package/dist/cjs/index.js +32 -0
  35. package/dist/cjs/setting/index.js +18 -0
  36. package/dist/cjs/setting/metadata.js +17 -0
  37. package/dist/cjs/setting/setting.js +36 -0
  38. package/dist/cjs/shared/aliases.js +41 -0
  39. package/dist/cjs/shared/base.js +14 -0
  40. package/dist/cjs/shared/object.js +19 -0
  41. package/dist/cjs/shared/preprocess.js +13 -0
  42. package/dist/cjs/shared/profile.js +33 -0
  43. package/dist/cjs/user/credential.js +26 -0
  44. package/dist/cjs/user/domain.js +56 -0
  45. package/dist/cjs/user/index.js +39 -0
  46. package/dist/cjs/user/model.js +24 -0
  47. package/dist/cjs/user/permission.js +32 -0
  48. package/dist/cjs/user/role.js +24 -0
  49. package/dist/cjs/user/user-profile-assigned.js +38 -0
  50. package/dist/cjs/user/user-profile.js +12 -0
  51. package/dist/cjs/user/user.js +44 -0
  52. package/dist/cjs/workflow/domain.js +44 -0
  53. package/dist/cjs/workflow/helpers.js +35 -0
  54. package/dist/cjs/workflow/index.js +40 -0
  55. package/dist/cjs/workflow/mcp-server.js +33 -0
  56. package/dist/cjs/workflow/memory-definition.js +21 -0
  57. package/dist/cjs/workflow/memory-record.js +44 -0
  58. package/dist/cjs/workflow/workflow-run.js +100 -0
  59. package/dist/cjs/workflow/workflow-version.js +39 -0
  60. package/dist/cjs/workflow/workflow.js +98 -0
  61. package/dist/esm/analytics/index.js +7 -0
  62. package/dist/esm/analytics/integration-health.js +27 -0
  63. package/dist/esm/analytics/stats.js +29 -0
  64. package/dist/esm/attachment/attachment.js +50 -0
  65. package/dist/esm/attachment/index.js +6 -0
  66. package/dist/esm/audit/audit-log.js +30 -0
  67. package/dist/esm/audit/index.js +6 -0
  68. package/dist/esm/channel/channel-metadata.js +12 -0
  69. package/dist/esm/channel/index.js +8 -0
  70. package/dist/esm/channel/source-full.js +13 -0
  71. package/dist/esm/channel/source.js +22 -0
  72. package/dist/esm/chat/attachment.js +29 -0
  73. package/dist/esm/chat/button.js +67 -0
  74. package/dist/esm/chat/index.js +15 -0
  75. package/dist/esm/chat/label-group.js +19 -0
  76. package/dist/esm/chat/label.js +34 -0
  77. package/dist/esm/chat/message-contract.js +180 -0
  78. package/dist/esm/chat/message.js +104 -0
  79. package/dist/esm/chat/options.js +119 -0
  80. package/dist/esm/chat/quick-reply.js +32 -0
  81. package/dist/esm/chat/subscriber.js +33 -0
  82. package/dist/esm/chat/thread.js +33 -0
  83. package/dist/esm/cms/content-type.js +14 -0
  84. package/dist/esm/cms/content.js +26 -0
  85. package/dist/esm/cms/index.js +8 -0
  86. package/dist/esm/cms/menu.js +33 -0
  87. package/dist/esm/dummy/dummy.js +15 -0
  88. package/dist/esm/dummy/index.js +6 -0
  89. package/dist/esm/i18n/index.js +7 -0
  90. package/dist/esm/i18n/language.js +16 -0
  91. package/dist/esm/i18n/translation.js +14 -0
  92. package/dist/esm/index.js +16 -0
  93. package/dist/esm/setting/index.js +7 -0
  94. package/dist/esm/setting/metadata.js +14 -0
  95. package/dist/esm/setting/setting.js +33 -0
  96. package/dist/esm/shared/aliases.js +35 -0
  97. package/dist/esm/shared/base.js +11 -0
  98. package/dist/esm/shared/object.js +14 -0
  99. package/dist/esm/shared/preprocess.js +9 -0
  100. package/dist/esm/shared/profile.js +30 -0
  101. package/dist/esm/user/credential.js +23 -0
  102. package/dist/esm/user/domain.js +53 -0
  103. package/dist/esm/user/index.js +13 -0
  104. package/dist/esm/user/model.js +21 -0
  105. package/dist/esm/user/permission.js +29 -0
  106. package/dist/esm/user/role.js +21 -0
  107. package/dist/esm/user/user-profile-assigned.js +35 -0
  108. package/dist/esm/user/user-profile.js +9 -0
  109. package/dist/esm/user/user.js +41 -0
  110. package/dist/esm/workflow/domain.js +41 -0
  111. package/dist/esm/workflow/helpers.js +30 -0
  112. package/dist/esm/workflow/index.js +12 -0
  113. package/dist/esm/workflow/mcp-server.js +30 -0
  114. package/dist/esm/workflow/memory-definition.js +18 -0
  115. package/dist/esm/workflow/memory-record.js +41 -0
  116. package/dist/esm/workflow/workflow-run.js +96 -0
  117. package/dist/esm/workflow/workflow-version.js +36 -0
  118. package/dist/esm/workflow/workflow.js +94 -0
  119. package/dist/types/analytics/index.d.ts +3 -0
  120. package/dist/types/analytics/index.d.ts.map +1 -0
  121. package/dist/types/analytics/integration-health.d.ts +55 -0
  122. package/dist/types/analytics/integration-health.d.ts.map +1 -0
  123. package/dist/types/analytics/stats.d.ts +43 -0
  124. package/dist/types/analytics/stats.d.ts.map +1 -0
  125. package/dist/types/attachment/attachment.d.ts +212 -0
  126. package/dist/types/attachment/attachment.d.ts.map +1 -0
  127. package/dist/types/attachment/index.d.ts +2 -0
  128. package/dist/types/attachment/index.d.ts.map +1 -0
  129. package/dist/types/audit/audit-log.d.ts +58 -0
  130. package/dist/types/audit/audit-log.d.ts.map +1 -0
  131. package/dist/types/audit/index.d.ts +2 -0
  132. package/dist/types/audit/index.d.ts.map +1 -0
  133. package/dist/types/channel/channel-metadata.d.ts +7 -0
  134. package/dist/types/channel/channel-metadata.d.ts.map +1 -0
  135. package/dist/types/channel/index.d.ts +4 -0
  136. package/dist/types/channel/index.d.ts.map +1 -0
  137. package/dist/types/channel/source-full.d.ts +58 -0
  138. package/dist/types/channel/source-full.d.ts.map +1 -0
  139. package/dist/types/channel/source.d.ts +44 -0
  140. package/dist/types/channel/source.d.ts.map +1 -0
  141. package/dist/types/chat/attachment.d.ts +34 -0
  142. package/dist/types/chat/attachment.d.ts.map +1 -0
  143. package/dist/types/chat/button.d.ts +31 -0
  144. package/dist/types/chat/button.d.ts.map +1 -0
  145. package/dist/types/chat/index.d.ts +11 -0
  146. package/dist/types/chat/index.d.ts.map +1 -0
  147. package/dist/types/chat/label-group.d.ts +47 -0
  148. package/dist/types/chat/label-group.d.ts.map +1 -0
  149. package/dist/types/chat/label.d.ts +102 -0
  150. package/dist/types/chat/label.d.ts.map +1 -0
  151. package/dist/types/chat/message-contract.d.ts +883 -0
  152. package/dist/types/chat/message-contract.d.ts.map +1 -0
  153. package/dist/types/chat/message.d.ts +1132 -0
  154. package/dist/types/chat/message.d.ts.map +1 -0
  155. package/dist/types/chat/options.d.ts +64 -0
  156. package/dist/types/chat/options.d.ts.map +1 -0
  157. package/dist/types/chat/quick-reply.d.ts +32 -0
  158. package/dist/types/chat/quick-reply.d.ts.map +1 -0
  159. package/dist/types/chat/subscriber.d.ts +236 -0
  160. package/dist/types/chat/subscriber.d.ts.map +1 -0
  161. package/dist/types/chat/thread.d.ts +126 -0
  162. package/dist/types/chat/thread.d.ts.map +1 -0
  163. package/dist/types/cms/content-type.d.ts +26 -0
  164. package/dist/types/cms/content-type.d.ts.map +1 -0
  165. package/dist/types/cms/content.d.ts +49 -0
  166. package/dist/types/cms/content.d.ts.map +1 -0
  167. package/dist/types/cms/index.d.ts +4 -0
  168. package/dist/types/cms/index.d.ts.map +1 -0
  169. package/dist/types/cms/menu.d.ts +85 -0
  170. package/dist/types/cms/menu.d.ts.map +1 -0
  171. package/dist/types/dummy/dummy.d.ts +26 -0
  172. package/dist/types/dummy/dummy.d.ts.map +1 -0
  173. package/dist/types/dummy/index.d.ts +2 -0
  174. package/dist/types/dummy/index.d.ts.map +1 -0
  175. package/dist/types/i18n/index.d.ts +3 -0
  176. package/dist/types/i18n/index.d.ts.map +1 -0
  177. package/dist/types/i18n/language.d.ts +32 -0
  178. package/dist/types/i18n/language.d.ts.map +1 -0
  179. package/dist/types/i18n/translation.d.ts +26 -0
  180. package/dist/types/i18n/translation.d.ts.map +1 -0
  181. package/dist/types/index.d.ts +12 -0
  182. package/dist/types/index.d.ts.map +1 -0
  183. package/dist/types/setting/index.d.ts +3 -0
  184. package/dist/types/setting/index.d.ts.map +1 -0
  185. package/dist/types/setting/metadata.d.ts +26 -0
  186. package/dist/types/setting/metadata.d.ts.map +1 -0
  187. package/dist/types/setting/setting.d.ts +41 -0
  188. package/dist/types/setting/setting.d.ts.map +1 -0
  189. package/dist/types/shared/aliases.d.ts +4 -0
  190. package/dist/types/shared/aliases.d.ts.map +1 -0
  191. package/dist/types/shared/base.d.ts +7 -0
  192. package/dist/types/shared/base.d.ts.map +1 -0
  193. package/dist/types/shared/object.d.ts +3 -0
  194. package/dist/types/shared/object.d.ts.map +1 -0
  195. package/dist/types/shared/preprocess.d.ts +3 -0
  196. package/dist/types/shared/preprocess.d.ts.map +1 -0
  197. package/dist/types/shared/profile.d.ts +40 -0
  198. package/dist/types/shared/profile.d.ts.map +1 -0
  199. package/dist/types/user/credential.d.ts +97 -0
  200. package/dist/types/user/credential.d.ts.map +1 -0
  201. package/dist/types/user/domain.d.ts +54 -0
  202. package/dist/types/user/domain.d.ts.map +1 -0
  203. package/dist/types/user/index.d.ts +9 -0
  204. package/dist/types/user/index.d.ts.map +1 -0
  205. package/dist/types/user/model.d.ts +142 -0
  206. package/dist/types/user/model.d.ts.map +1 -0
  207. package/dist/types/user/permission.d.ts +90 -0
  208. package/dist/types/user/permission.d.ts.map +1 -0
  209. package/dist/types/user/role.d.ts +111 -0
  210. package/dist/types/user/role.d.ts.map +1 -0
  211. package/dist/types/user/user-profile-assigned.d.ts +181 -0
  212. package/dist/types/user/user-profile-assigned.d.ts.map +1 -0
  213. package/dist/types/user/user-profile.d.ts +32 -0
  214. package/dist/types/user/user-profile.d.ts.map +1 -0
  215. package/dist/types/user/user.d.ts +270 -0
  216. package/dist/types/user/user.d.ts.map +1 -0
  217. package/dist/types/workflow/domain.d.ts +32 -0
  218. package/dist/types/workflow/domain.d.ts.map +1 -0
  219. package/dist/types/workflow/helpers.d.ts +117 -0
  220. package/dist/types/workflow/helpers.d.ts.map +1 -0
  221. package/dist/types/workflow/index.d.ts +8 -0
  222. package/dist/types/workflow/index.d.ts.map +1 -0
  223. package/dist/types/workflow/mcp-server.d.ts +67 -0
  224. package/dist/types/workflow/mcp-server.d.ts.map +1 -0
  225. package/dist/types/workflow/memory-definition.d.ts +35 -0
  226. package/dist/types/workflow/memory-definition.d.ts.map +1 -0
  227. package/dist/types/workflow/memory-record.d.ts +284 -0
  228. package/dist/types/workflow/memory-record.d.ts.map +1 -0
  229. package/dist/types/workflow/workflow-run.d.ts +375 -0
  230. package/dist/types/workflow/workflow-run.d.ts.map +1 -0
  231. package/dist/types/workflow/workflow-version.d.ts +179 -0
  232. package/dist/types/workflow/workflow-version.d.ts.map +1 -0
  233. package/dist/types/workflow/workflow.d.ts +366 -0
  234. package/dist/types/workflow/workflow.d.ts.map +1 -0
  235. package/package.json +59 -0
  236. package/src/analytics/index.ts +26 -0
  237. package/src/analytics/integration-health.ts +44 -0
  238. package/src/analytics/stats.ts +41 -0
  239. package/src/attachment/attachment.ts +75 -0
  240. package/src/attachment/index.ts +19 -0
  241. package/src/audit/audit-log.ts +42 -0
  242. package/src/audit/index.ts +12 -0
  243. package/src/channel/channel-metadata.ts +20 -0
  244. package/src/channel/index.ts +19 -0
  245. package/src/channel/source-full.ts +25 -0
  246. package/src/channel/source.ts +42 -0
  247. package/src/chat/attachment.ts +44 -0
  248. package/src/chat/button.ts +90 -0
  249. package/src/chat/index.ts +173 -0
  250. package/src/chat/label-group.ts +35 -0
  251. package/src/chat/label.ts +56 -0
  252. package/src/chat/message-contract.ts +370 -0
  253. package/src/chat/message.ts +164 -0
  254. package/src/chat/options.ts +130 -0
  255. package/src/chat/quick-reply.ts +41 -0
  256. package/src/chat/subscriber.ts +73 -0
  257. package/src/chat/thread.ts +55 -0
  258. package/src/cms/content-type.ts +26 -0
  259. package/src/cms/content.ts +48 -0
  260. package/src/cms/index.ts +33 -0
  261. package/src/cms/menu.ts +54 -0
  262. package/src/dummy/dummy.ts +30 -0
  263. package/src/dummy/index.ts +14 -0
  264. package/src/i18n/index.ts +23 -0
  265. package/src/i18n/language.ts +28 -0
  266. package/src/i18n/translation.ts +26 -0
  267. package/src/index.test.ts +1271 -0
  268. package/src/index.ts +27 -0
  269. package/src/setting/index.ts +25 -0
  270. package/src/setting/metadata.ts +26 -0
  271. package/src/setting/setting.ts +46 -0
  272. package/src/shared/aliases.ts +48 -0
  273. package/src/shared/base.ts +13 -0
  274. package/src/shared/object.ts +19 -0
  275. package/src/shared/preprocess.ts +14 -0
  276. package/src/shared/profile.ts +49 -0
  277. package/src/user/credential.ts +45 -0
  278. package/src/user/domain.ts +94 -0
  279. package/src/user/index.ts +78 -0
  280. package/src/user/model.ts +40 -0
  281. package/src/user/permission.ts +51 -0
  282. package/src/user/role.ts +43 -0
  283. package/src/user/user-profile-assigned.ts +70 -0
  284. package/src/user/user-profile.ts +21 -0
  285. package/src/user/user.ts +87 -0
  286. package/src/workflow/domain.ts +47 -0
  287. package/src/workflow/helpers.ts +40 -0
  288. package/src/workflow/index.ts +70 -0
  289. package/src/workflow/mcp-server.ts +64 -0
  290. package/src/workflow/memory-definition.ts +31 -0
  291. package/src/workflow/memory-record.ts +71 -0
  292. package/src/workflow/workflow-run.ts +156 -0
  293. package/src/workflow/workflow-version.ts +68 -0
  294. package/src/workflow/workflow.ts +163 -0
@@ -0,0 +1,1271 @@
1
+ /*
2
+ * Hexabot — Fair Core License (FCL-1.0-ALv2)
3
+ * Copyright (c) 2026 Hexastack.
4
+ * Full terms: see LICENSE.md.
5
+ */
6
+
7
+ import {
8
+ AttachmentAccess,
9
+ AttachmentCreatedByRef,
10
+ AttachmentResourceRef,
11
+ FieldType,
12
+ MemoryScope,
13
+ McpServerTransport,
14
+ MenuType,
15
+ StatsType,
16
+ WorkflowType,
17
+ WorkflowVersionAction,
18
+ auditLogFullSchema,
19
+ auditLogSchema,
20
+ attachmentFullSchema,
21
+ attachmentSchema,
22
+ channelMetadataSchema,
23
+ contentFullSchema,
24
+ contentSchema,
25
+ createWorkflowFullSchema,
26
+ credentialFullSchema,
27
+ credentialSchema,
28
+ dummySchema,
29
+ integrationHealthResponseSchema,
30
+ messageSchema,
31
+ IncomingMessageType,
32
+ labelFullSchema,
33
+ labelSchema,
34
+ mcpServerFullSchema,
35
+ mcpServerSchema,
36
+ memoryRecordFullSchema,
37
+ memoryRecordSchema,
38
+ menuFullSchema,
39
+ menuSchema,
40
+ modelSchema,
41
+ OutgoingMessageType,
42
+ sourceFullSchema,
43
+ sourceSchema,
44
+ stdOutgoingMessageSchema,
45
+ stdIncomingMessageSchema,
46
+ stdOutgoingEnvelopeSchema,
47
+ messageFullSchema,
48
+ permissionFullSchema,
49
+ permissionSchema,
50
+ resolveRunDurationMs,
51
+ settingSchema,
52
+ statsSchema,
53
+ subscriberFullSchema,
54
+ subscriberSchema,
55
+ threadFullSchema,
56
+ userFullSchema,
57
+ userSchema,
58
+ workflowFullSchema,
59
+ workflowRunFullSchema,
60
+ workflowRunSchema,
61
+ workflowVersionFullSchema,
62
+ } from "./index";
63
+
64
+ describe("@hexabot-ai/types schemas", () => {
65
+ const now = "2026-01-01T00:00:00.000Z";
66
+
67
+ it("maps user aliases and strips unknown keys", () => {
68
+ const parsed = userSchema.parse({
69
+ id: "u_1",
70
+ createdAt: now,
71
+ updatedAt: now,
72
+ firstName: "Ada",
73
+ lastName: "Lovelace",
74
+ language: "en",
75
+ timezone: 1,
76
+ locale: null,
77
+ gender: null,
78
+ country: null,
79
+ foreignId: "foreign-u-1",
80
+ assignedAt: null,
81
+ lastvisit: null,
82
+ retainedFrom: null,
83
+ channel: { name: "web", data: { id: "sub_1" } },
84
+ username: "ada",
85
+ email: "ada@example.com",
86
+ sendEmail: false,
87
+ state: true,
88
+ resetCount: 0,
89
+ resetToken: null,
90
+ provider: { strategy: "local" },
91
+ roleIds: ["r_1", "r_2"],
92
+ avatarId: "a_1",
93
+ shouldDrop: true,
94
+ });
95
+
96
+ expect(parsed.roles).toEqual(["r_1", "r_2"]);
97
+ expect(parsed.avatar).toBe("a_1");
98
+ expect("roleIds" in parsed).toBe(false);
99
+ expect("avatarId" in parsed).toBe(false);
100
+ expect("shouldDrop" in parsed).toBe(false);
101
+ });
102
+
103
+ it("parses strict nested user full payloads", () => {
104
+ const parsed = userFullSchema.parse({
105
+ id: "u_1",
106
+ createdAt: now,
107
+ updatedAt: now,
108
+ firstName: "Ada",
109
+ lastName: "Lovelace",
110
+ language: "en",
111
+ timezone: 1,
112
+ locale: null,
113
+ gender: null,
114
+ country: null,
115
+ foreignId: "foreign-u-1",
116
+ assignedAt: null,
117
+ lastvisit: null,
118
+ retainedFrom: null,
119
+ channel: { name: "web", data: { id: "sub_1" } },
120
+ username: "ada",
121
+ email: "ada@example.com",
122
+ sendEmail: false,
123
+ state: true,
124
+ resetCount: 0,
125
+ resetToken: null,
126
+ provider: { strategy: "local" },
127
+ roles: [
128
+ {
129
+ id: "r_1",
130
+ createdAt: now,
131
+ updatedAt: now,
132
+ name: "admin",
133
+ active: true,
134
+ shouldDrop: true,
135
+ },
136
+ ],
137
+ avatar: {
138
+ id: "a_1",
139
+ createdAt: now,
140
+ updatedAt: now,
141
+ name: "avatar.png",
142
+ type: "image/png",
143
+ size: 100,
144
+ location: "/tmp/avatar.png",
145
+ resourceRef: AttachmentResourceRef.UserAvatar,
146
+ access: AttachmentAccess.Private,
147
+ createdByRef: AttachmentCreatedByRef.User,
148
+ createdById: "u_1",
149
+ url: "/attachment/download/a_1/avatar.png",
150
+ },
151
+ });
152
+
153
+ expect(parsed.roles[0].id).toBe("r_1");
154
+ expect("shouldDrop" in parsed.roles[0]).toBe(false);
155
+ expect(parsed.avatar?.createdBy).toBe("u_1");
156
+ });
157
+
158
+ it("does not alias relation id fields into user full relation objects", () => {
159
+ const parsed = userFullSchema.parse({
160
+ id: "u_1",
161
+ createdAt: now,
162
+ updatedAt: now,
163
+ firstName: "Ada",
164
+ lastName: "Lovelace",
165
+ language: "en",
166
+ timezone: 1,
167
+ locale: null,
168
+ gender: null,
169
+ country: null,
170
+ foreignId: "foreign-u-1",
171
+ assignedAt: null,
172
+ lastvisit: null,
173
+ retainedFrom: null,
174
+ channel: { name: "web", data: { id: "sub_1" } },
175
+ username: "ada",
176
+ email: "ada@example.com",
177
+ sendEmail: false,
178
+ state: true,
179
+ resetCount: 0,
180
+ resetToken: null,
181
+ provider: { strategy: "local" },
182
+ roleIds: ["r_1"],
183
+ avatarId: "a_1",
184
+ labelIds: ["l_1"],
185
+ assignedToId: "u_2",
186
+ });
187
+
188
+ expect(parsed.roles).toEqual([]);
189
+ expect(parsed.avatar).toBeNull();
190
+ expect(parsed.labels).toEqual([]);
191
+ expect(parsed.assignedTo).toBeNull();
192
+ });
193
+
194
+ it("does not alias relation id fields into subscriber full relation objects", () => {
195
+ const parsed = subscriberFullSchema.parse({
196
+ id: "s_1",
197
+ createdAt: now,
198
+ updatedAt: now,
199
+ firstName: "Ada",
200
+ lastName: "Lovelace",
201
+ language: "en",
202
+ timezone: 0,
203
+ locale: null,
204
+ gender: null,
205
+ country: null,
206
+ foreignId: "foreign-s-1",
207
+ assignedAt: null,
208
+ lastvisit: null,
209
+ retainedFrom: null,
210
+ channel: { name: "web", data: null },
211
+ labelIds: ["l_1"],
212
+ assignedToId: "u_1",
213
+ avatarId: "a_1",
214
+ });
215
+
216
+ expect(parsed.labels).toEqual([]);
217
+ expect(parsed.assignedTo).toBeNull();
218
+ expect(parsed.avatar).toBeNull();
219
+ });
220
+
221
+ it("does not alias relation id fields into other strict full relation objects", () => {
222
+ const assertInvalidRelationField = (
223
+ result:
224
+ | ReturnType<typeof permissionFullSchema.safeParse>
225
+ | ReturnType<typeof contentFullSchema.safeParse>
226
+ | ReturnType<typeof threadFullSchema.safeParse>
227
+ | ReturnType<typeof messageFullSchema.safeParse>
228
+ | ReturnType<typeof workflowVersionFullSchema.safeParse>
229
+ | ReturnType<typeof workflowRunFullSchema.safeParse>,
230
+ relationField: string,
231
+ ) => {
232
+ expect(result.success).toBe(false);
233
+ if (!result.success) {
234
+ expect(
235
+ result.error.issues.some((issue) => issue.path[0] === relationField),
236
+ ).toBe(true);
237
+ }
238
+ };
239
+
240
+ assertInvalidRelationField(
241
+ permissionFullSchema.safeParse({
242
+ id: "p_1",
243
+ createdAt: now,
244
+ updatedAt: now,
245
+ action: "read",
246
+ relation: "role",
247
+ modelId: "m_1",
248
+ roleId: "r_1",
249
+ }),
250
+ "model",
251
+ );
252
+
253
+ assertInvalidRelationField(
254
+ contentFullSchema.safeParse({
255
+ id: "c_1",
256
+ createdAt: now,
257
+ updatedAt: now,
258
+ title: "Welcome",
259
+ status: true,
260
+ properties: {},
261
+ searchText: "welcome",
262
+ contentTypeId: "ct_1",
263
+ }),
264
+ "contentType",
265
+ );
266
+
267
+ assertInvalidRelationField(
268
+ threadFullSchema.safeParse({
269
+ id: "th_1",
270
+ createdAt: now,
271
+ updatedAt: now,
272
+ status: "open",
273
+ subscriberId: "s_1",
274
+ source: {
275
+ id: "source_1",
276
+ createdAt: now,
277
+ updatedAt: now,
278
+ name: "main-web",
279
+ channel: "web",
280
+ settings: {},
281
+ state: true,
282
+ defaultWorkflow: null,
283
+ },
284
+ }),
285
+ "subscriber",
286
+ );
287
+
288
+ assertInvalidRelationField(
289
+ messageFullSchema.safeParse({
290
+ id: "msg_1",
291
+ createdAt: now,
292
+ updatedAt: now,
293
+ message: "hello",
294
+ read: false,
295
+ delivery: false,
296
+ handover: false,
297
+ threadId: "th_1",
298
+ }),
299
+ "thread",
300
+ );
301
+
302
+ assertInvalidRelationField(
303
+ workflowVersionFullSchema.safeParse({
304
+ id: "wfv_1",
305
+ createdAt: now,
306
+ updatedAt: now,
307
+ version: 1,
308
+ definitionYml: "defs: {}\nflow: []\noutputs: {}",
309
+ checksum: "sha",
310
+ workflowId: "wf_1",
311
+ parentVersionId: null,
312
+ createdById: "u_1",
313
+ }),
314
+ "workflow",
315
+ );
316
+
317
+ assertInvalidRelationField(
318
+ workflowRunFullSchema.safeParse({
319
+ id: "run_1",
320
+ createdAt: now,
321
+ updatedAt: now,
322
+ status: "running",
323
+ context: {},
324
+ workflowId: "wf_1",
325
+ workflowVersionId: null,
326
+ triggeredById: "s_1",
327
+ threadId: "th_1",
328
+ }),
329
+ "workflow",
330
+ );
331
+
332
+ expect(() =>
333
+ memoryRecordFullSchema.parse({
334
+ id: "mem_1",
335
+ createdAt: now,
336
+ updatedAt: now,
337
+ value: { counter: 1 },
338
+ definitionId: "def_1",
339
+ ownerId: "u_1",
340
+ workflowId: "wf_1",
341
+ runId: "run_1",
342
+ threadId: "th_1",
343
+ }),
344
+ ).toThrow();
345
+ });
346
+
347
+ it("keeps nullable or optional full relation fields when only alias ids are provided", () => {
348
+ const credential = credentialFullSchema.parse({
349
+ id: "cred_1",
350
+ createdAt: now,
351
+ updatedAt: now,
352
+ name: "OPENAI_API_KEY",
353
+ ownerId: "u_1",
354
+ });
355
+ const label = labelFullSchema.parse({
356
+ id: "l_1",
357
+ createdAt: now,
358
+ updatedAt: now,
359
+ title: "VIP",
360
+ name: "vip",
361
+ builtin: false,
362
+ groupId: "g_1",
363
+ });
364
+ const menu = menuFullSchema.parse({
365
+ id: "mn_1",
366
+ createdAt: now,
367
+ updatedAt: now,
368
+ title: "Home",
369
+ type: MenuType.nested,
370
+ parentId: "mn_root",
371
+ });
372
+ const mcp = mcpServerFullSchema.parse({
373
+ id: "mcp_1",
374
+ createdAt: now,
375
+ updatedAt: now,
376
+ name: "Filesystem",
377
+ enabled: true,
378
+ transport: McpServerTransport.stdio,
379
+ credentialId: "cred_1",
380
+ });
381
+ const workflow = workflowFullSchema.parse({
382
+ id: "wf_1",
383
+ createdAt: now,
384
+ updatedAt: now,
385
+ name: "Main",
386
+ description: null,
387
+ type: WorkflowType.conversational,
388
+ schedule: null,
389
+ inputSchema: {},
390
+ builtin: false,
391
+ x: 0,
392
+ y: 0,
393
+ zoom: 1,
394
+ direction: "horizontal",
395
+ currentVersionId: "wfv_1",
396
+ publishedVersionId: "wfv_1",
397
+ createdById: "u_1",
398
+ });
399
+
400
+ expect(credential.owner).toBeNull();
401
+ expect(label.group).toBeUndefined();
402
+ expect(menu.parent).toBeUndefined();
403
+ expect(mcp.credential).toBeUndefined();
404
+ expect(workflow.currentVersion).toBeNull();
405
+ expect(workflow.publishedVersion).toBeNull();
406
+ expect(workflow.createdBy).toBeNull();
407
+ });
408
+
409
+ it("maps attachment aliases and strips unknown keys", () => {
410
+ const parsed = attachmentSchema.parse({
411
+ id: "a_1",
412
+ createdAt: now,
413
+ updatedAt: now,
414
+ name: "file.png",
415
+ type: "image/png",
416
+ size: 100,
417
+ location: "/tmp/file.png",
418
+ resourceRef: AttachmentResourceRef.MessageAttachment,
419
+ access: AttachmentAccess.Public,
420
+ createdByRef: AttachmentCreatedByRef.Subscriber,
421
+ createdById: "s_1",
422
+ url: "/attachment/download/a_1/file.png",
423
+ shouldDrop: true,
424
+ });
425
+
426
+ expect(parsed.createdBy).toBe("s_1");
427
+ expect("createdById" in parsed).toBe(false);
428
+ expect("shouldDrop" in parsed).toBe(false);
429
+ });
430
+
431
+ it("supports nullable nested attachment owner in full payloads", () => {
432
+ const parsed = attachmentFullSchema.parse({
433
+ id: "a_1",
434
+ createdAt: now,
435
+ updatedAt: now,
436
+ name: "file.png",
437
+ type: "image/png",
438
+ size: 100,
439
+ location: "/tmp/file.png",
440
+ resourceRef: AttachmentResourceRef.MessageAttachment,
441
+ access: AttachmentAccess.Public,
442
+ createdByRef: AttachmentCreatedByRef.Subscriber,
443
+ createdBy: null,
444
+ url: "/attachment/download/a_1/file.png",
445
+ });
446
+
447
+ expect(parsed.createdBy).toBeNull();
448
+ });
449
+
450
+ it("preserves alias mapping for chat/cms/user-access plain contracts", () => {
451
+ const label = labelSchema.parse({
452
+ id: "l_1",
453
+ createdAt: now,
454
+ updatedAt: now,
455
+ title: "VIP",
456
+ name: "VIP",
457
+ builtin: false,
458
+ groupId: "g_1",
459
+ users: [{ id: "s_1" }],
460
+ });
461
+ const subscriber = subscriberSchema.parse({
462
+ id: "s_1",
463
+ createdAt: now,
464
+ updatedAt: now,
465
+ firstName: "Ada",
466
+ lastName: "Lovelace",
467
+ language: "en",
468
+ timezone: 0,
469
+ locale: null,
470
+ gender: null,
471
+ country: null,
472
+ foreignId: "foreign-s-1",
473
+ assignedAt: null,
474
+ lastvisit: null,
475
+ retainedFrom: null,
476
+ channel: { name: "web", data: null },
477
+ labelIds: ["l_1"],
478
+ assignedToId: "u_1",
479
+ avatarId: "a_1",
480
+ sourceId: "source_1",
481
+ });
482
+ const content = contentSchema.parse({
483
+ id: "c_1",
484
+ createdAt: now,
485
+ updatedAt: now,
486
+ title: "Welcome",
487
+ status: true,
488
+ properties: { body: "hello" },
489
+ searchText: "Welcome hello",
490
+ contentTypeId: "ct_1",
491
+ });
492
+ const permission = permissionSchema.parse({
493
+ id: "p_1",
494
+ createdAt: now,
495
+ updatedAt: now,
496
+ action: "read",
497
+ relation: "role",
498
+ modelId: "m_1",
499
+ roleId: "r_1",
500
+ });
501
+
502
+ expect(label.group).toBe("g_1");
503
+ expect("users" in label).toBe(true);
504
+ expect(subscriber.labels).toEqual(["l_1"]);
505
+ expect(subscriber.assignedTo).toBe("u_1");
506
+ expect(subscriber.avatar).toBe("a_1");
507
+ expect(subscriber.source).toBe("source_1");
508
+ expect(content.contentType).toBe("ct_1");
509
+ expect(permission.model).toBe("m_1");
510
+ expect(permission.role).toBe("r_1");
511
+ });
512
+
513
+ it("parses source contracts and supports defaultWorkflowId alias", () => {
514
+ const source = sourceSchema.parse({
515
+ id: "source_1",
516
+ createdAt: now,
517
+ updatedAt: now,
518
+ name: "main-web",
519
+ channel: "web",
520
+ settings: { allowed_domains: "https://example.com" },
521
+ state: true,
522
+ defaultWorkflowId: "wf_1",
523
+ });
524
+ const sourceFull = sourceFullSchema.parse({
525
+ id: "source_1",
526
+ createdAt: now,
527
+ updatedAt: now,
528
+ name: "main-web",
529
+ channel: "web",
530
+ settings: { allowed_domains: "https://example.com" },
531
+ state: true,
532
+ defaultWorkflow: {
533
+ id: "wf_1",
534
+ createdAt: now,
535
+ updatedAt: now,
536
+ name: "default",
537
+ description: null,
538
+ type: WorkflowType.conversational,
539
+ schedule: null,
540
+ inputSchema: {},
541
+ builtin: false,
542
+ x: 0,
543
+ y: 0,
544
+ zoom: 1,
545
+ direction: "horizontal",
546
+ createdBy: null,
547
+ currentVersion: null,
548
+ publishedVersion: null,
549
+ },
550
+ });
551
+ const metadata = channelMetadataSchema.parse({
552
+ name: "web",
553
+ settingsSchema: {
554
+ type: "object",
555
+ },
556
+ });
557
+
558
+ expect(source.defaultWorkflow).toBe("wf_1");
559
+ expect(sourceFull.defaultWorkflow?.id).toBe("wf_1");
560
+ expect(metadata.name).toBe("web");
561
+ });
562
+
563
+ it("accepts source and auditlog as valid model identities", () => {
564
+ const model = modelSchema.parse({
565
+ id: "m_source",
566
+ createdAt: now,
567
+ updatedAt: now,
568
+ name: "Source",
569
+ identity: "source",
570
+ attributes: {},
571
+ });
572
+
573
+ expect(model.identity).toBe("source");
574
+ expect(
575
+ modelSchema.parse({
576
+ id: "m_auditlog",
577
+ createdAt: now,
578
+ updatedAt: now,
579
+ name: "AuditLog",
580
+ identity: "auditlog",
581
+ attributes: {},
582
+ }).identity,
583
+ ).toBe("auditlog");
584
+ });
585
+
586
+ it("parses audit log contracts", () => {
587
+ const auditLog = auditLogSchema.parse({
588
+ id: "audit_1",
589
+ createdAt: now,
590
+ updatedAt: now,
591
+ resourceId: "user_1",
592
+ resourceType: "User",
593
+ resourceLabel: "admin",
594
+ operationId: "typeorm.User.update",
595
+ operationType: "Update",
596
+ operationStatus: "SUCCEEDED",
597
+ actorId: "admin_1",
598
+ actorType: "admin",
599
+ actorLabel: "Admin User (admin)",
600
+ actorIp: "203.0.113.1",
601
+ actorAgent: "browser",
602
+ requestId: "req_1",
603
+ requestMethod: "PATCH",
604
+ requestPath: "/api/user/user_1",
605
+ dataBefore: { email: "old@example.com" },
606
+ dataAfter: { email: "new@example.com" },
607
+ dataDiff: {
608
+ email: {
609
+ before: "old@example.com",
610
+ after: "new@example.com",
611
+ },
612
+ },
613
+ raw: null,
614
+ shouldDrop: true,
615
+ });
616
+
617
+ expect(auditLog.resourceType).toBe("User");
618
+ expect(auditLog.resourceLabel).toBe("admin");
619
+ expect(auditLog.actorLabel).toBe("Admin User (admin)");
620
+ expect("shouldDrop" in auditLog).toBe(false);
621
+ expect(auditLogFullSchema.parse(auditLog).dataDiff).toEqual({
622
+ email: {
623
+ before: "old@example.com",
624
+ after: "new@example.com",
625
+ },
626
+ });
627
+ });
628
+
629
+ it("normalizes nullable and optional values for setting/menu/mcp", () => {
630
+ const setting = settingSchema.parse({
631
+ id: "st_1",
632
+ createdAt: now,
633
+ updatedAt: now,
634
+ group: "chatbot_settings",
635
+ subgroup: null,
636
+ label: "title",
637
+ value: "Hexabot",
638
+ });
639
+ const menu = menuSchema.parse({
640
+ id: "mn_1",
641
+ createdAt: now,
642
+ updatedAt: now,
643
+ title: "Home",
644
+ type: MenuType.nested,
645
+ parentId: null,
646
+ payload: null,
647
+ url: null,
648
+ });
649
+ const mcp = mcpServerSchema.parse({
650
+ id: "mcp_1",
651
+ createdAt: now,
652
+ updatedAt: now,
653
+ name: "Filesystem",
654
+ enabled: true,
655
+ transport: McpServerTransport.stdio,
656
+ command: "npx",
657
+ args: ["-y", "@mcp/server-filesystem"],
658
+ url: null,
659
+ cwd: "/tmp",
660
+ credentialId: null,
661
+ shouldDrop: true,
662
+ });
663
+
664
+ expect(setting.subgroup).toBeNull();
665
+ expect(menu.parent).toBeNull();
666
+ expect(mcp.credential).toBeNull();
667
+ expect("shouldDrop" in mcp).toBe(false);
668
+ });
669
+
670
+ it("keeps credential outputs free of secret value fields", () => {
671
+ const credential = credentialSchema.parse({
672
+ id: "cred_1",
673
+ createdAt: now,
674
+ updatedAt: now,
675
+ name: "OPENAI_API_KEY",
676
+ ownerId: "u_1",
677
+ value: "secret",
678
+ });
679
+
680
+ expect(credential.name).toBe("OPENAI_API_KEY");
681
+ expect(credential.owner).toBe("u_1");
682
+ expect("value" in credential).toBe(false);
683
+ });
684
+
685
+ it("computes workflow derived fields via parser bridge", () => {
686
+ const parser = jest.fn((yml: string) => ({
687
+ defs: {},
688
+ flow: [],
689
+ outputs: {},
690
+ yml,
691
+ }));
692
+ const parsed = createWorkflowFullSchema({ parseDefinition: parser }).parse({
693
+ id: "wf_1",
694
+ createdAt: now,
695
+ updatedAt: now,
696
+ name: "Main",
697
+ description: null,
698
+ type: WorkflowType.conversational,
699
+ schedule: null,
700
+ inputSchema: {},
701
+ builtin: false,
702
+ x: 0,
703
+ y: 0,
704
+ zoom: 1,
705
+ direction: "horizontal",
706
+ runAfterMs: 0,
707
+ createdBy: {
708
+ id: "u_1",
709
+ createdAt: now,
710
+ updatedAt: now,
711
+ firstName: "Ada",
712
+ lastName: "Lovelace",
713
+ language: "en",
714
+ timezone: 1,
715
+ locale: null,
716
+ gender: null,
717
+ country: null,
718
+ foreignId: "foreign-u-1",
719
+ assignedAt: null,
720
+ lastvisit: null,
721
+ retainedFrom: null,
722
+ channel: { name: "web" },
723
+ username: "ada",
724
+ email: "ada@example.com",
725
+ sendEmail: false,
726
+ state: true,
727
+ resetCount: 0,
728
+ resetToken: null,
729
+ roles: [],
730
+ labels: [],
731
+ assignedTo: null,
732
+ avatar: null,
733
+ },
734
+ currentVersion: {
735
+ id: "wfv_1",
736
+ createdAt: now,
737
+ updatedAt: now,
738
+ version: 1,
739
+ definitionYml: "defs: {}\nflow: []\noutputs: {}",
740
+ checksum: "sha",
741
+ message: null,
742
+ action: WorkflowVersionAction.create,
743
+ workflowId: "wf_1",
744
+ createdById: "u_1",
745
+ parentVersionId: null,
746
+ },
747
+ publishedVersion: null,
748
+ });
749
+
750
+ expect(parsed.definitionYml).toContain("defs");
751
+ expect(parsed.definition).toEqual({
752
+ defs: {},
753
+ flow: [],
754
+ outputs: {},
755
+ yml: "defs: {}\nflow: []\noutputs: {}",
756
+ });
757
+ expect(parser).toHaveBeenCalledTimes(1);
758
+ });
759
+
760
+ it("computes workflow run duration and mixed triggeredBy coercion", () => {
761
+ const run = workflowRunSchema.parse({
762
+ id: "run_1",
763
+ createdAt: "2026-01-01T00:00:00.000Z",
764
+ updatedAt: "2026-01-01T00:01:00.000Z",
765
+ status: "finished",
766
+ context: {},
767
+ workflowId: "wf_1",
768
+ workflowVersionId: "wfv_1",
769
+ triggeredById: "s_1",
770
+ threadId: "t_1",
771
+ finishedAt: "2026-01-01T00:02:00.000Z",
772
+ });
773
+ const full = workflowRunFullSchema.parse({
774
+ id: "run_1",
775
+ createdAt: "2026-01-01T00:00:00.000Z",
776
+ updatedAt: "2026-01-01T00:01:00.000Z",
777
+ status: "running",
778
+ context: {},
779
+ workflow: {
780
+ id: "wf_1",
781
+ createdAt: now,
782
+ updatedAt: now,
783
+ name: "Main",
784
+ description: null,
785
+ type: WorkflowType.conversational,
786
+ schedule: null,
787
+ inputSchema: {},
788
+ builtin: false,
789
+ x: 0,
790
+ y: 0,
791
+ zoom: 1,
792
+ direction: "horizontal",
793
+ currentVersion: null,
794
+ publishedVersion: null,
795
+ createdBy: "u_1",
796
+ runAfterMs: 0,
797
+ },
798
+ workflowVersion: null,
799
+ triggeredBy: {
800
+ id: "s_1",
801
+ createdAt: now,
802
+ updatedAt: now,
803
+ firstName: "Sub",
804
+ lastName: "User",
805
+ language: "en",
806
+ timezone: 0,
807
+ locale: null,
808
+ gender: null,
809
+ country: null,
810
+ foreignId: "foreign-s-1",
811
+ assignedAt: null,
812
+ lastvisit: null,
813
+ retainedFrom: null,
814
+ channel: { name: "web", data: {} },
815
+ labels: [],
816
+ assignedTo: null,
817
+ avatar: null,
818
+ },
819
+ thread: null,
820
+ });
821
+
822
+ expect(run.duration).toBe(120000);
823
+ expect(full.triggeredBy?.id).toBe("s_1");
824
+ expect(typeof resolveRunDurationMs(run)).toBe("number");
825
+ });
826
+
827
+ it("falls back to subscriber parsing when user keys exist but are undefined", () => {
828
+ const subscriberWithUndefinedUserFields = {
829
+ id: "s_2",
830
+ createdAt: now,
831
+ updatedAt: now,
832
+ firstName: "Sub",
833
+ lastName: "User",
834
+ language: "en",
835
+ timezone: 0,
836
+ locale: null,
837
+ gender: null,
838
+ country: null,
839
+ foreignId: "foreign-s-2",
840
+ assignedAt: null,
841
+ lastvisit: null,
842
+ retainedFrom: null,
843
+ channel: { name: "web", data: {} },
844
+ labels: [],
845
+ assignedTo: null,
846
+ avatar: null,
847
+ type: "SubscriberOrmEntity",
848
+ username: undefined,
849
+ email: undefined,
850
+ sendEmail: undefined,
851
+ resetCount: undefined,
852
+ };
853
+ const run = workflowRunFullSchema.parse({
854
+ id: "run_2",
855
+ createdAt: now,
856
+ updatedAt: now,
857
+ status: "running",
858
+ context: {},
859
+ workflow: {
860
+ id: "wf_2",
861
+ createdAt: now,
862
+ updatedAt: now,
863
+ name: "Main",
864
+ description: null,
865
+ type: WorkflowType.conversational,
866
+ schedule: null,
867
+ inputSchema: {},
868
+ builtin: false,
869
+ x: 0,
870
+ y: 0,
871
+ zoom: 1,
872
+ direction: "horizontal",
873
+ currentVersion: null,
874
+ publishedVersion: null,
875
+ createdBy: "u_1",
876
+ runAfterMs: 0,
877
+ },
878
+ workflowVersion: null,
879
+ triggeredBy: subscriberWithUndefinedUserFields,
880
+ thread: null,
881
+ });
882
+ const memoryRecord = memoryRecordFullSchema.parse({
883
+ id: "mem_2",
884
+ createdAt: now,
885
+ updatedAt: now,
886
+ value: { counter: 2 },
887
+ definition: {
888
+ id: "def_2",
889
+ createdAt: now,
890
+ updatedAt: now,
891
+ name: "Session",
892
+ slug: "session",
893
+ scope: MemoryScope.workflow,
894
+ schema: {},
895
+ },
896
+ owner: subscriberWithUndefinedUserFields,
897
+ workflow: null,
898
+ run: null,
899
+ thread: null,
900
+ });
901
+
902
+ expect(run.triggeredBy?.id).toBe("s_2");
903
+ expect(
904
+ Object.prototype.hasOwnProperty.call(run.triggeredBy ?? {}, "username"),
905
+ ).toBe(false);
906
+ expect(memoryRecord.owner.id).toBe("s_2");
907
+ expect(
908
+ Object.prototype.hasOwnProperty.call(memoryRecord.owner, "username"),
909
+ ).toBe(false);
910
+ });
911
+
912
+ it("coerces workflow memory aliases and test dummy fixtures", () => {
913
+ const record = memoryRecordSchema.parse({
914
+ id: "mem_1",
915
+ createdAt: now,
916
+ updatedAt: now,
917
+ value: { counter: 1 },
918
+ ttlSeconds: 60,
919
+ expiresAt: null,
920
+ definitionId: "def_1",
921
+ ownerId: "u_1",
922
+ workflowId: "wf_1",
923
+ runId: "run_1",
924
+ threadId: "th_1",
925
+ });
926
+ const dummy = dummySchema.parse({
927
+ id: "d_1",
928
+ createdAt: now,
929
+ updatedAt: now,
930
+ dummy: "ok",
931
+ dynamicField: { field: FieldType.text },
932
+ shouldDrop: true,
933
+ });
934
+
935
+ expect(record.definition).toBe("def_1");
936
+ expect(record.owner).toBe("u_1");
937
+ expect(record.workflow).toBe("wf_1");
938
+ expect(record.run).toBe("run_1");
939
+ expect(record.thread).toBe("th_1");
940
+ expect(dummy.dynamicField).toEqual({ field: FieldType.text });
941
+ expect("shouldDrop" in dummy).toBe(false);
942
+ });
943
+
944
+ it("supports analytics enums in stats payloads", () => {
945
+ const stats = statsSchema.parse({
946
+ id: "st_1",
947
+ createdAt: now,
948
+ updatedAt: now,
949
+ type: StatsType.new_threads,
950
+ day: now,
951
+ value: 15,
952
+ name: "New Threads",
953
+ });
954
+
955
+ expect(stats.type).toBe(StatsType.new_threads);
956
+ expect(
957
+ statsSchema.parse({
958
+ ...stats,
959
+ type: StatsType.handoffs,
960
+ name: "Handoffs",
961
+ }).type,
962
+ ).toBe(StatsType.handoffs);
963
+ });
964
+
965
+ it("parses integration health payload contracts", () => {
966
+ const health = integrationHealthResponseSchema.parse({
967
+ checkedAt: now,
968
+ integrations: [
969
+ {
970
+ id: "channel:web",
971
+ kind: "channel",
972
+ name: "Web",
973
+ status: "healthy",
974
+ checkedAt: now,
975
+ reason: "channel.active_sources",
976
+ message: "1 active source",
977
+ details: {
978
+ activeSources: 1,
979
+ inactiveSources: 0,
980
+ },
981
+ shouldDrop: true,
982
+ },
983
+ {
984
+ id: "service:smtp",
985
+ kind: "service",
986
+ name: "Email (SMTP)",
987
+ status: "disabled",
988
+ checkedAt: now,
989
+ },
990
+ ],
991
+ });
992
+
993
+ expect(health.integrations[0].status).toBe("healthy");
994
+ expect(health.integrations[1].kind).toBe("service");
995
+ expect("shouldDrop" in health.integrations[0]).toBe(false);
996
+ });
997
+
998
+ it("supports memory scope enum values", () => {
999
+ expect(MemoryScope.workflow).toBe("workflow");
1000
+ });
1001
+
1002
+ it("parses shared incoming and outgoing message payload contracts", () => {
1003
+ const expectedQuickReplies = [{ title: "Yes", payload: "yes" }];
1004
+ const outgoing = stdOutgoingMessageSchema.parse({
1005
+ type: OutgoingMessageType.quickReply,
1006
+ data: {
1007
+ text: "Hello from bot",
1008
+ quickReplies: expectedQuickReplies,
1009
+ },
1010
+ });
1011
+ const incoming = stdIncomingMessageSchema.parse({
1012
+ type: IncomingMessageType.location,
1013
+ data: {
1014
+ coordinates: { lat: 36.8, lon: 10.2 },
1015
+ },
1016
+ });
1017
+
1018
+ expect(outgoing).toEqual({
1019
+ type: OutgoingMessageType.quickReply,
1020
+ data: {
1021
+ text: "Hello from bot",
1022
+ quickReplies: expectedQuickReplies,
1023
+ },
1024
+ });
1025
+ expect(incoming).toEqual({
1026
+ type: IncomingMessageType.location,
1027
+ data: {
1028
+ coordinates: { lat: 36.8, lon: 10.2 },
1029
+ },
1030
+ });
1031
+ });
1032
+
1033
+ it("parses shared outgoing message envelopes including system type", () => {
1034
+ const textEnvelope = stdOutgoingEnvelopeSchema.parse({
1035
+ type: OutgoingMessageType.text,
1036
+ data: { text: "Hi" },
1037
+ });
1038
+ const systemEnvelope = stdOutgoingEnvelopeSchema.parse({
1039
+ type: OutgoingMessageType.system,
1040
+ data: { outcome: "ok", data: { source: "test" } },
1041
+ });
1042
+
1043
+ expect(textEnvelope.type).toBe(OutgoingMessageType.text);
1044
+ expect(systemEnvelope.type).toBe(OutgoingMessageType.system);
1045
+ expect(stdOutgoingMessageSchema.safeParse(systemEnvelope).success).toBe(
1046
+ false,
1047
+ );
1048
+ });
1049
+
1050
+ it("keeps outgoing quick replies and buttons in parsed message entities", () => {
1051
+ const base = {
1052
+ id: "msg_1",
1053
+ createdAt: now,
1054
+ updatedAt: now,
1055
+ read: false,
1056
+ delivery: false,
1057
+ handover: false,
1058
+ threadId: "th_1",
1059
+ };
1060
+ const quickRepliesMessage = messageSchema.parse({
1061
+ ...base,
1062
+ message: {
1063
+ type: OutgoingMessageType.quickReply,
1064
+ data: {
1065
+ text: "Choose one",
1066
+ quickReplies: [
1067
+ { title: "Yes", payload: "yes" },
1068
+ { title: "No", payload: "no" },
1069
+ ],
1070
+ },
1071
+ },
1072
+ });
1073
+ const buttonsMessage = messageSchema.parse({
1074
+ ...base,
1075
+ id: "msg_2",
1076
+ message: {
1077
+ type: OutgoingMessageType.buttons,
1078
+ data: {
1079
+ text: "Click one",
1080
+ buttons: [
1081
+ { type: "postback", title: "About", payload: "about" },
1082
+ { type: "web_url", title: "Website", url: "https://hexabot.ai" },
1083
+ ],
1084
+ },
1085
+ },
1086
+ });
1087
+
1088
+ expect(quickRepliesMessage.message).toEqual({
1089
+ type: OutgoingMessageType.quickReply,
1090
+ data: {
1091
+ text: "Choose one",
1092
+ quickReplies: [
1093
+ { title: "Yes", payload: "yes" },
1094
+ { title: "No", payload: "no" },
1095
+ ],
1096
+ },
1097
+ });
1098
+ expect(buttonsMessage.message).toEqual({
1099
+ type: OutgoingMessageType.buttons,
1100
+ data: {
1101
+ text: "Click one",
1102
+ buttons: [
1103
+ { type: "postback", title: "About", payload: "about" },
1104
+ { type: "web_url", title: "Website", url: "https://hexabot.ai" },
1105
+ ],
1106
+ },
1107
+ });
1108
+ });
1109
+
1110
+ it("keeps incoming postback payloads when parsing message entities", () => {
1111
+ const parsed = messageSchema.parse({
1112
+ id: "msg_postback",
1113
+ createdAt: now,
1114
+ updatedAt: now,
1115
+ read: false,
1116
+ delivery: false,
1117
+ handover: false,
1118
+ threadId: "th_1",
1119
+ message: {
1120
+ type: IncomingMessageType.postback,
1121
+ data: { text: "Clicked", payload: "about" },
1122
+ },
1123
+ });
1124
+
1125
+ expect(parsed.message).toEqual({
1126
+ type: IncomingMessageType.postback,
1127
+ data: { text: "Clicked", payload: "about" },
1128
+ });
1129
+ });
1130
+
1131
+ it("rejects legacy flat payloads and envelope aliases", () => {
1132
+ const legacyOutgoing = stdOutgoingMessageSchema.safeParse({
1133
+ text: "Hello from bot",
1134
+ });
1135
+ const legacyOutgoingFormatKey = stdOutgoingMessageSchema.safeParse({
1136
+ format: OutgoingMessageType.text,
1137
+ data: { text: "Hello from bot" },
1138
+ });
1139
+ const legacyIncomingTextValue = stdIncomingMessageSchema.safeParse({
1140
+ type: "message",
1141
+ data: { text: "Hello from user" },
1142
+ });
1143
+ const legacyIncomingQuickReplyValue = stdIncomingMessageSchema.safeParse({
1144
+ type: "quick_reply",
1145
+ data: { text: "Reply", payload: "yes" },
1146
+ });
1147
+ const legacyIncomingAttachmentValue = stdIncomingMessageSchema.safeParse({
1148
+ type: "attachments",
1149
+ data: {
1150
+ serializedText: "attachment:image:file.jpg",
1151
+ attachment: {
1152
+ type: "image",
1153
+ payload: { id: null, url: "https://example.com/file.jpg" },
1154
+ },
1155
+ },
1156
+ });
1157
+ const legacyIncomingSerializedTextKey = stdIncomingMessageSchema.safeParse({
1158
+ type: IncomingMessageType.attachment,
1159
+ data: {
1160
+ serialized_text: "attachment:image:file.jpg",
1161
+ attachment: {
1162
+ type: "image",
1163
+ payload: { id: null, url: "https://example.com/file.jpg" },
1164
+ },
1165
+ },
1166
+ });
1167
+ const legacyIncoming = stdIncomingMessageSchema.safeParse({
1168
+ type: IncomingMessageType.location,
1169
+ coordinates: { lat: 36.8, lon: 10.2 },
1170
+ });
1171
+ const legacyQuickRepliesAlias = stdOutgoingMessageSchema.safeParse({
1172
+ type: OutgoingMessageType.quickReply,
1173
+ data: {
1174
+ text: "Choose one",
1175
+ quick_replies: [{ title: "Yes", payload: "yes" }],
1176
+ },
1177
+ });
1178
+ const legacyEnvelopeShape = stdOutgoingEnvelopeSchema.safeParse({
1179
+ type: OutgoingMessageType.text,
1180
+ message: { text: "Hi" },
1181
+ });
1182
+
1183
+ expect(legacyOutgoing.success).toBe(false);
1184
+ expect(legacyOutgoingFormatKey.success).toBe(false);
1185
+ expect(legacyIncomingTextValue.success).toBe(false);
1186
+ expect(legacyIncomingQuickReplyValue.success).toBe(false);
1187
+ expect(legacyIncomingAttachmentValue.success).toBe(false);
1188
+ expect(legacyIncomingSerializedTextKey.success).toBe(false);
1189
+ expect(legacyIncoming.success).toBe(false);
1190
+ expect(legacyQuickRepliesAlias.success).toBe(false);
1191
+ expect(legacyEnvelopeShape.success).toBe(false);
1192
+ });
1193
+
1194
+ it("rejects plain-string message payloads in strict message schema", () => {
1195
+ const base = {
1196
+ id: "msg_1",
1197
+ createdAt: now,
1198
+ updatedAt: now,
1199
+ read: false,
1200
+ delivery: false,
1201
+ handover: false,
1202
+ threadId: "th_1",
1203
+ };
1204
+ const valid = messageSchema.safeParse({
1205
+ ...base,
1206
+ message: {
1207
+ type: OutgoingMessageType.text,
1208
+ data: { text: "Hello there" },
1209
+ },
1210
+ });
1211
+ const invalid = messageSchema.safeParse({
1212
+ ...base,
1213
+ message: "legacy-string-payload",
1214
+ });
1215
+
1216
+ expect(valid.success).toBe(true);
1217
+ expect(invalid.success).toBe(false);
1218
+ });
1219
+
1220
+ it("uses sender/recipient direction to validate ambiguous message types", () => {
1221
+ const base = {
1222
+ id: "msg_direction",
1223
+ createdAt: now,
1224
+ updatedAt: now,
1225
+ read: false,
1226
+ delivery: false,
1227
+ handover: false,
1228
+ threadId: "th_1",
1229
+ };
1230
+ const outgoingText = messageSchema.safeParse({
1231
+ ...base,
1232
+ recipient: "sub_1",
1233
+ message: {
1234
+ type: OutgoingMessageType.text,
1235
+ data: { text: "Bot text" },
1236
+ },
1237
+ });
1238
+ const incomingText = messageSchema.safeParse({
1239
+ ...base,
1240
+ sender: "sub_1",
1241
+ message: {
1242
+ type: IncomingMessageType.text,
1243
+ data: { text: "User text" },
1244
+ },
1245
+ });
1246
+ const invalidOutgoingAsIncoming = messageSchema.safeParse({
1247
+ ...base,
1248
+ recipient: "sub_1",
1249
+ message: {
1250
+ type: IncomingMessageType.postback,
1251
+ data: { text: "Clicked", payload: "go" },
1252
+ },
1253
+ });
1254
+ const invalidIncomingAsOutgoing = messageSchema.safeParse({
1255
+ ...base,
1256
+ sender: "sub_1",
1257
+ message: {
1258
+ type: OutgoingMessageType.buttons,
1259
+ data: {
1260
+ text: "Choose",
1261
+ buttons: [{ type: "postback", title: "Go", payload: "go" }],
1262
+ },
1263
+ },
1264
+ });
1265
+
1266
+ expect(outgoingText.success).toBe(true);
1267
+ expect(incomingText.success).toBe(true);
1268
+ expect(invalidOutgoingAsIncoming.success).toBe(false);
1269
+ expect(invalidIncomingAsOutgoing.success).toBe(false);
1270
+ });
1271
+ });