1+ import os from "node:os" ;
2+ import path from "node:path" ;
13import { beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
24import { createTestRegistry } from "../../test-utils/channel-plugins.js" ;
35import { extractAssistantText , sanitizeTextContent } from "./sessions-helpers.js" ;
@@ -7,15 +9,24 @@ vi.mock("../../gateway/call.js", () => ({
79 callGateway : ( opts : unknown ) => callGatewayMock ( opts ) ,
810} ) ) ;
911
12+ type SessionsToolTestConfig = {
13+ session : { scope : "per-sender" ; mainKey : string } ;
14+ tools : {
15+ agentToAgent : { enabled : boolean } ;
16+ sessions ?: { visibility : "all" | "own" } ;
17+ } ;
18+ } ;
19+
20+ const loadConfigMock = vi . fn < ( ) => SessionsToolTestConfig > ( ( ) => ( {
21+ session : { scope : "per-sender" , mainKey : "main" } ,
22+ tools : { agentToAgent : { enabled : false } } ,
23+ } ) ) ;
24+
1025vi . mock ( "../../config/config.js" , async ( importOriginal ) => {
1126 const actual = await importOriginal < typeof import ( "../../config/config.js" ) > ( ) ;
1227 return {
1328 ...actual ,
14- loadConfig : ( ) =>
15- ( {
16- session : { scope : "per-sender" , mainKey : "main" } ,
17- tools : { agentToAgent : { enabled : false } } ,
18- } ) as never ,
29+ loadConfig : ( ) => loadConfigMock ( ) as never ,
1930 } ;
2031} ) ;
2132
@@ -94,6 +105,14 @@ beforeAll(async () => {
94105 ( { setActivePluginRegistry } = await import ( "../../plugins/runtime.js" ) ) ;
95106} ) ;
96107
108+ beforeEach ( ( ) => {
109+ loadConfigMock . mockReset ( ) ;
110+ loadConfigMock . mockReturnValue ( {
111+ session : { scope : "per-sender" , mainKey : "main" } ,
112+ tools : { agentToAgent : { enabled : false } } ,
113+ } ) ;
114+ } ) ;
115+
97116describe ( "extractAssistantText" , ( ) => {
98117 it ( "sanitizes blocks without injecting newlines" , ( ) => {
99118 const message = {
@@ -199,6 +218,176 @@ describe("sessions_list gating", () => {
199218 } ) ;
200219} ) ;
201220
221+ describe ( "sessions_list transcriptPath resolution" , ( ) => {
222+ beforeEach ( ( ) => {
223+ callGatewayMock . mockClear ( ) ;
224+ loadConfigMock . mockReturnValue ( {
225+ session : { scope : "per-sender" , mainKey : "main" } ,
226+ tools : {
227+ agentToAgent : { enabled : true } ,
228+ sessions : { visibility : "all" } ,
229+ } ,
230+ } ) ;
231+ } ) ;
232+
233+ it ( "resolves cross-agent transcript paths from agent defaults when gateway store path is relative" , async ( ) => {
234+ const stateDir = path . join ( os . tmpdir ( ) , "openclaw-state-relative" ) ;
235+ vi . stubEnv ( "OPENCLAW_STATE_DIR" , stateDir ) ;
236+
237+ try {
238+ callGatewayMock . mockResolvedValueOnce ( {
239+ path : "agents/main/sessions/sessions.json" ,
240+ sessions : [
241+ {
242+ key : "agent:worker:main" ,
243+ kind : "direct" ,
244+ sessionId : "sess-worker" ,
245+ } ,
246+ ] ,
247+ } ) ;
248+
249+ const tool = createSessionsListTool ( { agentSessionKey : "agent:main:main" } ) ;
250+ const result = await tool . execute ( "call1" , { } ) ;
251+
252+ const details = result . details as
253+ | { sessions ?: Array < { key ?: string ; transcriptPath ?: string } > }
254+ | undefined ;
255+ const session = details ?. sessions ?. [ 0 ] ;
256+ expect ( session ) . toMatchObject ( { key : "agent:worker:main" } ) ;
257+ const transcriptPath = String ( session ?. transcriptPath ?? "" ) ;
258+ expect ( path . normalize ( transcriptPath ) ) . toContain ( path . join ( "agents" , "worker" , "sessions" ) ) ;
259+ expect ( transcriptPath ) . toMatch ( / s e s s - w o r k e r \. j s o n l $ / ) ;
260+ } finally {
261+ vi . unstubAllEnvs ( ) ;
262+ }
263+ } ) ;
264+
265+ it ( "resolves transcriptPath even when sessions.list does not return a store path" , async ( ) => {
266+ const stateDir = path . join ( os . tmpdir ( ) , "openclaw-state-no-path" ) ;
267+ vi . stubEnv ( "OPENCLAW_STATE_DIR" , stateDir ) ;
268+
269+ try {
270+ callGatewayMock . mockResolvedValueOnce ( {
271+ sessions : [
272+ {
273+ key : "agent:worker:main" ,
274+ kind : "direct" ,
275+ sessionId : "sess-worker-no-path" ,
276+ } ,
277+ ] ,
278+ } ) ;
279+
280+ const tool = createSessionsListTool ( { agentSessionKey : "agent:main:main" } ) ;
281+ const result = await tool . execute ( "call1" , { } ) ;
282+
283+ const details = result . details as
284+ | { sessions ?: Array < { key ?: string ; transcriptPath ?: string } > }
285+ | undefined ;
286+ const session = details ?. sessions ?. [ 0 ] ;
287+ expect ( session ) . toMatchObject ( { key : "agent:worker:main" } ) ;
288+ const transcriptPath = String ( session ?. transcriptPath ?? "" ) ;
289+ expect ( path . normalize ( transcriptPath ) ) . toContain ( path . join ( "agents" , "worker" , "sessions" ) ) ;
290+ expect ( transcriptPath ) . toMatch ( / s e s s - w o r k e r - n o - p a t h \. j s o n l $ / ) ;
291+ } finally {
292+ vi . unstubAllEnvs ( ) ;
293+ }
294+ } ) ;
295+
296+ it ( "falls back to agent defaults when gateway path is non-string" , async ( ) => {
297+ const stateDir = path . join ( os . tmpdir ( ) , "openclaw-state-non-string-path" ) ;
298+ vi . stubEnv ( "OPENCLAW_STATE_DIR" , stateDir ) ;
299+
300+ try {
301+ callGatewayMock . mockResolvedValueOnce ( {
302+ path : { raw : "agents/main/sessions/sessions.json" } ,
303+ sessions : [
304+ {
305+ key : "agent:worker:main" ,
306+ kind : "direct" ,
307+ sessionId : "sess-worker-shape" ,
308+ } ,
309+ ] ,
310+ } ) ;
311+
312+ const tool = createSessionsListTool ( { agentSessionKey : "agent:main:main" } ) ;
313+ const result = await tool . execute ( "call1" , { } ) ;
314+
315+ const details = result . details as
316+ | { sessions ?: Array < { key ?: string ; transcriptPath ?: string } > }
317+ | undefined ;
318+ const session = details ?. sessions ?. [ 0 ] ;
319+ expect ( session ) . toMatchObject ( { key : "agent:worker:main" } ) ;
320+ const transcriptPath = String ( session ?. transcriptPath ?? "" ) ;
321+ expect ( path . normalize ( transcriptPath ) ) . toContain ( path . join ( "agents" , "worker" , "sessions" ) ) ;
322+ expect ( transcriptPath ) . toMatch ( / s e s s - w o r k e r - s h a p e \. j s o n l $ / ) ;
323+ } finally {
324+ vi . unstubAllEnvs ( ) ;
325+ }
326+ } ) ;
327+
328+ it ( "falls back to agent defaults when gateway path is '(multiple)'" , async ( ) => {
329+ const stateDir = path . join ( os . tmpdir ( ) , "openclaw-state-multiple" ) ;
330+ vi . stubEnv ( "OPENCLAW_STATE_DIR" , stateDir ) ;
331+
332+ try {
333+ callGatewayMock . mockResolvedValueOnce ( {
334+ path : "(multiple)" ,
335+ sessions : [
336+ {
337+ key : "agent:worker:main" ,
338+ kind : "direct" ,
339+ sessionId : "sess-worker-multiple" ,
340+ } ,
341+ ] ,
342+ } ) ;
343+
344+ const tool = createSessionsListTool ( { agentSessionKey : "agent:main:main" } ) ;
345+ const result = await tool . execute ( "call1" , { } ) ;
346+
347+ const details = result . details as
348+ | { sessions ?: Array < { key ?: string ; transcriptPath ?: string } > }
349+ | undefined ;
350+ const session = details ?. sessions ?. [ 0 ] ;
351+ expect ( session ) . toMatchObject ( { key : "agent:worker:main" } ) ;
352+ const transcriptPath = String ( session ?. transcriptPath ?? "" ) ;
353+ expect ( path . normalize ( transcriptPath ) ) . toContain (
354+ path . join ( stateDir , "agents" , "worker" , "sessions" ) ,
355+ ) ;
356+ expect ( transcriptPath ) . toMatch ( / s e s s - w o r k e r - m u l t i p l e \. j s o n l $ / ) ;
357+ } finally {
358+ vi . unstubAllEnvs ( ) ;
359+ }
360+ } ) ;
361+
362+ it ( "resolves absolute {agentId} template paths per session agent" , async ( ) => {
363+ const templateStorePath = "/tmp/openclaw/agents/{agentId}/sessions/sessions.json" ;
364+
365+ callGatewayMock . mockResolvedValueOnce ( {
366+ path : templateStorePath ,
367+ sessions : [
368+ {
369+ key : "agent:worker:main" ,
370+ kind : "direct" ,
371+ sessionId : "sess-worker-template" ,
372+ } ,
373+ ] ,
374+ } ) ;
375+
376+ const tool = createSessionsListTool ( { agentSessionKey : "agent:main:main" } ) ;
377+ const result = await tool . execute ( "call1" , { } ) ;
378+
379+ const details = result . details as
380+ | { sessions ?: Array < { key ?: string ; transcriptPath ?: string } > }
381+ | undefined ;
382+ const session = details ?. sessions ?. [ 0 ] ;
383+ expect ( session ) . toMatchObject ( { key : "agent:worker:main" } ) ;
384+ const transcriptPath = String ( session ?. transcriptPath ?? "" ) ;
385+ const expectedSessionsDir = path . dirname ( templateStorePath . replace ( "{agentId}" , "worker" ) ) ;
386+ expect ( path . normalize ( transcriptPath ) ) . toContain ( path . normalize ( expectedSessionsDir ) ) ;
387+ expect ( transcriptPath ) . toMatch ( / s e s s - w o r k e r - t e m p l a t e \. j s o n l $ / ) ;
388+ } ) ;
389+ } ) ;
390+
202391describe ( "sessions_send gating" , ( ) => {
203392 beforeEach ( ( ) => {
204393 callGatewayMock . mockClear ( ) ;
0 commit comments