@@ -394,6 +394,100 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {
394394 } ) ,
395395 ) ;
396396
397+ for ( const verbose of [ false , true ] ) {
398+ it . effect ( `unwraps a JSON title in ${ verbose ? "verbose" : "normal" } Claude output` , ( ) => {
399+ const result = {
400+ type : "result" ,
401+ structured_output : { title : '{"title": "Refresh ev-stg APP ASG instances"}' } ,
402+ } ;
403+ return withFakeClaudeEnv (
404+ { output : JSON . stringify ( verbose ? [ result ] : result ) } ,
405+ ( textGeneration ) =>
406+ Effect . gen ( function * ( ) {
407+ const generated = yield * textGeneration . generateThreadTitle ( {
408+ cwd : process . cwd ( ) ,
409+ message : "Refresh ev-stg APP ASG instances" ,
410+ modelSelection : {
411+ instanceId : ProviderInstanceId . make ( "claudeAgent" ) ,
412+ model : SYNTHETIC_CLAUDE_STANDARD_MODEL ,
413+ } ,
414+ } ) ;
415+
416+ expect ( generated . title ) . toBe ( "Refresh ev-stg APP ASG instances" ) ;
417+ } ) ,
418+ ) ;
419+ } ) ;
420+ }
421+
422+ for ( const previousTitle of [ undefined , "Old thread title" ] ) {
423+ it . effect (
424+ `reads the result from verbose Claude output when ${ previousTitle ? "regenerating" : "generating" } a title` ,
425+ ( ) =>
426+ withFakeClaudeEnv (
427+ {
428+ output : JSON . stringify ( [
429+ { type : "system" , subtype : "init" } ,
430+ { type : "assistant" , message : { content : [ ] } } ,
431+ { type : "user" , message : { content : [ ] } } ,
432+ { type : "rate_limit_event" } ,
433+ {
434+ type : "result" ,
435+ subtype : "success" ,
436+ result : '{"title":"Refresh ev-stg APP ASG Instances"}' ,
437+ structured_output : { title : "Refresh ev-stg APP ASG Instances" } ,
438+ } ,
439+ ] ) ,
440+ } ,
441+ ( textGeneration ) =>
442+ Effect . gen ( function * ( ) {
443+ const generated = yield * textGeneration . generateThreadTitle ( {
444+ cwd : process . cwd ( ) ,
445+ message : "Refresh ev-stg APP ASG instances" ,
446+ previousTitle,
447+ modelSelection : {
448+ instanceId : ProviderInstanceId . make ( "claudeAgent" ) ,
449+ model : SYNTHETIC_CLAUDE_STANDARD_MODEL ,
450+ } ,
451+ } ) ;
452+
453+ expect ( generated . title ) . toBe ( "Refresh ev-stg APP ASG Instances" ) ;
454+ } ) ,
455+ ) ,
456+ ) ;
457+ }
458+
459+ for ( const [ name , output ] of [
460+ [ "empty message array" , [ ] ] ,
461+ [ "missing result" , [ { type : "assistant" , structured_output : { title : "Not a result" } } ] ] ,
462+ [ "invalid title" , [ { type : "result" , structured_output : { title : 42 } } ] ] ,
463+ [
464+ "final result without structured output" ,
465+ [
466+ { type : "result" , structured_output : { title : "Earlier result" } } ,
467+ { type : "result" , subtype : "error_max_structured_output_retries" } ,
468+ ] ,
469+ ] ,
470+ ] as const ) {
471+ it . effect ( `rejects verbose Claude output with ${ name } ` , ( ) =>
472+ withFakeClaudeEnv ( { output : JSON . stringify ( output ) } , ( textGeneration ) =>
473+ Effect . gen ( function * ( ) {
474+ const error = yield * Effect . flip (
475+ textGeneration . generateThreadTitle ( {
476+ cwd : process . cwd ( ) ,
477+ message : "Name this thread" ,
478+ modelSelection : {
479+ instanceId : ProviderInstanceId . make ( "claudeAgent" ) ,
480+ model : SYNTHETIC_CLAUDE_STANDARD_MODEL ,
481+ } ,
482+ } ) ,
483+ ) ;
484+
485+ expect ( error . _tag ) . toBe ( "TextGenerationError" ) ;
486+ } ) ,
487+ ) ,
488+ ) ;
489+ }
490+
397491 it . effect ( "falls back when Claude thread title normalization becomes whitespace-only" , ( ) =>
398492 withFakeClaudeEnv (
399493 {
0 commit comments