@terreno/agent-plugin 0.0.0-stage → 57.22.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 (98) hide show
  1. package/.claude-plugin/plugin.json +32 -0
  2. package/CHANGELOG.md +2155 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -2
  5. package/agents/pre-commit.md +65 -0
  6. package/agents/ui-verifier.md +93 -0
  7. package/package.json +32 -4
  8. package/references/async-review-bots.md +76 -0
  9. package/references/documentation-contract.md +61 -0
  10. package/references/execution-state.schema.json +77 -0
  11. package/references/github-attention-contract.md +165 -0
  12. package/references/independent-review.md +31 -0
  13. package/references/lifecycle-contract.md +211 -0
  14. package/references/loop-engineering.md +125 -0
  15. package/references/pick-roast-loop.md +112 -0
  16. package/references/pr-deployments.md +69 -0
  17. package/references/product-ci.md +164 -0
  18. package/references/push-ownership.md +91 -0
  19. package/references/stage-result.schema.json +82 -0
  20. package/references/subagent-briefing.md +43 -0
  21. package/references/testing-plan.md +163 -0
  22. package/references/user-guide.md +171 -0
  23. package/skills/1-grow/SKILL.md +145 -0
  24. package/skills/1-grow/references/grilling.md +243 -0
  25. package/skills/2-pick/SKILL.md +170 -0
  26. package/skills/2-pick/references/mocking.md +34 -0
  27. package/skills/2-pick/references/testing.md +43 -0
  28. package/skills/3-roast/SKILL.md +168 -0
  29. package/skills/4-brew/SKILL.md +201 -0
  30. package/skills/5-taste/SKILL.md +311 -0
  31. package/skills/ai-prompt-governance/SKILL.md +100 -0
  32. package/skills/attach-pr-media/SKILL.md +89 -0
  33. package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
  34. package/skills/backend-test-env/SKILL.md +64 -0
  35. package/skills/backfill-regression/SKILL.md +101 -0
  36. package/skills/backfill-user-guide/SKILL.md +96 -0
  37. package/skills/bootstrap-mcp-server/SKILL.md +244 -0
  38. package/skills/building-admin-interfaces/SKILL.md +83 -0
  39. package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
  40. package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
  41. package/skills/building-admin-interfaces/references/patterns-and-tools.md +73 -0
  42. package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
  43. package/skills/building-terreno-apps/SKILL.md +280 -0
  44. package/skills/building-terreno-apps/agents/openai.yaml +4 -0
  45. package/skills/building-terreno-apps/references/navigation.md +102 -0
  46. package/skills/building-terreno-apps/references/route-structure.md +91 -0
  47. package/skills/building-terreno-apps/references/screen-patterns.md +191 -0
  48. package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
  49. package/skills/deploy-gcp/SKILL.md +73 -0
  50. package/skills/deploy-gcp/references/troubleshooting.md +29 -0
  51. package/skills/generate-release-announcements/SKILL.md +145 -0
  52. package/skills/generate-sdk/SKILL.md +156 -0
  53. package/skills/mongoose-schema-safety/SKILL.md +164 -0
  54. package/skills/pick-roast-loop/SKILL.md +152 -0
  55. package/skills/planning-loop/SKILL.md +161 -0
  56. package/skills/prd/SKILL.md +138 -0
  57. package/skills/set-up-social-login/SKILL.md +76 -0
  58. package/skills/store-assets/SKILL.md +221 -0
  59. package/skills/store-assets/examples/screenshots.yaml +31 -0
  60. package/skills/store-assets/examples/storeAssets.config.json +44 -0
  61. package/skills/store-assets/scripts/banner.ts +165 -0
  62. package/skills/store-assets/scripts/config.ts +255 -0
  63. package/skills/store-assets/scripts/devClient.ts +635 -0
  64. package/skills/store-assets/scripts/devices.ts +286 -0
  65. package/skills/store-assets/scripts/icon.ts +81 -0
  66. package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
  67. package/skills/store-assets/scripts/maestro/login.yaml +27 -0
  68. package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
  69. package/skills/store-assets/scripts/screenshots.ts +848 -0
  70. package/skills/taste-sweep/SKILL.md +172 -0
  71. package/skills/terreno-backend-api/SKILL.md +355 -0
  72. package/skills/terreno-backend-api/agents/openai.yaml +4 -0
  73. package/skills/terreno-backend-api/references/custom-routes.md +132 -0
  74. package/skills/terreno-backend-api/references/model-router.md +116 -0
  75. package/skills/terreno-backend-api/references/server-setup.md +116 -0
  76. package/skills/terreno-backend-tests/SKILL.md +176 -0
  77. package/skills/terreno-data-fetching/SKILL.md +276 -0
  78. package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
  79. package/skills/terreno-data-fetching/references/auth-and-tokens.md +93 -0
  80. package/skills/terreno-data-fetching/references/realtime.md +45 -0
  81. package/skills/terreno-data-fetching/references/sdk-customization.md +84 -0
  82. package/skills/terreno-frontend-tests/SKILL.md +190 -0
  83. package/skills/terreno-mobile-tests/SKILL.md +127 -0
  84. package/skills/terreno-test-setup/SKILL.md +81 -0
  85. package/skills/terreno-test-setup/references/backend.md +131 -0
  86. package/skills/terreno-test-setup/references/frontend.md +169 -0
  87. package/skills/terreno-test-setup/references/mobile.md +134 -0
  88. package/skills/terreno-ui/SKILL.md +252 -0
  89. package/skills/terreno-ui/agents/openai.yaml +4 -0
  90. package/skills/terreno-ui/references/data-display.md +85 -0
  91. package/skills/terreno-ui/references/feedback.md +122 -0
  92. package/skills/terreno-ui/references/forms.md +119 -0
  93. package/skills/terreno-ui/references/layout.md +124 -0
  94. package/skills/terreno-ui/references/theming.md +87 -0
  95. package/skills/update-docs/SKILL.md +82 -0
  96. package/skills/upgrading-terreno/SKILL.md +86 -0
  97. package/skills/upgrading-terreno/references/ordering.md +40 -0
  98. package/skills/verify-ui-changes/SKILL.md +605 -0
@@ -0,0 +1,93 @@
1
+ # Auth and Tokens — Terreno
2
+
3
+ ## generateAuthSlice
4
+
5
+ `generateAuthSlice(api)` from `@terreno/rtk` creates:
6
+
7
+ - `authReducer` — tracks `userId`, `error`, `lastTokenRefreshTimestamp`
8
+ - `logout` action — clears tokens from secure storage
9
+ - `middleware` — stores tokens on login, removes on logout
10
+ - Built-in endpoints: `emailLogin`, `emailSignUp`, `googleLogin`, `resetPassword`
11
+
12
+ ## Login
13
+
14
+ ```tsx
15
+ import {useEmailLoginMutation} from "@/store/sdk";
16
+ import {useSelectCurrentUserId} from "@terreno/rtk";
17
+
18
+ const [login, {isLoading, error}] = useEmailLoginMutation();
19
+
20
+ await login({email, password}).unwrap();
21
+ // Tokens stored automatically; setUserId dispatched
22
+ ```
23
+
24
+ ## Logout
25
+
26
+ ```tsx
27
+ import {useAppDispatch, logout} from "../store/index.ts";
28
+
29
+ const dispatch = useAppDispatch();
30
+ dispatch(logout());
31
+ // Clears AUTH_TOKEN and REFRESH_TOKEN from storage
32
+ ```
33
+
34
+ ## Token storage
35
+
36
+ - **Native:** `expo-secure-store` (encrypted)
37
+ - **Web:** `@react-native-async-storage/async-storage` (SSR-safe wrapper)
38
+
39
+ Never store tokens in plain AsyncStorage on native or in `EXPO_PUBLIC_*` env vars.
40
+
41
+ ## Token refresh
42
+
43
+ `emptyApi` from `@terreno/rtk` automatically:
44
+
45
+ - Refreshes access token when < 2 minutes from expiry
46
+ - Retries failed requests after 401 with refreshed token
47
+ - Uses a mutex to prevent concurrent refresh storms
48
+
49
+ No manual refresh logic needed in app code.
50
+
51
+ ## Better Auth (optional)
52
+
53
+ For apps using Better Auth instead of JWT/Passport:
54
+
55
+ ```tsx
56
+ import {createBetterAuthClient, generateBetterAuthSlice, setTokenSource} from "@terreno/rtk";
57
+ import {betterAuthAdapter, bridgeBetterAuthReactClient} from "@terreno/syncdb";
58
+
59
+ const authClient = createBetterAuthClient({
60
+ baseURL: process.env.EXPO_PUBLIC_API_URL,
61
+ scheme: "myapp", // deep-link scheme, required
62
+ });
63
+
64
+ // {reducer, middleware, actions, slice, syncSession}; mount reducer at `betterAuth`
65
+ const betterAuth = generateBetterAuthSlice({authClient});
66
+
67
+ // One token source for syncdb and every rtk consumer, registered once at startup
68
+ export const authProvider = betterAuthAdapter(bridgeBetterAuthReactClient(authClient));
69
+ setTokenSource(authProvider);
70
+ ```
71
+
72
+ With the source registered, `emptySplitApi` sends the Better Auth token, sends no
73
+ `authorization` header when there is none, and logs out only after a 401 survives one
74
+ `refresh()`. Do not fork the base query to read the session. Without `setTokenSource`, rtk
75
+ uses the JWT tokens in storage (`jwtTokenSource`).
76
+
77
+ Use `SocialLoginButton` from `@terreno/ui` for OAuth providers.
78
+
79
+ ## Current user
80
+
81
+ ```tsx
82
+ import {useSelectCurrentUserId} from "@terreno/rtk";
83
+ import {useReadProfile} from "@/hooks/useReadProfile"; // app-specific
84
+
85
+ const userId = useSelectCurrentUserId();
86
+ const {data: profile} = useReadProfile(); // GET /auth/me via sdk.ts
87
+ ```
88
+
89
+ ## Protected routes
90
+
91
+ Gate navigation on `userId` in root `_layout.tsx`. Redirect unauthenticated users to `/login`.
92
+
93
+ For admin routes, also check `user.admin === true` and redirect non-admins.
@@ -0,0 +1,45 @@
1
+ # Realtime — Terreno
2
+
3
+ ## Realtime sockets
4
+
5
+ ```tsx
6
+ import {useSocketConnection, getAuthToken} from "@terreno/rtk";
7
+
8
+ const {socket, isSocketConnected} = useSocketConnection({
9
+ baseUrl: "ws://localhost:4000",
10
+ shouldConnect: !!userId,
11
+ getAuthToken: () => getAuthToken(),
12
+ });
13
+ ```
14
+
15
+ Sockets auto-reconnect and refresh auth on token rotation.
16
+
17
+ ## Feature flags
18
+
19
+ ```tsx
20
+ import {useTerrenoFeatureFlags} from "@terreno/rtk";
21
+
22
+ useTerrenoFeatureFlags(terrenoApi, {
23
+ skip: !userId,
24
+ socket,
25
+ userId,
26
+ });
27
+ ```
28
+
29
+ Wrap children in `OpenFeatureProvider` (see `example-frontend/app/_layout.tsx`).
30
+
31
+ ## Server status
32
+
33
+ ```tsx
34
+ import {useServerStatus} from "@terreno/rtk";
35
+
36
+ const {isServerReachable} = useServerStatus();
37
+ ```
38
+
39
+ Use to show degraded UI when the backend is unreachable.
40
+
41
+ ## Realtime model updates
42
+
43
+ `modelRouter` `realtime` (RTK cache-patching via `realtimeList` / `realtimeDocument`) is **deprecated** and **will be removed in Terreno 58**. Use `sync` on `modelRouter` with `@terreno/syncdb`. `RealtimeApp` stays as the socket host.
44
+
45
+ Do not add new `realtimeList` / `realtimeDocument` wiring. After adding `sync` to a backend model, use syncdb hooks on the client.
@@ -0,0 +1,84 @@
1
+ # SDK Customization — Terreno
2
+
3
+ ## Generated vs hand-maintained
4
+
5
+ | File | Editable? | Purpose |
6
+ |------|-----------|---------|
7
+ | `store/openApiSdk.ts` | **Never** | Auto-generated hooks from OpenAPI |
8
+ | `store/sdk.ts` | **Yes** | Extensions, cache tags, custom endpoints |
9
+ | `openapi-config.ts` | **Yes** | Codegen configuration |
10
+
11
+ ## injectEndpoints
12
+
13
+ Add app-specific endpoints that are not in the OpenAPI spec (rare — prefer backend routes):
14
+
15
+ ```tsx
16
+ import {openapi} from "./openApiSdk";
17
+
18
+ export const terrenoApi = openapi
19
+ .injectEndpoints({
20
+ endpoints: (build) => ({
21
+ getMe: build.query({
22
+ query: () => "/auth/me",
23
+ providesTags: ["profile"],
24
+ }),
25
+ patchMe: build.mutation({
26
+ query: (body) => ({
27
+ url: "/auth/me",
28
+ method: "PATCH",
29
+ body,
30
+ }),
31
+ invalidatesTags: ["profile"],
32
+ }),
33
+ }),
34
+ })
35
+ .enhanceEndpoints({
36
+ addTagTypes: ["todos", "users", "profile"],
37
+ });
38
+ ```
39
+
40
+ Import hooks from `@/store/sdk`, not `@/store/openApiSdk` directly.
41
+
42
+ ## Cache tags
43
+
44
+ Use `enhanceEndpoints` to add tag types, then `providesTags` / `invalidatesTags` in injected endpoints:
45
+
46
+ ```tsx
47
+ import {invalidatesIdTags, providesIdTags} from "@terreno/rtk";
48
+
49
+ // In injectEndpoints:
50
+ listItems: build.query({
51
+ query: (params) => ({url: "/items", params}),
52
+ providesTags: providesIdTags("items"),
53
+ }),
54
+ createItem: build.mutation({
55
+ query: (body) => ({url: "/items", method: "POST", body}),
56
+ invalidatesTags: invalidatesIdTags("items"),
57
+ }),
58
+ ```
59
+
60
+ ## Codegen config
61
+
62
+ ```typescript
63
+ // openapi-config.ts
64
+ const config: ConfigFile = {
65
+ apiFile: "@terreno/rtk",
66
+ apiImport: "emptySplitApi",
67
+ outputFile: "./store/openApiSdk.ts",
68
+ schemaFile: "http://localhost:4000/openapi.json",
69
+ hooks: true,
70
+ tag: true,
71
+ flattenArg: true,
72
+ };
73
+ ```
74
+
75
+ Override schema URL with `OPENAPI_URL` env var when needed.
76
+
77
+ ## Workflow after backend change
78
+
79
+ 1. Start backend (`bun run backend:dev`)
80
+ 2. Run `cd example-frontend && bun run sdk`
81
+ 3. Import new hooks from `@/store/sdk`
82
+ 4. If the endpoint needs custom cache behavior, update `sdk.ts`
83
+
84
+ See the `generate-sdk` skill for the full procedure.
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: terreno-frontend-tests
3
+ description: >-
4
+ Write and fix frontend tests for a Terreno Expo app: Bun + React Native Testing Library unit
5
+ tests for components, screens, hooks, Redux/RTK and syncdb data, and Playwright web E2E for
6
+ flows across screens and the backend. Covers renderWithTerreno, testID selectors, mocking
7
+ data hooks without leaking module mocks, async waiting, and Playwright login/fixtures. Use
8
+ when adding or changing frontend behavior or when a frontend test fails. If the app has no
9
+ test preload or Playwright config, run terreno-test-setup first.
10
+ ---
11
+
12
+ # Writing Terreno frontend tests
13
+
14
+ ## 1. Pick the layer
15
+
16
+ | Behavior | Layer |
17
+ |----------|-------|
18
+ | Pure function, reducer, selector, formatter | Bun unit test, no rendering |
19
+ | Component or screen renders states and reacts to input | Bun + React Native Testing Library |
20
+ | Custom hook | `renderHook` with `createTerrenoWrapper` |
21
+ | Flow across screens, real backend, auth, navigation, persistence | Playwright web E2E |
22
+ | Native-only behavior (gestures, push, camera, deep links on device) | `terreno-mobile-tests` |
23
+
24
+ Prefer the lowest layer that proves the behavior; add one E2E per user-visible flow, not per
25
+ edge case.
26
+
27
+ Done when: the test sits next to the code (`TodoList.tsx` → `TodoList.test.tsx`; E2E in
28
+ `e2e/<feature>.spec.ts`).
29
+
30
+ ## 2. testIDs first
31
+
32
+ Every element a test touches gets a `testID` in the component, named
33
+ `{screen}-{element}-{qualifier}`: `todos-screen`, `todos-create-button`,
34
+ `todo-item-${id}`, `login-screen-email-input`. Add missing IDs to the component before writing
35
+ the test. Select by `testID` in both layers; fall back to role, then text.
36
+
37
+ Always give testIDs to: each screen's root `Box`/`Page`, buttons and pressables, inputs, error
38
+ and validation text, loading indicators, list containers, and list items (dynamic IDs).
39
+
40
+ ## 3. Component tests (Bun + RNTL)
41
+
42
+ ```tsx
43
+ import {describe, expect, it, mock} from "bun:test";
44
+ import {renderWithTerreno} from "@terreno/test/expo/renderWithTerreno";
45
+ import {fireEvent} from "@testing-library/react-native";
46
+ import {TodoForm} from "./TodoForm";
47
+
48
+ describe("TodoForm", () => {
49
+ it("submits the trimmed title", async () => {
50
+ const onSubmit = mock((_title: string) => undefined);
51
+ const {findByTestId, getByTestId} = renderWithTerreno(<TodoForm onSubmit={onSubmit} />);
52
+ await findByTestId("todo-form");
53
+
54
+ fireEvent.changeText(getByTestId("todo-form-title-input"), " Buy milk ");
55
+ fireEvent.press(getByTestId("todo-form-submit-button"));
56
+
57
+ expect(onSubmit).toHaveBeenCalledWith("Buy milk");
58
+ });
59
+
60
+ it("shows a validation error for an empty title", async () => {
61
+ const {findByTestId, getByTestId} = renderWithTerreno(<TodoForm onSubmit={mock()} />);
62
+ await findByTestId("todo-form");
63
+ fireEvent.press(getByTestId("todo-form-submit-button"));
64
+ expect(await findByTestId("todo-form-title-error")).toBeTruthy();
65
+ });
66
+ });
67
+ ```
68
+
69
+ - `renderWithTerreno(ui, {store?, theme?})` wraps in `TerrenoProvider` (and Redux `Provider`
70
+ with `store`). Use it instead of hand-wrapping.
71
+ - Start every test with `await findByTestId("<root testID>")`: `TerrenoProvider` loads fonts
72
+ asynchronously, and querying synchronously before that settles logs
73
+ `not wrapped in act(...)` warnings.
74
+ - Then `getBy*` for what is rendered, `findBy*` for what appears after an effect or promise,
75
+ `queryBy*` to assert absence.
76
+ - Assert with `bun:test`'s `expect`: `expect(getByText("Buy milk")).toBeTruthy()`,
77
+ `within(getByTestId("todos-list")).getByText(...)`, `expect(queryByTestId(...)).toBeNull()`.
78
+ RNTL's extra matchers (`toHaveTextContent`, `toBeVisible`) run but do not type-check
79
+ against `bun:test`, so `tsc` fails on them.
80
+ - Import order: `bun:test`, then `@terreno/*`, then `@testing-library/*`, then local files
81
+ (what Biome's `organizeImports` expects).
82
+ - Interact with `fireEvent.press` / `fireEvent.changeText`; Terreno `Button` calls `onClick`
83
+ through `onPress`.
84
+ - Cover each state the component has: loading, empty, error, populated, disabled.
85
+ - Hooks: `renderHook(() => useThing(), {wrapper: createTerrenoWrapper({store})})` from
86
+ `@testing-library/react-native` and `@terreno/test/expo/renderWithTerreno`.
87
+
88
+ ## 4. Data in component tests
89
+
90
+ Choose in this order:
91
+
92
+ 1. **Props.** Split data loading (container) from rendering (presentational) and test the
93
+ presentational component with literal props. Most screens need nothing else.
94
+ 2. **syncdb collections in screens.** Wrap the app's collection hooks in one module
95
+ (`store/syncDbSdk.ts` exporting `useTodos()` etc.) and mock that module in a preload with
96
+ controllable rows, the same way as RTK hooks below. Test syncing behavior itself (outbox,
97
+ conflicts, deltas) against a real client built with `createFakeTransport` from
98
+ `@terreno/syncdb/testing`; it also needs a fake `httpChannel`, a memory
99
+ `persisterFactory`, and an `authProvider` — copy the `makeHarness` pattern from
100
+ `node_modules/@terreno/syncdb/src/client.test.ts`.
101
+ 3. **Generated RTK hooks (`useGetXQuery`) and app modules.** Mock them **once in a preload**
102
+ (a second preload file after `testPreload.ts`) with controllable state, never with
103
+ `mock.module` inside a test file — module mocks last for the whole process and leak into
104
+ every later file:
105
+ ```typescript
106
+ // tests/sdkMocks.ts (preloaded)
107
+ import {mock} from "bun:test";
108
+ export const sdkState = {todos: [] as unknown[], isLoading: false};
109
+ const actualSdk = await import("@/store/sdk");
110
+ mock.module("@/store/sdk", () => ({
111
+ ...actualSdk,
112
+ useGetTodosQuery: () => ({data: {data: sdkState.todos}, isLoading: sdkState.isLoading}),
113
+ }));
114
+ ```
115
+ Tests set `sdkState` in `beforeEach`; reset it in the preload's `afterEach`.
116
+
117
+ Auth tokens: `setAuthTokens({authToken: "token"})` / `clearAuthTokenStorage()` from
118
+ `@terreno/test/expo/authTokenStorage` seed the in-memory SecureStore/AsyncStorage the preload
119
+ installs (cleared after every test).
120
+
121
+ ## 5. Async, time, and isolation
122
+
123
+ - Wait for UI with `findBy*` or RNTL's `waitFor`. For non-render conditions (mock calls,
124
+ store state, socket events) use `waitUntil` / `waitForCalls` from `@terreno/test`. Never
125
+ sleep with `setTimeout`.
126
+ - Freeze the clock with `freezeTime("2024-06-01T10:30:00Z")` from `@terreno/test` (Date.now,
127
+ new Date, and Luxon together).
128
+ - `spyOn` is restored after every test by the preload. `mock.module` is not — keep it in
129
+ preload files.
130
+ - `terreno-test-hygiene` (from `@terreno/test`) flags module-scope `mock.module`, sleeps,
131
+ barrel mocks, and component snapshots; run it before pushing if the repo uses it.
132
+ - Do not snapshot components (`toMatchSnapshot`): assert the text, testIDs, and props that
133
+ matter.
134
+
135
+ ## 6. Playwright web E2E
136
+
137
+ ```typescript
138
+ import {expect, test} from "@playwright/test";
139
+ import {loginAs} from "./helpers/login";
140
+
141
+ test.describe("Todos", () => {
142
+ test.beforeEach(async ({page}) => {
143
+ await loginAs(page);
144
+ await page.getByTestId("todos-screen").waitFor({state: "visible"});
145
+ });
146
+
147
+ test("user can create a todo", async ({page}) => {
148
+ await page.getByTestId("todos-title-input").fill("Buy milk");
149
+ await page.getByTestId("todos-create-button").click();
150
+ await expect(page.getByTestId("todos-list")).toContainText("Buy milk");
151
+ });
152
+ });
153
+ ```
154
+
155
+ Rules:
156
+
157
+ - Select with `getByTestId()`; fall back to `getByRole()`; use `getByText()` sparingly (React
158
+ Native Web wraps text in extra elements). Never select by class name (`.css-abc123` changes
159
+ every build) or deep structure (`div > div > div`).
160
+ - React Native Web renders `View` as `div`, `Text` as `div`/`span`, `Pressable` as
161
+ `div[role="button"]`, `TextInput` as `input`/`textarea`, `Switch` as `div[role="switch"]`
162
+ with `aria-checked`.
163
+ - Never `page.waitForTimeout()`. After navigation, wait for the target screen's root testID to
164
+ be visible; after a fetch, wait for the content or for the spinner to be hidden; use
165
+ web-first assertions (`await expect(locator).toHaveText(...)`), which retry.
166
+ - Log in through `loginAs(page)` in `test.beforeEach`, never inside a test body.
167
+ - Group with `test.describe()`, navigate in `test.beforeEach()`, name tests as user behavior
168
+ (`"user can submit a valid form"`), and keep tests independent: each creates its own data
169
+ or uses its own fixture user from `e2e/fixtures/testUsers.ts`.
170
+ - Seed through the API (`request.post`) in `beforeEach`/`auth.setup.ts`, not through the UI,
171
+ unless the UI path is what is under test.
172
+
173
+ Run:
174
+
175
+ ```bash
176
+ bunx playwright test e2e/todos.spec.ts --reporter=list
177
+ bunx playwright test --ui # debug interactively
178
+ bunx playwright show-trace test-results/<test>/trace.zip
179
+ ```
180
+
181
+ ## Checklist
182
+
183
+ - [ ] Every touched element has a `{screen}-{element}-{qualifier}` testID
184
+ - [ ] Each component state (loading, empty, error, populated) has a test
185
+ - [ ] Data comes from props or a preload SDK/syncdb-hook mock — no `mock.module` in test
186
+ files
187
+ - [ ] No sleeps, `waitForTimeout`, class selectors, or component snapshots
188
+ - [ ] E2E logs in with `loginAs` in `beforeEach` and waits on screen root testIDs
189
+ - [ ] `bun test` (and `bunx playwright test` for E2E changes) passes for the whole app
190
+ - [ ] UI changes also get runtime evidence via `verify-ui-changes`
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: terreno-mobile-tests
3
+ description: >-
4
+ Write and fix mobile tests for a Terreno Expo app on iOS and Android: Maestro flows that
5
+ drive the native app on a simulator or emulator, and Bun unit tests for Platform.OS-specific
6
+ code. Covers testID selectors on native, login subflows, waiting, platform branches,
7
+ keyboards and system dialogs, reaching the local backend from a device, and debugging flaky
8
+ flows. Use when a change affects native-only behavior (navigation gestures, push, deep
9
+ links, permissions, keyboard, platform branches) or a Maestro flow fails. If the app has no
10
+ Maestro setup, run terreno-test-setup first.
11
+ ---
12
+
13
+ # Writing Terreno mobile tests
14
+
15
+ ## 1. Pick the layer
16
+
17
+ | Behavior | Layer |
18
+ |----------|-------|
19
+ | Logic that branches on `Platform.OS` / `Platform.select` | Bun unit test run per platform (`TEST_PLATFORM=android bun test`) |
20
+ | Component behavior identical on every platform | `terreno-frontend-tests` (Bun + RNTL), once |
21
+ | Flow on a real device: navigation, keyboard, deep link, permission prompt, push tap, app restart | Maestro flow |
22
+ | Same flow in a browser | Playwright (`terreno-frontend-tests`) |
23
+
24
+ Done when: you named the layer and, for Maestro, which platforms (iOS, Android, or both) the
25
+ flow must pass on.
26
+
27
+ ## 2. Platform unit tests
28
+
29
+ The preload fixes `Platform.OS` per run (see `terreno-test-setup`). Write the test once and
30
+ assert the branch for the current platform:
31
+
32
+ ```typescript
33
+ import {describe, expect, it} from "bun:test";
34
+ import {Platform} from "react-native";
35
+ import {keyboardOffset} from "./layout";
36
+
37
+ describe("keyboardOffset", () => {
38
+ it("uses the platform's offset", () => {
39
+ expect(keyboardOffset()).toBe(Platform.OS === "ios" ? 64 : 0);
40
+ });
41
+ });
42
+ ```
43
+
44
+ Run `bun test` and `bun run test:android` (and `test:web` if the code has a web branch).
45
+
46
+ ## 3. Maestro flows
47
+
48
+ Write a flow per user goal, under `maestro/`, reusing subflows for login and navigation:
49
+
50
+ ```yaml
51
+ appId: ${APP_ID}
52
+ ---
53
+ # Creates a todo and sees it in the list after an app restart.
54
+ - launchApp:
55
+ clearState: true
56
+ - runFlow: subflows/login.yaml
57
+ - extendedWaitUntil:
58
+ visible:
59
+ id: "todos-screen"
60
+ timeout: 30000
61
+ - tapOn:
62
+ id: "todos-title-input"
63
+ - inputText: "Buy milk"
64
+ - hideKeyboard
65
+ - tapOn:
66
+ id: "todos-create-button"
67
+ - assertVisible: "Buy milk"
68
+ - stopApp
69
+ - launchApp
70
+ - extendedWaitUntil:
71
+ visible: "Buy milk"
72
+ timeout: 30000
73
+ ```
74
+
75
+ Rules:
76
+
77
+ - **Select by `id:`** (the React Native `testID`). Fall back to visible text only for system
78
+ UI and text you are asserting. Never select by coordinates or index.
79
+ - **Wait for screens** with `extendedWaitUntil: {visible: {id: "<screen>-screen"}, timeout}`
80
+ after launch and every navigation. Never use fixed `waitForAnimationToEnd` chains or sleeps
81
+ as the main wait.
82
+ - **Keyboards**: `hideKeyboard` after `inputText` before tapping something the keyboard may
83
+ cover.
84
+ - **Fresh state**: `launchApp: {clearState: true}` at the start of a flow that must not depend
85
+ on a previous run; seed data through the backend (seed script or API) rather than the UI.
86
+ - **Per-platform steps** with `runFlow: {when: {platform: Android}, commands: [...]}`: Android
87
+ has a system `back`; iOS does not (navigate with the app's back button testID instead).
88
+ - **System dialogs** (permissions, iOS "Save Password?") are optional and platform-specific:
89
+ ```yaml
90
+ - runFlow:
91
+ when:
92
+ visible: "Not Now"
93
+ commands:
94
+ - tapOn: "Not Now"
95
+ ```
96
+ - **Secrets and IDs** come from `-e` env vars (`${APP_ID}`, `${EMAIL}`, `${PASSWORD}`), never
97
+ hard-coded.
98
+ - One goal per flow; `maestro/config.yaml` lists them in order for CI.
99
+
100
+ ## 4. Run and debug
101
+
102
+ ```bash
103
+ maestro test -e APP_ID=com.<org>.<app> -e EMAIL=... -e PASSWORD=... maestro/todos.yaml
104
+ maestro --device <udid|emulator-serial> test ... # pick a device when several are booted
105
+ maestro hierarchy # dump the screen: confirm a testID exists
106
+ maestro studio # build steps interactively
107
+ ```
108
+
109
+ | Symptom | Cause and fix |
110
+ |---------|---------------|
111
+ | `Element not found` for an `id:` | The component does not forward `testID`, or the element is off-screen: check `maestro hierarchy`, add `testID`, or `scrollUntilVisible` |
112
+ | App shows a network error | Backend unreachable: Android needs `adb reverse tcp:4000 tcp:4000` (and `tcp:8081` for Metro) after every emulator start |
113
+ | Passes on iOS, fails on Android | Keyboard covering the target (`hideKeyboard`), or a step that relies on iOS-only UI |
114
+ | First run on a fresh emulator times out | Maestro's driver install is slow the first time; rerun once |
115
+ | Red screen / stale bundle | Metro not running or serving another app: restart `expo start` for this app |
116
+
117
+ Save a screenshot on failure for the PR with `takeScreenshot: <name>` before the failing step,
118
+ or capture with `xcrun simctl io booted screenshot` / `adb exec-out screencap -p > shot.png`.
119
+
120
+ ## Checklist
121
+
122
+ - [ ] Platform branches have unit tests that run under each platform they branch on
123
+ - [ ] Each Maestro flow targets one user goal, starts from `clearState`, and seeds data via the
124
+ backend
125
+ - [ ] Every step selects by `id:` (testID) except system dialogs and asserted text
126
+ - [ ] Screens are awaited with `extendedWaitUntil`; no sleeps
127
+ - [ ] The flow passed on each platform the change affects, and you reported which devices ran
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: terreno-test-setup
3
+ description: >-
4
+ Bootstrap or repair the test setup of a Terreno app: backend Bun tests with in-memory
5
+ Mongo, frontend Bun + React Native Testing Library unit tests and Playwright web E2E, and
6
+ mobile Maestro flows on iOS/Android. Use when an app has no tests yet, `bun test` finds
7
+ nothing or crashes on import, a new backend/frontend/mobile package needs tests, or CI needs
8
+ a test job or sharding. Not for writing individual tests — use terreno-backend-tests,
9
+ terreno-frontend-tests, or terreno-mobile-tests.
10
+ ---
11
+
12
+ # Terreno test setup
13
+
14
+ Bootstrap one layer at a time. Each layer is done only when its completion check passes.
15
+ Apps from `create-terreno-app` have `"test": "bun test"` scripts but no preload, helpers, or
16
+ example test, so start with the backend.
17
+
18
+ ## 0. Inventory
19
+
20
+ 1. Read `backend/package.json`, `frontend/package.json`, every `bunfig.toml`, and any
21
+ existing `*.test.ts(x)`, `e2e/`, or `.maestro/` / `maestro/` directory.
22
+ 2. Note the installed `@terreno/*` version. All `@terreno/*` packages share one version, so
23
+ `@terreno/test` must match it.
24
+ 3. Pick the layers the user asked for. "Set up tests" with no qualifier means backend +
25
+ frontend unit; add Playwright and Maestro only when asked or when CI already expects them.
26
+
27
+ Done when: you can name, per layer, what exists and what is missing.
28
+
29
+ ## 1. Backend (Bun + in-memory Mongo)
30
+
31
+ Follow [references/backend.md](references/backend.md): add `@terreno/test`, a `bunfig.toml`
32
+ preload that calls `registerBackendPreload`, service mocks registered once in that preload,
33
+ and one route test.
34
+
35
+ Done when: `cd backend && bun test` passes with the example route test while the dev
36
+ `mongod` is stopped (proves the suite uses the in-memory server, not the dev database).
37
+
38
+ ## 2. Frontend unit tests (Bun + React Native Testing Library)
39
+
40
+ Follow [references/frontend.md](references/frontend.md): add `@terreno/test`,
41
+ `@testing-library/react-native@^13`, and `react-test-renderer`, a preload that calls
42
+ `registerExpoTestEnvironment`, and one component test using `renderWithTerreno`.
43
+
44
+ Done when: `cd frontend && bun test` passes with the example component test.
45
+
46
+ ## 3. Frontend web E2E (Playwright)
47
+
48
+ Follow [references/frontend.md#playwright-web-e2e](references/frontend.md#playwright-web-e2e):
49
+ `playwright.config.ts` with `webServer` entries for backend + web, `e2e/auth.setup.ts`,
50
+ `e2e/helpers/login.ts` (`loginAs`), and one spec selecting by `testID`.
51
+
52
+ Done when: `bunx playwright test` passes locally against a replica-set Mongo.
53
+
54
+ ## 4. Mobile (Maestro on iOS / Android)
55
+
56
+ Follow [references/mobile.md](references/mobile.md): a dev-client build on a simulator or
57
+ emulator, `maestro/config.yaml` plus one login flow keyed by `testID`, and optional
58
+ per-platform unit runs.
59
+
60
+ Done when: `maestro test maestro/` passes on at least one platform the user targets, and you
61
+ recorded which platforms and devices were exercised.
62
+
63
+ ## 5. Scripts and CI
64
+
65
+ 1. Package scripts: `test` (local), `test:ci` (same suite, no watch), plus `test:e2e` and
66
+ `test:maestro` when those layers exist.
67
+ 2. Large suites: shard with `terreno-test-shard --shard <i>/<n>` (ships with
68
+ `@terreno/test`); add `terreno-test-hygiene --update-baseline` once, then run
69
+ `terreno-test-hygiene` in CI so new flaky patterns fail. See
70
+ [references/backend.md#ci](references/backend.md#ci).
71
+ 3. E2E and Maestro jobs need a replica-set `mongod`, the backend, and the web or native app
72
+ running; unit jobs need nothing but `bun install`.
73
+
74
+ Done when: every layer you set up has a script, and the CI config (if the repo has one)
75
+ runs each script.
76
+
77
+ ## Finish
78
+
79
+ Report each layer as set up / skipped / blocked with the exact command that proves it, then
80
+ point the user at the matching writing skill: `terreno-backend-tests`,
81
+ `terreno-frontend-tests`, `terreno-mobile-tests`.