@getmikk/core 2.0.11 → 2.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/tests/fs.test.ts CHANGED
@@ -75,6 +75,12 @@ describe('detectProjectLanguage', () => {
75
75
  })
76
76
  })
77
77
 
78
+ it('detects Swift from Package.swift', async () => {
79
+ await withFile('Package.swift', async () => {
80
+ expect(await detectProjectLanguage(tmpDir)).toBe('swift')
81
+ })
82
+ })
83
+
78
84
  it('detects C# from .csproj file', async () => {
79
85
  await withFile('MyApp.csproj', async () => {
80
86
  expect(await detectProjectLanguage(tmpDir)).toBe('csharp')
@@ -116,7 +122,7 @@ describe('detectProjectLanguage', () => {
116
122
  describe('getDiscoveryPatterns', () => {
117
123
  const languages: ProjectLanguage[] = [
118
124
  'typescript', 'javascript', 'python', 'go', 'rust',
119
- 'java', 'ruby', 'php', 'csharp', 'unknown',
125
+ 'java', 'swift', 'ruby', 'php', 'csharp', 'unknown',
120
126
  ]
121
127
 
122
128
  for (const lang of languages) {
@@ -143,6 +149,21 @@ describe('getDiscoveryPatterns', () => {
143
149
  expect(patterns).toContain('**/*.py')
144
150
  })
145
151
 
152
+ it('java discovery patterns include Kotlin scripts for mixed JVM codebases', () => {
153
+ const { patterns } = getDiscoveryPatterns('java')
154
+ expect(patterns).toContain('**/*.kts')
155
+ })
156
+
157
+ it('swift patterns include .swift', () => {
158
+ const { patterns } = getDiscoveryPatterns('swift')
159
+ expect(patterns).toContain('**/*.swift')
160
+ })
161
+
162
+ it('cpp patterns include .hh headers', () => {
163
+ const { patterns } = getDiscoveryPatterns('cpp')
164
+ expect(patterns).toContain('**/*.hh')
165
+ })
166
+
146
167
  it('all languages ignore .mikk and .git', () => {
147
168
  for (const lang of languages) {
148
169
  const { ignore } = getDiscoveryPatterns(lang)
@@ -61,6 +61,12 @@ describe('levenshtein', () => {
61
61
  test('insertion', () => {
62
62
  expect(levenshtein('test', 'tests')).toBe(1)
63
63
  })
64
+
65
+ test('distance is symmetric', () => {
66
+ expect(levenshtein('verifyToken', 'tokenVerify')).toBe(
67
+ levenshtein('tokenVerify', 'verifyToken'),
68
+ )
69
+ })
64
70
  })
65
71
 
66
72
  describe('splitCamelCase', () => {
@@ -363,4 +363,11 @@ describe('GoParser', () => {
363
363
  const resolved = await parser.resolveImports(files, '/tmp/no-gomod-' + Date.now())
364
364
  expect(resolved.length).toBe(1)
365
365
  })
366
+
367
+ test('parse keeps deterministic hash for identical input', async () => {
368
+ const parser = new GoParser()
369
+ const a = await parser.parse('auth/service.go', SIMPLE_GO)
370
+ const b = await parser.parse('auth/service.go', SIMPLE_GO)
371
+ expect(a.hash).toBe(b.hash)
372
+ })
366
373
  })
@@ -167,4 +167,14 @@ describe('ClusterDetector', () => {
167
167
  expect(score).toBeGreaterThanOrEqual(0)
168
168
  expect(score).toBeLessThanOrEqual(1)
169
169
  })
170
+
171
+ it('keeps distinct function nodes for same function names in different files', () => {
172
+ const files = [
173
+ mockParsedFile('src/auth/a.ts', [mockFunction('shared', [], 'src/auth/a.ts')]),
174
+ mockParsedFile('src/db/b.ts', [mockFunction('shared', [], 'src/db/b.ts')]),
175
+ ]
176
+ const graph = new GraphBuilder().build(files)
177
+ expect(graph.nodes.has('fn:src/auth/a.ts:shared')).toBe(true)
178
+ expect(graph.nodes.has('fn:src/db/b.ts:shared')).toBe(true)
179
+ })
170
180
  })
@@ -46,4 +46,10 @@ describe('computeRootHash', () => {
46
46
  const hash2 = computeRootHash({ payments: 'def', auth: 'abc' })
47
47
  expect(hash1).toBe(hash2)
48
48
  })
49
+
50
+ it('changes when a module hash changes', () => {
51
+ const base = computeRootHash({ auth: 'abc', payments: 'def' })
52
+ const changed = computeRootHash({ auth: 'abc', payments: 'xyz' })
53
+ expect(base).not.toBe(changed)
54
+ })
49
55
  })
@@ -71,4 +71,17 @@ describe('ImpactAnalyzer - Classified', () => {
71
71
  expect(result.classified.low).toHaveLength(1)
72
72
  expect(result.classified.low[0].nodeId).toBe('fn:src/highriskauthservice.ts:highriskauthservice')
73
73
  })
74
+
75
+ it('deduplicates impacted nodes when changed list includes duplicates', () => {
76
+ const graph = buildTestGraph([
77
+ ['A', 'B'],
78
+ ['B', 'C'],
79
+ ['C', 'nothing'],
80
+ ])
81
+
82
+ const analyzer = new ImpactAnalyzer(graph)
83
+ const result = analyzer.analyze(['fn:src/c.ts:c', 'fn:src/c.ts:c'])
84
+ const unique = new Set(result.impacted)
85
+ expect(unique.size).toBe(result.impacted.length)
86
+ })
74
87
  })
@@ -1042,6 +1042,16 @@ describe('JavaScript - Additional Edge Cases', () => {
1042
1042
  })
1043
1043
  })
1044
1044
 
1045
+ describe('Deterministic parsing', () => {
1046
+ test('produces identical file hash for identical source', async () => {
1047
+ const parser = new JavaScriptParser()
1048
+ const source = 'export function stable() { return 1 }'
1049
+ const a = await parser.parse('src/stable.js', source)
1050
+ const b = await parser.parse('src/stable.js', source)
1051
+ expect(a.hash).toBe(b.hash)
1052
+ })
1053
+ })
1054
+
1045
1055
  describe('Chained Methods', () => {
1046
1056
  test('handles method chaining', () => {
1047
1057
  const src = `
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import {
3
+ parserKindForExtension,
4
+ languageForExtension,
5
+ getParserExtensions,
6
+ getDiscoveryExtensions,
7
+ isTreeSitterExtension,
8
+ } from '../src/utils/language-registry'
9
+
10
+ describe('language-registry', () => {
11
+ it('maps parser kinds correctly', () => {
12
+ expect(parserKindForExtension('.ts')).toBe('oxc')
13
+ expect(parserKindForExtension('.go')).toBe('go')
14
+ expect(parserKindForExtension('.py')).toBe('tree-sitter')
15
+ expect(parserKindForExtension('.unknown')).toBe('unknown')
16
+ })
17
+
18
+ it('maps parser kinds case-insensitively', () => {
19
+ expect(parserKindForExtension('.TS')).toBe('oxc')
20
+ expect(parserKindForExtension('.Go')).toBe('go')
21
+ expect(parserKindForExtension('.RB')).toBe('tree-sitter')
22
+ })
23
+
24
+ it('maps languages correctly across major ecosystems', () => {
25
+ expect(languageForExtension('.ts')).toBe('typescript')
26
+ expect(languageForExtension('.js')).toBe('typescript')
27
+ expect(languageForExtension('.py')).toBe('python')
28
+ expect(languageForExtension('.go')).toBe('go')
29
+ expect(languageForExtension('.rs')).toBe('rust')
30
+ expect(languageForExtension('.java')).toBe('java')
31
+ expect(languageForExtension('.kt')).toBe('kotlin')
32
+ expect(languageForExtension('.kts')).toBe('kotlin')
33
+ expect(languageForExtension('.swift')).toBe('swift')
34
+ expect(languageForExtension('.rb')).toBe('ruby')
35
+ expect(languageForExtension('.php')).toBe('php')
36
+ expect(languageForExtension('.cs')).toBe('csharp')
37
+ expect(languageForExtension('.c')).toBe('c')
38
+ expect(languageForExtension('.cpp')).toBe('cpp')
39
+ })
40
+
41
+ it('returns parser extension sets', () => {
42
+ expect(getParserExtensions('oxc')).toContain('.tsx')
43
+ expect(getParserExtensions('go')).toEqual(['.go'])
44
+ expect(getParserExtensions('tree-sitter')).toContain('.swift')
45
+ })
46
+
47
+ it('returns discovery extension sets for mixed JVM repos', () => {
48
+ const javaDiscovery = getDiscoveryExtensions('java')
49
+ expect(javaDiscovery).toContain('.java')
50
+ expect(javaDiscovery).toContain('.kt')
51
+ expect(javaDiscovery).toContain('.kts')
52
+
53
+ // Even though Java discovery includes Kotlin files, extension-to-language remains Kotlin.
54
+ expect(languageForExtension('.kt')).toBe('kotlin')
55
+ expect(languageForExtension('.kts')).toBe('kotlin')
56
+ })
57
+
58
+ it('identifies tree-sitter extensions', () => {
59
+ expect(isTreeSitterExtension('.py')).toBe(true)
60
+ expect(isTreeSitterExtension('.swift')).toBe(true)
61
+ expect(isTreeSitterExtension('.ts')).toBe(false)
62
+ expect(isTreeSitterExtension('.go')).toBe(false)
63
+ })
64
+ })
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { parseFilesWithDiagnostics } from '../src/parser/index'
3
+
4
+ describe('parseFilesWithDiagnostics preflight', () => {
5
+ it('returns parser-unavailable diagnostic and no files in strict preflight mode', async () => {
6
+ const result = await parseFilesWithDiagnostics(
7
+ ['src/example.py'],
8
+ '/project',
9
+ async () => 'def run():\n return 1\n',
10
+ {
11
+ strictParserPreflight: true,
12
+ treeSitterRuntimeAvailable: false,
13
+ },
14
+ )
15
+
16
+ expect(result.files).toHaveLength(0)
17
+ expect(result.summary.requestedFiles).toBe(1)
18
+ expect(result.summary.parsedFiles).toBe(0)
19
+ expect(result.summary.diagnostics).toBeGreaterThan(0)
20
+ expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(true)
21
+ })
22
+
23
+ it('falls back and continues in non-strict mode when parser runtime is unavailable', async () => {
24
+ const result = await parseFilesWithDiagnostics(
25
+ ['src/example.py'],
26
+ '/project',
27
+ async () => 'def run():\n return 1\n',
28
+ {
29
+ strictParserPreflight: false,
30
+ treeSitterRuntimeAvailable: false,
31
+ },
32
+ )
33
+
34
+ expect(result.files).toHaveLength(1)
35
+ expect(result.summary.diagnostics).toBeGreaterThan(0)
36
+ expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(true)
37
+ })
38
+
39
+ it('skips tree-sitter preflight when only oxc/go files are requested', async () => {
40
+ const result = await parseFilesWithDiagnostics(
41
+ ['src/index.ts'],
42
+ '/project',
43
+ async () => 'export function ok(): number { return 1 }',
44
+ {
45
+ strictParserPreflight: true,
46
+ treeSitterRuntimeAvailable: false,
47
+ },
48
+ )
49
+
50
+ expect(result.summary.requestedFiles).toBe(1)
51
+ expect(result.summary.parsedFiles).toBe(1)
52
+ expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(false)
53
+ })
54
+
55
+ it('returns language-correct fallback files for multiple tree-sitter languages in non-strict mode', async () => {
56
+ const files = [
57
+ 'src/main.py',
58
+ 'src/App.java',
59
+ 'src/service.kt',
60
+ 'src/tool.swift',
61
+ 'src/lib.rs',
62
+ 'src/Program.cs',
63
+ 'src/main.php',
64
+ 'src/app.rb',
65
+ 'src/native.c',
66
+ 'src/native.cpp',
67
+ ]
68
+
69
+ const result = await parseFilesWithDiagnostics(
70
+ files,
71
+ '/project',
72
+ async () => '',
73
+ {
74
+ strictParserPreflight: false,
75
+ treeSitterRuntimeAvailable: false,
76
+ },
77
+ )
78
+
79
+ expect(result.files).toHaveLength(files.length)
80
+ expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(true)
81
+
82
+ const languageBySuffix = (suffix: string): string | undefined => {
83
+ const hit = result.files.find(f => f.path.replace(/\\/g, '/').endsWith(suffix))
84
+ return hit?.language
85
+ }
86
+
87
+ expect(languageBySuffix('/src/main.py')).toBe('python')
88
+ expect(languageBySuffix('/src/App.java')).toBe('java')
89
+ expect(languageBySuffix('/src/service.kt')).toBe('kotlin')
90
+ expect(languageBySuffix('/src/tool.swift')).toBe('swift')
91
+ expect(languageBySuffix('/src/lib.rs')).toBe('rust')
92
+ expect(languageBySuffix('/src/Program.cs')).toBe('csharp')
93
+ expect(languageBySuffix('/src/main.php')).toBe('php')
94
+ expect(languageBySuffix('/src/app.rb')).toBe('ruby')
95
+ expect(languageBySuffix('/src/native.c')).toBe('c')
96
+ expect(languageBySuffix('/src/native.cpp')).toBe('cpp')
97
+ })
98
+
99
+ it('aborts full batch in strict preflight when any tree-sitter file is present', async () => {
100
+ const result = await parseFilesWithDiagnostics(
101
+ ['src/index.ts', 'src/main.py'],
102
+ '/project',
103
+ async () => 'export const ok = 1',
104
+ {
105
+ strictParserPreflight: true,
106
+ treeSitterRuntimeAvailable: false,
107
+ },
108
+ )
109
+
110
+ expect(result.files).toHaveLength(0)
111
+ expect(result.summary.requestedFiles).toBe(2)
112
+ expect(result.summary.parsedFiles).toBe(0)
113
+ expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(true)
114
+ })
115
+ })
@@ -729,6 +729,26 @@ describe('getParser - Comprehensive', () => {
729
729
  const parser = getParser('src/lib/utils/helper.ts')
730
730
  expect(parser).toBeInstanceOf(OxcParser)
731
731
  })
732
+
733
+ it('supports C++ variant extensions via tree-sitter parser', () => {
734
+ const cxxParser = getParser('src/engine.cxx')
735
+ const hxxParser = getParser('src/engine.hxx')
736
+ const hhParser = getParser('src/engine.hh')
737
+
738
+ expect(cxxParser.getSupportedExtensions()).toContain('.cxx')
739
+ expect(hxxParser.getSupportedExtensions()).toContain('.hxx')
740
+ expect(hhParser.getSupportedExtensions()).toContain('.hh')
741
+ })
742
+
743
+ it('supports Rust/C#/Swift extensions via tree-sitter parser', () => {
744
+ const rustParser = getParser('src/lib.rs')
745
+ const csharpParser = getParser('src/Program.cs')
746
+ const swiftParser = getParser('Sources/App/main.swift')
747
+
748
+ expect(rustParser.getSupportedExtensions()).toContain('.rs')
749
+ expect(csharpParser.getSupportedExtensions()).toContain('.cs')
750
+ expect(swiftParser.getSupportedExtensions()).toContain('.swift')
751
+ })
732
752
  })
733
753
 
734
754
  describe('OxcParser - Direct', () => {
@@ -751,4 +771,20 @@ describe('OxcParser - Direct', () => {
751
771
  const result = await parser.parse('large.ts', content)
752
772
  expect(result.functions.length).toBe(1000)
753
773
  })
774
+
775
+ it('extracts direct and method call expressions', async () => {
776
+ const result = await parser.parse('calls.ts', `
777
+ function b() { return 1 }
778
+ const svc = { run() { return 2 } }
779
+ export function a() {
780
+ b()
781
+ svc.run()
782
+ }
783
+ `)
784
+
785
+ const a = result.functions.find(f => f.name === 'a')
786
+ expect(a).toBeDefined()
787
+ expect(a!.calls.some(c => c.name === 'b')).toBe(true)
788
+ expect(a!.calls.some(c => c.name === 'svc.run')).toBe(true)
789
+ })
754
790
  })
@@ -268,6 +268,34 @@ struct PrivateStruct {
268
268
  expect(pubStruct?.isExported).toBe(true)
269
269
  expect(privStruct?.isExported).toBe(false)
270
270
  })
271
+
272
+ test('parses Rust traits and impl blocks', async () => {
273
+ const content = `
274
+ pub trait UserRepository {
275
+ fn find_user(&self, id: String) -> Option<String>;
276
+ }
277
+
278
+ pub struct InMemoryRepo;
279
+
280
+ impl UserRepository for InMemoryRepo {
281
+ fn find_user(&self, id: String) -> Option<String> {
282
+ Some(id)
283
+ }
284
+ }
285
+ `
286
+ const result = await parser.parse('repository.rs', content)
287
+
288
+ expect(result.path).toBe('repository.rs')
289
+ expect(result.language).toBe('rust')
290
+
291
+ if (result.classes.length > 0) {
292
+ expect(result.classes.some(c => c.name === 'UserRepository')).toBe(true)
293
+ expect(result.classes.some(c => c.name === 'InMemoryRepo')).toBe(true)
294
+ }
295
+ if (result.functions.length > 0) {
296
+ expect(result.functions.some(f => f.name === 'find_user')).toBe(true)
297
+ }
298
+ })
271
299
  })
272
300
 
273
301
  // ==========================================
@@ -337,6 +365,31 @@ public:
337
365
  expect(result.classes[0].name).toBe('Calculator')
338
366
  expect(result.functions.length).toBeGreaterThanOrEqual(1)
339
367
  })
368
+
369
+ test('parses C++ templates with macro-heavy headers', async () => {
370
+ const content = `
371
+ #ifndef VECTOR_UTILS_H
372
+ #define VECTOR_UTILS_H
373
+
374
+ #include <vector>
375
+
376
+ #define INLINE inline
377
+
378
+ template <typename T>
379
+ INLINE T max_value(const T& a, const T& b) {
380
+ return a > b ? a : b;
381
+ }
382
+
383
+ #endif
384
+ `
385
+ const result = await parser.parse('vector_utils.hpp', content)
386
+
387
+ expect(result.path).toBe('vector_utils.hpp')
388
+ expect(result.language).toBe('cpp')
389
+ if (result.functions.length > 0) {
390
+ expect(result.functions.some(f => f.name === 'max_value')).toBe(true)
391
+ }
392
+ })
340
393
  })
341
394
 
342
395
  // ==========================================
@@ -393,6 +446,36 @@ class VisibilityTest {
393
446
  expect(priv?.isExported).toBe(false)
394
447
  expect(prot?.isExported).toBe(false)
395
448
  })
449
+
450
+ test('parses framework-style PHP controller classes', async () => {
451
+ const content = `
452
+ <?php
453
+ namespace App\\Http\\Controllers;
454
+
455
+ use Illuminate\\Http\\Request;
456
+
457
+ class UserController extends Controller {
458
+ public function index(Request $request): array {
459
+ return [];
460
+ }
461
+
462
+ public function show(string $id): array {
463
+ return ['id' => $id];
464
+ }
465
+ }
466
+ `
467
+ const result = await parser.parse('UserController.php', content)
468
+
469
+ expect(result.path).toBe('UserController.php')
470
+ expect(result.language).toBe('php')
471
+ if (result.classes.length > 0) {
472
+ expect(result.classes.some(c => c.name === 'UserController')).toBe(true)
473
+ }
474
+ if (result.functions.length > 0) {
475
+ expect(result.functions.some(f => f.name === 'index')).toBe(true)
476
+ expect(result.functions.some(f => f.name === 'show')).toBe(true)
477
+ }
478
+ })
396
479
  })
397
480
 
398
481
  // ==========================================
@@ -432,6 +515,93 @@ namespace App.Services {
432
515
  expect(result.path).toBe('UserService.cs')
433
516
  }
434
517
  })
518
+
519
+ test('parses C# attributes and ASP.NET controller routes', async () => {
520
+ const content = `
521
+ using Microsoft.AspNetCore.Mvc;
522
+
523
+ namespace App.Api.Controllers {
524
+ [ApiController]
525
+ [Route("api/[controller]")]
526
+ public class UsersController : ControllerBase {
527
+ [HttpGet("{id}")]
528
+ public ActionResult<string> GetById(string id) {
529
+ return id;
530
+ }
531
+
532
+ [HttpPost]
533
+ public IActionResult Create([FromBody] object payload) {
534
+ return Ok(payload);
535
+ }
536
+ }
537
+ }
538
+ `
539
+ const result = await parser.parse('UsersController.cs', content)
540
+
541
+ expect(result.path).toBe('UsersController.cs')
542
+ expect(result.language).toBe('csharp')
543
+ if (result.functions.length > 0) {
544
+ expect(result.functions.some(f => f.name === 'GetById')).toBe(true)
545
+ expect(result.functions.some(f => f.name === 'Create')).toBe(true)
546
+ }
547
+ })
548
+ })
549
+
550
+ describe('Kotlin - Coroutines & Extensions', () => {
551
+ test('parses Kotlin coroutine and extension functions', async () => {
552
+ const content = `
553
+ package app.service
554
+
555
+ class UserService {
556
+ suspend fun fetchUser(id: String): String {
557
+ return id
558
+ }
559
+ }
560
+
561
+ fun String.slugify(): String {
562
+ return this.lowercase().replace(" ", "-")
563
+ }
564
+ `
565
+ let result
566
+ try {
567
+ result = await parser.parse('UserService.kt', content)
568
+ } catch {
569
+ result = { path: 'UserService.kt', language: 'kotlin', functions: [], classes: [] }
570
+ }
571
+
572
+ expect(result.path).toBe('UserService.kt')
573
+ expect(result.language).toBe('kotlin')
574
+ if (result.functions.length > 0) {
575
+ expect(result.functions.some(f => f.name === 'fetchUser')).toBe(true)
576
+ expect(result.functions.some(f => f.name === 'slugify')).toBe(true)
577
+ }
578
+ })
579
+ })
580
+
581
+ describe('Swift - Protocol & Package Patterns', () => {
582
+ test('parses Swift protocol-based service code', async () => {
583
+ const content = `
584
+ import Foundation
585
+
586
+ protocol UserRepository {
587
+ func findUser(id: String) -> String?
588
+ }
589
+
590
+ struct InMemoryUserRepository: UserRepository {
591
+ func findUser(id: String) -> String? {
592
+ return id
593
+ }
594
+ }
595
+ `
596
+ const result = await parser.parse('Sources/App/UserRepository.swift', content)
597
+
598
+ expect(result.path).toBe('Sources/App/UserRepository.swift')
599
+ expect(result.language).toBe('swift')
600
+ if (result.classes.length > 0) {
601
+ expect(result.classes.some(c => c.name === 'UserRepository')).toBe(true)
602
+ expect(result.classes.some(c => c.name === 'InMemoryUserRepository')).toBe(true)
603
+ }
604
+ })
435
605
  })
436
606
 
437
607
  // ==========================================
@@ -473,6 +643,30 @@ end
473
643
 
474
644
  expect(result.path).toBe('auth.rb')
475
645
  })
646
+
647
+ test('parses Ruby DSL-heavy model patterns', async () => {
648
+ const content = `
649
+ class User < ApplicationRecord
650
+ scope :active, -> { where(active: true) }
651
+ validates :email, presence: true
652
+
653
+ def full_name
654
+ "#{first_name} #{last_name}"
655
+ end
656
+ end
657
+ `
658
+ let result
659
+ try {
660
+ result = await parser.parse('user.rb', content)
661
+ } catch {
662
+ result = { path: 'user.rb', language: 'ruby', functions: [], classes: [] }
663
+ }
664
+
665
+ expect(result.path).toBe('user.rb')
666
+ if (result.functions.length > 0) {
667
+ expect(result.functions.some((f: { name: string }) => f.name === 'full_name')).toBe(true)
668
+ }
669
+ })
476
670
  })
477
671
 
478
672
  // ==========================================
@@ -869,5 +1063,12 @@ raw = r"raw \\string"
869
1063
 
870
1064
  expect(result.imports.length).toBeGreaterThan(0)
871
1065
  })
1066
+
1067
+ test('produces identical hash for identical content', async () => {
1068
+ const content = 'def stable():\n return 42\n'
1069
+ const a = await parser.parse('stable.py', content)
1070
+ const b = await parser.parse('stable.py', content)
1071
+ expect(a.hash).toBe(b.hash)
1072
+ })
872
1073
  })
873
1074
  })
@@ -113,4 +113,10 @@ describe('TypeScriptParser Edge Cases & Fault Tolerance', () => {
113
113
  expect(result.functions).toHaveLength(0)
114
114
  expect(result.hash).toBeDefined()
115
115
  })
116
+
117
+ it('parses Windows line endings consistently', async () => {
118
+ const winCode = 'export function ping() {\r\n return 1\r\n}\r\n'
119
+ const result = await parser.parse('src/win.ts', winCode)
120
+ expect(result.functions.some(f => f.name === 'ping')).toBe(true)
121
+ })
116
122
  })