11import { afterEach , describe , expect , it , vi } from "vitest" ;
22import { runGittensoryLinkedIssueSatisfaction , type LinkedIssueSatisfactionRunInput } from "../../src/services/linked-issue-satisfaction-run" ;
3- import { processJob , runLinkedIssueSatisfactionForAdvisory } from "../../src/queue/processors" ;
3+ import { buildAiReviewDiff , processJob , runLinkedIssueSatisfactionForAdvisory } from "../../src/queue/processors" ;
44import { evaluateGateCheck } from "../../src/rules/advisory" ;
55import {
66 getCachedLinkedIssueSatisfaction ,
@@ -294,6 +294,9 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)"
294294 { repoFullName : "acme/widgets" , pullNumber : 7 , path : "src/a.ts" , status : "modified" , additions : 40 , deletions : 2 , changes : 42 , payload : { patch : "@@\n+app.get('/stream', sse);" } } ,
295295 ] ;
296296 const pr = { number : 7 , title : "Add SSE stream endpoint" , body : "Implements the requested SSE stream." , linkedIssues : [ 1275 ] } ;
297+ const issueText = "Enrich SN74 Gittensor — add SSE stream\n\nWe need a live SSE stream surface for SN74 Gittensor." ;
298+ const processorFingerprint = ( ) =>
299+ linkedIssueSatisfactionCacheInputFingerprint ( { byok : false , provider : null , model : null , issueText, prTitle : pr . title , prBody : pr . body , diff : buildAiReviewDiff ( files ) } ) ;
297300 const advisoryMode = { linkedIssueSatisfactionGateMode : "advisory" , aiReviewByok : false } as RepositorySettings ;
298301 const blockMode = { linkedIssueSatisfactionGateMode : "block" , aiReviewByok : false } as RepositorySettings ;
299302
@@ -532,7 +535,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)"
532535 stubIssueFetch ( ) ;
533536 const run = vi . fn ( async ( ) => ( { response : satisfactionJson ( { status : "unaddressed" , confidence : 0.9 } ) } ) ) ;
534537 const env = enabledEnv ( run ) ;
535- const fingerprint = await linkedIssueSatisfactionCacheInputFingerprint ( { byok : false , provider : null , model : null } ) ;
538+ const fingerprint = await processorFingerprint ( ) ;
536539 await putCachedLinkedIssueSatisfaction ( env , "acme/widgets" , 7 , "sha7" , 1275 , fingerprint , {
537540 status : "ok" ,
538541 result : { status : "addressed" , rationale : "cached: looks done" , confidence : 0.8 } ,
@@ -548,7 +551,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)"
548551 stubIssueFetch ( ) ;
549552 const run = vi . fn ( ) ;
550553 const env = enabledEnv ( run ) ;
551- const fingerprint = await linkedIssueSatisfactionCacheInputFingerprint ( { byok : false , provider : null , model : null } ) ;
554+ const fingerprint = await processorFingerprint ( ) ;
552555 await putCachedLinkedIssueSatisfaction ( env , "acme/widgets" , 7 , "sha7" , 1275 , fingerprint , {
553556 status : "ok" ,
554557 result : { status : "addressed" , rationale : "cached: looks done" , confidence : 0.8 } ,
@@ -589,7 +592,7 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)"
589592 await runLinkedIssueSatisfactionForAdvisory ( env , { settings : advisoryMode , advisory : adv , repoFullName : "acme/widgets" , pr, author : "alice" , files, confirmedContributor : true , installationId : 1 } ) ;
590593 expect ( run ) . toHaveBeenCalledTimes ( 1 ) ;
591594
592- const fingerprint = await linkedIssueSatisfactionCacheInputFingerprint ( { byok : false , provider : null , model : null } ) ;
595+ const fingerprint = await processorFingerprint ( ) ;
593596 const cached = await getCachedLinkedIssueSatisfaction ( env , "acme/widgets" , 7 , "sha7" , 1275 , fingerprint ) ;
594597 expect ( cached ) . toMatchObject ( { status : "ok" , result : { status : "addressed" } } ) ;
595598
@@ -598,6 +601,27 @@ describe("runLinkedIssueSatisfactionForAdvisory (processor wiring, #1961/#3906)"
598601 expect ( run ) . toHaveBeenCalledTimes ( 1 ) ; // still 1 — second pass was a cache hit
599602 } ) ;
600603
604+ it ( "misses the cache when editable issue or PR text changes at the same head SHA" , async ( ) => {
605+ stubIssueFetch ( { body : "We need a live SSE stream surface for SN74 Gittensor." } ) ;
606+ const run = vi . fn ( async ( ) => ( { response : satisfactionJson ( { status : "addressed" , rationale : "The SSE endpoint satisfies the original ask." } ) } ) ) ;
607+ const env = enabledEnv ( run ) ;
608+ const adv = advisory ( ) ;
609+ await expect (
610+ runLinkedIssueSatisfactionForAdvisory ( env , { settings : blockMode , advisory : adv , repoFullName : "acme/widgets" , pr, author : "alice" , files, confirmedContributor : true , installationId : 1 } ) ,
611+ ) . resolves . toMatchObject ( { status : "addressed" } ) ;
612+
613+ stubIssueFetch ( { body : "We now need a GraphQL subscription instead of an SSE stream." } ) ;
614+ run . mockResolvedValueOnce ( { response : satisfactionJson ( { status : "unaddressed" , confidence : 0.9 , rationale : "The changed issue asks for GraphQL, but the diff still adds SSE." } ) } ) ;
615+ const changedAdvisory = advisory ( ) ;
616+ const changedPr = { ...pr , title : "Add SSE endpoint for the old issue" , body : "Still only implements SSE." } ;
617+ await expect (
618+ runLinkedIssueSatisfactionForAdvisory ( env , { settings : blockMode , advisory : changedAdvisory , repoFullName : "acme/widgets" , pr : changedPr , author : "alice" , files, confirmedContributor : true , installationId : 1 } ) ,
619+ ) . resolves . toMatchObject ( { status : "unaddressed" } ) ;
620+
621+ expect ( run ) . toHaveBeenCalledTimes ( 2 ) ;
622+ expect ( changedAdvisory . findings ) . toContainEqual ( expect . objectContaining ( { code : "linked_issue_scope_mismatch" } ) ) ;
623+ } ) ;
624+
601625 it ( "misses the cache when the PR's primary linked issue number changes, even at the same head SHA" , async ( ) => {
602626 stubIssueFetch ( ) ;
603627 const run = vi . fn ( async ( ) => ( { response : satisfactionJson ( { status : "addressed" } ) } ) ) ;
@@ -785,10 +809,15 @@ describe("linked-issue satisfaction wired end-to-end through the real webhook pi
785809 expect ( gatePatchBody . conclusion ) . toBe ( "failure" ) ;
786810 expect ( gatePatchBody . output ?. title ) . toContain ( "Linked issue does not appear to be satisfied" ) ;
787811
788- // The assessment was cached under the PR's primary linked issue number.
789- const fingerprint = await linkedIssueSatisfactionCacheInputFingerprint ( { byok : false , provider : null , model : null } ) ;
790- const cached = await getCachedLinkedIssueSatisfaction ( env , "JSONbored/metagraphed" , 3910 , "realvenus3910" , 1275 , fingerprint ) ;
791- expect ( cached ) . toMatchObject ( { status : "ok" , result : { status : "unaddressed" } } ) ;
812+ // The assessment was cached under the PR's primary linked issue number. The fingerprint itself includes
813+ // prompt text, which this end-to-end test does not need to reconstruct from the webhook pipeline.
814+ const cached = await env . DB . prepare (
815+ "SELECT status, result_json AS resultJson FROM linked_issue_satisfaction_cache WHERE repo_full_name = ? AND pull_number = ? AND head_sha = ? AND linked_issue_number = ?" ,
816+ )
817+ . bind ( "JSONbored/metagraphed" , 3910 , "realvenus3910" , 1275 )
818+ . first < { status : string ; resultJson : string } > ( ) ;
819+ expect ( cached ?. status ) . toBe ( "ok" ) ;
820+ expect ( JSON . parse ( cached ?. resultJson ?? "{}" ) ?. status ) . toBe ( "unaddressed" ) ;
792821 } ) ;
793822
794823 it ( "OFF mode (default): no fetch, no model spend, no cache row, and the comment never mentions linked-issue satisfaction at all — byte-identical to before this feature existed" , async ( ) => {
0 commit comments