@@ -56,6 +56,7 @@ interface TestFixture {
5656
5757let fixture : TestFixture ;
5858const wsRequests : WsRequestEnvelope [ "body" ] [ ] = [ ] ;
59+ let customWsRpcResolver : ( ( body : WsRequestEnvelope [ "body" ] ) => unknown | undefined ) | null = null ;
5960const wsLink = ws . link ( / w s ( s ) ? : \/ \/ .* / ) ;
6061
6162interface ViewportSpec {
@@ -433,6 +434,10 @@ function createSnapshotWithLongProposedPlan(): OrchestrationReadModel {
433434}
434435
435436function resolveWsRpc ( body : WsRequestEnvelope [ "body" ] ) : unknown {
437+ const customResult = customWsRpcResolver ?.( body ) ;
438+ if ( customResult !== undefined ) {
439+ return customResult ;
440+ }
436441 const tag = body . _tag ;
437442 if ( tag === ORCHESTRATION_WS_METHODS . getSnapshot ) {
438443 return fixture . snapshot ;
@@ -767,9 +772,11 @@ async function mountChatView(options: {
767772 viewport : ViewportSpec ;
768773 snapshot : OrchestrationReadModel ;
769774 configureFixture ?: ( fixture : TestFixture ) => void ;
775+ resolveRpc ?: ( body : WsRequestEnvelope [ "body" ] ) => unknown | undefined ;
770776} ) : Promise < MountedChatView > {
771777 fixture = buildFixture ( options . snapshot ) ;
772778 options . configureFixture ?.( fixture ) ;
779+ customWsRpcResolver = options . resolveRpc ?? null ;
773780 await setViewport ( options . viewport ) ;
774781 await waitForProductionStyles ( ) ;
775782
@@ -795,6 +802,7 @@ async function mountChatView(options: {
795802 await waitForLayout ( ) ;
796803
797804 const cleanup = async ( ) => {
805+ customWsRpcResolver = null ;
798806 await screen . unmount ( ) ;
799807 host . remove ( ) ;
800808 } ;
@@ -854,6 +862,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
854862 localStorage . clear ( ) ;
855863 document . body . innerHTML = "" ;
856864 wsRequests . length = 0 ;
865+ customWsRpcResolver = null ;
857866 useComposerDraftStore . setState ( {
858867 draftsByThreadId : { } ,
859868 draftThreadsByThreadId : { } ,
@@ -869,6 +878,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
869878 } ) ;
870879
871880 afterEach ( ( ) => {
881+ customWsRpcResolver = null ;
872882 document . body . innerHTML = "" ;
873883 } ) ;
874884
@@ -1357,6 +1367,154 @@ describe("ChatView timeline estimator parity (full app)", () => {
13571367 }
13581368 } ) ;
13591369
1370+ it ( "runs setup scripts after preparing a pull request worktree thread" , async ( ) => {
1371+ useComposerDraftStore . setState ( {
1372+ draftThreadsByThreadId : {
1373+ [ THREAD_ID ] : {
1374+ projectId : PROJECT_ID ,
1375+ createdAt : NOW_ISO ,
1376+ runtimeMode : "full-access" ,
1377+ interactionMode : "default" ,
1378+ branch : null ,
1379+ worktreePath : null ,
1380+ envMode : "local" ,
1381+ } ,
1382+ } ,
1383+ projectDraftThreadIdByProjectId : {
1384+ [ PROJECT_ID ] : THREAD_ID ,
1385+ } ,
1386+ } ) ;
1387+
1388+ const mounted = await mountChatView ( {
1389+ viewport : DEFAULT_VIEWPORT ,
1390+ snapshot : withProjectScripts ( createDraftOnlySnapshot ( ) , [
1391+ {
1392+ id : "setup" ,
1393+ name : "Setup" ,
1394+ command : "bun install" ,
1395+ icon : "configure" ,
1396+ runOnWorktreeCreate : true ,
1397+ } ,
1398+ ] ) ,
1399+ resolveRpc : ( body ) => {
1400+ if ( body . _tag === WS_METHODS . gitResolvePullRequest ) {
1401+ return {
1402+ pullRequest : {
1403+ number : 1359 ,
1404+ title : "Add thread archiving and settings navigation" ,
1405+ url : "https://github.com/pingdotgg/t3code/pull/1359" ,
1406+ baseBranch : "main" ,
1407+ headBranch : "archive-settings-overhaul" ,
1408+ state : "open" ,
1409+ } ,
1410+ } ;
1411+ }
1412+ if ( body . _tag === WS_METHODS . gitPreparePullRequestThread ) {
1413+ return {
1414+ pullRequest : {
1415+ number : 1359 ,
1416+ title : "Add thread archiving and settings navigation" ,
1417+ url : "https://github.com/pingdotgg/t3code/pull/1359" ,
1418+ baseBranch : "main" ,
1419+ headBranch : "archive-settings-overhaul" ,
1420+ state : "open" ,
1421+ } ,
1422+ branch : "archive-settings-overhaul" ,
1423+ worktreePath : "/repo/worktrees/pr-1359" ,
1424+ } ;
1425+ }
1426+ return undefined ;
1427+ } ,
1428+ } ) ;
1429+
1430+ try {
1431+ const branchButton = await waitForElement (
1432+ ( ) =>
1433+ Array . from ( document . querySelectorAll ( "button" ) ) . find (
1434+ ( button ) => button . textContent ?. trim ( ) === "main" ,
1435+ ) as HTMLButtonElement | null ,
1436+ "Unable to find branch selector button." ,
1437+ ) ;
1438+ branchButton . click ( ) ;
1439+
1440+ const branchInput = await waitForElement (
1441+ ( ) => document . querySelector < HTMLInputElement > ( 'input[placeholder="Search branches..."]' ) ,
1442+ "Unable to find branch search input." ,
1443+ ) ;
1444+ branchInput . focus ( ) ;
1445+ await page . getByPlaceholder ( "Search branches..." ) . fill ( "1359" ) ;
1446+
1447+ const checkoutItem = await waitForElement (
1448+ ( ) =>
1449+ Array . from ( document . querySelectorAll ( "span" ) ) . find (
1450+ ( element ) => element . textContent ?. trim ( ) === "Checkout Pull Request" ,
1451+ ) as HTMLSpanElement | null ,
1452+ "Unable to find checkout pull request option." ,
1453+ ) ;
1454+ checkoutItem . click ( ) ;
1455+
1456+ const worktreeButton = await waitForElement (
1457+ ( ) =>
1458+ Array . from ( document . querySelectorAll ( "button" ) ) . find (
1459+ ( button ) => button . textContent ?. trim ( ) === "Worktree" ,
1460+ ) as HTMLButtonElement | null ,
1461+ "Unable to find Worktree button." ,
1462+ ) ;
1463+ worktreeButton . click ( ) ;
1464+
1465+ await vi . waitFor (
1466+ ( ) => {
1467+ const prepareRequest = wsRequests . find (
1468+ ( request ) => request . _tag === WS_METHODS . gitPreparePullRequestThread ,
1469+ ) ;
1470+ expect ( prepareRequest ) . toMatchObject ( {
1471+ _tag : WS_METHODS . gitPreparePullRequestThread ,
1472+ cwd : "/repo/project" ,
1473+ reference : "1359" ,
1474+ mode : "worktree" ,
1475+ } ) ;
1476+ } ,
1477+ { timeout : 8_000 , interval : 16 } ,
1478+ ) ;
1479+
1480+ await vi . waitFor (
1481+ ( ) => {
1482+ const openRequest = wsRequests . find (
1483+ ( request ) =>
1484+ request . _tag === WS_METHODS . terminalOpen && request . cwd === "/repo/worktrees/pr-1359" ,
1485+ ) ;
1486+ expect ( openRequest ) . toMatchObject ( {
1487+ _tag : WS_METHODS . terminalOpen ,
1488+ threadId : expect . any ( String ) ,
1489+ cwd : "/repo/worktrees/pr-1359" ,
1490+ env : {
1491+ T3CODE_PROJECT_ROOT : "/repo/project" ,
1492+ T3CODE_WORKTREE_PATH : "/repo/worktrees/pr-1359" ,
1493+ } ,
1494+ } ) ;
1495+ } ,
1496+ { timeout : 8_000 , interval : 16 } ,
1497+ ) ;
1498+
1499+ await vi . waitFor (
1500+ ( ) => {
1501+ const writeRequest = wsRequests . find (
1502+ ( request ) =>
1503+ request . _tag === WS_METHODS . terminalWrite && request . data === "bun install\r" ,
1504+ ) ;
1505+ expect ( writeRequest ) . toMatchObject ( {
1506+ _tag : WS_METHODS . terminalWrite ,
1507+ threadId : expect . any ( String ) ,
1508+ data : "bun install\r" ,
1509+ } ) ;
1510+ } ,
1511+ { timeout : 8_000 , interval : 16 } ,
1512+ ) ;
1513+ } finally {
1514+ await mounted . cleanup ( ) ;
1515+ }
1516+ } ) ;
1517+
13601518 it ( "toggles plan mode with Shift+Tab only while the composer is focused" , async ( ) => {
13611519 const mounted = await mountChatView ( {
13621520 viewport : DEFAULT_VIEWPORT ,
0 commit comments