@@ -72,7 +72,9 @@ const mockFetchList = vi.mocked(fetchPackagistPackageList)
7272const mockParseList = vi . mocked ( parsePackagistPackageList )
7373const mockSeedInsert = vi . mocked ( insertPackagistPackages )
7474
75- const qx = { } as QueryExecutor
75+ const qx = {
76+ tx : vi . fn ( ( cb : ( t : QueryExecutor ) => Promise < void > ) => cb ( qx ) ) ,
77+ } as unknown as QueryExecutor
7678const PURL = 'pkg:composer/monolog/monolog'
7779const RUN_DATE = '2026-07-15'
7880const SCHEDULED_AT = '2026-07-15T00:00:00.000Z'
@@ -111,7 +113,7 @@ describe('ingestOnePackagistMetadata', () => {
111113 } )
112114 }
113115
114- it ( 'audits phase 1 immediately and phase 2 separately, then marks scanned with the fresh Last-Modified' , async ( ) => {
116+ it ( 'persists phase 1 immediately and phase 2 separately, then marks scanned with the fresh Last-Modified' , async ( ) => {
115117 happyMocks ( )
116118
117119 await ingestOnePackagistMetadata ( qx , candidate , SCHEDULED_AT )
@@ -120,10 +122,9 @@ describe('ingestOnePackagistMetadata', () => {
120122 expect ( mockFetchP2 ) . toHaveBeenCalledWith ( 'monolog/monolog' , 'Tue, 30 Jun 2026 00:00:00 GMT' )
121123 expect ( mockExpand ) . toHaveBeenCalledWith ( minified )
122124 expect ( mockPersistMetadata ) . toHaveBeenCalledWith ( qx , PURL , expanded )
123- // Two separate audit rows, not one merged call — phase 1 is logged as soon as it's
124- // committed, before the p2 fetch (which can throw) ever runs.
125- expect ( mockAudit ) . toHaveBeenNthCalledWith ( 1 , qx , 'packagist' , PURL , [ 'packages.description' ] )
126- expect ( mockAudit ) . toHaveBeenNthCalledWith ( 2 , qx , 'packagist' , PURL , [ 'versions.number' ] )
125+ // Each phase audits its own writes atomically inside its own persist* call (see
126+ // persistPackageInfo.test.ts / persistMetadata.test.ts) — phase 1 is committed and
127+ // audited before the p2 fetch (which can throw) ever runs.
127128 expect ( mockMarkMetadata ) . toHaveBeenCalledWith (
128129 qx ,
129130 PURL ,
@@ -209,8 +210,9 @@ describe('ingestOnePackagistMetadata', () => {
209210 )
210211 } )
211212
212- it ( 'audits phase-1 changes even when the p2 fetch gives up' , async ( ) => {
213- // the dynamic-endpoint writes are committed by then — their audit rows must not be dropped
213+ it ( 'still persists (and self-audits) phase 1 even when the p2 fetch gives up' , async ( ) => {
214+ // the dynamic-endpoint writes are committed and audited by persistPackagistPackageInfo
215+ // itself before the p2 fetch (which can throw) ever runs — see persistPackageInfo.test.ts
214216 vi . useFakeTimers ( )
215217 mockFetchStats . mockResolvedValue ( statsJson as never )
216218 mockPersistInfo . mockResolvedValue ( { found : true , changedFields : [ 'packages.description' ] } )
@@ -224,7 +226,7 @@ describe('ingestOnePackagistMetadata', () => {
224226 await vi . runAllTimersAsync ( )
225227 await p
226228
227- expect ( mockAudit ) . toHaveBeenCalledWith ( qx , 'packagist' , PURL , [ 'packages.description' ] )
229+ expect ( mockPersistInfo ) . toHaveBeenCalledWith ( qx , PURL , expect . anything ( ) )
228230 expect ( mockMarkMetadata ) . toHaveBeenCalledWith (
229231 qx ,
230232 PURL ,
@@ -233,32 +235,30 @@ describe('ingestOnePackagistMetadata', () => {
233235 )
234236 } )
235237
236- it ( 'throws on a transient p2 result without marking scanned, but still audits phase 1' , async ( ) => {
238+ it ( 'throws on a transient p2 result without marking scanned, but phase 1 already persisted ' , async ( ) => {
237239 mockFetchStats . mockResolvedValue ( statsJson as never )
238240 mockPersistInfo . mockResolvedValue ( { found : true , changedFields : [ 'packages.description' ] } )
239241 mockFetchP2 . mockResolvedValue ( { kind : 'TRANSIENT' , message : 'HTTP 502' } as never )
240242
241243 await expect ( ingestOnePackagistMetadata ( qx , candidate , SCHEDULED_AT ) ) . rejects . toThrow ( )
242244 expect ( mockMarkMetadata ) . not . toHaveBeenCalled ( )
243- // phase-1 writes are already committed when the throw happens — a retry re-runs
244- // phase 1 idempotently and reports no changes, so this audit event can't be deferred
245- expect ( mockAudit ) . toHaveBeenCalledWith ( qx , 'packagist' , PURL , [ 'packages.description' ] )
245+ // phase-1 writes (and their audit row) are already committed by
246+ // persistPackagistPackageInfo when the throw happens — a retry re-runs phase 1
247+ // idempotently and reports no changes, so nothing here can lose that audit event
248+ expect ( mockPersistInfo ) . toHaveBeenCalledWith ( qx , PURL , expect . anything ( ) )
246249 } )
247250} )
248251
249252// The monthly downloads-30d lane: dynamic fetch, window row only.
250253describe ( 'ingestOnePackagist30dWindow' , ( ) => {
251- it ( 'persists the observed rolling window, audits the change, and marks the run processed' , async ( ) => {
254+ it ( 'persists the observed rolling window (self-audited) and marks the run processed' , async ( ) => {
252255 mockFetchStats . mockResolvedValue ( statsJson as never )
253256 mockPersist30d . mockResolvedValue ( [ 'downloads_last_30d.count' , 'packages.downloads_last_30d' ] )
254257
255258 await ingestOnePackagist30dWindow ( qx , PURL , RUN_DATE , SCHEDULED_AT )
256259
257260 expect ( mockPersist30d ) . toHaveBeenCalledWith ( qx , PURL , 300 , RUN_DATE )
258- expect ( mockAudit ) . toHaveBeenCalledWith ( qx , 'packagist' , PURL , [
259- 'downloads_last_30d.count' ,
260- 'packages.downloads_last_30d' ,
261- ] )
261+ // persistPackagist30dWindow audits its own write atomically — see downloads.test.ts
262262 expect ( mockMark30d ) . toHaveBeenCalledWith (
263263 qx ,
264264 PURL ,
0 commit comments