docs: add Sub2API monitor implementation plan
This commit is contained in:
580
docs/plans/2026-06-23-sub2api-monitor-implementation.md
Normal file
580
docs/plans/2026-06-23-sub2api-monitor-implementation.md
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
# Sub2API Monitor Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Build a runnable native Android prototype with a Compose configuration screen and a Glance home-screen widget for monitoring Sub2API using mock data.
|
||||||
|
|
||||||
|
**Architecture:** Create a single-module Android app. Keep business state in small Kotlin domain models and repository interfaces, persist configuration and cached widget state through DataStore, render the app with Compose, and render the home-screen widget with Glance. The first data source is mock-only, but the repository boundary must allow a future real Sub2API admin client to replace it.
|
||||||
|
|
||||||
|
**Tech Stack:** Kotlin, Gradle Android plugin, Jetpack Compose, Jetpack Glance, DataStore, kotlinx.serialization, WorkManager boundary, JUnit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Scaffold Android Project
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `settings.gradle.kts`
|
||||||
|
- Create: `build.gradle.kts`
|
||||||
|
- Create: `gradle.properties`
|
||||||
|
- Create: `app/build.gradle.kts`
|
||||||
|
- Create: `app/src/main/AndroidManifest.xml`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/MainActivity.kt`
|
||||||
|
- Create: `app/src/test/java/com/sub2api/monitor/SmokeTest.kt`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `app/src/test/java/com/sub2api/monitor/SmokeTest.kt`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
package com.sub2api.monitor
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SmokeTest {
|
||||||
|
@Test
|
||||||
|
fun projectRunsUnitTests() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify project wiring**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest`
|
||||||
|
|
||||||
|
Expected before scaffolding is complete: command fails because no Android project or Gradle wrapper exists.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Create the Gradle Android project, Android manifest, and a minimal `MainActivity` that renders a Compose `Text("Sub2API Monitor")`.
|
||||||
|
|
||||||
|
Use package name:
|
||||||
|
|
||||||
|
```text
|
||||||
|
com.sub2api.monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
Use minimum SDK 26 and target/compile SDK available in the local Android toolchain.
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest`
|
||||||
|
|
||||||
|
Expected: `BUILD SUCCESSFUL`.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add settings.gradle.kts build.gradle.kts gradle.properties app
|
||||||
|
git commit -m "chore: scaffold Android app"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add Domain Models and Formatting
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/domain/Sub2ApiModels.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/domain/MetricFormatters.kt`
|
||||||
|
- Create: `app/src/test/java/com/sub2api/monitor/domain/MetricFormattersTest.kt`
|
||||||
|
|
||||||
|
**Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
Create tests for formatting behavior:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
package com.sub2api.monitor.domain
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MetricFormattersTest {
|
||||||
|
@Test
|
||||||
|
fun formatsLargeTokenCounts() {
|
||||||
|
assertEquals("12.3K", formatTokens(12_345))
|
||||||
|
assertEquals("9.9M", formatTokens(9_876_543))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun formatsCurrencyWithDollarPrefix() {
|
||||||
|
assertEquals("$3.21", formatCurrency(3.214))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun formatsLatencyAndRates() {
|
||||||
|
assertEquals("248 ms", formatLatency(248))
|
||||||
|
assertEquals("42 RPM", formatRpm(42))
|
||||||
|
assertEquals("18.5K TPM", formatTpm(18_500))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*MetricFormattersTest"`
|
||||||
|
|
||||||
|
Expected: FAIL because formatter functions do not exist.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Create immutable domain data classes:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class Sub2ApiSnapshot(...)
|
||||||
|
data class RecentCall(...)
|
||||||
|
data class ModelUsage(...)
|
||||||
|
enum class ServiceStatus { Healthy, Degraded, Down }
|
||||||
|
```
|
||||||
|
|
||||||
|
Add formatter functions for tokens, currency, latency, RPM, TPM, and display time.
|
||||||
|
|
||||||
|
**Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*MetricFormattersTest"`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/src/main/java/com/sub2api/monitor/domain app/src/test/java/com/sub2api/monitor/domain
|
||||||
|
git commit -m "feat: add monitoring domain models"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add Configuration Storage
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/data/AppConfig.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/data/ConfigRepository.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/data/DataStoreConfigRepository.kt`
|
||||||
|
- Create: `app/src/test/java/com/sub2api/monitor/data/ConfigRepositoryTest.kt`
|
||||||
|
|
||||||
|
**Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
Test pure configuration validation first:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
package com.sub2api.monitor.data
|
||||||
|
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ConfigRepositoryTest {
|
||||||
|
@Test
|
||||||
|
fun detectsMissingConfiguration() {
|
||||||
|
assertFalse(AppConfig().isConfigured)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun detectsSavedConfiguration() {
|
||||||
|
val config = AppConfig(
|
||||||
|
baseUrl = "https://sub2api.example.com",
|
||||||
|
adminKey = "secret",
|
||||||
|
refreshIntervalMinutes = 30,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(config.isConfigured)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*ConfigRepositoryTest"`
|
||||||
|
|
||||||
|
Expected: FAIL because `AppConfig` does not exist.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Create `AppConfig` with defaults and `isConfigured`.
|
||||||
|
|
||||||
|
Create repository interface:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
interface ConfigRepository {
|
||||||
|
val config: Flow<AppConfig>
|
||||||
|
suspend fun save(config: AppConfig)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement `DataStoreConfigRepository` with preferences DataStore. Keep admin key local and never expose it outside config/repository boundaries.
|
||||||
|
|
||||||
|
**Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*ConfigRepositoryTest"`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/src/main/java/com/sub2api/monitor/data app/src/test/java/com/sub2api/monitor/data
|
||||||
|
git commit -m "feat: add local configuration storage"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Add Mock Monitoring Repository
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/data/Sub2ApiRepository.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/data/MockSub2ApiRepository.kt`
|
||||||
|
- Create: `app/src/test/java/com/sub2api/monitor/data/MockSub2ApiRepositoryTest.kt`
|
||||||
|
|
||||||
|
**Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
package com.sub2api.monitor.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MockSub2ApiRepositoryTest {
|
||||||
|
@Test
|
||||||
|
fun returnsCompleteSnapshot() = runTest {
|
||||||
|
val snapshot = MockSub2ApiRepository().fetchSnapshot()
|
||||||
|
|
||||||
|
assertTrue(snapshot.todayTokens > 0)
|
||||||
|
assertTrue(snapshot.todayCost > 0.0)
|
||||||
|
assertEquals(5, snapshot.recentCalls.size)
|
||||||
|
assertEquals(4, snapshot.modelTop.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*MockSub2ApiRepositoryTest"`
|
||||||
|
|
||||||
|
Expected: FAIL because repository does not exist.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
interface Sub2ApiRepository {
|
||||||
|
suspend fun fetchSnapshot(): Sub2ApiSnapshot
|
||||||
|
suspend fun testConnection(config: AppConfig): Result<Unit>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement `MockSub2ApiRepository` returning realistic but deterministic data for the widget and app.
|
||||||
|
|
||||||
|
**Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*MockSub2ApiRepositoryTest"`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/src/main/java/com/sub2api/monitor/data app/src/test/java/com/sub2api/monitor/data
|
||||||
|
git commit -m "feat: add mock Sub2API repository"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Add Widget State and Refresh Behavior
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/widget/WidgetState.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/widget/WidgetStateRepository.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/widget/Sub2ApiWidgetViewModel.kt`
|
||||||
|
- Create: `app/src/test/java/com/sub2api/monitor/widget/Sub2ApiWidgetViewModelTest.kt`
|
||||||
|
|
||||||
|
**Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
package com.sub2api.monitor.widget
|
||||||
|
|
||||||
|
import com.sub2api.monitor.data.AppConfig
|
||||||
|
import com.sub2api.monitor.data.MockSub2ApiRepository
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class Sub2ApiWidgetViewModelTest {
|
||||||
|
@Test
|
||||||
|
fun missingConfigShowsSetupPrompt() = runTest {
|
||||||
|
val state = buildWidgetState(AppConfig(), null, null)
|
||||||
|
assertFalse(state.isConfigured)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun failedRefreshPreservesLastSuccessfulSnapshot() = runTest {
|
||||||
|
val previous = MockSub2ApiRepository().fetchSnapshot()
|
||||||
|
val state = buildWidgetState(
|
||||||
|
config = AppConfig("https://example.com", "secret", 30),
|
||||||
|
lastSuccessfulSnapshot = previous,
|
||||||
|
errorMessage = "Network error",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(state.isConfigured)
|
||||||
|
assertNotNull(state.snapshot)
|
||||||
|
assertTrue(state.hasError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*Sub2ApiWidgetViewModelTest"`
|
||||||
|
|
||||||
|
Expected: FAIL because widget state functions do not exist.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Create widget state models:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class Sub2ApiWidgetState(
|
||||||
|
val isConfigured: Boolean,
|
||||||
|
val snapshot: Sub2ApiSnapshot?,
|
||||||
|
val errorMessage: String?,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `buildWidgetState` to map config, last successful snapshot, and error into renderable state. Add repository methods for reading/writing last successful snapshot and last error.
|
||||||
|
|
||||||
|
**Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*Sub2ApiWidgetViewModelTest"`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/src/main/java/com/sub2api/monitor/widget app/src/test/java/com/sub2api/monitor/widget
|
||||||
|
git commit -m "feat: add widget state handling"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Build Compose Configuration Screen
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/ui/ConfigScreen.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/ui/ConfigViewModel.kt`
|
||||||
|
- Modify: `app/src/main/java/com/sub2api/monitor/MainActivity.kt`
|
||||||
|
- Create: `app/src/test/java/com/sub2api/monitor/ui/ConfigViewModelTest.kt`
|
||||||
|
|
||||||
|
**Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
Test ViewModel-level behavior:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
package com.sub2api.monitor.ui
|
||||||
|
|
||||||
|
import com.sub2api.monitor.data.AppConfig
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ConfigViewModelTest {
|
||||||
|
@Test
|
||||||
|
fun updatesFormFields() = runTest {
|
||||||
|
val state = ConfigUiState()
|
||||||
|
.withBaseUrl("https://sub2api.example.com")
|
||||||
|
.withAdminKey("secret")
|
||||||
|
.withRefreshInterval("30")
|
||||||
|
|
||||||
|
assertEquals("https://sub2api.example.com", state.baseUrl)
|
||||||
|
assertEquals("secret", state.adminKey)
|
||||||
|
assertEquals("30", state.refreshIntervalMinutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*ConfigViewModelTest"`
|
||||||
|
|
||||||
|
Expected: FAIL because UI state does not exist.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Create a Compose screen with:
|
||||||
|
|
||||||
|
- Title `Sub2API Monitor`
|
||||||
|
- URL text field
|
||||||
|
- Password admin key field
|
||||||
|
- Refresh interval input
|
||||||
|
- Test connection button
|
||||||
|
- Save configuration button
|
||||||
|
- Status message area
|
||||||
|
|
||||||
|
Use white/soft dashboard styling consistent with the widget. Wire `MainActivity` to display `ConfigScreen`.
|
||||||
|
|
||||||
|
**Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*ConfigViewModelTest"`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/src/main/java/com/sub2api/monitor/ui app/src/main/java/com/sub2api/monitor/MainActivity.kt app/src/test/java/com/sub2api/monitor/ui
|
||||||
|
git commit -m "feat: add configuration screen"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Build Glance Widget UI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/widget/Sub2ApiMonitorWidget.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/widget/Sub2ApiMonitorWidgetReceiver.kt`
|
||||||
|
- Create: `app/src/main/res/xml/sub2api_monitor_widget.xml`
|
||||||
|
- Modify: `app/src/main/AndroidManifest.xml`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Add a JVM test for the widget content source:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
package com.sub2api.monitor.widget
|
||||||
|
|
||||||
|
import com.sub2api.monitor.data.AppConfig
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class WidgetStateTextTest {
|
||||||
|
@Test
|
||||||
|
fun missingConfigUsesSetupPrompt() {
|
||||||
|
val state = buildWidgetState(AppConfig(), null, null)
|
||||||
|
assertEquals("请先配置 Sub2API", state.primaryMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*WidgetStateTextTest"`
|
||||||
|
|
||||||
|
Expected: FAIL because `primaryMessage` does not exist.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Add `primaryMessage` to widget state and build the Glance widget:
|
||||||
|
|
||||||
|
- White translucent rounded card.
|
||||||
|
- Header with title, last update, refresh action.
|
||||||
|
- Metric blocks for today's usage, cost, requests, status.
|
||||||
|
- Secondary metrics.
|
||||||
|
- Recent calls list.
|
||||||
|
- Model TOP4 list.
|
||||||
|
- Lifetime totals.
|
||||||
|
- Error banner when last refresh failed.
|
||||||
|
|
||||||
|
Register widget receiver and widget provider XML in the manifest.
|
||||||
|
|
||||||
|
**Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*WidgetStateTextTest"`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/src/main/java/com/sub2api/monitor/widget app/src/main/res/xml/sub2api_monitor_widget.xml app/src/main/AndroidManifest.xml
|
||||||
|
git commit -m "feat: add Sub2API home screen widget"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Add Refresh Actions and Scheduler Boundary
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/widget/RefreshWidgetAction.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/sync/RefreshScheduler.kt`
|
||||||
|
- Create: `app/src/main/java/com/sub2api/monitor/sync/WidgetRefreshWorker.kt`
|
||||||
|
- Modify: `app/src/main/java/com/sub2api/monitor/widget/Sub2ApiMonitorWidget.kt`
|
||||||
|
- Modify: `app/src/main/java/com/sub2api/monitor/ui/ConfigViewModel.kt`
|
||||||
|
- Create: `app/src/test/java/com/sub2api/monitor/sync/RefreshSchedulerTest.kt`
|
||||||
|
|
||||||
|
**Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
package com.sub2api.monitor.sync
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class RefreshSchedulerTest {
|
||||||
|
@Test
|
||||||
|
fun clampsRefreshIntervalToWorkManagerMinimum() {
|
||||||
|
assertEquals(15, sanitizeRefreshIntervalMinutes(5))
|
||||||
|
assertEquals(30, sanitizeRefreshIntervalMinutes(30))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*RefreshSchedulerTest"`
|
||||||
|
|
||||||
|
Expected: FAIL because scheduler helpers do not exist.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
Add manual refresh action for the widget. Add a scheduler boundary with WorkManager-compatible interval clamping. When configuration is saved, enqueue or update the periodic refresh request.
|
||||||
|
|
||||||
|
For the prototype, the worker can call the mock repository and write cached widget state.
|
||||||
|
|
||||||
|
**Step 4: Run tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest --tests "*RefreshSchedulerTest"`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/src/main/java/com/sub2api/monitor/sync app/src/main/java/com/sub2api/monitor/widget app/src/main/java/com/sub2api/monitor/ui app/src/test/java/com/sub2api/monitor/sync
|
||||||
|
git commit -m "feat: add widget refresh actions"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Final Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify only if verification reveals issues.
|
||||||
|
|
||||||
|
**Step 1: Run unit tests**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:testDebugUnitTest`
|
||||||
|
|
||||||
|
Expected: `BUILD SUCCESSFUL`.
|
||||||
|
|
||||||
|
**Step 2: Run Android build**
|
||||||
|
|
||||||
|
Run: `./gradlew :app:assembleDebug`
|
||||||
|
|
||||||
|
Expected: `BUILD SUCCESSFUL` and debug APK generated.
|
||||||
|
|
||||||
|
**Step 3: Inspect repository status**
|
||||||
|
|
||||||
|
Run: `git status --short`
|
||||||
|
|
||||||
|
Expected: no unintended files. Commit any intentional final fixes.
|
||||||
|
|
||||||
|
**Step 4: Report**
|
||||||
|
|
||||||
|
Summarize implemented features, verification commands, and any limitations such
|
||||||
|
as simulator/device testing not being run.
|
||||||
|
|
||||||
Reference in New Issue
Block a user