create-rigel 0.1.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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/cli.js +122 -0
  4. package/package.json +47 -0
  5. package/templates/express/.claude/CLAUDE.md +114 -0
  6. package/templates/express/.claude/agents/arch-validator.md +85 -0
  7. package/templates/express/.claude/agents/db-optimizer.md +72 -0
  8. package/templates/express/.claude/agents/doc-gardener.md +67 -0
  9. package/templates/express/.claude/agents/garbage-collector.md +94 -0
  10. package/templates/express/.claude/agents/gate-checker.md +176 -0
  11. package/templates/express/.claude/agents/reviewer.md +84 -0
  12. package/templates/express/.claude/agents/security-auditor.md +82 -0
  13. package/templates/express/.claude/hooks/post-write.sh +93 -0
  14. package/templates/express/.claude/rules/api.md +149 -0
  15. package/templates/express/.claude/rules/architecture.md +78 -0
  16. package/templates/express/.claude/rules/database.md +104 -0
  17. package/templates/express/.claude/rules/jobs.md +81 -0
  18. package/templates/express/.claude/rules/security.md +92 -0
  19. package/templates/express/.claude/rules/testing.md +97 -0
  20. package/templates/express/.claude/settings.json +35 -0
  21. package/templates/express/.claude/skills/00-infra-setup/SKILL.md +479 -0
  22. package/templates/express/.claude/skills/01-write-roadmap/SKILL.md +72 -0
  23. package/templates/express/.claude/skills/02-write-spec/SKILL.md +119 -0
  24. package/templates/express/.claude/skills/03-write-plan/SKILL.md +124 -0
  25. package/templates/express/.claude/skills/04-build-layer/SKILL.md +216 -0
  26. package/templates/express/.claude/skills/05-validate-layer/SKILL.md +43 -0
  27. package/templates/express/.claude/skills/06-push-layer/SKILL.md +59 -0
  28. package/templates/express/.claude/skills/07-layer-check/SKILL.md +62 -0
  29. package/templates/express/.claude/skills/08-garbage-collect/SKILL.md +86 -0
  30. package/templates/express/.claude/skills/09-doc-garden/SKILL.md +64 -0
  31. package/templates/express/.claude/skills/10-db-optimize/SKILL.md +96 -0
  32. package/templates/express/.claude/skills/11-load-test/SKILL.md +77 -0
  33. package/templates/express/.dockerignore +43 -0
  34. package/templates/express/.env.example +42 -0
  35. package/templates/express/.github/CODEOWNERS +22 -0
  36. package/templates/express/.github/dependabot.yml +48 -0
  37. package/templates/express/.github/pull_request_template.md +58 -0
  38. package/templates/express/.github/workflows/ci.yml +278 -0
  39. package/templates/express/.github/workflows/load-test.yml +41 -0
  40. package/templates/express/.gitleaks.toml +18 -0
  41. package/templates/express/.lintstagedrc.json +3 -0
  42. package/templates/express/.nvmrc +1 -0
  43. package/templates/express/.prettierrc +8 -0
  44. package/templates/express/.sequelizerc +8 -0
  45. package/templates/express/AGENTS.md +94 -0
  46. package/templates/express/ARCHITECTURE.md +161 -0
  47. package/templates/express/Dockerfile +40 -0
  48. package/templates/express/Makefile +79 -0
  49. package/templates/express/QUICKSTART.md +131 -0
  50. package/templates/express/docker-compose.yml +114 -0
  51. package/templates/express/docs/PLANS.md +111 -0
  52. package/templates/express/docs/QUALITY_SCORE.md +66 -0
  53. package/templates/express/docs/design-docs/core-beliefs.md +89 -0
  54. package/templates/express/docs/design-docs/decisions/ADR-000-infrastructure.md +70 -0
  55. package/templates/express/docs/design-docs/decisions/ADR-001-observability.md +82 -0
  56. package/templates/express/docs/design-docs/decisions/index.md +23 -0
  57. package/templates/express/docs/design-docs/index.md +11 -0
  58. package/templates/express/docs/design-docs/observability.md +194 -0
  59. package/templates/express/docs/design-docs/team-workflow.md +64 -0
  60. package/templates/express/docs/exec-plans/active/.gitkeep +0 -0
  61. package/templates/express/docs/exec-plans/completed/.gitkeep +0 -0
  62. package/templates/express/docs/exec-plans/tech-debt-tracker.md +31 -0
  63. package/templates/express/docs/generated/.gitkeep +0 -0
  64. package/templates/express/docs/product-specs/ROADMAP.md +49 -0
  65. package/templates/express/docs/product-specs/draft/.gitkeep +0 -0
  66. package/templates/express/docs/product-specs/index.md +21 -0
  67. package/templates/express/docs/product-specs/ready/.gitkeep +0 -0
  68. package/templates/express/eslint.config.mjs +94 -0
  69. package/templates/express/gitignore +36 -0
  70. package/templates/express/infra/monitoring/alloy/config.alloy +59 -0
  71. package/templates/express/jest.config.ts +61 -0
  72. package/templates/express/package.json +45 -0
  73. package/templates/express/scripts/openapi.export.ts +77 -0
  74. package/templates/express/tests/architecture/isolation.test.ts +51 -0
  75. package/templates/express/tests/architecture/layers.test.ts +83 -0
  76. package/templates/express/tests/integration/health.test.ts +40 -0
  77. package/templates/express/tests/integration/isolation.test.template.ts +88 -0
  78. package/templates/express/tests/load/smoke.js +46 -0
  79. package/templates/express/tests/load/soak.js +45 -0
  80. package/templates/express/tests/load/stress.js +44 -0
  81. package/templates/express/tests/unit/utils/response.util.test.ts +53 -0
  82. package/templates/express/tsconfig.build.json +8 -0
  83. package/templates/express/tsconfig.json +20 -0
  84. package/templates/fastapi/.claude/CLAUDE.md +122 -0
  85. package/templates/fastapi/.claude/agents/arch-validator.md +64 -0
  86. package/templates/fastapi/.claude/agents/db-optimizer.md +59 -0
  87. package/templates/fastapi/.claude/agents/doc-gardener.md +55 -0
  88. package/templates/fastapi/.claude/agents/garbage-collector.md +74 -0
  89. package/templates/fastapi/.claude/agents/gate-checker.md +133 -0
  90. package/templates/fastapi/.claude/agents/reviewer.md +74 -0
  91. package/templates/fastapi/.claude/agents/security-auditor.md +69 -0
  92. package/templates/fastapi/.claude/hooks/post-write.sh +65 -0
  93. package/templates/fastapi/.claude/rules/api.md +119 -0
  94. package/templates/fastapi/.claude/rules/architecture.md +84 -0
  95. package/templates/fastapi/.claude/rules/database.md +153 -0
  96. package/templates/fastapi/.claude/rules/jobs.md +85 -0
  97. package/templates/fastapi/.claude/rules/security.md +102 -0
  98. package/templates/fastapi/.claude/rules/testing.md +207 -0
  99. package/templates/fastapi/.claude/settings.json +50 -0
  100. package/templates/fastapi/.claude/skills/00-infra-setup/SKILL.md +311 -0
  101. package/templates/fastapi/.claude/skills/01-write-roadmap/SKILL.md +61 -0
  102. package/templates/fastapi/.claude/skills/02-write-spec/SKILL.md +82 -0
  103. package/templates/fastapi/.claude/skills/03-write-plan/SKILL.md +83 -0
  104. package/templates/fastapi/.claude/skills/04-build-layer/SKILL.md +170 -0
  105. package/templates/fastapi/.claude/skills/05-validate-layer/SKILL.md +20 -0
  106. package/templates/fastapi/.claude/skills/06-push-layer/SKILL.md +31 -0
  107. package/templates/fastapi/.claude/skills/07-layer-check/SKILL.md +51 -0
  108. package/templates/fastapi/.claude/skills/08-garbage-collect/SKILL.md +47 -0
  109. package/templates/fastapi/.claude/skills/09-doc-garden/SKILL.md +9 -0
  110. package/templates/fastapi/.claude/skills/10-db-optimize/SKILL.md +53 -0
  111. package/templates/fastapi/.claude/skills/11-load-test/SKILL.md +81 -0
  112. package/templates/fastapi/.dockerignore +30 -0
  113. package/templates/fastapi/.github/dependabot.yml +24 -0
  114. package/templates/fastapi/AGENTS.md +87 -0
  115. package/templates/fastapi/ARCHITECTURE.md +141 -0
  116. package/templates/fastapi/Makefile +42 -0
  117. package/templates/fastapi/README.md +179 -0
  118. package/templates/fastapi/docs/PLANS.md +26 -0
  119. package/templates/fastapi/docs/QUALITY_SCORE.md +26 -0
  120. package/templates/fastapi/docs/design-docs/core-beliefs.md +46 -0
  121. package/templates/fastapi/docs/design-docs/decisions/ADR-000-infrastructure.md +79 -0
  122. package/templates/fastapi/docs/design-docs/decisions/index.md +7 -0
  123. package/templates/fastapi/docs/design-docs/index.md +9 -0
  124. package/templates/fastapi/docs/exec-plans/active/.gitkeep +0 -0
  125. package/templates/fastapi/docs/exec-plans/completed/.gitkeep +0 -0
  126. package/templates/fastapi/docs/exec-plans/tech-debt-tracker.md +19 -0
  127. package/templates/fastapi/docs/generated/.gitkeep +0 -0
  128. package/templates/fastapi/docs/product-specs/ROADMAP.md +49 -0
  129. package/templates/fastapi/docs/product-specs/draft/.gitkeep +0 -0
  130. package/templates/fastapi/docs/product-specs/index.md +18 -0
  131. package/templates/fastapi/docs/product-specs/ready/.gitkeep +0 -0
  132. package/templates/fastapi/gitignore +46 -0
  133. package/templates/fastapi/scripts/gate.sh +68 -0
  134. package/templates/nestjs/.claude/CLAUDE.md +121 -0
  135. package/templates/nestjs/.claude/agents/db-optimizer.md +51 -0
  136. package/templates/nestjs/.claude/agents/doc-gardener.md +45 -0
  137. package/templates/nestjs/.claude/agents/garbage-collector.md +50 -0
  138. package/templates/nestjs/.claude/agents/gate-checker.md +127 -0
  139. package/templates/nestjs/.claude/agents/reviewer.md +68 -0
  140. package/templates/nestjs/.claude/agents/security-auditor.md +72 -0
  141. package/templates/nestjs/.claude/hooks/post-write.sh +91 -0
  142. package/templates/nestjs/.claude/rules/api.md +87 -0
  143. package/templates/nestjs/.claude/rules/architecture.md +104 -0
  144. package/templates/nestjs/.claude/rules/database.md +111 -0
  145. package/templates/nestjs/.claude/rules/security.md +110 -0
  146. package/templates/nestjs/.claude/rules/testing.md +145 -0
  147. package/templates/nestjs/.claude/settings.json +33 -0
  148. package/templates/nestjs/.claude/skills/00-infra-setup/SKILL.md +222 -0
  149. package/templates/nestjs/.claude/skills/01-write-spec/SKILL.md +25 -0
  150. package/templates/nestjs/.claude/skills/02-write-plan/SKILL.md +18 -0
  151. package/templates/nestjs/.claude/skills/03-build-layer/SKILL.md +242 -0
  152. package/templates/nestjs/.claude/skills/04-validate-layer/SKILL.md +4 -0
  153. package/templates/nestjs/.claude/skills/05-push-layer/SKILL.md +6 -0
  154. package/templates/nestjs/.claude/skills/06-layer-check/SKILL.md +12 -0
  155. package/templates/nestjs/.claude/skills/07-garbage-collect/SKILL.md +2 -0
  156. package/templates/nestjs/.claude/skills/08-doc-garden/SKILL.md +4 -0
  157. package/templates/nestjs/.claude/skills/09-api-sync/SKILL.md +36 -0
  158. package/templates/nestjs/.claude/skills/10-load-test/SKILL.md +29 -0
  159. package/templates/nestjs/.env.example +11 -0
  160. package/templates/nestjs/AGENTS.md +88 -0
  161. package/templates/nestjs/ARCHITECTURE.md +192 -0
  162. package/templates/nestjs/docs/PLANS.md +22 -0
  163. package/templates/nestjs/docs/QUALITY_SCORE.md +27 -0
  164. package/templates/nestjs/docs/design-docs/core-beliefs.md +18 -0
  165. package/templates/nestjs/docs/design-docs/decisions/ADR-000-infrastructure.md +58 -0
  166. package/templates/nestjs/docs/design-docs/decisions/index.md +7 -0
  167. package/templates/nestjs/docs/design-docs/index.md +7 -0
  168. package/templates/nestjs/docs/exec-plans/active/.gitkeep +0 -0
  169. package/templates/nestjs/docs/exec-plans/completed/.gitkeep +0 -0
  170. package/templates/nestjs/docs/exec-plans/tech-debt-tracker.md +16 -0
  171. package/templates/nestjs/docs/generated/.gitkeep +0 -0
  172. package/templates/nestjs/docs/product-specs/draft/.gitkeep +0 -0
  173. package/templates/nestjs/docs/product-specs/index.md +7 -0
  174. package/templates/nestjs/docs/product-specs/ready/.gitkeep +0 -0
  175. package/templates/nestjs/gitignore +8 -0
  176. package/templates/nextjs/.claude/CLAUDE.md +139 -0
  177. package/templates/nextjs/.claude/agents/arch-validator.md +66 -0
  178. package/templates/nextjs/.claude/agents/contract-checker.md +78 -0
  179. package/templates/nextjs/.claude/agents/doc-gardener.md +52 -0
  180. package/templates/nextjs/.claude/agents/garbage-collector.md +61 -0
  181. package/templates/nextjs/.claude/agents/gate-checker.md +129 -0
  182. package/templates/nextjs/.claude/agents/perf-auditor.md +109 -0
  183. package/templates/nextjs/.claude/agents/reviewer.md +79 -0
  184. package/templates/nextjs/.claude/agents/security-auditor.md +50 -0
  185. package/templates/nextjs/.claude/hooks/post-write.sh +121 -0
  186. package/templates/nextjs/.claude/rules/api-contract.md +166 -0
  187. package/templates/nextjs/.claude/rules/architecture.md +99 -0
  188. package/templates/nextjs/.claude/rules/components.md +122 -0
  189. package/templates/nextjs/.claude/rules/security.md +122 -0
  190. package/templates/nextjs/.claude/rules/testing.md +187 -0
  191. package/templates/nextjs/.claude/scripts/infra-setup.sh +550 -0
  192. package/templates/nextjs/.claude/settings.json +33 -0
  193. package/templates/nextjs/.claude/skills/00-infra-setup/SKILL.md +471 -0
  194. package/templates/nextjs/.claude/skills/01-write-roadmap/SKILL.md +77 -0
  195. package/templates/nextjs/.claude/skills/02-write-spec/SKILL.md +78 -0
  196. package/templates/nextjs/.claude/skills/03-write-plan/SKILL.md +91 -0
  197. package/templates/nextjs/.claude/skills/04-build-layer/SKILL.md +141 -0
  198. package/templates/nextjs/.claude/skills/05-validate-layer/SKILL.md +13 -0
  199. package/templates/nextjs/.claude/skills/06-push-layer/SKILL.md +20 -0
  200. package/templates/nextjs/.claude/skills/07-layer-check/SKILL.md +28 -0
  201. package/templates/nextjs/.claude/skills/08-garbage-collect/SKILL.md +14 -0
  202. package/templates/nextjs/.claude/skills/09-doc-garden/SKILL.md +12 -0
  203. package/templates/nextjs/.claude/skills/10-api-sync/SKILL.md +50 -0
  204. package/templates/nextjs/.claude/skills/11-perf-budget/SKILL.md +51 -0
  205. package/templates/nextjs/.dockerignore +25 -0
  206. package/templates/nextjs/.gitattributes +10 -0
  207. package/templates/nextjs/.github/CODEOWNERS +24 -0
  208. package/templates/nextjs/.github/dependabot.yml +31 -0
  209. package/templates/nextjs/.github/pull_request_template.md +59 -0
  210. package/templates/nextjs/.github/workflows/ci.yml +143 -0
  211. package/templates/nextjs/.github/workflows/lighthouse.yml +54 -0
  212. package/templates/nextjs/.github/workflows/load-test.yml +54 -0
  213. package/templates/nextjs/.husky/pre-commit +1 -0
  214. package/templates/nextjs/.lintstagedrc.json +4 -0
  215. package/templates/nextjs/.mcp.json +8 -0
  216. package/templates/nextjs/.prettierignore +13 -0
  217. package/templates/nextjs/.prettierrc +9 -0
  218. package/templates/nextjs/AGENTS.md +88 -0
  219. package/templates/nextjs/ARCHITECTURE.md +165 -0
  220. package/templates/nextjs/Dockerfile +38 -0
  221. package/templates/nextjs/Makefile +50 -0
  222. package/templates/nextjs/QUICKSTART.md +128 -0
  223. package/templates/nextjs/docs/PLANS.md +23 -0
  224. package/templates/nextjs/docs/QUALITY_SCORE.md +27 -0
  225. package/templates/nextjs/docs/design-docs/core-beliefs.md +43 -0
  226. package/templates/nextjs/docs/design-docs/decisions/ADR-000-infrastructure.md +72 -0
  227. package/templates/nextjs/docs/design-docs/decisions/index.md +7 -0
  228. package/templates/nextjs/docs/design-docs/index.md +7 -0
  229. package/templates/nextjs/docs/design-docs/team-workflow.md +66 -0
  230. package/templates/nextjs/docs/exec-plans/active/.gitkeep +0 -0
  231. package/templates/nextjs/docs/exec-plans/completed/.gitkeep +0 -0
  232. package/templates/nextjs/docs/exec-plans/tech-debt-tracker.md +19 -0
  233. package/templates/nextjs/docs/generated/.gitkeep +0 -0
  234. package/templates/nextjs/docs/product-specs/ROADMAP.md +59 -0
  235. package/templates/nextjs/docs/product-specs/draft/.gitkeep +0 -0
  236. package/templates/nextjs/docs/product-specs/index.md +18 -0
  237. package/templates/nextjs/docs/product-specs/ready/.gitkeep +0 -0
  238. package/templates/nextjs/eslint.config.mjs +150 -0
  239. package/templates/nextjs/gitignore +35 -0
  240. package/templates/nextjs/lighthouserc.js +37 -0
@@ -0,0 +1,122 @@
1
+ ---
2
+ paths:
3
+ - "src/lib/api-client.ts"
4
+ - "app/api/**/*.ts"
5
+ - "src/features/auth/**/*.ts"
6
+ - "src/features/auth/**/*.tsx"
7
+ ---
8
+
9
+ # Security Rules — Auto-injected on auth and API client edits
10
+
11
+ ## Auth Token Handling
12
+
13
+ ```typescript
14
+ // ✅ Access token in React context (memory only)
15
+ // src/features/auth/AuthProvider.tsx
16
+ const [accessToken, setAccessToken] = useState<string | null>(null)
17
+ // Store ONLY in React state — wiped on page refresh by design
18
+
19
+ // ✅ Inject access token via api-client middleware
20
+ // src/lib/api-client.ts
21
+ import createClient, { type Middleware } from 'openapi-fetch'
22
+ import type { paths } from '@/types/api.generated'
23
+
24
+ const authMiddleware: Middleware = {
25
+ async onRequest({ request }) {
26
+ const token = getAccessTokenFromContext() // from React context
27
+ if (token) {
28
+ request.headers.set('Authorization', `Bearer ${token}`)
29
+ }
30
+ return request
31
+ },
32
+ }
33
+
34
+ export const apiClient = createClient<paths>({ baseUrl: env.NEXT_PUBLIC_API_URL })
35
+ apiClient.use(authMiddleware)
36
+
37
+ // ❌ NEVER — token in storage
38
+ localStorage.setItem('accessToken', token)
39
+ sessionStorage.setItem('accessToken', token)
40
+ document.cookie = `accessToken=${token}` // use httpOnly cookies via backend
41
+ ```
42
+
43
+ ## Refresh Token Flow
44
+
45
+ ```typescript
46
+ // src/app/api/auth/refresh/route.ts — Next.js API route
47
+ // The refresh token is in an httpOnly cookie (set by backend)
48
+ // This route proxies the refresh request so the cookie is sent automatically
49
+
50
+ export async function POST() {
51
+ const response = await fetch(`${env.API_URL}/api/v1/auth/refresh`, {
52
+ method: 'POST',
53
+ credentials: 'include', // sends the httpOnly cookie
54
+ })
55
+ const data = await response.json()
56
+ // Return new access token to client — it goes into React context
57
+ return NextResponse.json({ accessToken: data.accessToken })
58
+ }
59
+ ```
60
+
61
+ ## Environment Variables Security
62
+
63
+ ```typescript
64
+ // ✅ Only NEXT_PUBLIC_* vars are safe to expose to client
65
+ // Non-NEXT_PUBLIC_ vars are server-only — NEVER access in client components
66
+
67
+ // src/lib/env.ts — validate all public env vars with Zod
68
+ import { z } from 'zod'
69
+ const envSchema = z.object({
70
+ NEXT_PUBLIC_API_URL: z.string().url(),
71
+ })
72
+ // Never put secrets (JWT_SECRET, DB passwords) in NEXT_PUBLIC_* vars
73
+
74
+ // ❌ Leaked secret
75
+ NEXT_PUBLIC_JWT_SECRET=my-secret // accessible to all users in browser
76
+ ```
77
+
78
+ ## Content Security Policy
79
+
80
+ ```typescript
81
+ // next.config.ts — CSP via headers
82
+ const ContentSecurityPolicy = `
83
+ default-src 'self';
84
+ script-src 'self' 'nonce-{NONCE}';
85
+ style-src 'self' 'unsafe-inline';
86
+ img-src 'self' data: https:;
87
+ connect-src 'self' ${process.env.NEXT_PUBLIC_API_URL};
88
+ frame-ancestors 'none';
89
+ `.replace(/\n/g, ' ')
90
+ ```
91
+
92
+ ## XSS Prevention
93
+
94
+ ```typescript
95
+ // ✅ React escapes by default — just use JSX
96
+ <p>{userContent}</p>
97
+
98
+ // ❌ Only with explicit sanitisation
99
+ <div dangerouslySetInnerHTML={{ __html: userContent }} />
100
+ // If required: use DOMPurify.sanitize(userContent) first
101
+ ```
102
+
103
+ ## Next.js API Routes (Cookie Proxy Only)
104
+
105
+ ```typescript
106
+ // app/api/ routes ONLY handle:
107
+ // 1. Cookie operations (set/clear httpOnly cookies)
108
+ // 2. Proxying auth requests
109
+ // NOT for business logic — that belongs in the backend
110
+
111
+ // ✅ Legitimate Next.js API route
112
+ export async function POST(request: Request) {
113
+ const body = await request.json()
114
+ const response = await fetch(`${env.API_URL}/api/v1/auth/login`, {
115
+ method: 'POST', body: JSON.stringify(body),
116
+ headers: { 'Content-Type': 'application/json' },
117
+ })
118
+ // Set httpOnly cookie with refresh token from backend response
119
+ const res = NextResponse.json(await response.json())
120
+ return res
121
+ }
122
+ ```
@@ -0,0 +1,187 @@
1
+ ---
2
+ paths:
3
+ - "tests/**/*.ts"
4
+ - "tests/**/*.tsx"
5
+ - "src/**/*.test.ts"
6
+ - "src/**/*.test.tsx"
7
+ ---
8
+
9
+ # Testing Rules — Auto-injected on test file edits
10
+
11
+ ## Coverage Thresholds (enforced in CI)
12
+
13
+ | Layer | Threshold | What to test |
14
+ |---|---|---|
15
+ | `src/utils/` | **100%** | Every branch |
16
+ | `src/hooks/` | **80%** | Loading, success, error states; mutations |
17
+ | `src/features/` | **70%** | Render, user interaction, form submission |
18
+ | `src/components/` | **70%** | Render + accessibility |
19
+
20
+ ## Utils Tests — Pure Functions, 100% Branches
21
+
22
+ ```typescript
23
+ // tests/unit/utils/parse-error.test.ts
24
+ import { parseError } from '@/utils/parse-error.util'
25
+
26
+ describe('parseError', () => {
27
+ it('extracts message from Error', () => {
28
+ expect(parseError(new Error('boom'))).toBe('boom')
29
+ })
30
+ it('passes through string errors', () => {
31
+ expect(parseError('nope')).toBe('nope')
32
+ })
33
+ it('falls back for unknown shapes', () => {
34
+ expect(parseError({ weird: true })).toBe('Something went wrong')
35
+ expect(parseError(null)).toBe('Something went wrong')
36
+ })
37
+ })
38
+ ```
39
+
40
+ ## Vitest + RTL — Hook Tests with MSW
41
+
42
+ ```typescript
43
+ // tests/unit/hooks/use-applications.test.ts
44
+ import { renderHook, waitFor } from '@testing-library/react'
45
+ import { http, HttpResponse } from 'msw'
46
+ import { server } from '../mocks/server' // MSW server
47
+ import { createWrapper } from '../utils/create-wrapper' // QueryClient wrapper
48
+ import { useApplications } from '@/hooks/use-applications'
49
+
50
+ describe('useApplications', () => {
51
+ it('returns applications on success', async () => {
52
+ server.use(
53
+ http.get('/api/v1/applications', () =>
54
+ HttpResponse.json({ items: [{ id: '1', stage: 'APPLIED', company: 'Acme' }], has_more: false })
55
+ )
56
+ )
57
+ const { result } = renderHook(() => useApplications(), { wrapper: createWrapper() })
58
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
59
+ expect(result.current.data?.items).toHaveLength(1)
60
+ })
61
+
62
+ it('returns error on failure', async () => {
63
+ server.use(
64
+ http.get('/api/v1/applications', () => HttpResponse.error())
65
+ )
66
+ const { result } = renderHook(() => useApplications(), { wrapper: createWrapper() })
67
+ await waitFor(() => expect(result.current.isError).toBe(true))
68
+ })
69
+ })
70
+ ```
71
+
72
+ ## Component Tests — Test Behaviour, Not Implementation
73
+ ```typescript
74
+ // tests/unit/features/ApplicationCard.test.tsx
75
+ import { render, screen } from '@testing-library/react'
76
+ import userEvent from '@testing-library/user-event'
77
+ import { ApplicationCard } from '@/features/applications/ApplicationCard'
78
+
79
+ describe('ApplicationCard', () => {
80
+ it('displays company and role', () => {
81
+ render(<ApplicationCard application={mockApplication} />)
82
+ expect(screen.getByText('Acme Corp')).toBeInTheDocument()
83
+ expect(screen.getByText('Software Engineer')).toBeInTheDocument()
84
+ })
85
+
86
+ it('calls onDelete when delete button clicked', async () => {
87
+ const onDelete = vi.fn()
88
+ render(<ApplicationCard application={mockApplication} onDelete={onDelete} />)
89
+ await userEvent.click(screen.getByRole('button', { name: /delete/i }))
90
+ expect(onDelete).toHaveBeenCalledWith(mockApplication.id)
91
+ })
92
+
93
+ it('is accessible — delete button has aria-label', () => {
94
+ render(<ApplicationCard application={mockApplication} onDelete={vi.fn()} />)
95
+ expect(screen.getByRole('button', { name: /delete application/i })).toBeInTheDocument()
96
+ })
97
+ })
98
+ ```
99
+
100
+ ## Playwright — E2E Critical Paths
101
+ ```typescript
102
+ // tests/e2e/applications.spec.ts
103
+ import { test, expect } from '@playwright/test'
104
+
105
+ test.describe('Applications', () => {
106
+ test.beforeEach(async ({ page }) => {
107
+ await page.goto('/login')
108
+ await page.getByLabel('Email').fill('test@example.com')
109
+ await page.getByLabel('Password').fill('password123')
110
+ await page.getByRole('button', { name: 'Sign in' }).click()
111
+ await expect(page).toHaveURL('/applications')
112
+ })
113
+
114
+ test('can create a new application', async ({ page }) => {
115
+ await page.getByRole('button', { name: 'New Application' }).click()
116
+ await page.getByLabel('Company').fill('Acme Corp')
117
+ await page.getByLabel('Role').fill('Software Engineer')
118
+ await page.getByRole('button', { name: 'Create' }).click()
119
+ await expect(page.getByText('Acme Corp')).toBeVisible()
120
+ })
121
+
122
+ test('shows empty state when no applications', async ({ page }) => {
123
+ // With MSW intercepting
124
+ await expect(page.getByText(/no applications yet/i)).toBeVisible()
125
+ })
126
+ })
127
+ ```
128
+
129
+ ## Cross-User Isolation — REQUIRED security test (one per resource)
130
+
131
+ The single most important security test: confirm User B cannot reach User A's resource,
132
+ and that the app reveals **404, not 403** (a 403 leaks that the resource exists).
133
+
134
+ ```typescript
135
+ // tests/e2e/isolation.spec.ts
136
+ import { test, expect } from '@playwright/test'
137
+ import { loginAs } from './helpers/auth' // logs in + returns to a known URL
138
+
139
+ test('User B cannot access User A resource (404, not 403, not the data)', async ({ page }) => {
140
+ // 1. User A creates a resource and we capture its id
141
+ await loginAs(page, 'usera@example.com')
142
+ await page.getByRole('button', { name: 'New Application' }).click()
143
+ await page.getByLabel('Company').fill('Acme Corp')
144
+ await page.getByRole('button', { name: 'Create' }).click()
145
+ await expect(page).toHaveURL(/\/applications\/(?<id>[\w-]+)$/)
146
+ const url = page.url()
147
+
148
+ // 2. User B logs in and visits A's resource URL directly
149
+ await loginAs(page, 'userb@example.com')
150
+ const res = await page.goto(url)
151
+
152
+ // 3. Must be denied as not-found — never expose A's data, never a bare 403
153
+ expect(res?.status()).toBe(404)
154
+ await expect(page.getByText('Acme Corp')).toHaveCount(0)
155
+ await expect(page.getByText(/not found/i)).toBeVisible()
156
+ })
157
+ ```
158
+
159
+ Add one of these per user-owned resource type. The backend enforces isolation via
160
+ `userId in WHERE`; this test proves the frontend surfaces it correctly.
161
+
162
+ ## Visual Regression (Playwright Snapshots)
163
+ ```typescript
164
+ test('application card snapshot', async ({ page }) => {
165
+ await page.goto('/applications')
166
+ await page.waitForSelector('[data-testid="application-card"]')
167
+ await expect(page.locator('[data-testid="application-card"]').first())
168
+ .toHaveScreenshot('application-card.png')
169
+ })
170
+ ```
171
+
172
+ ## MSW Setup (required)
173
+ ```typescript
174
+ // tests/mocks/server.ts
175
+ import { setupServer } from 'msw/node'
176
+ import { handlers } from './handlers'
177
+ export const server = setupServer(...handlers)
178
+
179
+ // tests/mocks/handlers.ts
180
+ import { http, HttpResponse } from 'msw'
181
+ export const handlers = [
182
+ http.get('/api/v1/applications', () =>
183
+ HttpResponse.json({ items: [], has_more: false, next_cursor_id: null })
184
+ ),
185
+ // Add per-test overrides via server.use() in individual tests
186
+ ]
187
+ ```