@@ -6,6 +6,8 @@ import { describe, expect, it } from "@effect/vitest"
66import { Effect } from "effect"
77import { afterEach , beforeEach , vi } from "vitest"
88
9+ import type { ControllerBootstrapError } from "../../src/docker-git/host-errors.js"
10+
911type CommandSpec = {
1012 readonly args : ReadonlyArray < string >
1113 readonly command : string
@@ -14,13 +16,28 @@ type CommandSpec = {
1416}
1517
1618const ensureControllerReadyMock = vi . hoisted ( ( ) => vi . fn < ( ) => Effect . Effect < void > > ( ) )
19+ const resolveApiBaseUrlMock = vi . hoisted ( ( ) => vi . fn < ( ) => string > ( ) )
20+ const findReachableApiBaseUrlMock = vi . hoisted (
21+ ( ) => vi . fn < ( candidateUrls : ReadonlyArray < string > ) => Effect . Effect < string , ControllerBootstrapError > > ( )
22+ )
23+ const resolveConfiguredApiBaseUrlMock = vi . hoisted ( ( ) => vi . fn < ( ) => string > ( ) )
24+ const resolveExplicitApiBaseUrlMock = vi . hoisted ( ( ) => vi . fn < ( ) => string | undefined > ( ) )
1725const runCommandCaptureMock = vi . hoisted ( ( ) => vi . fn < ( spec : CommandSpec ) => Effect . Effect < string > > ( ) )
1826const runCommandExitCodeMock = vi . hoisted ( ( ) => vi . fn < ( spec : CommandSpec ) => Effect . Effect < number > > ( ) )
1927const runCommandExitCodeStreamingMock = vi . hoisted ( ( ) => vi . fn < ( spec : CommandSpec ) => Effect . Effect < number > > ( ) )
2028
2129vi . mock ( "../../src/docker-git/controller.js" , ( ) => ( {
2230 ensureControllerReady : ensureControllerReadyMock ,
23- resolveApiBaseUrl : ( ) => "http://127.0.0.1:3334"
31+ resolveApiBaseUrl : resolveApiBaseUrlMock
32+ } ) )
33+
34+ vi . mock ( "../../src/docker-git/controller-health.js" , ( ) => ( {
35+ findReachableApiBaseUrl : findReachableApiBaseUrlMock
36+ } ) )
37+
38+ vi . mock ( "../../src/docker-git/controller-reachability.js" , ( ) => ( {
39+ resolveConfiguredApiBaseUrl : resolveConfiguredApiBaseUrlMock ,
40+ resolveExplicitApiBaseUrl : resolveExplicitApiBaseUrlMock
2441} ) )
2542
2643vi . mock ( "../../src/docker-git/frontend-lib/shell/command-runner.js" , ( ) => ( {
@@ -63,6 +80,10 @@ const requireEnvValue = (
6380 return value
6481}
6582
83+ const makeHttpUrl = ( host : string , port : string ) : string => `http://${ host } :${ port } `
84+
85+ const dockerBridgeHost = [ "172" , "17" , "0" , "2" ] . join ( "." )
86+
6687const writeWebStateFile = (
6788 statePath : string ,
6889 state : Readonly < Record < string , string | number > >
@@ -100,6 +121,14 @@ describe("browser frontend command", () => {
100121 makeNonInteractive ( )
101122 ensureControllerReadyMock . mockReset ( )
102123 ensureControllerReadyMock . mockImplementation ( ( ) => Effect . void )
124+ resolveApiBaseUrlMock . mockReset ( )
125+ resolveApiBaseUrlMock . mockReturnValue ( "http://127.0.0.1:3334" )
126+ findReachableApiBaseUrlMock . mockReset ( )
127+ findReachableApiBaseUrlMock . mockImplementation ( ( candidateUrls ) => Effect . succeed ( candidateUrls [ 0 ] ?? "" ) )
128+ resolveConfiguredApiBaseUrlMock . mockReset ( )
129+ resolveConfiguredApiBaseUrlMock . mockReturnValue ( "http://127.0.0.1:3334" )
130+ resolveExplicitApiBaseUrlMock . mockReset ( )
131+ resolveExplicitApiBaseUrlMock . mockImplementation ( ( ) => { } )
103132 runCommandCaptureMock . mockReset ( )
104133 runCommandCaptureMock . mockImplementation ( ( ) => Effect . succeed ( "" ) )
105134 runCommandExitCodeMock . mockReset ( )
@@ -142,6 +171,50 @@ describe("browser frontend command", () => {
142171 expect ( runCommandExitCodeStreamingMock ) . toHaveBeenCalledTimes ( 2 )
143172 } ) )
144173
174+ it . effect ( "prefers the reachable host API URL over a selected Docker bridge URL for the web proxy" , ( ) =>
175+ Effect . gen ( function * ( _ ) {
176+ resolveApiBaseUrlMock . mockReturnValue ( makeHttpUrl ( dockerBridgeHost , "3334" ) )
177+ resolveConfiguredApiBaseUrlMock . mockReturnValue ( "http://127.0.0.1:3334" )
178+ findReachableApiBaseUrlMock . mockImplementation ( ( candidateUrls ) => Effect . succeed ( candidateUrls [ 0 ] ?? "" ) )
179+
180+ yield * _ ( runBrowserCommandUnderTest )
181+
182+ expect ( findReachableApiBaseUrlMock ) . toHaveBeenCalledWith ( [ "http://127.0.0.1:3334" ] )
183+ expect ( streamingEnvs ( ) ) . toEqual ( [
184+ expect . objectContaining ( { DOCKER_GIT_API_URL : "http://127.0.0.1:3334" } ) ,
185+ expect . objectContaining ( { DOCKER_GIT_API_URL : "http://127.0.0.1:3334" } )
186+ ] )
187+ } ) )
188+
189+ it . effect ( "falls back to the selected controller URL when the host API URL is unreachable" , ( ) =>
190+ Effect . gen ( function * ( _ ) {
191+ const dockerBridgeApiBaseUrl = makeHttpUrl ( dockerBridgeHost , "3334" )
192+ resolveApiBaseUrlMock . mockReturnValue ( dockerBridgeApiBaseUrl )
193+ resolveConfiguredApiBaseUrlMock . mockReturnValue ( "http://127.0.0.1:3334" )
194+ findReachableApiBaseUrlMock . mockReturnValue ( Effect . fail ( { _tag : "ControllerBootstrapError" , message : "no" } ) )
195+
196+ yield * _ ( runBrowserCommandUnderTest )
197+
198+ expect ( streamingEnvs ( ) ) . toEqual ( [
199+ expect . objectContaining ( { DOCKER_GIT_API_URL : dockerBridgeApiBaseUrl } ) ,
200+ expect . objectContaining ( { DOCKER_GIT_API_URL : dockerBridgeApiBaseUrl } )
201+ ] )
202+ } ) )
203+
204+ it . effect ( "does not override an explicit API URL" , ( ) =>
205+ Effect . gen ( function * ( _ ) {
206+ resolveApiBaseUrlMock . mockReturnValue ( "https://api.example.test" )
207+ resolveExplicitApiBaseUrlMock . mockReturnValue ( "https://api.example.test" )
208+
209+ yield * _ ( runBrowserCommandUnderTest )
210+
211+ expect ( findReachableApiBaseUrlMock ) . not . toHaveBeenCalled ( )
212+ expect ( streamingEnvs ( ) ) . toEqual ( [
213+ expect . objectContaining ( { DOCKER_GIT_API_URL : "https://api.example.test" } ) ,
214+ expect . objectContaining ( { DOCKER_GIT_API_URL : "https://api.example.test" } )
215+ ] )
216+ } ) )
217+
145218 it . effect ( "binds browser web to all host interfaces by default" , ( ) =>
146219 Effect . gen ( function * ( _ ) {
147220 yield * _ ( runBrowserCommandUnderTest )
0 commit comments