@@ -23,7 +23,7 @@ import type {
2323 ReviewSource ,
2424} from '../types.ts' ;
2525import { createChangedFile } from './helpers/fixtures.ts' ;
26- import { waitFor } from './helpers/react.tsx' ;
26+ import { renderReact , waitFor } from './helpers/react.tsx' ;
2727
2828const reactActEnvironment = globalThis as typeof globalThis & {
2929 ResizeObserver ?: typeof ResizeObserver ;
@@ -1059,6 +1059,307 @@ test('commit details render inline in the diff view', async () => {
10591059 }
10601060} ) ;
10611061
1062+ test ( 'pull request descriptions render as provider-aware Markdown source context' , async ( ) => {
1063+ const changedFile = createChangedFile ( 'src/app.ts' ) ;
1064+ const cases : ReadonlyArray < {
1065+ label : string ;
1066+ source : Extract < ReviewSource , { type : 'pull-request' } > ;
1067+ } > = [
1068+ {
1069+ label : 'PR description' ,
1070+ source : {
1071+ author : {
1072+ avatarUrl : 'https://avatars.githubusercontent.com/u/1?v=4' ,
1073+ login : 'octocat' ,
1074+ url : 'https://github.com/octocat' ,
1075+ } ,
1076+ description : '## Intent\n\nShip **review** context.' ,
1077+ number : 12 ,
1078+ provider : 'github' ,
1079+ type : 'pull-request' ,
1080+ url : 'https://github.com/nkzw-tech/codiff/pull/12' ,
1081+ } ,
1082+ } ,
1083+ {
1084+ label : 'MR description' ,
1085+ source : {
1086+ description : '## Intent\n\nShip **review** context.' ,
1087+ number : 13 ,
1088+ provider : 'gitlab' ,
1089+ type : 'pull-request' ,
1090+ url : 'https://gitlab.example.com/group/project/-/merge_requests/13' ,
1091+ } ,
1092+ } ,
1093+ {
1094+ label : 'Description' ,
1095+ source : {
1096+ description : '## Intent\n\nShip **review** context.' ,
1097+ number : 14 ,
1098+ type : 'pull-request' ,
1099+ url : 'https://example.com/reviews/14' ,
1100+ } ,
1101+ } ,
1102+ ] ;
1103+
1104+ for ( const { label, source } of cases ) {
1105+ window . codiff = createCodiffMock ( {
1106+ getRepositoryState : vi . fn ( async ( ) => ( {
1107+ ...repositoryState ,
1108+ files : [ changedFile ] ,
1109+ source,
1110+ } ) ) ,
1111+ } ) ;
1112+
1113+ const app = await renderReact ( < App /> ) ;
1114+
1115+ try {
1116+ await waitFor ( ( ) => {
1117+ expect ( app . container . querySelector ( '.codiff-source-description-header' ) ) . not . toBeNull ( ) ;
1118+ } ) ;
1119+ const header = app . container . querySelector < HTMLElement > ( '.codiff-source-description-header' ) ;
1120+ const body = app . container . querySelector < HTMLElement > ( '.source-description-markdown' ) ;
1121+ expect ( body ?. textContent ) . toContain ( 'Intent' ) ;
1122+ expect ( body ?. textContent ) . toContain ( 'Ship review context.' ) ;
1123+ expect ( header ?. querySelector ( '.codiff-file-path' ) ?. textContent ) . toBe ( label ) ;
1124+ expect ( header ?. querySelector ( '.source-description-title' ) ) . toBeNull ( ) ;
1125+ if ( source . author ) {
1126+ expect ( header ?. querySelector ( '.source-description-author' ) ?. textContent ) . toContain (
1127+ `@${ source . author . login } ` ,
1128+ ) ;
1129+ } else {
1130+ expect ( header ?. querySelector ( '.source-description-author' ) ) . toBeNull ( ) ;
1131+ }
1132+ const toggle = header ?. querySelector < HTMLButtonElement > ( 'button.codiff-header-toggle' ) ;
1133+ expect ( toggle ) . not . toBeNull ( ) ;
1134+ expect ( toggle ?. getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' ) ;
1135+ expect ( toggle ?. type ) . toBe ( 'button' ) ;
1136+ } finally {
1137+ await app . cleanup ( ) ;
1138+ }
1139+ }
1140+ } ) ;
1141+
1142+ test ( 'pull request description collapse button toggles the markdown body' , async ( ) => {
1143+ const source = {
1144+ description : '## Intent\n\nShip **review** context.' ,
1145+ number : 12 ,
1146+ provider : 'github' ,
1147+ type : 'pull-request' ,
1148+ url : 'https://github.com/nkzw-tech/codiff/pull/12' ,
1149+ } satisfies ReviewSource ;
1150+
1151+ window . codiff = createCodiffMock ( {
1152+ getRepositoryState : vi . fn ( async ( ) => ( {
1153+ ...repositoryState ,
1154+ files : [ createChangedFile ( 'src/app.ts' ) ] ,
1155+ source,
1156+ } ) ) ,
1157+ } ) ;
1158+
1159+ const app = await renderReact ( < App /> ) ;
1160+
1161+ try {
1162+ await waitFor ( ( ) => {
1163+ expect ( app . container . querySelector ( '.source-description-markdown' ) ) . not . toBeNull ( ) ;
1164+ } ) ;
1165+
1166+ let toggle = app . container . querySelector < HTMLButtonElement > ( 'button.codiff-header-toggle' ) ;
1167+ expect ( toggle ?. getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' ) ;
1168+ expect ( app . container . querySelector ( '.source-description-markdown' ) ?. textContent ) . toContain (
1169+ 'Ship review context.' ,
1170+ ) ;
1171+
1172+ await act ( async ( ) => {
1173+ toggle ?. click ( ) ;
1174+ } ) ;
1175+
1176+ await waitFor ( ( ) => {
1177+ toggle = app . container . querySelector < HTMLButtonElement > ( 'button.codiff-header-toggle' ) ;
1178+ expect ( toggle ?. getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' ) ;
1179+ expect ( app . container . querySelector ( '.source-description-markdown' ) ) . toBeNull ( ) ;
1180+ } ) ;
1181+
1182+ await act ( async ( ) => {
1183+ toggle ?. click ( ) ;
1184+ } ) ;
1185+
1186+ await waitFor ( ( ) => {
1187+ expect (
1188+ app . container
1189+ . querySelector < HTMLButtonElement > ( 'button.codiff-header-toggle' )
1190+ ?. getAttribute ( 'aria-expanded' ) ,
1191+ ) . toBe ( 'true' ) ;
1192+ expect ( app . container . querySelector ( '.source-description-markdown' ) ?. textContent ) . toContain (
1193+ 'Ship review context.' ,
1194+ ) ;
1195+ } ) ;
1196+ } finally {
1197+ await app . cleanup ( ) ;
1198+ }
1199+ } ) ;
1200+
1201+ test ( 'missing pull request descriptions do not render placeholder source context' , async ( ) => {
1202+ window . codiff = createCodiffMock ( {
1203+ getRepositoryState : vi . fn ( async ( ) => ( {
1204+ ...repositoryState ,
1205+ files : [ createChangedFile ( 'src/app.ts' ) ] ,
1206+ source : {
1207+ description : ' ' ,
1208+ number : 12 ,
1209+ provider : 'github' ,
1210+ type : 'pull-request' ,
1211+ url : 'https://github.com/nkzw-tech/codiff/pull/12' ,
1212+ } satisfies ReviewSource ,
1213+ } ) ) ,
1214+ } ) ;
1215+
1216+ const app = await renderReact ( < App /> ) ;
1217+
1218+ try {
1219+ await waitFor ( ( ) => {
1220+ expect ( app . container . querySelector ( '.codiff-file-header' ) ) . not . toBeNull ( ) ;
1221+ } ) ;
1222+ expect ( app . container . querySelector ( '.codiff-source-description-header' ) ) . toBeNull ( ) ;
1223+ expect ( app . container . textContent ) . not . toContain ( 'PR description' ) ;
1224+ } finally {
1225+ await app . cleanup ( ) ;
1226+ }
1227+ } ) ;
1228+
1229+ test ( 'title-only pull request source context renders as a collapsed static header' , async ( ) => {
1230+ const cases : ReadonlyArray < Extract < ReviewSource , { type : 'pull-request' } > > = [
1231+ {
1232+ description : ' ' ,
1233+ number : 12 ,
1234+ provider : 'github' ,
1235+ title : 'Title without a body' ,
1236+ type : 'pull-request' ,
1237+ url : 'https://github.com/nkzw-tech/codiff/pull/12' ,
1238+ } ,
1239+ {
1240+ number : 13 ,
1241+ provider : 'gitlab' ,
1242+ title : 'Merge request title only' ,
1243+ type : 'pull-request' ,
1244+ url : 'https://gitlab.example.com/group/project/-/merge_requests/13' ,
1245+ } ,
1246+ ] ;
1247+
1248+ for ( const source of cases ) {
1249+ window . codiff = createCodiffMock ( {
1250+ getRepositoryState : vi . fn ( async ( ) => ( {
1251+ ...repositoryState ,
1252+ files : [ createChangedFile ( 'src/app.ts' ) ] ,
1253+ source,
1254+ } ) ) ,
1255+ } ) ;
1256+
1257+ const app = await renderReact ( < App /> ) ;
1258+
1259+ try {
1260+ await waitFor ( ( ) => {
1261+ expect ( app . container . querySelector ( '.codiff-source-description-header' ) ) . not . toBeNull ( ) ;
1262+ } ) ;
1263+ const header = app . container . querySelector < HTMLElement > ( '.codiff-source-description-header' ) ;
1264+ expect ( header ?. classList . contains ( 'collapsed' ) ) . toBe ( true ) ;
1265+ expect ( header ?. querySelector ( '.source-description-title' ) ?. textContent ) . toBe ( source . title ) ;
1266+ expect ( header ?. querySelector ( '.codiff-header-toggle-static' ) ) . not . toBeNull ( ) ;
1267+ expect ( header ?. querySelector ( 'button.codiff-header-toggle' ) ) . toBeNull ( ) ;
1268+ expect ( header ?. querySelector ( '.codiff-chevron-box' ) ) . toBeNull ( ) ;
1269+ expect ( app . container . querySelector ( '.source-description-markdown' ) ) . toBeNull ( ) ;
1270+ } finally {
1271+ await app . cleanup ( ) ;
1272+ }
1273+ }
1274+ } ) ;
1275+
1276+ test ( 'pull request descriptions stay visible in walkthrough mode' , async ( ) => {
1277+ const changedFile = createChangedFile ( 'src/app.ts' ) ;
1278+ const source = {
1279+ description : '## Summary\n\nKeep the PR context visible.' ,
1280+ number : 12 ,
1281+ provider : 'github' ,
1282+ title : 'Keep context visible in walkthrough' ,
1283+ type : 'pull-request' ,
1284+ url : 'https://github.com/nkzw-tech/codiff/pull/12' ,
1285+ } satisfies ReviewSource ;
1286+ const narrativeWalkthrough = {
1287+ agent : 'codex' ,
1288+ chapters : [
1289+ {
1290+ blurb : 'Review the implementation.' ,
1291+ icon : 'gear' ,
1292+ id : 'impl' ,
1293+ stops : [
1294+ {
1295+ added : 1 ,
1296+ deleted : 1 ,
1297+ hunkIds : [ 'src/app.ts:unstaged:h1' ] ,
1298+ hunks : [
1299+ {
1300+ added : 1 ,
1301+ anchor : { display : 'src/app.ts' , sectionId : 'src/app.ts:unstaged' , side : 'both' } ,
1302+ deleted : 1 ,
1303+ id : 'src/app.ts:unstaged:h1' ,
1304+ path : 'src/app.ts' ,
1305+ status : 'modified' ,
1306+ } ,
1307+ ] ,
1308+ id : 's1' ,
1309+ importance : 'critical' ,
1310+ prose : 'Review this file.' ,
1311+ title : 'Implementation path' ,
1312+ } ,
1313+ ] ,
1314+ title : 'Implementation' ,
1315+ } ,
1316+ ] ,
1317+ focus : 'Focus.' ,
1318+ generatedAt : '2026-06-07T00:00:00.000Z' ,
1319+ kind : 'narrative' ,
1320+ repo : { branch : 'main' , root : '/repo' } ,
1321+ source,
1322+ support : [ ] ,
1323+ title : 'Narrative' ,
1324+ version : 4 ,
1325+ } satisfies NarrativeWalkthrough ;
1326+
1327+ window . codiff = createCodiffMock ( {
1328+ getLaunchOptions : vi . fn ( async ( ) => ( {
1329+ repositoryPathProvided : true ,
1330+ source,
1331+ walkthrough : true ,
1332+ walkthroughFile : '/tmp/walkthrough.json' ,
1333+ } ) ) ,
1334+ getNarrativeWalkthrough : vi . fn ( async ( ) => ( {
1335+ status : 'ready' as const ,
1336+ walkthrough : narrativeWalkthrough ,
1337+ } ) ) ,
1338+ getRepositoryState : vi . fn ( async ( ) => ( {
1339+ ...repositoryState ,
1340+ files : [ changedFile ] ,
1341+ source,
1342+ } ) ) ,
1343+ } ) ;
1344+
1345+ const app = await renderReact ( < App /> ) ;
1346+
1347+ try {
1348+ await waitFor ( ( ) => {
1349+ expect ( app . container . querySelector ( '.codiff-source-description-header' ) ) . not . toBeNull ( ) ;
1350+ expect ( app . container . querySelector ( '.wt-stop-block' ) ) . not . toBeNull ( ) ;
1351+ } ) ;
1352+
1353+ const header = app . container . querySelector < HTMLElement > ( '.codiff-source-description-header' ) ;
1354+ expect ( header ?. textContent ) . toContain ( 'Keep context visible in walkthrough' ) ;
1355+ expect ( app . container . querySelector ( '.source-description-markdown' ) ?. textContent ) . toContain (
1356+ 'Keep the PR context visible.' ,
1357+ ) ;
1358+ } finally {
1359+ await app . cleanup ( ) ;
1360+ }
1361+ } ) ;
1362+
10621363test ( 'narrative walkthrough stops do not repeat commit details' , async ( ) => {
10631364 const changedFile = createChangedFile ( 'src/app.ts' ) ;
10641365 const source = { ref : 'abc1234' , type : 'commit' } satisfies ReviewSource ;
0 commit comments