@memberjunction/server 5.24.0 → 5.26.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/agents/skip-sdk.d.ts +12 -0
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +70 -1
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/config.d.ts +70 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +498 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2755 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts +15 -0
- package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -0
- package/dist/resolvers/ArtifactFileResolver.js +74 -0
- package/dist/resolvers/ArtifactFileResolver.js.map +1 -0
- package/dist/resolvers/AutotagPipelineResolver.d.ts +13 -0
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +103 -3
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
- package/dist/resolvers/CacheStatsResolver.d.ts +31 -0
- package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -0
- package/dist/resolvers/CacheStatsResolver.js +181 -0
- package/dist/resolvers/CacheStatsResolver.js.map +1 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +12 -32
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GeoResolver.d.ts +58 -0
- package/dist/resolvers/GeoResolver.d.ts.map +1 -0
- package/dist/resolvers/GeoResolver.js +302 -0
- package/dist/resolvers/GeoResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts +13 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +115 -20
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +21 -80
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +129 -604
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts +19 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +149 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -0
- package/package.json +66 -63
- package/src/__tests__/search-knowledge-tags.test.ts +177 -337
- package/src/__tests__/skip-sdk-organic-keys.test.ts +274 -0
- package/src/agents/skip-sdk.ts +83 -2
- package/src/config.ts +24 -0
- package/src/generated/generated.ts +1902 -1
- package/src/index.ts +18 -2
- package/src/resolvers/ArtifactFileResolver.ts +71 -0
- package/src/resolvers/AutotagPipelineResolver.ts +118 -4
- package/src/resolvers/CacheStatsResolver.ts +142 -0
- package/src/resolvers/FileResolver.ts +12 -41
- package/src/resolvers/GeoResolver.ts +258 -0
- package/src/resolvers/RunAIAgentResolver.ts +137 -23
- package/src/resolvers/SearchKnowledgeResolver.ts +114 -715
- package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +138 -0
|
@@ -1,415 +1,255 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Tests for SearchKnowledgeResolver —
|
|
5
|
-
*
|
|
4
|
+
* Tests for SearchKnowledgeResolver — the thin wrapper that delegates
|
|
5
|
+
* to SearchEngine.Instance.Search() and maps results to GraphQL types.
|
|
6
6
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
* and accessing them via prototype / casting tricks.
|
|
7
|
+
* The actual search logic (providers, fusion, enrichment, permissions)
|
|
8
|
+
* is tested in the @memberjunction/search-engine package.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
// ── Mocks ──────────────────────────────────────────────────────────────
|
|
13
12
|
|
|
14
|
-
const
|
|
13
|
+
const { mockSearch, mockPreviewSearch } = vi.hoisted(() => ({
|
|
14
|
+
mockSearch: vi.fn(),
|
|
15
|
+
mockPreviewSearch: vi.fn(),
|
|
16
|
+
}));
|
|
15
17
|
|
|
16
|
-
vi.mock('@memberjunction/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return val;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return { Success: true, Results: [] };
|
|
18
|
+
vi.mock('@memberjunction/search-engine', () => ({
|
|
19
|
+
SearchEngine: {
|
|
20
|
+
Instance: {
|
|
21
|
+
Config: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
Search: (...args: unknown[]) => mockSearch(...args),
|
|
23
|
+
PreviewSearch: (...args: unknown[]) => mockPreviewSearch(...args),
|
|
26
24
|
}
|
|
27
25
|
}
|
|
26
|
+
}));
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Icon: 'fa-solid fa-user',
|
|
34
|
-
NameField: { Name: 'FullName', IsNameField: true, Sequence: 1 },
|
|
35
|
-
Fields: [
|
|
36
|
-
{ Name: 'ID', IsNameField: false, IsPrimaryKey: true, Sequence: 0, TSType: 'string' },
|
|
37
|
-
{ Name: 'FirstName', IsNameField: true, Sequence: 1, TSType: 'string', IsPrimaryKey: false },
|
|
38
|
-
{ Name: 'LastName', IsNameField: true, Sequence: 2, TSType: 'string', IsPrimaryKey: false },
|
|
39
|
-
{ Name: 'Email', IsNameField: false, Sequence: 3, TSType: 'string', IsPrimaryKey: false },
|
|
40
|
-
]
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
Name: 'Companies',
|
|
44
|
-
ID: 'entity-companies-id',
|
|
45
|
-
Icon: 'fa-solid fa-building',
|
|
46
|
-
NameField: { Name: 'Name', IsNameField: true, Sequence: 1 },
|
|
47
|
-
Fields: [
|
|
48
|
-
{ Name: 'ID', IsNameField: false, IsPrimaryKey: true, Sequence: 0, TSType: 'string' },
|
|
49
|
-
{ Name: 'Name', IsNameField: true, Sequence: 1, TSType: 'string', IsPrimaryKey: false },
|
|
50
|
-
]
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
Name: 'Events',
|
|
54
|
-
ID: 'entity-events-id',
|
|
55
|
-
Icon: null,
|
|
56
|
-
NameField: null,
|
|
57
|
-
Fields: [
|
|
58
|
-
{ Name: 'ID', IsNameField: false, IsPrimaryKey: true, Sequence: 0, TSType: 'string' },
|
|
59
|
-
{ Name: 'EventCode', IsNameField: false, Sequence: 1, TSType: 'number', IsPrimaryKey: false },
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
class MockMetadata {
|
|
65
|
-
Entities = mockEntities;
|
|
66
|
-
}
|
|
28
|
+
vi.mock('@memberjunction/global', async (importOriginal) => {
|
|
29
|
+
const actual = await importOriginal<Record<string, unknown>>();
|
|
30
|
+
return { ...actual };
|
|
31
|
+
});
|
|
67
32
|
|
|
33
|
+
vi.mock('@memberjunction/core', async (importOriginal) => {
|
|
34
|
+
const actual = await importOriginal<Record<string, unknown>>();
|
|
68
35
|
return {
|
|
69
|
-
|
|
70
|
-
Metadata: MockMetadata,
|
|
36
|
+
...actual,
|
|
71
37
|
LogError: vi.fn(),
|
|
72
38
|
LogStatus: vi.fn(),
|
|
73
|
-
UserInfo: class {},
|
|
74
|
-
ComputeRRF: vi.fn(),
|
|
75
|
-
ScoredCandidate: class {},
|
|
76
|
-
EntityRecordNameInput: class {},
|
|
77
|
-
CompositeKey: class { LoadFromURLSegment = vi.fn(); },
|
|
78
39
|
};
|
|
79
40
|
});
|
|
80
41
|
|
|
81
|
-
vi.mock('@memberjunction/core-entities', () =>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Instance: {
|
|
86
|
-
Config: vi.fn(),
|
|
87
|
-
ContentSourceTypes: [],
|
|
88
|
-
ContentSources: [],
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
|
-
vi.mock('@memberjunction/ai', () => ({
|
|
94
|
-
GetAIAPIKey: vi.fn(() => 'mock-key'),
|
|
95
|
-
BaseEmbeddings: class {},
|
|
96
|
-
}));
|
|
97
|
-
|
|
98
|
-
vi.mock('@memberjunction/ai-vectordb', () => ({
|
|
99
|
-
VectorDBBase: class {},
|
|
100
|
-
BaseResponse: class {},
|
|
101
|
-
}));
|
|
42
|
+
vi.mock('@memberjunction/core-entities', async (importOriginal) => {
|
|
43
|
+
const actual = await importOriginal<Record<string, unknown>>();
|
|
44
|
+
return { ...actual };
|
|
45
|
+
});
|
|
102
46
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
47
|
+
// Mock type-graphql to avoid reflect-metadata dependency
|
|
48
|
+
vi.mock('type-graphql', () => {
|
|
49
|
+
const noop = () => () => undefined;
|
|
50
|
+
const passthrough = () => (target: unknown) => target;
|
|
51
|
+
return {
|
|
52
|
+
Resolver: passthrough,
|
|
53
|
+
Mutation: noop,
|
|
54
|
+
Query: noop,
|
|
55
|
+
Subscription: noop,
|
|
56
|
+
Arg: noop,
|
|
57
|
+
Args: noop,
|
|
58
|
+
Ctx: noop,
|
|
59
|
+
ObjectType: passthrough,
|
|
60
|
+
InputType: passthrough,
|
|
61
|
+
ArgsType: passthrough,
|
|
62
|
+
Field: noop,
|
|
63
|
+
Float: Number,
|
|
64
|
+
Int: Number,
|
|
65
|
+
ID: String,
|
|
66
|
+
Authorized: noop,
|
|
67
|
+
PubSub: noop,
|
|
68
|
+
Root: noop,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
106
71
|
|
|
107
|
-
|
|
108
|
-
MJGlobal: { Instance: { ClassFactory: { CreateInstance: vi.fn() } } },
|
|
109
|
-
UUIDsEqual: (a: string, b: string) => a?.toLowerCase() === b?.toLowerCase(),
|
|
110
|
-
}));
|
|
72
|
+
// ── Import after mocks ─────────────────────────────────────────────────
|
|
111
73
|
|
|
112
|
-
|
|
113
|
-
Resolver: () => (target: unknown) => target,
|
|
114
|
-
Query: () => (target: unknown, key: string, descriptor: PropertyDescriptor) => descriptor,
|
|
115
|
-
Mutation: () => (target: unknown, key: string, descriptor: PropertyDescriptor) => descriptor,
|
|
116
|
-
Arg: () => (target: unknown, key: string, index: number) => {},
|
|
117
|
-
Ctx: () => (target: unknown, key: string, index: number) => {},
|
|
118
|
-
ObjectType: () => (target: unknown) => target,
|
|
119
|
-
Field: () => (target: unknown, key: string) => {},
|
|
120
|
-
Float: Number,
|
|
121
|
-
Int: Number,
|
|
122
|
-
InputType: () => (target: unknown) => target,
|
|
123
|
-
}));
|
|
74
|
+
import { SearchKnowledgeResolver } from '../resolvers/SearchKnowledgeResolver.js';
|
|
124
75
|
|
|
125
|
-
|
|
126
|
-
ResolverBase: class {
|
|
127
|
-
GetUserFromPayload() { return {}; }
|
|
128
|
-
},
|
|
129
|
-
}));
|
|
76
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
130
77
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
78
|
+
function createResolver(): SearchKnowledgeResolver {
|
|
79
|
+
const resolver = new SearchKnowledgeResolver();
|
|
80
|
+
// Mock GetUserFromPayload to return a fake user
|
|
81
|
+
(resolver as Record<string, unknown>)['GetUserFromPayload'] = vi.fn().mockReturnValue({
|
|
82
|
+
ID: 'test-user-id',
|
|
83
|
+
Email: 'test@test.com',
|
|
84
|
+
Name: 'Test User'
|
|
85
|
+
});
|
|
86
|
+
return resolver;
|
|
87
|
+
}
|
|
134
88
|
|
|
135
|
-
|
|
89
|
+
function createMockSearchResult(overrides: Record<string, unknown> = {}) {
|
|
90
|
+
return {
|
|
91
|
+
Success: true,
|
|
92
|
+
Results: [
|
|
93
|
+
{
|
|
94
|
+
ID: 'result-1',
|
|
95
|
+
EntityName: 'Contacts',
|
|
96
|
+
RecordID: 'abc-123',
|
|
97
|
+
SourceType: 'entity',
|
|
98
|
+
ResultType: 'entity-record',
|
|
99
|
+
Title: 'John Smith',
|
|
100
|
+
Snippet: 'A contact record',
|
|
101
|
+
Score: 0.85,
|
|
102
|
+
ScoreBreakdown: { Entity: 0.85 },
|
|
103
|
+
Tags: ['VIP'],
|
|
104
|
+
EntityIcon: 'fa-solid fa-user',
|
|
105
|
+
RecordName: 'John Smith',
|
|
106
|
+
MatchedAt: new Date(),
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
TotalCount: 1,
|
|
110
|
+
ElapsedMs: 42,
|
|
111
|
+
SourceCounts: { Vector: 0, FullText: 0, Entity: 1, Storage: 0 },
|
|
112
|
+
...overrides
|
|
113
|
+
};
|
|
114
|
+
}
|
|
136
115
|
|
|
137
|
-
|
|
116
|
+
const fakeContext = { userPayload: { email: 'test@test.com' } };
|
|
138
117
|
|
|
139
|
-
//
|
|
140
|
-
function getPrivateMethod<T>(instance: T, method: string): (...args: unknown[]) => unknown {
|
|
141
|
-
return (instance as Record<string, unknown>)[method] as (...args: unknown[]) => unknown;
|
|
142
|
-
}
|
|
118
|
+
// ── Tests ───────────────────────────────────────────────────────────────
|
|
143
119
|
|
|
144
120
|
describe('SearchKnowledgeResolver', () => {
|
|
145
|
-
let resolver: SearchKnowledgeResolver;
|
|
146
|
-
|
|
147
121
|
beforeEach(() => {
|
|
148
|
-
|
|
149
|
-
mockRunViewResults.clear();
|
|
122
|
+
vi.clearAllMocks();
|
|
150
123
|
});
|
|
151
124
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
getPrivateMethod(resolver, 'extractDisplayTitle')(meta, fallback) as string;
|
|
157
|
-
|
|
158
|
-
it('should combine multiple IsNameField values for a multi-name entity', () => {
|
|
159
|
-
const meta = { Entity: 'Contacts', FirstName: 'Sarah', LastName: 'Chen' };
|
|
160
|
-
const title = extractDisplayTitle(meta, 'Contacts');
|
|
161
|
-
expect(title).toBe('Sarah Chen');
|
|
162
|
-
});
|
|
125
|
+
describe('SearchKnowledge mutation', () => {
|
|
126
|
+
it('should delegate to SearchEngine.Instance.Search()', async () => {
|
|
127
|
+
const resolver = createResolver();
|
|
128
|
+
mockSearch.mockResolvedValueOnce(createMockSearchResult());
|
|
163
129
|
|
|
164
|
-
|
|
165
|
-
const meta = { Entity: 'Companies', Name: 'Acme Corp' };
|
|
166
|
-
const title = extractDisplayTitle(meta, 'Companies');
|
|
167
|
-
// Companies has Name as IsNameField, so it should use it
|
|
168
|
-
expect(title).toBe('Acme Corp');
|
|
169
|
-
});
|
|
130
|
+
const result = await resolver.SearchKnowledge('test query', 20, undefined, undefined, fakeContext as never);
|
|
170
131
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
expect(
|
|
132
|
+
expect(mockSearch).toHaveBeenCalledOnce();
|
|
133
|
+
expect(result.Success).toBe(true);
|
|
134
|
+
expect(result.TotalCount).toBe(1);
|
|
135
|
+
expect(result.Results).toHaveLength(1);
|
|
175
136
|
});
|
|
176
137
|
|
|
177
|
-
it('should
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
expect(title).toBe('Widget A');
|
|
181
|
-
});
|
|
138
|
+
it('should pass query and maxResults to SearchEngine', async () => {
|
|
139
|
+
const resolver = createResolver();
|
|
140
|
+
mockSearch.mockResolvedValueOnce(createMockSearchResult());
|
|
182
141
|
|
|
183
|
-
|
|
184
|
-
const meta = { Entity: 'NonExistent', Subject: 'RE: Meeting' };
|
|
185
|
-
const title = extractDisplayTitle(meta, 'NonExistent');
|
|
186
|
-
expect(title).toBe('RE: Meeting');
|
|
187
|
-
});
|
|
142
|
+
await resolver.SearchKnowledge('cheese', 50, undefined, undefined, fakeContext as never);
|
|
188
143
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
expect(title).toBe('Events Record');
|
|
144
|
+
const searchParams = mockSearch.mock.calls[0][0];
|
|
145
|
+
expect(searchParams.Query).toBe('cheese');
|
|
146
|
+
expect(searchParams.MaxResults).toBe(50);
|
|
193
147
|
});
|
|
194
148
|
|
|
195
|
-
it('should
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
expect(title).toBe('Rodriguez');
|
|
199
|
-
});
|
|
149
|
+
it('should pass filters when provided', async () => {
|
|
150
|
+
const resolver = createResolver();
|
|
151
|
+
mockSearch.mockResolvedValueOnce(createMockSearchResult());
|
|
200
152
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const title = extractDisplayTitle(meta, 'SomeEntity');
|
|
204
|
-
expect(title).toBe('Fallback Name');
|
|
205
|
-
});
|
|
153
|
+
const filters = { EntityNames: ['Contacts'], SourceTypes: undefined, Tags: ['VIP'] };
|
|
154
|
+
await resolver.SearchKnowledge('test', 20, filters as never, undefined, fakeContext as never);
|
|
206
155
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
156
|
+
const searchParams = mockSearch.mock.calls[0][0];
|
|
157
|
+
expect(searchParams.Filters).toEqual({
|
|
158
|
+
EntityNames: ['Contacts'],
|
|
159
|
+
SourceTypes: undefined,
|
|
160
|
+
Tags: ['VIP']
|
|
161
|
+
});
|
|
211
162
|
});
|
|
212
|
-
});
|
|
213
163
|
|
|
214
|
-
|
|
164
|
+
it('should pass minScore when provided', async () => {
|
|
165
|
+
const resolver = createResolver();
|
|
166
|
+
mockSearch.mockResolvedValueOnce(createMockSearchResult());
|
|
215
167
|
|
|
216
|
-
|
|
217
|
-
const extractDisplaySnippet = (meta: Record<string, unknown>, indexName: string, score?: number) =>
|
|
218
|
-
getPrivateMethod(resolver, 'extractDisplaySnippet')(meta, indexName, score) as string;
|
|
168
|
+
await resolver.SearchKnowledge('test', 20, undefined, 0.5, fakeContext as never);
|
|
219
169
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
expect(extractDisplaySnippet(meta, 'idx-1', 0.95)).toBe('A detailed description of the record');
|
|
170
|
+
const searchParams = mockSearch.mock.calls[0][0];
|
|
171
|
+
expect(searchParams.MinScore).toBe(0.5);
|
|
223
172
|
});
|
|
224
173
|
|
|
225
|
-
it('should
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
const snippet = extractDisplaySnippet(meta, 'idx-1');
|
|
229
|
-
expect(snippet.length).toBe(203); // 200 + '...'
|
|
230
|
-
expect(snippet.endsWith('...')).toBe(true);
|
|
231
|
-
});
|
|
174
|
+
it('should map result fields correctly', async () => {
|
|
175
|
+
const resolver = createResolver();
|
|
176
|
+
mockSearch.mockResolvedValueOnce(createMockSearchResult());
|
|
232
177
|
|
|
233
|
-
|
|
234
|
-
const meta = { Status: 'Active', Type: 'Premium' };
|
|
235
|
-
const snippet = extractDisplaySnippet(meta, 'idx-1');
|
|
236
|
-
expect(snippet).toContain('Status: Active');
|
|
237
|
-
expect(snippet).toContain('Type: Premium');
|
|
238
|
-
});
|
|
178
|
+
const result = await resolver.SearchKnowledge('test', 20, undefined, undefined, fakeContext as never);
|
|
239
179
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
expect(
|
|
244
|
-
expect(
|
|
180
|
+
const item = result.Results[0];
|
|
181
|
+
expect(item.ID).toBe('result-1');
|
|
182
|
+
expect(item.EntityName).toBe('Contacts');
|
|
183
|
+
expect(item.RecordID).toBe('abc-123');
|
|
184
|
+
expect(item.Title).toBe('John Smith');
|
|
185
|
+
expect(item.Score).toBe(0.85);
|
|
186
|
+
expect(item.Tags).toEqual(['VIP']);
|
|
187
|
+
expect(item.EntityIcon).toBe('fa-solid fa-user');
|
|
245
188
|
});
|
|
246
|
-
});
|
|
247
189
|
|
|
248
|
-
|
|
190
|
+
it('should map source counts correctly', async () => {
|
|
191
|
+
const resolver = createResolver();
|
|
192
|
+
mockSearch.mockResolvedValueOnce(createMockSearchResult({
|
|
193
|
+
SourceCounts: { Vector: 5, FullText: 3, Entity: 10, Storage: 0 }
|
|
194
|
+
}));
|
|
249
195
|
|
|
250
|
-
|
|
251
|
-
it('should extract Tags array when present and is an array', () => {
|
|
252
|
-
const meta: Record<string, unknown> = { Tags: ['AI', 'Machine Learning'] };
|
|
253
|
-
const tags = Array.isArray(meta['Tags']) ? (meta['Tags'] as string[]) : [];
|
|
254
|
-
expect(tags).toEqual(['AI', 'Machine Learning']);
|
|
255
|
-
});
|
|
196
|
+
const result = await resolver.SearchKnowledge('test', 20, undefined, undefined, fakeContext as never);
|
|
256
197
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
expect(tags).toEqual([]);
|
|
198
|
+
expect(result.SourceCounts.Vector).toBe(5);
|
|
199
|
+
expect(result.SourceCounts.FullText).toBe(3);
|
|
200
|
+
expect(result.SourceCounts.Entity).toBe(10);
|
|
261
201
|
});
|
|
262
202
|
|
|
263
|
-
it('should
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
expect(tags).toEqual([]);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('should return empty array when metadata is empty', () => {
|
|
270
|
-
const meta: Record<string, unknown> = {};
|
|
271
|
-
const tags = Array.isArray(meta['Tags']) ? (meta['Tags'] as string[]) : [];
|
|
272
|
-
expect(tags).toEqual([]);
|
|
273
|
-
});
|
|
274
|
-
});
|
|
203
|
+
it('should pass empty query through to SearchEngine (validation happens there)', async () => {
|
|
204
|
+
const resolver = createResolver();
|
|
205
|
+
mockSearch.mockResolvedValueOnce(createMockSearchResult({ Success: false, ErrorMessage: 'Query cannot be empty' }));
|
|
275
206
|
|
|
276
|
-
|
|
207
|
+
const result = await resolver.SearchKnowledge(' ', 20, undefined, undefined, fakeContext as never);
|
|
277
208
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
results: Array<{ EntityName: string; RecordID: string; Tags: string[] }>,
|
|
281
|
-
md: { Entities: Array<{ Name: string; ID: string }> },
|
|
282
|
-
contextUser: unknown
|
|
283
|
-
) => getPrivateMethod(resolver, 'enrichResultsWithTags')(results, md, contextUser) as Promise<void>;
|
|
284
|
-
|
|
285
|
-
it('should not modify results when results array is empty', async () => {
|
|
286
|
-
const results: Array<{ EntityName: string; RecordID: string; Tags: string[] }> = [];
|
|
287
|
-
const md = { Entities: [{ Name: 'Contacts', ID: 'eid-1' }] };
|
|
288
|
-
await enrichResultsWithTags(results, md, {});
|
|
289
|
-
expect(results).toEqual([]);
|
|
209
|
+
expect(mockSearch).toHaveBeenCalled();
|
|
210
|
+
expect(result.Success).toBe(false);
|
|
290
211
|
});
|
|
291
212
|
|
|
292
|
-
it('should
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
await enrichResultsWithTags(results, md, {});
|
|
296
|
-
expect(results[0].Tags).toEqual([]);
|
|
297
|
-
});
|
|
213
|
+
it('should return error when user cannot be determined', async () => {
|
|
214
|
+
const resolver = new SearchKnowledgeResolver();
|
|
215
|
+
(resolver as Record<string, unknown>)['GetUserFromPayload'] = vi.fn().mockReturnValue(null);
|
|
298
216
|
|
|
299
|
-
|
|
300
|
-
const results = [
|
|
301
|
-
{ EntityName: 'Contacts', RecordID: 'rec-1', Tags: [] as string[] },
|
|
302
|
-
{ EntityName: 'Contacts', RecordID: 'rec-2', Tags: [] as string[] },
|
|
303
|
-
];
|
|
304
|
-
|
|
305
|
-
// Set up mock RunView to return tagged items
|
|
306
|
-
// loadTaggedItemTags queries 'MJ: Tagged Items' with EntityID+RecordID filter
|
|
307
|
-
mockRunViewResults.set('MJ: Tagged Items', {
|
|
308
|
-
Success: true,
|
|
309
|
-
Results: [
|
|
310
|
-
{ EntityID: 'entity-contacts-id', RecordID: 'rec-1', Tag: 'VIP' },
|
|
311
|
-
{ EntityID: 'entity-contacts-id', RecordID: 'rec-1', Tag: 'Partner' },
|
|
312
|
-
{ EntityID: 'entity-contacts-id', RecordID: 'rec-2', Tag: 'Prospect' },
|
|
313
|
-
]
|
|
314
|
-
});
|
|
217
|
+
const result = await resolver.SearchKnowledge('test', 20, undefined, undefined, fakeContext as never);
|
|
315
218
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const { Metadata: MetadataCtor } = await import('@memberjunction/core');
|
|
319
|
-
const md = new MetadataCtor();
|
|
320
|
-
await loadTaggedItemTags(results, md, {});
|
|
321
|
-
expect(results[0].Tags).toEqual(['VIP', 'Partner']);
|
|
322
|
-
expect(results[1].Tags).toEqual(['Prospect']);
|
|
219
|
+
expect(result.Success).toBe(false);
|
|
220
|
+
expect(result.ErrorMessage).toContain('current user');
|
|
323
221
|
});
|
|
324
222
|
|
|
325
|
-
it('should
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
];
|
|
329
|
-
const md = { Entities: [{ Name: 'Contacts', ID: 'eid-contacts' }] };
|
|
223
|
+
it('should handle SearchEngine errors gracefully', async () => {
|
|
224
|
+
const resolver = createResolver();
|
|
225
|
+
mockSearch.mockRejectedValueOnce(new Error('Connection failed'));
|
|
330
226
|
|
|
331
|
-
|
|
332
|
-
Success: false,
|
|
333
|
-
Results: []
|
|
334
|
-
});
|
|
227
|
+
const result = await resolver.SearchKnowledge('test', 20, undefined, undefined, fakeContext as never);
|
|
335
228
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// Tags should remain unchanged since RunView failed before tag assignment
|
|
339
|
-
expect(results[0].Tags).toEqual(['Existing']);
|
|
229
|
+
expect(result.Success).toBe(false);
|
|
230
|
+
expect(result.ErrorMessage).toContain('Connection failed');
|
|
340
231
|
});
|
|
341
232
|
});
|
|
342
233
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
getPrivateMethod(resolver, 'deduplicateResults')(results) as Array<{ EntityName: string; RecordID: string; Score: number }>;
|
|
348
|
-
|
|
349
|
-
it('should keep the highest-scored entry for duplicate entity+recordID', () => {
|
|
350
|
-
const results = [
|
|
351
|
-
{ EntityName: 'Contacts', RecordID: 'r1', Score: 0.8 },
|
|
352
|
-
{ EntityName: 'Contacts', RecordID: 'r1', Score: 0.95 },
|
|
353
|
-
{ EntityName: 'Contacts', RecordID: 'r1', Score: 0.7 },
|
|
354
|
-
];
|
|
355
|
-
const deduped = deduplicateResults(results);
|
|
356
|
-
expect(deduped).toHaveLength(1);
|
|
357
|
-
expect(deduped[0].Score).toBe(0.95);
|
|
358
|
-
});
|
|
234
|
+
describe('PreviewSearch mutation', () => {
|
|
235
|
+
it('should delegate to SearchEngine.Instance.PreviewSearch()', async () => {
|
|
236
|
+
const resolver = createResolver();
|
|
237
|
+
mockPreviewSearch.mockResolvedValueOnce(createMockSearchResult());
|
|
359
238
|
|
|
360
|
-
|
|
361
|
-
const results = [
|
|
362
|
-
{ EntityName: 'Contacts', RecordID: 'r1', Score: 0.8 },
|
|
363
|
-
{ EntityName: 'Companies', RecordID: 'r1', Score: 0.7 },
|
|
364
|
-
];
|
|
365
|
-
const deduped = deduplicateResults(results);
|
|
366
|
-
expect(deduped).toHaveLength(2);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it('should sort results by score descending', () => {
|
|
370
|
-
const results = [
|
|
371
|
-
{ EntityName: 'A', RecordID: 'r1', Score: 0.3 },
|
|
372
|
-
{ EntityName: 'B', RecordID: 'r2', Score: 0.9 },
|
|
373
|
-
{ EntityName: 'C', RecordID: 'r3', Score: 0.6 },
|
|
374
|
-
];
|
|
375
|
-
const deduped = deduplicateResults(results);
|
|
376
|
-
expect(deduped[0].Score).toBe(0.9);
|
|
377
|
-
expect(deduped[1].Score).toBe(0.6);
|
|
378
|
-
expect(deduped[2].Score).toBe(0.3);
|
|
379
|
-
});
|
|
239
|
+
const result = await resolver.PreviewSearch('test', 8, fakeContext as never);
|
|
380
240
|
|
|
381
|
-
|
|
382
|
-
expect(
|
|
241
|
+
expect(mockPreviewSearch).toHaveBeenCalledOnce();
|
|
242
|
+
expect(mockPreviewSearch).toHaveBeenCalledWith('test', 8, expect.anything());
|
|
243
|
+
expect(result.Success).toBe(true);
|
|
383
244
|
});
|
|
384
|
-
});
|
|
385
245
|
|
|
386
|
-
|
|
246
|
+
it('should default maxResults to 8', async () => {
|
|
247
|
+
const resolver = createResolver();
|
|
248
|
+
mockPreviewSearch.mockResolvedValueOnce(createMockSearchResult());
|
|
387
249
|
|
|
388
|
-
|
|
389
|
-
const buildPineconeFilter = (filters?: { EntityNames?: string[]; SourceTypes?: string[]; Tags?: string[] }) =>
|
|
390
|
-
getPrivateMethod(resolver, 'buildPineconeFilter')(filters) as object | undefined;
|
|
250
|
+
await resolver.PreviewSearch('test', 8, fakeContext as never);
|
|
391
251
|
|
|
392
|
-
|
|
393
|
-
expect(buildPineconeFilter(undefined)).toBeUndefined();
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('should return undefined for empty filters', () => {
|
|
397
|
-
expect(buildPineconeFilter({})).toBeUndefined();
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it('should return single condition without $and wrapper', () => {
|
|
401
|
-
const result = buildPineconeFilter({ EntityNames: ['Contacts'] });
|
|
402
|
-
expect(result).toEqual({ Entity: { $in: ['Contacts'] } });
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it('should combine multiple filters with $and', () => {
|
|
406
|
-
const result = buildPineconeFilter({
|
|
407
|
-
EntityNames: ['Contacts'],
|
|
408
|
-
Tags: ['VIP']
|
|
409
|
-
}) as Record<string, unknown>;
|
|
410
|
-
expect(result).toHaveProperty('$and');
|
|
411
|
-
const conditions = result['$and'] as object[];
|
|
412
|
-
expect(conditions).toHaveLength(2);
|
|
252
|
+
expect(mockPreviewSearch).toHaveBeenCalledWith('test', 8, expect.anything());
|
|
413
253
|
});
|
|
414
254
|
});
|
|
415
255
|
});
|