@terreno/agent-plugin 0.0.0-stage → 58.0.0-beta.2

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 +2213 -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 +86 -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 +74 -0
  42. package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
  43. package/skills/building-terreno-apps/SKILL.md +294 -0
  44. package/skills/building-terreno-apps/agents/openai.yaml +4 -0
  45. package/skills/building-terreno-apps/references/navigation.md +108 -0
  46. package/skills/building-terreno-apps/references/route-structure.md +104 -0
  47. package/skills/building-terreno-apps/references/screen-patterns.md +174 -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 +165 -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 +112 -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 +288 -0
  78. package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
  79. package/skills/terreno-data-fetching/references/auth-and-tokens.md +89 -0
  80. package/skills/terreno-data-fetching/references/realtime.md +50 -0
  81. package/skills/terreno-data-fetching/references/sdk-customization.md +55 -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 +87 -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 +93 -0
  97. package/skills/upgrading-terreno/references/ordering.md +40 -0
  98. package/skills/verify-ui-changes/SKILL.md +605 -0
@@ -0,0 +1,89 @@
1
+ # Auth and Tokens — Terreno
2
+
3
+ Session state lives in `@terreno/syncdb/auth`. There is no Redux store. Reference:
4
+ `docs/reference/syncdb.md#auth`, `example-frontend/lib/betterAuth.ts`, `app/login.tsx`.
5
+
6
+ ## Better Auth client
7
+
8
+ ```tsx
9
+ // lib/betterAuth.ts
10
+ import {createBetterAuthClient} from "@terreno/syncdb/auth";
11
+ import {baseUrl} from "@terreno/syncdb/config";
12
+
13
+ export const betterAuthClient = createBetterAuthClient({
14
+ baseURL: baseUrl,
15
+ scheme: "myapp",
16
+ storagePrefix: "myapp",
17
+ });
18
+ ```
19
+
20
+ Storage is `expo-secure-store` on native and AsyncStorage on web. Never store tokens in
21
+ plain AsyncStorage on native or in `EXPO_PUBLIC_*` env vars.
22
+
23
+ ## Session provider
24
+
25
+ ```tsx
26
+ <SessionProvider authClient={betterAuthClient} syncDbs={[syncDb]} onLogout={() => apiClient.reset()}>
27
+ <App />
28
+ </SessionProvider>
29
+ ```
30
+
31
+ ```tsx
32
+ import {useSession} from "@terreno/syncdb/auth";
33
+
34
+ const {userId, user, isAuthenticated, isLoading, error, logout, refresh} = useSession();
35
+ ```
36
+
37
+ ## Login
38
+
39
+ ```tsx
40
+ const {refresh} = useSession();
41
+
42
+ const result = await betterAuthClient.signIn.email({email, password});
43
+ if (!result.error) {
44
+ await refresh(); // resolve userId now instead of waiting for the session atom
45
+ }
46
+ ```
47
+
48
+ Use `SocialLoginButton` from `@terreno/ui` with `betterAuthClient.signIn.social({provider})`
49
+ for OAuth.
50
+
51
+ ## Logout
52
+
53
+ ```tsx
54
+ const {logout} = useSession();
55
+ await logout();
56
+ ```
57
+
58
+ `logout()` signs out each `syncDbs` client first (wiping local rows when created with
59
+ `wipeOnSignOut: true`), then the auth server, then runs `onLogout`.
60
+
61
+ ## Token refresh
62
+
63
+ `betterAuthAdapter(bridgeBetterAuthReactClient(betterAuthClient))` is the shared
64
+ `AuthProvider` for `createSyncDb` and `createApiClient`. The request client adds
65
+ `Authorization: Bearer`, and on a 401 calls `authProvider.refresh()` once and retries.
66
+ No manual refresh logic in app code. Code outside the client (SSE streams, `AdminProvider`
67
+ `getAuthHeaders`) reads `authProvider.getToken()`.
68
+
69
+ ## JWT backends (legacy)
70
+
71
+ Apps on `@terreno/api` JWT routes use `jwtAuthAdapter` as both the `authProvider` and the
72
+ `SessionProvider` `source`, plus `useJwtAuth({adapter})` for `login`, `signup`,
73
+ `googleLogin`, `resetPassword`. It refreshes when under 130 s remain and on a 401, with one
74
+ shared refresh in flight. See `docs/reference/syncdb.md#jwt-auth-legacy`.
75
+
76
+ ## Current user
77
+
78
+ ```tsx
79
+ import {useGetAuthMeQuery} from "@/store/apiSdk";
80
+
81
+ const {userId} = useSession();
82
+ const {data: me} = useGetAuthMeQuery(undefined, {skip: !userId});
83
+ ```
84
+
85
+ ## Protected routes
86
+
87
+ Gate navigation on `useSession().userId` in root `_layout.tsx`. Redirect unauthenticated
88
+ users to `/login`. For admin routes, check `canOpenAdminPage({admin: me?.admin, permissions:
89
+ me?.permissions})` from `@terreno/syncdb/app`.
@@ -0,0 +1,50 @@
1
+ # Realtime — Terreno
2
+
3
+ The syncdb client owns the app's only socket. Do not open a second one.
4
+
5
+ ## Server push events
6
+
7
+ ```tsx
8
+ import {useSocketEvent} from "@terreno/syncdb/react";
9
+
10
+ useSocketEvent("announcementPublished", (payload) => {
11
+ console.info("announcement", payload);
12
+ });
13
+ ```
14
+
15
+ Inside `SyncDbProvider` the hook uses that client; above it, pass `{client: syncDb}`, or
16
+ `{client: null}` to listen to nothing. Outside React: `syncDb.onSocketEvent(event, handler)`
17
+ returns an unsubscribe.
18
+
19
+ ## Feature flags
20
+
21
+ ```tsx
22
+ import {OpenFeatureProvider} from "@openfeature/react-sdk";
23
+ import {useTerrenoFeatureFlags} from "@terreno/syncdb/flags";
24
+
25
+ const {userId} = useSession();
26
+ useTerrenoFeatureFlags({client: apiClient, skip: !userId, socket: syncDb, userId});
27
+ return <OpenFeatureProvider domain="feature-flags">{children}</OpenFeatureProvider>;
28
+ ```
29
+
30
+ Flags load through the request client and refetch when the server pushes
31
+ `featureFlagsChanged` on the sync socket. Screens can read flat accessors with
32
+ `useFeatureFlags({skip, socket, userId})`. See `example-frontend/app/_layout.tsx` and
33
+ `docs/reference/feature-flags.md`.
34
+
35
+ ## Sync status
36
+
37
+ ```tsx
38
+ import {useSyncStatus} from "@terreno/syncdb/react";
39
+
40
+ const {isOnline, queuedCount, conflictCount, failedCount, paused} = useSyncStatus();
41
+ ```
42
+
43
+ Render `SyncStatusBanner` / `OfflineBanner` from `@terreno/ui` with these values.
44
+
45
+ ## Realtime model updates
46
+
47
+ Live collection updates come from `sync` on the backend `modelRouter`; read them with
48
+ `useQuery` / `useEntity`. `RealtimeApp` hosts the socket for `sync:delta` and app events.
49
+ Terreno 58 removed `modelRouter` `realtime` and every `@terreno/rtk` socket helper
50
+ (`useSocketConnection`, `realtimeList`, `realtimeDocument`, `useRealtimeDebug`).
@@ -0,0 +1,55 @@
1
+ # SDK Customization — Terreno
2
+
3
+ ## Generated vs hand-maintained
4
+
5
+ | File | Editable? | Purpose |
6
+ |------|-----------|---------|
7
+ | `store/apiSdk.ts` | **Never** | Typed hooks for every non-synced OpenAPI operation (`--api-out`) |
8
+ | `store/syncDbSdk.ts` | **Never** | `SYNC_COLLECTIONS` + collection hooks (`--out`) |
9
+ | `store/apiClient.ts` | **Yes** | `createApiClient` (auth, headers, `onError`) |
10
+ | `syncdb-codegen.json` | **Yes** | Codegen config (`excludePaths`, per-collection `overrides`) |
11
+
12
+ ## Generated hooks
13
+
14
+ Names follow the old RTK codegen rule: camelCased `operationId`, otherwise method + path with
15
+ `{param}` as `By<Param>` (`PATCH /gpt/histories/{id}` → `usePatchGptHistoriesByIdMutation`).
16
+ Each operation also gets `<Name>Args`, `<Name>Res`, and `<name>Request(arg)` for
17
+ `apiClient.request(...)`. Tags come from the operation's OpenAPI tags.
18
+
19
+ ```tsx
20
+ import {useGetAuthMeQuery, usePatchAuthMeMutation} from "@/store/apiSdk";
21
+ ```
22
+
23
+ ## Routes outside OpenAPI
24
+
25
+ Prefer documenting the route in OpenAPI and regenerating. When that is not possible, call it
26
+ directly through the request client:
27
+
28
+ ```tsx
29
+ import {useApiMutation, useApiQuery, useLazyApiQuery} from "@terreno/syncdb/react";
30
+
31
+ const {data, isLoading, refetch} = useApiQuery<Report>(
32
+ {url: "/reports/summary", params: {range}},
33
+ {providesTags: ["reports"], skip: !userId}
34
+ );
35
+
36
+ const [rebuild, {isLoading: isRebuilding}] = useApiMutation<RebuildBody, Report>(
37
+ (body) => ({body, method: "POST", url: "/reports/rebuild"}),
38
+ {invalidatesTags: ["reports"]}
39
+ );
40
+ await rebuild({range}).unwrap();
41
+ ```
42
+
43
+ ## Cache tags
44
+
45
+ Queries register `providesTags`; mutations `invalidatesTags`. Imperatively:
46
+ `apiClient.invalidateTags(["reports"])`. `apiClient.reset()` clears the cache (done on logout
47
+ via `SessionProvider` `onLogout`). Synced collections have no request cache; their data is local.
48
+
49
+ ## Workflow after backend change
50
+
51
+ 1. Start backend (`bun run backend:dev`)
52
+ 2. Run `cd example-frontend && bun run sdk` (and `bun run sync-sdk` for synced schema changes)
53
+ 3. Import new hooks from `@/store/apiSdk` (or `@/store/syncDbSdk`)
54
+
55
+ 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`.