@gblikas/querykit 0.2.0 → 0.3.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.
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Tests for Suggestions & Recovery features of parseWithContext:
3
+ * - Autocomplete suggestions
4
+ * - Error recovery hints
5
+ */
6
+
7
+ import { QueryParser } from './parser';
8
+ import { IFieldSchema } from './types';
9
+
10
+ describe('QueryParser.parseWithContext - Suggestions & Recovery', () => {
11
+ let parser: QueryParser;
12
+
13
+ const testSchema: Record<string, IFieldSchema> = {
14
+ status: {
15
+ type: 'string',
16
+ allowedValues: ['todo', 'doing', 'done'],
17
+ description: 'Task status'
18
+ },
19
+ priority: { type: 'number', description: 'Priority level' },
20
+ name: { type: 'string', description: 'Task name' },
21
+ createdAt: { type: 'date', description: 'Creation date' },
22
+ isActive: { type: 'boolean', description: 'Whether active' },
23
+ assignee: { type: 'string', description: 'Assigned user' }
24
+ };
25
+
26
+ beforeEach(() => {
27
+ parser = new QueryParser();
28
+ });
29
+
30
+ describe('Autocomplete Suggestions', () => {
31
+ describe('empty context', () => {
32
+ it('should suggest fields for empty input', () => {
33
+ const result = parser.parseWithContext('', {
34
+ cursorPosition: 0,
35
+ schema: testSchema
36
+ });
37
+
38
+ expect(result.suggestions).toBeDefined();
39
+ expect(result.suggestions?.context).toBe('empty');
40
+ expect(result.suggestions?.fields?.length).toBeGreaterThan(0);
41
+ });
42
+
43
+ it('should include all schema fields in suggestions', () => {
44
+ const result = parser.parseWithContext('', {
45
+ cursorPosition: 0,
46
+ schema: testSchema
47
+ });
48
+
49
+ const suggestedFields = result.suggestions?.fields?.map(f => f.field);
50
+ expect(suggestedFields).toContain('status');
51
+ expect(suggestedFields).toContain('priority');
52
+ expect(suggestedFields).toContain('name');
53
+ });
54
+
55
+ it('should include field metadata in suggestions', () => {
56
+ const result = parser.parseWithContext('', {
57
+ cursorPosition: 0,
58
+ schema: testSchema
59
+ });
60
+
61
+ const statusSuggestion = result.suggestions?.fields?.find(
62
+ f => f.field === 'status'
63
+ );
64
+ expect(statusSuggestion?.type).toBe('string');
65
+ expect(statusSuggestion?.description).toBe('Task status');
66
+ });
67
+ });
68
+
69
+ describe('field context', () => {
70
+ it('should suggest matching fields when typing field name', () => {
71
+ const result = parser.parseWithContext('sta', {
72
+ cursorPosition: 3,
73
+ schema: testSchema
74
+ });
75
+
76
+ expect(result.suggestions?.context).toBe('field');
77
+ expect(result.suggestions?.fields?.[0].field).toBe('status');
78
+ });
79
+
80
+ it('should rank prefix matches higher', () => {
81
+ const result = parser.parseWithContext('pri', {
82
+ cursorPosition: 3,
83
+ schema: testSchema
84
+ });
85
+
86
+ const fields = result.suggestions?.fields || [];
87
+ expect(fields[0].field).toBe('priority');
88
+ expect(fields[0].score).toBeGreaterThan(50);
89
+ });
90
+
91
+ it('should suggest similar fields for typos', () => {
92
+ const result = parser.parseWithContext('statis', {
93
+ cursorPosition: 6,
94
+ schema: testSchema
95
+ });
96
+
97
+ const fields = result.suggestions?.fields || [];
98
+ const statusSuggestion = fields.find(f => f.field === 'status');
99
+ expect(statusSuggestion).toBeDefined();
100
+ });
101
+ });
102
+
103
+ describe('value context', () => {
104
+ it('should suggest allowed values when in value position', () => {
105
+ const result = parser.parseWithContext('status:d', {
106
+ cursorPosition: 8,
107
+ schema: testSchema
108
+ });
109
+
110
+ expect(result.suggestions?.context).toBe('value');
111
+ expect(result.suggestions?.currentField).toBe('status');
112
+ expect(result.suggestions?.values).toBeDefined();
113
+ });
114
+
115
+ it('should filter values based on partial input', () => {
116
+ const result = parser.parseWithContext('status:do', {
117
+ cursorPosition: 9,
118
+ schema: testSchema
119
+ });
120
+
121
+ const values = result.suggestions?.values || [];
122
+ // "doing" and "done" both start with "do"
123
+ expect(values.length).toBeGreaterThanOrEqual(2);
124
+ expect(values.some(v => v.value === 'doing')).toBe(true);
125
+ expect(values.some(v => v.value === 'done')).toBe(true);
126
+ });
127
+
128
+ it('should suggest boolean values for boolean fields', () => {
129
+ const result = parser.parseWithContext('isActive:', {
130
+ cursorPosition: 9,
131
+ schema: testSchema
132
+ });
133
+
134
+ expect(result.suggestions?.context).toBe('value');
135
+ const values = result.suggestions?.values || [];
136
+ expect(values.some(v => v.value === true)).toBe(true);
137
+ expect(values.some(v => v.value === false)).toBe(true);
138
+ });
139
+ });
140
+
141
+ describe('operator context', () => {
142
+ it('should suggest operators', () => {
143
+ const result = parser.parseWithContext('priority:', {
144
+ cursorPosition: 9,
145
+ schema: testSchema
146
+ });
147
+
148
+ // After field: we're in value context, but let's test operator suggestions
149
+ // by looking at the operators property
150
+ expect(result.suggestions).toBeDefined();
151
+ });
152
+
153
+ it('should mark comparison operators as applicable for number fields', () => {
154
+ const result = parser.parseWithContext('priority', {
155
+ cursorPosition: 8,
156
+ schema: testSchema
157
+ });
158
+
159
+ // When we're at the end of a field name, we might suggest operators
160
+ expect(result.suggestions).toBeDefined();
161
+ });
162
+ });
163
+
164
+ describe('logical operator context', () => {
165
+ it('should suggest logical operators between terms', () => {
166
+ const result = parser.parseWithContext('status:done ', {
167
+ cursorPosition: 12,
168
+ schema: testSchema
169
+ });
170
+
171
+ expect(result.suggestions?.logicalOperators).toContain('AND');
172
+ expect(result.suggestions?.logicalOperators).toContain('OR');
173
+ });
174
+
175
+ it('should suggest when cursor is in a logical operator', () => {
176
+ const result = parser.parseWithContext(
177
+ 'status:done AND priority:high',
178
+ {
179
+ cursorPosition: 13, // in "AND"
180
+ schema: testSchema
181
+ }
182
+ );
183
+
184
+ expect(result.suggestions?.context).toBe('logical_operator');
185
+ expect(result.suggestions?.logicalOperators).toContain('AND');
186
+ expect(result.suggestions?.logicalOperators).toContain('OR');
187
+ expect(result.suggestions?.logicalOperators).toContain('NOT');
188
+ });
189
+ });
190
+
191
+ describe('without schema', () => {
192
+ it('should return suggestions without field details when no schema', () => {
193
+ const result = parser.parseWithContext('sta', {
194
+ cursorPosition: 3
195
+ });
196
+
197
+ expect(result.suggestions).toBeDefined();
198
+ expect(result.suggestions?.context).toBe('field');
199
+ expect(result.suggestions?.fields).toEqual([]);
200
+ });
201
+ });
202
+
203
+ describe('without cursor position', () => {
204
+ it('should not include suggestions when no cursor position', () => {
205
+ const result = parser.parseWithContext('status:done', {
206
+ schema: testSchema
207
+ });
208
+
209
+ expect(result.suggestions).toBeUndefined();
210
+ });
211
+ });
212
+ });
213
+
214
+ describe('Error Recovery', () => {
215
+ describe('unclosed quotes', () => {
216
+ it('should detect unclosed double quote', () => {
217
+ const result = parser.parseWithContext('name:"John');
218
+
219
+ expect(result.success).toBe(false);
220
+ expect(result.recovery).toBeDefined();
221
+ expect(result.recovery?.issue).toBe('unclosed_quote');
222
+ expect(result.recovery?.autofix).toBe('name:"John"');
223
+ });
224
+
225
+ it('should detect unclosed single quote', () => {
226
+ const result = parser.parseWithContext("name:'John");
227
+
228
+ expect(result.success).toBe(false);
229
+ expect(result.recovery?.issue).toBe('unclosed_quote');
230
+ expect(result.recovery?.autofix).toBe("name:'John'");
231
+ });
232
+
233
+ it('should include position of unclosed quote', () => {
234
+ const result = parser.parseWithContext('name:"John');
235
+
236
+ expect(result.recovery?.position).toBe(5); // position of "
237
+ });
238
+ });
239
+
240
+ describe('unclosed parentheses', () => {
241
+ it('should detect missing closing parenthesis', () => {
242
+ const result = parser.parseWithContext('(status:done');
243
+
244
+ expect(result.success).toBe(false);
245
+ expect(result.recovery?.issue).toBe('unclosed_parenthesis');
246
+ expect(result.recovery?.autofix).toBe('(status:done)');
247
+ });
248
+
249
+ it('should detect multiple missing closing parentheses', () => {
250
+ const result = parser.parseWithContext('((status:done');
251
+
252
+ expect(result.recovery?.issue).toBe('unclosed_parenthesis');
253
+ expect(result.recovery?.autofix).toBe('((status:done))');
254
+ });
255
+
256
+ it('should detect extra closing parenthesis', () => {
257
+ const result = parser.parseWithContext('status:done))');
258
+
259
+ expect(result.recovery?.issue).toBe('unclosed_parenthesis');
260
+ expect(result.recovery?.message).toContain('Extra');
261
+ });
262
+ });
263
+
264
+ describe('trailing operator', () => {
265
+ it('should detect trailing AND', () => {
266
+ const result = parser.parseWithContext('status:done AND');
267
+
268
+ expect(result.success).toBe(false);
269
+ expect(result.recovery?.issue).toBe('trailing_operator');
270
+ expect(result.recovery?.autofix).toBe('status:done');
271
+ });
272
+
273
+ it('should detect trailing OR', () => {
274
+ const result = parser.parseWithContext('status:done OR');
275
+
276
+ expect(result.recovery?.issue).toBe('trailing_operator');
277
+ expect(result.recovery?.autofix).toBe('status:done');
278
+ });
279
+
280
+ it('should detect trailing NOT', () => {
281
+ const result = parser.parseWithContext('status:done NOT');
282
+
283
+ expect(result.recovery?.issue).toBe('trailing_operator');
284
+ });
285
+
286
+ it('should handle trailing operator with whitespace', () => {
287
+ const result = parser.parseWithContext('status:done AND ');
288
+
289
+ expect(result.recovery?.issue).toBe('trailing_operator');
290
+ });
291
+ });
292
+
293
+ describe('missing value', () => {
294
+ it('should detect missing value after colon', () => {
295
+ const result = parser.parseWithContext('status:');
296
+
297
+ expect(result.success).toBe(false);
298
+ expect(result.recovery?.issue).toBe('missing_value');
299
+ });
300
+ });
301
+
302
+ describe('syntax error', () => {
303
+ it('should provide generic recovery for other syntax errors', () => {
304
+ const result = parser.parseWithContext('status:AND'); // AND is a keyword
305
+
306
+ expect(result.success).toBe(false);
307
+ expect(result.recovery).toBeDefined();
308
+ // This might be detected as syntax_error or another issue
309
+ expect(result.recovery?.issue).toBeDefined();
310
+ });
311
+ });
312
+
313
+ describe('successful parse', () => {
314
+ it('should not include recovery when parsing succeeds', () => {
315
+ const result = parser.parseWithContext('status:done');
316
+
317
+ expect(result.success).toBe(true);
318
+ expect(result.recovery).toBeUndefined();
319
+ });
320
+ });
321
+ });
322
+
323
+ describe('Integration with Other Features', () => {
324
+ it('should include all features together', () => {
325
+ const result = parser.parseWithContext('status:do', {
326
+ cursorPosition: 9,
327
+ schema: testSchema,
328
+ securityOptions: {
329
+ maxClauseCount: 10
330
+ }
331
+ });
332
+
333
+ // Core Parsing
334
+ expect(result.tokens).toBeDefined();
335
+ expect(result.structure).toBeDefined();
336
+
337
+ // Validation & Security
338
+ expect(result.fieldValidation).toBeDefined();
339
+ expect(result.security).toBeDefined();
340
+
341
+ // Suggestions & Recovery
342
+ expect(result.suggestions).toBeDefined();
343
+ expect(result.suggestions?.values?.some(v => v.value === 'done')).toBe(
344
+ true
345
+ );
346
+ });
347
+
348
+ it('should provide recovery and suggestions for failed parse', () => {
349
+ const result = parser.parseWithContext('status:', {
350
+ cursorPosition: 7,
351
+ schema: testSchema
352
+ });
353
+
354
+ expect(result.success).toBe(false);
355
+ expect(result.recovery).toBeDefined();
356
+ expect(result.suggestions).toBeDefined();
357
+ expect(result.suggestions?.context).toBe('value');
358
+ });
359
+ });
360
+ });