this repo has no description
at main 1759 lines 58 kB view raw
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})