@@ -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 } = { } ) {
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 } ,
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 } ,
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" , ( ) => {
@@ -1099,6 +1216,56 @@ test("rechecks provenance, labels, CI, and head immediately before merging", asy
10991216 }
11001217} ) ;
11011218
1219+ test (
1220+ "container worker uses runner identity and writable home without elevated capabilities" ,
1221+ { skip : process . platform === "win32" } ,
1222+ ( t ) => {
1223+ const workspace = temporaryWorkspace ( t ) ;
1224+ const trusted = path . join (
1225+ workspace ,
1226+ "trusted/.github/dependabot-automation" ,
1227+ ) ;
1228+ fs . mkdirSync ( trusted , { recursive : true } ) ;
1229+ for ( const name of [ "task" , "dependency" , "bin" ] ) {
1230+ fs . mkdirSync ( path . join ( workspace , name ) ) ;
1231+ }
1232+ for ( const name of [ "prompt.txt" , "worker.cjs" , "repair-protocol.cjs" ] ) {
1233+ fs . copyFileSync ( path . join ( __dirname , name ) , path . join ( trusted , name ) ) ;
1234+ }
1235+ const callsFile = path . join ( workspace , "docker-calls.jsonl" ) ;
1236+ fs . writeFileSync (
1237+ path . join ( workspace , "bin/docker" ) ,
1238+ '#!/usr/bin/env node\nconst fs = require("node:fs");\nfs.appendFileSync(process.env.DOCKER_CALLS, JSON.stringify(process.argv.slice(2)) + "\\n");\n' ,
1239+ { mode : 0o755 } ,
1240+ ) ;
1241+ execFileSync ( "bash" , [ path . join ( __dirname , "run-worker.sh" ) ] , {
1242+ cwd : workspace ,
1243+ env : {
1244+ ...process . env ,
1245+ PATH : `${ path . join ( workspace , "bin" ) } ${ path . delimiter } ${ process . env . PATH } ` ,
1246+ DOCKER_CALLS : callsFile ,
1247+ GITHUB_TOKEN : "test-token" ,
1248+ } ,
1249+ } ) ;
1250+ const calls = fs
1251+ . readFileSync ( callsFile , "utf8" )
1252+ . trim ( )
1253+ . split ( "\n" )
1254+ . map ( ( line ) => JSON . parse ( line ) ) ;
1255+ assert . equal ( calls . length , 2 ) ;
1256+ assert ( ! calls [ 0 ] . includes ( "GITHUB_TOKEN" ) ) ;
1257+ const worker = calls [ 1 ] ;
1258+ assert . equal (
1259+ worker [ worker . indexOf ( "--user" ) + 1 ] ,
1260+ `${ process . getuid ( ) } :${ process . getgid ( ) } ` ,
1261+ ) ;
1262+ assert ( worker . includes ( "HOME=/tmp" ) ) ;
1263+ assert ( worker . includes ( "GITHUB_TOKEN" ) ) ;
1264+ assert ( worker . includes ( "--cap-drop=ALL" ) ) ;
1265+ assert ( worker . includes ( "--security-opt=no-new-privileges" ) ) ;
1266+ } ,
1267+ ) ;
1268+
11021269test ( "worker collects tracked edits, new tests, and deletions" , ( t ) => {
11031270 const work = temporaryWorkspace ( t ) ;
11041271 const git = ( ...args ) =>
0 commit comments