11import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent" ;
2- import { describe , expect , it , vi } from "vitest" ;
2+ import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
33import {
44 createEmbeddedPiResourceLoader ,
55 EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS ,
6+ getResourceLoaderCacheSize ,
7+ invalidateResourceLoaderCache ,
8+ markResourceLoaderReloaded ,
9+ pruneResourceLoaderCache ,
610} from "./resource-loader.js" ;
711
812vi . mock ( "@earendil-works/pi-coding-agent" , ( ) => ( {
@@ -18,6 +22,17 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
1822} ) ) ;
1923
2024describe ( "createEmbeddedPiResourceLoader" , ( ) => {
25+ beforeEach ( ( ) => {
26+ // Clear cache before each test
27+ invalidateResourceLoaderCache ( ) ;
28+ // Reset mock call count
29+ vi . clearAllMocks ( ) ;
30+ } ) ;
31+
32+ afterEach ( ( ) => {
33+ invalidateResourceLoaderCache ( ) ;
34+ } ) ;
35+
2136 it ( "keeps inline extensions but disables Pi filesystem discovery" , ( ) => {
2237 const settingsManager = { } ;
2338 const extensionFactories = [ vi . fn ( ) ] ;
@@ -37,4 +52,231 @@ describe("createEmbeddedPiResourceLoader", () => {
3752 ...EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS ,
3853 } ) ;
3954 } ) ;
55+
56+ it ( "caches resource loader for repeated calls with same cwd/agentDir" , ( ) => {
57+ const settingsManager = { } ;
58+
59+ // First call creates new loader
60+ const loader1 = createEmbeddedPiResourceLoader ( {
61+ cwd : "/workspace" ,
62+ agentDir : "/agent" ,
63+ settingsManager : settingsManager as never ,
64+ extensionFactories : [ ] ,
65+ } ) ;
66+
67+ // Second call should return cached loader (no new DefaultResourceLoader call)
68+ const loader2 = createEmbeddedPiResourceLoader ( {
69+ cwd : "/workspace" ,
70+ agentDir : "/agent" ,
71+ settingsManager : settingsManager as never ,
72+ extensionFactories : [ ] ,
73+ } ) ;
74+
75+ // Should be same instance
76+ expect ( loader1 ) . toBe ( loader2 ) ;
77+ // Should only create one DefaultResourceLoader
78+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 1 ) ;
79+ } ) ;
80+
81+ it ( "creates separate loaders for different workspaces" , ( ) => {
82+ const loader1 = createEmbeddedPiResourceLoader ( {
83+ cwd : "/workspace1" ,
84+ agentDir : "/agent" ,
85+ settingsManager : { } as never ,
86+ extensionFactories : [ ] ,
87+ } ) ;
88+
89+ const loader2 = createEmbeddedPiResourceLoader ( {
90+ cwd : "/workspace2" ,
91+ agentDir : "/agent" ,
92+ settingsManager : { } as never ,
93+ extensionFactories : [ ] ,
94+ } ) ;
95+
96+ // Should be different instances
97+ expect ( loader1 ) . not . toBe ( loader2 ) ;
98+ // Should create two DefaultResourceLoader
99+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 2 ) ;
100+ } ) ;
101+
102+ it ( "creates separate loaders for different agentDirs" , ( ) => {
103+ const loader1 = createEmbeddedPiResourceLoader ( {
104+ cwd : "/workspace" ,
105+ agentDir : "/agent1" ,
106+ settingsManager : { } as never ,
107+ extensionFactories : [ ] ,
108+ } ) ;
109+
110+ const loader2 = createEmbeddedPiResourceLoader ( {
111+ cwd : "/workspace" ,
112+ agentDir : "/agent2" ,
113+ settingsManager : { } as never ,
114+ extensionFactories : [ ] ,
115+ } ) ;
116+
117+ expect ( loader1 ) . not . toBe ( loader2 ) ;
118+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 2 ) ;
119+ } ) ;
120+
121+ it ( "cache size tracks number of cached loaders" , ( ) => {
122+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 0 ) ;
123+
124+ createEmbeddedPiResourceLoader ( {
125+ cwd : "/workspace1" ,
126+ agentDir : "/agent" ,
127+ settingsManager : { } as never ,
128+ extensionFactories : [ ] ,
129+ } ) ;
130+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 1 ) ;
131+
132+ createEmbeddedPiResourceLoader ( {
133+ cwd : "/workspace2" ,
134+ agentDir : "/agent" ,
135+ settingsManager : { } as never ,
136+ extensionFactories : [ ] ,
137+ } ) ;
138+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 2 ) ;
139+
140+ // Same workspace should not increase cache size
141+ createEmbeddedPiResourceLoader ( {
142+ cwd : "/workspace1" ,
143+ agentDir : "/agent" ,
144+ settingsManager : { } as never ,
145+ extensionFactories : [ ] ,
146+ } ) ;
147+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 2 ) ;
148+ } ) ;
149+
150+ it ( "invalidateResourceLoaderCache clears all entries" , ( ) => {
151+ createEmbeddedPiResourceLoader ( {
152+ cwd : "/workspace1" ,
153+ agentDir : "/agent" ,
154+ settingsManager : { } as never ,
155+ extensionFactories : [ ] ,
156+ } ) ;
157+ createEmbeddedPiResourceLoader ( {
158+ cwd : "/workspace2" ,
159+ agentDir : "/agent" ,
160+ settingsManager : { } as never ,
161+ extensionFactories : [ ] ,
162+ } ) ;
163+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 2 ) ;
164+
165+ invalidateResourceLoaderCache ( ) ;
166+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 0 ) ;
167+
168+ // After invalidate, should create new loader
169+ createEmbeddedPiResourceLoader ( {
170+ cwd : "/workspace1" ,
171+ agentDir : "/agent" ,
172+ settingsManager : { } as never ,
173+ extensionFactories : [ ] ,
174+ } ) ;
175+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 3 ) ;
176+ } ) ;
177+
178+ it ( "invalidateResourceLoaderCache with specific cwd/agentDir only removes that entry" , ( ) => {
179+ createEmbeddedPiResourceLoader ( {
180+ cwd : "/workspace1" ,
181+ agentDir : "/agent1" ,
182+ settingsManager : { } as never ,
183+ extensionFactories : [ ] ,
184+ } ) ;
185+ createEmbeddedPiResourceLoader ( {
186+ cwd : "/workspace2" ,
187+ agentDir : "/agent2" ,
188+ settingsManager : { } as never ,
189+ extensionFactories : [ ] ,
190+ } ) ;
191+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 2 ) ;
192+
193+ invalidateResourceLoaderCache ( "/workspace1" , "/agent1" ) ;
194+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 1 ) ;
195+
196+ // workspace1 should create new loader
197+ createEmbeddedPiResourceLoader ( {
198+ cwd : "/workspace1" ,
199+ agentDir : "/agent1" ,
200+ settingsManager : { } as never ,
201+ extensionFactories : [ ] ,
202+ } ) ;
203+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 3 ) ;
204+
205+ // workspace2 should still use cached loader
206+ createEmbeddedPiResourceLoader ( {
207+ cwd : "/workspace2" ,
208+ agentDir : "/agent2" ,
209+ settingsManager : { } as never ,
210+ extensionFactories : [ ] ,
211+ } ) ;
212+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 3 ) ; // No new call
213+ } ) ;
214+
215+ it ( "markResourceLoaderReloaded updates lastReloadAt timestamp" , ( ) => {
216+ createEmbeddedPiResourceLoader ( {
217+ cwd : "/workspace" ,
218+ agentDir : "/agent" ,
219+ settingsManager : { } as never ,
220+ extensionFactories : [ ] ,
221+ } ) ;
222+
223+ // Should not throw
224+ markResourceLoaderReloaded ( "/workspace" , "/agent" ) ;
225+ } ) ;
226+
227+ it ( "pruneResourceLoaderCache removes expired entries when TTL env is set" , async ( ) => {
228+ // Set very short TTL for test
229+ process . env . OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS = "100" ;
230+
231+ createEmbeddedPiResourceLoader ( {
232+ cwd : "/workspace" ,
233+ agentDir : "/agent" ,
234+ settingsManager : { } as never ,
235+ extensionFactories : [ ] ,
236+ } ) ;
237+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 1 ) ;
238+
239+ // Wait for TTL to expire
240+ await new Promise ( ( resolve ) => setTimeout ( resolve , 150 ) ) ;
241+
242+ pruneResourceLoaderCache ( ) ;
243+ expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 0 ) ;
244+
245+ // After prune, should create new loader
246+ createEmbeddedPiResourceLoader ( {
247+ cwd : "/workspace" ,
248+ agentDir : "/agent" ,
249+ settingsManager : { } as never ,
250+ extensionFactories : [ ] ,
251+ } ) ;
252+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 2 ) ;
253+
254+ // Clean up env
255+ delete process . env . OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS ;
256+ } ) ;
257+
258+ it ( "respects OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS env var" , ( ) => {
259+ // Set TTL to 10 seconds (minimum)
260+ process . env . OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS = "10000" ;
261+
262+ createEmbeddedPiResourceLoader ( {
263+ cwd : "/workspace" ,
264+ agentDir : "/agent" ,
265+ settingsManager : { } as never ,
266+ extensionFactories : [ ] ,
267+ } ) ;
268+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 1 ) ;
269+
270+ // Immediate second call should use cache
271+ createEmbeddedPiResourceLoader ( {
272+ cwd : "/workspace" ,
273+ agentDir : "/agent" ,
274+ settingsManager : { } as never ,
275+ extensionFactories : [ ] ,
276+ } ) ;
277+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 1 ) ;
278+
279+ // Clean up env
280+ delete process . env . OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS ;
281+ } ) ;
40282} ) ;
0 commit comments