@@ -107,4 +107,138 @@ describe('detectPublishSecurityDowngradeForVersion', () => {
107107 )
108108 expect ( detectPublishSecurityDowngradeForVersion ( versions , '2.4.0' ) ) . toBeNull ( )
109109 } )
110+
111+ it ( 'skips deprecated versions when selecting trustedVersion' , ( ) => {
112+ const result = detectPublishSecurityDowngradeForVersion (
113+ [
114+ {
115+ version : '1.0.0' ,
116+ time : '2026-01-01T00:00:00.000Z' ,
117+ hasProvenance : true ,
118+ trustLevel : 'provenance' ,
119+ } ,
120+ {
121+ version : '1.0.1' ,
122+ time : '2026-01-02T00:00:00.000Z' ,
123+ hasProvenance : true ,
124+ trustLevel : 'provenance' ,
125+ deprecated : 'Use 1.0.2 instead' ,
126+ } ,
127+ {
128+ version : '1.0.2' ,
129+ time : '2026-01-03T00:00:00.000Z' ,
130+ hasProvenance : false ,
131+ trustLevel : 'none' ,
132+ } ,
133+ ] ,
134+ '1.0.2' ,
135+ )
136+
137+ // Should recommend 1.0.0 (not 1.0.1 which is deprecated)
138+ expect ( result ?. trustedVersion ) . toBe ( '1.0.0' )
139+ } )
140+
141+ it ( 'returns null when all older trusted versions are deprecated' , ( ) => {
142+ const result = detectPublishSecurityDowngradeForVersion (
143+ [
144+ {
145+ version : '1.0.0' ,
146+ time : '2026-01-01T00:00:00.000Z' ,
147+ hasProvenance : true ,
148+ trustLevel : 'provenance' ,
149+ deprecated : 'Deprecated' ,
150+ } ,
151+ {
152+ version : '1.0.1' ,
153+ time : '2026-01-02T00:00:00.000Z' ,
154+ hasProvenance : false ,
155+ trustLevel : 'none' ,
156+ } ,
157+ ] ,
158+ '1.0.1' ,
159+ )
160+
161+ expect ( result ) . toBeNull ( )
162+ } )
163+
164+ it ( 'detects cross-major downgrade but does not recommend a version' , ( ) => {
165+ const result = detectPublishSecurityDowngradeForVersion (
166+ [
167+ {
168+ version : '1.0.0' ,
169+ time : '2026-01-01T00:00:00.000Z' ,
170+ hasProvenance : true ,
171+ trustLevel : 'provenance' ,
172+ } ,
173+ {
174+ version : '2.0.0' ,
175+ time : '2026-01-02T00:00:00.000Z' ,
176+ hasProvenance : false ,
177+ trustLevel : 'none' ,
178+ } ,
179+ ] ,
180+ '2.0.0' ,
181+ )
182+
183+ // Downgrade is detected (v1.0.0 was trusted, v2.0.0 is not)
184+ expect ( result ) . not . toBeNull ( )
185+ expect ( result ?. downgradedVersion ) . toBe ( '2.0.0' )
186+ // But no trustedVersion recommendation since v1.0.0 is a different major
187+ expect ( result ?. trustedVersion ) . toBeUndefined ( )
188+ } )
189+
190+ it ( 'recommends same-major trusted version when cross-major exists' , ( ) => {
191+ const result = detectPublishSecurityDowngradeForVersion (
192+ [
193+ {
194+ version : '1.0.0' ,
195+ time : '2026-01-01T00:00:00.000Z' ,
196+ hasProvenance : true ,
197+ trustLevel : 'provenance' ,
198+ } ,
199+ {
200+ version : '2.0.0' ,
201+ time : '2026-01-02T00:00:00.000Z' ,
202+ hasProvenance : true ,
203+ trustLevel : 'provenance' ,
204+ } ,
205+ {
206+ version : '2.1.0' ,
207+ time : '2026-01-03T00:00:00.000Z' ,
208+ hasProvenance : false ,
209+ trustLevel : 'none' ,
210+ } ,
211+ ] ,
212+ '2.1.0' ,
213+ )
214+
215+ // Should recommend 2.0.0 (same major), not 1.0.0
216+ expect ( result ?. trustedVersion ) . toBe ( '2.0.0' )
217+ } )
218+
219+ it ( 'uses trustedPublisher rank (not provenance) for hasProvenance fallback without trustLevel' , ( ) => {
220+ // When trustLevel is absent, hasProvenance: true should map to trustedPublisher rank,
221+ // not provenance rank. This means a version with only hasProvenance: true should NOT
222+ // be considered a downgrade from trustedPublisher.
223+ const result = detectPublishSecurityDowngradeForVersion (
224+ [
225+ {
226+ version : '1.0.0' ,
227+ time : '2026-01-01T00:00:00.000Z' ,
228+ hasProvenance : true ,
229+ // no trustLevel — fallback path
230+ } ,
231+ {
232+ version : '1.0.1' ,
233+ time : '2026-01-02T00:00:00.000Z' ,
234+ hasProvenance : true ,
235+ trustLevel : 'trustedPublisher' ,
236+ } ,
237+ ] ,
238+ '1.0.1' ,
239+ )
240+
241+ // Both should be treated as trustedPublisher rank, so no downgrade
242+ expect ( result ) . toBeNull ( )
243+ } )
110244} )
0 commit comments