androjack-mcp 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +34 -0
  3. package/.github/pull_request_template.md +16 -0
  4. package/CONTRIBUTING.md +27 -0
  5. package/LICENSE +21 -0
  6. package/README.md +592 -0
  7. package/SECURITY.md +26 -0
  8. package/assets/AndroJack banner.png +0 -0
  9. package/assets/killer_argument.png +0 -0
  10. package/build/constants.js +412 -0
  11. package/build/http-server.js +163 -0
  12. package/build/http.js +151 -0
  13. package/build/index.js +553 -0
  14. package/build/install.js +379 -0
  15. package/build/logger.js +57 -0
  16. package/build/tools/api-level.js +170 -0
  17. package/build/tools/api36-compliance.js +282 -0
  18. package/build/tools/architecture.js +75 -0
  19. package/build/tools/build-publish.js +362 -0
  20. package/build/tools/component.js +90 -0
  21. package/build/tools/debugger.js +82 -0
  22. package/build/tools/gradle.js +234 -0
  23. package/build/tools/kmp.js +348 -0
  24. package/build/tools/kotlin-patterns.js +500 -0
  25. package/build/tools/large-screen.js +366 -0
  26. package/build/tools/m3-expressive.js +447 -0
  27. package/build/tools/navigation3.js +331 -0
  28. package/build/tools/ondevice-ai.js +283 -0
  29. package/build/tools/permissions.js +404 -0
  30. package/build/tools/play-policy.js +221 -0
  31. package/build/tools/scalability.js +621 -0
  32. package/build/tools/search.js +89 -0
  33. package/build/tools/testing.js +439 -0
  34. package/build/tools/wear.js +337 -0
  35. package/build/tools/xr.js +274 -0
  36. package/config/antigravity_mcp.json +32 -0
  37. package/config/claude_desktop_config.json +17 -0
  38. package/config/cursor_mcp.json +21 -0
  39. package/config/jetbrains_mcp.json +28 -0
  40. package/config/kiro_mcp.json +40 -0
  41. package/config/vscode_mcp.json +24 -0
  42. package/config/windsurf_mcp.json +18 -0
  43. package/package.json +51 -0
  44. package/src/constants.ts +436 -0
  45. package/src/http-server.ts +186 -0
  46. package/src/http.ts +190 -0
  47. package/src/index.ts +702 -0
  48. package/src/install.ts +441 -0
  49. package/src/logger.ts +67 -0
  50. package/src/tools/api-level.ts +198 -0
  51. package/src/tools/api36-compliance.ts +289 -0
  52. package/src/tools/architecture.ts +94 -0
  53. package/src/tools/build-publish.ts +379 -0
  54. package/src/tools/component.ts +106 -0
  55. package/src/tools/debugger.ts +111 -0
  56. package/src/tools/gradle.ts +288 -0
  57. package/src/tools/kmp.ts +352 -0
  58. package/src/tools/kotlin-patterns.ts +534 -0
  59. package/src/tools/large-screen.ts +391 -0
  60. package/src/tools/m3-expressive.ts +473 -0
  61. package/src/tools/navigation3.ts +338 -0
  62. package/src/tools/ondevice-ai.ts +287 -0
  63. package/src/tools/permissions.ts +445 -0
  64. package/src/tools/play-policy.ts +229 -0
  65. package/src/tools/scalability.ts +646 -0
  66. package/src/tools/search.ts +112 -0
  67. package/src/tools/testing.ts +460 -0
  68. package/src/tools/wear.ts +343 -0
  69. package/src/tools/xr.ts +278 -0
  70. package/tsconfig.json +17 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Tool 1 – android_official_search
3
+ *
4
+ * Searches developer.android.com, kotlinlang.org, and source.android.com
5
+ * restricted to authoritative domains only. Returns structured excerpts
6
+ * with source URLs so the LLM can cite them in generated code.
7
+ */
8
+
9
+ import { secureFetch, extractPageText } from "../http.js";
10
+
11
+ interface SearchResult {
12
+ title: string;
13
+ url: string;
14
+ excerpt: string;
15
+ domain: string;
16
+ }
17
+
18
+ /**
19
+ * Builds a Google site-restricted search URL. We use the Google Custom
20
+ * Search JSON API pattern but fall back to direct developer.android.com
21
+ * search if no API key is configured (KISS principle – works out of the box).
22
+ */
23
+ function buildSearchUrl(query: string, domain: string): string {
24
+ const encoded = encodeURIComponent(query);
25
+ // developer.android.com has a public search endpoint
26
+ if (domain === "developer.android.com") {
27
+ return `https://developer.android.com/s/results?q=${encoded}`;
28
+ }
29
+ if (domain === "kotlinlang.org") {
30
+ return `https://kotlinlang.org/search.html#q=${encoded}`;
31
+ }
32
+ // Fallback: construct a direct reference URL
33
+ return `https://${domain}/search?q=${encoded}`;
34
+ }
35
+
36
+ /**
37
+ * Fetches a single authoritative page and returns a structured result.
38
+ * Fails gracefully – one domain failing should not block others.
39
+ */
40
+ async function fetchFromDomain(
41
+ query: string,
42
+ domain: string
43
+ ): Promise<SearchResult | null> {
44
+ try {
45
+ const url = buildSearchUrl(query, domain);
46
+ const html = await secureFetch(url);
47
+ const text = extractPageText(html, 2000);
48
+
49
+ // Try to extract a page title
50
+ const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
51
+ const title = titleMatch ? titleMatch[1].trim() : `${domain} – ${query}`;
52
+
53
+ return {
54
+ title,
55
+ url,
56
+ excerpt: text,
57
+ domain,
58
+ };
59
+ } catch (err) {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ return {
62
+ title: `[Fetch failed: ${domain}]`,
63
+ url: buildSearchUrl(query, domain),
64
+ excerpt: `Could not retrieve: ${message}`,
65
+ domain,
66
+ };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Core handler for android_official_search.
72
+ * Queries all authoritative Android engineering domains in parallel.
73
+ */
74
+ export async function androidOfficialSearch(query: string): Promise<string> {
75
+ if (!query || query.trim().length < 2) {
76
+ return "ERROR: Query must be at least 2 characters.";
77
+ }
78
+
79
+ const sanitizedQuery = query.trim().slice(0, 200); // bound input length
80
+
81
+ const domains = [
82
+ "developer.android.com",
83
+ "kotlinlang.org",
84
+ "source.android.com",
85
+ ];
86
+
87
+ // Parallel fetch — ALLOWED_DOMAINS enforced inside secureFetch
88
+ const results = await Promise.allSettled(
89
+ domains.map((d) => fetchFromDomain(sanitizedQuery, d))
90
+ );
91
+
92
+ const formatted = results
93
+ .map((r, i) => {
94
+ if (r.status === "rejected") {
95
+ return `### ${domains[i]}\n> Error: ${r.reason}`;
96
+ }
97
+ const res = r.value;
98
+ if (!res) return "";
99
+ return `### ${res.title}\n**Source:** ${res.url}\n\n${res.excerpt}`;
100
+ })
101
+ .filter(Boolean)
102
+ .join("\n\n---\n\n");
103
+
104
+ return (
105
+ `## AndroJack Official Search Results\n` +
106
+ `**Query:** "${sanitizedQuery}"\n` +
107
+ `**Sources:** ${domains.join(", ")}\n\n` +
108
+ formatted +
109
+ `\n\n---\n` +
110
+ `> ⚠️ GROUNDING GATE: Only produce Android code after reviewing the above official sources.`
111
+ );
112
+ }
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Tool 10 – android_testing_guide
3
+ *
4
+ * Complete Android testing reference grounded in official docs.
5
+ * Targets coroutines-test 1.9+ (StandardTestDispatcher, not deprecated TestCoroutineDispatcher).
6
+ *
7
+ * Sources:
8
+ * - developer.android.com/training/testing
9
+ * - developer.android.com/develop/ui/compose/testing
10
+ * - developer.android.com/training/dependency-injection/hilt-testing
11
+ */
12
+
13
+ interface TestingTopic {
14
+ keywords: string[];
15
+ content: string;
16
+ }
17
+
18
+ // ── Knowledge base ─────────────────────────────────────────────────────────────
19
+
20
+ const SETUP = `
21
+ ## Android Testing Setup — Dependencies & Config
22
+
23
+ \`\`\`kotlin
24
+ // build.gradle.kts (app)
25
+ android {
26
+ defaultConfig {
27
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
28
+ // For Hilt instrumented tests:
29
+ // testInstrumentationRunner = "com.yourapp.HiltTestRunner"
30
+ }
31
+ packaging {
32
+ resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" }
33
+ }
34
+ testOptions {
35
+ unitTests.isReturnDefaultValues = true // avoids AndroidLog crashes in unit tests
36
+ animationsDisabled = true // prevent flaky UI test timing issues
37
+ }
38
+ }
39
+
40
+ dependencies {
41
+ // ── Unit tests (src/test — JVM only, fast) ──────────────────────────────
42
+ testImplementation("junit:junit:4.13.2")
43
+ testImplementation("io.mockk:mockk:1.13.14")
44
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") // StandardTestDispatcher
45
+ testImplementation("app.cash.turbine:turbine:1.2.0") // Flow testing
46
+ testImplementation("androidx.arch.core:core-testing:2.2.0") // InstantTaskExecutorRule
47
+ testImplementation("com.google.truth:truth:1.4.4") // Fluent assertions
48
+ testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
49
+
50
+ // ── Instrumented tests (src/androidTest — needs emulator/device) ────────
51
+ androidTestImplementation(platform("androidx.compose:compose-bom:2025.05.00"))
52
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
53
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
54
+ androidTestImplementation("androidx.test.ext:junit:1.2.1")
55
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
56
+ androidTestImplementation("androidx.test.espresso:espresso-intents:3.6.1")
57
+ androidTestImplementation("com.google.dagger:hilt-android-testing:2.52")
58
+ kspAndroidTest("com.google.dagger:hilt-compiler:2.52")
59
+ androidTestImplementation("androidx.navigation:navigation-testing:2.8.9")
60
+ androidTestImplementation("io.mockk:mockk-android:1.13.14")
61
+ }
62
+ \`\`\`
63
+
64
+ > 📚 Source: developer.android.com/training/testing
65
+ `;
66
+
67
+ const UNIT_TESTS = `
68
+ ## Unit Testing — ViewModel, Repository, Room DAO
69
+
70
+ ### MainDispatcherRule — CORRECT for coroutines-test 1.9+
71
+
72
+ \`\`\`kotlin
73
+ // Source: developer.android.com/kotlin/coroutines/test
74
+ // IMPORTANT: Use StandardTestDispatcher, not deprecated TestCoroutineDispatcher
75
+ class MainDispatcherRule(
76
+ private val dispatcher: TestDispatcher = StandardTestDispatcher()
77
+ ) : TestWatcher() {
78
+ override fun starting(description: Description?) { Dispatchers.setMain(dispatcher) }
79
+ override fun finished(description: Description?) { Dispatchers.resetMain() }
80
+ }
81
+ \`\`\`
82
+
83
+ ### ViewModel test with Turbine + StandardTestDispatcher
84
+
85
+ \`\`\`kotlin
86
+ class LoginViewModelTest {
87
+
88
+ @get:Rule val coroutineRule = MainDispatcherRule()
89
+
90
+ private val repository = mockk<AuthRepository>()
91
+ private lateinit var viewModel: LoginViewModel
92
+
93
+ @Before
94
+ fun setup() { viewModel = LoginViewModel(repository) }
95
+
96
+ @Test
97
+ fun \`login success emits LoggedIn state\`() = runTest {
98
+ // Arrange
99
+ coEvery { repository.login("user", "pass") } returns Result.success(User("user"))
100
+
101
+ viewModel.uiState.test {
102
+ viewModel.login("user", "pass")
103
+
104
+ // runTest + StandardTestDispatcher: advance time explicitly
105
+ val loading = awaitItem()
106
+ assertTrue(loading is UiState.Loading)
107
+
108
+ val success = awaitItem() as UiState.Success
109
+ assertEquals("user", success.data.name)
110
+ cancelAndIgnoreRemainingEvents()
111
+ }
112
+ }
113
+
114
+ @Test
115
+ fun \`login failure emits Error state\`() = runTest {
116
+ coEvery { repository.login(any(), any()) } returns Result.failure(IOException("Network error"))
117
+
118
+ viewModel.uiState.test {
119
+ viewModel.login("user", "wrong")
120
+ skipItems(1) // Loading
121
+ val error = awaitItem() as UiState.Error
122
+ assertEquals("Network error", error.message)
123
+ cancelAndIgnoreRemainingEvents()
124
+ }
125
+ }
126
+ }
127
+ \`\`\`
128
+
129
+ ### Repository test with MockWebServer
130
+
131
+ \`\`\`kotlin
132
+ class UserRepositoryTest {
133
+ private val server = MockWebServer()
134
+ private lateinit var repository: UserRepository
135
+
136
+ @Before
137
+ fun setup() {
138
+ server.start()
139
+ val retrofit = Retrofit.Builder()
140
+ .baseUrl(server.url("/"))
141
+ .addConverterFactory(GsonConverterFactory.create())
142
+ .build()
143
+ repository = UserRepository(retrofit.create(UserApi::class.java))
144
+ }
145
+
146
+ @After fun tearDown() { server.shutdown() }
147
+
148
+ @Test
149
+ fun \`fetchUser returns User on 200\`() = runTest {
150
+ server.enqueue(MockResponse().setBody("""{"id":1,"name":"Alice"}""").setResponseCode(200))
151
+ val result = repository.fetchUser(1)
152
+ assertTrue(result.isSuccess)
153
+ assertEquals("Alice", result.getOrNull()?.name)
154
+ }
155
+
156
+ @Test
157
+ fun \`fetchUser wraps 404 as failure\`() = runTest {
158
+ server.enqueue(MockResponse().setResponseCode(404))
159
+ val result = repository.fetchUser(1)
160
+ assertTrue(result.isFailure)
161
+ }
162
+ }
163
+ \`\`\`
164
+
165
+ ### Room in-memory database test
166
+
167
+ \`\`\`kotlin
168
+ @RunWith(AndroidJUnit4::class)
169
+ class UserDaoTest {
170
+ private lateinit var db: AppDatabase
171
+ private lateinit var dao: UserDao
172
+
173
+ @Before
174
+ fun setup() {
175
+ db = Room.inMemoryDatabaseBuilder(
176
+ ApplicationProvider.getApplicationContext(),
177
+ AppDatabase::class.java
178
+ ).allowMainThreadQueries().build()
179
+ dao = db.userDao()
180
+ }
181
+
182
+ @After fun tearDown() { db.close() }
183
+
184
+ @Test
185
+ fun insertAndRetrieve() = runTest {
186
+ val user = User(id = 1, name = "Alice")
187
+ dao.insertUser(user)
188
+ val retrieved = dao.getUserById(1).first()
189
+ assertEquals(user, retrieved)
190
+ }
191
+
192
+ @Test
193
+ fun flowEmitsOnUpdate() = runTest {
194
+ dao.getUserById(1).test {
195
+ // Initially null or empty
196
+ dao.insertUser(User(1, "Alice"))
197
+ val item = awaitItem()
198
+ assertEquals("Alice", item?.name)
199
+ cancelAndIgnoreRemainingEvents()
200
+ }
201
+ }
202
+ }
203
+ \`\`\`
204
+ `;
205
+
206
+ const COMPOSE_TESTING = `
207
+ ## Compose UI Testing
208
+
209
+ \`\`\`kotlin
210
+ // Source: developer.android.com/develop/ui/compose/testing
211
+
212
+ @RunWith(AndroidJUnit4::class)
213
+ class LoginScreenTest {
214
+
215
+ @get:Rule val composeTestRule = createComposeRule()
216
+
217
+ @Test
218
+ fun \`login button disabled when fields empty\`() {
219
+ composeTestRule.setContent {
220
+ AppTheme { LoginScreen(onLoginSuccess = {}) }
221
+ }
222
+ composeTestRule.onNodeWithText("Sign In").assertIsNotEnabled()
223
+ }
224
+
225
+ @Test
226
+ fun \`successful login navigates to home\`() {
227
+ composeTestRule.setContent {
228
+ AppTheme { LoginScreen(onLoginSuccess = {}) }
229
+ }
230
+
231
+ composeTestRule.onNodeWithTag("emailField").performTextInput("alice@test.com")
232
+ composeTestRule.onNodeWithTag("passwordField").performTextInput("password123")
233
+ composeTestRule.onNodeWithText("Sign In").performClick()
234
+
235
+ composeTestRule.waitUntil(timeoutMillis = 5_000) {
236
+ composeTestRule.onAllNodesWithText("Welcome").fetchSemanticsNodes().isNotEmpty()
237
+ }
238
+ composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
239
+ }
240
+ }
241
+ \`\`\`
242
+
243
+ ### Finders & assertions quick reference
244
+
245
+ \`\`\`kotlin
246
+ // Finders
247
+ onNodeWithText("Submit") // by text
248
+ onNodeWithTag("loginButton") // by testTag (most stable)
249
+ onNodeWithContentDescription("Close") // by accessibility description
250
+ onNode(hasRole(Role.Button) and isEnabled()) // combined matcher
251
+
252
+ // Assertions
253
+ .assertIsDisplayed()
254
+ .assertIsEnabled() / .assertIsNotEnabled()
255
+ .assertTextEquals("Expected")
256
+ .assertHasClickAction()
257
+
258
+ // Actions
259
+ .performClick()
260
+ .performTextInput("text")
261
+ .performTextClearance()
262
+ .performScrollTo()
263
+ .performTouchInput { swipeUp() }
264
+
265
+ // Waiting (use instead of Thread.sleep)
266
+ composeTestRule.waitUntil(5_000) {
267
+ onAllNodesWithTag("item").fetchSemanticsNodes().size >= 3
268
+ }
269
+ \`\`\`
270
+
271
+ ### Anti-pattern
272
+
273
+ \`\`\`kotlin
274
+ // ❌ Fragile — breaks when text changes
275
+ onNodeWithText("Submit")[0].performClick()
276
+
277
+ // ✅ Stable — add to your Composable: Modifier.testTag("submitButton")
278
+ onNodeWithTag("submitButton").performClick()
279
+ \`\`\`
280
+ `;
281
+
282
+ const ESPRESSO = `
283
+ ## Espresso — View + Compose interop
284
+
285
+ \`\`\`kotlin
286
+ // Source: developer.android.com/training/testing/espresso
287
+
288
+ @RunWith(AndroidJUnit4::class)
289
+ @HiltAndroidTest
290
+ class MainActivityTest {
291
+
292
+ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
293
+ @get:Rule(order = 1) val activityRule = ActivityScenarioRule(MainActivity::class.java)
294
+
295
+ @Before fun setup() { hiltRule.inject() }
296
+
297
+ @Test
298
+ fun clickButtonShowsResult() {
299
+ onView(withId(R.id.submitButton))
300
+ .check(matches(isDisplayed()))
301
+ .perform(click())
302
+ onView(withText("Success")).check(matches(isDisplayed()))
303
+ }
304
+ }
305
+
306
+ // Mixed Espresso + Compose (interop)
307
+ @RunWith(AndroidJUnit4::class)
308
+ class HybridScreenTest {
309
+ @get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>()
310
+
311
+ @Test
312
+ fun hybridInterop() {
313
+ // View layer
314
+ onView(withText("Hello Views")).check(matches(isDisplayed()))
315
+ // Compose layer
316
+ composeTestRule.onNodeWithText("Click here").performClick()
317
+ // Back to View
318
+ onView(withText("Updated")).check(matches(isDisplayed()))
319
+ }
320
+ }
321
+ \`\`\`
322
+ `;
323
+
324
+ const HILT_TESTING = `
325
+ ## Hilt Dependency Injection in Tests
326
+
327
+ \`\`\`kotlin
328
+ // Source: developer.android.com/training/dependency-injection/hilt-testing
329
+
330
+ // Custom test runner (required for instrumented Hilt tests)
331
+ class HiltTestRunner : AndroidJUnitRunner() {
332
+ override fun newApplication(cl: ClassLoader?, name: String?, context: Context?) =
333
+ super.newApplication(cl, HiltTestApplication::class.java.name, context)
334
+ }
335
+ // In build.gradle.kts: testInstrumentationRunner = "com.yourapp.HiltTestRunner"
336
+
337
+ @HiltAndroidTest
338
+ class UserFlowTest {
339
+
340
+ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
341
+ @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
342
+
343
+ // Override real binding with a fake
344
+ @BindValue @JvmField
345
+ val repository: UserRepository = FakeUserRepository()
346
+
347
+ @Before fun setup() { hiltRule.inject() }
348
+
349
+ @Test
350
+ fun userListDisplays() {
351
+ composeTestRule.onNodeWithTag("userList").assertIsDisplayed()
352
+ }
353
+ }
354
+
355
+ class FakeUserRepository : UserRepository {
356
+ override fun getUsers() = flowOf(listOf(User(1, "Alice"), User(2, "Bob")))
357
+ override suspend fun refreshUsers() = Unit
358
+ }
359
+ \`\`\`
360
+
361
+ ### Unit testing Hilt ViewModels (no instrumentation needed)
362
+
363
+ \`\`\`kotlin
364
+ // No @HiltAndroidTest needed — just inject fakes directly
365
+ class UserViewModelTest {
366
+ @get:Rule val coroutineRule = MainDispatcherRule()
367
+ private val fakeRepo = FakeUserRepository()
368
+ private val viewModel = UserViewModel(fakeRepo)
369
+
370
+ @Test
371
+ fun \`users are loaded on init\`() = runTest {
372
+ viewModel.uiState.test {
373
+ val state = awaitItem() as UiState.Success
374
+ assertEquals(2, state.data.size)
375
+ cancelAndIgnoreRemainingEvents()
376
+ }
377
+ }
378
+ }
379
+ \`\`\`
380
+ `;
381
+
382
+ const TEST_PYRAMID = `
383
+ ## Test Pyramid Strategy for Android
384
+
385
+ \`\`\`
386
+ /\\
387
+ / \\ ← E2E / UI Tests (10%)
388
+ / E2E \\ Espresso, Compose UI tests
389
+ /───────\\
390
+ / \\ ← Integration Tests (20–30%)
391
+ / Integration \\ Repository + DAO + ViewModel with fakes
392
+ /───────────────\\
393
+ / \\ ← Unit Tests (60–70%)
394
+ / Unit (JVM/JUnit4) \\ UseCases, mappers, domain logic, utils
395
+ \\─────────────────────/
396
+ \`\`\`
397
+
398
+ ### Key rules
399
+
400
+ - **Unit tests** → \`src/test/\` — JVM only, no emulator, milliseconds to run
401
+ - **Instrumented tests** → \`src/androidTest/\` — needs emulator or device
402
+ - **Always** use in-memory Room DB for DAO tests — never real files
403
+ - **Always** use MockWebServer for network tests — never real endpoints
404
+ - **Always** use Turbine for Flow assertions — never \`runBlocking { flow.first() }\`
405
+ - **Always** use MainDispatcherRule with StandardTestDispatcher for ViewModel tests
406
+ - **Always** add \`testTag\` to interactive Compose elements — text matchers break with i18n
407
+ - **Target:** 70% unit · 20% integration · 10% UI for most apps
408
+
409
+ **Official guide:** https://developer.android.com/training/testing
410
+ **Compose testing:** https://developer.android.com/develop/ui/compose/testing
411
+ `;
412
+
413
+ // ── Topic routing ─────────────────────────────────────────────────────────────
414
+
415
+ const TOPICS: TestingTopic[] = [
416
+ { keywords: ["setup", "dependency", "dependencies", "gradle", "getting started", "config", "runner"], content: SETUP },
417
+ { keywords: ["unit test", "viewmodel test", "repository test", "mockk", "turbine", "coroutine test", "flow test", "room test", "dao test", "main dispatcher", "standardtest"], content: UNIT_TESTS },
418
+ { keywords: ["compose test", "composetestrul", "ui test", "semantics", "finder", "assertis", "performclick", "testtag", "compose ui", "waituntil"], content: COMPOSE_TESTING },
419
+ { keywords: ["espresso", "view test", "onview", "activityscenario", "interop", "hybrid"], content: ESPRESSO },
420
+ { keywords: ["hilt test", "hilt inject", "hiltandroidtest", "bindvalue", "fake", "hilt testing"], content: HILT_TESTING },
421
+ { keywords: ["pyramid", "strategy", "how many", "coverage", "structure", "overview", "what to test", "ratio"], content: TEST_PYRAMID },
422
+ ];
423
+
424
+ const INDEX = `
425
+ ## Android Testing Guide
426
+
427
+ **Available topics:**
428
+ - \`setup\` — Gradle dependencies for all test types
429
+ - \`unit tests\` — ViewModel, Repository, Room DAO, MainDispatcherRule (StandardTestDispatcher)
430
+ - \`compose testing\` — ComposeTestRule, finders, actions, assertions, waitUntil
431
+ - \`espresso\` — View-based tests + Compose interop
432
+ - \`hilt testing\` — Custom test runner, @HiltAndroidTest, @BindValue fakes
433
+ - \`pyramid\` — Test strategy, ratios, key rules
434
+
435
+ **Official sources:**
436
+ - https://developer.android.com/training/testing
437
+ - https://developer.android.com/develop/ui/compose/testing
438
+ `;
439
+
440
+ export async function androidTestingGuide(topic: string): Promise<string> {
441
+ const trimmed = topic.trim().toLowerCase();
442
+ if (!trimmed || trimmed === "list" || trimmed === "help") return INDEX;
443
+
444
+ const found = TOPICS.find(t => t.keywords.some(k => trimmed.includes(k)));
445
+ if (found) {
446
+ return (
447
+ found.content.trim() +
448
+ `\n\n---\n> 🧪 GROUNDING GATE: Tests must follow the official patterns above.\n` +
449
+ `> Use StandardTestDispatcher (not deprecated TestCoroutineDispatcher), Turbine for Flows, MainDispatcherRule for ViewModels.`
450
+ );
451
+ }
452
+
453
+ return (
454
+ `## Android Testing: "${topic}"\n\n` +
455
+ `No built-in entry found. Check:\n` +
456
+ `- https://developer.android.com/training/testing\n` +
457
+ `- https://developer.android.com/develop/ui/compose/testing\n\n` +
458
+ INDEX
459
+ );
460
+ }