@@ -10,6 +10,7 @@ import {
1010 resolveAgentSessionStoreTargetsSync ,
1111 resolveAllAgentSessionStoreCandidateTargetsSync ,
1212 resolveAllAgentSessionStoreTargetsSync ,
13+ resolveExistingAgentSessionStoreTargetsSync ,
1314 resolveSessionStoreTargets ,
1415} from "./targets.js" ;
1516
@@ -247,6 +248,128 @@ describe("resolveAgentSessionStoreTargetsSync", () => {
247248 } ) ;
248249} ) ;
249250
251+ describe ( "resolveExistingAgentSessionStoreTargetsSync" , ( ) => {
252+ it ( "requires agent-specific rows instead of fixed-store file existence" , async ( ) => {
253+ await withTempHome ( async ( home ) => {
254+ const storePath = path . join ( home , "shared" , "sessions.json" ) ;
255+ await fs . mkdir ( path . dirname ( storePath ) , { recursive : true } ) ;
256+ await fs . writeFile ( storePath , "{}\n" , "utf8" ) ;
257+ const cfg : OpenClawConfig = {
258+ agents : { list : [ { id : "main" , default : true } ] } ,
259+ session : { store : storePath } ,
260+ } ;
261+
262+ expect ( resolveExistingAgentSessionStoreTargetsSync ( cfg , "ghost" ) ) . toEqual ( [ ] ) ;
263+
264+ await replaceSessionEntry (
265+ { agentId : "ghost" , sessionKey : "agent:ghost:existing" , storePath } ,
266+ { sessionId : "session-ghost" , updatedAt : 42 } ,
267+ ) ;
268+
269+ expect ( resolveExistingAgentSessionStoreTargetsSync ( cfg , "ghost" ) ) . toEqual ( [
270+ { agentId : "ghost" , storePath } ,
271+ ] ) ;
272+ } ) ;
273+ } ) ;
274+
275+ it ( "recognizes matching rows in a fixed legacy store without creating SQLite" , async ( ) => {
276+ await withTempHome ( async ( home ) => {
277+ const storePath = path . join ( home , "shared" , "sessions.json" ) ;
278+ await fs . mkdir ( path . dirname ( storePath ) , { recursive : true } ) ;
279+ await fs . writeFile (
280+ storePath ,
281+ JSON . stringify ( {
282+ "agent:retired:existing" : { sessionId : "legacy-retired" , updatedAt : 42 } ,
283+ "agent:other:existing" : { sessionId : "legacy-other" , updatedAt : 41 } ,
284+ } ) ,
285+ "utf8" ,
286+ ) ;
287+ const cfg : OpenClawConfig = {
288+ agents : { list : [ { id : "main" , default : true } ] } ,
289+ session : { store : storePath } ,
290+ } ;
291+
292+ expect ( resolveExistingAgentSessionStoreTargetsSync ( cfg , "retired" ) ) . toEqual ( [
293+ { agentId : "retired" , storePath } ,
294+ ] ) ;
295+ expect ( resolveExistingAgentSessionStoreTargetsSync ( cfg , "ghost" ) ) . toEqual ( [ ] ) ;
296+ expect ( await fs . readdir ( path . dirname ( storePath ) ) ) . toEqual ( [ "sessions.json" ] ) ;
297+ } ) ;
298+ } ) ;
299+
300+ it ( "includes existing deterministic template targets outside discoverable agent roots" , async ( ) => {
301+ await withTempHome ( async ( home ) => {
302+ const storeTemplate = path . join ( home , "external-stores" , "sessions-{agentId}.json" ) ;
303+ const cfg : OpenClawConfig = {
304+ agents : { list : [ { id : "main" , default : true } ] } ,
305+ session : { store : storeTemplate } ,
306+ } ;
307+ const legacyStorePath = resolveStorePath ( storeTemplate , { agentId : "retired-legacy" } ) ;
308+ const sqliteStorePath = resolveStorePath ( storeTemplate , { agentId : "retired-sqlite" } ) ;
309+ await fs . mkdir ( path . dirname ( legacyStorePath ) , { recursive : true } ) ;
310+ await fs . writeFile (
311+ legacyStorePath ,
312+ JSON . stringify ( {
313+ "agent:retired-legacy:existing" : { sessionId : "legacy-retired" , updatedAt : 42 } ,
314+ } ) ,
315+ "utf8" ,
316+ ) ;
317+ await replaceSessionEntry (
318+ {
319+ agentId : "retired-sqlite" ,
320+ sessionKey : "agent:retired-sqlite:existing" ,
321+ storePath : sqliteStorePath ,
322+ } ,
323+ { sessionId : "sqlite-retired" , updatedAt : 42 } ,
324+ ) ;
325+
326+ expect ( resolveAllAgentSessionStoreTargetsSync ( cfg ) ) . not . toContainEqual ( {
327+ agentId : "retired-legacy" ,
328+ storePath : legacyStorePath ,
329+ } ) ;
330+ expect ( resolveAllAgentSessionStoreTargetsSync ( cfg ) ) . not . toContainEqual ( {
331+ agentId : "retired-sqlite" ,
332+ storePath : sqliteStorePath ,
333+ } ) ;
334+ expect ( resolveExistingAgentSessionStoreTargetsSync ( cfg , "retired-legacy" ) ) . toEqual ( [
335+ { agentId : "retired-legacy" , storePath : legacyStorePath } ,
336+ ] ) ;
337+ expect ( resolveExistingAgentSessionStoreTargetsSync ( cfg , "retired-sqlite" ) ) . toEqual ( [
338+ { agentId : "retired-sqlite" , storePath : sqliteStorePath } ,
339+ ] ) ;
340+ expect ( resolveExistingAgentSessionStoreTargetsSync ( cfg , "ghost" ) ) . toEqual ( [ ] ) ;
341+ } ) ;
342+ } ) ;
343+
344+ it ( "rejects a deterministic target whose store symlink escapes the agents root" , async ( ) => {
345+ await withTempHome ( async ( home ) => {
346+ if ( process . platform === "win32" ) {
347+ return ;
348+ }
349+ const customRoot = path . join ( home , "custom-state" ) ;
350+ const escapedSessionsDir = path . join ( customRoot , "agents" , "escaped" , "sessions" ) ;
351+ const outsideStorePath = path . join ( home , "outside-sessions.json" ) ;
352+ const escapedStorePath = path . join ( escapedSessionsDir , "sessions.json" ) ;
353+ await fs . mkdir ( escapedSessionsDir , { recursive : true } ) ;
354+ await fs . writeFile (
355+ outsideStorePath ,
356+ JSON . stringify ( {
357+ "agent:escaped:secret" : { sessionId : "outside-session" , updatedAt : 42 } ,
358+ } ) ,
359+ "utf8" ,
360+ ) ;
361+ await fs . symlink ( outsideStorePath , escapedStorePath ) ;
362+
363+ const cfg = createCustomRootCfg ( customRoot , "main" ) ;
364+ expect ( resolveAllAgentSessionStoreTargetsSync ( cfg ) ) . not . toContainEqual ( {
365+ agentId : "escaped" ,
366+ storePath : escapedStorePath ,
367+ } ) ;
368+ expect ( resolveExistingAgentSessionStoreTargetsSync ( cfg , "escaped" ) ) . toEqual ( [ ] ) ;
369+ } ) ;
370+ } ) ;
371+ } ) ;
372+
250373describe ( "resolveAllAgentSessionStoreTargetsSync" , ( ) => {
251374 it ( "includes discovered on-disk agent stores alongside configured targets" , async ( ) => {
252375 await withTempHome ( async ( home ) => {
0 commit comments