From cd00706730c4b198057115dca7c1caa4bb3e49eb 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 a47b192ab8c4b3285bc8a199a45bba5799ba927c 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');