fix: enable voice input for fresh agent conversations
This commit is contained in:
@@ -1010,6 +1010,124 @@ describe('OpencodeChatPanel', () => {
|
||||
expect(screen.queryByText('课程槽位')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('records voice into a fresh Agent conversation without eagerly creating a session', async () => {
|
||||
installMockVoiceWavTranscoder();
|
||||
const activeProject = {
|
||||
id: 'prj_fresh_voice',
|
||||
path: 'D:/repo/fresh-voice',
|
||||
name: 'fresh-voice',
|
||||
createdAt: '2026-08-17T00:00:00.000Z',
|
||||
updatedAt: '2026-08-17T00:00:00.000Z',
|
||||
lastOpenedAt: '2026-08-17T00:00:00.000Z',
|
||||
};
|
||||
const config = createConfiguredProjectConfig();
|
||||
const getUserMedia = vi.fn().mockResolvedValue({
|
||||
getTracks: vi.fn(() => [{ stop: vi.fn() }]),
|
||||
});
|
||||
|
||||
class MockMediaRecorder {
|
||||
public ondataavailable: ((event: { data: Blob }) => void) | null = null;
|
||||
public onstop: (() => void) | null = null;
|
||||
public state: RecordingState = 'inactive';
|
||||
|
||||
static isTypeSupported(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.state = 'recording';
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.state = 'inactive';
|
||||
this.ondataavailable?.({ data: new Blob(['audio bytes'], { type: 'audio/webm' }) });
|
||||
this.onstop?.();
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(navigator, 'mediaDevices', {
|
||||
configurable: true,
|
||||
value: { getUserMedia },
|
||||
});
|
||||
Object.defineProperty(window, 'MediaRecorder', {
|
||||
configurable: true,
|
||||
value: MockMediaRecorder,
|
||||
});
|
||||
useAuthStore.setState({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: null,
|
||||
accessToken: 'access-token',
|
||||
refreshToken: null,
|
||||
tokenType: 'Bearer',
|
||||
expiresAt: Date.now() + 60_000,
|
||||
user: {
|
||||
username: 'student',
|
||||
userId: '42',
|
||||
tenantId: null,
|
||||
deptId: null,
|
||||
authorities: [],
|
||||
},
|
||||
});
|
||||
useProjectConfigStore.setState({
|
||||
configsByProjectId: { [activeProject.id]: config },
|
||||
});
|
||||
useOpencodeStore.setState({
|
||||
status: { state: 'running', port: 4096, url: 'http://127.0.0.1:4096' },
|
||||
projects: [activeProject],
|
||||
activeProject,
|
||||
sessions: [],
|
||||
selectedSessionId: null,
|
||||
sessionStatuses: {},
|
||||
sessionMessages: [],
|
||||
});
|
||||
hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => {
|
||||
if (path === '/api/works/speech/transcriptions' && init?.method === 'POST') {
|
||||
return {
|
||||
success: true,
|
||||
transcription: {
|
||||
text: 'voice before first message',
|
||||
model: 'gpt-4o-mini-transcribe',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (path === '/api/opencode/status') {
|
||||
return { state: 'running', port: 4096, url: 'http://127.0.0.1:4096' };
|
||||
}
|
||||
if (path === '/api/opencode/projects') {
|
||||
return { projects: [activeProject], activeProject };
|
||||
}
|
||||
if (path === '/api/opencode/config-summary') {
|
||||
return { summary: { model: 'openai/gpt-5', smallModel: null, providerIds: ['openai'], providerCount: 1 } };
|
||||
}
|
||||
if (path === '/api/opencode/sessions') return { sessions: [] };
|
||||
if (path === '/api/opencode/sessions/status') return { statuses: {} };
|
||||
throw new Error(`Unexpected path ${path}`);
|
||||
});
|
||||
|
||||
render(<OpencodeChatPanel variant="main" />);
|
||||
|
||||
fireEvent.click(await screen.findByTestId('project-agent-chat-game-art'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('project-agent-chat-game-art')).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
expect(useOpencodeStore.getState().selectedSessionId).toBeNull();
|
||||
|
||||
const voiceButton = screen.getByTestId('opencode-voice-input-button');
|
||||
expect(voiceButton).toBeEnabled();
|
||||
fireEvent.click(voiceButton);
|
||||
await waitFor(() => expect(getUserMedia).toHaveBeenCalledWith({ audio: true }));
|
||||
fireEvent.click(voiceButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox')).toHaveValue('voice before first message');
|
||||
});
|
||||
expect(hostApiFetchMock.mock.calls.some(([path, init]) => (
|
||||
path === '/api/opencode/sessions'
|
||||
&& (init as RequestInit | undefined)?.method === 'POST'
|
||||
))).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps project-agent actions enabled when a pre-start status response is delayed', async () => {
|
||||
const activeProject = { id: 'prj_delayed_status', path: 'D:/repo/delayed-status', name: 'delayed-status', createdAt: '2026-07-12T00:00:00.000Z', updatedAt: '2026-07-12T00:00:00.000Z', lastOpenedAt: '2026-07-12T00:00:00.000Z' };
|
||||
const config = createConfiguredProjectConfig();
|
||||
|
||||
Reference in New Issue
Block a user