@@ -27,7 +27,7 @@ import {
2727 CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE ,
2828 createCodexDynamicToolBridge ,
2929} from "./dynamic-tools.js" ;
30- import type { JsonValue } from "./protocol.js" ;
30+ import type { CodexDynamicToolFunctionSpec , CodexDynamicToolSpec , JsonValue } from "./protocol.js" ;
3131
3232function createTool ( overrides : Partial < AnyAgentTool > ) : AnyAgentTool {
3333 return {
@@ -115,6 +115,20 @@ function expectDynamicSpec(
115115 }
116116}
117117
118+ function flattenSpecsWithNamespace (
119+ specs : readonly CodexDynamicToolSpec [ ] ,
120+ ) : Array < CodexDynamicToolFunctionSpec & { namespace ?: string } > {
121+ return specs . flatMap ( ( spec ) =>
122+ spec . type === "namespace"
123+ ? spec . tools . map ( ( tool ) => ( { ...tool , namespace : spec . name } ) )
124+ : [ spec ] ,
125+ ) ;
126+ }
127+
128+ function specNames ( specs : readonly CodexDynamicToolSpec [ ] ) : string [ ] {
129+ return flattenSpecsWithNamespace ( specs ) . map ( ( tool ) => tool . name ) ;
130+ }
131+
118132function expectNoNamespace ( spec : unknown ) {
119133 const record = requireRecord ( spec , "tool spec" ) ;
120134 expect ( record ) . not . toHaveProperty ( "namespace" ) ;
@@ -176,11 +190,12 @@ describe("createCodexDynamicToolBridge", () => {
176190 signal : new AbortController ( ) . signal ,
177191 } ) ;
178192
179- const webSearch = bridge . specs . find ( ( tool ) => tool . name === "web_search" ) ;
180- const message = bridge . specs . find ( ( tool ) => tool . name === "message" ) ;
181- const heartbeat = bridge . specs . find ( ( tool ) => tool . name === HEARTBEAT_RESPONSE_TOOL_NAME ) ;
182- const sessionsSpawn = bridge . specs . find ( ( tool ) => tool . name === "sessions_spawn" ) ;
183- const sessionsYield = bridge . specs . find ( ( tool ) => tool . name === "sessions_yield" ) ;
193+ const specs = flattenSpecsWithNamespace ( bridge . specs ) ;
194+ const webSearch = specs . find ( ( tool ) => tool . name === "web_search" ) ;
195+ const message = specs . find ( ( tool ) => tool . name === "message" ) ;
196+ const heartbeat = specs . find ( ( tool ) => tool . name === HEARTBEAT_RESPONSE_TOOL_NAME ) ;
197+ const sessionsSpawn = specs . find ( ( tool ) => tool . name === "sessions_spawn" ) ;
198+ const sessionsYield = specs . find ( ( tool ) => tool . name === "sessions_yield" ) ;
184199
185200 expectDynamicSpec ( webSearch , {
186201 name : "web_search" ,
@@ -212,14 +227,21 @@ describe("createCodexDynamicToolBridge", () => {
212227 directToolNames : [ "message" ] ,
213228 } ) ;
214229
230+ const specs = flattenSpecsWithNamespace ( bridge . specs ) ;
215231 expect ( bridge . specs ) . toHaveLength ( 2 ) ;
216- expectDynamicSpec ( bridge . specs [ 0 ] , { name : "message" } ) ;
217- expectDynamicSpec ( bridge . specs [ 1 ] , {
218- name : "web_search" ,
219- namespace : CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE ,
220- deferLoading : true ,
221- } ) ;
222- expectNoNamespace ( bridge . specs [ 0 ] ) ;
232+ expectDynamicSpec (
233+ specs . find ( ( tool ) => tool . name === "message" ) ,
234+ { name : "message" } ,
235+ ) ;
236+ expectDynamicSpec (
237+ specs . find ( ( tool ) => tool . name === "web_search" ) ,
238+ {
239+ name : "web_search" ,
240+ namespace : CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE ,
241+ deferLoading : true ,
242+ } ,
243+ ) ;
244+ expectNoNamespace ( specs . find ( ( tool ) => tool . name === "message" ) ) ;
223245 } ) ;
224246
225247 it ( "can register a durable tool schema while denying execution for the current turn" , async ( ) => {
@@ -236,11 +258,8 @@ describe("createCodexDynamicToolBridge", () => {
236258 hookContext : { runId : "run-unavailable" , onToolOutcome } ,
237259 } ) ;
238260
239- expect ( bridge . availableSpecs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
240- expect ( bridge . specs . map ( ( tool ) => tool . name ) ) . toEqual ( [
241- "message" ,
242- HEARTBEAT_RESPONSE_TOOL_NAME ,
243- ] ) ;
261+ expect ( specNames ( bridge . availableSpecs ) ) . toEqual ( [ "message" ] ) ;
262+ expect ( specNames ( bridge . specs ) ) . toEqual ( [ "message" , HEARTBEAT_RESPONSE_TOOL_NAME ] ) ;
244263
245264 const result = await bridge . handleToolCall (
246265 {
@@ -312,11 +331,11 @@ describe("createCodexDynamicToolBridge", () => {
312331 signal : new AbortController ( ) . signal ,
313332 } ) ;
314333
315- expect ( bridge . availableSpecs [ 0 ] ?. inputSchema ) . toEqual ( {
334+ expect ( flattenSpecsWithNamespace ( bridge . availableSpecs ) [ 0 ] ?. inputSchema ) . toEqual ( {
316335 type : "object" ,
317336 properties : { current : { type : "string" } } ,
318337 } ) ;
319- expect ( bridge . specs [ 0 ] ?. inputSchema ) . toEqual ( {
338+ expect ( flattenSpecsWithNamespace ( bridge . specs ) [ 0 ] ?. inputSchema ) . toEqual ( {
320339 type : "object" ,
321340 properties : { durable : { type : "string" } } ,
322341 } ) ;
@@ -352,8 +371,8 @@ describe("createCodexDynamicToolBridge", () => {
352371 unsubscribeDiagnostics ( ) ;
353372 }
354373
355- expect ( bridge . availableSpecs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
356- expect ( bridge . specs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
374+ expect ( specNames ( bridge . availableSpecs ) ) . toEqual ( [ "message" ] ) ;
375+ expect ( specNames ( bridge . specs ) ) . toEqual ( [ "message" ] ) ;
357376 expect ( bridge . telemetry . quarantinedTools ) . toEqual ( [
358377 {
359378 tool : "fuzzplugin_move_angles" ,
@@ -450,8 +469,8 @@ describe("createCodexDynamicToolBridge", () => {
450469 signal : new AbortController ( ) . signal ,
451470 } ) ;
452471
453- expect ( bridge . availableSpecs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
454- expect ( bridge . specs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
472+ expect ( specNames ( bridge . availableSpecs ) ) . toEqual ( [ "message" ] ) ;
473+ expect ( specNames ( bridge . specs ) ) . toEqual ( [ "message" ] ) ;
455474 expect ( bridge . telemetry . quarantinedTools ) . toEqual ( [
456475 {
457476 tool : "tool[0]" ,
@@ -509,8 +528,8 @@ describe("createCodexDynamicToolBridge", () => {
509528 signal : new AbortController ( ) . signal ,
510529 } ) ;
511530
512- expect ( registeredBridge . availableSpecs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
513- expect ( registeredBridge . specs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
531+ expect ( specNames ( registeredBridge . availableSpecs ) ) . toEqual ( [ "message" ] ) ;
532+ expect ( specNames ( registeredBridge . specs ) ) . toEqual ( [ "message" ] ) ;
514533 } ) ;
515534
516535 it ( "can expose all dynamic tools directly for compatibility" , ( ) => {
0 commit comments