@@ -100,14 +100,19 @@ describe("cli hardlink helpers", () => {
100100 expect ( existsSync ( join ( tgt , "nigiri.cache" ) ) ) . toBe ( true ) ;
101101 } ) ;
102102
103- it ( "applyGeneratedHardlinks can no-op when plan is missing" , ( ) => {
104- const result = applyGeneratedHardlinks ( { rootDir : tmp , requirePlan : false } ) ;
103+ it ( "applyGeneratedHardlinks can no-op when plan is missing" , async ( ) => {
104+ const result = await applyGeneratedHardlinks ( {
105+ rootDir : tmp ,
106+ requirePlan : false ,
107+ forceLocal : true ,
108+ } ) ;
105109 expect ( result . applied ) . toBe ( false ) ;
106110 expect ( result . linked ) . toBe ( 0 ) ;
107111 expect ( result . pruned ) . toBe ( 0 ) ;
112+ expect ( result . via ) . toBe ( "none" ) ;
108113 } ) ;
109114
110- it ( "applyGeneratedHardlinks reads generated plan from infra/docker" , ( ) => {
115+ it ( "applyGeneratedHardlinks reads generated plan from infra/docker" , async ( ) => {
111116 const dataRoot = join ( tmp , "infra" , "docker" , "data" ) ;
112117 mkdirSync ( join ( dataRoot , "osm" ) , { recursive : true } ) ;
113118 writeFileSync ( join ( dataRoot , "osm" , "planet.osm.pbf" ) , "PBF" ) ;
@@ -126,27 +131,136 @@ describe("cli hardlink helpers", () => {
126131 "utf-8" ,
127132 ) ;
128133
129- const result = applyGeneratedHardlinks ( { rootDir : tmp , requirePlan : true , prune : true } ) ;
134+ const result = await applyGeneratedHardlinks ( {
135+ rootDir : tmp ,
136+ requirePlan : true ,
137+ prune : true ,
138+ forceLocal : true ,
139+ } ) ;
130140
131141 expect ( result . applied ) . toBe ( true ) ;
132142 expect ( result . linked ) . toBe ( 1 ) ;
133143 expect ( result . pruned ) . toBe ( 0 ) ;
144+ expect ( result . via ) . toBe ( "local" ) ;
134145 expect ( readFileSync ( join ( dataRoot , "nominatim" , "osm-pbf" , "data.osm.pbf" ) , "utf-8" ) ) . toBe (
135146 "PBF" ,
136147 ) ;
137148 } ) ;
138149
139- it ( "applyGeneratedHardlinks creates data root even with an empty plan" , ( ) => {
150+ it ( "applyGeneratedHardlinks creates data root even with an empty plan" , async ( ) => {
140151 const dataRoot = join ( tmp , "infra" , "docker" , "data" ) ;
141152 rmSync ( dataRoot , { recursive : true , force : true } ) ;
142153 writeFileSync ( join ( tmp , "infra" , "docker" , "docker-compose.generated.hardlinks.json" ) , "[]" ) ;
143154
144- const result = applyGeneratedHardlinks ( { rootDir : tmp , requirePlan : true , prune : true } ) ;
155+ const result = await applyGeneratedHardlinks ( {
156+ rootDir : tmp ,
157+ requirePlan : true ,
158+ prune : true ,
159+ forceLocal : true ,
160+ } ) ;
145161
146162 expect ( result . applied ) . toBe ( true ) ;
147163 expect ( result . linked ) . toBe ( 0 ) ;
148164 expect ( result . pruned ) . toBe ( 0 ) ;
165+ expect ( result . via ) . toBe ( "local" ) ;
149166 expect ( existsSync ( dataRoot ) ) . toBe ( true ) ;
150167 expect ( statSync ( dataRoot ) . isDirectory ( ) ) . toBe ( true ) ;
151168 } ) ;
169+
170+ it ( "falls through to the local linker when the data-manager probe fails" , async ( ) => {
171+ const dataRoot = join ( tmp , "infra" , "docker" , "data" ) ;
172+ mkdirSync ( join ( dataRoot , "osm" ) , { recursive : true } ) ;
173+ writeFileSync ( join ( dataRoot , "osm" , "planet.osm.pbf" ) , "PBF" ) ;
174+ mkdirSync ( join ( tmp , "infra" , "docker" ) , { recursive : true } ) ;
175+ writeFileSync (
176+ join ( tmp , "infra" , "docker" , "docker-compose.generated.hardlinks.json" ) ,
177+ JSON . stringify ( [
178+ {
179+ source : "data/osm" ,
180+ target : "data/valhalla/osm-pbf" ,
181+ consumerService : "valhalla" ,
182+ dataType : "osm-pbf" ,
183+ } ,
184+ ] ) ,
185+ "utf-8" ,
186+ ) ;
187+
188+ // Point at a port we know nothing is listening on so the reachability
189+ // probe times out fast — verifies the fallback wiring without standing
190+ // up a fake HTTP server.
191+ const result = await applyGeneratedHardlinks ( {
192+ rootDir : tmp ,
193+ requirePlan : true ,
194+ prune : true ,
195+ dataManagerUrl : "http://127.0.0.1:1" ,
196+ reachabilityTimeoutMs : 250 ,
197+ } ) ;
198+
199+ expect ( result . via ) . toBe ( "local" ) ;
200+ expect ( result . linked ) . toBe ( 1 ) ;
201+ } ) ;
202+
203+ it ( "uses the data-manager API when reachable" , async ( ) => {
204+ const dataRoot = join ( tmp , "infra" , "docker" , "data" ) ;
205+ mkdirSync ( join ( dataRoot , "osm" ) , { recursive : true } ) ;
206+ writeFileSync ( join ( dataRoot , "osm" , "planet.osm.pbf" ) , "PBF" ) ;
207+ mkdirSync ( join ( tmp , "infra" , "docker" ) , { recursive : true } ) ;
208+ writeFileSync (
209+ join ( tmp , "infra" , "docker" , "docker-compose.generated.hardlinks.json" ) ,
210+ JSON . stringify ( [
211+ {
212+ source : "data/osm" ,
213+ target : "data/valhalla/osm-pbf" ,
214+ consumerService : "valhalla" ,
215+ dataType : "osm-pbf" ,
216+ } ,
217+ ] ) ,
218+ "utf-8" ,
219+ ) ;
220+
221+ // Tiny mock data-manager: serves /status (probe) and /link (apply).
222+ const linkRequests : Array < { plan : unknown ; prune : unknown } > = [ ] ;
223+ const { createServer } = await import ( "node:http" ) ;
224+ const server = createServer ( ( req , res ) => {
225+ if ( req . url === "/status" ) {
226+ res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
227+ res . end ( JSON . stringify ( { ok : true , uptime : 1 , dataDir : "/data" } ) ) ;
228+ return ;
229+ }
230+ if ( req . url === "/link" && req . method === "POST" ) {
231+ const chunks : Buffer [ ] = [ ] ;
232+ req . on ( "data" , ( c ) => chunks . push ( Buffer . from ( c ) ) ) ;
233+ req . on ( "end" , ( ) => {
234+ const body = JSON . parse ( Buffer . concat ( chunks ) . toString ( "utf-8" ) ) ;
235+ linkRequests . push ( { plan : body . plan , prune : body . prune } ) ;
236+ res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
237+ res . end ( JSON . stringify ( { ok : true , linked : 7 , skipped : 2 , pruned : 1 } ) ) ;
238+ } ) ;
239+ return ;
240+ }
241+ res . writeHead ( 404 ) ;
242+ res . end ( ) ;
243+ } ) ;
244+ await new Promise < void > ( ( resolve ) => server . listen ( 0 , "127.0.0.1" , ( ) => resolve ( ) ) ) ;
245+ const port = ( server . address ( ) as { port : number } ) . port ;
246+
247+ try {
248+ const result = await applyGeneratedHardlinks ( {
249+ rootDir : tmp ,
250+ requirePlan : true ,
251+ prune : true ,
252+ dataManagerUrl : `http://127.0.0.1:${ port } ` ,
253+ } ) ;
254+
255+ expect ( result . via ) . toBe ( "data-manager" ) ;
256+ expect ( result . linked ) . toBe ( 7 ) ;
257+ expect ( result . skipped ) . toBe ( 2 ) ;
258+ expect ( result . pruned ) . toBe ( 1 ) ;
259+ expect ( linkRequests ) . toHaveLength ( 1 ) ;
260+ expect ( linkRequests [ 0 ] ?. prune ) . toBe ( true ) ;
261+ expect ( Array . isArray ( linkRequests [ 0 ] ?. plan ) ) . toBe ( true ) ;
262+ } finally {
263+ await new Promise < void > ( ( resolve ) => server . close ( ( ) => resolve ( ) ) ) ;
264+ }
265+ } ) ;
152266} ) ;
0 commit comments