@memberjunction/server 6.2.0-edge.4 → 6.2.0-edge.5

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 (287) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionJanitor.d.ts +91 -30
  4. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  5. package/dist/agentSessions/SessionJanitor.js +311 -81
  6. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  7. package/dist/agentSessions/SessionManager.d.ts +5 -1
  8. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  9. package/dist/agentSessions/SessionManager.js +13 -1
  10. package/dist/agentSessions/SessionManager.js.map +1 -1
  11. package/dist/apiRoot.d.ts +32 -0
  12. package/dist/apiRoot.d.ts.map +1 -0
  13. package/dist/apiRoot.js +67 -0
  14. package/dist/apiRoot.js.map +1 -0
  15. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  16. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  17. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  18. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  19. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  20. package/dist/auth/LoginScreenEngine.js +57 -0
  21. package/dist/auth/LoginScreenEngine.js.map +1 -0
  22. package/dist/auth/index.d.ts +1 -0
  23. package/dist/auth/index.d.ts.map +1 -1
  24. package/dist/auth/index.js +1 -0
  25. package/dist/auth/index.js.map +1 -1
  26. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  27. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  28. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  29. package/dist/config.d.ts +136 -22
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +43 -4
  32. package/dist/config.js.map +1 -1
  33. package/dist/coreRoutes.d.ts +29 -0
  34. package/dist/coreRoutes.d.ts.map +1 -0
  35. package/dist/coreRoutes.js +142 -0
  36. package/dist/coreRoutes.js.map +1 -0
  37. package/dist/generated/graphql-schemas/__mj.d.ts +369 -3
  38. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  39. package/dist/generated/graphql-schemas/__mj.js +1750 -7
  40. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  41. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  42. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  43. package/dist/generic/CacheInvalidationResolver.js +59 -19
  44. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  45. package/dist/generic/PushStatusResolver.js +1 -1
  46. package/dist/generic/PushStatusResolver.js.map +1 -1
  47. package/dist/generic/ResolverBase.d.ts +103 -44
  48. package/dist/generic/ResolverBase.d.ts.map +1 -1
  49. package/dist/generic/ResolverBase.js +264 -305
  50. package/dist/generic/ResolverBase.js.map +1 -1
  51. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  52. package/dist/generic/RunViewResolver.js +12 -0
  53. package/dist/generic/RunViewResolver.js.map +1 -1
  54. package/dist/globalCors.d.ts +33 -0
  55. package/dist/globalCors.d.ts.map +1 -0
  56. package/dist/globalCors.js +58 -0
  57. package/dist/globalCors.js.map +1 -0
  58. package/dist/index.d.ts +11 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +112 -160
  61. package/dist/index.js.map +1 -1
  62. package/dist/multiTenancy/index.d.ts.map +1 -1
  63. package/dist/multiTenancy/index.js +22 -2
  64. package/dist/multiTenancy/index.js.map +1 -1
  65. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -1
  66. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +2 -1
  67. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -1
  68. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -1
  69. package/dist/realtimeSessions/RealtimeVerifyRouter.js +2 -1
  70. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -1
  71. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  72. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  73. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  74. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  75. package/dist/realtimeSessions/index.d.ts +2 -0
  76. package/dist/realtimeSessions/index.d.ts.map +1 -1
  77. package/dist/realtimeSessions/index.js +2 -0
  78. package/dist/realtimeSessions/index.js.map +1 -1
  79. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  80. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  81. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  82. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  83. package/dist/realtimeSessions/sessionAccess.d.ts +6 -0
  84. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
  85. package/dist/realtimeSessions/sessionAccess.js +7 -3
  86. package/dist/realtimeSessions/sessionAccess.js.map +1 -1
  87. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  88. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  89. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  90. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  91. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  92. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  93. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  94. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  95. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  96. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  97. package/dist/resolvers/DatasetResolver.js +57 -8
  98. package/dist/resolvers/DatasetResolver.js.map +1 -1
  99. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  100. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  101. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  102. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  103. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +18 -3
  104. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  105. package/dist/resolvers/FetchEntityVectorsResolver.js +100 -35
  106. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  107. package/dist/resolvers/FileResolver.d.ts +82 -11
  108. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  109. package/dist/resolvers/FileResolver.js +279 -101
  110. package/dist/resolvers/FileResolver.js.map +1 -1
  111. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  112. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  113. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  114. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  115. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  116. package/dist/resolvers/GetDataResolver.js +6 -3
  117. package/dist/resolvers/GetDataResolver.js.map +1 -1
  118. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  119. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  120. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  121. package/dist/resolvers/QueryResolver.d.ts +2 -0
  122. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  123. package/dist/resolvers/QueryResolver.js +13 -0
  124. package/dist/resolvers/QueryResolver.js.map +1 -1
  125. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +2 -8
  126. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  127. package/dist/resolvers/RealtimeClientSessionResolver.js +10 -26
  128. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  129. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  130. package/dist/resolvers/RunAIAgentResolver.js +35 -6
  131. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  132. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  133. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  134. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  135. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  136. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  137. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  138. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  139. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  140. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  141. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  142. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  143. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  144. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  145. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  146. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  147. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  148. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  149. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  150. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  151. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  152. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  153. package/dist/resolvers/agent-run-data-guard.js +72 -0
  154. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  155. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  156. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  157. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  158. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  159. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  160. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  161. package/dist/rest/ViewOperationsHandler.js +21 -12
  162. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  163. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  164. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  165. package/dist/rest/setupRESTEndpoints.js +6 -0
  166. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  167. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  168. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  169. package/dist/serverExtensionReservedRoots.js +14 -17
  170. package/dist/serverExtensionReservedRoots.js.map +1 -1
  171. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  172. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  173. package/dist/services/MJStorageBlobStore.js +25 -19
  174. package/dist/services/MJStorageBlobStore.js.map +1 -1
  175. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  176. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  177. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  178. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  179. package/dist/services/TaskGraphActionRunner.js +27 -5
  180. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  181. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  182. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  183. package/dist/services/TaskGraphAgentRunner.js +157 -9
  184. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  185. package/dist/services/TaskGraphContinuationDeliverer.d.ts +54 -2
  186. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  187. package/dist/services/TaskGraphContinuationDeliverer.js +598 -15
  188. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  189. package/dist/sharedCache.d.ts.map +1 -1
  190. package/dist/sharedCache.js +3 -0
  191. package/dist/sharedCache.js.map +1 -1
  192. package/package.json +99 -98
  193. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  194. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  195. package/src/__tests__/FetchEntityVectorsResolver.test.ts +163 -15
  196. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  197. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  198. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  199. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  200. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  201. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  202. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  203. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  204. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  205. package/src/__tests__/ResolverBase.clientOldValues.test.ts +209 -6
  206. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  207. package/src/__tests__/RunAIAgentResolver.branch.test.ts +1 -0
  208. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +5 -3
  209. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  210. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  211. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  212. package/src/__tests__/SessionJanitor.test.ts +365 -10
  213. package/src/__tests__/SessionManager.test.ts +27 -0
  214. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  215. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  216. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +356 -22
  217. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  218. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  219. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  220. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  221. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  222. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  223. package/src/__tests__/apiRootBoot.test.ts +202 -0
  224. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  225. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  226. package/src/__tests__/globalCors.test.ts +217 -0
  227. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  228. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  229. package/src/__tests__/multiTenancy.test.ts +1 -1
  230. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  231. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  232. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  233. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  234. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -98
  235. package/src/__tests__/sharedCache.test.ts +5 -3
  236. package/src/agentSessions/SessionJanitor.ts +369 -93
  237. package/src/agentSessions/SessionManager.ts +13 -1
  238. package/src/apiRoot.ts +78 -0
  239. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  240. package/src/auth/LoginScreenEngine.ts +50 -0
  241. package/src/auth/index.ts +1 -0
  242. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  243. package/src/config.ts +46 -5
  244. package/src/coreRoutes.ts +166 -0
  245. package/src/generated/graphql-schemas/__mj.ts +1273 -10
  246. package/src/generic/CacheInvalidationResolver.ts +73 -18
  247. package/src/generic/PushStatusResolver.ts +1 -1
  248. package/src/generic/ResolverBase.ts +304 -325
  249. package/src/generic/RunViewResolver.ts +17 -0
  250. package/src/globalCors.ts +71 -0
  251. package/src/index.ts +116 -174
  252. package/src/multiTenancy/index.ts +24 -2
  253. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +2 -1
  254. package/src/realtimeSessions/RealtimeVerifyRouter.ts +2 -1
  255. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  256. package/src/realtimeSessions/index.ts +2 -0
  257. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  258. package/src/realtimeSessions/sessionAccess.ts +7 -3
  259. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  260. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  261. package/src/resolvers/DatasetResolver.ts +70 -10
  262. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  263. package/src/resolvers/FetchEntityVectorsResolver.ts +121 -39
  264. package/src/resolvers/FileResolver.ts +321 -106
  265. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  266. package/src/resolvers/GetDataResolver.ts +6 -3
  267. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  268. package/src/resolvers/QueryResolver.ts +11 -0
  269. package/src/resolvers/RealtimeClientSessionResolver.ts +10 -33
  270. package/src/resolvers/RunAIAgentResolver.ts +39 -4
  271. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  272. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  273. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  274. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  275. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  276. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  277. package/src/resolvers/agent-run-data-guard.ts +89 -0
  278. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  279. package/src/rest/ViewOperationsHandler.ts +29 -13
  280. package/src/rest/setupRESTEndpoints.ts +6 -0
  281. package/src/serverExtensionReservedRoots.ts +14 -17
  282. package/src/services/MJStorageBlobStore.ts +31 -30
  283. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  284. package/src/services/TaskGraphActionRunner.ts +25 -5
  285. package/src/services/TaskGraphAgentRunner.ts +188 -11
  286. package/src/services/TaskGraphContinuationDeliverer.ts +704 -14
  287. package/src/sharedCache.ts +3 -0
@@ -9,11 +9,28 @@
9
9
  * compare-and-swap that marks a completion delivered; an escaping error would either abort that
10
10
  * guard or leave the graph looking undelivered and re-notifying on every later sweep.
11
11
  */
12
+ import 'reflect-metadata';
12
13
  import { describe, it, expect, vi, beforeEach } from 'vitest';
13
14
 
15
+ const { fromMetadataProvider, runAgent, processAgentArtifacts } = vi.hoisted(() => ({
16
+ fromMetadataProvider: vi.fn(),
17
+ runAgent: vi.fn(),
18
+ processAgentArtifacts: vi.fn(),
19
+ }));
20
+
14
21
  vi.mock('@memberjunction/core', () => ({
15
22
  LogError: vi.fn(),
16
23
  LogStatus: vi.fn(),
24
+ RunView: {
25
+ FromMetadataProvider: fromMetadataProvider,
26
+ },
27
+ }));
28
+
29
+ vi.mock('@memberjunction/generic-database-provider', () => ({
30
+ UserCache: {
31
+ Instance: { Users: [{ ID: 'owner-user-99', Email: 'owner@example.com' }] },
32
+ Users: [{ ID: 'owner-user-99', Email: 'owner@example.com' }],
33
+ },
17
34
  }));
18
35
 
19
36
  vi.mock('@memberjunction/core-entities', () => ({
@@ -24,22 +41,27 @@ vi.mock('@memberjunction/core-entities', () => ({
24
41
  vi.mock('@memberjunction/ai-core-plus', () => ({ MJAIAgentEntityExtended: class {} }));
25
42
  vi.mock('@memberjunction/ai', () => ({ ChatMessageRole: { user: 'user' } }));
26
43
 
27
- const runAgent = vi.fn().mockResolvedValue({ success: true });
28
44
  vi.mock('@memberjunction/ai-agents', () => ({
29
- AgentRunner: class { RunAgent = runAgent; },
45
+ AgentRunner: class {
46
+ RunAgent = runAgent;
47
+ ProcessAgentArtifacts = processAgentArtifacts;
48
+ },
30
49
  }));
31
50
 
32
51
  import { TaskGraphContinuationDeliverer } from '../services/TaskGraphContinuationDeliverer';
33
52
  import type { TaskContinuationParams } from '@memberjunction/task-graph';
34
53
  import type { UserInfo } from '@memberjunction/core';
54
+ import type { PubSubEngine } from 'type-graphql';
35
55
 
36
56
  /** A conversation-detail row that can be loaded (the source) and saved (the reply). */
37
57
  function detailRow(over: Partial<{ loads: boolean; saves: boolean; conversationID: string; branchID: string | null }> = {}) {
38
58
  const { loads = true, saves = true, conversationID = 'conv-1', branchID = null } = over;
39
59
  return {
60
+ ID: 'reply-detail-1',
40
61
  ConversationID: conversationID,
41
62
  BranchID: branchID as string | null,
42
63
  Role: '', Status: '', HiddenToUser: true, Message: '',
64
+ AgentID: null as string | null,
43
65
  NewRecord: vi.fn(),
44
66
  Load: vi.fn().mockResolvedValue(loads),
45
67
  Save: vi.fn().mockResolvedValue(saves),
@@ -60,13 +82,13 @@ function harness(over: Parameters<typeof detailRow>[0] = {}) {
60
82
  }),
61
83
  };
62
84
  const providerFactory = { CreateProvider: vi.fn().mockResolvedValue(provider) };
85
+ const pubSub = { publish: vi.fn().mockResolvedValue(undefined) } as unknown as PubSubEngine;
63
86
  const deliverer = new TaskGraphContinuationDeliverer(
64
87
  providerFactory as never,
65
88
  { ID: 'user-1' } as UserInfo,
89
+ pubSub,
66
90
  );
67
- // First row is the source detail (Load), second is the reply (Save). Every reply row starts on a
68
- // 'stale' fork, so a test sees the deliverer set it.
69
- return { deliverer, providerFactory, provider, rows, reply: () => rows[1] };
91
+ return { deliverer, providerFactory, provider, rows, pubSub, reply: () => rows[1] };
70
92
  }
71
93
 
72
94
  const params = (over: Partial<TaskContinuationParams> = {}): TaskContinuationParams => ({
@@ -83,10 +105,21 @@ const params = (over: Partial<TaskContinuationParams> = {}): TaskContinuationPar
83
105
  ...over,
84
106
  });
85
107
 
86
- beforeEach(() => vi.clearAllMocks());
108
+ beforeEach(() => {
109
+ vi.clearAllMocks();
110
+ fromMetadataProvider.mockReturnValue({
111
+ RunView: vi.fn().mockResolvedValue({ Success: false, Results: [] }),
112
+ });
113
+ runAgent.mockResolvedValue({
114
+ success: true,
115
+ payload: 'Synthesized workflow outcome',
116
+ agentRun: { ID: 'cont-run-1', Message: 'Synthesized workflow outcome' },
117
+ });
118
+ processAgentArtifacts.mockResolvedValue(undefined);
119
+ });
87
120
 
88
121
  describe('posting the outcome', () => {
89
- it('writes an AI-role message into the graph\'s conversation', async () => {
122
+ it('writes an AI-role message into the graph\'s conversation and broadcasts completion via pubSub', async () => {
90
123
  const h = harness();
91
124
  await h.deliverer.PostMessage(params());
92
125
 
@@ -96,6 +129,7 @@ describe('posting the outcome', () => {
96
129
  expect(reply.Role).toBe('AI');
97
130
  expect(reply.Status).toBe('Complete');
98
131
  expect(reply.HiddenToUser).toBe(false);
132
+ expect(h.pubSub.publish).toHaveBeenCalledTimes(2);
99
133
  });
100
134
 
101
135
  it('names the workflow and carries the roll-up', async () => {
@@ -106,8 +140,6 @@ describe('posting the outcome', () => {
106
140
  });
107
141
 
108
142
  it('lists each step, not just an aggregate status', async () => {
109
- // A graph where nine of ten steps succeeded is a materially different message from one that
110
- // failed outright, and the roll-up alone cannot say which step went wrong.
111
143
  const h = harness();
112
144
  await h.deliverer.PostMessage(params({
113
145
  Tasks: [
@@ -130,7 +162,6 @@ describe('posting the outcome', () => {
130
162
  const message = h.reply().Message;
131
163
  expect(message).toContain('Step 0');
132
164
  expect(message).not.toContain('Step 49');
133
- // The count is stated rather than silently dropped — the task rows remain the full record.
134
165
  expect(message).toContain('30 more');
135
166
  });
136
167
 
@@ -147,8 +178,6 @@ describe('posting the outcome', () => {
147
178
  });
148
179
 
149
180
  it('mints a fresh provider per delivery', async () => {
150
- // Deliveries run outside any request and concurrently with task execution; a shared provider
151
- // would share one transaction scope across unrelated work.
152
181
  const h = harness();
153
182
  await h.deliverer.PostMessage(params());
154
183
  expect(h.providerFactory.CreateProvider).toHaveBeenCalledTimes(1);
@@ -157,8 +186,6 @@ describe('posting the outcome', () => {
157
186
 
158
187
  describe('it never throws — the dispatcher marks delivery inside a CAS guard', () => {
159
188
  it('does nothing for a headless graph, without treating it as an error', async () => {
160
- // A graph submitted by a schedule, an entity-change trigger or an API call has no
161
- // conversation to answer. Most workflows are in this shape.
162
189
  const h = harness();
163
190
  await expect(h.deliverer.PostMessage(params({ ConversationDetailID: null }))).resolves.toBeUndefined();
164
191
  expect(h.providerFactory.CreateProvider).not.toHaveBeenCalled();
@@ -167,7 +194,7 @@ describe('it never throws — the dispatcher marks delivery inside a CAS guard',
167
194
  it('survives a conversation detail that will not load', async () => {
168
195
  const h = harness({ loads: false });
169
196
  await expect(h.deliverer.PostMessage(params())).resolves.toBeUndefined();
170
- expect(h.rows.length).toBe(1); // never got as far as building a reply
197
+ expect(h.rows.length).toBe(1);
171
198
  });
172
199
 
173
200
  it('survives a failed save', async () => {
@@ -185,7 +212,6 @@ describe('it never throws — the dispatcher marks delivery inside a CAS guard',
185
212
  });
186
213
 
187
214
  describe('Reinvoke — restarting the submitting agent', () => {
188
- /** A provider that hands out loadable agent-run and agent rows, then the reply detail. */
189
215
  function reinvokeHarness(over: { runLoads?: boolean; agentLoads?: boolean } = {}) {
190
216
  const { runLoads = true, agentLoads = true } = over;
191
217
  const rows: Array<Record<string, unknown>> = [];
@@ -208,14 +234,16 @@ describe('Reinvoke — restarting the submitting agent', () => {
208
234
  return detail;
209
235
  }),
210
236
  };
237
+ const pubSub = { publish: vi.fn().mockResolvedValue(undefined) } as unknown as PubSubEngine;
211
238
  const deliverer = new TaskGraphContinuationDeliverer(
212
239
  { CreateProvider: vi.fn().mockResolvedValue(provider) } as never,
213
240
  { ID: 'user-1' } as UserInfo,
241
+ pubSub,
214
242
  );
215
- return { deliverer, provider, rows };
243
+ return { deliverer, provider, rows, pubSub };
216
244
  }
217
245
 
218
- it('starts the submitting agent a fresh turn carrying the outcome', async () => {
246
+ it('starts the submitting agent a fresh turn carrying the outcome, saves reply detail, processes artifacts, and publishes completion', async () => {
219
247
  const h = reinvokeHarness();
220
248
  await h.deliverer.Reinvoke(params());
221
249
 
@@ -223,11 +251,17 @@ describe('Reinvoke — restarting the submitting agent', () => {
223
251
  const call = runAgent.mock.calls[0][0];
224
252
  expect(call.agent.ID).toBe('agent-1');
225
253
  expect(call.conversationMessages[0].content).toContain('Weekly digest');
254
+ expect(call.conversationDetailId).toBe('reply-detail-1');
255
+
256
+ const reply = h.rows[3] as ReturnType<typeof detailRow>;
257
+ expect(reply.Role).toBe('AI');
258
+ expect(reply.Status).toBe('Complete');
259
+ expect(reply.Message).toBe('Synthesized workflow outcome');
260
+ expect(processAgentArtifacts).toHaveBeenCalledTimes(1);
261
+ expect(h.pubSub.publish).toHaveBeenCalledTimes(2);
226
262
  });
227
263
 
228
264
  it('stamps depth + 1 — the value that makes MAX_REINVOKE_DEPTH real', async () => {
229
- // Without this the next graph the restarted run submits begins the chain at zero again, and
230
- // the cap can never fire. It is the whole reason ContinuationDepth exists as a column.
231
265
  const h = reinvokeHarness();
232
266
  await h.deliverer.Reinvoke(params({ ReinvokeDepth: 3 }));
233
267
  expect(runAgent.mock.calls[0][0].continuationDepth).toBe(4);
@@ -240,7 +274,6 @@ describe('Reinvoke — restarting the submitting agent', () => {
240
274
  });
241
275
 
242
276
  it('falls back to posting when the graph records no submitting run', async () => {
243
- // A schedule- or trigger-started graph has no turn to continue.
244
277
  const h = reinvokeHarness();
245
278
  await h.deliverer.Reinvoke(params({ SubmittedByAgentRunID: null }));
246
279
  expect(runAgent).not.toHaveBeenCalled();
@@ -266,7 +299,6 @@ describe('Reinvoke — restarting the submitting agent', () => {
266
299
  });
267
300
 
268
301
  describe('Reinvoke — on the fork of the message that started the graph', () => {
269
- /** A provider whose agent run and agent load, and whose origin detail sits on `originBranch` (null is Main). */
270
302
  function forkHarness(originBranch: string | null) {
271
303
  const provider = {
272
304
  GetEntityObject: vi.fn().mockImplementation(async (name: string) => {
@@ -291,6 +323,308 @@ describe('Reinvoke — on the fork of the message that started the graph', () =>
291
323
  it('leaves the branch unset when that message is in Main', async () => {
292
324
  await forkHarness(null).Reinvoke(params());
293
325
 
326
+ expect(runAgent).toHaveBeenCalledTimes(1);
294
327
  expect(runAgent.mock.calls[0][0].ConversationBranchID).toBeUndefined();
295
328
  });
329
+
330
+ it('enriches placeholder "output available (...)" task summaries with OutputPayload excerpts from MJ: Tasks', async () => {
331
+ const { RunView } = await import('@memberjunction/core');
332
+ vi.mocked(RunView.FromMetadataProvider).mockReturnValue({
333
+ RunView: vi.fn().mockImplementation(async (rvParams: { EntityName: string }) => {
334
+ if (rvParams.EntityName === 'MJ: Tasks') {
335
+ return {
336
+ Success: true,
337
+ Results: [
338
+ {
339
+ ID: 't1',
340
+ Name: 'Generate Schema Count Query',
341
+ OutputPayload: JSON.stringify({
342
+ title: 'Entity Counts by Schema',
343
+ request: 'SELECT SchemaName, COUNT(*) AS EntityCount FROM __mj.vwEntities GROUP BY SchemaName',
344
+ }),
345
+ },
346
+ ],
347
+ };
348
+ }
349
+ return { Success: false, Results: [] };
350
+ }),
351
+ } as never);
352
+
353
+ await forkHarness(null).Reinvoke(
354
+ params({
355
+ Tasks: [
356
+ { TaskID: 't1', Name: 'Generate Schema Count Query', Status: 'Complete', Summary: 'output available (180 chars)' },
357
+ ],
358
+ }),
359
+ );
360
+
361
+ expect(runAgent).toHaveBeenCalledTimes(1);
362
+ const sentContent = runAgent.mock.calls[0][0].conversationMessages[0].content as string;
363
+ expect(sentContent).toContain('Title: Entity Counts by Schema');
364
+ expect(sentContent).toContain('SELECT SchemaName, COUNT(*) AS EntityCount FROM __mj.vwEntities GROUP BY SchemaName');
365
+ });
366
+
367
+ it('updates the originating message in-place when no intervening messages were sent (IsLatest = true)', async () => {
368
+ const { RunView } = await import('@memberjunction/core');
369
+ vi.mocked(RunView.FromMetadataProvider).mockReturnValue({
370
+ RunView: vi.fn().mockImplementation(async (rvParams: { EntityName: string }) => {
371
+ if (rvParams.EntityName === 'MJ: Conversation Details') {
372
+ return {
373
+ Success: true,
374
+ Results: [{ ID: 'detail-1' }],
375
+ };
376
+ }
377
+ return { Success: false, Results: [] };
378
+ }),
379
+ } as never);
380
+
381
+ const originDetail = {
382
+ ...detailRow(),
383
+ ID: 'detail-1',
384
+ Message: "Started **Weekly digest** — 2 task(s) running. I'll follow up when it finishes.",
385
+ };
386
+ const createdDetails: Array<ReturnType<typeof detailRow>> = [];
387
+ const provider = {
388
+ GetEntityObject: vi.fn().mockImplementation(async (name: string) => {
389
+ if (name === 'MJ: AI Agent Runs') return { AgentID: 'agent-1', Load: vi.fn().mockResolvedValue(true) };
390
+ if (name === 'MJ: AI Agents') return { ID: 'agent-1', Load: vi.fn().mockResolvedValue(true) };
391
+ if (createdDetails.length === 0) {
392
+ createdDetails.push(originDetail);
393
+ return originDetail;
394
+ }
395
+ const next = detailRow();
396
+ createdDetails.push(next);
397
+ return next;
398
+ }),
399
+ };
400
+ const pubSub = { publish: vi.fn().mockResolvedValue(undefined) } as unknown as PubSubEngine;
401
+ const deliverer = new TaskGraphContinuationDeliverer(
402
+ { CreateProvider: vi.fn().mockResolvedValue(provider) } as never,
403
+ { ID: 'user-1' } as UserInfo,
404
+ pubSub,
405
+ );
406
+
407
+ await deliverer.Reinvoke(params());
408
+
409
+ // Only the originating detail was loaded and updated in-place — no second detail row created!
410
+ expect(createdDetails.length).toBe(1);
411
+ expect(originDetail.NewRecord).not.toHaveBeenCalled();
412
+ expect(originDetail.Message).toBe('Synthesized workflow outcome');
413
+ expect(originDetail.Status).toBe('Complete');
414
+ expect(runAgent.mock.calls[0][0].conversationDetailId).toBe('detail-1');
415
+ expect(runAgent.mock.calls[0][0].inputArtifacts).toEqual([]);
416
+ expect(pubSub.publish).toHaveBeenCalledTimes(1);
417
+ });
418
+
419
+ it('creates a bottom continuation reply with ParentID = originDetailId and updates the origin message note when intervening messages exist (IsLatest = false)', async () => {
420
+ const { RunView } = await import('@memberjunction/core');
421
+ vi.mocked(RunView.FromMetadataProvider).mockReturnValue({
422
+ RunView: vi.fn().mockImplementation(async (rvParams: { EntityName: string }) => {
423
+ if (rvParams.EntityName === 'MJ: Conversation Details') {
424
+ return {
425
+ Success: true,
426
+ Results: [{ ID: 'intervening-user-msg-2' }],
427
+ };
428
+ }
429
+ return { Success: false, Results: [] };
430
+ }),
431
+ } as never);
432
+
433
+ const originDetail = {
434
+ ...detailRow(),
435
+ ID: 'detail-1',
436
+ ParentID: null as string | null,
437
+ Message: "Started **Weekly digest** — 2 task(s) running. I'll follow up when it finishes.",
438
+ };
439
+ const replyDetail = {
440
+ ...detailRow(),
441
+ ID: 'bottom-reply-3',
442
+ ParentID: null as string | null,
443
+ };
444
+ let detailCallCount = 0;
445
+ const provider = {
446
+ GetEntityObject: vi.fn().mockImplementation(async (name: string) => {
447
+ if (name === 'MJ: AI Agent Runs') return { AgentID: 'agent-1', Load: vi.fn().mockResolvedValue(true) };
448
+ if (name === 'MJ: AI Agents') return { ID: 'agent-1', Load: vi.fn().mockResolvedValue(true) };
449
+ detailCallCount++;
450
+ return detailCallCount === 1 ? originDetail : replyDetail;
451
+ }),
452
+ };
453
+ const pubSub = { publish: vi.fn().mockResolvedValue(undefined) } as unknown as PubSubEngine;
454
+ const deliverer = new TaskGraphContinuationDeliverer(
455
+ { CreateProvider: vi.fn().mockResolvedValue(provider) } as never,
456
+ { ID: 'user-1' } as UserInfo,
457
+ pubSub,
458
+ );
459
+
460
+ await deliverer.Reinvoke(params());
461
+
462
+ expect(replyDetail.NewRecord).toHaveBeenCalledTimes(1);
463
+ expect(replyDetail.ParentID).toBe('detail-1');
464
+ expect(replyDetail.Message).toBe('Synthesized workflow outcome');
465
+ expect(originDetail.Message).toContain('*(✅ **Weekly digest** completed — synthesized results posted below.)*');
466
+ expect(originDetail.Message).not.toContain("I'll follow up when it finishes");
467
+ expect(pubSub.publish).toHaveBeenCalledTimes(2);
468
+ });
469
+
470
+ it('strips inherited upstream payload keys across DAG steps and extracts nested synthesis and report fields', async () => {
471
+ const { RunView } = await import('@memberjunction/core');
472
+ const step1Payload = {
473
+ queryName: 'Annual Order Counts',
474
+ sql: 'SELECT YEAR(OrderDate) AS OrderYear, COUNT(*) AS OrderCount FROM Orders GROUP BY YEAR(OrderDate)',
475
+ explanation: 'Counts orders grouped by year.',
476
+ };
477
+ const step2Payload = {
478
+ ...step1Payload,
479
+ synthesis: {
480
+ executiveSummary: 'Order volume grew 22% YoY between 2022 and 2025.',
481
+ keyFindings: ['2024 peak growth', 'Steady enterprise retention'],
482
+ },
483
+ };
484
+ const step3Payload = {
485
+ ...step2Payload,
486
+ report: {
487
+ name: 'Annual Order Trend Executive Report',
488
+ description: 'Board-ready HTML report covering annual order volume and YoY trajectory.',
489
+ html: '<h1>Annual Order Trend</h1><p>Executive overview of multi-year order growth.</p>',
490
+ },
491
+ };
492
+
493
+ vi.mocked(RunView.FromMetadataProvider).mockReturnValue({
494
+ RunView: vi.fn().mockImplementation(async (rvParams: { EntityName: string }) => {
495
+ if (rvParams.EntityName === 'MJ: Tasks') {
496
+ return {
497
+ Success: true,
498
+ Results: [
499
+ { ID: 't1', Name: 'Step 1: Query', OutputPayload: JSON.stringify(step1Payload) },
500
+ { ID: 't2', Name: 'Step 2: Strategy', OutputPayload: JSON.stringify(step2Payload) },
501
+ { ID: 't3', Name: 'Step 3: Report', OutputPayload: JSON.stringify(step3Payload) },
502
+ ],
503
+ };
504
+ }
505
+ return { Success: false, Results: [] };
506
+ }),
507
+ } as never);
508
+
509
+ await forkHarness(null).Reinvoke(
510
+ params({
511
+ Tasks: [
512
+ { TaskID: 't1', Name: 'Step 1: Query', Status: 'Complete', Summary: 'output available (180 chars)' },
513
+ { TaskID: 't2', Name: 'Step 2: Strategy', Status: 'Complete', Summary: 'output available (320 chars)' },
514
+ { TaskID: 't3', Name: 'Step 3: Report', Status: 'Complete', Summary: 'output available (540 chars)' },
515
+ ],
516
+ }),
517
+ );
518
+
519
+ const sentContent = runAgent.mock.calls[0][0].conversationMessages[0].content as string;
520
+ expect(sentContent).toContain('Title: Annual Order Counts');
521
+ expect(sentContent).toContain('Executive Summary: Order volume grew 22% YoY between 2022 and 2025.');
522
+ expect(sentContent).toContain('Report Title: Annual Order Trend Executive Report');
523
+ expect(sentContent).toContain('Report Summary: Board-ready HTML report covering annual order volume and YoY trajectory.');
524
+ // Step 1's SQL should only appear once (under Step 1), not duplicated under Step 2 and Step 3!
525
+ const sqlOccurrences = sentContent.split('SELECT YEAR(OrderDate) AS OrderYear').length - 1;
526
+ expect(sqlOccurrences).toBe(1);
527
+ });
528
+
529
+ it('walks up ParentRunID so a sub-agent (Workflow Planner) submission reinvokes the root conversation agent (Sage) as the conversation owner', async () => {
530
+ const subAgentRun = {
531
+ ID: 'subagent-planner-run',
532
+ AgentID: 'workflow-planner-agent-id',
533
+ ParentRunID: 'root-sage-run',
534
+ UserID: null as string | null,
535
+ Load: vi.fn().mockResolvedValue(true),
536
+ };
537
+ const rootSageRun = {
538
+ ID: 'root-sage-run',
539
+ AgentID: 'sage-agent-id',
540
+ ParentRunID: null as string | null,
541
+ UserID: 'owner-user-99',
542
+ Load: vi.fn().mockResolvedValue(true),
543
+ };
544
+ const sageAgent = {
545
+ ID: 'sage-agent-id',
546
+ Name: 'Sage',
547
+ Load: vi.fn().mockResolvedValue(true),
548
+ };
549
+ const detail = detailRow();
550
+ let runCallCount = 0;
551
+ const detailUsers: Array<UserInfo | undefined> = [];
552
+
553
+ const provider = {
554
+ GetEntityObject: vi.fn().mockImplementation(async (name: string, user?: UserInfo) => {
555
+ if (name === 'MJ: AI Agent Runs') {
556
+ runCallCount++;
557
+ return runCallCount === 1 ? subAgentRun : rootSageRun;
558
+ }
559
+ if (name === 'MJ: AI Agents') {
560
+ return sageAgent;
561
+ }
562
+ detailUsers.push(user);
563
+ return detail;
564
+ }),
565
+ };
566
+
567
+ const deliverer = new TaskGraphContinuationDeliverer(
568
+ { CreateProvider: vi.fn().mockResolvedValue(provider) } as never,
569
+ { ID: 'system-user' } as UserInfo,
570
+ );
571
+
572
+ await deliverer.Reinvoke(params({ SubmittedByAgentRunID: 'subagent-planner-run' }));
573
+
574
+ expect(subAgentRun.Load).toHaveBeenCalledWith('subagent-planner-run');
575
+ expect(rootSageRun.Load).toHaveBeenCalledWith('root-sage-run');
576
+ expect(sageAgent.Load).toHaveBeenCalledWith('sage-agent-id');
577
+ expect(runAgent).toHaveBeenCalledTimes(1);
578
+ expect(runAgent.mock.calls[0][0].agent.ID).toBe('sage-agent-id');
579
+ expect(runAgent.mock.calls[0][0].contextUser.ID).toBe('owner-user-99');
580
+ expect(detailUsers[0]?.ID).toBe('owner-user-99');
581
+ });
582
+
583
+ it('enriches Skipped steps with their unmet dependency Condition from MJ: Task Dependencies', async () => {
584
+ const { RunView } = await import('@memberjunction/core');
585
+ vi.mocked(RunView.FromMetadataProvider).mockReturnValue({
586
+ RunView: vi.fn().mockImplementation(async (rvParams: { EntityName: string }) => {
587
+ if (rvParams.EntityName === 'MJ: Tasks') {
588
+ return {
589
+ Success: true,
590
+ Results: [
591
+ { ID: 't1', Name: 'Build Query', Status: 'Complete', OutputPayload: JSON.stringify({ queryName: 'Users by Role' }) },
592
+ { ID: 't2', Name: 'Run Query', Status: 'Complete', OutputPayload: JSON.stringify({ summary: 'Built query blueprint without live execution' }) },
593
+ { ID: 't3', Name: 'Analyze Results', Status: 'Skipped', OutputPayload: null },
594
+ ],
595
+ };
596
+ }
597
+ if (rvParams.EntityName === 'MJ: Task Dependencies') {
598
+ return {
599
+ Success: true,
600
+ Results: [
601
+ {
602
+ TaskID: 't3',
603
+ DependsOnTaskID: 't2',
604
+ Condition: 'output.rowCount > 0',
605
+ },
606
+ ],
607
+ };
608
+ }
609
+ return { Success: false, Results: [] };
610
+ }),
611
+ } as never);
612
+
613
+ await forkHarness(null).Reinvoke(
614
+ params({
615
+ Tasks: [
616
+ { TaskID: 't1', Name: 'Build Query', Status: 'Complete' },
617
+ { TaskID: 't2', Name: 'Run Query', Status: 'Complete' },
618
+ { TaskID: 't3', Name: 'Analyze Results', Status: 'Skipped' },
619
+ ],
620
+ Summary: '2 of 3 steps completed (1 skipped).',
621
+ }),
622
+ );
623
+
624
+ const sentContent = runAgent.mock.calls[0][0].conversationMessages[0].content as string;
625
+ expect(sentContent).toContain('- ⏭️ **Analyze Results** — Skipped: branch condition `output.rowCount > 0` on "Run Query" evaluated to false');
626
+ expect(sentContent).toContain('briefly explaining any conditional steps that were skipped');
627
+ });
296
628
  });
629
+
630
+
@@ -133,4 +133,26 @@ describe('TaskGraphFrameBroadcaster', () => {
133
133
  expect(p.CompletedCount).toBe(7);
134
134
  expect(p.TotalCount).toBe(7);
135
135
  });
136
+
137
+ it('bridges frames onto PUSH_STATUS_UPDATES_TOPIC when bridgeToPushStatus is enabled', () => {
138
+ new TaskGraphFrameBroadcaster(pubSub, true).OnFrame(
139
+ frame({ Kind: 'TaskCompleted', Status: 'Complete', CompletedCount: 1, TotalCount: 2 }),
140
+ );
141
+ expect(published).toHaveLength(2);
142
+ expect(published[0][0]).toBe(TASK_GRAPH_FRAMES_TOPIC);
143
+ expect(published[1][0]).toBe('PUSH_STATUS_UPDATES');
144
+ const pushPayload = published[1][1] as { sessionId: string; ownerUserId: string; message: string };
145
+ expect(pushPayload.sessionId).toBe('*');
146
+ expect(pushPayload.ownerUserId).toBe(OWNER);
147
+ const parsed = JSON.parse(pushPayload.message) as {
148
+ type: string;
149
+ status: string;
150
+ data: { Status: string; TaskID: string };
151
+ };
152
+ expect(parsed.type).toBe('TaskGraphFrame');
153
+ expect(parsed.status).toBe('ok');
154
+ expect(parsed.data.Status).toBe('Complete');
155
+ expect(parsed.data.TaskID).toBe('task-1');
156
+ });
136
157
  });
158
+