@@ -300,6 +300,7 @@ describe("fetchLinkedIssueLabelsForPropagation (#priority-linked-issue-gate)", (
300300 user : { login : "contrib" } ,
301301 labels : [ "gittensor:feature" , "gittensor:priority" ] ,
302302 } ) ;
303+ if ( url . includes ( "/issues/4279/timeline" ) ) return Response . json ( [ { event : "closed" , source : { issue : { number : 4494 , pull_request : { } } } } ] ) ;
303304 return new Response ( "not found" , { status : 404 } ) ;
304305 } ) ;
305306 const env = createTestEnv ( { } ) ;
@@ -310,10 +311,81 @@ describe("fetchLinkedIssueLabelsForPropagation (#priority-linked-issue-gate)", (
310311 installationId : 123 ,
311312 prAuthorLogin : "contrib" ,
312313 prMergedAt : "2026-07-09T22:15:13Z" ,
314+ prNumber : 4494 ,
313315 } ) ;
314316 expectPropagation ( result , [ "gittensor:feature" , "gittensor:priority" ] ) ;
315317 } ) ;
316318
319+ it ( "REGRESSION (#closed-issue-timestamp-spoof): does NOT propagate when an unrelated issue closed after this PR merged" , async ( ) => {
320+ stubFetch ( ( url ) => {
321+ if ( url . includes ( "/access_tokens" ) ) return Response . json ( { token : "installation-token" } ) ;
322+ if ( url . endsWith ( "/issues/9001" ) )
323+ return Response . json ( {
324+ number : 9001 ,
325+ state : "closed" ,
326+ closed_at : "2026-07-09T22:15:14Z" ,
327+ user : { login : "contrib" } ,
328+ labels : [ "gittensor:feature" , "gittensor:priority" ] ,
329+ } ) ;
330+ if ( url . includes ( "/issues/9001/timeline" ) ) return Response . json ( [ { event : "closed" , source : { issue : { number : 123 , pull_request : { } } } } ] ) ;
331+ return new Response ( "not found" , { status : 404 } ) ;
332+ } ) ;
333+ const env = createTestEnv ( { } ) ;
334+ const result = await fetchLinkedIssueLabelsForPropagation ( {
335+ env,
336+ repoFullName : "owner/repo" ,
337+ linkedIssues : [ 9001 ] ,
338+ installationId : 123 ,
339+ prAuthorLogin : "contrib" ,
340+ prMergedAt : "2026-07-09T22:15:13Z" ,
341+ prNumber : 4494 ,
342+ } ) ;
343+ expectPropagation ( result , [ ] ) ;
344+ } ) ;
345+
346+ it ( "does not propagate a timestamp-eligible closed issue when the caller cannot identify this PR number" , async ( ) => {
347+ const fetchSpy = vi . fn ( async ( input : RequestInfo | URL ) => {
348+ const url = input . toString ( ) ;
349+ if ( url . includes ( "/access_tokens" ) ) return Response . json ( { token : "installation-token" } ) ;
350+ if ( url . endsWith ( "/issues/9003" ) )
351+ return Response . json ( { number : 9003 , state : "closed" , closed_at : "2026-07-09T22:15:14Z" , user : { login : "contrib" } , labels : [ "gittensor:priority" ] } ) ;
352+ return new Response ( "not found" , { status : 404 } ) ;
353+ } ) ;
354+ vi . stubGlobal ( "fetch" , fetchSpy ) ;
355+ const env = createTestEnv ( { } ) ;
356+ const result = await fetchLinkedIssueLabelsForPropagation ( {
357+ env,
358+ repoFullName : "owner/repo" ,
359+ linkedIssues : [ 9003 ] ,
360+ installationId : 123 ,
361+ prAuthorLogin : "contrib" ,
362+ prMergedAt : "2026-07-09T22:15:13Z" ,
363+ } ) ;
364+ expectPropagation ( result , [ ] ) ;
365+ expect ( fetchSpy . mock . calls . some ( ( [ input ] ) => input . toString ( ) . includes ( "/timeline" ) ) ) . toBe ( false ) ;
366+ } ) ;
367+
368+ it ( "flags closed issue propagation inconclusive when the timeline closure check fails" , async ( ) => {
369+ stubFetch ( ( url ) => {
370+ if ( url . includes ( "/access_tokens" ) ) return Response . json ( { token : "installation-token" } ) ;
371+ if ( url . endsWith ( "/issues/9002" ) )
372+ return Response . json ( { number : 9002 , state : "closed" , closed_at : "2026-07-09T22:15:14Z" , user : { login : "contrib" } , labels : [ "gittensor:priority" ] } ) ;
373+ if ( url . includes ( "/issues/9002/timeline" ) ) return new Response ( "server error" , { status : 500 } ) ;
374+ return new Response ( "not found" , { status : 404 } ) ;
375+ } ) ;
376+ const env = createTestEnv ( { } ) ;
377+ const result = await fetchLinkedIssueLabelsForPropagation ( {
378+ env,
379+ repoFullName : "owner/repo" ,
380+ linkedIssues : [ 9002 ] ,
381+ installationId : 123 ,
382+ prAuthorLogin : "contrib" ,
383+ prMergedAt : "2026-07-09T22:15:13Z" ,
384+ prNumber : 4494 ,
385+ } ) ;
386+ expectPropagation ( result , [ ] , true ) ;
387+ } ) ;
388+
317389 it ( "does NOT propagate when the linked issue was already closed BEFORE this PR merged (anti-gaming: an unrelated, already-resolved issue can't be borrowed)" , async ( ) => {
318390 stubFetch ( ( url ) => {
319391 if ( url . includes ( "/access_tokens" ) ) return Response . json ( { token : "installation-token" } ) ;
@@ -372,6 +444,7 @@ describe("fetchLinkedIssueLabelsForPropagation (#priority-linked-issue-gate)", (
372444 labels : [ "gittensor:feature" ] ,
373445 } ) ;
374446 if ( url . endsWith ( "/pulls/4818" ) ) return Response . json ( { merged_at : "2026-07-11T02:26:24Z" } ) ;
447+ if ( url . includes ( "/issues/2192/timeline" ) ) return Response . json ( [ { event : "closed" , source : { issue : { number : 4818 , pull_request : { } } } } ] ) ;
375448 return new Response ( "not found" , { status : 404 } ) ;
376449 } ) ;
377450 const env = createTestEnv ( { } ) ;
0 commit comments