@@ -445,6 +445,191 @@ test("ensureClaudeSettings skips empty host effort level", async () => {
445445 }
446446} ) ;
447447
448+ test ( "ensureClaudeSettings inherits host provider env and model settings" , async ( ) => {
449+ const sandboxCreate = await loadFreshEsm < SandboxCreateModule > ( "lib/sandbox/commands/create.js" ) ;
450+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-settings-provider-" ) ) ;
451+ const hostHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-host-provider-" ) ) ;
452+
453+ try {
454+ fs . mkdirSync ( path . join ( hostHome , ".claude" ) , { recursive : true } ) ;
455+ fs . writeFileSync (
456+ path . join ( hostHome , ".claude" , "settings.json" ) ,
457+ JSON . stringify ( {
458+ env : {
459+ ANTHROPIC_BASE_URL : "https://api.deepseek.example/anthropic" ,
460+ ANTHROPIC_AUTH_TOKEN : "test-token-redacted" ,
461+ ANTHROPIC_MODEL : "deepseek-v4-pro[1m]" ,
462+ ANTHROPIC_OPUS_MODEL : "deepseek-v4-pro[1m]" ,
463+ ANTHROPIC_SONNET_MODEL : "deepseek-v4-flash" ,
464+ ANTHROPIC_HAIKU_MODEL : "deepseek-v4-flash" ,
465+ ANTHROPIC_SMALL_FAST_MODEL : "deepseek-v4-flash" ,
466+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC : "1"
467+ } ,
468+ model : "deepseek-v4-pro[1m]" ,
469+ fallbackModel : "deepseek-v4-flash" ,
470+ availableModels : [ "deepseek-v4-pro[1m]" , "deepseek-v4-flash" ] ,
471+ modelOverrides : {
472+ opus : "deepseek-v4-pro[1m]" ,
473+ sonnet : "deepseek-v4-flash" ,
474+ haiku : "deepseek-v4-flash" ,
475+ subagent : "deepseek-v4-flash"
476+ } ,
477+ enforceAvailableModels : true ,
478+ advisorModel : "deepseek-v4-flash" ,
479+ apiKeyHelper : "printf test-token-redacted" ,
480+ theme : "dark"
481+ } ) ,
482+ "utf8"
483+ ) ;
484+
485+ sandboxCreate . ensureClaudeSettings ( tmpDir , hostHome ) ;
486+ const data = JSON . parse ( fs . readFileSync ( path . join ( tmpDir , "settings.json" ) , "utf8" ) ) ;
487+ assert . deepEqual ( data . env , {
488+ ANTHROPIC_BASE_URL : "https://api.deepseek.example/anthropic" ,
489+ ANTHROPIC_AUTH_TOKEN : "test-token-redacted" ,
490+ ANTHROPIC_MODEL : "deepseek-v4-pro[1m]" ,
491+ ANTHROPIC_OPUS_MODEL : "deepseek-v4-pro[1m]" ,
492+ ANTHROPIC_SONNET_MODEL : "deepseek-v4-flash" ,
493+ ANTHROPIC_HAIKU_MODEL : "deepseek-v4-flash" ,
494+ ANTHROPIC_SMALL_FAST_MODEL : "deepseek-v4-flash" ,
495+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC : "1"
496+ } ) ;
497+ assert . equal ( data . model , "deepseek-v4-pro[1m]" ) ;
498+ assert . equal ( data . fallbackModel , "deepseek-v4-flash" ) ;
499+ assert . deepEqual ( data . availableModels , [ "deepseek-v4-pro[1m]" , "deepseek-v4-flash" ] ) ;
500+ assert . deepEqual ( data . modelOverrides , {
501+ opus : "deepseek-v4-pro[1m]" ,
502+ sonnet : "deepseek-v4-flash" ,
503+ haiku : "deepseek-v4-flash" ,
504+ subagent : "deepseek-v4-flash"
505+ } ) ;
506+ assert . equal ( data . enforceAvailableModels , true ) ;
507+ assert . equal ( data . advisorModel , "deepseek-v4-flash" ) ;
508+ assert . equal ( data . apiKeyHelper , "printf test-token-redacted" ) ;
509+ assert . equal ( Object . hasOwn ( data , "theme" ) , false ) ;
510+ assert . equal ( data . skipDangerousModePermissionPrompt , true ) ;
511+ } finally {
512+ fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
513+ fs . rmSync ( hostHome , { recursive : true , force : true } ) ;
514+ }
515+ } ) ;
516+
517+ test ( "ensureClaudeSettings preserves sandbox provider fields and fills missing env" , async ( ) => {
518+ const sandboxCreate = await loadFreshEsm < SandboxCreateModule > ( "lib/sandbox/commands/create.js" ) ;
519+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-settings-provider-keep-" ) ) ;
520+ const hostHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-host-provider-keep-" ) ) ;
521+
522+ try {
523+ fs . mkdirSync ( path . join ( hostHome , ".claude" ) , { recursive : true } ) ;
524+ fs . writeFileSync (
525+ path . join ( hostHome , ".claude" , "settings.json" ) ,
526+ JSON . stringify ( {
527+ env : {
528+ ANTHROPIC_MODEL : "host-model" ,
529+ ANTHROPIC_SMALL_FAST_MODEL : "host-fast-model"
530+ } ,
531+ model : "host-top-level-model" ,
532+ fallbackModel : "host-fallback-model"
533+ } ) ,
534+ "utf8"
535+ ) ;
536+ fs . writeFileSync (
537+ path . join ( tmpDir , "settings.json" ) ,
538+ JSON . stringify ( {
539+ skipDangerousModePermissionPrompt : true ,
540+ env : {
541+ ANTHROPIC_MODEL : "sandbox-model"
542+ } ,
543+ model : "sandbox-top-level-model"
544+ } ) ,
545+ "utf8"
546+ ) ;
547+
548+ sandboxCreate . ensureClaudeSettings ( tmpDir , hostHome ) ;
549+ const data = JSON . parse ( fs . readFileSync ( path . join ( tmpDir , "settings.json" ) , "utf8" ) ) ;
550+ assert . deepEqual ( data . env , {
551+ ANTHROPIC_MODEL : "sandbox-model" ,
552+ ANTHROPIC_SMALL_FAST_MODEL : "host-fast-model"
553+ } ) ;
554+ assert . equal ( data . model , "sandbox-top-level-model" ) ;
555+ assert . equal ( data . fallbackModel , "host-fallback-model" ) ;
556+ } finally {
557+ fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
558+ fs . rmSync ( hostHome , { recursive : true , force : true } ) ;
559+ }
560+ } ) ;
561+
562+ test ( "ensureClaudeSettings skips invalid host env values and non-object sandbox env" , async ( ) => {
563+ const sandboxCreate = await loadFreshEsm < SandboxCreateModule > ( "lib/sandbox/commands/create.js" ) ;
564+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-settings-provider-invalid-" ) ) ;
565+ const hostHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-host-provider-invalid-" ) ) ;
566+
567+ try {
568+ fs . mkdirSync ( path . join ( hostHome , ".claude" ) , { recursive : true } ) ;
569+ fs . writeFileSync (
570+ path . join ( hostHome , ".claude" , "settings.json" ) ,
571+ JSON . stringify ( {
572+ env : {
573+ ANTHROPIC_BASE_URL : "https://api.example.test" ,
574+ ANTHROPIC_AUTH_TOKEN : "" ,
575+ ANTHROPIC_MODEL : [ "not" , "a" , "string" ] ,
576+ ANTHROPIC_SMALL_FAST_MODEL : false
577+ }
578+ } ) ,
579+ "utf8"
580+ ) ;
581+ fs . writeFileSync (
582+ path . join ( tmpDir , "settings.json" ) ,
583+ JSON . stringify ( {
584+ skipDangerousModePermissionPrompt : true ,
585+ env : "keep-local-env-value"
586+ } ) ,
587+ "utf8"
588+ ) ;
589+
590+ sandboxCreate . ensureClaudeSettings ( tmpDir , hostHome ) ;
591+ const data = JSON . parse ( fs . readFileSync ( path . join ( tmpDir , "settings.json" ) , "utf8" ) ) ;
592+ assert . equal ( data . env , "keep-local-env-value" ) ;
593+ } finally {
594+ fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
595+ fs . rmSync ( hostHome , { recursive : true , force : true } ) ;
596+ }
597+ } ) ;
598+
599+ test ( "ensureClaudeSettings skips non-object host env without defaulting effort env" , async ( ) => {
600+ const sandboxCreate = await loadFreshEsm < SandboxCreateModule > ( "lib/sandbox/commands/create.js" ) ;
601+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-settings-host-env-invalid-" ) ) ;
602+ const hostHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-host-env-invalid-" ) ) ;
603+
604+ try {
605+ fs . mkdirSync ( path . join ( hostHome , ".claude" ) , { recursive : true } ) ;
606+ fs . writeFileSync ( path . join ( hostHome , ".claude" , "settings.json" ) , JSON . stringify ( { env : "invalid" } ) , "utf8" ) ;
607+ sandboxCreate . ensureClaudeSettings ( tmpDir , hostHome ) ;
608+ const data = JSON . parse ( fs . readFileSync ( path . join ( tmpDir , "settings.json" ) , "utf8" ) ) ;
609+ assert . equal ( Object . hasOwn ( data , "env" ) , false ) ;
610+ } finally {
611+ fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
612+ fs . rmSync ( hostHome , { recursive : true , force : true } ) ;
613+ }
614+ } ) ;
615+
616+ test ( "ensureClaudeSettings skips malformed host settings" , async ( ) => {
617+ const sandboxCreate = await loadFreshEsm < SandboxCreateModule > ( "lib/sandbox/commands/create.js" ) ;
618+ const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-settings-malformed-host-" ) ) ;
619+ const hostHome = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-claude-host-malformed-settings-" ) ) ;
620+
621+ try {
622+ fs . mkdirSync ( path . join ( hostHome , ".claude" ) , { recursive : true } ) ;
623+ fs . writeFileSync ( path . join ( hostHome , ".claude" , "settings.json" ) , "{not-json:test-token-redacted" , "utf8" ) ;
624+ sandboxCreate . ensureClaudeSettings ( tmpDir , hostHome ) ;
625+ const data = JSON . parse ( fs . readFileSync ( path . join ( tmpDir , "settings.json" ) , "utf8" ) ) ;
626+ assert . deepEqual ( data , { skipDangerousModePermissionPrompt : true } ) ;
627+ } finally {
628+ fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
629+ fs . rmSync ( hostHome , { recursive : true , force : true } ) ;
630+ }
631+ } ) ;
632+
448633test ( "ensureCodexModelInheritance creates config with host model fields" , async ( ) => {
449634 const sandboxCreate = await loadFreshEsm < SandboxCreateModule > ( "lib/sandbox/commands/create.js" ) ;
450635 const tmpDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-codex-model-" ) ) ;
0 commit comments