@aiready/context-analyzer 0.21.22 → 0.21.24

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.
Files changed (78) hide show
  1. package/.turbo/turbo-build.log +26 -25
  2. package/.turbo/turbo-lint.log +5 -5
  3. package/.turbo/turbo-test.log +91 -41
  4. package/coverage/clover.xml +2615 -1242
  5. package/coverage/coverage-final.json +30 -13
  6. package/coverage/dist/chunk-64U3PNO3.mjs.html +367 -0
  7. package/coverage/dist/chunk-J3MUOWHC.mjs.html +5326 -0
  8. package/coverage/dist/index.html +146 -0
  9. package/coverage/{classifier.ts.html → dist/index.mjs.html} +537 -912
  10. package/coverage/index.html +84 -189
  11. package/coverage/src/analyzer.ts.html +88 -0
  12. package/coverage/src/analyzers/index.html +116 -0
  13. package/coverage/src/analyzers/python-context.ts.html +910 -0
  14. package/coverage/{ast-utils.ts.html → src/ast-utils.ts.html} +84 -54
  15. package/coverage/src/classifier.ts.html +892 -0
  16. package/coverage/src/classify/classification-patterns.ts.html +307 -0
  17. package/coverage/src/classify/file-classifiers.ts.html +973 -0
  18. package/coverage/src/classify/index.html +131 -0
  19. package/coverage/{cluster-detector.ts.html → src/cluster-detector.ts.html} +154 -91
  20. package/coverage/{defaults.ts.html → src/defaults.ts.html} +74 -65
  21. package/coverage/{graph-builder.ts.html → src/graph-builder.ts.html} +268 -229
  22. package/coverage/src/index.html +341 -0
  23. package/coverage/{index.ts.html → src/index.ts.html} +70 -13
  24. package/coverage/{scoring.ts.html → src/issue-analyzer.ts.html} +201 -261
  25. package/coverage/src/mapper.ts.html +439 -0
  26. package/coverage/{metrics.ts.html → src/metrics.ts.html} +201 -132
  27. package/coverage/src/orchestrator.ts.html +493 -0
  28. package/coverage/{provider.ts.html → src/provider.ts.html} +21 -21
  29. package/coverage/{remediation.ts.html → src/remediation.ts.html} +112 -52
  30. package/coverage/src/report/console-report.ts.html +415 -0
  31. package/coverage/src/report/html-report.ts.html +361 -0
  32. package/coverage/src/report/index.html +146 -0
  33. package/coverage/src/report/interactive-setup.ts.html +373 -0
  34. package/coverage/src/scoring.ts.html +895 -0
  35. package/coverage/src/semantic/co-usage.ts.html +340 -0
  36. package/coverage/src/semantic/consolidation.ts.html +223 -0
  37. package/coverage/src/semantic/domain-inference.ts.html +859 -0
  38. package/coverage/src/semantic/index.html +161 -0
  39. package/coverage/src/semantic/type-graph.ts.html +163 -0
  40. package/coverage/{summary.ts.html → src/summary.ts.html} +155 -275
  41. package/coverage/{types.ts.html → src/types.ts.html} +133 -31
  42. package/coverage/src/utils/dependency-graph-utils.ts.html +463 -0
  43. package/coverage/src/utils/index.html +131 -0
  44. package/coverage/src/utils/string-utils.ts.html +148 -0
  45. package/dist/chunk-AMPK6SWS.mjs +1754 -0
  46. package/dist/chunk-BHCRDEE4.mjs +1745 -0
  47. package/dist/chunk-IKRP7ECY.mjs +1754 -0
  48. package/dist/chunk-J3MUOWHC.mjs +1747 -0
  49. package/dist/chunk-TWWPY7FD.mjs +1754 -0
  50. package/dist/chunk-Z5WY6A4P.mjs +1754 -0
  51. package/dist/cli.js +77 -185
  52. package/dist/cli.mjs +1 -1
  53. package/dist/index.d.mts +1 -1
  54. package/dist/index.d.ts +1 -1
  55. package/dist/index.js +73 -181
  56. package/dist/index.mjs +1 -1
  57. package/dist/python-context-BWDC4E5Z.mjs +162 -0
  58. package/package.json +3 -3
  59. package/src/__tests__/analyzer.test.ts +14 -14
  60. package/src/__tests__/auto-detection.test.ts +16 -16
  61. package/src/__tests__/consolidation.test.ts +247 -0
  62. package/src/__tests__/defaults.test.ts +121 -0
  63. package/src/__tests__/domain-inference.test.ts +420 -0
  64. package/src/__tests__/fragmentation-coupling.test.ts +4 -4
  65. package/src/__tests__/issue-analyzer.test.ts +155 -0
  66. package/src/__tests__/orchestrator.test.ts +143 -0
  67. package/src/__tests__/python-context.test.ts +100 -0
  68. package/src/__tests__/report/console-report.test.ts +292 -0
  69. package/src/__tests__/report/html-report.test.ts +243 -0
  70. package/src/__tests__/scoring.test.ts +17 -11
  71. package/src/analyzers/python-context.ts +2 -2
  72. package/src/ast-utils.ts +3 -3
  73. package/src/graph-builder.ts +4 -4
  74. package/src/mapper.ts +6 -0
  75. package/src/orchestrator.ts +1 -1
  76. package/src/report/html-report.ts +73 -181
  77. package/coverage/analyzer.ts.html +0 -1369
  78. package/coverage/semantic-analysis.ts.html +0 -1201
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { analyzeContext, calculateCohesion } from '../orchestrator';
3
+ import * as core from '@aiready/core';
4
+
5
+ // Mock the core module
6
+ vi.mock('@aiready/core', async () => {
7
+ const actual = await vi.importActual('@aiready/core');
8
+ return {
9
+ ...actual,
10
+ scanFiles: vi.fn(),
11
+ readFileContent: vi.fn(),
12
+ };
13
+ });
14
+
15
+ describe('analyzeContext', () => {
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ it('should analyze TypeScript files and return results', async () => {
21
+ const mockFiles = ['src/file1.ts', 'src/file2.ts'];
22
+ const mockContents: Record<string, string> = {
23
+ 'src/file1.ts': `
24
+ import { b } from './file2';
25
+ export const a = 1;
26
+ `,
27
+ 'src/file2.ts': `
28
+ export const b = 2;
29
+ `,
30
+ };
31
+
32
+ vi.mocked(core.scanFiles).mockResolvedValue(mockFiles);
33
+ vi.mocked(core.readFileContent).mockImplementation(async (path: string) => {
34
+ return mockContents[path] || '';
35
+ });
36
+
37
+ const results = await analyzeContext({
38
+ rootDir: '.',
39
+ maxDepth: 5,
40
+ maxContextBudget: 25000,
41
+ minCohesion: 0.6,
42
+ maxFragmentation: 0.5,
43
+ });
44
+
45
+ expect(results.length).toBe(2);
46
+ expect(results[0].file).toBeDefined();
47
+ expect(results[0].importDepth).toBeDefined();
48
+ expect(results[0].contextBudget).toBeDefined();
49
+ expect(core.scanFiles).toHaveBeenCalled();
50
+ expect(core.readFileContent).toHaveBeenCalledTimes(2);
51
+ });
52
+
53
+ it('should handle empty file list', async () => {
54
+ vi.mocked(core.scanFiles).mockResolvedValue([]);
55
+
56
+ const results = await analyzeContext({
57
+ rootDir: '.',
58
+ });
59
+
60
+ expect(results).toEqual([]);
61
+ });
62
+
63
+ it('should filter Python files when not present', async () => {
64
+ const mockFiles = ['src/file1.ts', 'src/file2.py'];
65
+ vi.mocked(core.scanFiles).mockResolvedValue(mockFiles);
66
+ vi.mocked(core.readFileContent).mockResolvedValue('export const a = 1;');
67
+
68
+ const results = await analyzeContext({
69
+ rootDir: '.',
70
+ });
71
+
72
+ // Should only process TypeScript files
73
+ expect(results.length).toBe(1);
74
+ expect(results[0].file).toBe('src/file1.ts');
75
+ });
76
+
77
+ it('should use default options when not provided', async () => {
78
+ vi.mocked(core.scanFiles).mockResolvedValue([]);
79
+ vi.mocked(core.readFileContent).mockResolvedValue('');
80
+
81
+ const results = await analyzeContext({
82
+ rootDir: '.',
83
+ });
84
+
85
+ expect(results).toEqual([]);
86
+ expect(core.scanFiles).toHaveBeenCalledWith(
87
+ expect.objectContaining({
88
+ rootDir: '.',
89
+ })
90
+ );
91
+ });
92
+
93
+ it('should handle includeNodeModules option', async () => {
94
+ const mockFiles = ['node_modules/pkg/index.js', 'src/file.ts'];
95
+ vi.mocked(core.scanFiles).mockResolvedValue(mockFiles);
96
+ vi.mocked(core.readFileContent).mockResolvedValue('export const a = 1;');
97
+
98
+ const results = await analyzeContext({
99
+ rootDir: '.',
100
+ includeNodeModules: true,
101
+ exclude: ['**/node_modules/**'],
102
+ });
103
+
104
+ // When includeNodeModules is true, node_modules should be processed
105
+ expect(results.length).toBe(2);
106
+ });
107
+ });
108
+
109
+ describe('calculateCohesion', () => {
110
+ it('should delegate to calculateEnhancedCohesion', () => {
111
+ const exports = [
112
+ { name: 'getUser', type: 'function' as const, inferredDomain: 'user' },
113
+ { name: 'updateUser', type: 'function' as const, inferredDomain: 'user' },
114
+ ];
115
+
116
+ const cohesion = calculateCohesion(exports);
117
+
118
+ // Should return a valid cohesion score
119
+ expect(cohesion).toBeGreaterThan(0);
120
+ expect(cohesion).toBeLessThanOrEqual(1);
121
+ });
122
+
123
+ it('should handle empty exports', () => {
124
+ const cohesion = calculateCohesion([]);
125
+ expect(cohesion).toBe(1); // Empty exports should have perfect cohesion
126
+ });
127
+
128
+ it('should pass through file path and options', () => {
129
+ const exports = [
130
+ { name: 'test', type: 'function' as const, inferredDomain: 'test' },
131
+ ];
132
+
133
+ // Test with file path
134
+ const cohesion = calculateCohesion(exports, 'src/test.ts');
135
+ expect(cohesion).toBe(1);
136
+
137
+ // Test with options
138
+ const cohesionWithOptions = calculateCohesion(exports, undefined, {
139
+ someOption: true,
140
+ });
141
+ expect(cohesionWithOptions).toBe(1);
142
+ });
143
+ });
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import * as pythonContext from '../analyzers/python-context';
3
+
4
+ // Mock @aiready/core
5
+ vi.mock('@aiready/core', () => ({
6
+ getParser: vi.fn(async (filename: string) => {
7
+ if (filename.endsWith('.py')) {
8
+ return {
9
+ initialize: vi.fn().mockResolvedValue(undefined),
10
+ parse: vi.fn(() => ({
11
+ imports: [
12
+ { source: 'os', specifiers: ['path'], isRelative: false },
13
+ { source: '.utils', specifiers: ['helper'], isRelative: true },
14
+ ],
15
+ exports: [
16
+ { name: 'MyClass', type: 'class' },
17
+ { name: 'my_function', type: 'function' },
18
+ ],
19
+ })),
20
+ language: 'python',
21
+ };
22
+ }
23
+ return null;
24
+ }),
25
+ estimateTokens: vi.fn((code: string) => Math.ceil(code.length / 4)),
26
+ }));
27
+
28
+ // Mock fs
29
+ vi.mock('fs', () => ({
30
+ promises: {
31
+ readFile: vi.fn(),
32
+ },
33
+ existsSync: vi.fn(),
34
+ }));
35
+
36
+ describe('python-context', () => {
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ });
40
+
41
+ describe('analyzePythonContext', () => {
42
+ it('should return empty array when parser is not available', async () => {
43
+ const { getParser } = await import('@aiready/core');
44
+ vi.mocked(getParser).mockResolvedValueOnce(null);
45
+
46
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
47
+
48
+ const results = await pythonContext.analyzePythonContext([], '/test');
49
+
50
+ expect(results).toEqual([]);
51
+ consoleSpy.mockRestore();
52
+ });
53
+
54
+ it('should filter for Python files only', async () => {
55
+ const files = ['src/file1.ts', 'src/file2.py', 'src/file3.js'];
56
+
57
+ const results = await pythonContext.analyzePythonContext(files, '/test');
58
+
59
+ // Should handle gracefully even if files don't exist
60
+ expect(Array.isArray(results)).toBe(true);
61
+ });
62
+
63
+ it('should analyze Python files and return metrics', async () => {
64
+ const fs = await import('fs');
65
+ vi.mocked(fs.promises.readFile).mockResolvedValue(`
66
+ import os
67
+ from .utils import helper
68
+
69
+ class MyClass:
70
+ pass
71
+
72
+ def my_function():
73
+ pass
74
+ `);
75
+
76
+ const files = ['src/test.py'];
77
+
78
+ const results = await pythonContext.analyzePythonContext(files, '/test');
79
+
80
+ // Results may be empty if parser returns null in mock
81
+ expect(Array.isArray(results)).toBe(true);
82
+ });
83
+
84
+ it('should handle file read errors gracefully', async () => {
85
+ const fs = await import('fs');
86
+ vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
87
+ new Error('File not found')
88
+ );
89
+
90
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
91
+
92
+ const files = ['src/bad.py'];
93
+ const results = await pythonContext.analyzePythonContext(files, '/test');
94
+
95
+ // Should return empty results or partial results
96
+ expect(Array.isArray(results)).toBe(true);
97
+ consoleSpy.mockRestore();
98
+ });
99
+ });
100
+ });
@@ -0,0 +1,292 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { displayConsoleReport } from '../../report/console-report';
3
+ import type { ContextSummary, ContextAnalysisResult } from '../../types';
4
+
5
+ // Mock chalk to avoid ANSI codes in tests
6
+ vi.mock('chalk', () => ({
7
+ default: {
8
+ bold: (str: string) => str,
9
+ cyan: (str: string) => str,
10
+ green: (str: string) => str,
11
+ red: (str: string) => str,
12
+ yellow: (str: string) => str,
13
+ blue: (str: string) => str,
14
+ white: (str: string) => str,
15
+ dim: (str: string) => str,
16
+ },
17
+ }));
18
+
19
+ // Mock the analyzer to provide the results type
20
+ vi.mock('../../analyzer', () => ({
21
+ analyzeContext: vi.fn(),
22
+ }));
23
+
24
+ describe('displayConsoleReport', () => {
25
+ let consoleLogSpy: any;
26
+
27
+ beforeEach(() => {
28
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
29
+ });
30
+
31
+ afterEach(() => {
32
+ consoleLogSpy.mockRestore();
33
+ });
34
+
35
+ const createMockSummary = (
36
+ overrides: Partial<ContextSummary> = {}
37
+ ): ContextSummary => ({
38
+ totalFiles: 10,
39
+ totalTokens: 50000,
40
+ avgContextBudget: 5000,
41
+ maxContextBudget: 10000,
42
+ avgImportDepth: 3,
43
+ maxImportDepth: 7,
44
+ deepFiles: [{ file: 'src/deep/file.ts', depth: 7 }],
45
+ avgFragmentation: 0.3,
46
+ fragmentedModules: [],
47
+ avgCohesion: 0.7,
48
+ lowCohesionFiles: [],
49
+ criticalIssues: 2,
50
+ majorIssues: 5,
51
+ minorIssues: 3,
52
+ totalPotentialSavings: 10000,
53
+ topExpensiveFiles: [],
54
+ config: {},
55
+ ...overrides,
56
+ });
57
+
58
+ const createMockResults = (): ContextAnalysisResult[] => [
59
+ {
60
+ file: 'src/services/user-service.ts',
61
+ tokenCost: 5000,
62
+ linesOfCode: 500,
63
+ importDepth: 5,
64
+ dependencyCount: 10,
65
+ dependencyList: [],
66
+ circularDeps: [],
67
+ cohesionScore: 0.8,
68
+ domains: ['user'],
69
+ exportCount: 5,
70
+ contextBudget: 15000,
71
+ fragmentationScore: 0.3,
72
+ relatedFiles: [],
73
+ fileClassification: 'service-file',
74
+ severity: 'critical',
75
+ issues: ['High context budget'],
76
+ recommendations: ['Split into smaller modules'],
77
+ potentialSavings: 5000,
78
+ },
79
+ {
80
+ file: 'src/utils/helper.ts',
81
+ tokenCost: 1000,
82
+ linesOfCode: 100,
83
+ importDepth: 2,
84
+ dependencyCount: 3,
85
+ dependencyList: [],
86
+ circularDeps: [],
87
+ cohesionScore: 0.9,
88
+ domains: ['utils'],
89
+ exportCount: 3,
90
+ contextBudget: 3000,
91
+ fragmentationScore: 0.1,
92
+ relatedFiles: [],
93
+ fileClassification: 'utility-module',
94
+ severity: 'info',
95
+ issues: [],
96
+ recommendations: [],
97
+ potentialSavings: 0,
98
+ },
99
+ ];
100
+
101
+ it('should display summary with all metrics', () => {
102
+ const summary = createMockSummary();
103
+ const results = createMockResults();
104
+
105
+ displayConsoleReport(summary, results);
106
+
107
+ expect(consoleLogSpy).toHaveBeenCalledWith(
108
+ expect.stringContaining('Context Analysis Summary')
109
+ );
110
+ expect(consoleLogSpy).toHaveBeenCalledWith(
111
+ expect.stringContaining('Total Files:')
112
+ );
113
+ expect(consoleLogSpy).toHaveBeenCalledWith(
114
+ expect.stringContaining('Total Tokens:')
115
+ );
116
+ });
117
+
118
+ it('should display issues when present', () => {
119
+ const summary = createMockSummary({
120
+ criticalIssues: 2,
121
+ majorIssues: 5,
122
+ minorIssues: 3,
123
+ });
124
+ const results = createMockResults();
125
+
126
+ displayConsoleReport(summary, results);
127
+
128
+ expect(consoleLogSpy).toHaveBeenCalledWith(
129
+ expect.stringContaining('Issues Detected')
130
+ );
131
+ expect(consoleLogSpy).toHaveBeenCalledWith(
132
+ expect.stringContaining('Critical:')
133
+ );
134
+ expect(consoleLogSpy).toHaveBeenCalledWith(
135
+ expect.stringContaining('Major:')
136
+ );
137
+ expect(consoleLogSpy).toHaveBeenCalledWith(
138
+ expect.stringContaining('Minor:')
139
+ );
140
+ });
141
+
142
+ it('should display no issues message when all counts are zero', () => {
143
+ const summary = createMockSummary({
144
+ criticalIssues: 0,
145
+ majorIssues: 0,
146
+ minorIssues: 0,
147
+ });
148
+ const results: ContextAnalysisResult[] = [];
149
+
150
+ displayConsoleReport(summary, results);
151
+
152
+ expect(consoleLogSpy).toHaveBeenCalledWith(
153
+ expect.stringContaining('No significant context issues detected')
154
+ );
155
+ });
156
+
157
+ it('should display fragmented modules when present', () => {
158
+ const summary = createMockSummary({
159
+ fragmentedModules: [
160
+ {
161
+ domain: 'src/features',
162
+ files: [
163
+ 'src/features/a.ts',
164
+ 'src/features/b.ts',
165
+ 'src/features/c.ts',
166
+ ],
167
+ totalTokens: 3000,
168
+ fragmentationScore: 0.75,
169
+ avgCohesion: 0.4,
170
+ suggestedStructure: {
171
+ targetFiles: 2,
172
+ consolidationPlan: ['Consolidate into fewer modules'],
173
+ },
174
+ },
175
+ ],
176
+ });
177
+ const results: ContextAnalysisResult[] = [];
178
+
179
+ displayConsoleReport(summary, results);
180
+
181
+ expect(consoleLogSpy).toHaveBeenCalledWith(
182
+ expect.stringContaining('Fragmented Modules')
183
+ );
184
+ });
185
+
186
+ it('should display top expensive files when present', () => {
187
+ const summary = createMockSummary({
188
+ topExpensiveFiles: [
189
+ {
190
+ file: 'src/services/user-service.ts',
191
+ contextBudget: 15000,
192
+ severity: 'critical',
193
+ },
194
+ {
195
+ file: 'src/services/order-service.ts',
196
+ contextBudget: 12000,
197
+ severity: 'major',
198
+ },
199
+ ],
200
+ });
201
+ const results: ContextAnalysisResult[] = [];
202
+
203
+ displayConsoleReport(summary, results);
204
+
205
+ expect(consoleLogSpy).toHaveBeenCalledWith(
206
+ expect.stringContaining('Most Expensive Files')
207
+ );
208
+ });
209
+
210
+ it('should display recommendations for critical and major issues', () => {
211
+ const summary = createMockSummary({
212
+ criticalIssues: 1,
213
+ majorIssues: 1,
214
+ minorIssues: 0,
215
+ });
216
+ const results: ContextAnalysisResult[] = [
217
+ {
218
+ file: 'src/bad/file.ts',
219
+ tokenCost: 10000,
220
+ linesOfCode: 1000,
221
+ importDepth: 8,
222
+ dependencyCount: 20,
223
+ dependencyList: [],
224
+ circularDeps: [],
225
+ cohesionScore: 0.2,
226
+ domains: [],
227
+ exportCount: 20,
228
+ contextBudget: 30000,
229
+ fragmentationScore: 0.8,
230
+ relatedFiles: [],
231
+ fileClassification: 'mixed-concerns',
232
+ severity: 'critical',
233
+ issues: ['Very high context budget', 'Deep import chain'],
234
+ recommendations: ['Split into multiple files', 'Reduce dependencies'],
235
+ potentialSavings: 15000,
236
+ },
237
+ ];
238
+
239
+ displayConsoleReport(summary, results);
240
+
241
+ expect(consoleLogSpy).toHaveBeenCalledWith(
242
+ expect.stringContaining('Recommendations')
243
+ );
244
+ });
245
+
246
+ it('should respect maxResults parameter', () => {
247
+ const summary = createMockSummary({
248
+ topExpensiveFiles: [
249
+ { file: 'src/file1.ts', contextBudget: 15000, severity: 'critical' },
250
+ { file: 'src/file2.ts', contextBudget: 12000, severity: 'critical' },
251
+ { file: 'src/file3.ts', contextBudget: 10000, severity: 'major' },
252
+ { file: 'src/file4.ts', contextBudget: 8000, severity: 'major' },
253
+ ],
254
+ fragmentedModules: [
255
+ {
256
+ domain: 'src/domain1',
257
+ files: ['a.ts', 'b.ts', 'c.ts', 'd.ts'],
258
+ totalTokens: 2000,
259
+ fragmentationScore: 0.6,
260
+ avgCohesion: 0.5,
261
+ suggestedStructure: { targetFiles: 2, consolidationPlan: [] },
262
+ },
263
+ {
264
+ domain: 'src/domain2',
265
+ files: ['e.ts', 'f.ts'],
266
+ totalTokens: 1000,
267
+ fragmentationScore: 0.5,
268
+ avgCohesion: 0.6,
269
+ suggestedStructure: { targetFiles: 1, consolidationPlan: [] },
270
+ },
271
+ ],
272
+ });
273
+ const results: ContextAnalysisResult[] = [];
274
+
275
+ displayConsoleReport(summary, results, 2);
276
+
277
+ // With maxResults=2, should only show top 2 items
278
+ // The function uses slice(0, maxResults) internally
279
+ expect(consoleLogSpy).toHaveBeenCalled();
280
+ });
281
+
282
+ it('should display footer with links', () => {
283
+ const summary = createMockSummary();
284
+ const results: ContextAnalysisResult[] = [];
285
+
286
+ displayConsoleReport(summary, results);
287
+
288
+ expect(consoleLogSpy).toHaveBeenCalledWith(
289
+ expect.stringContaining('github.com')
290
+ );
291
+ });
292
+ });