@getmikk/core 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -6
- package/out.log +0 -0
- package/package.json +4 -3
- package/src/contract/adr-manager.ts +75 -0
- package/src/contract/index.ts +2 -0
- package/src/graph/dead-code-detector.ts +194 -0
- package/src/graph/graph-builder.ts +6 -2
- package/src/graph/impact-analyzer.ts +53 -2
- package/src/graph/index.ts +4 -1
- package/src/graph/types.ts +21 -0
- package/src/parser/go/go-extractor.ts +712 -0
- package/src/parser/go/go-parser.ts +41 -0
- package/src/parser/go/go-resolver.ts +70 -0
- package/src/parser/index.ts +27 -6
- package/src/parser/types.ts +1 -1
- package/src/parser/typescript/ts-extractor.ts +65 -18
- package/src/parser/typescript/ts-parser.ts +41 -1
- package/test-output.txt +0 -0
- package/tests/adr-manager.test.ts +97 -0
- package/tests/dead-code.test.ts +134 -0
- package/tests/go-parser.test.ts +366 -0
- package/tests/impact-classified.test.ts +78 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { GoExtractor } from '../src/parser/go/go-extractor'
|
|
3
|
+
import { GoParser } from '../src/parser/go/go-parser'
|
|
4
|
+
|
|
5
|
+
// ─── Sample Go source files ────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const SIMPLE_GO = `
|
|
8
|
+
package auth
|
|
9
|
+
|
|
10
|
+
import (
|
|
11
|
+
"context"
|
|
12
|
+
"errors"
|
|
13
|
+
"github.com/golang-jwt/jwt/v5"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// UserClaims holds the JWT payload for authenticated users.
|
|
17
|
+
type UserClaims struct {
|
|
18
|
+
UserID string
|
|
19
|
+
Email string
|
|
20
|
+
Role string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// AuthService handles token validation and user authentication.
|
|
24
|
+
type AuthService struct {
|
|
25
|
+
secretKey string
|
|
26
|
+
db DBClient
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// VerifyToken validates a JWT string and returns the decoded claims.
|
|
30
|
+
func (s *AuthService) VerifyToken(tokenStr string) (*UserClaims, error) {
|
|
31
|
+
if tokenStr == "" {
|
|
32
|
+
return nil, errors.New("empty token")
|
|
33
|
+
}
|
|
34
|
+
token, err := jwt.Parse(tokenStr, keyFunc(s.secretKey))
|
|
35
|
+
if err != nil {
|
|
36
|
+
return nil, err
|
|
37
|
+
}
|
|
38
|
+
claims, ok := token.Claims.(*UserClaims)
|
|
39
|
+
if !ok {
|
|
40
|
+
return nil, errors.New("invalid claims")
|
|
41
|
+
}
|
|
42
|
+
return claims, nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// GetUserByID fetches a user from the database by ID.
|
|
46
|
+
func (s *AuthService) GetUserByID(ctx context.Context, id string) (*User, error) {
|
|
47
|
+
if id == "" {
|
|
48
|
+
return nil, errors.New("empty id")
|
|
49
|
+
}
|
|
50
|
+
return s.db.FindUser(ctx, id)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// hashPassword hashes a plain-text password using bcrypt.
|
|
54
|
+
func hashPassword(password string) (string, error) {
|
|
55
|
+
return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// keyFunc is an internal helper to build a jwt key lookup function.
|
|
59
|
+
func keyFunc(secret string) jwt.Keyfunc {
|
|
60
|
+
return func(token *jwt.Token) (interface{}, error) {
|
|
61
|
+
return []byte(secret), nil
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`
|
|
65
|
+
|
|
66
|
+
const ROUTES_GO = `
|
|
67
|
+
package api
|
|
68
|
+
|
|
69
|
+
import "github.com/gin-gonic/gin"
|
|
70
|
+
|
|
71
|
+
func RegisterRoutes(r *gin.Engine) {
|
|
72
|
+
r.GET("/health", healthCheck)
|
|
73
|
+
r.POST("/api/users", createUser)
|
|
74
|
+
r.GET("/api/users/:id", getUserByID)
|
|
75
|
+
r.PUT("/api/users/:id", updateUser)
|
|
76
|
+
r.DELETE("/api/users/:id", deleteUser)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func healthCheck(c *gin.Context) {
|
|
80
|
+
c.JSON(200, gin.H{"status": "ok"})
|
|
81
|
+
}
|
|
82
|
+
`
|
|
83
|
+
|
|
84
|
+
const TOPLEVEL_GO = `
|
|
85
|
+
package utils
|
|
86
|
+
|
|
87
|
+
import (
|
|
88
|
+
"fmt"
|
|
89
|
+
"strings"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// FormatName formats first and last name together.
|
|
93
|
+
func FormatName(first, last string) string {
|
|
94
|
+
return fmt.Sprintf("%s %s", strings.TrimSpace(first), strings.TrimSpace(last))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// IsEmpty checks if a string has no meaningful content.
|
|
98
|
+
func IsEmpty(s string) bool {
|
|
99
|
+
return strings.TrimSpace(s) == ""
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// internal helper: not exported
|
|
103
|
+
func normalize(s string) string {
|
|
104
|
+
return strings.ToLower(strings.TrimSpace(s))
|
|
105
|
+
}
|
|
106
|
+
`
|
|
107
|
+
|
|
108
|
+
// ─── GoExtractor tests ─────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe('GoExtractor', () => {
|
|
111
|
+
describe('function extraction', () => {
|
|
112
|
+
test('extracts top-level exported functions', () => {
|
|
113
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
114
|
+
const fns = ext.extractFunctions()
|
|
115
|
+
const names = fns.map(f => f.name)
|
|
116
|
+
expect(names).toContain('FormatName')
|
|
117
|
+
expect(names).toContain('IsEmpty')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('extracts unexported top-level functions', () => {
|
|
121
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
122
|
+
const fns = ext.extractFunctions()
|
|
123
|
+
const names = fns.map(f => f.name)
|
|
124
|
+
expect(names).toContain('normalize')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('marks exported functions correctly', () => {
|
|
128
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
129
|
+
const fns = ext.extractFunctions()
|
|
130
|
+
const formatName = fns.find(f => f.name === 'FormatName')
|
|
131
|
+
const normalizeF = fns.find(f => f.name === 'normalize')
|
|
132
|
+
expect(formatName?.isExported).toBe(true)
|
|
133
|
+
expect(normalizeF?.isExported).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('does NOT include methods in extractFunctions()', () => {
|
|
137
|
+
const ext = new GoExtractor('auth/service.go', SIMPLE_GO)
|
|
138
|
+
const fns = ext.extractFunctions()
|
|
139
|
+
const names = fns.map(f => f.name)
|
|
140
|
+
// Methods have receiver — should not appear in top-level functions
|
|
141
|
+
expect(names).not.toContain('VerifyToken')
|
|
142
|
+
expect(names).not.toContain('GetUserByID')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('extracts purpose from leading comment', () => {
|
|
146
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
147
|
+
const fns = ext.extractFunctions()
|
|
148
|
+
const formatName = fns.find(f => f.name === 'FormatName')
|
|
149
|
+
expect(formatName?.purpose).toContain('formats')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('extracts params with name and type', () => {
|
|
153
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
154
|
+
const fns = ext.extractFunctions()
|
|
155
|
+
const formatName = fns.find(f => f.name === 'FormatName')
|
|
156
|
+
expect(formatName?.params.length).toBeGreaterThan(0)
|
|
157
|
+
const paramNames = formatName!.params.map(p => p.name)
|
|
158
|
+
expect(paramNames).toContain('first')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('extracts return type', () => {
|
|
162
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
163
|
+
const fns = ext.extractFunctions()
|
|
164
|
+
const formatName = fns.find(f => f.name === 'FormatName')
|
|
165
|
+
expect(formatName?.returnType).toBeTruthy()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('extracts function ID with correct format', () => {
|
|
169
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
170
|
+
const fns = ext.extractFunctions()
|
|
171
|
+
const formatName = fns.find(f => f.name === 'FormatName')
|
|
172
|
+
expect(formatName?.id).toBe('fn:utils/format.go:FormatName')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('populates startLine and endLine', () => {
|
|
176
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
177
|
+
const fns = ext.extractFunctions()
|
|
178
|
+
for (const fn of fns) {
|
|
179
|
+
expect(fn.startLine).toBeGreaterThan(0)
|
|
180
|
+
expect(fn.endLine).toBeGreaterThanOrEqual(fn.startLine)
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('extracts calls within function body', () => {
|
|
185
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
186
|
+
const fns = ext.extractFunctions()
|
|
187
|
+
const formatName = fns.find(f => f.name === 'FormatName')
|
|
188
|
+
// Should detect calls to Sprintf, TrimSpace
|
|
189
|
+
expect(formatName?.calls.length).toBeGreaterThan(0)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('class extraction (structs)', () => {
|
|
194
|
+
test('extracts struct types as classes', () => {
|
|
195
|
+
const ext = new GoExtractor('auth/service.go', SIMPLE_GO)
|
|
196
|
+
const classes = ext.extractClasses()
|
|
197
|
+
const names = classes.map(c => c.name)
|
|
198
|
+
expect(names).toContain('AuthService')
|
|
199
|
+
expect(names).toContain('UserClaims')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('groups receiver methods into struct classes', () => {
|
|
203
|
+
const ext = new GoExtractor('auth/service.go', SIMPLE_GO)
|
|
204
|
+
const classes = ext.extractClasses()
|
|
205
|
+
const authService = classes.find(c => c.name === 'AuthService')
|
|
206
|
+
expect(authService).toBeDefined()
|
|
207
|
+
const methodNames = authService!.methods.map(m => m.name)
|
|
208
|
+
expect(methodNames.some(n => n.includes('VerifyToken'))).toBe(true)
|
|
209
|
+
expect(methodNames.some(n => n.includes('GetUserByID'))).toBe(true)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('marks exported structs correctly', () => {
|
|
213
|
+
const ext = new GoExtractor('auth/service.go', SIMPLE_GO)
|
|
214
|
+
const classes = ext.extractClasses()
|
|
215
|
+
const authService = classes.find(c => c.name === 'AuthService')
|
|
216
|
+
expect(authService?.isExported).toBe(true)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('method IDs use Receiver.Method format', () => {
|
|
220
|
+
const ext = new GoExtractor('auth/service.go', SIMPLE_GO)
|
|
221
|
+
const classes = ext.extractClasses()
|
|
222
|
+
const authService = classes.find(c => c.name === 'AuthService')
|
|
223
|
+
const verifyToken = authService?.methods.find(m => m.name.includes('VerifyToken'))
|
|
224
|
+
expect(verifyToken?.id).toBe('fn:auth/service.go:AuthService.VerifyToken')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('extracts purpose for structs', () => {
|
|
228
|
+
const ext = new GoExtractor('auth/service.go', SIMPLE_GO)
|
|
229
|
+
const classes = ext.extractClasses()
|
|
230
|
+
const authService = classes.find(c => c.name === 'AuthService')
|
|
231
|
+
expect(authService?.purpose).toContain('authentication')
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe('import extraction', () => {
|
|
236
|
+
test('extracts block imports', () => {
|
|
237
|
+
const ext = new GoExtractor('auth/service.go', SIMPLE_GO)
|
|
238
|
+
const imports = ext.extractImports()
|
|
239
|
+
const sources = imports.map(i => i.source)
|
|
240
|
+
expect(sources).toContain('context')
|
|
241
|
+
expect(sources).toContain('errors')
|
|
242
|
+
expect(sources).toContain('github.com/golang-jwt/jwt/v5')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('extracts single-line imports', () => {
|
|
246
|
+
const src = `package main\nimport "fmt"\nimport "os"\n`
|
|
247
|
+
const ext = new GoExtractor('main.go', src)
|
|
248
|
+
const imports = ext.extractImports()
|
|
249
|
+
const sources = imports.map(i => i.source)
|
|
250
|
+
expect(sources).toContain('fmt')
|
|
251
|
+
expect(sources).toContain('os')
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('uses last path segment as import name', () => {
|
|
255
|
+
const ext = new GoExtractor('auth/service.go', SIMPLE_GO)
|
|
256
|
+
const imports = ext.extractImports()
|
|
257
|
+
const jwtImport = imports.find(i => i.source.includes('jwt'))
|
|
258
|
+
expect(jwtImport?.names[0]).toBe('v5')
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('export extraction', () => {
|
|
263
|
+
test('only exports uppercase identifiers', () => {
|
|
264
|
+
const ext = new GoExtractor('utils/format.go', TOPLEVEL_GO)
|
|
265
|
+
const exports = ext.extractExports()
|
|
266
|
+
const names = exports.map(e => e.name)
|
|
267
|
+
expect(names).toContain('FormatName')
|
|
268
|
+
expect(names).toContain('IsEmpty')
|
|
269
|
+
expect(names).not.toContain('normalize')
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('route detection', () => {
|
|
274
|
+
test('detects Gin routes with correct methods', () => {
|
|
275
|
+
const ext = new GoExtractor('api/routes.go', ROUTES_GO)
|
|
276
|
+
const routes = ext.extractRoutes()
|
|
277
|
+
expect(routes.length).toBeGreaterThanOrEqual(4)
|
|
278
|
+
const methods = routes.map(r => r.method)
|
|
279
|
+
expect(methods).toContain('GET')
|
|
280
|
+
expect(methods).toContain('POST')
|
|
281
|
+
expect(methods).toContain('PUT')
|
|
282
|
+
expect(methods).toContain('DELETE')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('extracts route paths correctly', () => {
|
|
286
|
+
const ext = new GoExtractor('api/routes.go', ROUTES_GO)
|
|
287
|
+
const routes = ext.extractRoutes()
|
|
288
|
+
const paths = routes.map(r => r.path)
|
|
289
|
+
expect(paths).toContain('/health')
|
|
290
|
+
expect(paths).toContain('/api/users')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('routes have correct file and line', () => {
|
|
294
|
+
const ext = new GoExtractor('api/routes.go', ROUTES_GO)
|
|
295
|
+
const routes = ext.extractRoutes()
|
|
296
|
+
for (const route of routes) {
|
|
297
|
+
expect(route.file).toBe('api/routes.go')
|
|
298
|
+
expect(route.line).toBeGreaterThan(0)
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('error handling extraction', () => {
|
|
304
|
+
test('detects "if err != nil" patterns', () => {
|
|
305
|
+
const ext = new GoExtractor('auth/service.go', SIMPLE_GO)
|
|
306
|
+
const classes = ext.extractClasses()
|
|
307
|
+
const authService = classes.find(c => c.name === 'AuthService')
|
|
308
|
+
const verifyToken = authService?.methods.find(m => m.name.includes('VerifyToken'))
|
|
309
|
+
expect(verifyToken?.errorHandling.length).toBeGreaterThan(0)
|
|
310
|
+
expect(verifyToken?.errorHandling[0].type).toBe('try-catch')
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// ─── GoParser integration tests ────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
describe('GoParser', () => {
|
|
318
|
+
test('getSupportedExtensions returns .go', () => {
|
|
319
|
+
const parser = new GoParser()
|
|
320
|
+
expect(parser.getSupportedExtensions()).toContain('.go')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('parse returns a well-formed ParsedFile', () => {
|
|
324
|
+
const parser = new GoParser()
|
|
325
|
+
const result = parser.parse('auth/service.go', SIMPLE_GO)
|
|
326
|
+
expect(result.path).toBe('auth/service.go')
|
|
327
|
+
expect(result.language).toBe('go')
|
|
328
|
+
expect(Array.isArray(result.functions)).toBe(true)
|
|
329
|
+
expect(Array.isArray(result.classes)).toBe(true)
|
|
330
|
+
expect(Array.isArray(result.imports)).toBe(true)
|
|
331
|
+
expect(Array.isArray(result.exports)).toBe(true)
|
|
332
|
+
expect(Array.isArray(result.routes)).toBe(true)
|
|
333
|
+
expect(typeof result.hash).toBe('string')
|
|
334
|
+
expect(result.hash.length).toBeGreaterThan(0)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test('parse populates functions from a real Go service', () => {
|
|
338
|
+
const parser = new GoParser()
|
|
339
|
+
const result = parser.parse('auth/service.go', SIMPLE_GO)
|
|
340
|
+
// Top-level funcs: hashPassword, keyFunc
|
|
341
|
+
expect(result.functions.some(f => f.name === 'hashPassword')).toBe(true)
|
|
342
|
+
expect(result.functions.some(f => f.name === 'keyFunc')).toBe(true)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('parse populates classes for structs with methods', () => {
|
|
346
|
+
const parser = new GoParser()
|
|
347
|
+
const result = parser.parse('auth/service.go', SIMPLE_GO)
|
|
348
|
+
const authService = result.classes.find(c => c.name === 'AuthService')
|
|
349
|
+
expect(authService).toBeDefined()
|
|
350
|
+
expect(authService!.methods.length).toBeGreaterThan(0)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
test('parse detects routes in a Gin router file', () => {
|
|
354
|
+
const parser = new GoParser()
|
|
355
|
+
const result = parser.parse('api/routes.go', ROUTES_GO)
|
|
356
|
+
expect(result.routes.length).toBeGreaterThanOrEqual(4)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('resolveImports passes through without crashing on no go.mod', () => {
|
|
360
|
+
const parser = new GoParser()
|
|
361
|
+
const files = [parser.parse('utils/format.go', TOPLEVEL_GO)]
|
|
362
|
+
// Should not throw even without go.mod
|
|
363
|
+
const resolved = parser.resolveImports(files, '/tmp/no-gomod-' + Date.now())
|
|
364
|
+
expect(resolved.length).toBe(1)
|
|
365
|
+
})
|
|
366
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { ImpactAnalyzer } from '../src/graph/impact-analyzer'
|
|
3
|
+
import { buildTestGraph } from './helpers'
|
|
4
|
+
import { GraphBuilder } from '../src/graph/graph-builder'
|
|
5
|
+
|
|
6
|
+
describe('ImpactAnalyzer - Classified', () => {
|
|
7
|
+
|
|
8
|
+
it('classifies impacts based on depth and module boundaries', () => {
|
|
9
|
+
// Build graph with module boundaries:
|
|
10
|
+
// A (module: m1) calls B (module: m1) calls C (module: m2) calls D (module: m3)
|
|
11
|
+
// We are changing D. What is the impact?
|
|
12
|
+
// D is changed
|
|
13
|
+
// C is depth 1, crosses boundary -> CRITICAL
|
|
14
|
+
// B is depth 2 -> MEDIUM
|
|
15
|
+
// A is depth 3 -> LOW
|
|
16
|
+
|
|
17
|
+
const graph = buildTestGraph([
|
|
18
|
+
['A', 'B'],
|
|
19
|
+
['B', 'C'],
|
|
20
|
+
['C', 'D'],
|
|
21
|
+
['D', 'nothing']
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
// Assign modules manually for the test
|
|
25
|
+
graph.nodes.get('src/A.ts')!.moduleId = 'm1'
|
|
26
|
+
graph.nodes.get('fn:src/A.ts:A')!.moduleId = 'm1'
|
|
27
|
+
|
|
28
|
+
graph.nodes.get('src/B.ts')!.moduleId = 'm1'
|
|
29
|
+
graph.nodes.get('fn:src/B.ts:B')!.moduleId = 'm1'
|
|
30
|
+
|
|
31
|
+
graph.nodes.get('src/C.ts')!.moduleId = 'm2'
|
|
32
|
+
graph.nodes.get('fn:src/C.ts:C')!.moduleId = 'm2'
|
|
33
|
+
|
|
34
|
+
graph.nodes.get('src/D.ts')!.moduleId = 'm3'
|
|
35
|
+
graph.nodes.get('fn:src/D.ts:D')!.moduleId = 'm3'
|
|
36
|
+
|
|
37
|
+
const analyzer = new ImpactAnalyzer(graph)
|
|
38
|
+
const result = analyzer.analyze(['fn:src/D.ts:D'])
|
|
39
|
+
|
|
40
|
+
expect(result.impacted.length).toBe(3)
|
|
41
|
+
|
|
42
|
+
expect(result.classified.critical).toHaveLength(1)
|
|
43
|
+
expect(result.classified.critical[0].nodeId).toBe('fn:src/C.ts:C')
|
|
44
|
+
|
|
45
|
+
expect(result.classified.high).toHaveLength(0) // No depth 1 in same module
|
|
46
|
+
|
|
47
|
+
expect(result.classified.medium).toHaveLength(1)
|
|
48
|
+
expect(result.classified.medium[0].nodeId).toBe('fn:src/B.ts:B')
|
|
49
|
+
|
|
50
|
+
expect(result.classified.low).toHaveLength(1)
|
|
51
|
+
expect(result.classified.low[0].nodeId).toBe('fn:src/A.ts:A')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('classifies same-module depth-1 impact as HIGH, not CRITICAL', () => {
|
|
55
|
+
// E (module: m4) calls F (module: m4) calls G (module: m4)
|
|
56
|
+
// change G
|
|
57
|
+
// F is depth 1, same module -> HIGH
|
|
58
|
+
// E is depth 2 -> MEDIUM
|
|
59
|
+
const graph = buildTestGraph([
|
|
60
|
+
['E', 'F'],
|
|
61
|
+
['F', 'G'],
|
|
62
|
+
['G', 'nothing']
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
for (const id of graph.nodes.keys()) {
|
|
66
|
+
graph.nodes.get(id)!.moduleId = 'm4'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const analyzer = new ImpactAnalyzer(graph)
|
|
70
|
+
const result = analyzer.analyze(['fn:src/G.ts:G'])
|
|
71
|
+
|
|
72
|
+
expect(result.classified.critical).toHaveLength(0)
|
|
73
|
+
expect(result.classified.high).toHaveLength(1)
|
|
74
|
+
expect(result.classified.high[0].nodeId).toBe('fn:src/F.ts:F') // depth 1
|
|
75
|
+
expect(result.classified.medium).toHaveLength(1)
|
|
76
|
+
expect(result.classified.medium[0].nodeId).toBe('fn:src/E.ts:E') // depth 2
|
|
77
|
+
})
|
|
78
|
+
})
|