Initial commit: Voice controlled device application, specs and Gitea Actions workflow

This commit is contained in:
2026-08-10 08:26:00 +00:00
commit d449469116
69 changed files with 3209 additions and 0 deletions
@@ -0,0 +1,41 @@
package com.example.voiceapp.domain
import com.example.voiceapp.domain.executor.GetTimeDateExecutor
import com.example.voiceapp.domain.executor.SetAlarmTimerExecutor
import com.example.voiceapp.domain.model.CommandId
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class CommandParserTest {
private lateinit var parser: CommandParser
@Before
fun setUp() {
val definitions = listOf(
CommandDefinition(CommandId.GET_TIME_DATE, listOf(Regex(".*(今何時|何時ですか|日付|今日).*")), GetTimeDateExecutor()),
CommandDefinition(CommandId.SET_ALARM_TIMER, listOf(Regex(".*(\\d+)\\s*(分|秒|時間)の?タイマー.*"), Regex(".*(\\d+)\\s*時にアラーム.*")), SetAlarmTimerExecutor())
)
parser = CommandParser(definitions)
}
@Test
fun parse_getTime_returnsGetTimeDateCommand() {
val result = parser.parse("今何時?")
assertEquals(CommandId.GET_TIME_DATE, result.commandId)
}
@Test
fun parse_setTimer_withKanji_returnsSetAlarmTimerCommand() {
val result = parser.parse("三分タイマーかけて")
assertEquals(CommandId.SET_ALARM_TIMER, result.commandId)
assertEquals("3", result.extractedParameters["value"])
assertEquals("分", result.extractedParameters["unit"])
}
@Test
fun parse_unknownText_returnsUnknownFallback() {
val result = parser.parse("こんにちは")
assertEquals(CommandId.UNKNOWN_FALLBACK, result.commandId)
}
}
@@ -0,0 +1,30 @@
package com.example.voiceapp.domain
import com.example.voiceapp.domain.executor.GetTimeDateExecutor
import com.example.voiceapp.domain.model.CommandId
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class ExecuteCommandUseCaseTest {
private lateinit var useCase: ExecuteCommandUseCase
@Before
fun setUp() {
val definitions = listOf(
CommandDefinition(CommandId.GET_TIME_DATE, listOf(Regex(".*(今何時|何時ですか|日付|今日).*")), GetTimeDateExecutor())
)
val parser = CommandParser(definitions)
useCase = ExecuteCommandUseCase(parser)
}
@Test
fun invoke_getTime_returnsSuccessResult() = runTest {
val result = useCase("今何時")
assertEquals(CommandId.GET_TIME_DATE, result.commandId)
assertTrue(result.isSuccess)
assertTrue(result.responseText.contains("現在"))
}
}
@@ -0,0 +1,16 @@
package com.example.voiceapp.domain
import com.example.voiceapp.domain.converter.KanjiToNumberConverter
import org.junit.Assert.assertEquals
import org.junit.Test
class KanjiToNumberConverterTest {
@Test
fun convert_kanjiDigits_convertsToNumbers() {
assertEquals("3分", KanjiToNumberConverter.convert("三分"))
assertEquals("7時", KanjiToNumberConverter.convert("七時"))
assertEquals("15分", KanjiToNumberConverter.convert("十五分"))
assertEquals("3分", KanjiToNumberConverter.convert("3 minutes"))
}
}
@@ -0,0 +1,68 @@
package com.example.voiceapp.ui
import app.cash.turbine.test
import com.example.voiceapp.domain.ExecuteCommandUseCase
import com.example.voiceapp.domain.model.CommandId
import com.example.voiceapp.domain.model.CommandResult
import com.example.voiceapp.voice.ITextToSpeech
import com.example.voiceapp.voice.IVoiceRecognizer
import com.example.voiceapp.voice.TtsStatus
import com.example.voiceapp.voice.VoiceRecognitionState
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private val useCase: ExecuteCommandUseCase = mockk()
private val recognizer: IVoiceRecognizer = mockk(relaxed = true)
private val tts: ITextToSpeech = mockk(relaxed = true)
private val recognizerState = MutableStateFlow<VoiceRecognitionState>(VoiceRecognitionState.Idle)
private val rmsDbState = MutableStateFlow(0f)
private val ttsStatusState = MutableStateFlow<TtsStatus>(TtsStatus.Idle)
private lateinit var viewModel: MainViewModel
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
every { recognizer.state } returns recognizerState
every { recognizer.rmsDb } returns rmsDbState
every { tts.status } returns ttsStatusState
viewModel = MainViewModel(useCase, recognizer, tts)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun processUserSpeech_updatesHistoryAndState() = runTest {
coEvery { useCase("今何時") } returns CommandResult(CommandId.GET_TIME_DATE, true, "現在は10時00分です。")
viewModel.processUserSpeech("今何時")
testDispatcher.scheduler.advanceUntilIdle()
viewModel.conversationHistory.test {
val history = awaitItem()
assertEquals(2, history.size)
assertEquals("今何時", history[0].text)
assertEquals("現在は10時00分です。", history[1].text)
}
}
}