fix(robot): scope devices to selected agent
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# Task: Fix Robot device selection and binding
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260816-robot-device-selection-f6a2
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260816-robot-device-selection-f6a2-robot-device-selection
|
||||
- Worktree: D:\Datas\OthersProjects\makelore-robot-device-selection-f6a2
|
||||
- Base commit: 3b4669722e0ea671909beb039da1e07c2483341a
|
||||
- Owner: codex-root
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Fix the Robot page so the device panel follows the currently selected agent.
|
||||
- Prove that binding a new activation code targets the selected agent while preserving the existing guided/manual binding flow.
|
||||
- Clarify that the current product adds devices through a six-digit activation code; nearby-device discovery remains out of scope.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- Treat devices as agent-scoped child resources in the UI while retaining the account-wide overview DTO.
|
||||
- Do not add a new backend route, automatic discovery, firmware behavior, or a second hardware module.
|
||||
- Preserve Main-owned provisioning, activation-code secrecy, idempotency, reassignment revisions, and the default-off guided Hotspot flow.
|
||||
- Use a deterministic page regression test that reproduces the exact stale-device symptom before changing production code.
|
||||
|
||||
## Project Context Loaded
|
||||
|
||||
- Task: `20260816-robot-device-selection-f6a2`, Feature mode, isolated worktree and branch, base `3b466972`.
|
||||
- Read: active task record, planning entry, project memory index, positioning, current state, decision index, system overview, business rules, success criteria, and the completed provisioning/hotspot-binding peer records.
|
||||
- Peer assessment: Guided Hotspot Binding overlaps the same page but is already merged into this base; its state machine and security constraints must be preserved. No active task owns this worktree or is implementing the same selected-agent device seam.
|
||||
- Gate result: Concurrent Task Gate and Planning Gate passed.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add a red page test with two agents and two devices that asserts the device panel follows the selected agent.
|
||||
2. Add a red binding test proving a new activation code is submitted with the selected agent ID.
|
||||
3. Filter the overview device projection by selected agent and tighten agent-scoped device copy without changing API contracts.
|
||||
4. Run focused tests, typecheck, scoped lint, diff checks, project-document gates, and an independent Sol review.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Reproduced the stale-device bug: the page projected every account device regardless of the selected agent, so changing agents refreshed configuration but not the device panel.
|
||||
- Filtered the device projection by `device.agent_id === selectedAgentId`; switching agents now changes both configuration and visible devices.
|
||||
- Added an agent-scoped device count to each agent row, matching Xiaozhi's verified `deviceCount` semantics without adding a new API.
|
||||
- Clarified the device panel, empty state, and binding action as belonging to the current agent.
|
||||
- Made the binding target immutable for the dialog lifetime: it shows the current agent read-only, while the editable agent selector remains exclusive to existing-device reassignment.
|
||||
- Proved that opening the binding flow after selecting the second agent submits the new six-digit activation code with that agent's ID.
|
||||
- Kept existing atomic reassignment, guided Hotspot provisioning, activation-code secrecy, and account-wide overview contracts unchanged. No backend or firmware code was changed.
|
||||
|
||||
## Verification
|
||||
|
||||
- RED: the new two-agent/two-device page regression failed against the previous implementation because the first agent panel also rendered the second agent's device.
|
||||
- PASS: `pnpm exec vitest run tests/unit/ai-hardware-page.test.tsx` — 1 file, 39 tests.
|
||||
- PASS: `pnpm exec vitest run tests/unit/ai-hardware-page.test.tsx tests/unit/ai-hardware-api.test.ts tests/unit/ai-hardware-routes.test.ts` — 3 files, 92 tests.
|
||||
- PASS: `pnpm run typecheck`.
|
||||
- PASS: scoped ESLint for the changed page and test.
|
||||
- PASS: `git diff --check` (line-ending warnings only).
|
||||
- Source comparison confirmed Xiaozhi agent cards expose per-agent device totals and device management filters by agent; adding a normal device uses a six-digit activation code. Makelore derives the same count from its safe overview DTO.
|
||||
- Independent Sol review initially returned FAIL with one P1 and one P2: the binding target remained editable and the short `aria-label` hid the scoped visible action. Both were corrected, the complete focused suite was rerun, and re-review returned PASS with no P0-P3 findings.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Automatic nearby-device discovery and management-side manual MAC insertion remain intentionally unsupported by the public Makelore/Works contract.
|
||||
- A physical multi-device smoke should bind a second real device with its own fresh activation code, switch between two agents, and verify atomic reassignment before the next packaged release.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- None recorded.
|
||||
@@ -326,7 +326,10 @@ export function AiHardware() {
|
||||
}, [configReloadKey, selectedAgentId]);
|
||||
|
||||
const selectedAgent = overview?.agents.find((item) => item.id === selectedAgentId) ?? null;
|
||||
const devices = useMemo(() => overview?.devices ?? [], [overview]);
|
||||
const devices = useMemo(
|
||||
() => overview?.devices.filter((device) => device.agent_id === selectedAgentId) ?? [],
|
||||
[overview, selectedAgentId],
|
||||
);
|
||||
const resetDialog = () => {
|
||||
setDialogError(null); setBusy(false);
|
||||
clearCreateOperation(); clearBindOperation(); clearConfigOperation(); clearAssignmentOperation();
|
||||
@@ -625,7 +628,10 @@ export function AiHardware() {
|
||||
<p className="text-sm text-muted-foreground">等待机器人联网并播报新的 6 位激活码。没有听到时请检查设备网络,或重新开始配网。</p>
|
||||
) : null}
|
||||
<div><Label htmlFor="hardware-activation-code">6 位激活码</Label><Input ref={activationCodeInputRef} id="hardware-activation-code" autoFocus type="text" inputMode="numeric" autoComplete="off" maxLength={6} onInput={(e) => { e.currentTarget.value = e.currentTarget.value.replace(/\D/g, '').slice(0, 6); }} /></div>
|
||||
<AgentSelect agents={overview.agents} value={dialogAgentId} onChange={(value) => { setDialogAgentId(value); clearBindOperation(); }} />
|
||||
<div className="rounded-xl bg-surface-subtle px-3 py-2">
|
||||
<p className="text-xs text-muted-foreground">绑定到当前智能体</p>
|
||||
<p className="mt-1 text-sm font-semibold">{overview.agents.find((agent) => agent.id === dialogAgentId)?.name ?? '未选择'}</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">激活码只用于本次绑定,关闭窗口后会立即清除。</p>
|
||||
</div>
|
||||
);
|
||||
@@ -665,10 +671,13 @@ export function AiHardware() {
|
||||
<Card className="mx-auto max-w-md text-center"><CardHeader><CardTitle>创建第一个智能体</CardTitle><CardDescription>{overview.status === 'unprovisioned' ? '创建智能体将同时开通你的机器人工作台。' : '智能体创建后,才能把机器人设备绑定给它。'}</CardDescription></CardHeader><CardContent><Button onClick={() => { resetDialog(); setCreateOpen(true); }}><Plus className="mr-2 h-4 w-4" />创建智能体</Button></CardContent></Card>
|
||||
) : (
|
||||
<div className="grid gap-5 lg:grid-cols-[minmax(240px,0.7fr)_minmax(0,1.3fr)]">
|
||||
<Card><CardHeader className="flex-row items-center justify-between"><div><CardTitle>智能体</CardTitle><CardDescription>{overview.agents.length} 个</CardDescription></div><Button size="icon" aria-label="创建智能体" onClick={() => { resetDialog(); setCreateOpen(true); }}><Plus className="h-4 w-4" /></Button></CardHeader><CardContent className="space-y-2">{overview.agents.map((agent) => <button key={agent.id} type="button" aria-pressed={agent.id === selectedAgentId} onClick={() => setSelectedAgentId(agent.id)} className={`motion-press flex min-h-12 w-full items-center gap-3 rounded-xl px-3 py-2 text-left ${agent.id === selectedAgentId ? 'bg-brand-soft' : 'bg-surface-subtle hover:bg-surface-tertiary'}`}><Bot className="h-4 w-4 shrink-0" /><span className="min-w-0 flex-1"><span className="block truncate text-sm font-semibold">{agent.name}</span><span title={agent.id} className="block text-xs tabular-nums text-muted-foreground">{shortId(agent.id)} · r{agent.config_revision}</span></span></button>)}</CardContent></Card>
|
||||
<Card><CardHeader className="flex-row items-center justify-between"><div><CardTitle>智能体</CardTitle><CardDescription>{overview.agents.length} 个</CardDescription></div><Button size="icon" aria-label="创建智能体" onClick={() => { resetDialog(); setCreateOpen(true); }}><Plus className="h-4 w-4" /></Button></CardHeader><CardContent className="space-y-2">{overview.agents.map((agent) => {
|
||||
const deviceCount = overview.devices.filter((device) => device.agent_id === agent.id).length;
|
||||
return <button key={agent.id} type="button" aria-pressed={agent.id === selectedAgentId} onClick={() => setSelectedAgentId(agent.id)} className={`motion-press flex min-h-12 w-full items-center gap-3 rounded-xl px-3 py-2 text-left ${agent.id === selectedAgentId ? 'bg-brand-soft' : 'bg-surface-subtle hover:bg-surface-tertiary'}`}><Bot className="h-4 w-4 shrink-0" /><span className="min-w-0 flex-1"><span className="block truncate text-sm font-semibold">{agent.name}</span><span title={agent.id} className="block text-xs tabular-nums text-muted-foreground">{shortId(agent.id)} · r{agent.config_revision} · {deviceCount} 台设备</span></span></button>;
|
||||
})}</CardContent></Card>
|
||||
<div className="space-y-5">
|
||||
<Card><CardHeader className="flex-row items-center justify-between"><div><CardTitle>{selectedAgent?.name ?? '智能体配置'}</CardTitle><CardDescription>基础对话和语音设置</CardDescription></div><Button variant="outline" disabled={configLoading || !config} onClick={openConfigurationEditor}>{configLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Settings2 className="mr-2 h-4 w-4" />}编辑配置</Button></CardHeader><CardContent>{config ? <dl className="grid gap-3 text-sm sm:grid-cols-2"><div><dt className="text-muted-foreground">语言</dt><dd>{config.language || config.lang_code || '未设置'}</dd></div><div><dt className="text-muted-foreground">语音</dt><dd>{config.tts_voice_id || '未设置'}</dd></div><div className="sm:col-span-2"><dt className="text-muted-foreground">系统提示</dt><dd className="mt-1 whitespace-pre-wrap">{config.system_prompt || '未设置'}</dd></div></dl> : configLoading ? <FeedbackState state="loading" title="正在读取配置" /> : configLoadFailed ? <FeedbackState state="error" title="无法读取智能体配置" description="请检查服务连接后重试。" action={<Button variant="outline" onClick={() => setConfigReloadKey((value) => value + 1)}>重试读取配置</Button>} /> : null}</CardContent></Card>
|
||||
<Card><CardHeader className="flex-row items-center justify-between"><div><CardTitle>设备</CardTitle><CardDescription>{devices.length} 台已绑定设备</CardDescription></div><Button onClick={openBindingDialog}><Link2 className="mr-2 h-4 w-4" />绑定设备</Button></CardHeader><CardContent>{devices.length ? <div className="space-y-2">{devices.map((device) => <div key={device.id} className="flex min-h-12 items-center justify-between gap-3 rounded-xl bg-surface-subtle px-3 py-2"><span className="min-w-0"><span className="block text-sm font-semibold">设备 {shortId(device.id)}</span><span title={device.id} className="text-xs tabular-nums text-muted-foreground">指派 r{device.assignment_revision}</span></span><Button variant="outline" size="sm" onClick={() => void openAssignment(device)}>重新指派</Button></div>)}</div> : <FeedbackState state="empty" title="还没有绑定设备" description="使用设备上的 6 位激活码完成绑定。" />}</CardContent></Card>
|
||||
<Card><CardHeader className="flex-row items-center justify-between"><div><CardTitle>当前智能体设备</CardTitle><CardDescription>{devices.length} 台设备绑定到当前智能体</CardDescription></div><Button onClick={openBindingDialog}><Link2 className="mr-2 h-4 w-4" />为当前智能体绑定设备</Button></CardHeader><CardContent>{devices.length ? <div className="space-y-2">{devices.map((device) => <div key={device.id} className="flex min-h-12 items-center justify-between gap-3 rounded-xl bg-surface-subtle px-3 py-2"><span className="min-w-0"><span className="block text-sm font-semibold">设备 {shortId(device.id)}</span><span title={device.id} className="text-xs tabular-nums text-muted-foreground">指派 r{device.assignment_revision}</span></span><Button variant="outline" size="sm" onClick={() => void openAssignment(device)}>重新指派</Button></div>)}</div> : <FeedbackState state="empty" title="当前智能体还没有绑定设备" description="使用设备上的 6 位激活码绑定到当前智能体。" />}</CardContent></Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AiHardware } from '@/pages/AiHardware';
|
||||
import {
|
||||
@@ -154,7 +154,7 @@ describe('AI hardware page', () => {
|
||||
status: 422, code: 'ai_hardware_activation_code_invalid', message: `invalid ${secret}`,
|
||||
}));
|
||||
render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
const input = screen.getByLabelText('6 位激活码');
|
||||
fireEvent.change(input, { target: { value: '12x' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定' }));
|
||||
@@ -171,13 +171,45 @@ describe('AI hardware page', () => {
|
||||
|
||||
it('keeps the existing direct-code binding experience when guided provisioning is disabled', async () => {
|
||||
render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
|
||||
expect(screen.getByRole('heading', { name: '输入 6 位激活码' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '开始引导配网' })).not.toBeInTheDocument();
|
||||
expect(api.openAiHardwareProvisioningPortal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows and binds devices for the currently selected agent', async () => {
|
||||
const agentOneDevice = { id: 'device-one', agent_id: agentOne.id, assignment_revision: 3 };
|
||||
const agentTwoDevice = { id: 'device-two', agent_id: agentTwo.id, assignment_revision: 1 };
|
||||
api.getAiHardwareOverview.mockResolvedValueOnce({
|
||||
status: 'active',
|
||||
agents: [agentOne, agentTwo],
|
||||
devices: [agentOneDevice, agentTwoDevice],
|
||||
});
|
||||
api.bindAiHardwareDevice.mockResolvedValueOnce(agentTwoDevice);
|
||||
|
||||
render(<AiHardware />);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /客厅助手.*1 台设备/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /书房助手.*1 台设备/ })).toBeInTheDocument();
|
||||
expect(await screen.findByText('设备 device-one')).toBeInTheDocument();
|
||||
expect(screen.queryByText('设备 device-two')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /书房助手/ }));
|
||||
expect(await screen.findByText('设备 device-two')).toBeInTheDocument();
|
||||
expect(screen.queryByText('设备 device-one')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
const bindingDialog = screen.getByRole('dialog');
|
||||
expect(within(bindingDialog).queryByLabelText('智能体')).not.toBeInTheDocument();
|
||||
expect(within(bindingDialog).getByText('绑定到当前智能体')).toBeInTheDocument();
|
||||
expect(within(bindingDialog).getByText('书房助手')).toBeInTheDocument();
|
||||
fireEvent.change(screen.getByLabelText('6 位激活码'), { target: { value: '654321' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定' }));
|
||||
|
||||
await waitFor(() => expect(api.bindAiHardwareDevice).toHaveBeenCalledWith('654321', agentTwo.id));
|
||||
});
|
||||
|
||||
it('guides the existing hotspot flow, locks navigation while binding, and avoids online claims', async () => {
|
||||
api.getAiHardwareProvisioningCapabilities.mockResolvedValueOnce({ guidedHotspotBinding: true });
|
||||
const binding = deferred<typeof device>();
|
||||
@@ -185,7 +217,7 @@ describe('AI hardware page', () => {
|
||||
render(<AiHardware />);
|
||||
await waitFor(() => expect(api.getAiHardwareProvisioningCapabilities).toHaveBeenCalled());
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '开始引导配网' }));
|
||||
expect(screen.getByText(/设备热点没有加密保护/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '机器人已进入配网模式' }));
|
||||
@@ -219,7 +251,7 @@ describe('AI hardware page', () => {
|
||||
api.getAiHardwareProvisioningCapabilities.mockResolvedValueOnce({ guidedHotspotBinding: true });
|
||||
render(<AiHardware />);
|
||||
await waitFor(() => expect(api.getAiHardwareProvisioningCapabilities).toHaveBeenCalled());
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '我已有 6 位激活码' }));
|
||||
expect(screen.getByRole('heading', { name: '输入 6 位激活码' })).toBeInTheDocument();
|
||||
@@ -255,12 +287,12 @@ describe('AI hardware page', () => {
|
||||
api.getAiHardwareProvisioningCapabilities.mockResolvedValueOnce({ guidedHotspotBinding: true });
|
||||
render(<AiHardware />);
|
||||
await waitFor(() => expect(api.getAiHardwareProvisioningCapabilities).toHaveBeenCalled());
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始引导配网' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '机器人已进入配网模式' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }));
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
expect(screen.getByRole('heading', { name: '绑定机器人设备' })).toBeInTheDocument();
|
||||
expect(api.openAiHardwareProvisioningPortal).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -271,7 +303,7 @@ describe('AI hardware page', () => {
|
||||
api.openAiHardwareProvisioningPortal.mockReturnValueOnce(opening.promise);
|
||||
render(<AiHardware />);
|
||||
await waitFor(() => expect(api.getAiHardwareProvisioningCapabilities).toHaveBeenCalled());
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始引导配网' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '机器人已进入配网模式' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '电脑已连接设备热点' }));
|
||||
@@ -284,7 +316,7 @@ describe('AI hardware page', () => {
|
||||
opening.resolve({ opened: true });
|
||||
fireEvent.click(await screen.findByRole('button', { name: '我已完成设备配网' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
expect(screen.getByRole('heading', { name: '绑定机器人设备' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -304,7 +336,7 @@ describe('AI hardware page', () => {
|
||||
.mockResolvedValueOnce({ opened: true });
|
||||
render(<AiHardware />);
|
||||
await waitFor(() => expect(api.getAiHardwareProvisioningCapabilities).toHaveBeenCalled());
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始引导配网' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '机器人已进入配网模式' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '电脑已连接设备热点' }));
|
||||
@@ -320,7 +352,7 @@ describe('AI hardware page', () => {
|
||||
await waitFor(() => expect(api.openAiHardwareProvisioningPortal).toHaveBeenCalledTimes(2));
|
||||
fireEvent.click(screen.getByRole('button', { name: '我已完成设备配网' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
expect(screen.getByRole('button', { name: '开始引导配网' })).toBeInTheDocument();
|
||||
expect(screen.queryByText(/不会撤销机器人已经保存的 Wi-Fi 设置/)).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -508,7 +540,7 @@ describe('AI hardware page', () => {
|
||||
status: 409, code, message: 'secret detail activation=031425', retryAfterSeconds: 7,
|
||||
}));
|
||||
render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
fireEvent.change(screen.getByLabelText('6 位激活码'), { target: { value: '031425' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定' }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(expected);
|
||||
@@ -520,7 +552,7 @@ describe('AI hardware page', () => {
|
||||
status: 409, code: 'ai_hardware_operation_in_progress', message: 'private', retryAfterSeconds: 7,
|
||||
}));
|
||||
render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
fireEvent.change(screen.getByLabelText('6 位激活码'), { target: { value: '031425' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定' }));
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('7 秒后重试');
|
||||
@@ -536,7 +568,7 @@ describe('AI hardware page', () => {
|
||||
message: 'private upstream state',
|
||||
}));
|
||||
render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
fireEvent.change(screen.getByLabelText('6 位激活码'), { target: { value: '031425' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定' }));
|
||||
|
||||
@@ -554,7 +586,7 @@ describe('AI hardware page', () => {
|
||||
}))
|
||||
.mockResolvedValueOnce(device);
|
||||
render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
const input = screen.getByLabelText('6 位激活码');
|
||||
fireEvent.change(input, { target: { value: '031425' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定' }));
|
||||
@@ -580,14 +612,14 @@ describe('AI hardware page', () => {
|
||||
}))
|
||||
.mockResolvedValueOnce(device);
|
||||
const first = render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
fireEvent.change(screen.getByLabelText('6 位激活码'), { target: { value: '031425' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定' }));
|
||||
await screen.findByLabelText('6 位激活码');
|
||||
first.unmount();
|
||||
|
||||
render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
fireEvent.change(screen.getByLabelText('6 位激活码'), { target: { value: '654321' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '绑定' }));
|
||||
|
||||
@@ -605,7 +637,7 @@ describe('AI hardware page', () => {
|
||||
}))
|
||||
.mockResolvedValueOnce(device);
|
||||
render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
const input = screen.getByLabelText('6 位激活码');
|
||||
|
||||
fireEvent.change(input, { target: { value: '031425' } });
|
||||
@@ -634,7 +666,7 @@ describe('AI hardware page', () => {
|
||||
}))
|
||||
.mockResolvedValueOnce(device);
|
||||
render(<AiHardware />);
|
||||
fireEvent.click(await screen.findByRole('button', { name: '绑定设备' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '为当前智能体绑定设备' }));
|
||||
const input = screen.getByLabelText('6 位激活码');
|
||||
|
||||
fireEvent.change(input, { target: { value: '031425' } });
|
||||
|
||||
Reference in New Issue
Block a user