@@ -830,6 +830,154 @@ test('Open PR drawer base dropdown updates from mocked repo branches', async ({
830830 ) . toBe ( true )
831831} )
832832
833+ test ( 'Open PR drawer keeps a single active PR context in localStorage' , async ( {
834+ page,
835+ } ) => {
836+ await page . route ( 'https://api.github.com/user/repos**' , async route => {
837+ await route . fulfill ( {
838+ status : 200 ,
839+ contentType : 'application/json' ,
840+ body : JSON . stringify ( [
841+ {
842+ id : 2 ,
843+ owner : { login : 'knightedcodemonkey' } ,
844+ name : 'develop' ,
845+ full_name : 'knightedcodemonkey/develop' ,
846+ default_branch : 'main' ,
847+ permissions : { push : true } ,
848+ } ,
849+ {
850+ id : 1 ,
851+ owner : { login : 'knightedcodemonkey' } ,
852+ name : 'css' ,
853+ full_name : 'knightedcodemonkey/css' ,
854+ default_branch : 'stable' ,
855+ permissions : { push : true } ,
856+ } ,
857+ ] ) ,
858+ } )
859+ } )
860+
861+ await mockRepositoryBranches ( page , {
862+ 'knightedcodemonkey/develop' : [ 'main' , 'develop-next' ] ,
863+ 'knightedcodemonkey/css' : [ 'stable' , 'release/1.x' ] ,
864+ } )
865+
866+ await waitForAppReady ( page , `${ appEntryPath } ?feature-ai=true` )
867+
868+ await page . locator ( '#github-token-input' ) . fill ( 'github_pat_fake_1234567890' )
869+ await page . locator ( '#github-token-add' ) . click ( )
870+ await ensureOpenPrDrawerOpen ( page )
871+
872+ const repoSelect = page . locator ( '#github-pr-repo-select' )
873+ const componentPath = page . locator ( '#github-pr-component-path' )
874+
875+ await repoSelect . selectOption ( 'knightedcodemonkey/develop' )
876+ await componentPath . fill ( 'examples/develop/App.tsx' )
877+ await componentPath . blur ( )
878+
879+ await repoSelect . selectOption ( 'knightedcodemonkey/css' )
880+ await componentPath . fill ( 'examples/css/App.tsx' )
881+ await componentPath . blur ( )
882+
883+ const activeContext = await page . evaluate ( ( ) => {
884+ const storagePrefix = 'knighted:develop:github-pr-config:'
885+ const keys = Object . keys ( localStorage ) . filter ( key => key . startsWith ( storagePrefix ) )
886+ const key = keys [ 0 ] ?? null
887+ const raw = key ? localStorage . getItem ( key ) : null
888+
889+ let parsed = null
890+ try {
891+ parsed = raw ? JSON . parse ( raw ) : null
892+ } catch {
893+ parsed = null
894+ }
895+
896+ return { keys, key, parsed }
897+ } )
898+
899+ expect ( activeContext . keys ) . toHaveLength ( 1 )
900+ expect ( activeContext . key ) . toBe (
901+ 'knighted:develop:github-pr-config:knightedcodemonkey/css' ,
902+ )
903+ expect ( activeContext . parsed ?. componentFilePath ) . toBe ( 'examples/css/App.tsx' )
904+ } )
905+
906+ test ( 'Open PR drawer does not prune saved PR context on repo switch before save' , async ( {
907+ page,
908+ } ) => {
909+ await page . route ( 'https://api.github.com/user/repos**' , async route => {
910+ await route . fulfill ( {
911+ status : 200 ,
912+ contentType : 'application/json' ,
913+ body : JSON . stringify ( [
914+ {
915+ id : 2 ,
916+ owner : { login : 'knightedcodemonkey' } ,
917+ name : 'develop' ,
918+ full_name : 'knightedcodemonkey/develop' ,
919+ default_branch : 'main' ,
920+ permissions : { push : true } ,
921+ } ,
922+ {
923+ id : 1 ,
924+ owner : { login : 'knightedcodemonkey' } ,
925+ name : 'css' ,
926+ full_name : 'knightedcodemonkey/css' ,
927+ default_branch : 'stable' ,
928+ permissions : { push : true } ,
929+ } ,
930+ ] ) ,
931+ } )
932+ } )
933+
934+ await mockRepositoryBranches ( page , {
935+ 'knightedcodemonkey/develop' : [ 'main' , 'develop-next' ] ,
936+ 'knightedcodemonkey/css' : [ 'stable' , 'release/1.x' ] ,
937+ } )
938+
939+ await waitForAppReady ( page , `${ appEntryPath } ?feature-ai=true` )
940+
941+ await page . locator ( '#github-token-input' ) . fill ( 'github_pat_fake_1234567890' )
942+ await page . locator ( '#github-token-add' ) . click ( )
943+ await ensureOpenPrDrawerOpen ( page )
944+
945+ const repoSelect = page . locator ( '#github-pr-repo-select' )
946+ const componentPath = page . locator ( '#github-pr-component-path' )
947+
948+ await repoSelect . selectOption ( 'knightedcodemonkey/develop' )
949+ await componentPath . fill ( 'examples/develop/App.tsx' )
950+ await componentPath . blur ( )
951+
952+ await repoSelect . selectOption ( 'knightedcodemonkey/css' )
953+
954+ const contexts = await page . evaluate ( ( ) => {
955+ const storagePrefix = 'knighted:develop:github-pr-config:'
956+ const keys = Object . keys ( localStorage )
957+ . filter ( key => key . startsWith ( storagePrefix ) )
958+ . sort ( ( left , right ) => left . localeCompare ( right ) )
959+
960+ return keys . map ( key => {
961+ const raw = localStorage . getItem ( key )
962+ let parsed = null
963+
964+ try {
965+ parsed = raw ? JSON . parse ( raw ) : null
966+ } catch {
967+ parsed = null
968+ }
969+
970+ return { key, parsed }
971+ } )
972+ } )
973+
974+ expect ( contexts ) . toHaveLength ( 1 )
975+ expect ( contexts [ 0 ] ?. key ) . toBe (
976+ 'knighted:develop:github-pr-config:knightedcodemonkey/develop' ,
977+ )
978+ expect ( contexts [ 0 ] ?. parsed ?. componentFilePath ) . toBe ( 'examples/develop/App.tsx' )
979+ } )
980+
833981test ( 'Open PR drawer validates unsafe filepaths' , async ( { page } ) => {
834982 await waitForAppReady ( page , `${ appEntryPath } ?feature-ai=true` )
835983 await connectByotWithSingleRepo ( page )
0 commit comments