@memberjunction/server 5.43.0 → 5.45.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 (309) hide show
  1. package/dist/agentSessions/ReturningVisitorRecap.d.ts +39 -0
  2. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -0
  3. package/dist/agentSessions/ReturningVisitorRecap.js +198 -0
  4. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -0
  5. package/dist/agentSessions/SessionJanitor.d.ts +13 -0
  6. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  7. package/dist/agentSessions/SessionJanitor.js +65 -7
  8. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  9. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  10. package/dist/agentSessions/SessionManager.js +40 -0
  11. package/dist/agentSessions/SessionManager.js.map +1 -1
  12. package/dist/agentSessions/index.d.ts.map +1 -1
  13. package/dist/agentSessions/index.js +3 -1
  14. package/dist/agentSessions/index.js.map +1 -1
  15. package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
  16. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  17. package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
  18. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  19. package/dist/auth/magicLink/index.d.ts +1 -1
  20. package/dist/auth/magicLink/index.d.ts.map +1 -1
  21. package/dist/auth/magicLink/index.js +1 -1
  22. package/dist/auth/magicLink/index.js.map +1 -1
  23. package/dist/auth/magicLink/types.d.ts +26 -0
  24. package/dist/auth/magicLink/types.d.ts.map +1 -1
  25. package/dist/config.d.ts +1883 -144
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +160 -36
  28. package/dist/config.js.map +1 -1
  29. package/dist/context.d.ts.map +1 -1
  30. package/dist/context.js +45 -2
  31. package/dist/context.js.map +1 -1
  32. package/dist/generated/generated.d.ts +1684 -54
  33. package/dist/generated/generated.d.ts.map +1 -1
  34. package/dist/generated/generated.js +57105 -48029
  35. package/dist/generated/generated.js.map +1 -1
  36. package/dist/generic/ResolverBase.d.ts +13 -0
  37. package/dist/generic/ResolverBase.d.ts.map +1 -1
  38. package/dist/generic/ResolverBase.js +22 -0
  39. package/dist/generic/ResolverBase.js.map +1 -1
  40. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  41. package/dist/generic/RunViewResolver.js +4 -0
  42. package/dist/generic/RunViewResolver.js.map +1 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +110 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  48. package/dist/integration/CustomColumnPromoter.js +6 -0
  49. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  50. package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
  51. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
  52. package/dist/realtimeWidget/WidgetRouter.js +182 -0
  53. package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
  54. package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
  55. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
  56. package/dist/realtimeWidget/WidgetSessionService.js +477 -0
  57. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
  58. package/dist/realtimeWidget/host-identity.d.ts +40 -0
  59. package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
  60. package/dist/realtimeWidget/host-identity.js +58 -0
  61. package/dist/realtimeWidget/host-identity.js.map +1 -0
  62. package/dist/realtimeWidget/index.d.ts +10 -0
  63. package/dist/realtimeWidget/index.d.ts.map +1 -0
  64. package/dist/realtimeWidget/index.js +9 -0
  65. package/dist/realtimeWidget/index.js.map +1 -0
  66. package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
  67. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
  68. package/dist/realtimeWidget/visitorIdentity.js +202 -0
  69. package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
  70. package/dist/realtimeWidget/widgetCore.d.ts +106 -0
  71. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
  72. package/dist/realtimeWidget/widgetCore.js +173 -0
  73. package/dist/realtimeWidget/widgetCore.js.map +1 -0
  74. package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
  75. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
  76. package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
  77. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
  78. package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
  79. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  80. package/dist/resolvers/ComponentRegistryResolver.js +31 -8
  81. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  82. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
  83. package/dist/resolvers/EntityPermissionResolver.js +1 -2
  84. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  85. package/dist/resolvers/FileResolver.d.ts +74 -0
  86. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  87. package/dist/resolvers/FileResolver.js +211 -2
  88. package/dist/resolvers/FileResolver.js.map +1 -1
  89. package/dist/resolvers/QueryResolver.d.ts +6 -4
  90. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  91. package/dist/resolvers/QueryResolver.js +29 -14
  92. package/dist/resolvers/QueryResolver.js.map +1 -1
  93. package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
  94. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  95. package/dist/resolvers/QuerySystemUserResolver.js +15 -13
  96. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  97. package/dist/resolvers/RealtimeBridgeResolver.d.ts +8 -1
  98. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  99. package/dist/resolvers/RealtimeBridgeResolver.js +28 -4
  100. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  101. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +91 -2
  102. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  103. package/dist/resolvers/RealtimeClientSessionResolver.js +337 -17
  104. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  105. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  106. package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
  107. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  108. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
  109. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
  110. package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
  111. package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
  112. package/dist/resolvers/RunAIAgentResolver.d.ts +12 -3
  113. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  114. package/dist/resolvers/RunAIAgentResolver.js +50 -17
  115. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  116. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  117. package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
  118. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  119. package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
  120. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
  121. package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
  122. package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
  123. package/dist/resolvers/TelephonyResolver.d.ts +26 -0
  124. package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
  125. package/dist/resolvers/TelephonyResolver.js +86 -0
  126. package/dist/resolvers/TelephonyResolver.js.map +1 -0
  127. package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
  128. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  129. package/dist/resolvers/TestQuerySQLResolver.js +2 -3
  130. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  131. package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
  132. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
  133. package/dist/resolvers/VonageTelephonyResolver.js +86 -0
  134. package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
  135. package/dist/resolvers/meetingRecordingRegistration.d.ts +124 -0
  136. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
  137. package/dist/resolvers/meetingRecordingRegistration.js +311 -0
  138. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
  139. package/dist/resolvers/peaksSidecar.d.ts +30 -0
  140. package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
  141. package/dist/resolvers/peaksSidecar.js +51 -0
  142. package/dist/resolvers/peaksSidecar.js.map +1 -0
  143. package/dist/rest/MediaAccessKeys.d.ts +68 -0
  144. package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
  145. package/dist/rest/MediaAccessKeys.js +96 -0
  146. package/dist/rest/MediaAccessKeys.js.map +1 -0
  147. package/dist/rest/MediaStreamHandler.d.ts +26 -0
  148. package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
  149. package/dist/rest/MediaStreamHandler.js +195 -0
  150. package/dist/rest/MediaStreamHandler.js.map +1 -0
  151. package/dist/rest/mediaRange.d.ts +41 -0
  152. package/dist/rest/mediaRange.d.ts.map +1 -0
  153. package/dist/rest/mediaRange.js +60 -0
  154. package/dist/rest/mediaRange.js.map +1 -0
  155. package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
  156. package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
  157. package/dist/telephony/RingCentralTelephonyService.js +262 -0
  158. package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
  159. package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
  160. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
  161. package/dist/telephony/TeamsMeetingsRouter.js +96 -0
  162. package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
  163. package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
  164. package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
  165. package/dist/telephony/TeamsMeetingsService.js +196 -0
  166. package/dist/telephony/TeamsMeetingsService.js.map +1 -0
  167. package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
  168. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
  169. package/dist/telephony/TwilioTelephonyRouter.js +143 -0
  170. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
  171. package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
  172. package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
  173. package/dist/telephony/TwilioTelephonyService.js +193 -0
  174. package/dist/telephony/TwilioTelephonyService.js.map +1 -0
  175. package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
  176. package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
  177. package/dist/telephony/VonageTelephonyRouter.js +201 -0
  178. package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
  179. package/dist/telephony/VonageTelephonyService.d.ts +90 -0
  180. package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
  181. package/dist/telephony/VonageTelephonyService.js +191 -0
  182. package/dist/telephony/VonageTelephonyService.js.map +1 -0
  183. package/dist/telephony/calendar-scheduler.d.ts +67 -0
  184. package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
  185. package/dist/telephony/calendar-scheduler.js +133 -0
  186. package/dist/telephony/calendar-scheduler.js.map +1 -0
  187. package/dist/telephony/index.d.ts +29 -0
  188. package/dist/telephony/index.d.ts.map +1 -0
  189. package/dist/telephony/index.js +38 -0
  190. package/dist/telephony/index.js.map +1 -0
  191. package/dist/telephony/media-upgrade-router.d.ts +33 -0
  192. package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
  193. package/dist/telephony/media-upgrade-router.js +56 -0
  194. package/dist/telephony/media-upgrade-router.js.map +1 -0
  195. package/dist/telephony/ringcentral-runtime.d.ts +14 -0
  196. package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
  197. package/dist/telephony/ringcentral-runtime.js +18 -0
  198. package/dist/telephony/ringcentral-runtime.js.map +1 -0
  199. package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
  200. package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
  201. package/dist/telephony/teams-meetings-runtime.js +20 -0
  202. package/dist/telephony/teams-meetings-runtime.js.map +1 -0
  203. package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
  204. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
  205. package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
  206. package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
  207. package/dist/telephony/telephony-runtime.d.ts +14 -0
  208. package/dist/telephony/telephony-runtime.d.ts.map +1 -0
  209. package/dist/telephony/telephony-runtime.js +18 -0
  210. package/dist/telephony/telephony-runtime.js.map +1 -0
  211. package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
  212. package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
  213. package/dist/telephony/twilioMediaRegistry.js +106 -0
  214. package/dist/telephony/twilioMediaRegistry.js.map +1 -0
  215. package/dist/telephony/vonage-runtime.d.ts +14 -0
  216. package/dist/telephony/vonage-runtime.d.ts.map +1 -0
  217. package/dist/telephony/vonage-runtime.js +18 -0
  218. package/dist/telephony/vonage-runtime.js.map +1 -0
  219. package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
  220. package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
  221. package/dist/telephony/vonageMediaRegistry.js +158 -0
  222. package/dist/telephony/vonageMediaRegistry.js.map +1 -0
  223. package/package.json +88 -82
  224. package/src/__tests__/FileResolverPeaks.test.ts +64 -0
  225. package/src/__tests__/MediaAccessKeys.test.ts +68 -0
  226. package/src/__tests__/MediaStreamHandler.test.ts +91 -0
  227. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
  228. package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
  229. package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
  230. package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
  231. package/src/__tests__/SessionManager.test.ts +2 -0
  232. package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
  233. package/src/__tests__/search-knowledge-tags.test.ts +9 -9
  234. package/src/__tests__/teams-meetings-service.test.ts +65 -0
  235. package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
  236. package/src/__tests__/twilio-telephony-service.test.ts +23 -0
  237. package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
  238. package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
  239. package/src/__tests__/widget.test.ts +204 -0
  240. package/src/__tests__/widgetGuestElevation.test.ts +57 -0
  241. package/src/__tests__/widgetHostIdentity.test.ts +117 -0
  242. package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
  243. package/src/agentSessions/SessionJanitor.ts +66 -6
  244. package/src/agentSessions/SessionManager.ts +41 -0
  245. package/src/agentSessions/index.ts +3 -1
  246. package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
  247. package/src/auth/magicLink/index.ts +1 -0
  248. package/src/auth/magicLink/types.ts +26 -0
  249. package/src/config.ts +172 -38
  250. package/src/context.ts +50 -3
  251. package/src/generated/generated.ts +26726 -20464
  252. package/src/generic/ResolverBase.ts +28 -0
  253. package/src/generic/RunViewResolver.ts +4 -0
  254. package/src/index.ts +119 -3
  255. package/src/integration/CustomColumnPromoter.ts +6 -0
  256. package/src/realtimeWidget/WidgetRouter.ts +204 -0
  257. package/src/realtimeWidget/WidgetSessionService.ts +714 -0
  258. package/src/realtimeWidget/host-identity.ts +84 -0
  259. package/src/realtimeWidget/index.ts +15 -0
  260. package/src/realtimeWidget/visitorIdentity.ts +258 -0
  261. package/src/realtimeWidget/widgetCore.ts +231 -0
  262. package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
  263. package/src/resolvers/ComponentRegistryResolver.ts +36 -10
  264. package/src/resolvers/EntityPermissionResolver.ts +1 -2
  265. package/src/resolvers/FileResolver.ts +199 -1
  266. package/src/resolvers/QueryResolver.ts +33 -19
  267. package/src/resolvers/QuerySystemUserResolver.ts +14 -10
  268. package/src/resolvers/RealtimeBridgeResolver.ts +36 -4
  269. package/src/resolvers/RealtimeClientSessionResolver.ts +383 -9
  270. package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
  271. package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
  272. package/src/resolvers/RunAIAgentResolver.ts +60 -15
  273. package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
  274. package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
  275. package/src/resolvers/TelephonyResolver.ts +63 -0
  276. package/src/resolvers/TestQuerySQLResolver.ts +2 -3
  277. package/src/resolvers/VonageTelephonyResolver.ts +63 -0
  278. package/src/resolvers/meetingRecordingRegistration.ts +432 -0
  279. package/src/resolvers/peaksSidecar.ts +52 -0
  280. package/src/rest/MediaAccessKeys.ts +121 -0
  281. package/src/rest/MediaStreamHandler.ts +223 -0
  282. package/src/rest/mediaRange.ts +76 -0
  283. package/src/telephony/RingCentralTelephonyService.ts +342 -0
  284. package/src/telephony/TeamsMeetingsRouter.ts +124 -0
  285. package/src/telephony/TeamsMeetingsService.ts +268 -0
  286. package/src/telephony/TwilioTelephonyRouter.ts +184 -0
  287. package/src/telephony/TwilioTelephonyService.ts +261 -0
  288. package/src/telephony/VonageTelephonyRouter.ts +239 -0
  289. package/src/telephony/VonageTelephonyService.ts +259 -0
  290. package/src/telephony/calendar-scheduler.ts +176 -0
  291. package/src/telephony/index.ts +43 -0
  292. package/src/telephony/media-upgrade-router.ts +62 -0
  293. package/src/telephony/ringcentral-runtime.ts +22 -0
  294. package/src/telephony/teams-meetings-runtime.ts +24 -0
  295. package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
  296. package/src/telephony/telephony-runtime.ts +22 -0
  297. package/src/telephony/twilioMediaRegistry.ts +137 -0
  298. package/src/telephony/vonage-runtime.ts +22 -0
  299. package/src/telephony/vonageMediaRegistry.ts +200 -0
  300. package/dist/agents/skip-agent.d.ts +0 -111
  301. package/dist/agents/skip-agent.d.ts.map +0 -1
  302. package/dist/agents/skip-agent.js +0 -1487
  303. package/dist/agents/skip-agent.js.map +0 -1
  304. package/dist/agents/skip-sdk.d.ts +0 -261
  305. package/dist/agents/skip-sdk.d.ts.map +0 -1
  306. package/dist/agents/skip-sdk.js +0 -909
  307. package/dist/agents/skip-sdk.js.map +0 -1
  308. package/src/agents/skip-agent.ts +0 -1594
  309. package/src/agents/skip-sdk.ts +0 -1210
@@ -2603,3 +2603,72 @@ describe('RealtimeClientSessionResolver.StartRealtimeClientSession — runtime-o
2603
2603
  expect(result.EffectiveConfigJson).toBeUndefined();
2604
2604
  });
2605
2605
  });
2606
+
2607
+ describe('RealtimeClientSessionResolver — app awareness (applicationId / appContext / allowedAgents)', () => {
2608
+ it('forwards applicationId + parsed appContext to PrepareClientSession and persists the allowed union', async () => {
2609
+ hasPermissionMock.mockResolvedValue(true);
2610
+ currentProvider = makeProvider(() => makeSessionEntity());
2611
+ const createdSession = makeSessionEntity({ ID: 'session-app', ConversationID: 'conv-1' });
2612
+ createSessionMock.mockResolvedValue(createdSession);
2613
+ prepareClientSessionMock.mockResolvedValue({
2614
+ Success: true,
2615
+ ClientConfig: { Provider: 'openai', Model: 'm', EphemeralToken: 'ek', ExpiresAt: '2099-01-01T00:00:00Z', SessionConfig: {} },
2616
+ // Effective config carries the union-accumulated allowed agents (from the app's RelevantAgents).
2617
+ EffectiveConfig: { realtime: { allowedAgents: [{ agentId: 'skip-1', label: 'Skip' }] } },
2618
+ });
2619
+ const resolver = makeResolver();
2620
+ const appContext = { App: { Name: 'Data Explorer', Description: '' }, ActiveNavItem: { Name: 'Queries' }, OtherNavItems: [], User: { Name: 'A', Roles: [] } };
2621
+
2622
+ await resolver.StartRealtimeClientSession(
2623
+ 'target-1', makeCtx(),
2624
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
2625
+ 'app-9', JSON.stringify(appContext),
2626
+ );
2627
+
2628
+ // App context reached the prepare service.
2629
+ const prepArg = prepareClientSessionMock.mock.calls[0][0] as { ApplicationID?: string; AppContext?: { App?: { Name?: string } } };
2630
+ expect(prepArg.ApplicationID).toBe('app-9');
2631
+ expect(prepArg.AppContext?.App?.Name).toBe('Data Explorer');
2632
+
2633
+ // The allowed union + applicationID were persisted onto the session config for relayed-call validation.
2634
+ const persisted = JSON.parse(createdSession.Config_ as string) as { applicationID?: string; allowedAgents?: Array<{ agentId: string }> };
2635
+ expect(persisted.applicationID).toBe('app-9');
2636
+ expect(persisted.allowedAgents?.map(a => a.agentId)).toEqual(['skip-1']);
2637
+ });
2638
+
2639
+ it('tolerates a malformed appContextJson (no app context, never fails the start)', async () => {
2640
+ hasPermissionMock.mockResolvedValue(true);
2641
+ currentProvider = makeProvider(() => makeSessionEntity());
2642
+ createSessionMock.mockResolvedValue(makeSessionEntity({ ID: 'session-bad' }));
2643
+ prepareClientSessionMock.mockResolvedValue({
2644
+ Success: true, ClientConfig: { Provider: 'o', Model: 'm', EphemeralToken: 'ek', ExpiresAt: '2099-01-01T00:00:00Z', SessionConfig: {} },
2645
+ });
2646
+ const resolver = makeResolver();
2647
+
2648
+ await resolver.StartRealtimeClientSession(
2649
+ 'target-1', makeCtx(),
2650
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
2651
+ 'app-9', 'not-json',
2652
+ );
2653
+ const prepArg = prepareClientSessionMock.mock.calls[0][0] as { ApplicationID?: string; AppContext?: unknown };
2654
+ expect(prepArg.ApplicationID).toBe('app-9');
2655
+ expect(prepArg.AppContext).toBeUndefined();
2656
+ });
2657
+
2658
+ it('passes the persisted allowedAgents from the session config into ExecuteRelayedTool', async () => {
2659
+ currentProvider = makeProvider(() =>
2660
+ makeSessionEntity({
2661
+ Config_: JSON.stringify({ targetAgentID: 'lead-1', allowedAgents: [{ agentId: 'skip-1', label: 'Skip' }] }),
2662
+ }),
2663
+ );
2664
+ executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
2665
+ const resolver = makeResolver();
2666
+
2667
+ await resolver.ExecuteRealtimeSessionTool(
2668
+ 'session-1', 'call-1', 'invoke-target-agent', '{"agent":"Skip","request":"x"}', makeCtx(), makePubSub(),
2669
+ );
2670
+
2671
+ const relayArg = executeRelayedToolMock.mock.calls[0][0] as { AllowedAgents?: Array<{ agentId: string }> };
2672
+ expect(relayArg.AllowedAgents?.map(a => a.agentId)).toEqual(['skip-1']);
2673
+ });
2674
+ });
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @fileoverview HEADLESS INTEGRATION test — the Realtime Co-Agent "app awareness" stack composed
3
+ * end-to-end across packages: the app config cascade (`@memberjunction/ai-agents`
4
+ * realtime-coagent-config), the `AppContextSnapshot` mint-time injection
5
+ * (`@memberjunction/ai-core-plus`), the colleagues framing, and the multi-target delegation routing
6
+ * — all driven through the REAL {@link RealtimeClientSessionService} (only DB/SDK seams stubbed).
7
+ *
8
+ * This is the integration counterpart to the per-package unit tests: it proves the Move 1/3/4 layers
9
+ * actually compose (an app's AgentSettings → cascade → effective config → system prompt + allowed
10
+ * union → delegation), with no provider socket and no DB. Voice itself is not exercised (that is the
11
+ * live-verification step), but everything up to "what the model is told and which agent a delegation
12
+ * runs" is.
13
+ */
14
+ import { describe, it, expect, vi } from 'vitest';
15
+ import {
16
+ BaseRealtimeModel,
17
+ ClientRealtimeSessionConfig,
18
+ IRealtimeSession,
19
+ RealtimeSessionParams,
20
+ } from '@memberjunction/ai';
21
+ import { UserInfo, IMetadataProvider } from '@memberjunction/core';
22
+ import { MJAIAgentEntityExtended, AppContextSnapshot, ExecuteAgentResult } from '@memberjunction/ai-core-plus';
23
+ import {
24
+ RealtimeClientSessionService,
25
+ PrepareClientSessionInput,
26
+ ExecuteRelayedToolInput,
27
+ RealtimeModelResolution,
28
+ CoAgentSystemPromptResolution,
29
+ } from '@memberjunction/ai-agents';
30
+ import {
31
+ BuildAppRealtimeOverridesJson,
32
+ INVOKE_TARGET_AGENT_TOOL_NAME,
33
+ DelegateToTargetRequest,
34
+ } from '@memberjunction/ai-agents';
35
+
36
+ class StubSession implements IRealtimeSession {
37
+ SendInput(): void {}
38
+ async RegisterTools(): Promise<void> {}
39
+ OnOutput(): void {}
40
+ OnTranscript(): void {}
41
+ OnToolCall(): void {}
42
+ async SendToolResult(): Promise<void> {}
43
+ OnInterruption(): void {}
44
+ OnUsage(): void {}
45
+ OnError(): void {}
46
+ async Close(): Promise<void> {}
47
+ }
48
+
49
+ class StubModel extends BaseRealtimeModel {
50
+ constructor() { super('k'); }
51
+ public override get SupportsClientDirect(): boolean { return true; }
52
+ async StartSession(): Promise<IRealtimeSession> { return new StubSession(); }
53
+ public override async CreateClientSession(params: RealtimeSessionParams): Promise<ClientRealtimeSessionConfig> {
54
+ return {
55
+ Provider: 'stub', Model: params.Model, EphemeralToken: 't', ExpiresAt: '2099-01-01T00:00:00Z',
56
+ SessionConfig: { instructions: params.SystemPrompt },
57
+ };
58
+ }
59
+ }
60
+
61
+ /** Real service with only the DB/SDK seams stubbed; cascade + prompt + delegation routing stay REAL. */
62
+ class IntegrationService extends RealtimeClientSessionService {
63
+ private model = new StubModel();
64
+ public AppOverridesJson: string | null = null;
65
+ /** Captures which agent a delegation actually ran (the routing outcome under test). */
66
+ public RanAgent: MJAIAgentEntityExtended | null = null;
67
+ public TargetNames: Record<string, string> = { 'lead-1': 'Sage' };
68
+
69
+ protected override async configureEngine(): Promise<void> {}
70
+ protected override async resolveRealtimeModel(): Promise<RealtimeModelResolution | null> {
71
+ return { Model: this.model, ModelID: 'm', VendorID: 'v', APIName: 'stub-realtime' };
72
+ }
73
+ protected override getCoAgentSystemPromptText(): string { return 'CO-AGENT BODY'; }
74
+ protected override resolveCoAgentSystemPrompt(): CoAgentSystemPromptResolution {
75
+ return { Text: 'CO-AGENT BODY', PromptID: 'p1' };
76
+ }
77
+ protected override async createCoAgentObservabilityRun(): Promise<{ CoAgentRunID: string } | null> {
78
+ return { CoAgentRunID: 'run-1' };
79
+ }
80
+ protected override resolveTargetAgent(id: string): MJAIAgentEntityExtended | null {
81
+ if (!id) return null;
82
+ const name = this.TargetNames[id] ?? id;
83
+ return { ID: id, Name: name, Description: `${name} agent.` } as unknown as MJAIAgentEntityExtended;
84
+ }
85
+ protected override async resolveAppRealtimeOverrides(): Promise<string | null> { return this.AppOverridesJson; }
86
+ // Capture the routed target without running a real agent.
87
+ protected override async runDelegatedAgent(
88
+ _input: ExecuteRelayedToolInput,
89
+ _request: DelegateToTargetRequest,
90
+ target: MJAIAgentEntityExtended,
91
+ ): Promise<ExecuteAgentResult> {
92
+ this.RanAgent = target;
93
+ return { success: true, payload: null, agentRun: { Status: 'Completed' } } as unknown as ExecuteAgentResult;
94
+ }
95
+ protected override async createDelegatedRunArtifacts(): Promise<undefined> { return undefined; }
96
+ }
97
+
98
+ const user = { ID: 'u1' } as unknown as UserInfo;
99
+ const provider = {} as unknown as IMetadataProvider;
100
+
101
+ function coAgent(): MJAIAgentEntityExtended {
102
+ return { ID: 'co-1', Name: 'Realtime Co-Agent', InjectNotes: false, InjectExamples: false } as unknown as MJAIAgentEntityExtended;
103
+ }
104
+ function input(over: Partial<PrepareClientSessionInput> = {}): PrepareClientSessionInput {
105
+ return { CoAgent: coAgent(), TargetAgentID: 'lead-1', AgentSessionID: 's1', ConversationMessages: [], ...over };
106
+ }
107
+
108
+ describe('Realtime Co-Agent app awareness — integration', () => {
109
+ it('composes app AgentSettings → cascade → prompt (app-context + colleagues) + allowed union', async () => {
110
+ const svc = new IntegrationService();
111
+ svc.TargetNames = { 'lead-1': 'Sage', 'skip-1': 'Skip' };
112
+ svc.AppOverridesJson = BuildAppRealtimeOverridesJson(
113
+ { Disclosure: 'mention', Persona: { Tone: 'warm', SpeakingStyle: 'concise' } },
114
+ [{ agentId: 'skip-1', label: 'Skip', disclosure: 'silent' }],
115
+ );
116
+ const appContext: AppContextSnapshot = {
117
+ App: { Name: 'Knowledge Hub', Description: '' },
118
+ ActiveNavItem: { Name: 'Analytics' },
119
+ OtherNavItems: [],
120
+ User: { Name: 'Amith', Roles: [] },
121
+ View: { FreeText: 'looking at the cluster chart' },
122
+ Capabilities: { Agents: [{ AgentID: 'skip-1', Name: 'Skip' }] },
123
+ };
124
+
125
+ const result = await svc.PrepareClientSession(input({ AppContext: appContext }), user, provider);
126
+ expect(result.Success).toBe(true);
127
+
128
+ const prompt = result.SessionParams!.SystemPrompt;
129
+ // Identity (lead) + colleague (from app RelevantAgents) + per-target disclosure guidance.
130
+ expect(prompt).toContain('voice for the agent "Sage"');
131
+ expect(prompt).toContain('Your colleagues:');
132
+ expect(prompt).toContain('"Skip"');
133
+ expect(prompt).toContain('speak their result as your own'); // Skip's silent disclosure
134
+ // Persona (app override) folded in.
135
+ expect(prompt).toContain('Voice & manner');
136
+ // App-context snapshot injected at mint.
137
+ expect(prompt).toContain('CURRENT APP CONTEXT');
138
+ expect(prompt).toContain('Knowledge Hub');
139
+
140
+ // Effective config carries the union-accumulated allowed agents (surfaced to the resolver to persist).
141
+ expect(result.EffectiveConfig?.realtime?.allowedAgents?.map(a => a.agentId)).toContain('skip-1');
142
+ expect(result.EffectiveConfig?.realtime?.disclosure).toBe('mention');
143
+ });
144
+
145
+ it('routes a relayed invoke-target-agent call to a named colleague in the allowed union', async () => {
146
+ const svc = new IntegrationService();
147
+ svc.TargetNames = { 'lead-1': 'Sage', 'skip-1': 'Skip' };
148
+ const relay: ExecuteRelayedToolInput = {
149
+ AgentSessionID: 's1',
150
+ TargetAgentID: 'lead-1',
151
+ AllowedAgents: [{ agentId: 'skip-1', label: 'Skip' }],
152
+ Call: {
153
+ CallID: 'c1',
154
+ ToolName: INVOKE_TARGET_AGENT_TOOL_NAME,
155
+ Arguments: JSON.stringify({ agent: 'Skip', request: 'analyze this' }),
156
+ },
157
+ };
158
+ const out = await svc.ExecuteRelayedTool(relay, user, provider);
159
+ expect(JSON.parse(out.ResultJson).success).toBe(true);
160
+ expect(svc.RanAgent?.ID).toBe('skip-1'); // routed to the named colleague, not the lead
161
+ });
162
+
163
+ it('routes to the lead when no colleague is named', async () => {
164
+ const svc = new IntegrationService();
165
+ const relay: ExecuteRelayedToolInput = {
166
+ AgentSessionID: 's1',
167
+ TargetAgentID: 'lead-1',
168
+ AllowedAgents: [{ agentId: 'skip-1', label: 'Skip' }],
169
+ Call: { CallID: 'c1', ToolName: INVOKE_TARGET_AGENT_TOOL_NAME, Arguments: JSON.stringify({ request: 'do it' }) },
170
+ };
171
+ await svc.ExecuteRelayedTool(relay, user, provider);
172
+ expect(svc.RanAgent?.ID).toBe('lead-1');
173
+ });
174
+
175
+ it('returns a structured not-available error for an unknown colleague', async () => {
176
+ const svc = new IntegrationService();
177
+ const relay: ExecuteRelayedToolInput = {
178
+ AgentSessionID: 's1',
179
+ TargetAgentID: 'lead-1',
180
+ AllowedAgents: [{ agentId: 'skip-1', label: 'Skip' }],
181
+ Call: { CallID: 'c1', ToolName: INVOKE_TARGET_AGENT_TOOL_NAME, Arguments: JSON.stringify({ agent: 'Ghost', request: 'x' }) },
182
+ };
183
+ const out = await svc.ExecuteRelayedTool(relay, user, provider);
184
+ const parsed = JSON.parse(out.ResultJson);
185
+ expect(parsed.success).toBe(false);
186
+ // A not-available colleague is a structured delegation outcome ({success:false, output}),
187
+ // not a thrown error — the model narrates `output` to self-correct.
188
+ expect(parsed.output).toContain('Ghost');
189
+ expect(svc.RanAgent).toBeNull(); // nothing ran
190
+ });
191
+ });
@@ -0,0 +1,123 @@
1
+ // type-graphql decorators on the resolver call `Reflect.getMetadata`, which only exists when this
2
+ // polyfill is loaded first. MUST precede any import that pulls in the resolver file.
3
+ import 'reflect-metadata';
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+
7
+ // type-graphql's `@Field()`/`@ObjectType()`/`@Query()` decorators need `emitDecoratorMetadata`
8
+ // reflection (a build-time tsc feature) that vitest's esbuild transform does NOT apply, so importing the
9
+ // decorated resolver throws "Unable to infer GraphQL type". This unit test exercises the resolver's plain
10
+ // TS logic (the snapshot best-effort contract), not its GraphQL schema — so we replace the decorators with
11
+ // no-ops. (The real schema is validated by the build + integration tests.)
12
+ vi.mock('type-graphql', () => {
13
+ const noopDecorator = () => () => undefined;
14
+ return {
15
+ Resolver: noopDecorator,
16
+ Mutation: noopDecorator,
17
+ Query: noopDecorator,
18
+ Subscription: noopDecorator,
19
+ ObjectType: noopDecorator,
20
+ InputType: noopDecorator,
21
+ Field: noopDecorator,
22
+ Arg: noopDecorator,
23
+ Args: noopDecorator,
24
+ Ctx: noopDecorator,
25
+ PubSub: noopDecorator,
26
+ Root: noopDecorator,
27
+ Float: class {},
28
+ Int: class {},
29
+ ID: class {},
30
+ };
31
+ });
32
+
33
+ // --- Mock the live-session engine the resolver reads. A single mutable session object lets each test
34
+ // script what CaptureScreenshot does (resolve a frame / throw "Browser not launched"). ---
35
+ interface FakeLiveSession {
36
+ CaptureScreenshot: ReturnType<typeof vi.fn>;
37
+ GetCurrentUrl: ReturnType<typeof vi.fn>;
38
+ }
39
+ const liveSession: FakeLiveSession = {
40
+ CaptureScreenshot: vi.fn(async () => 'QUJD'),
41
+ GetCurrentUrl: vi.fn(() => 'https://www.wikipedia.org/'),
42
+ };
43
+ // The snapshot path is a pure READ: it looks up an existing session via GetSessionForAgentSession and never
44
+ // lazily starts one. `undefined` models "no live browser for this agent session".
45
+ const getSessionMock = vi.fn<[], FakeLiveSession | undefined>(() => liveSession);
46
+ vi.mock('@memberjunction/remote-browser-server', () => ({
47
+ RemoteBrowserEngine: {
48
+ Instance: {
49
+ GetSessionForAgentSession: (...args: unknown[]) => getSessionMock(...(args as [])),
50
+ },
51
+ },
52
+ }));
53
+
54
+ // Keep the AI imports (visual interpreter) inert — they aren't exercised by the snapshot path.
55
+ vi.mock('@memberjunction/aiengine', () => ({ AIEngine: { Instance: { Config: vi.fn(), Prompts: [] } } }));
56
+ vi.mock('@memberjunction/ai-prompts', () => ({ AIPromptRunner: class {} }));
57
+ // Spread the real module so transitively-loaded code still resolves with its REAL base classes; only
58
+ // AIPromptParams is overridden for the test.
59
+ vi.mock('@memberjunction/ai-core-plus', async (importOriginal) => ({
60
+ ...(await importOriginal<typeof import('@memberjunction/ai-core-plus')>()),
61
+ AIPromptParams: class {},
62
+ }));
63
+
64
+ import { RemoteBrowserActionResolver } from '../resolvers/RemoteBrowserActionResolver.js';
65
+
66
+ /**
67
+ * A test subclass that bypasses the ownership/provider plumbing (covered elsewhere) so the snapshot
68
+ * best-effort contract is exercised in isolation.
69
+ */
70
+ class TestableResolver extends RemoteBrowserActionResolver {
71
+ protected requireUserAndProvider() {
72
+ return { contextUser: { ID: 'user-1' }, provider: {} } as never;
73
+ }
74
+ protected async loadOwnedSession() {
75
+ return { ID: 'sess-1', AgentID: 'agent-1', UserID: 'user-1' } as never;
76
+ }
77
+ }
78
+
79
+ const ctx = { userPayload: { sessionId: 'push-sess-1' }, providers: {} } as never;
80
+
81
+ describe('RemoteBrowserActionResolver — snapshot (best-effort perception poll)', () => {
82
+ let resolver: TestableResolver;
83
+
84
+ beforeEach(() => {
85
+ resolver = new TestableResolver();
86
+ liveSession.CaptureScreenshot.mockReset().mockResolvedValue('QUJD');
87
+ liveSession.GetCurrentUrl.mockReset().mockReturnValue('https://www.wikipedia.org/');
88
+ getSessionMock.mockReset().mockReturnValue(liveSession);
89
+ });
90
+
91
+ it('returns the screenshot + URL when the browser is live', async () => {
92
+ const result = await resolver.RemoteBrowserSnapshot('sess-1', ctx);
93
+ expect(result).toEqual({ ScreenshotBase64: 'QUJD', CurrentUrl: 'https://www.wikipedia.org/' });
94
+ expect(liveSession.CaptureScreenshot).toHaveBeenCalledTimes(1);
95
+ });
96
+
97
+ it('returns an empty snapshot when no live browser exists (never started / already torn down)', async () => {
98
+ getSessionMock.mockReturnValue(undefined);
99
+ const result = await resolver.RemoteBrowserSnapshot('sess-1', ctx);
100
+ expect(result).toEqual({});
101
+ expect(liveSession.CaptureScreenshot).not.toHaveBeenCalled();
102
+ });
103
+
104
+ // The regression the fix targets: a session handle survives in the live map but its underlying browser
105
+ // adapter has been torn down, so CaptureScreenshot throws "Browser not launched". The ~700ms client poll
106
+ // must NOT surface that as a recurring GraphQL error — the query's documented contract is empty fields,
107
+ // not an error. So the resolver catches and degrades to {} (the surface keeps its last good frame).
108
+ it('degrades to an empty snapshot — does NOT throw — when the adapter is torn down', async () => {
109
+ liveSession.CaptureScreenshot.mockRejectedValueOnce(
110
+ new Error('Browser not launched. Call Launch() before using the adapter.'),
111
+ );
112
+ const result = await resolver.RemoteBrowserSnapshot('sess-1', ctx);
113
+ expect(result).toEqual({});
114
+ expect(liveSession.CaptureScreenshot).toHaveBeenCalledTimes(1);
115
+ });
116
+
117
+ it('does not read the URL when the capture fails (no half-populated snapshot)', async () => {
118
+ liveSession.CaptureScreenshot.mockRejectedValueOnce(new Error('Target page, context or browser has been closed'));
119
+ const result = await resolver.RemoteBrowserSnapshot('sess-1', ctx);
120
+ expect(result).toEqual({});
121
+ expect(liveSession.GetCurrentUrl).not.toHaveBeenCalled();
122
+ });
123
+ });
@@ -397,6 +397,8 @@ describe('SessionManager — server-side channel plugin lifecycle notifications'
397
397
  AgentID: 'agent-1',
398
398
  UserID: 'user-1',
399
399
  ConversationID: 'conv-existing',
400
+ // Verbatim per-session config blob handed to data-aware channels; null when unset.
401
+ AgentSessionConfig: null,
400
402
  });
401
403
  expect((user as { ID: string }).ID).toBe('user-1');
402
404
  expect(prov).toBe(provider);
@@ -0,0 +1,248 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
3
+
4
+ // ── Mocks ──────────────────────────────────────────────────────────────────
5
+ // The helper uses `RunView.FromMetadataProvider(provider).RunView(...)` for conversation lookups and
6
+ // `FileStorageEngine.Instance` for storage-account/driver/upload. We stub both so the test is pure
7
+ // (no DB, no network, no LiveKit/storage SDK).
8
+
9
+ const runViewMock = vi.fn();
10
+
11
+ vi.mock('@memberjunction/core', async (importOriginal) => {
12
+ const actual = await importOriginal<Record<string, unknown>>();
13
+ class RunView {
14
+ static FromMetadataProvider() {
15
+ return { RunView: runViewMock };
16
+ }
17
+ RunView = runViewMock;
18
+ }
19
+ return { ...actual, RunView, LogError: vi.fn(), LogStatus: vi.fn() };
20
+ });
21
+
22
+ // FileStorageEngine — a configurable fake. Tests reset its behavior in beforeEach.
23
+ const storage = {
24
+ accountsByProvider: new Map<string, Array<{ ID: string }>>(),
25
+ getObject: vi.fn(async () => Buffer.from('mp4-bytes')),
26
+ uploadFile: vi.fn(async () => ({ FileID: 'copied-file', StoragePath: 'meeting-recordings/2026/x.mp4' })),
27
+ };
28
+
29
+ vi.mock('@memberjunction/storage', () => ({
30
+ FileStorageEngine: {
31
+ Instance: {
32
+ Config: vi.fn(async () => undefined),
33
+ GetAccountsByProviderID: (id: string) => storage.accountsByProvider.get(id) ?? [],
34
+ GetDriver: vi.fn(async () => ({ GetObject: storage.getObject })),
35
+ UploadFile: (...args: unknown[]) => storage.uploadFile(...args),
36
+ },
37
+ },
38
+ }));
39
+
40
+ vi.mock('@memberjunction/core-entities', async (importOriginal) => {
41
+ const actual = await importOriginal<Record<string, unknown>>();
42
+ return { ...actual };
43
+ });
44
+
45
+ import {
46
+ registerMeetingRecordingFile,
47
+ resolveMeetingConversation,
48
+ type MeetingRecordingEgressResult,
49
+ } from '../resolvers/meetingRecordingRegistration';
50
+
51
+ // ── Fakes ──────────────────────────────────────────────────────────────────
52
+
53
+ const user = { ID: 'U1', Name: 'Amith', Email: 'amith@x.com' } as unknown as UserInfo;
54
+
55
+ /** A fake BaseEntity for Conversations / Files — records the values set on it. */
56
+ class FakeEntity {
57
+ public fields: Record<string, unknown> = {};
58
+ public ID = 'new-id';
59
+ public saved = false;
60
+ public loadedID: string | null = null;
61
+ constructor(public kind: 'conversation' | 'file', private saveResult = true) {}
62
+ NewRecord() {}
63
+ async Load(id: string) { this.loadedID = id; return true; }
64
+ async Save() { this.saved = this.saveResult; return this.saveResult; }
65
+ get LatestResult() { return { CompleteMessage: 'save failed' }; }
66
+ // Conversation fields
67
+ set UserID(v: string) { this.fields.UserID = v; }
68
+ set Name(v: string) { this.fields.Name = v; }
69
+ set Type(v: string) { this.fields.Type = v; }
70
+ set ExternalID(v: string) { this.fields.ExternalID = v; }
71
+ set ApplicationScope(v: string) { this.fields.ApplicationScope = v; }
72
+ set RecordingFileID(v: string) { this.fields.RecordingFileID = v; }
73
+ get RecordingFileID() { return this.fields.RecordingFileID as string; }
74
+ set EgressID(v: string) { this.fields.EgressID = v; }
75
+ get EgressID() { return this.fields.EgressID as string; }
76
+ // File fields
77
+ set ContentType(v: string) { this.fields.ContentType = v; }
78
+ set ProviderID(v: string) { this.fields.ProviderID = v; }
79
+ set ProviderKey(v: string) { this.fields.ProviderKey = v; }
80
+ set Status(v: string) { this.fields.Status = v; }
81
+ }
82
+
83
+ /** Builds a provider whose GetEntityObject hands back fresh fakes and records the created Files row. */
84
+ function makeProvider(opts: { fileSaveOk?: boolean } = {}) {
85
+ const created: { conversation: FakeEntity[]; file: FakeEntity[] } = { conversation: [], file: [] };
86
+ const provider = {
87
+ GetEntityObject: vi.fn(async (entityName: string) => {
88
+ if (entityName === 'MJ: Files') {
89
+ const f = new FakeEntity('file', opts.fileSaveOk ?? true);
90
+ f.ID = 'file-123';
91
+ created.file.push(f);
92
+ return f;
93
+ }
94
+ const c = new FakeEntity('conversation');
95
+ c.ID = 'conv-new';
96
+ created.conversation.push(c);
97
+ return c;
98
+ }),
99
+ } as unknown as IMetadataProvider;
100
+ return { provider, created };
101
+ }
102
+
103
+ const egress: MeetingRecordingEgressResult = {
104
+ EgressID: 'eg-1',
105
+ RoomName: 'room-1',
106
+ OutputLocation: 'room-1/2026.mp4',
107
+ OutputSizeBytes: 1234,
108
+ };
109
+
110
+ beforeEach(() => {
111
+ vi.clearAllMocks();
112
+ vi.unstubAllEnvs();
113
+ runViewMock.mockResolvedValue({ Success: true, Results: [] });
114
+ storage.accountsByProvider = new Map([['prov-sink', [{ ID: 'acct-sink' }]]]);
115
+ process.env.MJ_MEETING_RECORDING_STORAGE_PROVIDER = 'prov-sink';
116
+ delete process.env.MJ_MEETING_RECORDING_CANONICAL_STORAGE_PROVIDER;
117
+ });
118
+
119
+ describe('registerMeetingRecordingFile', () => {
120
+ it('resolves the conversation, creates the Files row pointing at the sink, and stamps the Conversation', async () => {
121
+ // First RunView (by EgressID) → no match; second (by RoomName) → an existing conversation.
122
+ runViewMock
123
+ .mockResolvedValueOnce({ Success: true, Results: [] })
124
+ .mockResolvedValueOnce({ Success: true, Results: [{ ID: 'conv-existing' }] });
125
+
126
+ const { provider, created } = makeProvider();
127
+ const result = await registerMeetingRecordingFile(egress, user, provider);
128
+
129
+ expect(result.Success).toBe(true);
130
+ expect(result.RecordingFileID).toBe('file-123');
131
+ expect(result.ConversationID).toBe('conv-existing');
132
+
133
+ // Files row: correct provider/key/content-type/status, pointing directly at the egress output.
134
+ const file = created.file[0];
135
+ expect(file.fields.ProviderID).toBe('prov-sink');
136
+ expect(file.fields.ProviderKey).toBe('room-1/2026.mp4');
137
+ expect(file.fields.ContentType).toBe('video/mp4');
138
+ expect(file.fields.Status).toBe('Uploaded');
139
+ expect(file.saved).toBe(true);
140
+
141
+ // Conversation stamped with the file id (loaded by id, then saved).
142
+ const conv = created.conversation[0];
143
+ expect(conv.loadedID).toBe('conv-existing');
144
+ expect(conv.fields.RecordingFileID).toBe('file-123');
145
+ expect(conv.fields.EgressID).toBe('eg-1');
146
+ expect(conv.saved).toBe(true);
147
+
148
+ // No byte copy in the default v1 path.
149
+ expect(storage.getObject).not.toHaveBeenCalled();
150
+ expect(storage.uploadFile).not.toHaveBeenCalled();
151
+ });
152
+
153
+ it('prefers matching the conversation by EgressID when present', async () => {
154
+ runViewMock.mockResolvedValueOnce({ Success: true, Results: [{ ID: 'conv-by-egress' }] });
155
+ const { provider } = makeProvider();
156
+ const result = await registerMeetingRecordingFile(egress, user, provider);
157
+ expect(result.ConversationID).toBe('conv-by-egress');
158
+ // Only the EgressID lookup ran (no fall-through to room-name lookup).
159
+ expect(runViewMock).toHaveBeenCalledTimes(1);
160
+ });
161
+
162
+ it('creates a Meeting-Room Conversation when none exists', async () => {
163
+ runViewMock.mockResolvedValue({ Success: true, Results: [] }); // neither lookup matches
164
+ const { provider, created } = makeProvider();
165
+ const result = await registerMeetingRecordingFile(egress, user, provider);
166
+ expect(result.Success).toBe(true);
167
+ const conv = created.conversation[0];
168
+ expect(conv.fields.Type).toBe('Meeting Room');
169
+ expect(conv.fields.ExternalID).toBe('room-1');
170
+ expect(conv.fields.ApplicationScope).toBe('Application');
171
+ });
172
+
173
+ it('returns a graceful failure when the storage provider is not configured', async () => {
174
+ delete process.env.MJ_MEETING_RECORDING_STORAGE_PROVIDER;
175
+ const { provider } = makeProvider();
176
+ const result = await registerMeetingRecordingFile(egress, user, provider);
177
+ expect(result.Success).toBe(false);
178
+ expect(result.ErrorMessage).toMatch(/MJ_MEETING_RECORDING_STORAGE_PROVIDER/);
179
+ expect(result.RecordingFileID).toBeUndefined();
180
+ });
181
+
182
+ it('returns a graceful failure when no account is linked to the configured provider', async () => {
183
+ storage.accountsByProvider = new Map(); // no accounts for prov-sink
184
+ const { provider } = makeProvider();
185
+ const result = await registerMeetingRecordingFile(egress, user, provider);
186
+ expect(result.Success).toBe(false);
187
+ expect(result.ErrorMessage).toMatch(/No active MJStorage account/i);
188
+ });
189
+
190
+ it('returns a graceful failure when the egress has no output yet', async () => {
191
+ const { provider } = makeProvider();
192
+ const result = await registerMeetingRecordingFile({ ...egress, OutputLocation: undefined }, user, provider);
193
+ expect(result.Success).toBe(false);
194
+ expect(result.ErrorMessage).toMatch(/no output/i);
195
+ });
196
+
197
+ describe('copy-to-canonical (the "copy into Box" option)', () => {
198
+ it('copies the MP4 into a differing canonical provider and points the Files row there', async () => {
199
+ storage.accountsByProvider = new Map([
200
+ ['prov-sink', [{ ID: 'acct-sink' }]],
201
+ ['prov-canonical', [{ ID: 'acct-canonical' }]],
202
+ ]);
203
+ process.env.MJ_MEETING_RECORDING_CANONICAL_STORAGE_PROVIDER = 'prov-canonical';
204
+ runViewMock.mockResolvedValueOnce({ Success: true, Results: [{ ID: 'conv-1' }] });
205
+
206
+ const { provider, created } = makeProvider();
207
+ const result = await registerMeetingRecordingFile(egress, user, provider);
208
+
209
+ expect(result.Success).toBe(true);
210
+ // Bytes were read from the sink and uploaded into the canonical provider.
211
+ expect(storage.getObject).toHaveBeenCalledWith({ fullPath: 'room-1/2026.mp4' });
212
+ expect(storage.uploadFile).toHaveBeenCalledOnce();
213
+
214
+ // The Files row points at the canonical provider + its uploaded key.
215
+ const file = created.file[0];
216
+ expect(file.fields.ProviderID).toBe('prov-canonical');
217
+ expect(file.fields.ProviderKey).toBe('meeting-recordings/2026/x.mp4');
218
+ });
219
+
220
+ it('does NOT copy when the canonical provider equals the sink provider', async () => {
221
+ process.env.MJ_MEETING_RECORDING_CANONICAL_STORAGE_PROVIDER = 'prov-sink'; // same as sink
222
+ runViewMock.mockResolvedValueOnce({ Success: true, Results: [{ ID: 'conv-1' }] });
223
+ const { provider, created } = makeProvider();
224
+ const result = await registerMeetingRecordingFile(egress, user, provider);
225
+ expect(result.Success).toBe(true);
226
+ expect(storage.getObject).not.toHaveBeenCalled();
227
+ expect(created.file[0].fields.ProviderID).toBe('prov-sink');
228
+ });
229
+ });
230
+
231
+ it('fails gracefully (no throw) when the Files row save fails', async () => {
232
+ runViewMock.mockResolvedValueOnce({ Success: true, Results: [{ ID: 'conv-1' }] });
233
+ const { provider } = makeProvider({ fileSaveOk: false });
234
+ const result = await registerMeetingRecordingFile(egress, user, provider);
235
+ expect(result.Success).toBe(false);
236
+ expect(result.ErrorMessage).toMatch(/Files row/i);
237
+ });
238
+ });
239
+
240
+ describe('resolveMeetingConversation', () => {
241
+ it('falls back to creating a conversation when neither lookup matches', async () => {
242
+ runViewMock.mockResolvedValue({ Success: true, Results: [] });
243
+ const { provider, created } = makeProvider();
244
+ const id = await resolveMeetingConversation(egress, user, provider);
245
+ expect(id).toBe('conv-new');
246
+ expect(created.conversation).toHaveLength(1);
247
+ });
248
+ });