11import { describe , it , expect } from 'vitest' ;
2- import { createClaudeAdapter , CLAUDE_CAPS } from './claude' ;
2+ import { createClaudeAdapter , CLAUDE_CAPS , toClaudeMode , fromClaudeMode } from './claude' ;
33import type { EngineAdapter , SessionCtx } from './types' ;
44
55// Fixtures below are condensed from live `claude --print --input-format
@@ -128,6 +128,41 @@ describe('claude adapter: startup', () => {
128128 }
129129 } ) ;
130130
131+ it ( 'the list_models prefetch ack seeds the model button before the first turn' , ( ) => {
132+ // Bug: a new claude session showed the "model" placeholder until the first
133+ // message, because the real model only arrives with the first system/init.
134+ // The onStart list_models prefetch now seeds it from the default alias.
135+ const { lines, io } = makeIo ( ) ;
136+ const adapter = createClaudeAdapter ( ) ;
137+ adapter . onStart ( io , CTX ) ;
138+ adapter . translate ( {
139+ type : 'control_response' ,
140+ response : { subtype : 'success' , request_id : parse ( lines [ 0 ] ) . request_id , response : { mode : 'default' } }
141+ } ) ;
142+ const seeded = adapter . translate ( {
143+ type : 'control_response' ,
144+ response : { subtype : 'success' , request_id : parse ( lines [ 1 ] ) . request_id , response : { models : CATALOG } }
145+ } ) ;
146+ expect ( seeded ) . toEqual ( [
147+ {
148+ type : 'model_status' ,
149+ provider : 'anthropic' ,
150+ // The default/recommended alias's concrete resolvedModel + compact label.
151+ model : 'claude-opus-4-8[1m]' ,
152+ model_label : 'Opus 4.8 (1M)' ,
153+ reasoning_effort : 'medium' ,
154+ reasoning_efforts : [ 'low' , 'medium' , 'high' , 'xhigh' , 'max' ] ,
155+ context_window : 0 ,
156+ context_limit : 0
157+ }
158+ ] ) ;
159+ // The first init reports the same resolved model → no duplicate model_status
160+ // (init still emits the full startup sequence regardless).
161+ const init = adapter . translate ( initFrame ( ) ) ;
162+ expect ( init ) . toHaveLength ( 5 ) ;
163+ expect ( init [ 0 ] ) . toMatchObject ( { type : 'startup' , model : 'claude-opus-4-8[1m]' } ) ;
164+ } ) ;
165+
131166 it ( 'the bootstrap ack doubles as the readiness signal (init only arrives with the first turn)' , ( ) => {
132167 const { lines, io } = makeIo ( ) ;
133168 const adapter = createClaudeAdapter ( ) ;
@@ -448,10 +483,11 @@ describe('claude adapter: turns', () => {
448483 expect ( JSON . parse ( String ( denied [ 0 ] . output ) ) ) . toEqual ( { path : '/etc/x' , error : 'probe denies this' } ) ;
449484 } ) ;
450485
451- it ( 'drops Task-subagent frames (parent_tool_use_id ) and synthetic user notices' , ( ) => {
486+ it ( 'drops Task-subagent chatter (text/reasoning ) and synthetic user notices' , ( ) => {
452487 const { lines } = makeIo ( ) ;
453488 const adapter = createClaudeAdapter ( ) ;
454489 boot ( adapter , lines ) ;
490+ // Subagent text stream is dropped (we don't render subagent chatter).
455491 expect (
456492 adapter . translate (
457493 { ...streamEvent ( { type : 'content_block_delta' , index : 0 , delta : { type : 'text_delta' , text : 'inner' } } ) , parent_tool_use_id : 'toolu_task' }
@@ -466,6 +502,56 @@ describe('claude adapter: turns', () => {
466502 } )
467503 ) . toEqual ( [ ] ) ;
468504 } ) ;
505+
506+ it ( 'surfaces a Task-subagent tool so its card fills and completes (bug: stuck running)' , ( ) => {
507+ // A subagent's inner tool frames carry a non-null parent_tool_use_id. The
508+ // old guard dropped EVERY such frame, so the card never filled its input and
509+ // never completed — empty + spinning forever. Now the tool activity is
510+ // surfaced (its assistant text / reasoning / user echoes stay dropped).
511+ const { lines } = makeIo ( ) ;
512+ const adapter = createClaudeAdapter ( ) ;
513+ boot ( adapter , lines ) ;
514+ // The subagent's streamed tool_use start (non-authoritative, empty input).
515+ const start = adapter . translate ( {
516+ ...streamEvent ( {
517+ type : 'content_block_start' ,
518+ index : 0 ,
519+ content_block : { type : 'tool_use' , id : 'toolu_sub' , name : 'Read' , input : { } }
520+ } ) ,
521+ parent_tool_use_id : 'toolu_task'
522+ } ) ;
523+ expect ( start ) . toEqual ( [
524+ { type : 'tool_start' , call_id : 'toolu_sub' , name : 'read' } ,
525+ { type : 'tool_update' , call_id : 'toolu_sub' , output : JSON . stringify ( { path : '' } ) }
526+ ] ) ;
527+ // The authoritative assistant tool_use frame fills the input.
528+ const filled = adapter . translate ( {
529+ type : 'assistant' ,
530+ message : {
531+ id : 'm' ,
532+ model : 'x' ,
533+ content : [ { type : 'tool_use' , id : 'toolu_sub' , name : 'Read' , input : { file_path : '/proj/x.ts' } } ]
534+ } ,
535+ session_id : SID ,
536+ parent_tool_use_id : 'toolu_task'
537+ } ) ;
538+ expect ( filled ) . toEqual ( [
539+ { type : 'tool_update' , call_id : 'toolu_sub' , output : JSON . stringify ( { path : '/proj/x.ts' } ) }
540+ ] ) ;
541+ // The subagent's tool_result completes the card.
542+ const done = adapter . translate ( {
543+ type : 'user' ,
544+ message : {
545+ role : 'user' ,
546+ content : [ { type : 'tool_result' , tool_use_id : 'toolu_sub' , content : 'file body' , is_error : false } ]
547+ } ,
548+ session_id : SID ,
549+ parent_tool_use_id : 'toolu_task'
550+ } ) ;
551+ expect ( done ) . toEqual ( [
552+ { type : 'tool_output' , call_id : 'toolu_sub' , name : 'read' , output : JSON . stringify ( { path : '/proj/x.ts' } ) , is_error : false }
553+ ] ) ;
554+ } ) ;
469555} ) ;
470556
471557describe ( 'claude adapter: approvals' , ( ) => {
@@ -636,6 +722,41 @@ describe('claude adapter: interrupt / modes / results', () => {
636722 ) . toEqual ( [ { type : 'approval_mode' , mode : 'full-auto' } ] ) ;
637723 } ) ;
638724
725+ it ( 'maps all five approval modes to claude permission modes and back' , ( ) => {
726+ expect ( toClaudeMode ( 'read-only' ) ) . toBe ( 'default' ) ;
727+ expect ( toClaudeMode ( 'plan' ) ) . toBe ( 'plan' ) ;
728+ expect ( toClaudeMode ( 'auto' ) ) . toBe ( 'auto' ) ;
729+ expect ( toClaudeMode ( 'auto-edit' ) ) . toBe ( 'acceptEdits' ) ;
730+ expect ( toClaudeMode ( 'full-auto' ) ) . toBe ( 'bypassPermissions' ) ;
731+ for ( const m of [ 'read-only' , 'plan' , 'auto' , 'auto-edit' , 'full-auto' ] as const )
732+ expect ( fromClaudeMode ( toClaudeMode ( m ) ) ) . toBe ( m ) ;
733+ // Aliases the CLI may report.
734+ expect ( fromClaudeMode ( 'manual' ) ) . toBe ( 'read-only' ) ;
735+ expect ( fromClaudeMode ( 'dontAsk' ) ) . toBe ( 'full-auto' ) ;
736+ // plan no longer collapses to read-only (the old bug).
737+ expect ( fromClaudeMode ( 'plan' ) ) . toBe ( 'plan' ) ;
738+ } ) ;
739+
740+ it ( 'plan and auto switch live via set_permission_mode + status ack' , ( ) => {
741+ for ( const [ engineMode , claudeMode ] of [
742+ [ 'plan' , 'plan' ] ,
743+ [ 'auto' , 'auto' ]
744+ ] as const ) {
745+ const { lines } = makeIo ( ) ;
746+ const adapter = createClaudeAdapter ( ) ;
747+ boot ( adapter , lines ) ;
748+ const frames = adapter . encodeOp ( { op : 'set_approval_mode' , mode : engineMode } ) ;
749+ expect ( parse ( frames ! [ 0 ] ) . request ) . toEqual ( { subtype : 'set_permission_mode' , mode : claudeMode } ) ;
750+ expect (
751+ adapter . translate ( { type : 'system' , subtype : 'status' , status : null , permissionMode : claudeMode , session_id : SID } )
752+ ) . toEqual ( [ { type : 'approval_mode' , mode : engineMode } ] ) ;
753+ }
754+ } ) ;
755+
756+ it ( 'extendedApprovalModes cap gates the claude-only plan/auto picker options' , ( ) => {
757+ expect ( CLAUDE_CAPS . extendedApprovalModes ) . toBe ( true ) ;
758+ } ) ;
759+
639760 it ( 'surfaces failed results as error + ready, with login guidance on auth failures' , ( ) => {
640761 const { lines } = makeIo ( ) ;
641762 const adapter = createClaudeAdapter ( ) ;
0 commit comments