@@ -12,20 +12,45 @@ const KNOWN_ATTACKS: Record<string, { outcome: "success" | "failure" }> = {
1212 "atk-success" : { outcome : "success" } ,
1313 "atk-failure" : { outcome : "failure" } ,
1414} ;
15+ const ROUTING_TARGET_HASH = "routing-target-full-hash" ;
16+ const ROUTING_TARGET = {
17+ target_registry_name : "routing-target" ,
18+ identifier : {
19+ class_name : "OpenAIChatTarget" ,
20+ hash : ROUTING_TARGET_HASH ,
21+ endpoint : null ,
22+ model_name : "gpt-4o" ,
23+ } ,
24+ capabilities : {
25+ supports_multi_turn : true ,
26+ supports_json_schema : false ,
27+ supports_json_output : false ,
28+ supports_system_prompt : false ,
29+ supported_input_modalities : [ "text" ] ,
30+ supported_output_modalities : [ "text" ] ,
31+ } ,
32+ target_specific_params : null ,
33+ inner_targets : null ,
34+ } ;
1535
1636/** Build an attack summary for the single-attack (getAttack) endpoint. */
1737function makeAttackSummary ( attackResultId : string , outcome : "success" | "failure" ) {
1838 return {
1939 attack_result_id : attackResultId ,
2040 conversation_id : `conv-${ attackResultId } ` ,
2141 attack_type : "SingleTurnAttack" ,
22- target : { target_type : "OpenAIChatTarget" , model_name : "gpt-4o" } ,
42+ target : {
43+ target_type : "OpenAIChatTarget" ,
44+ target_registry_name : ROUTING_TARGET . target_registry_name ,
45+ model_name : "gpt-4o" ,
46+ identifier_hash : ROUTING_TARGET_HASH ,
47+ } ,
2348 converters : [ ] ,
2449 outcome,
2550 last_message_preview : null ,
2651 message_count : 1 ,
2752 related_conversation_ids : [ ] ,
28- labels : { operator : "alice" } ,
53+ labels : { } ,
2954 created_at : new Date ( ) . toISOString ( ) ,
3055 updated_at : new Date ( ) . toISOString ( ) ,
3156 } ;
@@ -78,12 +103,18 @@ const MARKDOWN_PREFERENCE_STORAGE_KEY = "pyrit.chatMarkdownMode";
78103
79104/** Register every API mock the routing tests rely on. */
80105async function mockRoutingAPIs ( page : Page ) {
81- // No active target configured – the chat ribbon shows "No target selected".
82106 await page . route ( / \/ a p i \/ t a r g e t s / , async ( route ) => {
107+ const pathname = new URL ( route . request ( ) . url ( ) ) . pathname ;
108+ const body = pathname . endsWith ( `/${ ROUTING_TARGET . target_registry_name } ` )
109+ ? ROUTING_TARGET
110+ : {
111+ items : [ ROUTING_TARGET ] ,
112+ pagination : { limit : 200 , has_more : false , next_cursor : null , prev_cursor : null } ,
113+ } ;
83114 await route . fulfill ( {
84115 status : 200 ,
85116 contentType : "application/json" ,
86- body : JSON . stringify ( { items : [ ] } ) ,
117+ body : JSON . stringify ( body ) ,
87118 } ) ;
88119 } ) ;
89120
@@ -214,13 +245,21 @@ test.describe("URL-driven routing", () => {
214245 await expect ( page . getByTestId ( "attack-row-atk-failure" ) ) . not . toBeVisible ( ) ;
215246 } ) ;
216247
217- test ( "deep-links into an attack and hydrates its conversation " , async ( { page } ) => {
248+ test ( "deep-links into an attack and restores its target after reload " , async ( { page } ) => {
218249 await page . goto ( "/attacks/atk-1" ) ;
219250
220251 // The router keeps the deep link, and the attack named by the URL – not
221252 // some default/empty conversation – drives the chat window.
222253 await expect ( page ) . toHaveURL ( / \/ a t t a c k s \/ a t k - 1 $ / ) ;
223254 await expect ( page . getByText ( "Loaded from atk-1" ) ) . toBeVisible ( { timeout : 10_000 } ) ;
255+ await expect ( page . getByTestId ( "chat-input" ) ) . toBeEnabled ( ) ;
256+ await expect ( page . getByTestId ( "no-target-banner" ) ) . not . toBeVisible ( ) ;
257+
258+ await page . reload ( ) ;
259+
260+ await expect ( page . getByText ( "Loaded from atk-1" ) ) . toBeVisible ( { timeout : 10_000 } ) ;
261+ await expect ( page . getByTestId ( "chat-input" ) ) . toBeEnabled ( ) ;
262+ await expect ( page . getByTestId ( "no-target-banner" ) ) . not . toBeVisible ( ) ;
224263 } ) ;
225264
226265 test ( "shows the not-found screen for an unknown attack id" , async ( { page } ) => {
0 commit comments