From 6021e8fa4573933ad15a3b633038d09c3bdf40f2 Mon Sep 17 00:00:00 2001 From: Gujiassh Date: Thu, 12 Mar 2026 12:36:22 +0900 Subject: [PATCH 1/2] fix(mobile): allow one-off status URL overrides Let the mobile API client probe a draft server URL without mutating the saved base URL so settings checks can target the address currently being edited. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../__tests__/services/api.service.test.ts | 20 +++++++++++++++ ui/mobile/src/services/api.service.ts | 25 +++++++++++-------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/ui/mobile/src/__tests__/services/api.service.test.ts b/ui/mobile/src/__tests__/services/api.service.test.ts index bf54ad500..6c0672753 100644 --- a/ui/mobile/src/__tests__/services/api.service.test.ts +++ b/ui/mobile/src/__tests__/services/api.service.test.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { API_POSE_STATUS_PATH } from '@/constants/api'; jest.mock('axios', () => { const mockAxiosInstance = { @@ -103,6 +104,25 @@ describe('ApiService', () => { }); }); + describe('getStatus', () => { + it('supports a one-off base URL override without mutating the saved base URL', async () => { + apiService.setBaseUrl('http://saved-host:3000'); + mockRequest.mockResolvedValueOnce({ data: { ok: true } }); + await apiService.getStatus('http://draft-host:4000'); + + expect(mockRequest).toHaveBeenCalledWith( + expect.objectContaining({ url: `http://draft-host:4000${API_POSE_STATUS_PATH}` }), + ); + + mockRequest.mockResolvedValueOnce({ data: { ok: true } }); + await apiService.get('/api/next'); + + expect(mockRequest).toHaveBeenLastCalledWith( + expect.objectContaining({ url: 'http://saved-host:3000/api/next' }), + ); + }); + }); + describe('post', () => { it('sends body data', () => { apiService.setBaseUrl('http://localhost:3000'); diff --git a/ui/mobile/src/services/api.service.ts b/ui/mobile/src/services/api.service.ts index 64e27fc9b..007820675 100644 --- a/ui/mobile/src/services/api.service.ts +++ b/ui/mobile/src/services/api.service.ts @@ -20,14 +20,15 @@ class ApiService { this.baseUrl = url ?? ''; } - private buildUrl(path: string): string { - if (!this.baseUrl) { + private buildUrl(path: string, baseUrlOverride?: string): string { + const baseUrl = baseUrlOverride ?? this.baseUrl; + if (!baseUrl) { return path; } if (path.startsWith('http://') || path.startsWith('https://')) { return path; } - const normalized = this.baseUrl.replace(/\/$/, ''); + const normalized = baseUrl.replace(/\/$/, ''); return `${normalized}${path.startsWith('/') ? path : `/${path}`}`; } @@ -53,31 +54,35 @@ class ApiService { return { message: 'Unknown error' }; } - private async requestWithRetry(config: AxiosRequestConfig, retriesLeft: number): Promise { + private async requestWithRetry( + config: AxiosRequestConfig, + retriesLeft: number, + baseUrlOverride?: string, + ): Promise { try { const response = await this.client.request({ ...config, - url: this.buildUrl(config.url || ''), + url: this.buildUrl(config.url || '', baseUrlOverride), }); return response.data; } catch (error) { if (retriesLeft > 0) { - return this.requestWithRetry(config, retriesLeft - 1); + return this.requestWithRetry(config, retriesLeft - 1, baseUrlOverride); } throw this.normalizeError(error); } } - get(path: string): Promise { - return this.requestWithRetry({ method: 'GET', url: path }, 2); + get(path: string, baseUrlOverride?: string): Promise { + return this.requestWithRetry({ method: 'GET', url: path }, 2, baseUrlOverride); } post(path: string, body: unknown): Promise { return this.requestWithRetry({ method: 'POST', url: path, data: body }, 2); } - getStatus(): Promise { - return this.get(API_POSE_STATUS_PATH); + getStatus(baseUrlOverride?: string): Promise { + return this.get(API_POSE_STATUS_PATH, baseUrlOverride); } getZones(): Promise { From fd308093da296a14308abccdf1cf3c22a911d8b3 Mon Sep 17 00:00:00 2001 From: Gujiassh Date: Thu, 12 Mar 2026 12:36:22 +0900 Subject: [PATCH 2/2] fix(mobile): test the draft settings URL Use the current Settings draft URL for connection checks so the UI validates what the user is editing rather than the last saved server address. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../__tests__/screens/SettingsScreen.test.tsx | 25 ++++++++++++++++++- .../screens/SettingsScreen/ServerUrlInput.tsx | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ui/mobile/src/__tests__/screens/SettingsScreen.test.tsx b/ui/mobile/src/__tests__/screens/SettingsScreen.test.tsx index c21e3153c..f6d0bdd8a 100644 --- a/ui/mobile/src/__tests__/screens/SettingsScreen.test.tsx +++ b/ui/mobile/src/__tests__/screens/SettingsScreen.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { render, screen } from '@testing-library/react-native'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react-native'; import { ThemeProvider } from '@/theme/ThemeContext'; +import { apiService } from '@/services/api.service'; import { useSettingsStore } from '@/stores/settingsStore'; jest.mock('@/services/ws.service', () => ({ @@ -23,6 +24,7 @@ jest.mock('@/services/api.service', () => ({ describe('SettingsScreen', () => { beforeEach(() => { + jest.clearAllMocks(); useSettingsStore.setState({ serverUrl: 'http://localhost:3000', rssiScanEnabled: false, @@ -82,4 +84,25 @@ describe('SettingsScreen', () => { expect(screen.getByText('ABOUT')).toBeTruthy(); expect(screen.getByText('WiFi-DensePose Mobile v1.0.0')).toBeTruthy(); }); + + it('tests the current draft URL before it is saved', async () => { + const { SettingsScreen } = require('@/screens/SettingsScreen'); + (apiService.getStatus as jest.Mock).mockResolvedValue({ ok: true }); + + render( + + + , + ); + + fireEvent.changeText( + screen.getByPlaceholderText('http://192.168.1.100:8080'), + 'http://10.0.0.42:9090', + ); + fireEvent.press(screen.getByText('Test Connection')); + + await waitFor(() => { + expect(apiService.getStatus).toHaveBeenCalledWith('http://10.0.0.42:9090'); + }); + }); }); diff --git a/ui/mobile/src/screens/SettingsScreen/ServerUrlInput.tsx b/ui/mobile/src/screens/SettingsScreen/ServerUrlInput.tsx index 79a4cc98e..f9d0db936 100644 --- a/ui/mobile/src/screens/SettingsScreen/ServerUrlInput.tsx +++ b/ui/mobile/src/screens/SettingsScreen/ServerUrlInput.tsx @@ -25,7 +25,7 @@ export const ServerUrlInput = ({ value, onChange, onSave }: ServerUrlInputProps) const start = Date.now(); try { - await apiService.getStatus(); + await apiService.getStatus(value.trim()); setTestResult(`✓ ${Date.now() - start}ms`); } catch { setTestResult('✗ Failed');