@@ -2,42 +2,44 @@ import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
22import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
33import {
44 createEmbeddedPiResourceLoader ,
5+ createEmbeddedPiResourceLoaderSync ,
56 EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS ,
6- getResourceLoaderCacheSize ,
7- invalidateResourceLoaderCache ,
87 markResourceLoaderReloaded ,
9- pruneResourceLoaderCache ,
108} from "./resource-loader.js" ;
119
10+ // Mock DefaultResourceLoader to track construction and method calls
11+ const mockLoadExtensionFactories = vi . fn ( async ( runtime : unknown ) => ( {
12+ extensions : [ ] ,
13+ errors : [ ] ,
14+ } ) ) ;
15+
1216vi . mock ( "@earendil-works/pi-coding-agent" , ( ) => ( {
1317 DefaultResourceLoader : vi . fn ( function DefaultResourceLoader (
1418 this : Record < string , unknown > ,
1519 options : unknown ,
1620 ) {
1721 Object . assign ( this , {
1822 options,
19- reload : vi . fn ( async ( ) => undefined ) ,
23+ extensionsResult : { extensions : [ ] , errors : [ ] , runtime : { } } ,
24+ loadExtensionFactories : mockLoadExtensionFactories ,
2025 } ) ;
2126 } ) ,
2227} ) ) ;
2328
2429describe ( "createEmbeddedPiResourceLoader" , ( ) => {
2530 beforeEach ( ( ) => {
26- // Clear cache before each test
27- invalidateResourceLoaderCache ( ) ;
28- // Reset mock call count
2931 vi . clearAllMocks ( ) ;
3032 } ) ;
3133
3234 afterEach ( ( ) => {
33- invalidateResourceLoaderCache ( ) ;
35+ vi . clearAllMocks ( ) ;
3436 } ) ;
3537
36- it ( "keeps inline extensions but disables Pi filesystem discovery" , ( ) => {
38+ it ( "passes correct options to DefaultResourceLoader including discovery flags" , async ( ) => {
3739 const settingsManager = { } ;
3840 const extensionFactories = [ vi . fn ( ) ] ;
3941
40- createEmbeddedPiResourceLoader ( {
42+ await createEmbeddedPiResourceLoader ( {
4143 cwd : "/workspace" ,
4244 agentDir : "/agent" ,
4345 settingsManager : settingsManager as never ,
@@ -53,230 +55,136 @@ describe("createEmbeddedPiResourceLoader", () => {
5355 } ) ;
5456 } ) ;
5557
56- it ( "caches resource loader for repeated calls with same cwd/agentDir" , ( ) => {
58+ it ( "calls loadExtensionFactories directly without reload()" , async ( ) => {
5759 const settingsManager = { } ;
5860
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 ( {
61+ await createEmbeddedPiResourceLoader ( {
6962 cwd : "/workspace" ,
7063 agentDir : "/agent" ,
7164 settingsManager : settingsManager as never ,
7265 extensionFactories : [ ] ,
7366 } ) ;
7467
75- // Should be same instance
76- expect ( loader1 ) . toBe ( loader2 ) ;
77- // Should only create one DefaultResourceLoader
78- expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 1 ) ;
68+ // Should call loadExtensionFactories directly
69+ expect ( mockLoadExtensionFactories ) . toHaveBeenCalledTimes ( 1 ) ;
70+ // Should not have a reload method called (we skip it entirely)
71+ // The mock doesn't have reload, so if it was called it would throw
7972 } ) ;
8073
81- it ( "creates separate loaders for different workspaces" , ( ) => {
82- const loader1 = createEmbeddedPiResourceLoader ( {
83- cwd : "/workspace1" ,
84- agentDir : "/agent" ,
85- settingsManager : { } as never ,
86- extensionFactories : [ ] ,
74+ it ( "loads inline extensionFactories into extensionsResult" , async ( ) => {
75+ const extensionFactories = [ vi . fn ( ) , vi . fn ( ) ] ;
76+ const mockExtensions = [ { name : "test-extension" } ] ;
77+
78+ mockLoadExtensionFactories . mockResolvedValueOnce ( {
79+ extensions : mockExtensions ,
80+ errors : [ ] ,
8781 } ) ;
8882
89- const loader2 = createEmbeddedPiResourceLoader ( {
90- cwd : "/workspace2 " ,
83+ const loader = await createEmbeddedPiResourceLoader ( {
84+ cwd : "/workspace " ,
9185 agentDir : "/agent" ,
9286 settingsManager : { } as never ,
93- extensionFactories : [ ] ,
87+ extensionFactories : extensionFactories as never ,
9488 } ) ;
9589
96- // Should be different instances
97- expect ( loader1 ) . not . toBe ( loader2 ) ;
98- // Should create two DefaultResourceLoader
99- expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 2 ) ;
90+ // Extensions should be loaded into extensionsResult
91+ expect ( ( loader as any ) . extensionsResult . extensions ) . toEqual ( mockExtensions ) ;
10092 } ) ;
10193
102- it ( "creates separate loaders for different agentDirs" , ( ) => {
103- const loader1 = createEmbeddedPiResourceLoader ( {
104- cwd : "/workspace" ,
105- agentDir : "/agent1" ,
106- settingsManager : { } as never ,
107- extensionFactories : [ ] ,
94+ it ( "accumulates errors from loadExtensionFactories" , async ( ) => {
95+ const mockErrors = [ { path : "<inline:1>" , error : "Failed to load" } ] ;
96+
97+ mockLoadExtensionFactories . mockResolvedValueOnce ( {
98+ extensions : [ ] ,
99+ errors : mockErrors ,
108100 } ) ;
109101
110- const loader2 = createEmbeddedPiResourceLoader ( {
102+ const loader = await createEmbeddedPiResourceLoader ( {
111103 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" ,
126104 agentDir : "/agent" ,
127105 settingsManager : { } as never ,
128- extensionFactories : [ ] ,
106+ extensionFactories : [ vi . fn ( ) ] as never ,
129107 } ) ;
130- expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 1 ) ;
131108
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 ) ;
109+ expect ( ( loader as any ) . extensionsResult . errors ) . toEqual ( mockErrors ) ;
148110 } ) ;
149111
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" ,
112+ it ( "is async to allow extension factory loading" , async ( ) => {
113+ // createEmbeddedPiResourceLoader returns a Promise
114+ const result = createEmbeddedPiResourceLoader ( {
115+ cwd : "/workspace" ,
159116 agentDir : "/agent" ,
160117 settingsManager : { } as never ,
161118 extensionFactories : [ ] ,
162119 } ) ;
163- expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 2 ) ;
164-
165- invalidateResourceLoaderCache ( ) ;
166- expect ( getResourceLoaderCacheSize ( ) ) . toBe ( 0 ) ;
167120
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 ) ;
121+ expect ( result ) . toBeInstanceOf ( Promise ) ;
122+ await result ; // Should resolve without error
176123 } ) ;
124+ } ) ;
177125
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
126+ describe ( "createEmbeddedPiResourceLoaderSync" , ( ) => {
127+ beforeEach ( ( ) => {
128+ vi . clearAllMocks ( ) ;
213129 } ) ;
214130
215- it ( "markResourceLoaderReloaded updates lastReloadAt timestamp " , ( ) => {
216- createEmbeddedPiResourceLoader ( {
131+ it ( "creates loader synchronously without extensionFactories " , ( ) => {
132+ const loader = createEmbeddedPiResourceLoaderSync ( {
217133 cwd : "/workspace" ,
218134 agentDir : "/agent" ,
219135 settingsManager : { } as never ,
220- extensionFactories : [ ] ,
221136 } ) ;
222137
223- // Should not throw
224- markResourceLoaderReloaded ( "/workspace" , "/agent" ) ;
138+ expect ( loader ) . toBeDefined ( ) ;
139+ expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 1 ) ;
225140 } ) ;
226141
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 ( {
142+ it ( "does not call loadExtensionFactories for sync version" , ( ) => {
143+ createEmbeddedPiResourceLoaderSync ( {
232144 cwd : "/workspace" ,
233145 agentDir : "/agent" ,
234146 settingsManager : { } as never ,
235- extensionFactories : [ ] ,
236147 } ) ;
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 ) ;
244148
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 ;
149+ // Sync version skips extension loading
150+ expect ( mockLoadExtensionFactories ) . not . toHaveBeenCalled ( ) ;
256151 } ) ;
257152
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 ( {
153+ it ( "passes empty extensionFactories array" , ( ) => {
154+ createEmbeddedPiResourceLoaderSync ( {
263155 cwd : "/workspace" ,
264156 agentDir : "/agent" ,
265157 settingsManager : { } as never ,
266- extensionFactories : [ ] ,
267158 } ) ;
268- expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 1 ) ;
269159
270- // Immediate second call should use cache
271- createEmbeddedPiResourceLoader ( {
160+ expect ( DefaultResourceLoader ) . toHaveBeenCalledWith ( {
272161 cwd : "/workspace" ,
273162 agentDir : "/agent" ,
274- settingsManager : { } as never ,
163+ settingsManager : { } ,
275164 extensionFactories : [ ] ,
165+ ...EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS ,
276166 } ) ;
277- expect ( DefaultResourceLoader ) . toHaveBeenCalledTimes ( 1 ) ;
167+ } ) ;
168+ } ) ;
278169
279- // Clean up env
280- delete process . env . OPENCLAW_RESOURCE_LOADER_CACHE_TTL_MS ;
170+ describe ( "EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS" , ( ) => {
171+ it ( "disables all filesystem discovery" , ( ) => {
172+ expect ( EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS ) . toEqual ( {
173+ noExtensions : true ,
174+ noSkills : true ,
175+ noPromptTemplates : true ,
176+ noThemes : true ,
177+ noContextFiles : true ,
178+ } ) ;
281179 } ) ;
282180} ) ;
181+
182+ describe ( "markResourceLoaderReloaded" , ( ) => {
183+ it ( "is a no-op that does not throw" , ( ) => {
184+ // Should not throw for any arguments
185+ markResourceLoaderReloaded ( "/workspace" , "/agent" ) ;
186+ markResourceLoaderReloaded ( "" , "" ) ;
187+ markResourceLoaderReloaded ( "any" , "path" ) ;
188+ expect ( true ) . toBe ( true ) ; // Explicit pass
189+ } ) ;
190+ } ) ;
0 commit comments