@memberjunction/server 6.2.0-edge.1 → 6.2.0-edge.3

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 (220) hide show
  1. package/README.md +30 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/config.d.ts +2189 -71
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +123 -3
  6. package/dist/config.js.map +1 -1
  7. package/dist/context.d.ts.map +1 -1
  8. package/dist/context.js +11 -23
  9. package/dist/context.js.map +1 -1
  10. package/dist/generated/graphql-schemas/__mj.d.ts +2143 -205
  11. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  12. package/dist/generated/graphql-schemas/__mj.js +24441 -14433
  13. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  14. package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
  15. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
  16. package/dist/generic/FireAndForgetHeartbeat.js +16 -2
  17. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
  18. package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
  19. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
  20. package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
  21. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
  22. package/dist/generic/PushStatusResolver.d.ts +38 -0
  23. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  24. package/dist/generic/PushStatusResolver.js +84 -1
  25. package/dist/generic/PushStatusResolver.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +56 -4
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +191 -74
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +4 -0
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +33 -1
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +204 -50
  37. package/dist/index.js.map +1 -1
  38. package/dist/postgresqlCompat.d.ts +8 -0
  39. package/dist/postgresqlCompat.d.ts.map +1 -0
  40. package/dist/postgresqlCompat.js +19 -0
  41. package/dist/postgresqlCompat.js.map +1 -0
  42. package/dist/postgresqlPoolSettings.d.ts +108 -0
  43. package/dist/postgresqlPoolSettings.d.ts.map +1 -0
  44. package/dist/postgresqlPoolSettings.js +134 -0
  45. package/dist/postgresqlPoolSettings.js.map +1 -0
  46. package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
  47. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  48. package/dist/resolvers/AdhocQueryResolver.js +77 -167
  49. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  50. package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
  51. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
  52. package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
  53. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
  54. package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
  55. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
  56. package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
  57. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
  58. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
  59. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
  60. package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
  61. package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
  62. package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
  63. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  64. package/dist/resolvers/EntityRecordNameResolver.js +9 -47
  65. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  66. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  67. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
  68. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  69. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  70. package/dist/resolvers/GetDataResolver.js +9 -0
  71. package/dist/resolvers/GetDataResolver.js.map +1 -1
  72. package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
  73. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
  74. package/dist/resolvers/HumanHandoffResolver.js +324 -0
  75. package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
  76. package/dist/resolvers/MeetingResolver.d.ts +69 -0
  77. package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
  78. package/dist/resolvers/MeetingResolver.js +381 -0
  79. package/dist/resolvers/MeetingResolver.js.map +1 -0
  80. package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
  81. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  82. package/dist/resolvers/MergeRecordsResolver.js +16 -2
  83. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  84. package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
  85. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  86. package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
  87. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  88. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +45 -89
  89. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  90. package/dist/resolvers/RealtimeClientSessionResolver.js +94 -240
  91. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  92. package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
  93. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  94. package/dist/resolvers/RunAIAgentResolver.js +23 -153
  95. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  96. package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
  97. package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
  98. package/dist/resolvers/RunDecisionResolver.js +396 -0
  99. package/dist/resolvers/RunDecisionResolver.js.map +1 -0
  100. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  101. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  102. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  103. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  104. package/dist/resolvers/adhocTimeout.d.ts +7 -0
  105. package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
  106. package/dist/resolvers/adhocTimeout.js +12 -0
  107. package/dist/resolvers/adhocTimeout.js.map +1 -0
  108. package/dist/resolvers/meetings/index.d.ts +5 -0
  109. package/dist/resolvers/meetings/index.d.ts.map +1 -0
  110. package/dist/resolvers/meetings/index.js +5 -0
  111. package/dist/resolvers/meetings/index.js.map +1 -0
  112. package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
  113. package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
  114. package/dist/resolvers/meetings/meetingHelpers.js +212 -0
  115. package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
  116. package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
  117. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
  118. package/dist/resolvers/meetings/meetingMutations.js +418 -0
  119. package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
  120. package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
  121. package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
  122. package/dist/resolvers/meetings/meetingQueries.js +113 -0
  123. package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
  124. package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
  125. package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
  126. package/dist/resolvers/meetings/meetingTypes.js +345 -0
  127. package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
  128. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  129. package/dist/rest/ViewOperationsHandler.js +8 -5
  130. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  131. package/dist/services/TaskGraphActionRunner.d.ts +8 -0
  132. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  133. package/dist/services/TaskGraphActionRunner.js +27 -1
  134. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  135. package/dist/services/WorkQueueHostService.d.ts +36 -0
  136. package/dist/services/WorkQueueHostService.d.ts.map +1 -0
  137. package/dist/services/WorkQueueHostService.js +97 -0
  138. package/dist/services/WorkQueueHostService.js.map +1 -0
  139. package/dist/services/workQueueConfig.d.ts +103 -0
  140. package/dist/services/workQueueConfig.d.ts.map +1 -0
  141. package/dist/services/workQueueConfig.js +48 -0
  142. package/dist/services/workQueueConfig.js.map +1 -0
  143. package/dist/types.d.ts +5 -0
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js.map +1 -1
  146. package/package.json +95 -92
  147. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
  148. package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
  149. package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
  150. package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
  151. package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
  152. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  153. package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
  154. package/src/__tests__/MeetingResolver.test.ts +374 -0
  155. package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
  156. package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
  157. package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
  158. package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
  159. package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
  160. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
  161. package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
  162. package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
  163. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
  164. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
  165. package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
  166. package/src/__tests__/RunDecisionResolver.test.ts +699 -0
  167. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +43 -0
  168. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  169. package/src/__tests__/WorkQueueHostService.test.ts +133 -0
  170. package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
  171. package/src/__tests__/conversationRunEvents.test.ts +399 -0
  172. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
  173. package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
  174. package/src/__tests__/pushStatusFanOut.test.ts +206 -0
  175. package/src/__tests__/translateBracketsToPG.test.ts +20 -0
  176. package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
  177. package/src/config.ts +130 -3
  178. package/src/context.ts +11 -24
  179. package/src/generated/graphql-schemas/__mj.ts +24183 -17182
  180. package/src/generic/FireAndForgetHeartbeat.ts +16 -2
  181. package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
  182. package/src/generic/PushStatusResolver.ts +97 -1
  183. package/src/generic/ResolverBase.ts +220 -76
  184. package/src/generic/RunViewResolver.ts +33 -2
  185. package/src/index.ts +228 -51
  186. package/src/postgresqlCompat.ts +21 -0
  187. package/src/postgresqlPoolSettings.ts +213 -0
  188. package/src/resolvers/AdhocQueryResolver.ts +88 -188
  189. package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
  190. package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
  191. package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
  192. package/src/resolvers/EntityRecordNameResolver.ts +9 -51
  193. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
  194. package/src/resolvers/GetDataResolver.ts +10 -0
  195. package/src/resolvers/HumanHandoffResolver.ts +304 -0
  196. package/src/resolvers/MeetingResolver.ts +438 -0
  197. package/src/resolvers/MergeRecordsResolver.ts +14 -2
  198. package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
  199. package/src/resolvers/RealtimeClientSessionResolver.ts +99 -287
  200. package/src/resolvers/RunAIAgentResolver.ts +28 -174
  201. package/src/resolvers/RunDecisionResolver.ts +435 -0
  202. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  203. package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
  204. package/src/resolvers/adhocTimeout.ts +12 -0
  205. package/src/resolvers/meetings/index.ts +4 -0
  206. package/src/resolvers/meetings/meetingHelpers.ts +307 -0
  207. package/src/resolvers/meetings/meetingMutations.ts +535 -0
  208. package/src/resolvers/meetings/meetingQueries.ts +172 -0
  209. package/src/resolvers/meetings/meetingTypes.ts +238 -0
  210. package/src/rest/ViewOperationsHandler.ts +8 -5
  211. package/src/services/TaskGraphActionRunner.ts +29 -1
  212. package/src/services/WorkQueueHostService.ts +129 -0
  213. package/src/services/workQueueConfig.ts +52 -0
  214. package/src/types.ts +5 -0
  215. package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
  216. package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
  217. package/dist/resolvers/adhoc-query-helpers.js +0 -50
  218. package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
  219. package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
  220. package/src/resolvers/adhoc-query-helpers.ts +0 -67
@@ -0,0 +1,374 @@
1
+ import 'reflect-metadata';
2
+
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
5
+ import type {
6
+ MJMeetingEntity,
7
+ MJMeetingParticipantEntity,
8
+ MJPhoneNumberEntity,
9
+ MJInteractionEntity,
10
+ MJInteractionEventEntity,
11
+ } from '@memberjunction/core-entities';
12
+
13
+ const mocks = vi.hoisted(() => ({
14
+ mintClientToken: vi.fn(async (roomName: string, identity: string, name?: string) => ({
15
+ Token: `token-for-${roomName}-${identity}`,
16
+ Identity: identity,
17
+ ServerUrl: 'wss://livekit.example.com',
18
+ })),
19
+ startAgentRoomSession: vi.fn(async () => ({
20
+ SessionBridgeID: 'bridge-123',
21
+ RoomName: 'room-123',
22
+ })),
23
+ stopAllAgentsInRoom: vi.fn(async () => undefined),
24
+ startRoomRecording: vi.fn(async (params: { RoomName: string }) => ({
25
+ EgressID: 'egress-456',
26
+ RoomName: params.RoomName,
27
+ Status: 'EGRESS_STARTING',
28
+ })),
29
+ stopRecording: vi.fn(async (egressID: string) => ({
30
+ EgressID: egressID,
31
+ RoomName: 'room-123',
32
+ Status: 'EGRESS_COMPLETE',
33
+ })),
34
+ sendNotification: vi.fn(async () => ({ success: true })),
35
+ notificationConfig: vi.fn(async () => undefined),
36
+ createSession: vi.fn(async () => ({ ID: 'agent-session-789' })),
37
+ }));
38
+
39
+ vi.mock('@memberjunction/livekit-room-server', () => ({
40
+ LiveKitTokenService: class {
41
+ MintClientToken = mocks.mintClientToken;
42
+ },
43
+ LiveKitAgentRoomCoordinator: {
44
+ Instance: {
45
+ StartAgentRoomSession: mocks.startAgentRoomSession,
46
+ StopAllAgentsInRoom: mocks.stopAllAgentsInRoom,
47
+ },
48
+ },
49
+ LiveKitEgressService: class {
50
+ StartRoomRecording = mocks.startRoomRecording;
51
+ StopRecording = mocks.stopRecording;
52
+ },
53
+ LiveKitUserIdentity: (id: string) => `user_${id.toLowerCase()}`,
54
+ }));
55
+
56
+ vi.mock('@memberjunction/notifications', () => ({
57
+ NotificationEngine: {
58
+ Instance: {
59
+ Config: mocks.notificationConfig,
60
+ SendNotification: mocks.sendNotification,
61
+ },
62
+ },
63
+ }));
64
+
65
+ import {
66
+ MeetingResolver,
67
+ type CreateMeetingInput,
68
+ type UpdateMeetingInput,
69
+ type RSVPMeetingInput,
70
+ } from '../resolvers/MeetingResolver.js';
71
+ import type { AppContext } from '../types.js';
72
+
73
+ class MockEntityBase {
74
+ public ID: string = `mock-${Date.now()}-${Math.random()}`;
75
+ public LatestResult?: { CompleteMessage?: string };
76
+ public async Save(): Promise<boolean> {
77
+ return true;
78
+ }
79
+ }
80
+
81
+ class MockMeetingEntity extends MockEntityBase {
82
+ public Title: string = '';
83
+ public Description: string | null = null;
84
+ public HostUserID: string = '';
85
+ public HostUser: string = 'Host User';
86
+ public RoomName: string = '';
87
+ public Status: MJMeetingEntity['Status'] = 'Scheduled';
88
+ public ScheduledStartAt: Date | null = null;
89
+ public ScheduledEndAt: Date | null = null;
90
+ public StartedAt: Date | null = null;
91
+ public EndedAt: Date | null = null;
92
+ public AllowPhoneDialIn: boolean = false;
93
+ public DialInPhoneNumberID: string | null = null;
94
+ public DialInCode: string | null = null;
95
+ public RecordingPolicy: MJMeetingEntity['RecordingPolicy'] = 'Off';
96
+ public ConversationID: string | null = null;
97
+ public __mj_CreatedAt: Date = new Date();
98
+ public __mj_UpdatedAt: Date = new Date();
99
+ }
100
+
101
+ class MockMeetingParticipantEntity extends MockEntityBase {
102
+ public MeetingID: string = '';
103
+ public UserID: string | null = null;
104
+ public User: string | null = null;
105
+ public AgentID: string | null = null;
106
+ public Agent: string | null = null;
107
+ public ExternalName: string | null = null;
108
+ public ExternalEmail: string | null = null;
109
+ public ExternalPhone: string | null = null;
110
+ public Role: MJMeetingParticipantEntity['Role'] = 'Attendee';
111
+ public InviteStatus: MJMeetingParticipantEntity['InviteStatus'] = 'Invited';
112
+ public JoinedAt: Date | null = null;
113
+ public LeftAt: Date | null = null;
114
+ }
115
+
116
+ class MockPhoneNumberEntity extends MockEntityBase {
117
+ public Number: string = '+15551234567';
118
+ public Status: MJPhoneNumberEntity['Status'] = 'Active';
119
+ public Label: string | null = 'Main Dial-In';
120
+ public __mj_CreatedAt: Date = new Date();
121
+ }
122
+
123
+ class MockInteractionEntity extends MockEntityBase {
124
+ public Channel: MJInteractionEntity['Channel'] = 'Meeting';
125
+ public Direction: MJInteractionEntity['Direction'] = 'Inbound';
126
+ public Status: MJInteractionEntity['Status'] = 'Active';
127
+ public RoomName: string | null = null;
128
+ public StartedAt: Date = new Date();
129
+ public AnsweredAt: Date | null = new Date();
130
+ public EndedAt: Date | null = null;
131
+ public RecordingEnabled: boolean = false;
132
+ }
133
+
134
+ class MockInteractionEventEntity extends MockEntityBase {
135
+ public InteractionID: string = '';
136
+ public EventType: MJInteractionEventEntity['EventType'] = 'Created';
137
+ public ActorUserID: string | null = null;
138
+ public OccurredAt: Date = new Date();
139
+ public Details: string | null = null;
140
+ }
141
+
142
+ class TestableMeetingResolver extends MeetingResolver {
143
+ public user: UserInfo | undefined = { ID: 'user-host-1', Name: 'Alice Host', Email: 'alice@example.com' } as unknown as UserInfo;
144
+
145
+ public mockMeetings: MockMeetingEntity[] = [];
146
+ public mockParticipants: MockMeetingParticipantEntity[] = [];
147
+ public mockPhoneNumbers: MockPhoneNumberEntity[] = [];
148
+ public mockInteractions: MockInteractionEntity[] = [];
149
+ public mockEvents: MockInteractionEventEntity[] = [];
150
+
151
+ protected override GetUserFromPayload(): UserInfo | undefined {
152
+ return this.user;
153
+ }
154
+
155
+ public getMockProvider(): IMetadataProvider {
156
+ return {
157
+ RunView: async () => ({ Success: true, Results: [] }),
158
+ GetEntityObject: async <T>(entityName: string) => {
159
+ if (entityName === 'MJ: Meetings') {
160
+ const m = new MockMeetingEntity() as unknown as T;
161
+ this.mockMeetings.push(m as unknown as MockMeetingEntity);
162
+ return m;
163
+ }
164
+ if (entityName === 'MJ: Meeting Participants') {
165
+ const p = new MockMeetingParticipantEntity() as unknown as T;
166
+ this.mockParticipants.push(p as unknown as MockMeetingParticipantEntity);
167
+ return p;
168
+ }
169
+ if (entityName === 'MJ: Interactions') {
170
+ const i = new MockInteractionEntity() as unknown as T;
171
+ this.mockInteractions.push(i as unknown as MockInteractionEntity);
172
+ return i;
173
+ }
174
+ if (entityName === 'MJ: Interaction Events') {
175
+ const e = new MockInteractionEventEntity() as unknown as T;
176
+ this.mockEvents.push(e as unknown as MockInteractionEventEntity);
177
+ return e;
178
+ }
179
+ throw new Error(`Unknown entity: ${entityName}`);
180
+ },
181
+ } as unknown as IMetadataProvider;
182
+ }
183
+ }
184
+
185
+ describe('MeetingResolver', () => {
186
+ let resolver: TestableMeetingResolver;
187
+ let mockProvider: IMetadataProvider;
188
+ let ctx: AppContext;
189
+
190
+ beforeEach(() => {
191
+ resolver = new TestableMeetingResolver();
192
+ mockProvider = resolver.getMockProvider();
193
+ ctx = {
194
+ providers: [{ type: 'Read-Write', provider: mockProvider }],
195
+ } as unknown as AppContext;
196
+ mocks.mintClientToken.mockClear();
197
+ mocks.startAgentRoomSession.mockClear();
198
+ mocks.stopAllAgentsInRoom.mockClear();
199
+ mocks.startRoomRecording.mockClear();
200
+ mocks.sendNotification.mockClear();
201
+ });
202
+
203
+ describe('CreateMeeting', () => {
204
+ it('creates a scheduled meeting with generated unguessable room name and adds host participant', async () => {
205
+ const input: CreateMeetingInput = {
206
+ Title: 'Sprint Planning',
207
+ Description: 'Bi-weekly sprint planning meeting',
208
+ ScheduledStartAt: new Date(Date.now() + 3600000),
209
+ ScheduledEndAt: new Date(Date.now() + 7200000),
210
+ Participants: [
211
+ { UserID: 'user-guest-2', Role: 'Attendee' },
212
+ { AgentID: 'agent-bot-1', Role: 'Agent' },
213
+ ],
214
+ };
215
+
216
+ const result = await resolver.CreateMeeting(input, ctx);
217
+ expect(result.Success).toBe(true);
218
+ expect(result.Meeting).toBeDefined();
219
+ expect(result.Meeting?.Title).toBe('Sprint Planning');
220
+ expect(result.Meeting?.RoomName).toMatch(/^mj-mtg-[a-f0-9-]+$/);
221
+ expect(result.Meeting?.Status).toBe('Scheduled');
222
+ expect(result.Meeting?.HostUserID).toBe('user-host-1');
223
+
224
+ // Verify participants were created: host + 2 invitees
225
+ expect(resolver.mockParticipants.length).toBe(3);
226
+ const hostPart = resolver.mockParticipants.find((p) => p.UserID === 'user-host-1');
227
+ expect(hostPart).toBeDefined();
228
+ expect(hostPart?.Role).toBe('Host');
229
+ expect(hostPart?.InviteStatus).toBe('Accepted');
230
+
231
+ const agentPart = resolver.mockParticipants.find((p) => p.AgentID === 'agent-bot-1');
232
+ expect(agentPart).toBeDefined();
233
+ expect(agentPart?.Role).toBe('Agent');
234
+
235
+ // Notification sent to invited user
236
+ expect(mocks.sendNotification).toHaveBeenCalled();
237
+ });
238
+
239
+ it('validates required title', async () => {
240
+ const result = await resolver.CreateMeeting({ Title: ' ' }, ctx);
241
+ expect(result.Success).toBe(false);
242
+ expect(result.ErrorMessage).toContain('title is required');
243
+ });
244
+
245
+ it('validates scheduled end time is after scheduled start time', async () => {
246
+ const start = new Date(Date.now() + 7200000);
247
+ const end = new Date(Date.now() + 3600000);
248
+ const result = await resolver.CreateMeeting({ Title: 'Invalid Times', ScheduledStartAt: start, ScheduledEndAt: end }, ctx);
249
+ expect(result.Success).toBe(false);
250
+ expect(result.ErrorMessage).toContain('must be after');
251
+ });
252
+ });
253
+
254
+ describe('UpdateMeeting authorization and validation', () => {
255
+ it('refuses update from non-host/co-host users', async () => {
256
+ // Mock existing meeting where HostUserID is different
257
+ const m = new MockMeetingEntity();
258
+ m.ID = 'mtg-existing-1';
259
+ m.HostUserID = 'other-user';
260
+ m.Title = 'Existing';
261
+ resolver.mockMeetings.push(m);
262
+
263
+ // Spy on loadMeetingEntity and isUserHostOrCoHost
264
+ vi.spyOn(resolver as unknown as { loadMeetingEntity: Function }, 'loadMeetingEntity').mockResolvedValue(m);
265
+ vi.spyOn(resolver as unknown as { isUserHostOrCoHost: Function }, 'isUserHostOrCoHost').mockResolvedValue(false);
266
+
267
+ const updateInput: UpdateMeetingInput = {
268
+ MeetingID: 'mtg-existing-1',
269
+ Title: 'Hacked Title',
270
+ };
271
+
272
+ const result = await resolver.UpdateMeeting(updateInput, ctx);
273
+ expect(result.Success).toBe(false);
274
+ expect(result.ErrorMessage).toContain('Unauthorized');
275
+ });
276
+
277
+ it('allows host to update title and details', async () => {
278
+ const m = new MockMeetingEntity();
279
+ m.ID = 'mtg-existing-1';
280
+ m.HostUserID = 'user-host-1';
281
+ m.Title = 'Old Title';
282
+ resolver.mockMeetings.push(m);
283
+
284
+ vi.spyOn(resolver as unknown as { loadMeetingEntity: Function }, 'loadMeetingEntity').mockResolvedValue(m);
285
+ vi.spyOn(resolver as unknown as { isUserHostOrCoHost: Function }, 'isUserHostOrCoHost').mockResolvedValue(true);
286
+ vi.spyOn(resolver as unknown as { loadParticipants: Function }, 'loadParticipants').mockResolvedValue([]);
287
+
288
+ const updateInput: UpdateMeetingInput = {
289
+ MeetingID: 'mtg-existing-1',
290
+ Title: 'Updated Title',
291
+ Description: 'New agenda',
292
+ };
293
+
294
+ const result = await resolver.UpdateMeeting(updateInput, ctx);
295
+ expect(result.Success).toBe(true);
296
+ expect(m.Title).toBe('Updated Title');
297
+ expect(m.Description).toBe('New agenda');
298
+ });
299
+ });
300
+
301
+ describe('StartMeeting and EndMeeting lifecycle', () => {
302
+ it('starts meeting, creates interaction, spins up agent participants, and mints token', async () => {
303
+ const m = new MockMeetingEntity();
304
+ m.ID = 'mtg-live-1';
305
+ m.HostUserID = 'user-host-1';
306
+ m.Title = 'Team Standup';
307
+ m.RoomName = 'mj-mtg-standup-1';
308
+ m.Status = 'Scheduled';
309
+ m.RecordingPolicy = 'Automatic';
310
+ resolver.mockMeetings.push(m);
311
+
312
+ const agentPart = new MockMeetingParticipantEntity();
313
+ agentPart.Role = 'Agent';
314
+ agentPart.AgentID = 'agent-co-1';
315
+
316
+ vi.spyOn(resolver as unknown as { loadMeetingEntity: Function }, 'loadMeetingEntity').mockResolvedValue(m);
317
+ vi.spyOn(resolver as unknown as { isUserHostOrCoHost: Function }, 'isUserHostOrCoHost').mockResolvedValue(true);
318
+ vi.spyOn(resolver as unknown as { loadParticipants: Function }, 'loadParticipants').mockResolvedValue([
319
+ { ID: 'p-1', MeetingID: m.ID, Role: 'Agent', AgentID: 'agent-co-1', InviteStatus: 'Accepted' },
320
+ ]);
321
+ vi.spyOn((resolver as unknown as { sessionManager: { CreateSession: Function } }).sessionManager, 'CreateSession').mockResolvedValue({
322
+ ID: 'agent-session-789',
323
+ });
324
+
325
+ const result = await resolver.StartMeeting(m.ID, ctx);
326
+ expect(result.Success).toBe(true);
327
+ expect(result.RoomName).toBe('mj-mtg-standup-1');
328
+ expect(result.ClientToken).toBeDefined();
329
+ expect(m.Status).toBe('Live');
330
+ expect(m.StartedAt).toBeDefined();
331
+
332
+ // Check agent session coordinator was called
333
+ expect(mocks.startAgentRoomSession).toHaveBeenCalledWith(
334
+ expect.objectContaining({
335
+ RoomName: 'mj-mtg-standup-1',
336
+ AgentID: 'agent-co-1',
337
+ }),
338
+ );
339
+
340
+ // Check automatic recording initiated
341
+ expect(mocks.startRoomRecording).toHaveBeenCalledWith({ RoomName: 'mj-mtg-standup-1' });
342
+
343
+ // Check client token minted
344
+ expect(mocks.mintClientToken).toHaveBeenCalled();
345
+ });
346
+
347
+ it('ends meeting, stops agents in room, and marks status Ended', async () => {
348
+ const m = new MockMeetingEntity();
349
+ m.ID = 'mtg-live-1';
350
+ m.HostUserID = 'user-host-1';
351
+ m.RoomName = 'mj-mtg-standup-1';
352
+ m.Status = 'Live';
353
+ resolver.mockMeetings.push(m);
354
+
355
+ vi.spyOn(resolver as unknown as { loadMeetingEntity: Function }, 'loadMeetingEntity').mockResolvedValue(m);
356
+ vi.spyOn(resolver as unknown as { isUserHostOrCoHost: Function }, 'isUserHostOrCoHost').mockResolvedValue(true);
357
+ vi.spyOn(resolver as unknown as { loadParticipants: Function }, 'loadParticipants').mockResolvedValue([]);
358
+
359
+ const result = await resolver.EndMeeting(m.ID, ctx);
360
+ expect(result.Success).toBe(true);
361
+ expect(m.Status).toBe('Ended');
362
+ expect(m.EndedAt).toBeDefined();
363
+ expect(mocks.stopAllAgentsInRoom).toHaveBeenCalledWith('mj-mtg-standup-1', 'Explicit', resolver.user, mockProvider);
364
+ });
365
+ });
366
+
367
+ describe('RSVPMeeting', () => {
368
+ it('validates RSVP response values', async () => {
369
+ const result = await resolver.RSVPMeeting({ MeetingID: 'mtg-1', InviteStatus: 'Maybe' }, ctx);
370
+ expect(result.Success).toBe(false);
371
+ expect(result.ErrorMessage).toContain('Invalid RSVP status');
372
+ });
373
+ });
374
+ });
@@ -13,8 +13,17 @@ const h = vi.hoisted(() => ({
13
13
  Identity: identity,
14
14
  RoomName: room,
15
15
  })),
16
+ startAgentRoomSession: vi.fn(async () => ({ SessionBridgeID: 'bridge-std', RoomName: 'room-std', ServerUrl: 'wss://x.livekit.cloud' })),
17
+ handoffDeps: { AgentStarter: undefined as undefined | ((req: Record<string, unknown>) => Promise<{ SessionBridgeID: string }>) },
16
18
  startRecording: vi.fn(async () => ({ EgressID: 'eg-1', RoomName: 'room-1', Status: 'EGRESS_ACTIVE' })),
19
+ getRoomTurnState: vi.fn((room: string): unknown => (room === 'busy-room' ? { RoomId: room, Agents: [] } : null)),
20
+ setTurnToolBinder: vi.fn(),
21
+ getBridgeRuntime: vi.fn(),
17
22
  stopRecording: vi.fn(async () => ({ EgressID: 'eg-1', RoomName: 'room-1', Status: 'EGRESS_COMPLETE' })),
23
+ getRoomForEgress: vi.fn(async (_id: string): Promise<string | undefined> => 'room-1'),
24
+ stopAgentRoomSession: vi.fn(async () => true),
25
+ stopAllAgentsInRoom: vi.fn(async () => 1),
26
+ getRoomForBridge: vi.fn((_id: string): string | undefined => 'room-1'),
18
27
  }));
19
28
 
20
29
  // These two are instantiated with `new` by the resolver, so they must be constructible. They were
@@ -25,11 +34,40 @@ vi.mock('@memberjunction/livekit-room-server', () => ({
25
34
  MintClientToken = h.mintClientToken;
26
35
  },
27
36
  // SetSessionFactory is exercised by the resolver's module-load binding of the realtime-session factory.
28
- LiveKitAgentRoomCoordinator: { Instance: { StartAgentRoomSession: vi.fn(), SetSessionFactory: vi.fn() } },
37
+ LiveKitAgentRoomCoordinator: {
38
+ Instance: {
39
+ StartAgentRoomSession: h.startAgentRoomSession,
40
+ SetSessionFactory: vi.fn(),
41
+ SetTurnToolBinder: h.setTurnToolBinder,
42
+ GetRoomTurnState: h.getRoomTurnState,
43
+ StopAgentRoomSession: h.stopAgentRoomSession,
44
+ StopAllAgentsInRoom: h.stopAllAgentsInRoom,
45
+ GetRoomForBridge: h.getRoomForBridge,
46
+ },
47
+ },
48
+ LiveKitUserIdentity: (id: string) => `user-${id}`.toLowerCase(),
49
+ RoomHandoffEngine: { Instance: { Deps: h.handoffDeps } },
29
50
  LiveKitEgressService: class {
30
51
  StartRoomRecording = h.startRecording;
31
52
  StopRecording = h.stopRecording;
53
+ GetRoomForEgress = h.getRoomForEgress;
32
54
  },
55
+ RoomAuthorizationService: (() => {
56
+ let testAuthorizer: ((roomName: string, user: unknown, provider?: unknown) => Promise<{ Authorized: boolean; Reason?: string }>) | undefined = undefined;
57
+ return {
58
+ Instance: {
59
+ AuthorizeRoomAccess: vi.fn(async (roomName: string, user: unknown, provider?: unknown) => {
60
+ if (testAuthorizer) {
61
+ return await testAuthorizer(roomName, user, provider);
62
+ }
63
+ return { Authorized: true };
64
+ }),
65
+ SetAuthorizerForTesting: vi.fn((fn?: (roomName: string, user: unknown, provider?: unknown) => Promise<{ Authorized: boolean; Reason?: string }>) => {
66
+ testAuthorizer = fn;
67
+ }),
68
+ },
69
+ };
70
+ })(),
33
71
  }));
34
72
 
35
73
  // Mock the agent factory so importing the resolver doesn't pull the heavy @memberjunction/ai-agents graph
@@ -39,6 +77,7 @@ vi.mock('@memberjunction/ai-agents', () => ({
39
77
  FinalizeBridgeCoAgentRuns: vi.fn(),
40
78
  GetRealtimeModelVoices: vi.fn(),
41
79
  CreateBridgeRoomTranscriptSink: vi.fn(),
80
+ GetBridgeRealtimeRuntime: h.getBridgeRuntime,
42
81
  // SessionManager's constructor defaults to `new RealtimeClientSessionService()` when this resolver
43
82
  // doesn't inject one (it never finalizes client-direct co-agent runs itself) — the mock must still
44
83
  // export the class so that default construction doesn't throw.
@@ -56,7 +95,8 @@ vi.mock('../resolvers/meetingRecordingRegistration', () => ({
56
95
  get correlateRecordingStart() { return this.CorrelateRecordingStart; },
57
96
  }));
58
97
 
59
- import { RealtimeBridgeResolver, MintLiveKitClientTokenInput, LiveKitRecordingInput } from '../resolvers/RealtimeBridgeResolver';
98
+ import { RealtimeBridgeResolver, MintLiveKitClientTokenInput, LiveKitRecordingInput, StartLiveKitAgentRoomSessionInput } from '../resolvers/RealtimeBridgeResolver';
99
+ import { RoomAuthorizationService } from '@memberjunction/livekit-room-server';
60
100
  import type { AppContext } from '../types.js';
61
101
 
62
102
  /** A resolver subclass that supplies a fake authenticated user (GetUserFromPayload is protected). */
@@ -69,11 +109,17 @@ class TestableResolver extends RealtimeBridgeResolver {
69
109
 
70
110
  const ctx = {} as AppContext;
71
111
 
112
+ // The resolver module installs the binder once, as it loads. `restoreMocks` clears call history before each test,
113
+ // so take it now rather than reading `mock.calls` inside a test.
114
+ type TurnToolBinderFn = (session: object, handler: object) => void;
115
+ const installedTurnToolBinder = h.setTurnToolBinder.mock.calls[0]?.[0] as TurnToolBinderFn | undefined;
116
+
72
117
  describe('RealtimeBridgeResolver', () => {
73
118
  let resolver: TestableResolver;
74
119
 
75
120
  beforeEach(() => {
76
121
  resolver = new TestableResolver();
122
+ RoomAuthorizationService.Instance.SetAuthorizerForTesting(async () => ({ Authorized: true }));
77
123
  });
78
124
 
79
125
  describe('MintLiveKitClientToken', () => {
@@ -97,6 +143,53 @@ describe('RealtimeBridgeResolver', () => {
97
143
  });
98
144
  });
99
145
 
146
+ describe('GetLiveKitRoomTurnState', () => {
147
+ it('serves a room\'s snapshot as JSON for the typed client to parse', async () => {
148
+ const result = await resolver.GetLiveKitRoomTurnState('busy-room', ctx);
149
+ expect(result.Success).toBe(true);
150
+ expect(JSON.parse(result.StateJSON as string)).toEqual({ RoomId: 'busy-room', Agents: [] });
151
+ });
152
+
153
+ it('reports success with no state when the room holds no agents', async () => {
154
+ const result = await resolver.GetLiveKitRoomTurnState('empty-room', ctx);
155
+ expect(result).toEqual({ Success: true, StateJSON: undefined });
156
+ });
157
+
158
+ it('requires an authenticated user', async () => {
159
+ resolver.user = undefined;
160
+ const result = await resolver.GetLiveKitRoomTurnState('busy-room', ctx);
161
+ expect(result.Success).toBe(false);
162
+ expect(result.ErrorMessage).toMatch(/current user/i);
163
+ });
164
+
165
+ it('turns a failure into a structured error instead of throwing', async () => {
166
+ h.getRoomTurnState.mockImplementationOnce(() => {
167
+ throw new Error('boom');
168
+ });
169
+ const result = await resolver.GetLiveKitRoomTurnState('busy-room', ctx);
170
+ expect(result).toMatchObject({ Success: false, ErrorMessage: 'boom' });
171
+ });
172
+ });
173
+
174
+ describe('turn-taking tool binding', () => {
175
+ it('installs the engine\'s tool handler as the model session runtime\'s local tool handler', () => {
176
+ const binder = installedTurnToolBinder!;
177
+ const setLocalToolHandler = vi.fn();
178
+ h.getBridgeRuntime.mockReturnValueOnce({ SetLocalToolHandler: setLocalToolHandler });
179
+ const session = {};
180
+ const handler = {};
181
+ binder(session, handler);
182
+ expect(h.getBridgeRuntime).toHaveBeenCalledWith(session);
183
+ expect(setLocalToolHandler).toHaveBeenCalledWith(handler);
184
+ });
185
+
186
+ it('does not throw when the session has no bridge runtime', () => {
187
+ const binder = installedTurnToolBinder!;
188
+ h.getBridgeRuntime.mockReturnValueOnce(undefined);
189
+ expect(() => binder({}, {})).not.toThrow();
190
+ });
191
+ });
192
+
100
193
  describe('recording', () => {
101
194
  it('starts a recording and maps the result', async () => {
102
195
  const input = Object.assign(new LiveKitRecordingInput(), { RoomName: 'room-1', Layout: 'grid' });
@@ -114,6 +207,23 @@ describe('RealtimeBridgeResolver', () => {
114
207
  expect(result.RecordingFileID).toBe('file-1');
115
208
  });
116
209
 
210
+ it('refuses to stop a recording when user is unauthorized for the room', async () => {
211
+ RoomAuthorizationService.Instance.SetAuthorizerForTesting(async () => ({
212
+ Authorized: false,
213
+ Reason: 'Not allowed to access this room',
214
+ }));
215
+ const result = await resolver.StopLiveKitRecording('eg-1', ctx);
216
+ expect(result.Success).toBe(false);
217
+ expect(result.ErrorMessage).toMatch(/Not allowed to access this room/);
218
+ });
219
+
220
+ it('refuses to stop a recording when room cannot be determined for egress ID', async () => {
221
+ h.getRoomForEgress.mockResolvedValueOnce(undefined);
222
+ const result = await resolver.StopLiveKitRecording('unknown-egress', ctx);
223
+ expect(result.Success).toBe(false);
224
+ expect(result.ErrorMessage).toMatch(/Unable to determine room/);
225
+ });
226
+
117
227
  it('requires an authenticated user to record', async () => {
118
228
  resolver.user = undefined;
119
229
  const input = Object.assign(new LiveKitRecordingInput(), { RoomName: 'room-1' });
@@ -122,4 +232,132 @@ describe('RealtimeBridgeResolver', () => {
122
232
  expect(result.ErrorMessage).toMatch(/current user/i);
123
233
  });
124
234
  });
235
+ describe('StartLiveKitAgentRoomSession with handoff', () => {
236
+ const input = (over: Partial<StartLiveKitAgentRoomSessionInput> = {}) =>
237
+ Object.assign(new StartLiveKitAgentRoomSessionInput(), { RoomName: 'room-h', AgentID: 'co-agent', TargetAgentID: 'target-agent', AgentName: 'Sage', AgentSessionID: 'AS1', ...over });
238
+
239
+ beforeEach(() => {
240
+ h.startAgentRoomSession.mockClear();
241
+ h.mintClientToken.mockClear();
242
+ h.handoffDeps.AgentStarter = undefined;
243
+ });
244
+
245
+ it('starts the agent through the handoff-capable starter when asked, naming the signed-in user as the caller, and returns a token for the same room', async () => {
246
+ const starter = vi.fn(async () => ({ SessionBridgeID: 'bridge-h' }));
247
+ h.handoffDeps.AgentStarter = starter;
248
+ const result = await resolver.StartLiveKitAgentRoomSession(input({ EnableHandoff: true, RealtimeVoice: 'echo' }), ctx);
249
+ expect(result).toMatchObject({ Success: true, SessionBridgeID: 'bridge-h', RoomName: 'room-h', ClientToken: 'jwt-user-u1', Identity: 'user-u1' });
250
+ expect(starter).toHaveBeenCalledWith(
251
+ expect.objectContaining({ RoomName: 'room-h', AgentID: 'target-agent', AgentName: 'Sage', CallerLabel: 'Amith', RealtimeVoice: 'echo' }),
252
+ );
253
+ expect(h.startAgentRoomSession).not.toHaveBeenCalled();
254
+ });
255
+
256
+ it('falls back to the standard room start when handoff is requested but the server cannot do it', async () => {
257
+ const result = await resolver.StartLiveKitAgentRoomSession(input({ EnableHandoff: true }), ctx);
258
+ expect(result).toMatchObject({ Success: true, SessionBridgeID: 'bridge-std' });
259
+ expect(h.startAgentRoomSession).toHaveBeenCalledTimes(1);
260
+ });
261
+
262
+ it('does not touch the handoff starter unless asked', async () => {
263
+ const starter = vi.fn(async () => ({ SessionBridgeID: 'bridge-h' }));
264
+ h.handoffDeps.AgentStarter = starter;
265
+ await resolver.StartLiveKitAgentRoomSession(input(), ctx);
266
+ expect(starter).not.toHaveBeenCalled();
267
+ expect(h.startAgentRoomSession).toHaveBeenCalledTimes(1);
268
+ });
269
+
270
+ it('reports a failure to start the handoff-capable agent instead of silently starting a plain one', async () => {
271
+ h.handoffDeps.AgentStarter = vi.fn(async () => {
272
+ throw new Error('All agent lines are busy right now.');
273
+ });
274
+ const result = await resolver.StartLiveKitAgentRoomSession(input({ EnableHandoff: true }), ctx);
275
+ expect(result.Success).toBe(false);
276
+ expect(result.ErrorMessage).toContain('busy');
277
+ expect(h.startAgentRoomSession).not.toHaveBeenCalled();
278
+ });
279
+
280
+ it('requires an authenticated user', async () => {
281
+ resolver.user = undefined;
282
+ const result = await resolver.StartLiveKitAgentRoomSession(input({ EnableHandoff: true }), ctx);
283
+ expect(result.Success).toBe(false);
284
+ expect(result.ErrorMessage).toMatch(/current user/i);
285
+ });
286
+ });
287
+
288
+ describe('Per-room authorization enforcement', () => {
289
+ beforeEach(() => {
290
+ vi.clearAllMocks();
291
+ RoomAuthorizationService.Instance.SetAuthorizerForTesting(async () => ({
292
+ Authorized: false,
293
+ Reason: 'User is not authorized for this room.',
294
+ }));
295
+ });
296
+
297
+ it('MintLiveKitClientToken rejects unauthorized room access', async () => {
298
+ const input = Object.assign(new MintLiveKitClientTokenInput(), { RoomName: 'private-room' });
299
+ const result = await resolver.MintLiveKitClientToken(input, ctx);
300
+ expect(result.Success).toBe(false);
301
+ expect(result.ErrorMessage).toBe('User is not authorized for this room.');
302
+ expect(h.mintClientToken).not.toHaveBeenCalled();
303
+ });
304
+
305
+ it('StartLiveKitAgentRoomSession rejects unauthorized room access', async () => {
306
+ const input = Object.assign(new StartLiveKitAgentRoomSessionInput(), {
307
+ RoomName: 'private-room',
308
+ AgentID: 'agent-1',
309
+ });
310
+ const result = await resolver.StartLiveKitAgentRoomSession(input, ctx);
311
+ expect(result.Success).toBe(false);
312
+ expect(result.ErrorMessage).toBe('User is not authorized for this room.');
313
+ expect(h.startAgentRoomSession).not.toHaveBeenCalled();
314
+ });
315
+
316
+ it('StopLiveKitAgentRoomSession rejects unauthorized room access', async () => {
317
+ h.getRoomForBridge.mockReturnValueOnce('private-room');
318
+ const result = await resolver.StopLiveKitAgentRoomSession('bridge-1', ctx);
319
+ expect(result).toBe(false);
320
+ expect(h.stopAgentRoomSession).not.toHaveBeenCalled();
321
+ });
322
+
323
+ it('StopLiveKitAgentRoomSession proceeds when room access is authorized', async () => {
324
+ RoomAuthorizationService.Instance.SetAuthorizerForTesting(async () => ({ Authorized: true }));
325
+ h.getRoomForBridge.mockReturnValueOnce('private-room');
326
+ const result = await resolver.StopLiveKitAgentRoomSession('bridge-1', ctx);
327
+ expect(result).toBe(true);
328
+ expect(h.stopAgentRoomSession).toHaveBeenCalledWith('bridge-1', 'Explicit', resolver.user, null);
329
+ });
330
+
331
+ it('EndLiveKitRoom rejects unauthorized room access', async () => {
332
+ const result = await resolver.EndLiveKitRoom('private-room', ctx);
333
+ expect(result).toBe(false);
334
+ expect(h.stopAllAgentsInRoom).not.toHaveBeenCalled();
335
+ });
336
+
337
+ it('EndLiveKitRoom proceeds when room access is authorized', async () => {
338
+ RoomAuthorizationService.Instance.SetAuthorizerForTesting(async () => ({ Authorized: true }));
339
+ const result = await resolver.EndLiveKitRoom('private-room', ctx);
340
+ expect(result).toBe(true);
341
+ expect(h.stopAllAgentsInRoom).toHaveBeenCalledWith('private-room', 'Explicit', resolver.user, null);
342
+ });
343
+
344
+ it('GetLiveKitRoomTurnState rejects unauthorized room access', async () => {
345
+ const result = await resolver.GetLiveKitRoomTurnState('private-room', ctx);
346
+ expect(result.Success).toBe(false);
347
+ expect(result.ErrorMessage).toBe('User is not authorized for this room.');
348
+ });
349
+
350
+ it('InviteUsersToLiveKitRoom rejects unauthorized room access', async () => {
351
+ const result = await resolver.InviteUsersToLiveKitRoom('private-room', ['user-2'], ctx);
352
+ expect(result).toBe(false);
353
+ });
354
+
355
+ it('StartLiveKitRecording rejects unauthorized room access', async () => {
356
+ const input = Object.assign(new LiveKitRecordingInput(), { RoomName: 'private-room' });
357
+ const result = await resolver.StartLiveKitRecording(input, ctx);
358
+ expect(result.Success).toBe(false);
359
+ expect(result.ErrorMessage).toBe('User is not authorized for this room.');
360
+ expect(h.startRecording).not.toHaveBeenCalled();
361
+ });
362
+ });
125
363
  });