@memberjunction/server 6.2.0-edge.7 → 6.2.0-edge.8
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/.tsbuildinfo +1 -1
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -1
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +2 -1
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -8
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +839 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +14652 -10253
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +8 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/globalCors.d.ts.map +1 -1
- package/dist/globalCors.js +1 -0
- package/dist/globalCors.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -8
- package/dist/index.js.map +1 -1
- package/dist/logging/StartupLogger.d.ts +50 -0
- package/dist/logging/StartupLogger.d.ts.map +1 -1
- package/dist/logging/StartupLogger.js +15 -22
- package/dist/logging/StartupLogger.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +21 -3
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +4 -2
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +10 -0
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.js +18 -2
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +59 -15
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
- package/dist/resolvers/InfoResolver.js +1 -1
- package/dist/resolvers/InfoResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +2 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +12 -6
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +7 -8
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/TagGovernanceResolver.d.ts +12 -0
- package/dist/resolvers/TagGovernanceResolver.d.ts.map +1 -1
- package/dist/resolvers/TagGovernanceResolver.js +84 -0
- package/dist/resolvers/TagGovernanceResolver.js.map +1 -1
- package/dist/resolvers/TelemetryResolver.d.ts +13 -8
- package/dist/resolvers/TelemetryResolver.d.ts.map +1 -1
- package/dist/resolvers/TelemetryResolver.js +40 -9
- package/dist/resolvers/TelemetryResolver.js.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.js +34 -4
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +55 -2
- package/dist/util.js.map +1 -1
- package/package.json +107 -100
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +4 -0
- package/src/__tests__/ResolverBase.diagnosticSenderRegistrations.test.ts +26 -0
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +28 -4
- package/src/__tests__/TagGovernanceAndAutotagResolvers.test.ts +261 -0
- package/src/__tests__/TagGovernanceResolver.test.ts +60 -2
- package/src/__tests__/globalCors.test.ts +12 -0
- package/src/__tests__/review-capture-valid-token-log.test.ts +98 -0
- package/src/__tests__/runStreams.fieldSecurity.test.ts +5 -5
- package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +2 -1
- package/src/context.ts +7 -9
- package/src/generated/graphql-schemas/__mj.ts +9449 -6383
- package/src/generic/ResolverBase.ts +8 -0
- package/src/globalCors.ts +1 -0
- package/src/index.ts +45 -9
- package/src/logging/StartupLogger.ts +24 -0
- package/src/middleware/BaseServerMiddleware.ts +20 -3
- package/src/resolvers/AgentRunStatusPublisher.ts +27 -2
- package/src/resolvers/AutotagPipelineResolver.ts +57 -13
- package/src/resolvers/InfoResolver.ts +1 -1
- package/src/resolvers/QuerySystemUserResolver.ts +19 -7
- package/src/resolvers/SqlLoggingConfigResolver.ts +7 -9
- package/src/resolvers/TagGovernanceResolver.ts +65 -0
- package/src/resolvers/TelemetryResolver.ts +44 -9
- package/src/resolvers/meetings/meetingMutations.ts +37 -7
- package/src/util.ts +55 -3
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
mockTagEngineConfig,
|
|
5
|
+
mockMergeTags,
|
|
6
|
+
mockRecomputeCoOccurrence,
|
|
7
|
+
mockRunViewFn,
|
|
8
|
+
mockGetEntityObjectFn,
|
|
9
|
+
mockActionConfig,
|
|
10
|
+
mockRunAction,
|
|
11
|
+
mockPublish,
|
|
12
|
+
actionsList,
|
|
13
|
+
} = vi.hoisted(() => ({
|
|
14
|
+
mockTagEngineConfig: vi.fn().mockResolvedValue(undefined),
|
|
15
|
+
mockMergeTags: vi.fn(),
|
|
16
|
+
mockRecomputeCoOccurrence: vi.fn(),
|
|
17
|
+
mockRunViewFn: vi.fn(),
|
|
18
|
+
mockGetEntityObjectFn: vi.fn(),
|
|
19
|
+
mockActionConfig: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
mockRunAction: vi.fn(),
|
|
21
|
+
mockPublish: vi.fn(),
|
|
22
|
+
actionsList: [{ Name: 'Autotag and Vectorize Content' }] as Array<{ Name: string }>,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock('type-graphql', () => {
|
|
26
|
+
const noop = () => () => undefined;
|
|
27
|
+
const passthrough = () => (target: unknown) => target;
|
|
28
|
+
return {
|
|
29
|
+
Resolver: passthrough,
|
|
30
|
+
Mutation: noop,
|
|
31
|
+
Query: noop,
|
|
32
|
+
Subscription: noop,
|
|
33
|
+
Arg: noop,
|
|
34
|
+
Args: noop,
|
|
35
|
+
Ctx: noop,
|
|
36
|
+
Root: noop,
|
|
37
|
+
ObjectType: passthrough,
|
|
38
|
+
InputType: passthrough,
|
|
39
|
+
ArgsType: passthrough,
|
|
40
|
+
Field: noop,
|
|
41
|
+
Float: Number,
|
|
42
|
+
Int: Number,
|
|
43
|
+
ID: String,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
vi.mock('../generic/ResolverBase.js', () => ({
|
|
48
|
+
ResolverBase: class {
|
|
49
|
+
GetUserFromPayload(payload: unknown) {
|
|
50
|
+
if (!payload) return null;
|
|
51
|
+
return { ID: 'user-1', Email: 'u@example.com', Name: 'User' };
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
vi.mock('@memberjunction/tag-engine', () => ({
|
|
57
|
+
TagEngine: {
|
|
58
|
+
Instance: {
|
|
59
|
+
Config: (...args: unknown[]) => mockTagEngineConfig(...args),
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
TagGovernanceEngine: {
|
|
63
|
+
Instance: {
|
|
64
|
+
MergeTags: (...args: unknown[]) => mockMergeTags(...args),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
TagHealthJob: {
|
|
68
|
+
Instance: {
|
|
69
|
+
Run: vi.fn(),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
DEFAULT_TAG_HEALTH_THRESHOLDS: {
|
|
73
|
+
minCoOccurrence: 5,
|
|
74
|
+
minEmbeddingSimilarity: 0.85,
|
|
75
|
+
minNameSimilarity: 0.5,
|
|
76
|
+
maxUsage: 2,
|
|
77
|
+
maxImplicitChildren: 25,
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
vi.mock('@memberjunction/tag-engine-base', () => ({
|
|
82
|
+
TagCoOccurrenceEngine: {
|
|
83
|
+
Instance: {
|
|
84
|
+
RecomputeCoOccurrence: (...args: unknown[]) => mockRecomputeCoOccurrence(...args),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
vi.mock('@memberjunction/actions', () => ({
|
|
90
|
+
ActionEngineServer: {
|
|
91
|
+
Instance: {
|
|
92
|
+
Config: (...args: unknown[]) => mockActionConfig(...args),
|
|
93
|
+
get Actions() {
|
|
94
|
+
return actionsList;
|
|
95
|
+
},
|
|
96
|
+
RunAction: (...args: unknown[]) => mockRunAction(...args),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
vi.mock('../generic/PubSubManager.js', () => ({
|
|
102
|
+
PubSubManager: {
|
|
103
|
+
Instance: {
|
|
104
|
+
Publish: (...args: unknown[]) => mockPublish(...args),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
vi.mock('../util.js', () => ({
|
|
110
|
+
GetReadWriteProvider: () => ({
|
|
111
|
+
GetEntityObject: (...args: unknown[]) => mockGetEntityObjectFn(...args),
|
|
112
|
+
}),
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
vi.mock('@memberjunction/core', async (importOriginal) => {
|
|
116
|
+
const actual = await importOriginal<typeof import('@memberjunction/core')>();
|
|
117
|
+
class MockRunView {
|
|
118
|
+
RunView = mockRunViewFn;
|
|
119
|
+
static FromMetadataProvider() {
|
|
120
|
+
return new MockRunView();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
class MockMetadata {
|
|
124
|
+
GetEntityObject = mockGetEntityObjectFn;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...actual,
|
|
128
|
+
RunView: MockRunView,
|
|
129
|
+
Metadata: MockMetadata,
|
|
130
|
+
LogError: vi.fn(),
|
|
131
|
+
LogStatus: vi.fn(),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
import { TagGovernanceResolver } from '../resolvers/TagGovernanceResolver.js';
|
|
136
|
+
import { AutotagPipelineResolver } from '../resolvers/AutotagPipelineResolver.js';
|
|
137
|
+
import type { AppContext } from '../types.js';
|
|
138
|
+
|
|
139
|
+
const mockUser = { ID: 'user-1', Email: 'u@example.com', Name: 'User' };
|
|
140
|
+
|
|
141
|
+
describe('TagGovernanceResolver', () => {
|
|
142
|
+
let resolver: TagGovernanceResolver;
|
|
143
|
+
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
vi.clearAllMocks();
|
|
146
|
+
resolver = new TagGovernanceResolver();
|
|
147
|
+
(resolver as unknown as Record<string, unknown>)['GetUserFromPayload'] = vi.fn().mockReturnValue(mockUser);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('RecomputeTagCoOccurrence delegates to TagCoOccurrenceEngine.Instance.RecomputeCoOccurrence', async () => {
|
|
151
|
+
mockRecomputeCoOccurrence.mockResolvedValueOnce({ PairsUpdated: 12, PairsDeleted: 3 });
|
|
152
|
+
|
|
153
|
+
const res = await resolver.RecomputeTagCoOccurrence({} as AppContext);
|
|
154
|
+
|
|
155
|
+
expect(mockRecomputeCoOccurrence).toHaveBeenCalledWith(mockUser);
|
|
156
|
+
expect(res).toEqual({ PairsUpdated: 12, PairsDeleted: 3 });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('MergeTags calls TagEngine.Instance.Config first and returns ItemsMoved and TaggedItemsMoved', async () => {
|
|
160
|
+
mockMergeTags.mockResolvedValueOnce({ ItemsMoved: 4, TaggedItemsMoved: 7 });
|
|
161
|
+
|
|
162
|
+
const res = await resolver.MergeTags(['src-1'], 'surviving-1', {} as AppContext);
|
|
163
|
+
|
|
164
|
+
expect(mockTagEngineConfig).toHaveBeenCalledWith(false, mockUser);
|
|
165
|
+
expect(mockMergeTags).toHaveBeenCalledWith(['src-1'], 'surviving-1', mockUser);
|
|
166
|
+
expect(res).toEqual({
|
|
167
|
+
Success: true,
|
|
168
|
+
ItemsMoved: 4,
|
|
169
|
+
TaggedItemsMoved: 7,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('MergeTags catches engine errors and maps them into ErrorMessage', async () => {
|
|
174
|
+
mockMergeTags.mockRejectedValueOnce(new Error('Failed to load surviving tag'));
|
|
175
|
+
|
|
176
|
+
const res = await resolver.MergeTags(['src-1'], 'bad-id', {} as AppContext);
|
|
177
|
+
|
|
178
|
+
expect(res).toEqual({
|
|
179
|
+
Success: false,
|
|
180
|
+
ItemsMoved: 0,
|
|
181
|
+
TaggedItemsMoved: 0,
|
|
182
|
+
ErrorMessage: 'Failed to load surviving tag',
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('AutotagPipelineResolver', () => {
|
|
188
|
+
let resolver: AutotagPipelineResolver;
|
|
189
|
+
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
vi.clearAllMocks();
|
|
192
|
+
resolver = new AutotagPipelineResolver();
|
|
193
|
+
(resolver as unknown as Record<string, unknown>)['GetUserFromPayload'] = vi.fn().mockReturnValue(mockUser);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('rejects a client-supplied pipelineRunID that is not a valid UUID', async () => {
|
|
197
|
+
const res = await resolver.RunAutotagPipeline(undefined, undefined, 'not-a-uuid', {} as AppContext);
|
|
198
|
+
|
|
199
|
+
expect(res.Success).toBe(false);
|
|
200
|
+
expect(res.ErrorMessage).toMatch(/Invalid pipelineRunID/);
|
|
201
|
+
expect(mockRunViewFn).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('rejects a client-supplied pipelineRunID when a Content Process Run with that ID already exists', async () => {
|
|
205
|
+
const existingId = '11111111-2222-3333-4444-555555555555';
|
|
206
|
+
mockRunViewFn.mockResolvedValueOnce({
|
|
207
|
+
Success: true,
|
|
208
|
+
Results: [{ ID: existingId }],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const res = await resolver.RunAutotagPipeline(undefined, undefined, existingId, {} as AppContext);
|
|
212
|
+
|
|
213
|
+
expect(res.Success).toBe(false);
|
|
214
|
+
expect(res.ErrorMessage).toContain('already exists');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('passes processed item count (not pct) to publishProgress and clears heartbeatTimer in finally', async () => {
|
|
218
|
+
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
|
|
219
|
+
mockGetEntityObjectFn.mockResolvedValue({
|
|
220
|
+
NewRecord: vi.fn(),
|
|
221
|
+
Save: vi.fn().mockResolvedValue(true),
|
|
222
|
+
LatestResult: { CompleteMessage: '' },
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
mockRunAction.mockImplementationOnce(async ({ Params }: { Params: Array<{ Name: string; Value: unknown }> }) => {
|
|
226
|
+
const cbParam = Params.find(p => p.Name === '__progressCallback');
|
|
227
|
+
const cb = cbParam?.Value as ((processed: number, total: number, currentItem?: string) => void) | undefined;
|
|
228
|
+
cb?.(3, 10, 'https://example.com/page-3');
|
|
229
|
+
return { Success: true, Message: '' };
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const runInBg = (resolver as unknown as {
|
|
233
|
+
runPipelineInBackground(
|
|
234
|
+
pipelineRunID: string,
|
|
235
|
+
currentUser: unknown,
|
|
236
|
+
provider: unknown,
|
|
237
|
+
contentSourceIDs?: string[],
|
|
238
|
+
forceReprocess?: boolean,
|
|
239
|
+
): Promise<void>;
|
|
240
|
+
}).runPipelineInBackground.bind(resolver);
|
|
241
|
+
|
|
242
|
+
await runInBg(
|
|
243
|
+
'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
|
244
|
+
mockUser,
|
|
245
|
+
{ GetEntityObject: mockGetEntityObjectFn },
|
|
246
|
+
['22222222-3333-4444-5555-666666666666'],
|
|
247
|
+
false,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const autotagEvent = mockPublish.mock.calls
|
|
251
|
+
.map(c => c[1] as Record<string, unknown>)
|
|
252
|
+
.find(n => n['Stage'] === 'autotag' && n['TotalItems'] === 10);
|
|
253
|
+
|
|
254
|
+
expect(autotagEvent).toBeDefined();
|
|
255
|
+
expect(autotagEvent?.['ProcessedItems']).toBe(3);
|
|
256
|
+
expect(autotagEvent?.['PercentComplete']).toBe(30);
|
|
257
|
+
expect(clearIntervalSpy).toHaveBeenCalled();
|
|
258
|
+
clearIntervalSpy.mockRestore();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
@@ -10,12 +10,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
10
10
|
* how errors are surfaced.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
const { promoteCalls, rejectCalls, rebuildCalls, healthCalls, behaviorFlags } = vi.hoisted(() => ({
|
|
13
|
+
const { promoteCalls, rejectCalls, rebuildCalls, healthCalls, mergeCalls, recomputeCalls, configCalls, behaviorFlags } = vi.hoisted(() => ({
|
|
14
14
|
promoteCalls: [] as Array<{ id: string; strategy: unknown; user: unknown }>,
|
|
15
15
|
rejectCalls: [] as Array<{ id: string; notes: string | null; user: unknown }>,
|
|
16
16
|
rebuildCalls: [] as Array<{ user: unknown }>,
|
|
17
17
|
healthCalls: [] as Array<{ thresholds: unknown; user: unknown }>,
|
|
18
|
-
|
|
18
|
+
mergeCalls: [] as Array<{ sourceTagIDs: string[]; survivingTagID: string; user: unknown }>,
|
|
19
|
+
recomputeCalls: [] as Array<{ user: unknown }>,
|
|
20
|
+
configCalls: [] as Array<{ forceRefresh: boolean; user: unknown }>,
|
|
21
|
+
behaviorFlags: { promoteThrows: false, rejectThrows: false, rebuildThrows: false, healthThrows: false, mergeThrows: false },
|
|
19
22
|
}));
|
|
20
23
|
|
|
21
24
|
vi.mock('@memberjunction/tag-engine', () => ({
|
|
@@ -30,10 +33,18 @@ vi.mock('@memberjunction/tag-engine', () => ({
|
|
|
30
33
|
rejectCalls.push({ id, notes, user });
|
|
31
34
|
if (behaviorFlags.rejectThrows) throw new Error('reject-fail');
|
|
32
35
|
},
|
|
36
|
+
MergeTags: async (sourceTagIDs: string[], survivingTagID: string, user: unknown) => {
|
|
37
|
+
mergeCalls.push({ sourceTagIDs, survivingTagID, user });
|
|
38
|
+
if (behaviorFlags.mergeThrows) throw new Error('merge-fail');
|
|
39
|
+
return { ItemsMoved: 4, TaggedItemsMoved: 7 };
|
|
40
|
+
},
|
|
33
41
|
},
|
|
34
42
|
},
|
|
35
43
|
TagEngine: {
|
|
36
44
|
Instance: {
|
|
45
|
+
Config: async (forceRefresh: boolean, user: unknown) => {
|
|
46
|
+
configCalls.push({ forceRefresh, user });
|
|
47
|
+
},
|
|
37
48
|
RebuildTagEmbeddings: async (user: unknown) => {
|
|
38
49
|
rebuildCalls.push({ user });
|
|
39
50
|
if (behaviorFlags.rebuildThrows) throw new Error('rebuild-fail');
|
|
@@ -59,6 +70,17 @@ vi.mock('@memberjunction/tag-engine', () => ({
|
|
|
59
70
|
},
|
|
60
71
|
}));
|
|
61
72
|
|
|
73
|
+
vi.mock('@memberjunction/tag-engine-base', () => ({
|
|
74
|
+
TagCoOccurrenceEngine: {
|
|
75
|
+
Instance: {
|
|
76
|
+
RecomputeCoOccurrence: async (user: unknown) => {
|
|
77
|
+
recomputeCalls.push({ user });
|
|
78
|
+
return { PairsUpdated: 12, PairsDeleted: 3 };
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
|
|
62
84
|
// Minimal type-graphql shim — we only need the decorators to be no-ops so the
|
|
63
85
|
// resolver class can load. Method behavior is what we test.
|
|
64
86
|
vi.mock('type-graphql', () => ({
|
|
@@ -98,10 +120,14 @@ describe('TagGovernanceResolver', () => {
|
|
|
98
120
|
rejectCalls.length = 0;
|
|
99
121
|
rebuildCalls.length = 0;
|
|
100
122
|
healthCalls.length = 0;
|
|
123
|
+
mergeCalls.length = 0;
|
|
124
|
+
recomputeCalls.length = 0;
|
|
125
|
+
configCalls.length = 0;
|
|
101
126
|
behaviorFlags.promoteThrows = false;
|
|
102
127
|
behaviorFlags.rejectThrows = false;
|
|
103
128
|
behaviorFlags.rebuildThrows = false;
|
|
104
129
|
behaviorFlags.healthThrows = false;
|
|
130
|
+
behaviorFlags.mergeThrows = false;
|
|
105
131
|
resolver = new TagGovernanceResolver();
|
|
106
132
|
});
|
|
107
133
|
|
|
@@ -252,4 +278,36 @@ describe('TagGovernanceResolver', () => {
|
|
|
252
278
|
expect(r.ErrorMessage).toBe('health-fail');
|
|
253
279
|
});
|
|
254
280
|
});
|
|
281
|
+
|
|
282
|
+
// ---- MergeTags ---------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
describe('MergeTags', () => {
|
|
285
|
+
it('calls TagEngine.Instance.Config first and delegates to TagGovernanceEngine.Instance.MergeTags', async () => {
|
|
286
|
+
const r = await resolver.MergeTags(['src-1'], 'surviving-1', ctx() as never);
|
|
287
|
+
expect(r.Success).toBe(true);
|
|
288
|
+
expect(r.ItemsMoved).toBe(4);
|
|
289
|
+
expect(r.TaggedItemsMoved).toBe(7);
|
|
290
|
+
expect(configCalls).toHaveLength(1);
|
|
291
|
+
expect(mergeCalls[0]).toMatchObject({ sourceTagIDs: ['src-1'], survivingTagID: 'surviving-1' });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('maps engine errors into ErrorMessage', async () => {
|
|
295
|
+
behaviorFlags.mergeThrows = true;
|
|
296
|
+
const r = await resolver.MergeTags(['src-1'], 'surviving-1', ctx() as never);
|
|
297
|
+
expect(r.Success).toBe(false);
|
|
298
|
+
expect(r.ItemsMoved).toBe(0);
|
|
299
|
+
expect(r.TaggedItemsMoved).toBe(0);
|
|
300
|
+
expect(r.ErrorMessage).toBe('merge-fail');
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ---- RecomputeTagCoOccurrence ------------------------------------------
|
|
305
|
+
|
|
306
|
+
describe('RecomputeTagCoOccurrence', () => {
|
|
307
|
+
it('delegates to TagCoOccurrenceEngine.Instance.RecomputeCoOccurrence', async () => {
|
|
308
|
+
const r = await resolver.RecomputeTagCoOccurrence(ctx() as never);
|
|
309
|
+
expect(r).toEqual({ PairsUpdated: 12, PairsDeleted: 3 });
|
|
310
|
+
expect(recomputeCalls).toHaveLength(1);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
255
313
|
});
|
|
@@ -215,3 +215,15 @@ describe('X-Powered-By', () => {
|
|
|
215
215
|
expect(src.match(/app\.disable\('x-powered-by'\)/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
|
+
|
|
219
|
+
describe('X-MJ-Diagnostic-Trigger', () => {
|
|
220
|
+
it('is exposed to the browser by the global CORS and by the GraphQL CORS', async () => {
|
|
221
|
+
const base = await startApp([]);
|
|
222
|
+
const res = await fetch(`${base}/core`, { headers: { Origin: OTHER_ORIGIN } });
|
|
223
|
+
expect(res.headers.get('access-control-expose-headers')).toContain('X-MJ-Diagnostic-Trigger');
|
|
224
|
+
|
|
225
|
+
const src = readFileSync(new URL('../index.ts', import.meta.url), 'utf8');
|
|
226
|
+
const graphqlCors = src.match(/app\.use\(\s*graphqlRootPath,\s*cors<cors\.CorsRequest>\(\{\s*exposedHeaders: \[([^\]]*)\]/);
|
|
227
|
+
expect(graphqlCors?.[1]).toContain('X-MJ-Diagnostic-Trigger');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests the debug log GetUserPayload writes for a freshly verified token. All auth and database
|
|
3
|
+
* dependencies are mocked; no connection is opened.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
|
|
7
|
+
vi.mock('jsonwebtoken', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
decode: vi.fn(() => ({ iss: 'https://issuer.example.com/', exp: Math.floor(Date.now() / 1000) + 3600, sub: 'idp-subject' })),
|
|
10
|
+
verify: vi.fn((_token: string, _keys: unknown, _opts: unknown, cb: (err: Error | null, decoded: unknown) => void) =>
|
|
11
|
+
cb(null, { payload: { iss: 'https://issuer.example.com/', sub: 'idp-subject' } })
|
|
12
|
+
),
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('../auth/index.js', () => ({
|
|
17
|
+
GetSigningKeys: vi.fn(() => vi.fn()),
|
|
18
|
+
GetSystemUser: vi.fn(),
|
|
19
|
+
GetValidationOptions: vi.fn(() => ({ audience: 'mjapi' })),
|
|
20
|
+
ExtractUserInfoFromPayload: vi.fn(() => ({
|
|
21
|
+
fullName: 'Alice Smith',
|
|
22
|
+
firstName: 'Alice',
|
|
23
|
+
lastName: 'Smith',
|
|
24
|
+
email: 'alice@example.com',
|
|
25
|
+
preferredUsername: 'alice@example.com',
|
|
26
|
+
})),
|
|
27
|
+
VerifyUserRecord: vi.fn(async () => ({ ID: 'USER-ID-1', IsActive: true, Email: 'alice@example.com', UserRoles: [] })),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('@memberjunction/auth-providers', () => ({
|
|
31
|
+
TokenExpiredError: class extends Error {},
|
|
32
|
+
AuthProviderFactory: { Instance: { getByIssuer: vi.fn(() => ({})) } },
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock('../logging/StartupLogger.js', () => ({
|
|
36
|
+
StartupLogger: { resolveLevelFromConfig: vi.fn(() => 'debug') },
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock('../util.js', () => ({
|
|
40
|
+
GetReadOnlyDataSource: vi.fn(() => undefined),
|
|
41
|
+
GetReadWriteDataSource: vi.fn(() => undefined),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock('../config.js', () => ({
|
|
45
|
+
configInfo: { databaseSettings: { metadataCacheRefreshInterval: 0 } },
|
|
46
|
+
userEmailMap: {},
|
|
47
|
+
apiKey: 'test-api-key',
|
|
48
|
+
mj_core_schema: '__mj',
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
|
|
52
|
+
SQLServerDataProvider: class {},
|
|
53
|
+
SQLServerProviderConfigData: class {},
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
vi.mock('@memberjunction/postgresql-dataprovider', () => ({
|
|
57
|
+
PostgreSQLDataProvider: class {},
|
|
58
|
+
PostgreSQLProviderConfigData: class {},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
vi.mock('@memberjunction/api-keys', () => ({
|
|
62
|
+
GetAPIKeyEngine: vi.fn(),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
vi.mock('@memberjunction/core', async (importOriginal) => {
|
|
66
|
+
const actual = await importOriginal<typeof import('@memberjunction/core')>();
|
|
67
|
+
return { ...actual, Metadata: { Provider: undefined } };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
import { GetUserPayload } from '../context.js';
|
|
71
|
+
|
|
72
|
+
describe('GetUserPayload valid-token debug log', () => {
|
|
73
|
+
let logged: string[];
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
logged = [];
|
|
77
|
+
vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
|
|
78
|
+
logged.push(args.map(String).join(' '));
|
|
79
|
+
});
|
|
80
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
81
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
vi.restoreAllMocks();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('logs the user ID, not the name or email', async () => {
|
|
89
|
+
const result = await GetUserPayload('Bearer token-for-log-test', 'session-1', []);
|
|
90
|
+
|
|
91
|
+
expect(result.userRecord?.ID).toBe('USER-ID-1');
|
|
92
|
+
const tokenLines = logged.filter((line) => line.includes('Valid token'));
|
|
93
|
+
expect(tokenLines).toHaveLength(1);
|
|
94
|
+
expect(tokenLines[0]).toContain('USER-ID-1');
|
|
95
|
+
expect(tokenLines[0]).not.toContain('Alice');
|
|
96
|
+
expect(tokenLines[0]).not.toContain('alice@example.com');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Field-level security on the agent-run and test-run streams.
|
|
3
3
|
*
|
|
4
|
-
* Each of these publishes an entity object's
|
|
5
|
-
* every progress
|
|
6
|
-
* message. The objects come from long-running engines and can have
|
|
7
|
-
* identity, so the transport projects them for the recipient. The objects here are FULL WIDTH,
|
|
4
|
+
* Each of these publishes an entity object's values to the caller's session: the agent run on
|
|
5
|
+
* every progress message and in the final result, its summary on every streaming message, and the
|
|
6
|
+
* test run on every progress message. The objects come from long-running engines and can have
|
|
7
|
+
* been built for another identity, so the transport projects them for the recipient. The objects here are FULL WIDTH,
|
|
8
8
|
* and the recipient may not read `Salary`.
|
|
9
9
|
*/
|
|
10
10
|
import 'reflect-metadata';
|
|
@@ -64,8 +64,8 @@ describe('AgentRunStatusPublisher: the run is projected for the recipient', () =
|
|
|
64
64
|
expect(messages()).toHaveLength(2);
|
|
65
65
|
for (const message of messages()) {
|
|
66
66
|
expect(message).not.toContain(String(SECRET_SALARY));
|
|
67
|
-
expect(message).toContain('"Name":"Ada"');
|
|
68
67
|
}
|
|
68
|
+
expect(messages()[0]).toContain('"Name":"Ada"');
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
|
|
@@ -153,7 +153,8 @@ export const DEFAULT_HIGH_CHURN_SYSTEM_ENTITIES: readonly string[] = [
|
|
|
153
153
|
'MJ: AI Prompt Runs',
|
|
154
154
|
'MJ: AI Agent Runs',
|
|
155
155
|
'MJ: AI Agent Run Steps',
|
|
156
|
-
'MJ:
|
|
156
|
+
'MJ: Diagnostic Session Chunks',
|
|
157
|
+
'MJ: Diagnostic Bundles',
|
|
157
158
|
];
|
|
158
159
|
|
|
159
160
|
/**
|
package/src/context.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
|
|
|
21
21
|
import { v4 as uuidv4 } from 'uuid';
|
|
22
22
|
import e from 'express';
|
|
23
23
|
import type { RequestHandler, Request, Response, NextFunction } from 'express';
|
|
24
|
-
import { DatabaseProviderBase, UserInfo, type IMetadataProvider, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
|
|
24
|
+
import { DatabaseProviderBase, UserInfo, GetDiagnosticContext, type IMetadataProvider, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
|
|
25
25
|
import { SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
|
|
26
26
|
import { PostgreSQLDataProvider, PostgreSQLProviderConfigData } from '@memberjunction/postgresql-dataprovider';
|
|
27
27
|
import { Metadata, IsValidTimeZone, TIME_ZONE_HEADER } from '@memberjunction/core';
|
|
@@ -49,7 +49,7 @@ function isDebugLogLevel(): boolean {
|
|
|
49
49
|
if (isDebugLevelCache === undefined) {
|
|
50
50
|
isDebugLevelCache = StartupLogger.resolveLevelFromConfig() === 'debug';
|
|
51
51
|
}
|
|
52
|
-
return isDebugLevelCache;
|
|
52
|
+
return isDebugLevelCache || GetDiagnosticContext()?.sessionId != null;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const SESSION_AUDIT_TYPE = 'Session Established';
|
|
@@ -371,13 +371,6 @@ const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayloa
|
|
|
371
371
|
jwt.verify(token, GetSigningKeys(issuer), verifyOptions, (err, jwt) => {
|
|
372
372
|
if (jwt && typeof jwt !== 'string' && !err) {
|
|
373
373
|
const payload = jwt.payload ?? jwt;
|
|
374
|
-
|
|
375
|
-
// Per-request token confirmation — debug-only (one of the worst
|
|
376
|
-
// "constantly on" offenders on an authenticated server).
|
|
377
|
-
if (isDebugLogLevel()) {
|
|
378
|
-
const userInfo = ExtractUserInfoFromPayload(payload);
|
|
379
|
-
console.log(`Valid token: ${userInfo.fullName || 'Unknown'} (${userInfo.email || userInfo.preferredUsername || 'Unknown'})`);
|
|
380
|
-
}
|
|
381
374
|
resolve(payload);
|
|
382
375
|
} else {
|
|
383
376
|
console.warn('Invalid token');
|
|
@@ -578,6 +571,11 @@ export const GetUserPayload = async (
|
|
|
578
571
|
// for everything else.
|
|
579
572
|
const sessionUser = BuildMagicLinkSessionUser(userRecord, payload);
|
|
580
573
|
|
|
574
|
+
// Debug-only confirmation of a freshly verified token. It names the user by ID only.
|
|
575
|
+
if (!cachedAuth && isDebugLogLevel()) {
|
|
576
|
+
console.log(`Valid token: user ${sessionUser.ID}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
581
579
|
// Session-established audit — once per issued token (deduped), fire-and-forget so
|
|
582
580
|
// it never adds latency to the request. The dedup mark is set synchronously here.
|
|
583
581
|
if (markSessionAuditSeen(sessionAuditKey('sess', payload))) {
|