@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.
- package/dist/agentSessions/ReturningVisitorRecap.d.ts +39 -0
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -0
- package/dist/agentSessions/ReturningVisitorRecap.js +198 -0
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -0
- package/dist/agentSessions/SessionJanitor.d.ts +13 -0
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +65 -7
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +40 -0
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +3 -1
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/index.d.ts +1 -1
- package/dist/auth/magicLink/index.d.ts.map +1 -1
- package/dist/auth/magicLink/index.js +1 -1
- package/dist/auth/magicLink/index.js.map +1 -1
- package/dist/auth/magicLink/types.d.ts +26 -0
- package/dist/auth/magicLink/types.d.ts.map +1 -1
- package/dist/config.d.ts +1883 -144
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +160 -36
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +45 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +1684 -54
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +57105 -48029
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +13 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +22 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +4 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +110 -3
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +6 -0
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
- package/dist/realtimeWidget/WidgetRouter.js +182 -0
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.js +477 -0
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
- package/dist/realtimeWidget/host-identity.d.ts +40 -0
- package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
- package/dist/realtimeWidget/host-identity.js +58 -0
- package/dist/realtimeWidget/host-identity.js.map +1 -0
- package/dist/realtimeWidget/index.d.ts +10 -0
- package/dist/realtimeWidget/index.d.ts.map +1 -0
- package/dist/realtimeWidget/index.js +9 -0
- package/dist/realtimeWidget/index.js.map +1 -0
- package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
- package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
- package/dist/realtimeWidget/visitorIdentity.js +202 -0
- package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
- package/dist/realtimeWidget/widgetCore.d.ts +106 -0
- package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
- package/dist/realtimeWidget/widgetCore.js +173 -0
- package/dist/realtimeWidget/widgetCore.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +31 -8
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -2
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +74 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +211 -2
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +6 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +15 -13
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +8 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +28 -4
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +91 -2
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +337 -17
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts +12 -3
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +50 -17
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
- package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
- package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
- package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
- package/dist/resolvers/TelephonyResolver.d.ts +26 -0
- package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/TelephonyResolver.js +86 -0
- package/dist/resolvers/TelephonyResolver.js.map +1 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +2 -3
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
- package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/VonageTelephonyResolver.js +86 -0
- package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
- package/dist/resolvers/meetingRecordingRegistration.d.ts +124 -0
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
- package/dist/resolvers/meetingRecordingRegistration.js +311 -0
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
- package/dist/resolvers/peaksSidecar.d.ts +30 -0
- package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
- package/dist/resolvers/peaksSidecar.js +51 -0
- package/dist/resolvers/peaksSidecar.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +68 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
- package/dist/rest/MediaAccessKeys.js +96 -0
- package/dist/rest/MediaAccessKeys.js.map +1 -0
- package/dist/rest/MediaStreamHandler.d.ts +26 -0
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
- package/dist/rest/MediaStreamHandler.js +195 -0
- package/dist/rest/MediaStreamHandler.js.map +1 -0
- package/dist/rest/mediaRange.d.ts +41 -0
- package/dist/rest/mediaRange.d.ts.map +1 -0
- package/dist/rest/mediaRange.js +60 -0
- package/dist/rest/mediaRange.js.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
- package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +262 -0
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.js +96 -0
- package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
- package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
- package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsService.js +196 -0
- package/dist/telephony/TeamsMeetingsService.js.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.js +143 -0
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
- package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
- package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyService.js +193 -0
- package/dist/telephony/TwilioTelephonyService.js.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.js +201 -0
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
- package/dist/telephony/VonageTelephonyService.d.ts +90 -0
- package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyService.js +191 -0
- package/dist/telephony/VonageTelephonyService.js.map +1 -0
- package/dist/telephony/calendar-scheduler.d.ts +67 -0
- package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
- package/dist/telephony/calendar-scheduler.js +133 -0
- package/dist/telephony/calendar-scheduler.js.map +1 -0
- package/dist/telephony/index.d.ts +29 -0
- package/dist/telephony/index.d.ts.map +1 -0
- package/dist/telephony/index.js +38 -0
- package/dist/telephony/index.js.map +1 -0
- package/dist/telephony/media-upgrade-router.d.ts +33 -0
- package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
- package/dist/telephony/media-upgrade-router.js +56 -0
- package/dist/telephony/media-upgrade-router.js.map +1 -0
- package/dist/telephony/ringcentral-runtime.d.ts +14 -0
- package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
- package/dist/telephony/ringcentral-runtime.js +18 -0
- package/dist/telephony/ringcentral-runtime.js.map +1 -0
- package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
- package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
- package/dist/telephony/teams-meetings-runtime.js +20 -0
- package/dist/telephony/teams-meetings-runtime.js.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
- package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
- package/dist/telephony/telephony-runtime.d.ts +14 -0
- package/dist/telephony/telephony-runtime.d.ts.map +1 -0
- package/dist/telephony/telephony-runtime.js +18 -0
- package/dist/telephony/telephony-runtime.js.map +1 -0
- package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
- package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/twilioMediaRegistry.js +106 -0
- package/dist/telephony/twilioMediaRegistry.js.map +1 -0
- package/dist/telephony/vonage-runtime.d.ts +14 -0
- package/dist/telephony/vonage-runtime.d.ts.map +1 -0
- package/dist/telephony/vonage-runtime.js +18 -0
- package/dist/telephony/vonage-runtime.js.map +1 -0
- package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
- package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/vonageMediaRegistry.js +158 -0
- package/dist/telephony/vonageMediaRegistry.js.map +1 -0
- package/package.json +88 -82
- package/src/__tests__/FileResolverPeaks.test.ts +64 -0
- package/src/__tests__/MediaAccessKeys.test.ts +68 -0
- package/src/__tests__/MediaStreamHandler.test.ts +91 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
- package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
- package/src/__tests__/SessionManager.test.ts +2 -0
- package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
- package/src/__tests__/search-knowledge-tags.test.ts +9 -9
- package/src/__tests__/teams-meetings-service.test.ts +65 -0
- package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
- package/src/__tests__/twilio-telephony-service.test.ts +23 -0
- package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
- package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
- package/src/__tests__/widget.test.ts +204 -0
- package/src/__tests__/widgetGuestElevation.test.ts +57 -0
- package/src/__tests__/widgetHostIdentity.test.ts +117 -0
- package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
- package/src/agentSessions/SessionJanitor.ts +66 -6
- package/src/agentSessions/SessionManager.ts +41 -0
- package/src/agentSessions/index.ts +3 -1
- package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
- package/src/auth/magicLink/index.ts +1 -0
- package/src/auth/magicLink/types.ts +26 -0
- package/src/config.ts +172 -38
- package/src/context.ts +50 -3
- package/src/generated/generated.ts +26726 -20464
- package/src/generic/ResolverBase.ts +28 -0
- package/src/generic/RunViewResolver.ts +4 -0
- package/src/index.ts +119 -3
- package/src/integration/CustomColumnPromoter.ts +6 -0
- package/src/realtimeWidget/WidgetRouter.ts +204 -0
- package/src/realtimeWidget/WidgetSessionService.ts +714 -0
- package/src/realtimeWidget/host-identity.ts +84 -0
- package/src/realtimeWidget/index.ts +15 -0
- package/src/realtimeWidget/visitorIdentity.ts +258 -0
- package/src/realtimeWidget/widgetCore.ts +231 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
- package/src/resolvers/ComponentRegistryResolver.ts +36 -10
- package/src/resolvers/EntityPermissionResolver.ts +1 -2
- package/src/resolvers/FileResolver.ts +199 -1
- package/src/resolvers/QueryResolver.ts +33 -19
- package/src/resolvers/QuerySystemUserResolver.ts +14 -10
- package/src/resolvers/RealtimeBridgeResolver.ts +36 -4
- package/src/resolvers/RealtimeClientSessionResolver.ts +383 -9
- package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
- package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
- package/src/resolvers/RunAIAgentResolver.ts +60 -15
- package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
- package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
- package/src/resolvers/TelephonyResolver.ts +63 -0
- package/src/resolvers/TestQuerySQLResolver.ts +2 -3
- package/src/resolvers/VonageTelephonyResolver.ts +63 -0
- package/src/resolvers/meetingRecordingRegistration.ts +432 -0
- package/src/resolvers/peaksSidecar.ts +52 -0
- package/src/rest/MediaAccessKeys.ts +121 -0
- package/src/rest/MediaStreamHandler.ts +223 -0
- package/src/rest/mediaRange.ts +76 -0
- package/src/telephony/RingCentralTelephonyService.ts +342 -0
- package/src/telephony/TeamsMeetingsRouter.ts +124 -0
- package/src/telephony/TeamsMeetingsService.ts +268 -0
- package/src/telephony/TwilioTelephonyRouter.ts +184 -0
- package/src/telephony/TwilioTelephonyService.ts +261 -0
- package/src/telephony/VonageTelephonyRouter.ts +239 -0
- package/src/telephony/VonageTelephonyService.ts +259 -0
- package/src/telephony/calendar-scheduler.ts +176 -0
- package/src/telephony/index.ts +43 -0
- package/src/telephony/media-upgrade-router.ts +62 -0
- package/src/telephony/ringcentral-runtime.ts +22 -0
- package/src/telephony/teams-meetings-runtime.ts +24 -0
- package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
- package/src/telephony/telephony-runtime.ts +22 -0
- package/src/telephony/twilioMediaRegistry.ts +137 -0
- package/src/telephony/vonage-runtime.ts +22 -0
- package/src/telephony/vonageMediaRegistry.ts +200 -0
- package/dist/agents/skip-agent.d.ts +0 -111
- package/dist/agents/skip-agent.d.ts.map +0 -1
- package/dist/agents/skip-agent.js +0 -1487
- package/dist/agents/skip-agent.js.map +0 -1
- package/dist/agents/skip-sdk.d.ts +0 -261
- package/dist/agents/skip-sdk.d.ts.map +0 -1
- package/dist/agents/skip-sdk.js +0 -909
- package/dist/agents/skip-sdk.js.map +0 -1
- package/src/agents/skip-agent.ts +0 -1594
- 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
|
+
});
|