@@ -16,7 +16,7 @@ const {
1616const { writeInvestigation } = require ( "./evidence.cjs" ) ;
1717const { allowedFile, validateResult } = require ( "./repair-protocol.cjs" ) ;
1818const { collectFiles, run } = require ( "./worker.cjs" ) ;
19- const { dryRun } = require ( "./dry-run.cjs" ) ;
19+ const { dryRun, reportPreview } = require ( "./dry-run.cjs" ) ;
2020
2121const head = "a" . repeat ( 40 ) ;
2222const repo = { owner : "github" , repo : "vscode-codeql" } ;
@@ -80,6 +80,34 @@ function workerWorkspace(t) {
8080 return { work, task, output } ;
8181}
8282
83+ function summaryFixture ( ) {
84+ return {
85+ content : [ ] ,
86+ written : false ,
87+ addHeading ( text ) {
88+ this . content . push ( text ) ;
89+ return this ;
90+ } ,
91+ addRaw ( text ) {
92+ this . content . push ( text ) ;
93+ return this ;
94+ } ,
95+ addTable ( rows ) {
96+ this . content . push (
97+ ...rows . flat ( ) . filter ( ( cell ) => typeof cell === "string" ) ,
98+ ) ;
99+ return this ;
100+ } ,
101+ addList ( items ) {
102+ this . content . push ( ...items ) ;
103+ return this ;
104+ } ,
105+ async write ( ) {
106+ this . written = true ;
107+ } ,
108+ } ;
109+ }
110+
83111function harness ( { workspace, runNumber = 1 } = { } ) {
84112 const state = {
85113 pr : candidate ( ) ,
@@ -343,57 +371,146 @@ test("planning propagates unavailable CI instead of reporting a decision", async
343371 assert . deepEqual ( state . outputs , { } ) ;
344372} ) ;
345373
346- test ( "dry-run orchestration reports investigation using only read APIs" , async ( ) => {
347- const { state, github } = harness ( ) ;
348- state . pr . auto_merge = { } ;
349- state . runs [ 0 ] . conclusion = "failure" ;
350- state . runs [ 0 ] . run_attempt = 2 ;
351- const readOnlyGithub = {
352- rest : {
353- pulls : {
354- list : github . rest . pulls . list ,
355- get : github . rest . pulls . get ,
356- listCommits : github . rest . pulls . listCommits ,
357- } ,
358- actions : {
359- listWorkflowRunsForRepo : github . rest . actions . listWorkflowRunsForRepo ,
360- } ,
361- } ,
362- paginate : github . paginate ,
363- } ;
364- const content = [ ] ;
365- let written = false ;
366- const summary = {
367- addHeading ( text ) {
368- content . push ( text ) ;
369- return this ;
370- } ,
371- addRaw ( text ) {
372- content . push ( text ) ;
373- return this ;
374- } ,
375- addTable ( rows ) {
376- content . push ( ...rows . flat ( ) . filter ( ( cell ) => typeof cell === "string" ) ) ;
377- return this ;
378- } ,
379- async write ( ) {
380- written = true ;
381- } ,
382- } ;
383- await dryRun ( {
384- github : readOnlyGithub ,
385- context : { repo, runNumber : 1 } ,
386- core : { info ( ) { } , setOutput ( ) { } , summary } ,
387- } ) ;
388- assert ( written ) ;
389- const report = content . join ( "\n" ) ;
390- assert . match ( report , / # 4 2 / ) ;
391- assert ( report . includes ( head ) ) ;
392- assert . match ( report , / A d d r e v i e w h o l d , c o l l e c t e v i d e n c e , a n d i n v o k e C o p i l o t / ) ;
393- assert . match ( report , / R e a d - o n l y p r e v i e w / ) ;
394- assert . match ( report , / n o t v a l i d a t e d / ) ;
395- assert . deepEqual ( state . calls , [ ] ) ;
396- assert . deepEqual ( state . pr . labels , [ ] ) ;
374+ test ( "dry-run prepares only failed-PR previews without reruns or PR writes" , async ( t ) => {
375+ for ( const [ name , attempt ] of [
376+ [ "passing" , null ] ,
377+ [ "retry bypass" , 1 ] ,
378+ [ "investigation" , 2 ] ,
379+ ] ) {
380+ await t . test ( name , async ( t ) => {
381+ const workspace = temporaryWorkspace ( t ) ;
382+ const { state, github } = harness ( { workspace } ) ;
383+ state . pr . auto_merge = { } ;
384+ if ( attempt !== null ) {
385+ state . runs [ 0 ] . conclusion = "failure" ;
386+ state . runs [ 0 ] . run_attempt = attempt ;
387+ }
388+ const originalPr = structuredClone ( state . pr ) ;
389+ const readOnlyGithub = {
390+ rest : {
391+ pulls : {
392+ list : github . rest . pulls . list ,
393+ get : github . rest . pulls . get ,
394+ listCommits : github . rest . pulls . listCommits ,
395+ listFiles : github . rest . pulls . listFiles ,
396+ } ,
397+ actions : {
398+ listWorkflowRunsForRepo :
399+ github . rest . actions . listWorkflowRunsForRepo ,
400+ listJobsForWorkflowRun : github . rest . actions . listJobsForWorkflowRun ,
401+ downloadJobLogsForWorkflowRun :
402+ github . rest . actions . downloadJobLogsForWorkflowRun ,
403+ } ,
404+ } ,
405+ paginate : github . paginate ,
406+ } ;
407+ const summary = summaryFixture ( ) ;
408+ await dryRun ( {
409+ github : readOnlyGithub ,
410+ context : { repo, runNumber : 1 } ,
411+ core : {
412+ info ( ) { } ,
413+ setOutput : ( name , value ) => ( state . outputs [ name ] = value ) ,
414+ summary,
415+ } ,
416+ workspace,
417+ } ) ;
418+ assert ( summary . written ) ;
419+ const report = summary . content . join ( "\n" ) ;
420+ assert . match ( report , / # 4 2 / ) ;
421+ assert ( report . includes ( head ) ) ;
422+ const previews = JSON . parse ( state . outputs . previews ) ;
423+ if ( attempt === null ) {
424+ assert . deepEqual ( previews , [ ] ) ;
425+ assert . equal ( state . outputs [ "has-previews" ] , false ) ;
426+ assert . deepEqual ( fs . readdirSync ( workspace ) , [ ] ) ;
427+ } else {
428+ assert . deepEqual ( previews , [ { pr : 42 , head } ] ) ;
429+ assert . equal ( state . outputs [ "has-previews" ] , true ) ;
430+ const directory = path . join ( workspace , "investigations/42" ) ;
431+ const evidence = JSON . parse (
432+ fs . readFileSync ( path . join ( directory , "context.json" ) , "utf8" ) ,
433+ ) ;
434+ assert . equal ( evidence . preview , true ) ;
435+ assert . equal ( evidence . head , head ) ;
436+ assert . deepEqual (
437+ evidence . runs . map ( ( run ) => run . id ) ,
438+ [ 1 ] ,
439+ ) ;
440+ assert . equal (
441+ fs . readFileSync (
442+ path . join ( directory , evidence . jobs [ 0 ] . log . file ) ,
443+ "utf8" ,
444+ ) ,
445+ state . log ,
446+ ) ;
447+ assert . equal ( state . runs [ 0 ] . run_attempt , attempt ) ;
448+ if ( attempt === 1 ) assert . match ( report , / Y e s \( r e t r y b y p a s s e d \) / ) ;
449+ }
450+ assert . deepEqual ( state . calls , [ ] ) ;
451+ assert . deepEqual ( state . comments , [ ] ) ;
452+ assert . deepEqual ( state . pr , originalPr ) ;
453+ } ) ;
454+ }
455+ } ) ;
456+
457+ test ( "Copilot preview reports validated outcomes as text without publishing" , async ( t ) => {
458+ for ( const outcome of [ "patch" , "needs-maintainer" ] ) {
459+ await t . test ( outcome , async ( t ) => {
460+ const workspace = temporaryWorkspace ( t ) ;
461+ fs . mkdirSync ( path . join ( workspace , "result" ) ) ;
462+ const files =
463+ outcome === "patch"
464+ ? [
465+ { path : "extensions/ql-vscode/src/a.ts" , content : "fixed" } ,
466+ { path : "extensions/ql-vscode/src/deleted.ts" , content : null } ,
467+ ]
468+ : [ ] ;
469+ fs . writeFileSync (
470+ path . join ( workspace , "result/result.json" ) ,
471+ JSON . stringify ( {
472+ outcome,
473+ summary : 'Explanation with <img src="untrusted"> & evidence.' ,
474+ files,
475+ } ) ,
476+ ) ;
477+ const summary = summaryFixture ( ) ;
478+ await reportPreview ( {
479+ core : { summary } ,
480+ number : 42 ,
481+ head,
482+ workspace,
483+ } ) ;
484+ assert ( summary . written ) ;
485+ const report = summary . content . join ( "\n" ) ;
486+ assert . match ( report , / P R # 4 2 / ) ;
487+ assert ( report . includes ( head ) ) ;
488+ assert ( report . includes ( outcome ) ) ;
489+ assert ( ! report . includes ( "<img" ) ) ;
490+ assert ( report . includes ( '<img src="untrusted"> & evidence.' ) ) ;
491+ for ( const file of files ) assert ( report . includes ( file . path ) ) ;
492+ } ) ;
493+ }
494+ } ) ;
495+
496+ test ( "Copilot preview rejects unsafe artifacts before writing its report" , async ( t ) => {
497+ const workspace = temporaryWorkspace ( t ) ;
498+ fs . mkdirSync ( path . join ( workspace , "result" ) ) ;
499+ fs . writeFileSync (
500+ path . join ( workspace , "result/result.json" ) ,
501+ JSON . stringify ( {
502+ outcome : "patch" ,
503+ summary : "Unsafe repair" ,
504+ files : [ { path : ".github/workflows/main.yml" , content : "untrusted" } ] ,
505+ } ) ,
506+ ) ;
507+ const summary = summaryFixture ( ) ;
508+ await assert . rejects (
509+ reportPreview ( { core : { summary } , number : 42 , head, workspace } ) ,
510+ / u n s u p p o r t e d , o r i n v a l i d f i l e / ,
511+ ) ;
512+ assert . equal ( summary . written , false ) ;
513+ assert . deepEqual ( summary . content , [ ] ) ;
397514} ) ;
398515
399516test ( "only open, non-draft, same-repository Dependabot PRs qualify" , ( ) => {
0 commit comments