@@ -161,15 +161,19 @@ class ServerEditViewModelTest {
161 assertTrue(vm.state.value.canSave)
161 assertTrue(vm.state.value.canSave)
164− // --- save() ---
164+ // --- connect() save path (probe Reachable → save + connect + ping) ---
165+ // The former save()-specific tests now exercise the same persist path via connect(),
166+ // since the standalone "Save without connecting" action was removed: connect() is the
167+ // single primary action and its probe IS the connection test.
167− fun save_persistsProfileAndCallsOnDone() = testScope.runTest {
170+ fun connect_persistsProfileAndCallsOnDone() = testScope.runTest {
168 val container = FakeAppContainer()
171 val container = FakeAppContainer()
172+ container.probeResult = ProbeResult.Reachable
169 val vm = makeVm(container)
173 val vm = makeVm(container)
170 vm.update { it.copy(baseUrl = "http://localhost:3000", label = "Test") }
174 vm.update { it.copy(baseUrl = "http://localhost:3000", label = "Test") }
171 var doneCalled = false
175 var doneCalled = false
172− vm.save { doneCalled = true }
176+ vm.connect { doneCalled = true }
173 testScheduler.advanceUntilIdle()
177 testScheduler.advanceUntilIdle()
174 assertTrue(doneCalled)
178 assertTrue(doneCalled)
175 assertNotNull(container.fakeProfileStore.savedProfile)
179 assertNotNull(container.fakeProfileStore.savedProfile)
@@ -178,36 +182,12 @@ class ServerEditViewModelTest {
181− fun save_invalidUrlDoesNotSave() = testScope.runTest {
185+ fun connect_generatesIdForNewProfile() = testScope.runTest {
182− val container = FakeAppContainer()
183− val vm = makeVm(container)
184− vm.update { it.copy(baseUrl = "not a url") }
185− var doneCalled = false
186− vm.save { doneCalled = true }
187− testScheduler.advanceUntilIdle()
188− assertFalse(doneCalled)
189− assertNull(container.fakeProfileStore.savedProfile)
193− fun save_whileSavingDoesNotSaveAgain() = testScope.runTest {
194− val container = FakeAppContainer()
195− val vm = makeVm(container)
196− vm.update { it.copy(baseUrl = "http://localhost:3000") }
198− vm.save { doneCount++ }
199− // While saving, try to save again
200− vm.save { doneCount++ }
201− testScheduler.advanceUntilIdle()
202− assertEquals(1, doneCount)
206− fun save_generatesIdForNewProfile() = testScope.runTest {
207 val container = FakeAppContainer()
186 val container = FakeAppContainer()
187+ container.probeResult = ProbeResult.Reachable
208 val vm = makeVm(container)
188 val vm = makeVm(container)
209 vm.update { it.copy(baseUrl = "http://localhost:3000") }
189 vm.update { it.copy(baseUrl = "http://localhost:3000") }
210− vm.save { }
190+ vm.connect { }
211 testScheduler.advanceUntilIdle()
191 testScheduler.advanceUntilIdle()
212 val saved = container.fakeProfileStore.savedProfile
192 val saved = container.fakeProfileStore.savedProfile
213 assertNotNull(saved?.id)
193 assertNotNull(saved?.id)
@@ -215,23 +195,25 @@ class ServerEditViewModelTest {
218− fun save_preservesIdForExistingProfile() = testScope.runTest {
198+ fun connect_preservesIdForExistingProfile() = testScope.runTest {
219 val container = FakeAppContainer()
199 val container = FakeAppContainer()
200+ container.probeResult = ProbeResult.Reachable
220 val profile = ServerProfile(id = "existing-id", label = "Old", baseUrl = "http://old")
201 val profile = ServerProfile(id = "existing-id", label = "Old", baseUrl = "http://old")
221 container.fakeProfileStore.setProfiles(listOf(profile))
202 container.fakeProfileStore.setProfiles(listOf(profile))
222 val vm = makeVm(container, profileId = "existing-id")
203 val vm = makeVm(container, profileId = "existing-id")
223 vm.update { it.copy(baseUrl = "http://new") }
204 vm.update { it.copy(baseUrl = "http://new") }
224− vm.save { }
205+ vm.connect { }
225 testScheduler.advanceUntilIdle()
206 testScheduler.advanceUntilIdle()
226 assertEquals("existing-id", container.fakeProfileStore.savedProfile?.id)
207 assertEquals("existing-id", container.fakeProfileStore.savedProfile?.id)
230− fun save_trimsFields() = testScope.runTest {
211+ fun connect_trimsFields() = testScope.runTest {
231 val container = FakeAppContainer()
212 val container = FakeAppContainer()
213+ container.probeResult = ProbeResult.Reachable
232 val vm = makeVm(container)
214 val vm = makeVm(container)
233 vm.update { it.copy(baseUrl = " http://localhost:3000 ", label = " Test ", username = " admin ") }
215 vm.update { it.copy(baseUrl = " http://localhost:3000 ", label = " Test ", username = " admin ") }
234− vm.save { }
216+ vm.connect { }
235 testScheduler.advanceUntilIdle()
217 testScheduler.advanceUntilIdle()
236 val saved = container.fakeProfileStore.savedProfile!!
218 val saved = container.fakeProfileStore.savedProfile!!
237 assertEquals("http://localhost:3000", saved.baseUrl)
219 assertEquals("http://localhost:3000", saved.baseUrl)
@@ -240,11 +222,12 @@ class ServerEditViewModelTest {
243− fun save_appliesHttpsSchemeToBareLanHost() = testScope.runTest {
225+ fun connect_appliesHttpsSchemeToBareLanHost() = testScope.runTest {
244 val container = FakeAppContainer()
226 val container = FakeAppContainer()
227+ container.probeResult = ProbeResult.Reachable
245 val vm = makeVm(container)
228 val vm = makeVm(container)
246 vm.update { it.copy(baseUrl = "localhost:3000", label = "LAN") }
229 vm.update { it.copy(baseUrl = "localhost:3000", label = "LAN") }
247− vm.save { }
230+ vm.connect { }
248 testScheduler.advanceUntilIdle()
231 testScheduler.advanceUntilIdle()
249 val saved = container.fakeProfileStore.savedProfile!!
232 val saved = container.fakeProfileStore.savedProfile!!
250 // A bare host is always schemed to https:// (the safe default). The user can still
233 // A bare host is always schemed to https:// (the safe default). The user can still
@@ -258,35 +241,38 @@ class ServerEditViewModelTest {
261− fun save_appliesHttpsSchemeToBarePublicDomain() = testScope.runTest {
244+ fun connect_appliesHttpsSchemeToBarePublicDomain() = testScope.runTest {
262 val container = FakeAppContainer()
245 val container = FakeAppContainer()
246+ container.probeResult = ProbeResult.Reachable
263 val vm = makeVm(container)
247 val vm = makeVm(container)
264 vm.update { it.copy(baseUrl = "example.com") }
248 vm.update { it.copy(baseUrl = "example.com") }
265− vm.save { }
249+ vm.connect { }
266 testScheduler.advanceUntilIdle()
250 testScheduler.advanceUntilIdle()
267 val saved = container.fakeProfileStore.savedProfile!!
251 val saved = container.fakeProfileStore.savedProfile!!
268 assertEquals("https://example.com", saved.baseUrl)
252 assertEquals("https://example.com", saved.baseUrl)
272− fun save_preservesExplicitHttpSchemeForPublicDomain() = testScope.runTest {
256+ fun connect_preservesExplicitHttpSchemeForPublicDomain() = testScope.runTest {
273 val container = FakeAppContainer()
257 val container = FakeAppContainer()
258+ container.probeResult = ProbeResult.Reachable
274 val vm = makeVm(container)
259 val vm = makeVm(container)
275 // The user typed the scheme explicitly — don't upgrade a public domain to https://
260 // The user typed the scheme explicitly — don't upgrade a public domain to https://
276 // behind their back; preserve their explicit http:// choice.
261 // behind their back; preserve their explicit http:// choice.
277 vm.update { it.copy(baseUrl = "http://example.com") }
262 vm.update { it.copy(baseUrl = "http://example.com") }
278− vm.save { }
263+ vm.connect { }
279 testScheduler.advanceUntilIdle()
264 testScheduler.advanceUntilIdle()
280 val saved = container.fakeProfileStore.savedProfile!!
265 val saved = container.fakeProfileStore.savedProfile!!
281 assertEquals("http://example.com", saved.baseUrl)
266 assertEquals("http://example.com", saved.baseUrl)
285− fun save_emptyUsernameClearsCredentials() = testScope.runTest {
270+ fun connect_emptyUsernameClearsCredentials() = testScope.runTest {
286 val container = FakeAppContainer()
271 val container = FakeAppContainer()
272+ container.probeResult = ProbeResult.Reachable
287 val vm = makeVm(container)
273 val vm = makeVm(container)
288 vm.update { it.copy(baseUrl = "http://localhost:3000", username = "", password = "") }
274 vm.update { it.copy(baseUrl = "http://localhost:3000", username = "", password = "") }
289− vm.save { }
275+ vm.connect { }
290 testScheduler.advanceUntilIdle()
276 testScheduler.advanceUntilIdle()
291 val saved = container.fakeProfileStore.savedProfile!!
277 val saved = container.fakeProfileStore.savedProfile!!
292 assertNull(saved.username)
278 assertNull(saved.username)
@@ -294,13 +280,14 @@ class ServerEditViewModelTest {
297− fun save_failureSetsErrorAndClearsSaving() = testScope.runTest {
283+ fun connect_saveFailureSetsErrorAndClearsSaving() = testScope.runTest {
298 val container = FakeAppContainer()
284 val container = FakeAppContainer()
285+ container.probeResult = ProbeResult.Reachable
299 container.fakeProfileStore.saveException = RuntimeException("disk full")
286 container.fakeProfileStore.saveException = RuntimeException("disk full")
300 val vm = makeVm(container)
287 val vm = makeVm(container)
301 vm.update { it.copy(baseUrl = "http://localhost:3000") }
288 vm.update { it.copy(baseUrl = "http://localhost:3000") }
302 var doneCalled = false
289 var doneCalled = false
303− vm.save { doneCalled = true }
290+ vm.connect { doneCalled = true }
304 testScheduler.advanceUntilIdle()
291 testScheduler.advanceUntilIdle()
305 assertFalse(doneCalled)
292 assertFalse(doneCalled)
306 assertFalse(vm.state.value.saving)
293 assertFalse(vm.state.value.saving)
@@ -310,12 +297,13 @@ class ServerEditViewModelTest {
313− fun save_failureDoesNotClobberUserEdits() = testScope.runTest {
300+ fun connect_saveFailureDoesNotClobberUserEdits() = testScope.runTest {
314 val container = FakeAppContainer()
301 val container = FakeAppContainer()
302+ container.probeResult = ProbeResult.Reachable
315 container.fakeProfileStore.saveException = RuntimeException("disk full")
303 container.fakeProfileStore.saveException = RuntimeException("disk full")
316 val vm = makeVm(container)
304 val vm = makeVm(container)
317 vm.update { it.copy(baseUrl = "http://localhost:3000", label = "Original") }
305 vm.update { it.copy(baseUrl = "http://localhost:3000", label = "Original") }
318− vm.save { }
306+ vm.connect { }
319 // Simulate the user editing the label while the save coroutine is in-flight.
307 // Simulate the user editing the label while the save coroutine is in-flight.
320 vm.update { it.copy(label = "Edited") }
308 vm.update { it.copy(label = "Edited") }
321 testScheduler.advanceUntilIdle()
309 testScheduler.advanceUntilIdle()