33 */
44import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
55
6- const { mockExecuteTool } = vi . hoisted ( ( ) => ( {
6+ const { mockExecuteTool, mockMaterializeSecrets } = vi . hoisted ( ( ) => ( {
77 mockExecuteTool : vi . fn ( ) . mockResolvedValue ( { success : true , output : { } } ) ,
8+ mockMaterializeSecrets : vi
9+ . fn ( )
10+ . mockResolvedValue ( { envVars : { API_KEY : 'test-value' } , catalogEntries : [ ] } ) ,
811} ) )
912
1013vi . mock ( '@/tools' , ( ) => ( { executeTool : mockExecuteTool } ) )
11- vi . mock ( '@/executor/utils/code-secret-references' , ( ) => ( {
12- extractCodeSecretNames : vi . fn ( ) . mockResolvedValue ( [ ] ) ,
14+ vi . mock ( '@/lib/mothership/tools/secret-mount-materializer.server' , ( ) => ( {
15+ materializeCopilotCodeSecrets : mockMaterializeSecrets ,
16+ CopilotCodeSecretAccessError : class extends Error { } ,
1317} ) )
1418vi . mock ( '@/executor/utils/resolved-secret-trace-registry' , ( ) => ( {
1519 ResolvedSecretTraceRegistry : class {
@@ -51,6 +55,7 @@ const BASE_CONTEXT: ToolExecutionContext = {
5155describe ( 'executeFunctionExecute session plumbing' , ( ) => {
5256 beforeEach ( ( ) => {
5357 mockExecuteTool . mockClear ( )
58+ mockMaterializeSecrets . mockClear ( )
5459 } )
5560
5661 it ( 'derives the session key from the chat, one per chat' , async ( ) => {
@@ -77,6 +82,57 @@ describe('executeFunctionExecute session plumbing', () => {
7782 expect ( params . sandboxSessionKey ) . toBeUndefined ( )
7883 } )
7984
85+ it . each ( [
86+ { language : 'python' , code : 'import json\nprint(json.dumps({"apiKey": "{{EXA_API_KEY}}"}))' } ,
87+ { language : 'python' , code : 'print("{{" + "EXA_API_KEY" + "}}")' } ,
88+ { language : 'javascript' , code : 'return JSON.stringify({ apiKey: "{{EXA_API_KEY}}" })' } ,
89+ { language : 'shell' , code : "printf '%s' '{{EXA_API_KEY}}'" } ,
90+ ] ) ( 'keeps authored $language templates literal without requesting secrets' , async ( params ) => {
91+ await executeFunctionExecute ( params , BASE_CONTEXT )
92+ expect ( mockMaterializeSecrets ) . not . toHaveBeenCalled ( )
93+ expect ( mockExecuteTool . mock . calls [ 0 ] [ 1 ] ) . toMatchObject ( {
94+ code : params . code ,
95+ envVars : { } ,
96+ secretScope : 'selected' ,
97+ mountedSecrets : [ ] ,
98+ } )
99+ } )
100+
101+ it ( 'mounts only explicitly named secrets within the caller policy' , async ( ) => {
102+ await executeFunctionExecute (
103+ {
104+ code : "return environmentVariables['API_KEY']" ,
105+ language : 'javascript' ,
106+ secrets : [ ' API_KEY ' , 'API_KEY' ] ,
107+ } ,
108+ {
109+ ...BASE_CONTEXT ,
110+ secretMountPolicy : { secretScope : 'selected' , mountedSecrets : [ 'API_KEY' ] } ,
111+ }
112+ )
113+ expect ( mockMaterializeSecrets ) . toHaveBeenCalledExactlyOnceWith ( {
114+ actorUserId : 'user-1' ,
115+ workspaceId : 'ws-1' ,
116+ requestedNames : [ 'API_KEY' ] ,
117+ } )
118+ expect ( mockExecuteTool . mock . calls [ 0 ] [ 1 ] ) . toMatchObject ( {
119+ envVars : { API_KEY : 'test-value' } ,
120+ mountedSecrets : [ 'API_KEY' ] ,
121+ } )
122+ expect ( mockExecuteTool . mock . calls [ 0 ] [ 1 ] ) . not . toHaveProperty ( 'secrets' )
123+ } )
124+
125+ it ( 'rejects explicit secrets outside the allowlist before materialization or execution' , async ( ) => {
126+ await expect (
127+ executeFunctionExecute (
128+ { code : 'return 1' , secrets : [ 'API_KEY' ] } ,
129+ { ...BASE_CONTEXT , secretMountPolicy : { secretScope : 'selected' , mountedSecrets : [ ] } }
130+ )
131+ ) . rejects . toThrow ( 'Secret access is not allowed for: API_KEY' )
132+ expect ( mockMaterializeSecrets ) . not . toHaveBeenCalled ( )
133+ expect ( mockExecuteTool ) . not . toHaveBeenCalled ( )
134+ } )
135+
80136 it ( 'converts second-denominated timeouts, including string values' , async ( ) => {
81137 // The catalog doc promises seconds; models also send the number as a string.
82138 // Without the tolerant parse, "90" reached the body schema's z.coerce and
0 commit comments