engsys 1.0.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 (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/core/agents/aaron.md +152 -0
  4. package/core/agents/bert.md +115 -0
  5. package/core/agents/isabelle.md +136 -0
  6. package/core/agents/jody.md +150 -0
  7. package/core/agents/leith.md +111 -0
  8. package/core/agents/marcelo.md +282 -0
  9. package/core/agents/melvin.md +101 -0
  10. package/core/agents/nyx.md +152 -0
  11. package/core/agents/otto.md +168 -0
  12. package/core/agents/patricia.md +283 -0
  13. package/core/commands/design-audit-local.md +155 -0
  14. package/core/commands/design-audit.md +235 -0
  15. package/core/commands/design-critique.md +96 -0
  16. package/core/commands/file-issue.md +22 -0
  17. package/core/commands/generate-project.md +45 -0
  18. package/core/commands/implement-issue.md +37 -0
  19. package/core/commands/implement-project.md +40 -0
  20. package/core/commands/naturalize.md +61 -0
  21. package/core/commands/pre-push.md +29 -0
  22. package/core/commands/prep-review-collect.md +130 -0
  23. package/core/commands/prep-review-finalize.md +121 -0
  24. package/core/commands/prep-review-publish.md +113 -0
  25. package/core/commands/prep-review.md +65 -0
  26. package/core/commands/project-closeout.md +25 -0
  27. package/core/skills/agentic-eval/SKILL.md +195 -0
  28. package/core/skills/chrome-devtools/SKILL.md +97 -0
  29. package/core/skills/code-review/SKILL.md +26 -0
  30. package/core/skills/gh-cli/SKILL.md +2202 -0
  31. package/core/skills/git-commit/SKILL.md +124 -0
  32. package/core/skills/git-workflow-agents/SKILL.md +462 -0
  33. package/core/skills/git-workflow-agents/reference.md +220 -0
  34. package/core/skills/github-actions/SKILL.md +190 -0
  35. package/core/skills/github-issues/SKILL.md +154 -0
  36. package/core/skills/llm-structured-outputs/SKILL.md +323 -0
  37. package/core/skills/llm-structured-outputs/references/provider-details.md +392 -0
  38. package/core/skills/pre-push/SKILL.md +115 -0
  39. package/core/skills/refactor/SKILL.md +645 -0
  40. package/core/skills/web-design-reviewer/SKILL.md +371 -0
  41. package/core/skills/webapp-testing/SKILL.md +127 -0
  42. package/core/skills/webapp-testing/test-helper.js +56 -0
  43. package/core/templates/CLAUDE.md.tmpl +98 -0
  44. package/core/templates/adr-template.md +67 -0
  45. package/core/templates/gh-issue-templates/bug.md +39 -0
  46. package/core/templates/gh-issue-templates/content.md +42 -0
  47. package/core/templates/gh-issue-templates/enhancement.md +36 -0
  48. package/core/templates/gh-issue-templates/feature.md +39 -0
  49. package/core/templates/gh-issue-templates/infrastructure.md +41 -0
  50. package/core/templates/post-edit-reminders.sh.tmpl +19 -0
  51. package/core/templates/settings.json.tmpl +90 -0
  52. package/core/templates/settings.local.json.tmpl +3 -0
  53. package/core/workflows/agent-implementation-workflow.md +346 -0
  54. package/core/workflows/generate-project.md +258 -0
  55. package/core/workflows/implement-project-workflow.md +190 -0
  56. package/core/workflows/issue-tracking.md +89 -0
  57. package/core/workflows/project-closeout-ceremony.md +77 -0
  58. package/core/workflows/review-workflow.md +266 -0
  59. package/engsys.config.example.yaml +46 -0
  60. package/install +202 -0
  61. package/lessons-library/README.md +80 -0
  62. package/lessons-library/async-callbacks-verify-liveness.md +15 -0
  63. package/lessons-library/change-isnt-done-until-every-surface-updated.md +15 -0
  64. package/lessons-library/claim-then-act-for-irreversible-ops.md +16 -0
  65. package/lessons-library/co-commit-entangled-work.md +15 -0
  66. package/lessons-library/dependabot-triage-playbook.md +17 -0
  67. package/lessons-library/deploy-by-digest-and-verify-the-running-revision.md +15 -0
  68. package/lessons-library/enforce-your-guarantee-at-your-boundary.md +16 -0
  69. package/lessons-library/gate-changes-on-measurement-not-vibes.md +15 -0
  70. package/lessons-library/iac-first-no-console-changes.md +15 -0
  71. package/lessons-library/independent-objective-review-gate.md +15 -0
  72. package/lessons-library/keep-an-immutable-source-of-truth.md +15 -0
  73. package/lessons-library/long-agent-runs-checkpoint-not-poll.md +15 -0
  74. package/lessons-library/model-identity-with-stable-ids-and-provenance.md +15 -0
  75. package/lessons-library/operator-choices-are-first-class.md +15 -0
  76. package/lessons-library/prefer-tool-enforced-structured-output.md +15 -0
  77. package/lessons-library/prove-causation-before-acting.md +15 -0
  78. package/lessons-library/re-read-state-before-acting.md +14 -0
  79. package/lessons-library/read-layer-tolerates-unbackfilled-rows.md +15 -0
  80. package/lessons-library/shell-safety-pipefail-and-validate-before-teardown.md +14 -0
  81. package/lessons-library/shift-correctness-left-and-distrust-false-greens.md +15 -0
  82. package/lessons-library/stray-control-bytes-hide-changes.md +14 -0
  83. package/lessons-library/tests-can-assert-the-bug.md +15 -0
  84. package/lessons-library/verify-ground-truth-not-reports.md +15 -0
  85. package/lessons-library/worktrees-need-bootstrap-from-origin-main.md +15 -0
  86. package/lib/commands.js +356 -0
  87. package/lib/generate-team-avatars.mjs +251 -0
  88. package/lib/manifest.js +155 -0
  89. package/lib/render.js +135 -0
  90. package/lib/selftest.js +90 -0
  91. package/lib/util.js +89 -0
  92. package/lib/yaml.js +156 -0
  93. package/optional-agents/gary.md +86 -0
  94. package/optional-agents/jos.md +136 -0
  95. package/optional-agents/sandy.md +101 -0
  96. package/optional-agents/steve.md +161 -0
  97. package/package.json +43 -0
  98. package/stacks/cloud/aws/claude.fragment.md +17 -0
  99. package/stacks/cloud/aws/settings.fragment.json +39 -0
  100. package/stacks/cloud/aws/skills/aws-deployment-preflight/SKILL.md +165 -0
  101. package/stacks/cloud/aws/skills/cloud-architecture-aws/SKILL.md +265 -0
  102. package/stacks/cloud/azure/claude.fragment.md +17 -0
  103. package/stacks/cloud/azure/settings.fragment.json +45 -0
  104. package/stacks/cloud/azure/skills/azure-deployment-preflight/SKILL.md +175 -0
  105. package/stacks/cloud/azure/skills/cloud-architecture-azure/SKILL.md +211 -0
  106. package/stacks/cloud/cloudflare/claude.fragment.md +21 -0
  107. package/stacks/cloud/cloudflare/settings.fragment.json +31 -0
  108. package/stacks/cloud/cloudflare/skills/cloud-architecture-cloudflare/SKILL.md +294 -0
  109. package/stacks/cloud/cloudflare/skills/cloudflare-deployment-preflight/SKILL.md +175 -0
  110. package/stacks/cloud/gcp/claude.fragment.md +17 -0
  111. package/stacks/cloud/gcp/settings.fragment.json +40 -0
  112. package/stacks/cloud/gcp/skills/cloud-architecture-gcp/SKILL.md +208 -0
  113. package/stacks/cloud/gcp/skills/gcp-deployment-preflight/SKILL.md +137 -0
  114. package/stacks/db/mongo/skills/mongo-conventions/SKILL.md +96 -0
  115. package/stacks/db/prisma/claude.fragment.md +49 -0
  116. package/stacks/db/prisma/skills/docker-database-package-copy/SKILL.md +44 -0
  117. package/stacks/db/prisma/skills/prisma-conventions/SKILL.md +37 -0
  118. package/stacks/domain/mobile-growth/skills/apple-ads/SKILL.md +184 -0
  119. package/stacks/domain/mobile-growth/skills/apple-ads/references/benchmark-notes.md +47 -0
  120. package/stacks/domain/mobile-growth/skills/apple-ads/references/official-links.md +53 -0
  121. package/stacks/domain/mobile-growth/skills/google-play-growth/SKILL.md +197 -0
  122. package/stacks/domain/mobile-growth/skills/google-play-growth/references/benchmark-notes.md +47 -0
  123. package/stacks/domain/mobile-growth/skills/google-play-growth/references/official-links.md +45 -0
  124. package/stacks/iac/bicep/claude.fragment.md +14 -0
  125. package/stacks/iac/bicep/settings.fragment.json +20 -0
  126. package/stacks/iac/bicep/skills/iac-bicep/SKILL.md +113 -0
  127. package/stacks/iac/cdk/claude.fragment.md +14 -0
  128. package/stacks/iac/cdk/settings.fragment.json +23 -0
  129. package/stacks/iac/cdk/skills/iac-cdk/SKILL.md +104 -0
  130. package/stacks/iac/terraform/claude.fragment.md +13 -0
  131. package/stacks/iac/terraform/settings.fragment.json +25 -0
  132. package/stacks/iac/terraform/skills/iac-terraform/SKILL.md +93 -0
  133. package/stacks/iac/terraform/skills/terraform-conventions/SKILL.md +87 -0
  134. package/stacks/lang/kotlin/skills/android-testing/SKILL.md +263 -0
  135. package/stacks/lang/kotlin/skills/jetpack-compose/SKILL.md +264 -0
  136. package/stacks/lang/kotlin/skills/kotlin-coroutines/SKILL.md +329 -0
  137. package/stacks/lang/python/skills/python-conventions/SKILL.md +61 -0
  138. package/stacks/lang/shell/skills/shell-scripting/SKILL.md +110 -0
  139. package/stacks/lang/swift/skills/swift-concurrency/SKILL.md +423 -0
  140. package/stacks/lang/swift/skills/swift-concurrency/references/approachable-concurrency.md +80 -0
  141. package/stacks/lang/swift/skills/swift-concurrency/references/concurrency-patterns.md +233 -0
  142. package/stacks/lang/swift/skills/swift-concurrency/references/swiftui-concurrency.md +187 -0
  143. package/stacks/lang/swift/skills/swift-concurrency/references/synchronization-primitives.md +341 -0
  144. package/stacks/lang/swift/skills/swift-testing/SKILL.md +497 -0
  145. package/stacks/lang/swift/skills/swift-testing/references/testing-advanced.md +106 -0
  146. package/stacks/lang/swift/skills/swift-testing/references/testing-patterns.md +504 -0
  147. package/stacks/lang/swift/skills/swiftdata/SKILL.md +334 -0
  148. package/stacks/lang/swift/skills/swiftdata/references/core-data-coexistence.md +504 -0
  149. package/stacks/lang/swift/skills/swiftdata/references/swiftdata-advanced.md +975 -0
  150. package/stacks/lang/swift/skills/swiftdata/references/swiftdata-queries.md +675 -0
  151. package/stacks/lang/swift/skills/swiftui-patterns/SKILL.md +371 -0
  152. package/stacks/lang/swift/skills/swiftui-patterns/references/architecture-patterns.md +486 -0
  153. package/stacks/lang/swift/skills/swiftui-patterns/references/deprecated-migration.md +1097 -0
  154. package/stacks/lang/swift/skills/swiftui-patterns/references/design-polish.md +780 -0
  155. package/stacks/lang/swift/skills/swiftui-patterns/references/platform-and-sharing.md +696 -0
  156. package/stacks/lang/typescript/skills/typescript-conventions/SKILL.md +91 -0
  157. package/stacks/platform/android/claude.fragment.md +40 -0
  158. package/stacks/platform/android/hooks/pre-push-gradle.sh +70 -0
  159. package/stacks/platform/android/settings.fragment.json +13 -0
  160. package/stacks/platform/android/skills/android-build-conventions/SKILL.md +247 -0
  161. package/stacks/platform/ios/claude.fragment.md +24 -0
  162. package/stacks/platform/ios/hooks/pre-push-xcodebuild.sh +82 -0
  163. package/stacks/platform/ios/settings.fragment.json +21 -0
  164. package/stacks/platform/ios/skills/xcodebuildmcp-simulator-logs/SKILL.md +76 -0
  165. package/stacks/platform/web/skills/frontend-testing/SKILL.md +246 -0
  166. package/stacks/platform/web/skills/react-conventions/SKILL.md +261 -0
  167. package/stacks/platform/web/skills/web-platform-conventions/SKILL.md +55 -0
  168. package/stacks/tooling/issue-tracker-github/claude.fragment.md +10 -0
  169. package/stacks/tooling/issue-tracker-github/settings.fragment.json +24 -0
  170. package/stacks/tooling/issue-tracker-github/skills/issue-tracker-github/SKILL.md +278 -0
  171. package/stacks/tooling/issue-tracker-linear/claude.fragment.md +17 -0
  172. package/stacks/tooling/issue-tracker-linear/settings.fragment.json +9 -0
  173. package/stacks/tooling/issue-tracker-linear/skills/issue-tracker-linear/SKILL.md +183 -0
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: iac-terraform
3
+ description: Terraform discipline for any project where Terraform is the active IaC tool — modules, remote state, workspaces, backends, plan/apply gates, drift detection, and import/state surgery. Activate when working on *.tf / *.tfvars files, terraform plan/apply/state operations, backend or workspace config, provider versioning, or diagnosing drift and partial applies.
4
+ ---
5
+
6
+ # Terraform Discipline
7
+
8
+ The operational discipline for Terraform as the active IaC tool — cloud-independent.
9
+ Service-level resource detail comes from the active `cloud-architecture-<cloud>` pack;
10
+ project file layout and backend config come from `CLAUDE.md`. For repo-specific style
11
+ (naming, ordering, security defaults) see the `terraform-conventions` skill if present.
12
+
13
+ ## Core stance
14
+
15
+ - **Infrastructure is software.** If it only works once, it doesn't work. "Just apply it
16
+ again" is not a strategy — understand *why* it failed first.
17
+ - **Plan is the contract.** Never `apply` without reading the `plan`. The plan is the
18
+ what-if; treat a surprising plan as a bug to investigate, not a step to skip.
19
+ - **Pin everything.** Required Terraform version + provider versions in a lockfile
20
+ (`.terraform.lock.hcl`, committed). Unpinned providers are how "it worked yesterday"
21
+ happens.
22
+
23
+ ## State
24
+
25
+ - **Remote state with locking, always.** Local state is a single point of failure and
26
+ blocks collaboration. Use a backend with locking (e.g. S3 + DynamoDB lock table, GCS,
27
+ azurerm with blob lease, or Terraform Cloud). Never commit state — it contains secrets.
28
+ - **Separate state per major component / environment** — smaller blast radius, faster
29
+ plan/apply, and a failure in one doesn't lock the others. Check the project's
30
+ `backend.tf` / backend config before touching anything stateful.
31
+ - **State surgery** when reality and state diverge:
32
+ - `terraform state list` — see what's tracked.
33
+ - `terraform import <addr> <id>` — adopt an existing resource.
34
+ - `terraform state mv` — rename/move without destroy+recreate.
35
+ - `terraform state rm` — stop tracking (does NOT delete the real resource).
36
+ - Always `plan` after surgery to confirm convergence. Back up state first.
37
+
38
+ ## Modules
39
+
40
+ - **Modules for groups of related resources only.** Don't wrap a single resource in a
41
+ module — that's indirection without benefit.
42
+ - Pin module sources to a version/ref. Expose interesting attributes via `output`; mark
43
+ sensitive ones `sensitive = true`.
44
+ - Avoid deep nesting and circular dependencies. A module should have a clear, narrow
45
+ interface (typed `variable`s with `description`, validated where it matters).
46
+ - Prefer composition (root config wires modules together) over monolithic mega-modules.
47
+
48
+ ## Workspaces & environments
49
+
50
+ - Workspaces (`terraform workspace`) give you state isolation for *the same config across
51
+ environments* — useful, but they share the same backend key prefix and are easy to
52
+ misuse. For genuinely different environments, **separate backend keys / directories +
53
+ tfvars** is usually clearer and safer than relying on workspace interpolation.
54
+ - Never let `dev` and `prod` share state. Parameterize via `*.tfvars` per environment;
55
+ keep secrets out of tfvars (use a secrets manager + data sources / env vars).
56
+
57
+ ## Plan / apply gates
58
+
59
+ - **`terraform fmt` → `validate` → `plan` → review → `apply`** is the pipeline. In CI:
60
+ plan on PR (post the plan), apply only on merge to the protected branch, behind
61
+ approval for prod.
62
+ - **Read the plan for destroys and replacements.** A `-/+` (replace) on a stateful
63
+ resource (database, disk, bucket) is a data-loss event — stop and confirm. Use
64
+ `lifecycle { prevent_destroy = true }` on the truly precious.
65
+ - Keep infra applies separate from app deploys unless there's a very good reason not to.
66
+ - `-target` is an escape hatch for recovering a broken apply, not a normal workflow —
67
+ it produces partial state. Note it when you use it.
68
+
69
+ ## Drift
70
+
71
+ - **Detect drift before it bites:** `terraform plan` (or `plan -refresh-only`) on a
72
+ schedule shows out-of-band (click-ops) changes. A non-empty plan on an unchanged config
73
+ *is* drift.
74
+ - Resolve drift deliberately: either bring the change into code (and apply), or revert
75
+ the manual change. Don't let an unexplained diff sit — it compounds.
76
+ - **No click-ops in production**, ever. Manual changes create snowflake environments that
77
+ can't be recreated.
78
+
79
+ ## Troubleshooting
80
+
81
+ - **Partial apply:** read which resources succeeded, reconcile state (import/refresh),
82
+ then re-plan. Don't blindly re-apply.
83
+ - **API throttling:** tune `-parallelism`, add provider-level retries, back off.
84
+ - **Provider auth / IAM:** trace the credential chain and the role/policy actually in
85
+ use; permission errors lie about the real missing action — read them for meaning.
86
+ - **Lock held:** a crashed run can leave a stale lock; `force-unlock` only when you're
87
+ certain no apply is in flight.
88
+
89
+ ## Preflight
90
+
91
+ Before applying, run the active cloud's `*-deployment-preflight` skill — it covers the
92
+ cloud-specific concerns (stale/failed deployments, globally-unique naming, quota/SKU
93
+ limits) that `terraform plan` alone won't surface.
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: terraform-conventions
3
+ description: Terraform code conventions — security, modularity, maintainability, style, documentation, and testing expectations for any *.tf file. Activate when writing or reviewing Terraform configuration (typically under infra/terraform/ or the project's IaC directory) to enforce consistent structure and safe defaults.
4
+ ---
5
+
6
+ # Terraform Conventions
7
+
8
+ Applies to any `*.tf` file. Primary location is the project's IaC directory (commonly
9
+ `infra/terraform/` — confirm in `CLAUDE.md`). These are code conventions; for the
10
+ operational discipline (state, plan/apply gates, drift, imports) see the `iac-terraform`
11
+ skill, and for service-level resource detail see the active `cloud-architecture-<cloud>`
12
+ pack.
13
+
14
+ ## Security
15
+
16
+ - Use the latest stable Terraform + provider versions; patch regularly. Pin versions and
17
+ commit `.terraform.lock.hcl`.
18
+ - **Secrets never in state files, variables, or version control.** Store them in the
19
+ cloud's secret manager (AWS Secrets Manager / SSM, Azure Key Vault, GCP Secret Manager)
20
+ and reference them via data sources or environment variables. Rotate; automate rotation
21
+ where possible.
22
+ - Mark any sensitive variable/output `sensitive = true`.
23
+ - Least-privilege IAM/roles. Restrict network access with the cloud's native controls
24
+ (security groups + NACLs, NSGs, firewall rules).
25
+ - Resources in private subnets/networks by default; public only for load balancers / NAT
26
+ / similar entry points.
27
+ - Encryption at rest (disks, object storage, managed databases) and in transit (TLS).
28
+ - Scan with `trivy`, `tfsec`, or `checkov` in CI.
29
+
30
+ ## Modularity
31
+
32
+ - Separate state/projects for major components — reduces blast radius, speeds up
33
+ plan/apply.
34
+ - Modules for groups of related resources only. Don't wrap a single resource in a module.
35
+ - Avoid deep nesting and circular dependencies.
36
+ - Expose interesting attributes via `output`; mark sensitive ones.
37
+
38
+ ## Maintainability
39
+
40
+ - Prefer readable, explicit configs over clever ones.
41
+ - Variables (with sensible defaults where appropriate) instead of hard-coded values.
42
+ - Data sources for *existing* external resources; outputs for in-config references.
43
+ - `locals` for repeated values to enforce consistency.
44
+ - Avoid stale/unnecessary data sources — they slow plan/apply.
45
+
46
+ ## Style
47
+
48
+ - 2-space indents. Run `terraform fmt`, `terraform validate`, `tflint`.
49
+ - File naming by resource grouping (`providers.tf`, `variables.tf`, `network.tf`,
50
+ `outputs.tf`, etc.).
51
+ - Alphabetize providers, variables, data sources, resources, and outputs.
52
+ - Order within a resource: `depends_on` → `for_each`/`count` → attributes (required, then
53
+ optional) → `lifecycle`.
54
+ - Use `for_each` for collections; `count` for numeric iteration.
55
+ - Blank lines to separate logical sections.
56
+
57
+ ## Documentation
58
+
59
+ - `description` + `type` on every variable and output.
60
+ - Comment intent and non-obvious decisions, not the obvious.
61
+ - `README.md` per project; consider `terraform-docs` for generated module docs.
62
+
63
+ ## Testing
64
+
65
+ - Use `.tftest.hcl` for tests.
66
+ - Cover positive and negative scenarios.
67
+ - Tests must be idempotent.
68
+
69
+ ## This repo's stack
70
+
71
+ - The active cloud, services, and IaC directory are declared in `CLAUDE.md`; the
72
+ `cloud-architecture-<cloud>` pack carries the service-level detail.
73
+ - Check the backend config (`backend.tf` or equivalent) before changing anything
74
+ stateful.
75
+
76
+ ## Hard-won lessons
77
+
78
+ ### Span two control planes? Use a multi-provider tool so one apply wires both sides
79
+ **Symptom:** Infrastructure crosses a cloud *and* a managed SaaS (e.g. Azure +
80
+ MongoDB Atlas, where a Private Endpoint requires resources on **both** the Atlas
81
+ Private Link service and the Azure side, cross-referenced).
82
+ **Cause:** Single-cloud IaC (Bicep, CDK, raw ARM) can only manage its own cloud — it
83
+ structurally cannot touch the SaaS half, so that half drops back to console clicks or
84
+ a second tool, breaking "everything repeatable" for the exact resources that matter.
85
+ **Fix:** Choose **Terraform** (multi-provider — e.g. `azurerm` + `mongodbatlas` in
86
+ one graph) so a single `terraform apply` wires both sides of the integration. Adopt
87
+ pre-existing/shared resources as data sources or `terraform import`; don't recreate.
@@ -0,0 +1,263 @@
1
+ ---
2
+ name: android-testing
3
+ description: "Write and review Android/Kotlin tests across the pyramid: JUnit (4/5) unit tests, MockK, Turbine for Flow/StateFlow assertions, coroutine testing with runTest and test dispatchers, Robolectric JVM tests, Espresso instrumented UI tests, and Compose UI testing with createComposeRule and semantics matchers. Use when writing or fixing unit tests, testing coroutines/Flows, mocking dependencies, choosing JVM vs instrumented tests, or testing Compose UI and ViewModels on Android."
4
+ ---
5
+
6
+ # Android Testing
7
+
8
+ Write and review tests for Android/Kotlin targeting JUnit, MockK, Turbine,
9
+ kotlinx-coroutines-test, Robolectric, Espresso, and Compose UI testing. Favor a
10
+ test pyramid: many fast JVM unit tests, fewer Robolectric/Compose tests, and a
11
+ thin layer of instrumented end-to-end tests. Test behavior, not implementation.
12
+
13
+ ## Contents
14
+
15
+ - [Test Pyramid and Source Sets](#test-pyramid-and-source-sets)
16
+ - [Unit Tests (JUnit)](#unit-tests-junit)
17
+ - [Mocking with MockK](#mocking-with-mockk)
18
+ - [Testing Coroutines](#testing-coroutines)
19
+ - [Testing Flows with Turbine](#testing-flows-with-turbine)
20
+ - [Testing ViewModels](#testing-viewmodels)
21
+ - [Robolectric](#robolectric)
22
+ - [Compose UI Testing](#compose-ui-testing)
23
+ - [Espresso](#espresso)
24
+ - [Common Mistakes](#common-mistakes)
25
+ - [Review Checklist](#review-checklist)
26
+
27
+ ## Test Pyramid and Source Sets
28
+
29
+ | Location | Runs on | Use for |
30
+ |---|---|---|
31
+ | `src/test/` | Local JVM (`testDebugUnitTest`) | Logic, ViewModels, repos, mappers, Robolectric |
32
+ | `src/androidTest/` | Device/emulator (`connectedDebugAndroidTest`) | Espresso, Compose UI, integration |
33
+
34
+ Keep most tests in `src/test/` — they are fast and run in the unit-test gate.
35
+ Reserve `src/androidTest/` for tests that genuinely need a device/emulator.
36
+
37
+ ## Unit Tests (JUnit)
38
+
39
+ JUnit 4 is still the Android default; JUnit 5 is fine for pure-JVM modules with
40
+ the platform engine configured. Name tests by behavior.
41
+
42
+ ```kotlin
43
+ class PriceFormatterTest {
44
+ @Test
45
+ fun `formats whole dollars without decimals`() {
46
+ assertEquals("$5", PriceFormatter.format(500))
47
+ }
48
+ }
49
+ ```
50
+
51
+ Prefer a fluent assertion library (Truth or AssertK) for readable failures:
52
+
53
+ ```kotlin
54
+ assertThat(result).isEqualTo(expected)
55
+ assertThat(items).containsExactly(a, b).inOrder()
56
+ ```
57
+
58
+ ## Mocking with MockK
59
+
60
+ MockK is the idiomatic Kotlin mocking library (handles final classes, coroutines,
61
+ and relaxed mocks). Prefer fakes for owned interfaces; use mocks for verifying
62
+ interactions or stubbing boundaries.
63
+
64
+ ```kotlin
65
+ @Test
66
+ fun `loads profile from repo`() = runTest {
67
+ val repo = mockk<ProfileRepository>()
68
+ coEvery { repo.profile(42) } returns Profile(name = "Ada")
69
+
70
+ val result = ProfileService(repo).load(42)
71
+
72
+ assertThat(result.name).isEqualTo("Ada")
73
+ coVerify(exactly = 1) { repo.profile(42) }
74
+ }
75
+ ```
76
+
77
+ - Use `coEvery`/`coVerify` for suspend functions.
78
+ - Use `relaxed = true` to auto-stub return values you don't care about.
79
+ - Prefer hand-written **fakes** over mocks for repositories/data sources you own —
80
+ they're more robust and document behavior.
81
+
82
+ ## Testing Coroutines
83
+
84
+ Use `kotlinx-coroutines-test`. `runTest` provides a `TestScope` with a virtual
85
+ clock that auto-advances through `delay`.
86
+
87
+ ```kotlin
88
+ @Test
89
+ fun `retries after delay`() = runTest {
90
+ val result = withTimeout(1.seconds) { service.fetchWithRetry() }
91
+ assertThat(result).isNotNull()
92
+ // delay(30_000) inside fetchWithRetry is skipped — virtual time
93
+ }
94
+ ```
95
+
96
+ - Inject dispatchers so tests can substitute a test dispatcher. Provide a
97
+ `MainDispatcherRule` to swap `Dispatchers.Main` (needed for `viewModelScope`):
98
+
99
+ ```kotlin
100
+ class MainDispatcherRule(
101
+ private val dispatcher: TestDispatcher = StandardTestDispatcher(),
102
+ ) : TestWatcher() {
103
+ override fun starting(d: Description) = Dispatchers.setMain(dispatcher)
104
+ override fun finished(d: Description) = Dispatchers.resetMain()
105
+ }
106
+ ```
107
+
108
+ - `StandardTestDispatcher` queues coroutines — call `advanceUntilIdle()` /
109
+ `runCurrent()` to run them. `UnconfinedTestDispatcher` runs eagerly — handy for
110
+ simple cases but order-sensitive.
111
+ - Never use `Thread.sleep` or real `delay` waits in tests.
112
+
113
+ ## Testing Flows with Turbine
114
+
115
+ Turbine makes Flow assertions deterministic without manual collection
116
+ boilerplate. Use it for `Flow`, `StateFlow`, and `SharedFlow`.
117
+
118
+ ```kotlin
119
+ @Test
120
+ fun `emits loading then loaded`() = runTest {
121
+ viewModel.uiState.test {
122
+ assertThat(awaitItem()).isEqualTo(UiState.Loading)
123
+ viewModel.load()
124
+ assertThat(awaitItem()).isInstanceOf(UiState.Loaded::class.java)
125
+ cancelAndIgnoreRemainingEvents()
126
+ }
127
+ }
128
+ ```
129
+
130
+ - `awaitItem()` suspends for the next emission; `awaitError()` / `awaitComplete()`
131
+ for terminal events.
132
+ - For `StateFlow`, the first `awaitItem()` is the current value; call
133
+ `cancelAndIgnoreRemainingEvents()` (or `expectMostRecentItem()`) to finish.
134
+ - `test {}` fails if unconsumed events remain — this catches unexpected emissions.
135
+
136
+ ## Testing ViewModels
137
+
138
+ Combine the `MainDispatcherRule`, `runTest`, fakes, and Turbine:
139
+
140
+ ```kotlin
141
+ class FeedViewModelTest {
142
+ @get:Rule val mainDispatcherRule = MainDispatcherRule()
143
+
144
+ @Test
145
+ fun `shows error when repo throws`() = runTest {
146
+ val repo = FakeFeedRepository(throws = IOException())
147
+ val vm = FeedViewModel(repo)
148
+
149
+ vm.state.test {
150
+ assertThat(awaitItem()).isEqualTo(FeedUiState.Loading)
151
+ assertThat(awaitItem()).isInstanceOf(FeedUiState.Error::class.java)
152
+ cancelAndIgnoreRemainingEvents()
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ ## Robolectric
159
+
160
+ Robolectric runs Android-framework-dependent code on the JVM (no emulator), so
161
+ it stays in `src/test/`. Use it when a unit test needs `Context`, resources,
162
+ `SharedPreferences`, or Android components — but the logic doesn't need a real
163
+ device.
164
+
165
+ ```kotlin
166
+ @RunWith(RobolectricTestRunner::class)
167
+ class ResourceTest {
168
+ @Test
169
+ fun `reads string resource`() {
170
+ val context = ApplicationProvider.getApplicationContext<Context>()
171
+ assertThat(context.getString(R.string.app_name)).isEqualTo("Demo")
172
+ }
173
+ }
174
+ ```
175
+
176
+ Prefer pure unit tests where possible; reach for Robolectric only when framework
177
+ types are genuinely involved. It is slower than plain JVM tests but far faster
178
+ than instrumented ones.
179
+
180
+ ## Compose UI Testing
181
+
182
+ Use `createComposeRule()` (no Activity) or `createAndroidComposeRule<Activity>()`.
183
+ These can run as Robolectric tests in `src/test/` or instrumented in
184
+ `src/androidTest/`. Query by **semantics**, and add `Modifier.testTag` /
185
+ content descriptions for stable selectors.
186
+
187
+ ```kotlin
188
+ class CounterTest {
189
+ @get:Rule val composeRule = createComposeRule()
190
+
191
+ @Test
192
+ fun increments_on_click() {
193
+ composeRule.setContent { Counter() }
194
+
195
+ composeRule.onNodeWithText("Count: 0").assertIsDisplayed()
196
+ composeRule.onNodeWithTag("increment").performClick()
197
+ composeRule.onNodeWithText("Count: 1").assertExists()
198
+ }
199
+ }
200
+ ```
201
+
202
+ - Compose tests synchronize automatically with the runtime; avoid arbitrary
203
+ waits. For non-Compose async, use `composeRule.waitUntil { ... }`.
204
+ - Use `onNodeWithTag`/`onNodeWithContentDescription` for robust, localized-text-
205
+ independent selectors.
206
+ - Disable indeterminate animations or use `mainClock.autoAdvance = false` to
207
+ control time when asserting animated states.
208
+
209
+ ## Espresso
210
+
211
+ Espresso drives real instrumented UI tests (`src/androidTest/`, runs on an
212
+ emulator/device). Use for end-to-end flows and legacy View-based screens.
213
+
214
+ ```kotlin
215
+ @RunWith(AndroidJUnit4::class)
216
+ class LoginFlowTest {
217
+ @get:Rule val activityRule = ActivityScenarioRule(LoginActivity::class.java)
218
+
219
+ @Test
220
+ fun successful_login_navigates_home() {
221
+ onView(withId(R.id.email)).perform(typeText("a@b.com"))
222
+ onView(withId(R.id.submit)).perform(click())
223
+ onView(withId(R.id.home)).check(matches(isDisplayed()))
224
+ }
225
+ }
226
+ ```
227
+
228
+ - Idle synchronization is automatic for the main thread; register an
229
+ `IdlingResource` for custom async work.
230
+ - Keep Espresso tests few — they are slow and flaky-prone. Push coverage down to
231
+ unit/Compose tests.
232
+
233
+ ## Common Mistakes
234
+
235
+ 1. **`Thread.sleep`/real delays in tests** — use `runTest` virtual time and
236
+ Turbine/`waitUntil`.
237
+ 2. **No `MainDispatcherRule`** when testing `viewModelScope` — `Dispatchers.Main`
238
+ isn't available on the JVM and the test fails or hangs.
239
+ 3. **Hardcoded dispatchers** in production code — can't substitute test
240
+ dispatchers; inject them.
241
+ 4. **Manual Flow collection** in tests — use Turbine for determinism.
242
+ 5. **Over-mocking owned types** — prefer fakes; mocks couple tests to call order.
243
+ 6. **Putting everything in `androidTest`** — slow; most tests belong in `test/`.
244
+ 7. **Asserting on localized text** in UI tests — use `testTag`/content
245
+ descriptions.
246
+ 8. **Testing implementation details** — assert observable behavior/state.
247
+ 9. **Leaking `Dispatchers.setMain`** — always `resetMain()` (the rule handles it).
248
+ 10. **Ignoring Turbine unconsumed-event failures** — they reveal real bugs;
249
+ don't blanket-`cancelAndIgnore` without checking.
250
+
251
+ ## Review Checklist
252
+
253
+ - [ ] Most tests are fast JVM unit tests in `src/test/`
254
+ - [ ] Coroutine tests use `runTest`; no real sleeps/delays
255
+ - [ ] `MainDispatcherRule` present for ViewModel/`viewModelScope` tests
256
+ - [ ] Dispatchers injected for testability
257
+ - [ ] Flow assertions use Turbine (`awaitItem`/`awaitError`)
258
+ - [ ] Fakes preferred over mocks for owned dependencies; MockK for boundaries
259
+ - [ ] Robolectric used only when Android framework types are needed
260
+ - [ ] Compose tests query by semantics/`testTag`, not localized text
261
+ - [ ] Espresso reserved for true end-to-end instrumented flows
262
+ - [ ] Tests assert behavior, not implementation
263
+ - [ ] Error/cancellation paths covered, not just the happy path