this repo has no description
1import assert from 'node:assert'
2import { describe, test } from 'node:test'
3import * as fc from 'fast-check'
4import { TaggedStringParser } from '../../src/TaggedStringParser.ts'
5import type { EntitySchema } from '../../src/types.ts'
6
7describe('Property-Based Tests', () => {
8 describe('property-based tests for quoted string extraction', () => {
9 // Helper to access private method for testing
10 type ParserWithExtractQuotedString = {
11 extractQuotedString: (
12 message: string,
13 startPos: number,
14 ) => { content: string; endPosition: number } | null
15 }
16
17 /**
18 * Feature: delimiter-free-parsing, Property 5: Escape sequences are processed
19 * Validates: Requirements 3.4, 4.3, 5.1, 5.2, 5.3
20 */
21 test('Property 5: Escape sequences are processed correctly', () => {
22 const parser = new TaggedStringParser()
23 const extract = (
24 parser as unknown as ParserWithExtractQuotedString
25 ).extractQuotedString.bind(parser)
26
27 // Generator for strings that may contain quotes and backslashes
28 const contentArbitrary = fc.string({
29 minLength: 0,
30 maxLength: 50,
31 })
32
33 fc.assert(
34 fc.property(contentArbitrary, (content) => {
35 // Build a properly escaped quoted string
36 // Replace \ with \\ and " with \"
37 const escaped = content.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
38 const quotedString = `"${escaped}"`
39
40 // Extract the quoted string
41 const result = extract(quotedString, 0)
42
43 // Property: extraction should succeed and return the original content
44 assert.notStrictEqual(
45 result,
46 null,
47 `Failed to extract quoted string: ${quotedString}`,
48 )
49 assert.strictEqual(
50 result?.content,
51 content,
52 `Escape sequences not processed correctly. Expected: ${content}, Got: ${result?.content}`,
53 )
54
55 // Property: endPosition should be at the closing quote
56 assert.strictEqual(
57 result?.endPosition,
58 quotedString.length,
59 'End position should be at closing quote',
60 )
61 }),
62 { numRuns: 100 },
63 )
64 })
65
66 test('Property 5 (edge case): Escaped quotes within content', () => {
67 const parser = new TaggedStringParser()
68 const extract = (
69 parser as unknown as ParserWithExtractQuotedString
70 ).extractQuotedString.bind(parser)
71
72 // Generator specifically for strings with quotes
73 const stringWithQuotesArbitrary = fc
74 .array(fc.constantFrom('"', '\\', 'a', 'b', ' ', ':', '=', 'x', 'y'), {
75 minLength: 0,
76 maxLength: 20,
77 })
78 .map((chars) => chars.join(''))
79
80 fc.assert(
81 fc.property(stringWithQuotesArbitrary, (content) => {
82 // Escape the content properly
83 const escaped = content.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
84 const quotedString = `"${escaped}"`
85
86 const result = extract(quotedString, 0)
87
88 // Should successfully extract and unescape
89 assert.notStrictEqual(result, null)
90 assert.strictEqual(result?.content, content)
91 }),
92 { numRuns: 100 },
93 )
94 })
95
96 test('Property 5 (edge case): Backslashes at various positions', () => {
97 const parser = new TaggedStringParser()
98 const extract = (
99 parser as unknown as ParserWithExtractQuotedString
100 ).extractQuotedString.bind(parser)
101
102 // Generator for strings with backslashes
103 const stringWithBackslashesArbitrary = fc
104 .array(fc.constantFrom('\\', 'a', 'b', 'c', ' ', 'd', 'e'), {
105 minLength: 0,
106 maxLength: 20,
107 })
108 .map((chars) => chars.join(''))
109
110 fc.assert(
111 fc.property(stringWithBackslashesArbitrary, (content) => {
112 // Properly escape backslashes and quotes
113 const escaped = content.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
114 const quotedString = `"${escaped}"`
115
116 const result = extract(quotedString, 0)
117
118 // Should successfully extract with backslashes preserved
119 assert.notStrictEqual(result, null)
120 assert.strictEqual(result?.content, content)
121 }),
122 { numRuns: 100 },
123 )
124 })
125 })
126
127 describe('property-based tests for quoted strings', () => {
128 /**
129 * Feature: delimiter-free-parsing, Property 4: Quoted strings preserve content
130 * Validates: Requirements 3.1, 3.2, 3.3, 4.1, 4.2
131 */
132 test('Property 4: Quoted strings preserve content', () => {
133 // Generator for content that may contain spaces and special characters
134 const contentArbitrary = fc
135 .string({
136 minLength: 0,
137 maxLength: 50,
138 })
139 .filter((s) => !s.includes('"') && !s.includes('\\'))
140
141 // Test in delimited mode
142 fc.assert(
143 fc.property(contentArbitrary, contentArbitrary, (key, value) => {
144 const parser = new TaggedStringParser({ typeSeparator: '=' })
145
146 // Build a tag with quoted key and value
147 const quotedKey = `"${key}"`
148 const quotedValue = `"${value}"`
149 const message = `[${quotedKey}=${quotedValue}]`
150
151 const result = parser.parse(message)
152
153 // Property: Content should be preserved exactly
154 if (result.entities.length > 0) {
155 assert.strictEqual(
156 result.entities[0].type,
157 key,
158 `Key content not preserved. Expected: "${key}", Got: "${result.entities[0].type}"`,
159 )
160 assert.strictEqual(
161 result.entities[0].value,
162 value,
163 `Value content not preserved. Expected: "${value}", Got: "${result.entities[0].value}"`,
164 )
165 }
166 }),
167 { numRuns: 100 },
168 )
169
170 // Test in delimiter-free mode
171 fc.assert(
172 fc.property(
173 contentArbitrary.filter((s) => s.trim().length > 0),
174 contentArbitrary.filter((s) => s.trim().length > 0),
175 (key, value) => {
176 const parser = new TaggedStringParser({
177 delimiters: false,
178 typeSeparator: '=',
179 })
180
181 // Build a delimiter-free pattern with quoted key and value
182 const quotedKey = `"${key}"`
183 const quotedValue = `"${value}"`
184 const message = `${quotedKey}=${quotedValue}`
185
186 const result = parser.parse(message)
187
188 // Property: Content should be preserved exactly
189 if (result.entities.length > 0) {
190 assert.strictEqual(
191 result.entities[0].type,
192 key,
193 `Key content not preserved in delimiter-free mode. Expected: "${key}", Got: "${result.entities[0].type}"`,
194 )
195 assert.strictEqual(
196 result.entities[0].value,
197 value,
198 `Value content not preserved in delimiter-free mode. Expected: "${value}", Got: "${result.entities[0].value}"`,
199 )
200 }
201 },
202 ),
203 { numRuns: 100 },
204 )
205 })
206
207 test('Property 4 (edge case): Quoted strings with separator characters', () => {
208 // Generator for content that includes separator characters
209 const contentWithSeparatorArbitrary = fc
210 .array(fc.constantFrom('a', 'b', '=', ':', ' ', 'x', 'y'), {
211 minLength: 1,
212 maxLength: 20,
213 })
214 .map((chars) => chars.join(''))
215
216 fc.assert(
217 fc.property(contentWithSeparatorArbitrary, (value) => {
218 const parser = new TaggedStringParser({ typeSeparator: '=' })
219
220 // Build a tag with quoted value containing separator
221 const message = `[key="${value}"]`
222
223 const result = parser.parse(message)
224
225 // Property: Separator characters in quoted values should be preserved
226 assert.strictEqual(result.entities.length, 1)
227 assert.strictEqual(
228 result.entities[0].value,
229 value,
230 `Separator in quoted value not preserved. Expected: "${value}", Got: "${result.entities[0].value}"`,
231 )
232 }),
233 { numRuns: 100 },
234 )
235 })
236
237 test('Property 4 (edge case): Quoted strings with spaces', () => {
238 // Generator for content with multiple spaces
239 const contentWithSpacesArbitrary = fc
240 .array(fc.constantFrom('a', 'b', ' ', 'c', 'd', ' '), {
241 minLength: 1,
242 maxLength: 20,
243 })
244 .map((chars) => chars.join(''))
245
246 fc.assert(
247 fc.property(contentWithSpacesArbitrary, (content) => {
248 const parser = new TaggedStringParser({ typeSeparator: '=' })
249
250 // Test with quoted key
251 const message1 = `["${content}"=value]`
252 const result1 = parser.parse(message1)
253
254 if (result1.entities.length > 0) {
255 assert.strictEqual(
256 result1.entities[0].type,
257 content,
258 `Spaces in quoted key not preserved. Expected: "${content}", Got: "${result1.entities[0].type}"`,
259 )
260 }
261
262 // Test with quoted value
263 const message2 = `[key="${content}"]`
264 const result2 = parser.parse(message2)
265
266 if (result2.entities.length > 0) {
267 assert.strictEqual(
268 result2.entities[0].value,
269 content,
270 `Spaces in quoted value not preserved. Expected: "${content}", Got: "${result2.entities[0].value}"`,
271 )
272 }
273 }),
274 { numRuns: 100 },
275 )
276 })
277
278 /**
279 * Feature: delimiter-free-parsing, Property 7: Quoted keys work in both modes
280 * Validates: Requirements 4.5
281 */
282 test('Property 7: Quoted keys work in both modes', () => {
283 // Generator for keys with spaces (which require quoting)
284 const keyWithSpacesArbitrary = fc
285 .array(fc.constantFrom('a', 'b', ' ', 'c', 'd', 'e'), {
286 minLength: 2,
287 maxLength: 20,
288 })
289 .map((chars) => chars.join(''))
290 .filter((s) => s.includes(' ') && s.trim().length > 0)
291
292 // Generator for simple values (no quotes, no spaces, no delimiters)
293 const simpleValueArbitrary = fc
294 .string({
295 minLength: 1,
296 maxLength: 20,
297 })
298 .filter(
299 (s) =>
300 !s.includes('"') &&
301 !s.includes('\\') &&
302 !/\s/.test(s) &&
303 !s.includes('[') &&
304 !s.includes(']'),
305 )
306
307 fc.assert(
308 fc.property(
309 keyWithSpacesArbitrary,
310 simpleValueArbitrary,
311 (key, value) => {
312 // Test in delimited mode
313 const delimitedParser = new TaggedStringParser({
314 typeSeparator: '=',
315 })
316 const delimitedMessage = `["${key}"=${value}]`
317 const delimitedResult = delimitedParser.parse(delimitedMessage)
318
319 // Property: Quoted key should be extracted in delimited mode
320 assert.strictEqual(
321 delimitedResult.entities.length,
322 1,
323 `Delimited mode should extract entity with quoted key. Message: ${delimitedMessage}`,
324 )
325 assert.strictEqual(
326 delimitedResult.entities[0].type,
327 key,
328 `Delimited mode: Key not preserved. Expected: "${key}", Got: "${delimitedResult.entities[0].type}"`,
329 )
330 assert.strictEqual(
331 delimitedResult.entities[0].value,
332 value,
333 `Delimited mode: Value not preserved. Expected: "${value}", Got: "${delimitedResult.entities[0].value}"`,
334 )
335
336 // Test in delimiter-free mode
337 const delimiterFreeParser = new TaggedStringParser({
338 delimiters: false,
339 typeSeparator: '=',
340 })
341 const delimiterFreeMessage = `"${key}"=${value}`
342 const delimiterFreeResult =
343 delimiterFreeParser.parse(delimiterFreeMessage)
344
345 // Property: Quoted key should be extracted in delimiter-free mode
346 assert.strictEqual(
347 delimiterFreeResult.entities.length,
348 1,
349 `Delimiter-free mode should extract entity with quoted key. Message: ${delimiterFreeMessage}`,
350 )
351 assert.strictEqual(
352 delimiterFreeResult.entities[0].type,
353 key,
354 `Delimiter-free mode: Key not preserved. Expected: "${key}", Got: "${delimiterFreeResult.entities[0].type}"`,
355 )
356 assert.strictEqual(
357 delimiterFreeResult.entities[0].value,
358 value,
359 `Delimiter-free mode: Value not preserved. Expected: "${value}", Got: "${delimiterFreeResult.entities[0].value}"`,
360 )
361 },
362 ),
363 { numRuns: 100 },
364 )
365 })
366
367 test('Property 7 (edge case): Quoted keys with special characters in both modes', () => {
368 // Generator for keys with special characters that would normally break parsing
369 const keyWithSpecialCharsArbitrary = fc
370 .array(fc.constantFrom('a', 'b', '=', ':', ' ', '[', ']', 'x'), {
371 minLength: 2,
372 maxLength: 15,
373 })
374 .map((chars) => chars.join(''))
375 .filter((s) => s.trim().length > 0)
376
377 const simpleValueArbitrary = fc
378 .string({
379 minLength: 1,
380 maxLength: 15,
381 })
382 .filter(
383 (s) =>
384 !s.includes('"') &&
385 !s.includes('\\') &&
386 !/\s/.test(s) &&
387 !s.includes('[') &&
388 !s.includes(']'),
389 )
390
391 fc.assert(
392 fc.property(
393 keyWithSpecialCharsArbitrary,
394 simpleValueArbitrary,
395 (key, value) => {
396 // Test in delimited mode
397 const delimitedParser = new TaggedStringParser({
398 typeSeparator: '=',
399 })
400 const delimitedMessage = `["${key}"=${value}]`
401 const delimitedResult = delimitedParser.parse(delimitedMessage)
402
403 if (delimitedResult.entities.length > 0) {
404 assert.strictEqual(
405 delimitedResult.entities[0].type,
406 key,
407 `Delimited mode: Special chars in key not preserved. Expected: "${key}", Got: "${delimitedResult.entities[0].type}"`,
408 )
409 }
410
411 // Test in delimiter-free mode
412 const delimiterFreeParser = new TaggedStringParser({
413 delimiters: false,
414 typeSeparator: '=',
415 })
416 const delimiterFreeMessage = `"${key}"=${value}`
417 const delimiterFreeResult =
418 delimiterFreeParser.parse(delimiterFreeMessage)
419
420 if (delimiterFreeResult.entities.length > 0) {
421 assert.strictEqual(
422 delimiterFreeResult.entities[0].type,
423 key,
424 `Delimiter-free mode: Special chars in key not preserved. Expected: "${key}", Got: "${delimiterFreeResult.entities[0].type}"`,
425 )
426 }
427 },
428 ),
429 { numRuns: 100 },
430 )
431 })
432 })
433
434 describe('property-based tests for escape sequence scope', () => {
435 /**
436 * Feature: delimiter-free-parsing, Property 6: Escape sequences only apply in quoted strings
437 * Validates: Requirements 5.5
438 */
439 test('Property 6: Escape sequences only apply in quoted strings', () => {
440 // Generator for strings that may contain backslashes but NOT quotes
441 // (quotes would trigger quoted string parsing, which is a different code path)
442 const stringWithBackslashesArbitrary = fc
443 .array(fc.constantFrom('\\', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'), {
444 minLength: 1,
445 maxLength: 20,
446 })
447 .map((chars) => chars.join(''))
448 .filter((s) => s.includes('\\') && !/\s/.test(s) && !s.includes('='))
449
450 fc.assert(
451 fc.property(stringWithBackslashesArbitrary, (unquotedValue) => {
452 // Test in delimiter-free mode with unquoted value
453 const parser = new TaggedStringParser({
454 delimiters: false,
455 typeSeparator: '=',
456 })
457
458 const message = `key=${unquotedValue}`
459 const result = parser.parse(message)
460
461 // Property: Backslashes in unquoted text should be treated as literal characters
462 // No escape sequence processing should occur
463 assert.strictEqual(
464 result.entities.length,
465 1,
466 `Should extract one entity from: ${message}`,
467 )
468 assert.strictEqual(
469 result.entities[0].value,
470 unquotedValue,
471 `Backslashes in unquoted value should be literal. Expected: "${unquotedValue}", Got: "${result.entities[0].value}"`,
472 )
473 }),
474 { numRuns: 100 },
475 )
476
477 // Test in delimited mode with unquoted value
478 fc.assert(
479 fc.property(stringWithBackslashesArbitrary, (unquotedValue) => {
480 const parser = new TaggedStringParser({ typeSeparator: '=' })
481
482 const message = `[key=${unquotedValue}]`
483 const result = parser.parse(message)
484
485 // Property: Backslashes in unquoted text should be treated as literal characters
486 assert.strictEqual(
487 result.entities.length,
488 1,
489 `Should extract one entity from: ${message}`,
490 )
491 assert.strictEqual(
492 result.entities[0].value,
493 unquotedValue,
494 `Backslashes in unquoted value (delimited mode) should be literal. Expected: "${unquotedValue}", Got: "${result.entities[0].value}"`,
495 )
496 }),
497 { numRuns: 100 },
498 )
499 })
500
501 test('Property 6 (edge case): Backslash-quote in unquoted text', () => {
502 // Test that \\" in unquoted text is treated as literal backslash followed by quote
503 const parser1 = new TaggedStringParser({
504 delimiters: false,
505 typeSeparator: '=',
506 })
507
508 // In unquoted context, backslashes should be literal
509 const result1 = parser1.parse('key=test\\"value')
510
511 assert.strictEqual(result1.entities.length, 1)
512 assert.strictEqual(
513 result1.entities[0].value,
514 'test\\"value',
515 'Backslash-quote in unquoted value should be literal',
516 )
517
518 // Test in delimited mode
519 const parser2 = new TaggedStringParser({ typeSeparator: '=' })
520 const result2 = parser2.parse('[key=test\\"value]')
521
522 assert.strictEqual(result2.entities.length, 1)
523 assert.strictEqual(
524 result2.entities[0].value,
525 'test\\"value',
526 'Backslash-quote in unquoted value (delimited) should be literal',
527 )
528 })
529
530 test('Property 6 (edge case): Backslash-backslash in unquoted text', () => {
531 // Test that \\\\ in unquoted text is treated as literal backslashes
532 const parser1 = new TaggedStringParser({
533 delimiters: false,
534 typeSeparator: '=',
535 })
536
537 const result1 = parser1.parse('key=path\\\\to\\\\file')
538
539 assert.strictEqual(result1.entities.length, 1)
540 assert.strictEqual(
541 result1.entities[0].value,
542 'path\\\\to\\\\file',
543 'Double backslashes in unquoted value should be literal',
544 )
545
546 // Test in delimited mode
547 const parser2 = new TaggedStringParser({ typeSeparator: '=' })
548 const result2 = parser2.parse('[key=path\\\\to\\\\file]')
549
550 assert.strictEqual(result2.entities.length, 1)
551 assert.strictEqual(
552 result2.entities[0].value,
553 'path\\\\to\\\\file',
554 'Double backslashes in unquoted value (delimited) should be literal',
555 )
556 })
557
558 test('Property 6 (contrast): Escape sequences DO work in quoted strings', () => {
559 // Verify that escape sequences still work in quoted strings (contrast test)
560 const parser1 = new TaggedStringParser({
561 delimiters: false,
562 typeSeparator: '=',
563 })
564
565 // In quoted context, escape sequences should be processed
566 const result1 = parser1.parse('key="test\\"value"')
567
568 assert.strictEqual(result1.entities.length, 1)
569 assert.strictEqual(
570 result1.entities[0].value,
571 'test"value',
572 'Escape sequences should work in quoted strings',
573 )
574
575 // Test backslash-backslash in quoted string
576 const result2 = parser1.parse('key="path\\\\to\\\\file"')
577
578 assert.strictEqual(result2.entities.length, 1)
579 assert.strictEqual(
580 result2.entities[0].value,
581 'path\\to\\file',
582 'Double backslashes should be processed in quoted strings',
583 )
584 })
585 })
586
587 describe('property-based tests for backward compatibility', () => {
588 /**
589 * Feature: delimiter-free-parsing, Property 8: Delimited mode ignores delimiter-free patterns
590 * Validates: Requirements 6.2
591 */
592 test('Property 8: Delimited mode ignores delimiter-free patterns', () => {
593 // Generator for valid keys (no whitespace, no separator, no quotes, no delimiters)
594 const keyArbitrary = fc
595 .string({
596 minLength: 1,
597 maxLength: 20,
598 })
599 .filter(
600 (s) =>
601 s.trim().length > 0 &&
602 !s.includes('=') &&
603 !s.includes(':') &&
604 !s.includes('"') &&
605 !s.includes('[') &&
606 !s.includes(']') &&
607 !/\s/.test(s),
608 )
609
610 // Generator for valid values (no whitespace, no quotes, no delimiters)
611 const valueArbitrary = fc
612 .string({
613 minLength: 1,
614 maxLength: 20,
615 })
616 .filter(
617 (s) =>
618 s.trim().length > 0 &&
619 !s.includes('"') &&
620 !s.includes('[') &&
621 !s.includes(']') &&
622 !/\s/.test(s),
623 )
624
625 // Generator for key-value pairs
626 const keyValuePairArbitrary = fc.tuple(keyArbitrary, valueArbitrary)
627
628 // Generator for arrays of key-value pairs (delimiter-free patterns)
629 const keyValueArrayArbitrary = fc.array(keyValuePairArbitrary, {
630 minLength: 1,
631 maxLength: 3,
632 })
633
634 fc.assert(
635 fc.property(keyValueArrayArbitrary, (pairs) => {
636 const parser = new TaggedStringParser({
637 delimiters: ['[', ']'],
638 typeSeparator: '=',
639 })
640
641 // Build a string with key=value patterns (delimiter-free syntax)
642 // These should NOT be extracted in delimited mode
643 const message = pairs
644 .map(([key, value]) => `${key}=${value}`)
645 .join(' ')
646
647 const result = parser.parse(message)
648
649 // Property: Delimited mode should NOT extract delimiter-free patterns
650 assert.strictEqual(
651 result.entities.length,
652 0,
653 `Delimited mode should not extract delimiter-free patterns. Found ${result.entities.length} entities from: ${message}`,
654 )
655 }),
656 { numRuns: 100 },
657 )
658 })
659
660 test('Property 8 (edge case): Delimited mode extracts only delimited entities', () => {
661 // Generator for valid keys
662 const keyArbitrary = fc
663 .string({
664 minLength: 1,
665 maxLength: 20,
666 })
667 .filter(
668 (s) =>
669 s.trim().length > 0 &&
670 !s.includes('=') &&
671 !s.includes(':') &&
672 !s.includes('"') &&
673 !s.includes('[') &&
674 !s.includes(']') &&
675 !/\s/.test(s),
676 )
677
678 // Generator for valid values
679 const valueArbitrary = fc
680 .string({
681 minLength: 1,
682 maxLength: 20,
683 })
684 .filter(
685 (s) =>
686 s.trim().length > 0 &&
687 !s.includes('"') &&
688 !s.includes('[') &&
689 !s.includes(']') &&
690 !/\s/.test(s),
691 )
692
693 fc.assert(
694 fc.property(
695 keyArbitrary,
696 valueArbitrary,
697 keyArbitrary,
698 valueArbitrary,
699 (key1, value1, key2, value2) => {
700 const parser = new TaggedStringParser({
701 delimiters: ['[', ']'],
702 typeSeparator: '=',
703 })
704
705 // Mix delimiter-free pattern with delimited entity
706 const message = `${key1}=${value1} [${key2}=${value2}]`
707
708 const result = parser.parse(message)
709
710 // Property: Should only extract the delimited entity, not the delimiter-free pattern
711 assert.strictEqual(
712 result.entities.length,
713 1,
714 `Should extract only delimited entity from: ${message}`,
715 )
716
717 assert.strictEqual(
718 result.entities[0].type,
719 key2,
720 `Should extract delimited entity key. Expected: ${key2}, Got: ${result.entities[0].type}`,
721 )
722 assert.strictEqual(
723 result.entities[0].value,
724 value2,
725 `Should extract delimited entity value. Expected: ${value2}, Got: ${result.entities[0].value}`,
726 )
727 },
728 ),
729 { numRuns: 100 },
730 )
731 })
732
733 /**
734 * Feature: delimiter-free-parsing, Property 9: Backward compatibility is maintained
735 * Validates: Requirements 6.3
736 */
737 test('Property 9: Backward compatibility is maintained', () => {
738 // Generator for valid keys (no separator, no quotes, no delimiters)
739 const keyArbitrary = fc
740 .string({
741 minLength: 1,
742 maxLength: 20,
743 })
744 .filter(
745 (s) =>
746 s.trim().length > 0 &&
747 !s.includes(':') &&
748 !s.includes('=') &&
749 !s.includes('"') &&
750 !s.includes('[') &&
751 !s.includes(']') &&
752 !/\s/.test(s),
753 )
754
755 // Generator for valid values (no quotes, no delimiters)
756 const valueArbitrary = fc
757 .string({
758 minLength: 1,
759 maxLength: 20,
760 })
761 .filter(
762 (s) =>
763 s.trim().length > 0 &&
764 !s.includes('"') &&
765 !s.includes('[') &&
766 !s.includes(']'),
767 )
768
769 // Generator for type separator
770 const separatorArbitrary = fc.constantFrom(':', '=', '|')
771
772 fc.assert(
773 fc.property(
774 keyArbitrary,
775 valueArbitrary,
776 separatorArbitrary,
777 (key, value, separator) => {
778 // Create parser with explicit delimiters (new API)
779 const newParser = new TaggedStringParser({
780 delimiters: ['[', ']'],
781 typeSeparator: separator,
782 })
783
784 // Create parser with old API (backward compatible)
785 const oldParser = new TaggedStringParser({
786 openDelimiter: '[',
787 closeDelimiter: ']',
788 typeSeparator: separator,
789 })
790
791 // Build a delimited message
792 const message = `[${key}${separator}${value}]`
793
794 const newResult = newParser.parse(message)
795 const oldResult = oldParser.parse(message)
796
797 // Property: Both parsers should produce identical results
798 assert.strictEqual(
799 newResult.entities.length,
800 oldResult.entities.length,
801 `Entity count mismatch for: ${message}`,
802 )
803
804 if (newResult.entities.length > 0) {
805 assert.strictEqual(
806 newResult.entities[0].type,
807 oldResult.entities[0].type,
808 `Type mismatch. New: ${newResult.entities[0].type}, Old: ${oldResult.entities[0].type}`,
809 )
810 assert.strictEqual(
811 newResult.entities[0].value,
812 oldResult.entities[0].value,
813 `Value mismatch. New: ${newResult.entities[0].value}, Old: ${oldResult.entities[0].value}`,
814 )
815 assert.strictEqual(
816 newResult.entities[0].parsedValue,
817 oldResult.entities[0].parsedValue,
818 `Parsed value mismatch`,
819 )
820 assert.strictEqual(
821 newResult.entities[0].inferredType,
822 oldResult.entities[0].inferredType,
823 `Inferred type mismatch`,
824 )
825 assert.strictEqual(
826 newResult.entities[0].formattedValue,
827 oldResult.entities[0].formattedValue,
828 `Formatted value mismatch`,
829 )
830 assert.strictEqual(
831 newResult.entities[0].position,
832 oldResult.entities[0].position,
833 `Position mismatch`,
834 )
835 assert.strictEqual(
836 newResult.entities[0].endPosition,
837 oldResult.entities[0].endPosition,
838 `End position mismatch`,
839 )
840 }
841 },
842 ),
843 { numRuns: 100 },
844 )
845 })
846
847 test('Property 9 (edge case): Type inference remains unchanged', () => {
848 // Generator for numeric values
849 const numericValueArbitrary = fc.integer({ min: -1000, max: 1000 })
850
851 // Generator for boolean values
852 const booleanValueArbitrary = fc.boolean()
853
854 // Generator for string values
855 const stringValueArbitrary = fc
856 .string({
857 minLength: 1,
858 maxLength: 20,
859 })
860 .filter(
861 (s) =>
862 s.trim().length > 0 &&
863 !s.includes('[') &&
864 !s.includes(']') &&
865 !s.includes('"') &&
866 !/^-?\d+(\.\d+)?$/.test(s) && // Not a number
867 !/^(true|false)$/i.test(s), // Not a boolean
868 )
869
870 fc.assert(
871 fc.property(
872 fc.oneof(
873 numericValueArbitrary.map((v) => ({
874 value: String(v),
875 expectedType: 'number' as const,
876 })),
877 booleanValueArbitrary.map((v) => ({
878 value: String(v),
879 expectedType: 'boolean' as const,
880 })),
881 stringValueArbitrary.map((v) => ({
882 value: v,
883 expectedType: 'string' as const,
884 })),
885 ),
886 ({ value, expectedType }) => {
887 // Create parser with new API
888 const newParser = new TaggedStringParser({
889 delimiters: ['[', ']'],
890 })
891
892 // Create parser with old API
893 const oldParser = new TaggedStringParser({
894 openDelimiter: '[',
895 closeDelimiter: ']',
896 })
897
898 const message = `[key:${value}]`
899
900 const newResult = newParser.parse(message)
901 const oldResult = oldParser.parse(message)
902
903 // Property: Type inference should be identical
904 if (
905 newResult.entities.length > 0 &&
906 oldResult.entities.length > 0
907 ) {
908 assert.strictEqual(
909 newResult.entities[0].inferredType,
910 oldResult.entities[0].inferredType,
911 `Type inference mismatch for value: ${value}`,
912 )
913 assert.strictEqual(
914 newResult.entities[0].inferredType,
915 expectedType,
916 `Type inference incorrect for value: ${value}`,
917 )
918 }
919 },
920 ),
921 { numRuns: 100 },
922 )
923 })
924
925 test('Property 9 (edge case): Custom delimiters work identically', () => {
926 // Generator for custom delimiters
927 const delimiterPairArbitrary = fc.constantFrom(
928 ['{{', '}}'],
929 ['<', '>'],
930 ['<<<', '>>>'],
931 ['{', '}'],
932 )
933
934 // Generator for valid keys
935 const keyArbitrary = fc
936 .string({
937 minLength: 1,
938 maxLength: 15,
939 })
940 .filter(
941 (s) =>
942 s.trim().length > 0 &&
943 !s.includes(':') &&
944 !s.includes('"') &&
945 !/\s/.test(s),
946 )
947
948 // Generator for valid values
949 const valueArbitrary = fc
950 .string({
951 minLength: 1,
952 maxLength: 15,
953 })
954 .filter((s) => s.trim().length > 0 && !s.includes('"'))
955
956 fc.assert(
957 fc.property(
958 delimiterPairArbitrary,
959 keyArbitrary,
960 valueArbitrary,
961 ([open, close], key, value) => {
962 // Filter out keys/values that contain the delimiters
963 if (
964 key.includes(open) ||
965 key.includes(close) ||
966 value.includes(open) ||
967 value.includes(close)
968 ) {
969 return
970 }
971
972 // Create parser with new API
973 const newParser = new TaggedStringParser({
974 delimiters: [open, close],
975 })
976
977 // Create parser with old API
978 const oldParser = new TaggedStringParser({
979 openDelimiter: open,
980 closeDelimiter: close,
981 })
982
983 const message = `${open}${key}:${value}${close}`
984
985 const newResult = newParser.parse(message)
986 const oldResult = oldParser.parse(message)
987
988 // Property: Results should be identical
989 assert.strictEqual(
990 newResult.entities.length,
991 oldResult.entities.length,
992 `Entity count mismatch for custom delimiters: ${open}${close}`,
993 )
994
995 if (newResult.entities.length > 0) {
996 assert.strictEqual(
997 newResult.entities[0].type,
998 oldResult.entities[0].type,
999 `Type mismatch with custom delimiters`,
1000 )
1001 assert.strictEqual(
1002 newResult.entities[0].value,
1003 oldResult.entities[0].value,
1004 `Value mismatch with custom delimiters`,
1005 )
1006 }
1007 },
1008 ),
1009 { numRuns: 100 },
1010 )
1011 })
1012
1013 /**
1014 * Feature: delimiter-free-parsing, Property 10: Schema and formatters work in both modes
1015 * Validates: Requirements 6.4, 6.5
1016 */
1017 test('Property 10: Schema and formatters work in both modes', () => {
1018 // Generator for entity types
1019 const entityTypeArbitrary = fc.constantFrom(
1020 'count',
1021 'enabled',
1022 'name',
1023 'price',
1024 )
1025
1026 // Generator for values based on type
1027 const valueForTypeArbitrary = (type: string) => {
1028 switch (type) {
1029 case 'count':
1030 return fc.integer({ min: 0, max: 1000 }).map(String)
1031 case 'enabled':
1032 return fc.boolean().map(String)
1033 case 'name':
1034 return fc
1035 .string({ minLength: 1, maxLength: 20 })
1036 .filter(
1037 (s) =>
1038 s.trim().length > 0 &&
1039 !s.includes('"') &&
1040 !s.includes('[') &&
1041 !s.includes(']') &&
1042 !/\s/.test(s),
1043 )
1044 case 'price':
1045 return fc
1046 .float({ min: 0, max: 1000, noNaN: true })
1047 .map((n) => n.toFixed(2))
1048 default:
1049 return fc.constant('test')
1050 }
1051 }
1052
1053 // Define schema with formatters
1054 const schema: EntitySchema = {
1055 count: {
1056 type: 'number',
1057 format: (val) => `[${val} items]`,
1058 },
1059 enabled: {
1060 type: 'boolean',
1061 format: (val) => (val ? 'YES' : 'NO'),
1062 },
1063 name: {
1064 type: 'string',
1065 format: (val) => String(val).toUpperCase(),
1066 },
1067 price: {
1068 type: 'number',
1069 format: (val) => `$${val}`,
1070 },
1071 }
1072
1073 fc.assert(
1074 fc.property(
1075 entityTypeArbitrary.chain((type) =>
1076 valueForTypeArbitrary(type).map((value) => ({ type, value })),
1077 ),
1078 ({ type, value }) => {
1079 // Test in delimited mode
1080 const delimitedParser = new TaggedStringParser({
1081 schema,
1082 delimiters: ['[', ']'],
1083 typeSeparator: '=',
1084 })
1085
1086 const delimitedMessage = `[${type}=${value}]`
1087 const delimitedResult = delimitedParser.parse(delimitedMessage)
1088
1089 // Test in delimiter-free mode
1090 const delimiterFreeParser = new TaggedStringParser({
1091 schema,
1092 delimiters: false,
1093 typeSeparator: '=',
1094 })
1095
1096 const delimiterFreeMessage = `${type}=${value}`
1097 const delimiterFreeResult =
1098 delimiterFreeParser.parse(delimiterFreeMessage)
1099
1100 // Property: Schema should apply in both modes
1101 if (
1102 delimitedResult.entities.length > 0 &&
1103 delimiterFreeResult.entities.length > 0
1104 ) {
1105 const delimitedEntity = delimitedResult.entities[0]
1106 const delimiterFreeEntity = delimiterFreeResult.entities[0]
1107
1108 // Type should match
1109 assert.strictEqual(
1110 delimitedEntity.type,
1111 delimiterFreeEntity.type,
1112 `Type mismatch for ${type}`,
1113 )
1114
1115 // Inferred type should match schema
1116 const schemaEntry = schema[type]
1117 const expectedType =
1118 typeof schemaEntry === 'string'
1119 ? schemaEntry
1120 : schemaEntry?.type || 'string'
1121 assert.strictEqual(
1122 delimitedEntity.inferredType,
1123 expectedType,
1124 `Delimited mode: Schema type not applied for ${type}`,
1125 )
1126 assert.strictEqual(
1127 delimiterFreeEntity.inferredType,
1128 expectedType,
1129 `Delimiter-free mode: Schema type not applied for ${type}`,
1130 )
1131
1132 // Parsed values should be identical
1133 assert.deepStrictEqual(
1134 delimitedEntity.parsedValue,
1135 delimiterFreeEntity.parsedValue,
1136 `Parsed value mismatch for ${type}`,
1137 )
1138
1139 // Formatted values should be identical (formatter applied in both modes)
1140 assert.strictEqual(
1141 delimitedEntity.formattedValue,
1142 delimiterFreeEntity.formattedValue,
1143 `Formatted value mismatch for ${type}. Delimited: ${delimitedEntity.formattedValue}, Delimiter-free: ${delimiterFreeEntity.formattedValue}`,
1144 )
1145 }
1146 },
1147 ),
1148 { numRuns: 100 },
1149 )
1150 })
1151
1152 test('Property 10 (edge case): Schema with shorthand syntax works in both modes', () => {
1153 // Simple schema with shorthand syntax (just type, no formatter)
1154 const schema: EntitySchema = {
1155 count: 'number',
1156 enabled: 'boolean',
1157 name: 'string',
1158 }
1159
1160 // Generator for entity types
1161 const entityTypeArbitrary = fc.constantFrom('count', 'enabled', 'name')
1162
1163 // Generator for values
1164 const valueArbitrary = fc.oneof(
1165 fc.integer({ min: 0, max: 100 }).map(String),
1166 fc.boolean().map(String),
1167 fc
1168 .string({ minLength: 1, maxLength: 15 })
1169 .filter(
1170 (s) =>
1171 s.trim().length > 0 &&
1172 !s.includes('"') &&
1173 !s.includes('[') &&
1174 !s.includes(']') &&
1175 !/\s/.test(s) &&
1176 !/^-?\d+$/.test(s) &&
1177 !/^(true|false)$/i.test(s),
1178 ),
1179 )
1180
1181 fc.assert(
1182 fc.property(entityTypeArbitrary, valueArbitrary, (type, value) => {
1183 // Test in delimited mode
1184 const delimitedParser = new TaggedStringParser({
1185 schema,
1186 delimiters: ['[', ']'],
1187 })
1188
1189 const delimitedMessage = `[${type}:${value}]`
1190 const delimitedResult = delimitedParser.parse(delimitedMessage)
1191
1192 // Test in delimiter-free mode
1193 const delimiterFreeParser = new TaggedStringParser({
1194 schema,
1195 delimiters: false,
1196 })
1197
1198 const delimiterFreeMessage = `${type}:${value}`
1199 const delimiterFreeResult =
1200 delimiterFreeParser.parse(delimiterFreeMessage)
1201
1202 // Property: Schema should apply identically in both modes
1203 if (
1204 delimitedResult.entities.length > 0 &&
1205 delimiterFreeResult.entities.length > 0
1206 ) {
1207 assert.strictEqual(
1208 delimitedResult.entities[0].inferredType,
1209 delimiterFreeResult.entities[0].inferredType,
1210 `Inferred type mismatch for ${type}:${value}`,
1211 )
1212
1213 assert.deepStrictEqual(
1214 delimitedResult.entities[0].parsedValue,
1215 delimiterFreeResult.entities[0].parsedValue,
1216 `Parsed value mismatch for ${type}:${value}`,
1217 )
1218 }
1219 }),
1220 { numRuns: 100 },
1221 )
1222 })
1223
1224 test('Property 10 (edge case): Formatters handle edge cases in both modes', () => {
1225 // Schema with formatter that handles edge cases
1226 const schema: EntitySchema = {
1227 value: {
1228 type: 'string',
1229 format: (val) => {
1230 const str = String(val)
1231 return str.length > 10 ? `${str.substring(0, 10)}...` : str
1232 },
1233 },
1234 }
1235
1236 // Generator for values of varying lengths
1237 const valueArbitrary = fc
1238 .string({ minLength: 0, maxLength: 30 })
1239 .filter(
1240 (s) =>
1241 !s.includes('"') &&
1242 !s.includes('[') &&
1243 !s.includes(']') &&
1244 !/\s/.test(s),
1245 )
1246
1247 fc.assert(
1248 fc.property(valueArbitrary, (value) => {
1249 if (value.length === 0) return // Skip empty values
1250
1251 // Test in delimited mode
1252 const delimitedParser = new TaggedStringParser({
1253 schema,
1254 delimiters: ['[', ']'],
1255 })
1256
1257 const delimitedMessage = `[value:${value}]`
1258 const delimitedResult = delimitedParser.parse(delimitedMessage)
1259
1260 // Test in delimiter-free mode
1261 const delimiterFreeParser = new TaggedStringParser({
1262 schema,
1263 delimiters: false,
1264 })
1265
1266 const delimiterFreeMessage = `value:${value}`
1267 const delimiterFreeResult =
1268 delimiterFreeParser.parse(delimiterFreeMessage)
1269
1270 // Property: Formatter should produce identical output in both modes
1271 if (
1272 delimitedResult.entities.length > 0 &&
1273 delimiterFreeResult.entities.length > 0
1274 ) {
1275 assert.strictEqual(
1276 delimitedResult.entities[0].formattedValue,
1277 delimiterFreeResult.entities[0].formattedValue,
1278 `Formatter output mismatch for value: ${value}`,
1279 )
1280
1281 // Verify formatter was actually applied
1282 const expected =
1283 value.length > 10 ? `${value.substring(0, 10)}...` : value
1284 assert.strictEqual(
1285 delimitedResult.entities[0].formattedValue,
1286 expected,
1287 `Formatter not applied correctly in delimited mode`,
1288 )
1289 assert.strictEqual(
1290 delimiterFreeResult.entities[0].formattedValue,
1291 expected,
1292 `Formatter not applied correctly in delimiter-free mode`,
1293 )
1294 }
1295 }),
1296 { numRuns: 100 },
1297 )
1298 })
1299
1300 /**
1301 * Feature: delimiter-free-parsing, Property 11: Parser continues after malformed entities
1302 * Validates: Requirements 8.5
1303 */
1304 test('Property 11: Parser continues after malformed entities', () => {
1305 // Generator for valid keys
1306 const validKeyArbitrary = fc
1307 .string({
1308 minLength: 1,
1309 maxLength: 15,
1310 })
1311 .filter(
1312 (s) =>
1313 s.trim().length > 0 &&
1314 !s.includes('=') &&
1315 !s.includes(':') &&
1316 !s.includes('"') &&
1317 !/\s/.test(s),
1318 )
1319
1320 // Generator for valid values
1321 const validValueArbitrary = fc
1322 .string({
1323 minLength: 1,
1324 maxLength: 15,
1325 })
1326 .filter((s) => s.trim().length > 0 && !s.includes('"') && !/\s/.test(s))
1327
1328 // Generator for malformed patterns
1329 const malformedPatternArbitrary = fc.constantFrom(
1330 'key="unclosed', // Unclosed quoted value
1331 '"unclosed=value', // Unclosed quoted key
1332 'key=', // Missing value
1333 '=value', // Missing key
1334 'key==value', // Double separator
1335 )
1336
1337 fc.assert(
1338 fc.property(
1339 malformedPatternArbitrary,
1340 validKeyArbitrary,
1341 validValueArbitrary,
1342 (malformed, validKey, validValue) => {
1343 const parser = new TaggedStringParser({
1344 delimiters: false,
1345 typeSeparator: '=',
1346 })
1347
1348 // Build a message with malformed entity followed by valid entity
1349 const message = `${malformed} ${validKey}=${validValue}`
1350
1351 const result = parser.parse(message)
1352
1353 // Property: Parser should skip malformed entity and continue to extract valid entity
1354 // We should get at least the valid entity (may get more if malformed parts are partially valid)
1355 const validEntity = result.entities.find(
1356 (e) => e.type === validKey && e.value === validValue,
1357 )
1358
1359 assert.ok(
1360 validEntity,
1361 `Parser should extract valid entity after malformed pattern. Message: ${message}, Entities: ${JSON.stringify(result.entities)}`,
1362 )
1363
1364 assert.strictEqual(
1365 validEntity.type,
1366 validKey,
1367 `Valid entity type should be preserved`,
1368 )
1369 assert.strictEqual(
1370 validEntity.value,
1371 validValue,
1372 `Valid entity value should be preserved`,
1373 )
1374 },
1375 ),
1376 { numRuns: 100 },
1377 )
1378 })
1379
1380 test('Property 11 (edge case): Multiple malformed entities are all skipped', () => {
1381 // Generator for valid entity
1382 const validEntityArbitrary = fc.tuple(
1383 fc
1384 .string({ minLength: 1, maxLength: 10 })
1385 .filter(
1386 (s) =>
1387 s.trim().length > 0 &&
1388 !s.includes('=') &&
1389 !s.includes('"') &&
1390 !/\s/.test(s),
1391 ),
1392 fc
1393 .string({ minLength: 1, maxLength: 10 })
1394 .filter(
1395 (s) => s.trim().length > 0 && !s.includes('"') && !/\s/.test(s),
1396 ),
1397 )
1398
1399 // Generator for multiple malformed patterns
1400 const malformedArrayArbitrary = fc.array(
1401 fc.constantFrom(
1402 'bad="unclosed',
1403 '"unclosed=val',
1404 'empty=',
1405 '=nokey',
1406 'double==sep',
1407 ),
1408 { minLength: 1, maxLength: 3 },
1409 )
1410
1411 fc.assert(
1412 fc.property(
1413 malformedArrayArbitrary,
1414 validEntityArbitrary,
1415 (malformedPatterns, [validKey, validValue]) => {
1416 const parser = new TaggedStringParser({
1417 delimiters: false,
1418 typeSeparator: '=',
1419 })
1420
1421 // Build message with multiple malformed entities and one valid entity at the end
1422 const message = `${malformedPatterns.join(' ')} ${validKey}=${validValue}`
1423
1424 const result = parser.parse(message)
1425
1426 // Property: Parser should skip all malformed entities and extract the valid one
1427 const validEntity = result.entities.find(
1428 (e) => e.type === validKey && e.value === validValue,
1429 )
1430
1431 assert.ok(
1432 validEntity,
1433 `Parser should extract valid entity after multiple malformed patterns. Message: ${message}`,
1434 )
1435 },
1436 ),
1437 { numRuns: 100 },
1438 )
1439 })
1440
1441 test('Property 11 (edge case): Malformed entities in delimited mode', () => {
1442 // Generator for valid entity (alphanumeric only to avoid special characters)
1443 const validEntityArbitrary = fc.tuple(
1444 fc.string({ minLength: 1, maxLength: 10 }).filter(
1445 (s) =>
1446 /^[a-zA-Z0-9]+$/.test(s) && // Only alphanumeric
1447 s.trim().length > 0,
1448 ),
1449 fc.string({ minLength: 1, maxLength: 10 }).filter(
1450 (s) =>
1451 /^[a-zA-Z0-9]+$/.test(s) && // Only alphanumeric
1452 s.trim().length > 0,
1453 ),
1454 )
1455
1456 // Generator for malformed delimited patterns that won't interfere with next tag
1457 const malformedDelimitedArbitrary = fc.constantFrom(
1458 '[]', // Empty tag
1459 '[ ]', // Whitespace only
1460 '[key:]', // Empty value (this gets skipped)
1461 )
1462
1463 fc.assert(
1464 fc.property(
1465 malformedDelimitedArbitrary,
1466 validEntityArbitrary,
1467 (malformed, [validKey, validValue]) => {
1468 const parser = new TaggedStringParser({
1469 delimiters: ['[', ']'],
1470 })
1471
1472 // Build message with malformed tag followed by valid tag
1473 const message = `${malformed} [${validKey}:${validValue}]`
1474
1475 const result = parser.parse(message)
1476
1477 // Property: Parser should skip malformed tag and extract valid tag
1478 const validEntity = result.entities.find(
1479 (e) => e.type === validKey && e.value === validValue,
1480 )
1481
1482 assert.ok(
1483 validEntity,
1484 `Parser should extract valid tag after malformed tag. Message: ${message}, Entities: ${JSON.stringify(result.entities)}`,
1485 )
1486 },
1487 ),
1488 { numRuns: 100 },
1489 )
1490 })
1491
1492 test('Property 11 (edge case): Parser returns empty result for all malformed input', () => {
1493 // Generator for completely malformed input (no valid entities)
1494 const allMalformedArbitrary = fc.array(
1495 fc.constantFrom(
1496 'key="unclosed',
1497 '"unclosed=value',
1498 'empty=',
1499 '=nokey',
1500 'double==sep',
1501 ),
1502 { minLength: 1, maxLength: 5 },
1503 )
1504
1505 fc.assert(
1506 fc.property(allMalformedArbitrary, (malformedPatterns) => {
1507 const parser = new TaggedStringParser({
1508 delimiters: false,
1509 typeSeparator: '=',
1510 })
1511
1512 const message = malformedPatterns.join(' ')
1513
1514 const result = parser.parse(message)
1515
1516 // Property: Parser should not crash and should return a valid ParseResult
1517 // (may be empty or contain partially extracted entities, but should not throw)
1518 assert.ok(result, 'Parser should return a result')
1519 assert.ok(
1520 Array.isArray(result.entities),
1521 'Result should have entities array',
1522 )
1523 assert.strictEqual(
1524 result.originalMessage,
1525 message,
1526 'Original message should be preserved',
1527 )
1528 }),
1529 { numRuns: 100 },
1530 )
1531 })
1532 })
1533
1534 describe('property-based tests for delimiter-free parsing', () => {
1535 /**
1536 * Feature: delimiter-free-parsing, Property 1: Delimiter-free mode extracts key-value patterns
1537 * Validates: Requirements 1.4, 2.1, 2.5
1538 */
1539 test('Property 1: Delimiter-free mode extracts key-value patterns', () => {
1540 // Generator for valid keys (alphanumeric, no whitespace, no separator, no quotes)
1541 const keyArbitrary = fc
1542 .string({
1543 minLength: 1,
1544 maxLength: 20,
1545 })
1546 .filter(
1547 (s) =>
1548 s.trim().length > 0 &&
1549 !s.includes('=') &&
1550 !s.includes('"') &&
1551 !/\s/.test(s),
1552 )
1553
1554 // Generator for valid values (alphanumeric, no whitespace, no quotes)
1555 const valueArbitrary = fc
1556 .string({
1557 minLength: 1,
1558 maxLength: 20,
1559 })
1560 .filter((s) => s.trim().length > 0 && !s.includes('"') && !/\s/.test(s))
1561
1562 // Generator for key-value pairs
1563 const keyValuePairArbitrary = fc.tuple(keyArbitrary, valueArbitrary)
1564
1565 // Generator for arrays of key-value pairs
1566 const keyValueArrayArbitrary = fc.array(keyValuePairArbitrary, {
1567 minLength: 1,
1568 maxLength: 5,
1569 })
1570
1571 fc.assert(
1572 fc.property(keyValueArrayArbitrary, (pairs) => {
1573 const parser = new TaggedStringParser({
1574 delimiters: false,
1575 typeSeparator: '=',
1576 })
1577
1578 // Build a string with key=value patterns separated by spaces
1579 const message = pairs
1580 .map(([key, value]) => `${key}=${value}`)
1581 .join(' ')
1582
1583 const result = parser.parse(message)
1584
1585 // Property: All key-value pairs should be extracted
1586 assert.strictEqual(
1587 result.entities.length,
1588 pairs.length,
1589 `Expected ${pairs.length} entities, got ${result.entities.length} from: ${message}`,
1590 )
1591
1592 // Property: Each entity should match the corresponding pair
1593 for (let i = 0; i < pairs.length; i++) {
1594 const [expectedKey, expectedValue] = pairs[i]
1595 const entity = result.entities[i]
1596
1597 assert.strictEqual(
1598 entity.type,
1599 expectedKey,
1600 `Entity ${i} type mismatch. Expected: ${expectedKey}, Got: ${entity.type}`,
1601 )
1602 assert.strictEqual(
1603 entity.value,
1604 expectedValue,
1605 `Entity ${i} value mismatch. Expected: ${expectedValue}, Got: ${entity.value}`,
1606 )
1607 }
1608 }),
1609 { numRuns: 100 },
1610 )
1611 })
1612
1613 /**
1614 * Feature: delimiter-free-parsing, Property 2: Whitespace defines entity boundaries
1615 * Validates: Requirements 2.2, 2.3
1616 */
1617 test('Property 2: Whitespace defines entity boundaries', () => {
1618 // Generator for valid keys (no whitespace, no separator, no quotes)
1619 const keyArbitrary = fc
1620 .string({
1621 minLength: 1,
1622 maxLength: 20,
1623 })
1624 .filter(
1625 (s) =>
1626 s.trim().length > 0 &&
1627 !s.includes('=') &&
1628 !s.includes('"') &&
1629 !/\s/.test(s),
1630 )
1631
1632 // Generator for valid values (no whitespace, no quotes)
1633 const valueArbitrary = fc
1634 .string({
1635 minLength: 1,
1636 maxLength: 20,
1637 })
1638 .filter((s) => s.trim().length > 0 && !s.includes('"') && !/\s/.test(s))
1639
1640 // Generator for whitespace (space, tab, multiple spaces)
1641 const whitespaceArbitrary = fc.constantFrom(' ', ' ', '\t', ' ')
1642
1643 // Generator for key-value pair with surrounding whitespace
1644 const keyValueWithWhitespaceArbitrary = fc.tuple(
1645 keyArbitrary,
1646 valueArbitrary,
1647 whitespaceArbitrary,
1648 whitespaceArbitrary,
1649 )
1650
1651 fc.assert(
1652 fc.property(
1653 keyValueWithWhitespaceArbitrary,
1654 ([key, value, before, after]) => {
1655 const parser = new TaggedStringParser({
1656 delimiters: false,
1657 typeSeparator: '=',
1658 })
1659
1660 // Build a string with whitespace before and after the entity
1661 const message = `${before}${key}=${value}${after}`
1662
1663 const result = parser.parse(message)
1664
1665 // Property: Whitespace should define boundaries, entity should be extracted
1666 assert.strictEqual(
1667 result.entities.length,
1668 1,
1669 `Expected 1 entity from: "${message}"`,
1670 )
1671
1672 assert.strictEqual(
1673 result.entities[0].type,
1674 key,
1675 `Key mismatch. Expected: ${key}, Got: ${result.entities[0].type}`,
1676 )
1677 assert.strictEqual(
1678 result.entities[0].value,
1679 value,
1680 `Value mismatch. Expected: ${value}, Got: ${result.entities[0].value}`,
1681 )
1682 },
1683 ),
1684 { numRuns: 100 },
1685 )
1686 })
1687
1688 /**
1689 * Feature: delimiter-free-parsing, Property 3: Type separator is respected
1690 * Validates: Requirements 2.4
1691 */
1692 test('Property 3: Type separator is respected', () => {
1693 // Generator that creates separator, key, and value together
1694 const testCaseArbitrary = fc
1695 .constantFrom(':', '=', '|')
1696 .chain((separator) => {
1697 // Generator for valid keys (no whitespace, no current separator, no quotes)
1698 const keyArbitrary = fc
1699 .string({
1700 minLength: 1,
1701 maxLength: 20,
1702 })
1703 .filter(
1704 (s) =>
1705 s.trim().length > 0 &&
1706 !s.includes(separator) &&
1707 !s.includes('"') &&
1708 !/\s/.test(s),
1709 )
1710
1711 // Generator for valid values (no whitespace, no quotes)
1712 const valueArbitrary = fc
1713 .string({
1714 minLength: 1,
1715 maxLength: 20,
1716 })
1717 .filter(
1718 (s) => s.trim().length > 0 && !s.includes('"') && !/\s/.test(s),
1719 )
1720
1721 return fc
1722 .tuple(keyArbitrary, valueArbitrary)
1723 .map(([key, value]) => ({ separator, key, value }))
1724 })
1725
1726 fc.assert(
1727 fc.property(testCaseArbitrary, ({ separator, key, value }) => {
1728 const parser = new TaggedStringParser({
1729 delimiters: false,
1730 typeSeparator: separator,
1731 })
1732
1733 const message = `${key}${separator}${value}`
1734
1735 const result = parser.parse(message)
1736
1737 // Property: The configured separator should be used to split key and value
1738 assert.strictEqual(
1739 result.entities.length,
1740 1,
1741 `Expected 1 entity from: "${message}" with separator "${separator}"`,
1742 )
1743
1744 assert.strictEqual(
1745 result.entities[0].type,
1746 key,
1747 `Key mismatch. Expected: ${key}, Got: ${result.entities[0].type}`,
1748 )
1749 assert.strictEqual(
1750 result.entities[0].value,
1751 value,
1752 `Value mismatch. Expected: ${value}, Got: ${result.entities[0].value}`,
1753 )
1754 }),
1755 { numRuns: 100 },
1756 )
1757 })
1758 })
1759})