feat(robot): connect provisioning hotspots in app

This commit is contained in:
brother7 committed 2026-08-16 20:18:28 +08:00
1 parent abecd5f344
commit c1326a2980
17 files changed
+1950 -11

No files matched your search

+194
View File
@@ -44,6 +44,23 @@ function setup(
options: {
guidedHotspotBinding?: boolean;
openExternal?: (url: string) => Promise<void>;
robotHotspot?: {
scan: () => Promise<{
platform: 'windows' | 'macos';
hotspots: Array<{
candidateId: string;
ssid: string;
signalPercent: number;
connected: boolean;
}>;
}>;
connect: (candidateId: string) => Promise<{
connected: true;
candidateId: string;
ssid: string;
}>;
clear: () => void;
};
} = {},
) {
const getAccessToken = vi.fn().mockResolvedValue('secret-token');
@@ -263,6 +280,183 @@ describe('AI hardware Host API route', () => {
expect(getAccessToken).not.toHaveBeenCalled();
expect(fetchImpl).not.toHaveBeenCalled();
});
it('scans and connects Robot hotspots locally before credentials or upstream access', async () => {
const robotHotspot = {
scan: vi.fn().mockResolvedValue({
platform: 'windows' as const,
hotspots: [{
candidateId: 'candidate_123',
ssid: 'Xiaozhi-Desk',
signalPercent: 78,
connected: false,
nativeInterface: 'must-not-pass',
}],
}),
connect: vi.fn().mockResolvedValue({
connected: true as const,
candidateId: 'candidate_123',
ssid: 'Xiaozhi-Desk',
profile: 'must-not-pass',
}),
clear: vi.fn(),
};
const { handler, fetchImpl, getAccessToken } = setup(undefined, { robotHotspot });
const scanned = await invoke(
handler,
'POST',
'/api/works/ai-hardware/provisioning-hotspots/scan',
{},
);
const connected = await invoke(
handler,
'POST',
'/api/works/ai-hardware/provisioning-hotspots/connect',
{ candidate_id: 'candidate_123' },
);
expect(scanned.payload).toEqual({
success: true,
data: {
platform: 'windows',
hotspots: [{
candidate_id: 'candidate_123',
ssid: 'Xiaozhi-Desk',
signal_percent: 78,
connected: false,
}],
},
});
expect(connected.payload).toEqual({
success: true,
data: { connected: true, candidate_id: 'candidate_123', ssid: 'Xiaozhi-Desk' },
});
expect(robotHotspot.connect).toHaveBeenCalledWith('candidate_123');
expect(getAccessToken).not.toHaveBeenCalled();
expect(fetchImpl).not.toHaveBeenCalled();
});
it('rejects hotspot queries, missing or expanded bodies, and invalid candidate IDs locally', async () => {
const robotHotspot = {
scan: vi.fn().mockResolvedValue({ platform: 'windows' as const, hotspots: [] }),
connect: vi.fn(),
clear: vi.fn(),
};
const { handler, fetchImpl, getAccessToken } = setup(undefined, { robotHotspot });
const requests = [
invoke(handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/scan?refresh=1', {}),
invoke(handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/scan'),
invoke(handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/scan', { extra: true }),
invoke(handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/connect?retry=1', { candidate_id: 'candidate_123' }),
invoke(handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/connect', {}),
invoke(handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/connect', { candidate_id: '../ssid' }),
invoke(handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/connect', {
candidate_id: 'a'.repeat(129),
}),
invoke(handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/connect', {
candidate_id: 'candidate_123', ssid: 'Xiaozhi-Evil',
}),
];
for (const result of await Promise.all(requests)) {
expect(result.payload).toMatchObject({
success: false,
status: 400,
code: 'AI_HARDWARE_INVALID_REQUEST',
});
}
expect(robotHotspot.scan).not.toHaveBeenCalled();
expect(robotHotspot.connect).not.toHaveBeenCalled();
expect(getAccessToken).not.toHaveBeenCalled();
expect(fetchImpl).not.toHaveBeenCalled();
});
it('disables both local hotspot operations with the guided provisioning capability', async () => {
const robotHotspot = {
scan: vi.fn().mockResolvedValue({ platform: 'windows' as const, hotspots: [] }),
connect: vi.fn(),
clear: vi.fn(),
};
const disabled = setup(undefined, { guidedHotspotBinding: false, robotHotspot });
for (const result of await Promise.all([
invoke(disabled.handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/scan', {}),
invoke(disabled.handler, 'POST', '/api/works/ai-hardware/provisioning-hotspots/connect', {
candidate_id: 'candidate_123',
}),
])) {
expect(result.payload).toMatchObject({
success: false,
status: 403,
code: 'AI_HARDWARE_PROVISIONING_DISABLED',
});
}
expect(robotHotspot.scan).not.toHaveBeenCalled();
expect(robotHotspot.connect).not.toHaveBeenCalled();
expect(disabled.getAccessToken).not.toHaveBeenCalled();
expect(disabled.fetchImpl).not.toHaveBeenCalled();
});
it.each([
['unsupported', 501, 'AI_HARDWARE_HOTSPOT_UNSUPPORTED'],
['permission_denied', 403, 'AI_HARDWARE_HOTSPOT_PERMISSION_DENIED'],
['busy', 409, 'AI_HARDWARE_HOTSPOT_BUSY'],
['candidate_expired', 409, 'AI_HARDWARE_HOTSPOT_CANDIDATE_EXPIRED'],
['scan_failed', 502, 'AI_HARDWARE_HOTSPOT_SCAN_FAILED'],
])('projects native hotspot scan error %s safely', async (nativeCode, status, code) => {
const nativeError = Object.assign(new Error('native interface and location secret'), {
code: nativeCode,
});
const robotHotspot = {
scan: vi.fn().mockRejectedValue(nativeError),
connect: vi.fn(),
clear: vi.fn(),
};
const local = setup(undefined, { robotHotspot });
const result = await invoke(
local.handler,
'POST',
'/api/works/ai-hardware/provisioning-hotspots/scan',
{},
);
expect(result.status).toBe(200);
expect(result.payload).toMatchObject({ success: false, status, code, retryable: false });
expect(JSON.stringify(result.payload)).not.toContain('native interface');
expect(local.getAccessToken).not.toHaveBeenCalled();
expect(local.fetchImpl).not.toHaveBeenCalled();
});
it('projects native connect failures safely', async () => {
const robotHotspot = {
scan: vi.fn().mockResolvedValue({ platform: 'macos' as const, hotspots: [] }),
connect: vi.fn().mockRejectedValue(Object.assign(new Error('native profile secret'), {
code: 'connect_failed',
})),
clear: vi.fn(),
};
const local = setup(undefined, { robotHotspot });
const result = await invoke(
local.handler,
'POST',
'/api/works/ai-hardware/provisioning-hotspots/connect',
{ candidate_id: 'candidate_123' },
);
expect(result.status).toBe(200);
expect(result.payload).toEqual({
success: false,
status: 502,
code: 'AI_HARDWARE_HOTSPOT_CONNECT_FAILED',
error: 'The Robot hotspot could not be connected',
retryable: false,
});
expect(JSON.stringify(result.payload)).not.toContain('native profile secret');
expect(local.getAccessToken).not.toHaveBeenCalled();
expect(local.fetchImpl).not.toHaveBeenCalled();
});
it('proxies only the fixed catalog query and projects its safe DTO', async () => {
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse({
schema_version: 1,