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.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/core/agents/aaron.md +152 -0
- package/core/agents/bert.md +115 -0
- package/core/agents/isabelle.md +136 -0
- package/core/agents/jody.md +150 -0
- package/core/agents/leith.md +111 -0
- package/core/agents/marcelo.md +282 -0
- package/core/agents/melvin.md +101 -0
- package/core/agents/nyx.md +152 -0
- package/core/agents/otto.md +168 -0
- package/core/agents/patricia.md +283 -0
- package/core/commands/design-audit-local.md +155 -0
- package/core/commands/design-audit.md +235 -0
- package/core/commands/design-critique.md +96 -0
- package/core/commands/file-issue.md +22 -0
- package/core/commands/generate-project.md +45 -0
- package/core/commands/implement-issue.md +37 -0
- package/core/commands/implement-project.md +40 -0
- package/core/commands/naturalize.md +61 -0
- package/core/commands/pre-push.md +29 -0
- package/core/commands/prep-review-collect.md +130 -0
- package/core/commands/prep-review-finalize.md +121 -0
- package/core/commands/prep-review-publish.md +113 -0
- package/core/commands/prep-review.md +65 -0
- package/core/commands/project-closeout.md +25 -0
- package/core/skills/agentic-eval/SKILL.md +195 -0
- package/core/skills/chrome-devtools/SKILL.md +97 -0
- package/core/skills/code-review/SKILL.md +26 -0
- package/core/skills/gh-cli/SKILL.md +2202 -0
- package/core/skills/git-commit/SKILL.md +124 -0
- package/core/skills/git-workflow-agents/SKILL.md +462 -0
- package/core/skills/git-workflow-agents/reference.md +220 -0
- package/core/skills/github-actions/SKILL.md +190 -0
- package/core/skills/github-issues/SKILL.md +154 -0
- package/core/skills/llm-structured-outputs/SKILL.md +323 -0
- package/core/skills/llm-structured-outputs/references/provider-details.md +392 -0
- package/core/skills/pre-push/SKILL.md +115 -0
- package/core/skills/refactor/SKILL.md +645 -0
- package/core/skills/web-design-reviewer/SKILL.md +371 -0
- package/core/skills/webapp-testing/SKILL.md +127 -0
- package/core/skills/webapp-testing/test-helper.js +56 -0
- package/core/templates/CLAUDE.md.tmpl +98 -0
- package/core/templates/adr-template.md +67 -0
- package/core/templates/gh-issue-templates/bug.md +39 -0
- package/core/templates/gh-issue-templates/content.md +42 -0
- package/core/templates/gh-issue-templates/enhancement.md +36 -0
- package/core/templates/gh-issue-templates/feature.md +39 -0
- package/core/templates/gh-issue-templates/infrastructure.md +41 -0
- package/core/templates/post-edit-reminders.sh.tmpl +19 -0
- package/core/templates/settings.json.tmpl +90 -0
- package/core/templates/settings.local.json.tmpl +3 -0
- package/core/workflows/agent-implementation-workflow.md +346 -0
- package/core/workflows/generate-project.md +258 -0
- package/core/workflows/implement-project-workflow.md +190 -0
- package/core/workflows/issue-tracking.md +89 -0
- package/core/workflows/project-closeout-ceremony.md +77 -0
- package/core/workflows/review-workflow.md +266 -0
- package/engsys.config.example.yaml +46 -0
- package/install +202 -0
- package/lessons-library/README.md +80 -0
- package/lessons-library/async-callbacks-verify-liveness.md +15 -0
- package/lessons-library/change-isnt-done-until-every-surface-updated.md +15 -0
- package/lessons-library/claim-then-act-for-irreversible-ops.md +16 -0
- package/lessons-library/co-commit-entangled-work.md +15 -0
- package/lessons-library/dependabot-triage-playbook.md +17 -0
- package/lessons-library/deploy-by-digest-and-verify-the-running-revision.md +15 -0
- package/lessons-library/enforce-your-guarantee-at-your-boundary.md +16 -0
- package/lessons-library/gate-changes-on-measurement-not-vibes.md +15 -0
- package/lessons-library/iac-first-no-console-changes.md +15 -0
- package/lessons-library/independent-objective-review-gate.md +15 -0
- package/lessons-library/keep-an-immutable-source-of-truth.md +15 -0
- package/lessons-library/long-agent-runs-checkpoint-not-poll.md +15 -0
- package/lessons-library/model-identity-with-stable-ids-and-provenance.md +15 -0
- package/lessons-library/operator-choices-are-first-class.md +15 -0
- package/lessons-library/prefer-tool-enforced-structured-output.md +15 -0
- package/lessons-library/prove-causation-before-acting.md +15 -0
- package/lessons-library/re-read-state-before-acting.md +14 -0
- package/lessons-library/read-layer-tolerates-unbackfilled-rows.md +15 -0
- package/lessons-library/shell-safety-pipefail-and-validate-before-teardown.md +14 -0
- package/lessons-library/shift-correctness-left-and-distrust-false-greens.md +15 -0
- package/lessons-library/stray-control-bytes-hide-changes.md +14 -0
- package/lessons-library/tests-can-assert-the-bug.md +15 -0
- package/lessons-library/verify-ground-truth-not-reports.md +15 -0
- package/lessons-library/worktrees-need-bootstrap-from-origin-main.md +15 -0
- package/lib/commands.js +356 -0
- package/lib/generate-team-avatars.mjs +251 -0
- package/lib/manifest.js +155 -0
- package/lib/render.js +135 -0
- package/lib/selftest.js +90 -0
- package/lib/util.js +89 -0
- package/lib/yaml.js +156 -0
- package/optional-agents/gary.md +86 -0
- package/optional-agents/jos.md +136 -0
- package/optional-agents/sandy.md +101 -0
- package/optional-agents/steve.md +161 -0
- package/package.json +43 -0
- package/stacks/cloud/aws/claude.fragment.md +17 -0
- package/stacks/cloud/aws/settings.fragment.json +39 -0
- package/stacks/cloud/aws/skills/aws-deployment-preflight/SKILL.md +165 -0
- package/stacks/cloud/aws/skills/cloud-architecture-aws/SKILL.md +265 -0
- package/stacks/cloud/azure/claude.fragment.md +17 -0
- package/stacks/cloud/azure/settings.fragment.json +45 -0
- package/stacks/cloud/azure/skills/azure-deployment-preflight/SKILL.md +175 -0
- package/stacks/cloud/azure/skills/cloud-architecture-azure/SKILL.md +211 -0
- package/stacks/cloud/cloudflare/claude.fragment.md +21 -0
- package/stacks/cloud/cloudflare/settings.fragment.json +31 -0
- package/stacks/cloud/cloudflare/skills/cloud-architecture-cloudflare/SKILL.md +294 -0
- package/stacks/cloud/cloudflare/skills/cloudflare-deployment-preflight/SKILL.md +175 -0
- package/stacks/cloud/gcp/claude.fragment.md +17 -0
- package/stacks/cloud/gcp/settings.fragment.json +40 -0
- package/stacks/cloud/gcp/skills/cloud-architecture-gcp/SKILL.md +208 -0
- package/stacks/cloud/gcp/skills/gcp-deployment-preflight/SKILL.md +137 -0
- package/stacks/db/mongo/skills/mongo-conventions/SKILL.md +96 -0
- package/stacks/db/prisma/claude.fragment.md +49 -0
- package/stacks/db/prisma/skills/docker-database-package-copy/SKILL.md +44 -0
- package/stacks/db/prisma/skills/prisma-conventions/SKILL.md +37 -0
- package/stacks/domain/mobile-growth/skills/apple-ads/SKILL.md +184 -0
- package/stacks/domain/mobile-growth/skills/apple-ads/references/benchmark-notes.md +47 -0
- package/stacks/domain/mobile-growth/skills/apple-ads/references/official-links.md +53 -0
- package/stacks/domain/mobile-growth/skills/google-play-growth/SKILL.md +197 -0
- package/stacks/domain/mobile-growth/skills/google-play-growth/references/benchmark-notes.md +47 -0
- package/stacks/domain/mobile-growth/skills/google-play-growth/references/official-links.md +45 -0
- package/stacks/iac/bicep/claude.fragment.md +14 -0
- package/stacks/iac/bicep/settings.fragment.json +20 -0
- package/stacks/iac/bicep/skills/iac-bicep/SKILL.md +113 -0
- package/stacks/iac/cdk/claude.fragment.md +14 -0
- package/stacks/iac/cdk/settings.fragment.json +23 -0
- package/stacks/iac/cdk/skills/iac-cdk/SKILL.md +104 -0
- package/stacks/iac/terraform/claude.fragment.md +13 -0
- package/stacks/iac/terraform/settings.fragment.json +25 -0
- package/stacks/iac/terraform/skills/iac-terraform/SKILL.md +93 -0
- package/stacks/iac/terraform/skills/terraform-conventions/SKILL.md +87 -0
- package/stacks/lang/kotlin/skills/android-testing/SKILL.md +263 -0
- package/stacks/lang/kotlin/skills/jetpack-compose/SKILL.md +264 -0
- package/stacks/lang/kotlin/skills/kotlin-coroutines/SKILL.md +329 -0
- package/stacks/lang/python/skills/python-conventions/SKILL.md +61 -0
- package/stacks/lang/shell/skills/shell-scripting/SKILL.md +110 -0
- package/stacks/lang/swift/skills/swift-concurrency/SKILL.md +423 -0
- package/stacks/lang/swift/skills/swift-concurrency/references/approachable-concurrency.md +80 -0
- package/stacks/lang/swift/skills/swift-concurrency/references/concurrency-patterns.md +233 -0
- package/stacks/lang/swift/skills/swift-concurrency/references/swiftui-concurrency.md +187 -0
- package/stacks/lang/swift/skills/swift-concurrency/references/synchronization-primitives.md +341 -0
- package/stacks/lang/swift/skills/swift-testing/SKILL.md +497 -0
- package/stacks/lang/swift/skills/swift-testing/references/testing-advanced.md +106 -0
- package/stacks/lang/swift/skills/swift-testing/references/testing-patterns.md +504 -0
- package/stacks/lang/swift/skills/swiftdata/SKILL.md +334 -0
- package/stacks/lang/swift/skills/swiftdata/references/core-data-coexistence.md +504 -0
- package/stacks/lang/swift/skills/swiftdata/references/swiftdata-advanced.md +975 -0
- package/stacks/lang/swift/skills/swiftdata/references/swiftdata-queries.md +675 -0
- package/stacks/lang/swift/skills/swiftui-patterns/SKILL.md +371 -0
- package/stacks/lang/swift/skills/swiftui-patterns/references/architecture-patterns.md +486 -0
- package/stacks/lang/swift/skills/swiftui-patterns/references/deprecated-migration.md +1097 -0
- package/stacks/lang/swift/skills/swiftui-patterns/references/design-polish.md +780 -0
- package/stacks/lang/swift/skills/swiftui-patterns/references/platform-and-sharing.md +696 -0
- package/stacks/lang/typescript/skills/typescript-conventions/SKILL.md +91 -0
- package/stacks/platform/android/claude.fragment.md +40 -0
- package/stacks/platform/android/hooks/pre-push-gradle.sh +70 -0
- package/stacks/platform/android/settings.fragment.json +13 -0
- package/stacks/platform/android/skills/android-build-conventions/SKILL.md +247 -0
- package/stacks/platform/ios/claude.fragment.md +24 -0
- package/stacks/platform/ios/hooks/pre-push-xcodebuild.sh +82 -0
- package/stacks/platform/ios/settings.fragment.json +21 -0
- package/stacks/platform/ios/skills/xcodebuildmcp-simulator-logs/SKILL.md +76 -0
- package/stacks/platform/web/skills/frontend-testing/SKILL.md +246 -0
- package/stacks/platform/web/skills/react-conventions/SKILL.md +261 -0
- package/stacks/platform/web/skills/web-platform-conventions/SKILL.md +55 -0
- package/stacks/tooling/issue-tracker-github/claude.fragment.md +10 -0
- package/stacks/tooling/issue-tracker-github/settings.fragment.json +24 -0
- package/stacks/tooling/issue-tracker-github/skills/issue-tracker-github/SKILL.md +278 -0
- package/stacks/tooling/issue-tracker-linear/claude.fragment.md +17 -0
- package/stacks/tooling/issue-tracker-linear/settings.fragment.json +9 -0
- 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
|