1
+ import { beforeEach , describe , expect , it , spyOn } from 'bun:test'
2
+ import { RegistryClient } from '../src/registry/registry-client'
3
+ import fs from 'node:fs'
4
+
5
+ describe ( 'Composer Constraint Filtering' , ( ) => {
6
+ let registryClient : RegistryClient
7
+
8
+ beforeEach ( ( ) => {
9
+ const mockConfig = {
10
+ packages : {
11
+ strategy : 'all' as const ,
12
+ ignore : [ ] ,
13
+ includePrerelease : false ,
14
+ excludeMajor : false
15
+ }
16
+ }
17
+ const mockLogger = {
18
+ info : ( ...args : any [ ] ) => console . log ( '[INFO]' , ...args ) ,
19
+ warn : ( ...args : any [ ] ) => console . log ( '[WARN]' , ...args ) ,
20
+ error : ( ...args : any [ ] ) => console . log ( '[ERROR]' , ...args ) ,
21
+ success : ( ...args : any [ ] ) => console . log ( '[SUCCESS]' , ...args ) ,
22
+ debug : ( ...args : any [ ] ) => console . log ( '[DEBUG]' , ...args )
23
+ }
24
+ registryClient = new RegistryClient ( mockConfig , '.' , mockLogger as any )
25
+ } )
26
+
27
+ it ( 'should include minor/patch updates within caret constraints' , async ( ) => {
28
+ // Mock composer.json with caret constraints (real constraints from user's repo)
29
+ const mockComposerJson = {
30
+ "require" : {
31
+ "laravel/framework" : "^10.0" ,
32
+ "symfony/console" : "^6.0" ,
33
+ "monolog/monolog" : "^3.0" ,
34
+ "doctrine/dbal" : "^3.0" ,
35
+ "guzzlehttp/guzzle" : "^7.0"
36
+ } ,
37
+ "require-dev" : {
38
+ "phpunit/phpunit" : "^10.0" ,
39
+ "mockery/mockery" : "^1.5" ,
40
+ "fakerphp/faker" : "^1.20"
41
+ }
42
+ }
43
+
44
+ // Mock composer outdated output - this is what the real command returns
45
+ const mockComposerOutdated = {
46
+ "installed" : [
47
+ // These should be INCLUDED (minor/patch updates within constraints)
48
+ {
49
+ "name" : "laravel/framework" ,
50
+ "version" : "10.0.0" ,
51
+ "latest" : "10.48.29" // Minor update within ^10.0
52
+ } ,
53
+ {
54
+ "name" : "symfony/console" ,
55
+ "version" : "6.0.0" ,
56
+ "latest" : "6.4.23" // Minor update within ^6.0
57
+ } ,
58
+ {
59
+ "name" : "monolog/monolog" ,
60
+ "version" : "3.0.0" ,
61
+ "latest" : "3.9.0" // Minor update within ^3.0
62
+ } ,
63
+ {
64
+ "name" : "doctrine/dbal" ,
65
+ "version" : "3.0.0" ,
66
+ "latest" : "3.10.0" // Minor update within ^3.0
67
+ } ,
68
+ {
69
+ "name" : "guzzlehttp/guzzle" ,
70
+ "version" : "7.0.0" ,
71
+ "latest" : "7.9.3" // Minor update within ^7.0
72
+ } ,
73
+ {
74
+ "name" : "phpunit/phpunit" ,
75
+ "version" : "10.0.0" ,
76
+ "latest" : "10.5.48" // Minor update within ^10.0
77
+ } ,
78
+ {
79
+ "name" : "mockery/mockery" ,
80
+ "version" : "1.5.0" ,
81
+ "latest" : "1.6.12" // Minor update within ^1.5
82
+ } ,
83
+ {
84
+ "name" : "fakerphp/faker" ,
85
+ "version" : "1.20.0" ,
86
+ "latest" : "1.24.1" // Minor update within ^1.20
87
+ } ,
88
+ // These should be EXCLUDED (major updates outside constraints)
89
+ {
90
+ "name" : "symfony/console" ,
91
+ "version" : "6.4.23" ,
92
+ "latest" : "7.3.1" // Major update outside ^6.0
93
+ } ,
94
+ {
95
+ "name" : "laravel/framework" ,
96
+ "version" : "10.48.29" ,
97
+ "latest" : "12.21.0" // Major update outside ^10.0
98
+ }
99
+ ]
100
+ }
101
+
102
+ // Mock file system and commands
103
+ spyOn ( fs , 'existsSync' ) . mockReturnValue ( true )
104
+ spyOn ( fs , 'readFileSync' ) . mockReturnValue ( JSON . stringify ( mockComposerJson ) )
105
+
106
+ spyOn ( registryClient as any , 'runCommand' ) . mockImplementation ( ( command : string , args : string [ ] ) => {
107
+ if ( command === 'composer' && args [ 0 ] === '--version' ) {
108
+ return Promise . resolve ( 'Composer version 2.7.1' )
109
+ }
110
+ if ( command === 'composer' && args . includes ( 'outdated' ) ) {
111
+ return Promise . resolve ( JSON . stringify ( mockComposerOutdated ) )
112
+ }
113
+ return Promise . reject ( new Error ( 'Unexpected command' ) )
114
+ } )
115
+
116
+ const updates = await registryClient . getComposerOutdatedPackages ( )
117
+
118
+ // Should find 8 minor/patch updates, exclude 2 major updates
119
+ expect ( updates ) . toHaveLength ( 8 )
120
+
121
+ // Verify included packages
122
+ const packageNames = updates . map ( u => u . name )
123
+ expect ( packageNames ) . toContain ( 'laravel/framework' )
124
+ expect ( packageNames ) . toContain ( 'symfony/console' )
125
+ expect ( packageNames ) . toContain ( 'monolog/monolog' )
126
+ expect ( packageNames ) . toContain ( 'doctrine/dbal' )
127
+ expect ( packageNames ) . toContain ( 'guzzlehttp/guzzle' )
128
+ expect ( packageNames ) . toContain ( 'phpunit/phpunit' )
129
+ expect ( packageNames ) . toContain ( 'mockery/mockery' )
130
+ expect ( packageNames ) . toContain ( 'fakerphp/faker' )
131
+
132
+ // Verify versions are the minor updates, not major
133
+ const laravelUpdate = updates . find ( u => u . name === 'laravel/framework' ) !
134
+ expect ( laravelUpdate . newVersion ) . toBe ( '10.48.29' ) // Not 12.21.0
135
+
136
+ const symfonyUpdate = updates . find ( u => u . name === 'symfony/console' ) !
137
+ expect ( symfonyUpdate . newVersion ) . toBe ( '6.4.23' ) // Not 7.3.1
138
+
139
+ // Verify all are minor/patch updates
140
+ updates . forEach ( update => {
141
+ expect ( [ 'minor' , 'patch' ] ) . toContain ( update . updateType )
142
+ } )
143
+ } )
144
+
145
+ it ( 'should allow major updates when no constraints restrict them' , async ( ) => {
146
+ // Mock composer.json with loose constraints or no constraints
147
+ const mockComposerJson = {
148
+ "require" : {
149
+ "some/package" : ">=1.0" , // Allows major updates
150
+ "other/package" : "*" // Allows any version
151
+ }
152
+ }
153
+
154
+ const mockComposerOutdated = {
155
+ "installed" : [
156
+ {
157
+ "name" : "some/package" ,
158
+ "version" : "1.0.0" ,
159
+ "latest" : "2.0.0" // Major update allowed by >=1.0
160
+ } ,
161
+ {
162
+ "name" : "other/package" ,
163
+ "version" : "1.0.0" ,
164
+ "latest" : "3.0.0" // Major update allowed by *
165
+ }
166
+ ]
167
+ }
168
+
169
+ spyOn ( fs , 'existsSync' ) . mockReturnValue ( true )
170
+ spyOn ( fs , 'readFileSync' ) . mockReturnValue ( JSON . stringify ( mockComposerJson ) )
171
+
172
+ spyOn ( registryClient as any , 'runCommand' ) . mockImplementation ( ( command : string , args : string [ ] ) => {
173
+ if ( command === 'composer' && args [ 0 ] === '--version' ) {
174
+ return Promise . resolve ( 'Composer version 2.7.1' )
175
+ }
176
+ if ( command === 'composer' && args . includes ( 'outdated' ) ) {
177
+ return Promise . resolve ( JSON . stringify ( mockComposerOutdated ) )
178
+ }
179
+ return Promise . reject ( new Error ( 'Unexpected command' ) )
180
+ } )
181
+
182
+ const updates = await registryClient . getComposerOutdatedPackages ( )
183
+
184
+ // Should include both major updates
185
+ expect ( updates ) . toHaveLength ( 2 )
186
+
187
+ updates . forEach ( update => {
188
+ expect ( update . updateType ) . toBe ( 'major' )
189
+ } )
190
+ } )
191
+
192
+ it ( 'should handle tilde constraints correctly' , async ( ) => {
193
+ const mockComposerJson = {
194
+ "require" : {
195
+ "patch-only" : "~1.2.3" , // Only allows 1.2.x patches
196
+ "minor-allowed" : "~1.2" // Allows 1.x.x minor/patch
197
+ }
198
+ }
199
+
200
+ const mockComposerOutdated = {
201
+ "installed" : [
202
+ // Should be included
203
+ {
204
+ "name" : "patch-only" ,
205
+ "version" : "1.2.3" ,
206
+ "latest" : "1.2.5" // Patch within ~1.2.3
207
+ } ,
208
+ {
209
+ "name" : "minor-allowed" ,
210
+ "version" : "1.2.0" ,
211
+ "latest" : "1.5.0" // Minor within ~1.2
212
+ } ,
213
+ // Should be excluded
214
+ {
215
+ "name" : "patch-only" ,
216
+ "version" : "1.2.5" ,
217
+ "latest" : "1.3.0" // Minor outside ~1.2.3
218
+ } ,
219
+ {
220
+ "name" : "minor-allowed" ,
221
+ "version" : "1.5.0" ,
222
+ "latest" : "2.0.0" // Major outside ~1.2
223
+ }
224
+ ]
225
+ }
226
+
227
+ spyOn ( fs , 'existsSync' ) . mockReturnValue ( true )
228
+ spyOn ( fs , 'readFileSync' ) . mockReturnValue ( JSON . stringify ( mockComposerJson ) )
229
+
230
+ spyOn ( registryClient as any , 'runCommand' ) . mockImplementation ( ( command : string , args : string [ ] ) => {
231
+ if ( command === 'composer' && args [ 0 ] === '--version' ) {
232
+ return Promise . resolve ( 'Composer version 2.7.1' )
233
+ }
234
+ if ( command === 'composer' && args . includes ( 'outdated' ) ) {
235
+ return Promise . resolve ( JSON . stringify ( mockComposerOutdated ) )
236
+ }
237
+ return Promise . reject ( new Error ( 'Unexpected command' ) )
238
+ } )
239
+
240
+ const updates = await registryClient . getComposerOutdatedPackages ( )
241
+
242
+ // Should find 2 allowed updates
243
+ expect ( updates ) . toHaveLength ( 2 )
244
+
245
+ const patchUpdate = updates . find ( u => u . name === 'patch-only' ) !
246
+ expect ( patchUpdate . newVersion ) . toBe ( '1.2.5' )
247
+
248
+ const minorUpdate = updates . find ( u => u . name === 'minor-allowed' ) !
249
+ expect ( minorUpdate . newVersion ) . toBe ( '1.5.0' )
250
+ } )
251
+ } )
0 commit comments