@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
@@ -82,7 +82,9 @@ describe('TaskGraphActionRunner — durable payload round-trips by param name (#
82
82
  beforeEach(() => {
83
83
  // vitest's `restoreMocks: true` clears mock implementations before every test, including
84
84
  // the ones assigned inside the `vi.mock` factory above — so the return value is (re)armed here.
85
+ // Call history on a vi.fn created inside vi.mock survives that restore, so clear it too.
85
86
  vi.mocked(ActionEngineServer.Instance.Config).mockResolvedValue(undefined);
87
+ vi.mocked(ActionEngineServer.Instance.RunAction, { partial: true }).mockClear();
86
88
  vi.mocked(ActionEngineServer.Instance.RunAction, { partial: true }).mockResolvedValue({
87
89
  Success: true,
88
90
  Params: [],
@@ -127,3 +129,44 @@ describe('TaskGraphActionRunner — durable payload round-trips by param name (#
127
129
  expect(sentParams.map((p) => p.Name).sort()).toEqual(['EntityName', 'Record']);
128
130
  });
129
131
  });
132
+
133
+ /**
134
+ * A durable step's Output is stored on the Task row as JSON. An action that hands back an output
135
+ * param holding a live object (an entity, an observable) used to surface much later, from the
136
+ * dispatcher, as a bare "Converting circular structure to JSON" with no hint of WHICH action or
137
+ * param — seen live with Execute Agent's AgentResult. The runner is the boundary that knows both
138
+ * names, so it refuses the output there and says which param it was.
139
+ */
140
+ describe('TaskGraphActionRunner: outputs must be storable', () => {
141
+ beforeEach(() => vi.mocked(ActionEngineServer.Instance.RunAction).mockReset());
142
+
143
+ it('fails the step, naming the action and the param, when an output param cannot be serialized', async () => {
144
+ const cyclic: Record<string, unknown> = { kind: 'OperatorSubscriber' };
145
+ cyclic.self = cyclic;
146
+ vi.mocked(ActionEngineServer.Instance.RunAction).mockResolvedValue({
147
+ Success: true,
148
+ Message: 'started',
149
+ Params: [{ Name: 'AgentRunID', Type: 'Output', Value: 'RUN-1' }, { Name: 'AgentResult', Type: 'Output', Value: cyclic }],
150
+ LogEntry: { ID: 'LOG-1' },
151
+ } as never);
152
+
153
+ const result = await new TaskGraphActionRunner().RunActionForTask(baseRunParams({}));
154
+
155
+ expect(result.Success).toBe(false);
156
+ expect(result.ErrorMessage).toMatch(/Log Activity.*AgentResult.*cannot be stored/i);
157
+ expect(result.ActionLogID).toBe('LOG-1');
158
+ });
159
+
160
+ it('passes plain outputs through unchanged', async () => {
161
+ vi.mocked(ActionEngineServer.Instance.RunAction).mockResolvedValue({
162
+ Success: true,
163
+ Message: 'ok',
164
+ Params: [{ Name: 'AgentRunID', Type: 'Output', Value: 'RUN-1' }],
165
+ LogEntry: { ID: 'LOG-2' },
166
+ } as never);
167
+
168
+ const result = await new TaskGraphActionRunner().RunActionForTask(baseRunParams({}));
169
+
170
+ expect(result).toMatchObject({ Success: true, Output: { AgentRunID: 'RUN-1' }, ActionLogID: 'LOG-2' });
171
+ });
172
+ });
@@ -0,0 +1,106 @@
1
+ import 'reflect-metadata';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { DatabaseProviderBase, UserInfo, type IMetadataProvider } from '@memberjunction/core';
4
+ import sql from 'mssql';
5
+ import { VectorizeEntityResolver } from '../resolvers/VectorizeEntityResolver.js';
6
+ import type { AppContext, ProviderInfo } from '../types.js';
7
+
8
+ const mockSyncerConstructedWith: (IMetadataProvider | undefined)[] = [];
9
+
10
+ vi.mock('@memberjunction/ai-vector-sync', () => {
11
+ return {
12
+ EntityVectorSyncer: class {
13
+ constructor(provider?: IMetadataProvider) {
14
+ mockSyncerConstructedWith.push(provider);
15
+ }
16
+ Config() {
17
+ return Promise.resolve(undefined);
18
+ }
19
+ VectorizeEntity() {
20
+ return Promise.resolve({ success: true, status: 'Completed' });
21
+ }
22
+ },
23
+ };
24
+ });
25
+
26
+ vi.mock('../generic/PubSubManager.js', () => ({
27
+ PubSubManager: {
28
+ Instance: {
29
+ Publish: vi.fn(),
30
+ },
31
+ },
32
+ }));
33
+
34
+ function createAppContext(providers?: ProviderInfo[]): AppContext {
35
+ return {
36
+ dataSource: Object.create(sql.ConnectionPool.prototype) as sql.ConnectionPool,
37
+ dataSources: [],
38
+ providers: providers ?? [],
39
+ userPayload: {
40
+ email: 'test@example.com',
41
+ sessionId: 'test-session',
42
+ userRecord: new UserInfo(),
43
+ },
44
+ };
45
+ }
46
+
47
+ describe('VectorizeEntityResolver — provider propagation', () => {
48
+ beforeEach(() => {
49
+ mockSyncerConstructedWith.length = 0;
50
+ });
51
+
52
+ it('passes request-scoped Read-Write provider from AppContext to EntityVectorSyncer', async () => {
53
+ const mockProvider = Object.create(DatabaseProviderBase.prototype) as DatabaseProviderBase;
54
+ const providerInfo: ProviderInfo = {
55
+ provider: mockProvider,
56
+ type: 'Read-Write',
57
+ };
58
+
59
+ const resolver = new VectorizeEntityResolver();
60
+ const result = await resolver.VectorizeEntity(
61
+ 'doc-1',
62
+ 'entity-1',
63
+ 10,
64
+ createAppContext([providerInfo])
65
+ );
66
+
67
+ expect(result.Success).toBe(true);
68
+ expect(result.Status).toBe('Started');
69
+ expect(mockSyncerConstructedWith.length).toBe(1);
70
+ expect(mockSyncerConstructedWith[0]).toBe(mockProvider);
71
+ });
72
+
73
+ it('falls back to Read-Only provider when no Read-Write provider exists', async () => {
74
+ const readOnlyProvider = Object.create(DatabaseProviderBase.prototype) as DatabaseProviderBase;
75
+ const providerInfo: ProviderInfo = {
76
+ provider: readOnlyProvider,
77
+ type: 'Read-Only',
78
+ };
79
+
80
+ const resolver = new VectorizeEntityResolver();
81
+ const result = await resolver.VectorizeEntity(
82
+ 'doc-2',
83
+ 'entity-2',
84
+ 10,
85
+ createAppContext([providerInfo])
86
+ );
87
+
88
+ expect(result.Success).toBe(true);
89
+ expect(mockSyncerConstructedWith.length).toBe(1);
90
+ expect(mockSyncerConstructedWith[0]).toBe(readOnlyProvider);
91
+ });
92
+
93
+ it('passes undefined provider to EntityVectorSyncer when no providers in AppContext', async () => {
94
+ const resolver = new VectorizeEntityResolver();
95
+ const result = await resolver.VectorizeEntity(
96
+ 'doc-3',
97
+ 'entity-3',
98
+ 10,
99
+ createAppContext([])
100
+ );
101
+
102
+ expect(result.Success).toBe(true);
103
+ expect(mockSyncerConstructedWith.length).toBe(1);
104
+ expect(mockSyncerConstructedWith[0]).toBeUndefined();
105
+ });
106
+ });
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+
3
+ // TaskGraphProviderFactory imports config.ts (which validates DB env at module load); UserCache needs a live
4
+ // provider. Both are replaced; the tests pass explicit dependencies.
5
+ vi.mock('../config.js', () => ({ configInfo: {}, mj_core_schema: '__mj' }));
6
+ vi.mock('@memberjunction/generic-database-provider', async (importOriginal) => ({
7
+ ...(await importOriginal<typeof import('@memberjunction/generic-database-provider')>()),
8
+ UserCache: { get Users() { return []; } },
9
+ }));
10
+
11
+ import type { DatabaseProviderBase, IMetadataProvider, UserInfo } from '@memberjunction/core';
12
+ import type { WorkQueueEngine, WorkQueueHostConfig, WorkQueueProviderSource } from '@memberjunction/work-queue-engine';
13
+ import { WorkQueueSchema } from '../services/workQueueConfig.js';
14
+ import type { WorkQueueConfig } from '../services/workQueueConfig.js';
15
+ import {
16
+ BuildWorkQueueHostConfig, CreateWorkQueueInstanceID, IsWorkQueueHostDisabledByEnv, MJServerWorkQueueProviderSource,
17
+ StartWorkQueueHost, WORK_QUEUE_DISABLE_ENV,
18
+ } from '../services/WorkQueueHostService.js';
19
+ import type { WorkQueueHostStartDependencies } from '../services/WorkQueueHostService.js';
20
+
21
+ const PROVIDER = { Name: 'server-provider' } as unknown as DatabaseProviderBase;
22
+ const SYSTEM_USER = { ID: 'AAAAAAAA-1111-4111-8111-000000000001', Email: 'system@memberjunction.org' } as UserInfo;
23
+ const ENGINE = { Name: 'engine' } as unknown as WorkQueueEngine;
24
+ const PROVIDER_SOURCE: WorkQueueProviderSource = { CreateProvider: async (): Promise<IMetadataProvider> => PROVIDER };
25
+
26
+ interface Recorder {
27
+ Dependencies: WorkQueueHostStartDependencies;
28
+ Started: WorkQueueHostConfig[];
29
+ EngineCalls: number;
30
+ }
31
+
32
+ function recorder(users: UserInfo[] = [SYSTEM_USER], env: Record<string, string | undefined> = {}): Recorder {
33
+ const result: Recorder = {
34
+ Started: [],
35
+ EngineCalls: 0,
36
+ Dependencies: {
37
+ Env: env,
38
+ FindUserByEmail: email => users.find(user => user.Email === email),
39
+ ConfigureEngine: async (user, provider) => {
40
+ expect(user).toBe(SYSTEM_USER);
41
+ expect(provider).toBe(PROVIDER);
42
+ result.EngineCalls++;
43
+ return ENGINE;
44
+ },
45
+ CreateHost: (config, engine, user, provider, source) => {
46
+ expect([engine, user, provider, source]).toEqual([ENGINE, SYSTEM_USER, PROVIDER, PROVIDER_SOURCE]);
47
+ return { Start: async () => { result.Started.push(config); } };
48
+ },
49
+ },
50
+ };
51
+ return result;
52
+ }
53
+
54
+ function enabled(overrides: Partial<WorkQueueConfig> = {}): WorkQueueConfig {
55
+ return { ...WorkQueueSchema.parse({ enabled: true }), ...overrides };
56
+ }
57
+
58
+ describe('workQueue configuration', () => {
59
+ it('defaults to disabled, running every MJWorker subscription once enabled', () => {
60
+ expect(WorkQueueSchema.parse({})).toEqual({
61
+ enabled: false, systemUserEmail: 'system@memberjunction.org', subscriptions: [{ name: '*', concurrency: 4 }],
62
+ idlePollMinMs: 250, idlePollMaxMs: 5000, shutdownDrainMs: 8000, sweeperEnabled: true, sweeperIntervalMs: 60000,
63
+ reconcileIntervalMs: 30000,
64
+ });
65
+ });
66
+
67
+ it('rejects an idle poll maximum below the minimum', () => {
68
+ expect(WorkQueueSchema.safeParse({ idlePollMinMs: 1000, idlePollMaxMs: 500 }).success).toBe(false);
69
+ });
70
+ });
71
+
72
+ describe('host configuration helpers', () => {
73
+ it('maps the section to a host config and disables the sweeper when asked', () => {
74
+ const config = enabled({ subscriptions: [{ name: 'email.unsubscribe', concurrency: 8 }], sweeperEnabled: false });
75
+ expect(BuildWorkQueueHostConfig(config, 'api-1')).toEqual({
76
+ InstanceID: 'api-1', Subscriptions: [{ Name: 'email.unsubscribe', Concurrency: 8 }], IdlePollMinMs: 250,
77
+ IdlePollMaxMs: 5000, ShutdownDrainMs: 8000, SweeperIntervalMs: 0, ReconcileIntervalMs: 30000,
78
+ });
79
+ });
80
+
81
+ it('builds a unique instance ID from host, pid and random entropy', () => {
82
+ const first = CreateWorkQueueInstanceID({ HOSTNAME: 'api-7' });
83
+ expect(first).toMatch(new RegExp(`^api-7-${process.pid}-[0-9a-f]{8}$`));
84
+ expect(CreateWorkQueueInstanceID({ HOSTNAME: 'api-7' })).not.toBe(first);
85
+ });
86
+
87
+ it(`treats only ${WORK_QUEUE_DISABLE_ENV}=1 as the kill switch`, () => {
88
+ expect(IsWorkQueueHostDisabledByEnv({ [WORK_QUEUE_DISABLE_ENV]: '1' })).toBe(true);
89
+ expect(IsWorkQueueHostDisabledByEnv({ [WORK_QUEUE_DISABLE_ENV]: 'true' })).toBe(false);
90
+ expect(IsWorkQueueHostDisabledByEnv({})).toBe(false);
91
+ });
92
+ });
93
+
94
+ describe('StartWorkQueueHost', () => {
95
+ it('starts nothing when the section is disabled', async () => {
96
+ const rec = recorder();
97
+ expect(await StartWorkQueueHost(WorkQueueSchema.parse({}), PROVIDER, PROVIDER_SOURCE, rec.Dependencies)).toBeNull();
98
+ expect(rec.EngineCalls).toBe(0);
99
+ });
100
+
101
+ it('starts nothing when the kill switch is set', async () => {
102
+ const rec = recorder([SYSTEM_USER], { [WORK_QUEUE_DISABLE_ENV]: '1' });
103
+ expect(await StartWorkQueueHost(enabled(), PROVIDER, PROVIDER_SOURCE, rec.Dependencies)).toBeNull();
104
+ expect(rec.EngineCalls).toBe(0);
105
+ });
106
+
107
+ it('refuses to start without the system user', async () => {
108
+ const rec = recorder([]);
109
+ await expect(StartWorkQueueHost(enabled(), PROVIDER, PROVIDER_SOURCE, rec.Dependencies)).rejects.toThrow('System user not found with email: system@memberjunction.org');
110
+ });
111
+
112
+ it('names the setting to fix when the placeholder email from DEFAULT_SERVER_CONFIG is still in effect', async () => {
113
+ const rec = recorder();
114
+ await expect(StartWorkQueueHost(enabled({ systemUserEmail: 'not.set@nowhere.com' }), PROVIDER, PROVIDER_SOURCE, rec.Dependencies))
115
+ .rejects.toThrow('set workQueue.systemUserEmail');
116
+ });
117
+
118
+ it('configures the engine, then creates and starts the host', async () => {
119
+ const rec = recorder();
120
+ const host = await StartWorkQueueHost(enabled(), PROVIDER, PROVIDER_SOURCE, rec.Dependencies);
121
+ expect(host).not.toBeNull();
122
+ expect(rec.EngineCalls).toBe(1);
123
+ expect(rec.Started).toHaveLength(1);
124
+ expect(rec.Started[0]).toMatchObject({ Subscriptions: [{ Name: '*', Concurrency: 4 }], SweeperIntervalMs: 60000 });
125
+ });
126
+ });
127
+
128
+ describe('MJServerWorkQueueProviderSource', () => {
129
+ it('falls back to the shared server provider when there is no SQL Server pool', async () => {
130
+ const source = new MJServerWorkQueueProviderSource(null, PROVIDER);
131
+ expect(await source.CreateProvider()).toBe(PROVIDER);
132
+ });
133
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * An ad-hoc query's timeout never exceeds the server's requestTimeout, so a caller cannot hold a
3
+ * read-only connection longer than any other request may.
4
+ */
5
+ import { describe, it, expect } from 'vitest';
6
+ import { ClampAdhocTimeoutSeconds } from '../resolvers/adhocTimeout';
7
+
8
+ describe('ClampAdhocTimeoutSeconds', () => {
9
+ it('caps a longer request at requestTimeout', () => {
10
+ expect(ClampAdhocTimeoutSeconds(600, 30000)).toBe(30);
11
+ });
12
+
13
+ it('keeps a shorter request', () => {
14
+ expect(ClampAdhocTimeoutSeconds(5, 30000)).toBe(5);
15
+ });
16
+
17
+ it('uses 30 seconds when the caller names none, within the cap', () => {
18
+ expect(ClampAdhocTimeoutSeconds(undefined, 30000)).toBe(30);
19
+ expect(ClampAdhocTimeoutSeconds(undefined, 10000)).toBe(10);
20
+ expect(ClampAdhocTimeoutSeconds(0, 30000)).toBe(30);
21
+ });
22
+
23
+ it('leaves the request alone when the server sets no limit', () => {
24
+ expect(ClampAdhocTimeoutSeconds(600, 0)).toBe(600);
25
+ });
26
+ });