@@ -306,6 +306,148 @@ argv = ["claude", "--resume", "session id"]
306306 ) ;
307307}
308308
309+ #[ test]
310+ fn presentation_metadata_lowers_from_kdl_toml_and_json_without_changing_identity ( ) {
311+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
312+ write (
313+ tmp. path ( ) ,
314+ "agents/h/kdl/agent.kdl" ,
315+ r#"agent "kdl" {
316+ host "h"
317+ name "Display label"
318+ description "Enduring responsibility"
319+ command "true"
320+ }"# ,
321+ ) ;
322+ write (
323+ tmp. path ( ) ,
324+ "agents/h/toml/agent.toml" ,
325+ r#"identity = "toml"
326+ host = "h"
327+ name = "Display label"
328+ description = "Enduring responsibility"
329+ command = "true"
330+ "# ,
331+ ) ;
332+ write (
333+ tmp. path ( ) ,
334+ "agents/h/json/agent.json" ,
335+ r#"{"identity":"json","host":"h","name":"Display label","description":"Enduring responsibility","command":"true"}"# ,
336+ ) ;
337+
338+ let found = discover ( tmp. path ( ) ) ;
339+ assert ! ( found. errors. is_empty( ) , "{:?}" , found. errors) ;
340+ for identity in [ "kdl" , "toml" , "json" ] {
341+ let spec = find ( & found. specs , identity) ;
342+ assert_eq ! ( spec. identity, identity) ;
343+ assert_eq ! ( spec. name. as_deref( ) , Some ( "Display label" ) ) ;
344+ assert_eq ! ( spec. description. as_deref( ) , Some ( "Enduring responsibility" ) ) ;
345+ }
346+ }
347+
348+ #[ test]
349+ fn malformed_or_duplicate_kdl_presentation_is_rejected ( ) {
350+ for ( case, body) in [
351+ ( "duplicate" , "name \" one\" ; name \" two\" " ) ,
352+ ( "wrong-type" , "description 42" ) ,
353+ ( "children" , "description { nested \" no\" }" ) ,
354+ ] {
355+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
356+ write (
357+ tmp. path ( ) ,
358+ & format ! ( "agents/h/{case}/agent.kdl" ) ,
359+ & format ! ( "agent {case:?} {{ host \" h\" ; {body}; command \" true\" }}" ) ,
360+ ) ;
361+ let found = discover ( tmp. path ( ) ) ;
362+ assert ! ( found. specs. is_empty( ) , "{case}: {:?}" , found. specs) ;
363+ assert_eq ! ( found. errors. len( ) , 1 , "{case}: {:?}" , found. errors) ;
364+ assert ! (
365+ found. errors[ 0 ] . message. contains( "must contain" )
366+ || found. errors[ 0 ] . message. contains( "more than once" ) ,
367+ "{case}: {}" ,
368+ found. errors[ 0 ] . message
369+ ) ;
370+ }
371+ }
372+
373+ #[ test]
374+ fn presentation_bounds_count_unicode_scalars_and_reject_noncanonical_values ( ) {
375+ use agent_spec:: spec:: {
376+ AGENT_DESCRIPTION_MAX_CHARS , AGENT_NAME_MAX_CHARS , validate_presentation,
377+ } ;
378+
379+ let name_at_limit = "é" . repeat ( AGENT_NAME_MAX_CHARS ) ;
380+ let description_at_limit = "界" . repeat ( AGENT_DESCRIPTION_MAX_CHARS ) ;
381+ assert ! ( validate_presentation( "name" , Some ( & name_at_limit) , AGENT_NAME_MAX_CHARS ) . is_ok( ) ) ;
382+ assert ! (
383+ validate_presentation(
384+ "description" ,
385+ Some ( & description_at_limit) ,
386+ AGENT_DESCRIPTION_MAX_CHARS ,
387+ )
388+ . is_ok( )
389+ ) ;
390+ assert ! (
391+ validate_presentation(
392+ "name" ,
393+ Some ( & format!( "{name_at_limit}x" ) ) ,
394+ AGENT_NAME_MAX_CHARS ,
395+ )
396+ . is_err( )
397+ ) ;
398+ assert ! (
399+ validate_presentation(
400+ "description" ,
401+ Some ( & format!( "{description_at_limit}x" ) ) ,
402+ AGENT_DESCRIPTION_MAX_CHARS ,
403+ )
404+ . is_err( )
405+ ) ;
406+ for ( field, max_chars) in [
407+ ( "name" , AGENT_NAME_MAX_CHARS ) ,
408+ ( "description" , AGENT_DESCRIPTION_MAX_CHARS ) ,
409+ ] {
410+ assert ! ( validate_presentation( field, Some ( r"slash/name\path" ) , max_chars) . is_ok( ) ) ;
411+ for invalid in [
412+ "" ,
413+ " leading" ,
414+ "trailing " ,
415+ "two\n lines" ,
416+ "control\u{7f} " ,
417+ "line\u{2028} separator" ,
418+ "paragraph\u{2029} separator" ,
419+ ] {
420+ assert ! (
421+ validate_presentation( field, Some ( invalid) , max_chars) . is_err( ) ,
422+ "accepted {field} {invalid:?}"
423+ ) ;
424+ }
425+ }
426+ }
427+
428+ #[ test]
429+ fn presentation_parser_rejects_unicode_line_and_paragraph_separators ( ) {
430+ for field in [ "name" , "description" ] {
431+ for separator in [ '\u{2028}' , '\u{2029}' ] {
432+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
433+ write (
434+ tmp. path ( ) ,
435+ "agents/h/worker/agent.kdl" ,
436+ & format ! (
437+ "agent \" worker\" {{\n host \" h\" \n type \" service\" \n {field} \" left{separator}right\" \n pty \" agent\" {{ command \" true\" }}\n }}\n "
438+ ) ,
439+ ) ;
440+ let found = discover ( tmp. path ( ) ) ;
441+ assert ! (
442+ found. specs. is_empty( ) ,
443+ "accepted {field} U+{:04X}" ,
444+ separator as u32
445+ ) ;
446+ assert_eq ! ( found. errors. len( ) , 1 , "{field}: {:?}" , found. errors) ;
447+ }
448+ }
449+ }
450+
309451#[ test]
310452fn named_resource_bindings_are_typed_uri_identities_and_order_independent ( ) {
311453 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments