@h-ai/cli 0.1.0-alpha5

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 (109) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +167 -0
  3. package/dist/chunk-2H7UOFLK.js +10 -0
  4. package/dist/chunk-2H7UOFLK.js.map +1 -0
  5. package/dist/chunk-LANIDJC7.js +659 -0
  6. package/dist/chunk-LANIDJC7.js.map +1 -0
  7. package/dist/chunk-YV7WPUI6.js +206 -0
  8. package/dist/chunk-YV7WPUI6.js.map +1 -0
  9. package/dist/config-templates-S5RI2MC6.js +5 -0
  10. package/dist/config-templates-S5RI2MC6.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +1645 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/skill-templates-N5DU5FNX.js +5 -0
  15. package/dist/skill-templates-N5DU5FNX.js.map +1 -0
  16. package/dist/src-XJ6YXNAF.js +15030 -0
  17. package/dist/src-XJ6YXNAF.js.map +1 -0
  18. package/package.json +59 -0
  19. package/templates/README.md +126 -0
  20. package/templates/apps/admin/messages/en-US.json +33 -0
  21. package/templates/apps/admin/messages/zh-CN.json +33 -0
  22. package/templates/apps/admin/src/routes/+layout.svelte.hbs +30 -0
  23. package/templates/apps/admin/src/routes/+page.svelte.hbs +35 -0
  24. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +44 -0
  25. package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +45 -0
  26. package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +37 -0
  27. package/templates/apps/admin/src/routes/api/health/+server.ts +12 -0
  28. package/templates/apps/android-app/.env.hbs +3 -0
  29. package/templates/apps/android-app/capacitor.config.ts.hbs +12 -0
  30. package/templates/apps/android-app/messages/en-US.json +24 -0
  31. package/templates/apps/android-app/messages/zh-CN.json +24 -0
  32. package/templates/apps/android-app/src/lib/api.ts.hbs +30 -0
  33. package/templates/apps/android-app/src/lib/capacitor.ts +21 -0
  34. package/templates/apps/android-app/src/routes/+layout.svelte.hbs +42 -0
  35. package/templates/apps/android-app/src/routes/+layout.ts +7 -0
  36. package/templates/apps/android-app/src/routes/+page.svelte.hbs +27 -0
  37. package/templates/apps/api/src/routes/+layout.svelte +14 -0
  38. package/templates/apps/api/src/routes/+page.svelte +11 -0
  39. package/templates/apps/api/src/routes/api/v1/health/+server.ts +14 -0
  40. package/templates/apps/api/src/routes/api/v1/items/+server.ts +37 -0
  41. package/templates/apps/api/src/routes/api/v1/items/[id]/+server.ts +30 -0
  42. package/templates/apps/h5/messages/en-US.json +37 -0
  43. package/templates/apps/h5/messages/zh-CN.json +37 -0
  44. package/templates/apps/h5/src/routes/+layout.svelte.hbs +57 -0
  45. package/templates/apps/h5/src/routes/+page.svelte.hbs +27 -0
  46. package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +27 -0
  47. package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +27 -0
  48. package/templates/apps/website/messages/en-US.json +32 -0
  49. package/templates/apps/website/messages/zh-CN.json +32 -0
  50. package/templates/apps/website/src/routes/+layout.svelte.hbs +66 -0
  51. package/templates/apps/website/src/routes/+page.svelte.hbs +33 -0
  52. package/templates/apps/website/src/routes/about/+page.svelte.hbs +27 -0
  53. package/templates/apps/website/src/routes/api/health/+server.ts +12 -0
  54. package/templates/apps/website/src/routes/contact/+page.svelte.hbs +59 -0
  55. package/templates/apps/website/src/routes/services/+page.svelte.hbs +63 -0
  56. package/templates/base/e2e/helpers.ts +49 -0
  57. package/templates/base/package.json.hbs +67 -0
  58. package/templates/base/playwright.config.ts +32 -0
  59. package/templates/base/src/app.css +2 -0
  60. package/templates/base/src/app.d.ts.hbs +18 -0
  61. package/templates/base/src/app.html.hbs +16 -0
  62. package/templates/base/src/hooks.server.ts.hbs +123 -0
  63. package/templates/base/src/lib/server/init.ts.hbs +190 -0
  64. package/templates/base/svelte.config.js.hbs +30 -0
  65. package/templates/base/tsconfig.json +8 -0
  66. package/templates/base/vite.config.ts.hbs +27 -0
  67. package/templates/features/ai/routes/api/chat/+server.ts +26 -0
  68. package/templates/features/datapipe/routes/api/datapipe/pipeline/+server.ts +41 -0
  69. package/templates/features/iam/routes-admin/(auth)/+layout.svelte.hbs +21 -0
  70. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +98 -0
  71. package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +124 -0
  72. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +98 -0
  73. package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +114 -0
  74. package/templates/features/iam/routes-shared/api/auth/login/+server.ts +32 -0
  75. package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +15 -0
  76. package/templates/features/iam/routes-shared/api/auth/me/+server.ts +12 -0
  77. package/templates/features/iam/routes-shared/api/auth/register/+server.ts +26 -0
  78. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +30 -0
  79. package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +42 -0
  80. package/templates/features/payment/routes/api/v1/payment/query/[orderNo]/+server.ts +25 -0
  81. package/templates/features/storage/routes/api/upload/+server.ts +35 -0
  82. package/templates/features/vecdb/routes/api/vecdb/search/+server.ts +36 -0
  83. package/templates/i18n/project.inlang/settings.json +11 -0
  84. package/templates/skills/AGENTS.md +26 -0
  85. package/templates/skills/CLAUDE.md +12 -0
  86. package/templates/skills/copilot-instructions.md +61 -0
  87. package/templates/skills/hai-ai/SKILL.md +659 -0
  88. package/templates/skills/hai-api-client/SKILL.md +346 -0
  89. package/templates/skills/hai-app-create/SKILL.md +371 -0
  90. package/templates/skills/hai-app-review/SKILL.md +156 -0
  91. package/templates/skills/hai-app-tests/SKILL.md +468 -0
  92. package/templates/skills/hai-audit/SKILL.md +141 -0
  93. package/templates/skills/hai-build/SKILL.md +319 -0
  94. package/templates/skills/hai-cache/SKILL.md +184 -0
  95. package/templates/skills/hai-capacitor/SKILL.md +389 -0
  96. package/templates/skills/hai-core/SKILL.md +544 -0
  97. package/templates/skills/hai-crypto/SKILL.md +183 -0
  98. package/templates/skills/hai-datapipe/SKILL.md +214 -0
  99. package/templates/skills/hai-deploy/SKILL.md +210 -0
  100. package/templates/skills/hai-iam/SKILL.md +355 -0
  101. package/templates/skills/hai-kit/SKILL.md +319 -0
  102. package/templates/skills/hai-payment/SKILL.md +289 -0
  103. package/templates/skills/hai-reach/SKILL.md +325 -0
  104. package/templates/skills/hai-reldb/SKILL.md +459 -0
  105. package/templates/skills/hai-scheduler/SKILL.md +242 -0
  106. package/templates/skills/hai-storage/SKILL.md +230 -0
  107. package/templates/skills/hai-ui/SKILL.md +275 -0
  108. package/templates/skills/hai-vecdb/SKILL.md +273 -0
  109. package/templates/skills/opencode.json +11 -0
@@ -0,0 +1,468 @@
1
+ ---
2
+ name: hai-app-tests
3
+ description: 以 TDD 驱动应用开发:先分析需求、生成单元测试(Vitest)与 E2E 测试(Playwright),确认测试全部失败后再编码实现直至测试通过;当需求涉及应用测试、TDD 流程、覆盖率提升、E2E 测试时使用。
4
+ ---
5
+
6
+ # hai-app-tests — TDD 驱动的应用测试规范
7
+
8
+ > 面向 AI 助手的 TDD 测试指南。**测试先行**:先写测试定义行为,再写实现让测试通过。涵盖 Vitest 单元测试与 Playwright E2E 测试。
9
+
10
+ ---
11
+
12
+ ## 适用场景
13
+
14
+ - 以 TDD 方式开发新功能(先写测试 → 确认失败 → 再实现 → 确认通过)
15
+ - 为 `$lib/server/services/` 编写服务层单元测试
16
+ - 为 API 端点编写 E2E 测试(Playwright)
17
+ - 为页面交互编写 UI E2E 测试(Playwright)
18
+ - 为工具函数、Schema 校验编写单元测试
19
+ - 需要提升测试覆盖率
20
+
21
+ ---
22
+
23
+ ## TDD 核心工作流(必须严格遵循)
24
+
25
+ ### 阶段一:需求分析(Red 前准备)
26
+
27
+ 1. **理解需求**:明确功能的输入、输出、边界条件、错误场景
28
+ 2. **拆分测试点**:将需求转化为可验证的测试用例清单
29
+ 3. **确定测试类型**:
30
+ - **单元测试(Vitest)**:服务层、工具函数、Schema 校验、HaiResult 处理
31
+ - **E2E 测试(Playwright)**:API 端点、页面交互、认证流程、跨模块集成
32
+
33
+ ### 阶段二:Red — 编写失败的测试
34
+
35
+ 1. **编写单元测试**:覆盖正常路径、边界路径、权限路径、错误路径
36
+ 2. **编写 E2E 测试**:覆盖 API 端点和关键用户交互流程
37
+ 3. **运行测试确认全部失败**:
38
+ ```bash
39
+ pnpm --filter <app-name> test # 单元测试 — 应全部 FAIL
40
+ pnpm --filter <app-name> test:e2e # E2E 测试 — 应全部 FAIL
41
+ ```
42
+ 4. 如果测试意外通过,说明测试没有测到新功能,需要修正测试
43
+
44
+ ### 阶段三:Green — 最小实现
45
+
46
+ 1. 参照 `hai-app-create` 技能编写实现代码
47
+ 2. 每实现一个功能点,立即运行对应测试确认通过
48
+ 3. 只写让测试通过的最小代码,不做过度设计
49
+ 4. 运行全部测试确认通过:
50
+ ```bash
51
+ pnpm --filter <app-name> test # 单元测试 — 应全部 PASS
52
+ pnpm --filter <app-name> test:e2e # E2E 测试 — 应全部 PASS
53
+ ```
54
+
55
+ ### 阶段四:Refactor — 重构优化
56
+
57
+ 1. 参照 `hai-app-review` 技能审查代码规范
58
+ 2. 重构后再次运行所有测试确认仍然通过
59
+ 3. 运行质量门禁:`pnpm typecheck && pnpm lint && pnpm test`
60
+
61
+ ---
62
+
63
+ ## 测试目录约定
64
+
65
+ ```
66
+ tests/ # 单元测试(Vitest)
67
+ services/
68
+ user.test.ts # 服务层测试
69
+ article.test.ts
70
+ schemas/
71
+ user-schema.test.ts # Schema 校验测试
72
+ utils/
73
+ helper.test.ts # 工具函数测试
74
+ e2e/ # E2E 测试(Playwright)
75
+ helpers.ts # E2E 共用工具函数
76
+ health.spec.ts # 健康检查 API
77
+ auth-api.spec.ts # 认证 API 测试
78
+ auth-ui.spec.ts # 认证页面 UI 测试
79
+ users-api.spec.ts # 用户管理 API 测试
80
+ users-ui.spec.ts # 用户管理页面 UI 测试
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 核心规则
86
+
87
+ 1. **测试先行**:先写测试定义预期行为,再写实现代码。
88
+ 2. **统一入口**:通过框架模块公共 API 测试(`reldb.sql`、`iam.authn`),不直接调用内部实现。
89
+ 3. **从实际场景出发**:测试用例验证真实业务行为,不做形式覆盖。
90
+ 4. **先审查后修复**:测试不通过时先审查业务逻辑是否有问题,不直接修改测试迎合通过。
91
+ 5. **Mock 外部依赖**:单元测试中,数据库、缓存、网络请求使用 mock,不直连真实服务。
92
+ 6. **E2E 真实环境**:E2E 测试运行在完整应用环境中,不使用 mock。
93
+
94
+ ---
95
+
96
+ ## 覆盖范围
97
+
98
+ 每个功能至少覆盖以下路径(单元测试 + E2E 按需):
99
+
100
+ | 路径 | 说明 | 单元测试示例 | E2E 测试示例 |
101
+ | -------- | ------------------------ | ------------------ | ----------------------- |
102
+ | 正常路径 | 核心功能可用 | 创建用户成功 | POST /api/users → 201 |
103
+ | 边界路径 | 非法输入、空值、格式错误 | 空名称、超长字符串 | 空表单提交 → 校验提示 |
104
+ | 权限路径 | 未认证、无权限 | 未登录调用服务 | 未登录访问 → 401/重定向 |
105
+ | 错误路径 | 服务异常、数据库错误 | DB 失败时的 HaiResult | 服务不可用 → 错误提示页 |
106
+
107
+ ---
108
+
109
+ ## 单元测试(Vitest)
110
+
111
+ ### 服务层测试模板
112
+
113
+ ```typescript
114
+ import { createArticle, listArticles } from '$lib/server/services/article'
115
+ // tests/services/article.test.ts
116
+ import { reldb } from '@h-ai/reldb'
117
+ import { describe, expect, it, vi } from 'vitest'
118
+
119
+ // Mock 依赖模块
120
+ vi.mock('@h-ai/reldb', () => ({
121
+ db: {
122
+ sql: { query: vi.fn(), execute: vi.fn() },
123
+ crud: { create: vi.fn(), findById: vi.fn(), paginate: vi.fn() },
124
+ },
125
+ }))
126
+
127
+ vi.mock('@h-ai/core', () => ({
128
+ core: { logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() } },
129
+ }))
130
+
131
+ describe('ArticleService', () => {
132
+ // 正常路径
133
+ it('should create article successfully', async () => {
134
+ const mockArticle = { id: '1', title: 'Test', authorId: 'u1' }
135
+ vi.mocked(reldb.crud.create).mockResolvedValue({ success: true, data: mockArticle })
136
+
137
+ const result = await createArticle({ title: 'Test', authorId: 'u1' })
138
+
139
+ expect(result.success).toBe(true)
140
+ if (result.success) {
141
+ expect(result.data.title).toBe('Test')
142
+ }
143
+ })
144
+
145
+ // 边界路径
146
+ it('should return error for empty title', async () => {
147
+ const result = await createArticle({ title: '', authorId: 'u1' })
148
+
149
+ expect(result.success).toBe(false)
150
+ if (!result.success) {
151
+ expect(result.error.code).toBeDefined()
152
+ }
153
+ })
154
+
155
+ // 错误路径
156
+ it('should return error when db fails', async () => {
157
+ vi.mocked(reldb.crud.create).mockResolvedValue({
158
+ success: false,
159
+ error: { code: 'DB_ERROR', message: 'Connection lost' },
160
+ })
161
+
162
+ const result = await createArticle({ title: 'Test', authorId: 'u1' })
163
+
164
+ expect(result.success).toBe(false)
165
+ })
166
+ })
167
+ ```
168
+
169
+ ### Schema 校验测试模板
170
+
171
+ ```typescript
172
+ import { CreateUserSchema } from '$lib/server/schemas/user'
173
+ // tests/schemas/user-schema.test.ts
174
+ import { describe, expect, it } from 'vitest'
175
+
176
+ describe('CreateUserSchema', () => {
177
+ it('should accept valid input', () => {
178
+ const result = CreateUserSchema.safeParse({
179
+ username: 'testuser',
180
+ email: 'test@example.com',
181
+ password: '12345678',
182
+ })
183
+ expect(result.success).toBe(true)
184
+ })
185
+
186
+ it('should reject short username', () => {
187
+ const result = CreateUserSchema.safeParse({
188
+ username: 'ab',
189
+ email: 'test@example.com',
190
+ password: '12345678',
191
+ })
192
+ expect(result.success).toBe(false)
193
+ })
194
+
195
+ it('should reject invalid email', () => {
196
+ const result = CreateUserSchema.safeParse({
197
+ username: 'testuser',
198
+ email: 'not-email',
199
+ password: '12345678',
200
+ })
201
+ expect(result.success).toBe(false)
202
+ })
203
+ })
204
+ ```
205
+
206
+ ---
207
+
208
+ ## E2E 测试(Playwright)
209
+
210
+ ### Playwright 配置
211
+
212
+ 项目中应有 `playwright.config.ts`(由 CLI 生成):
213
+
214
+ ```typescript
215
+ // playwright.config.ts
216
+ import process from 'node:process'
217
+ import { defineConfig } from '@playwright/test'
218
+
219
+ const baseURL = process.env.BASE_URL || 'http://localhost:4173'
220
+
221
+ export default defineConfig({
222
+ testDir: './e2e',
223
+ fullyParallel: false,
224
+ forbidOnly: !!process.env.CI,
225
+ retries: process.env.CI ? 2 : 0,
226
+ workers: 2,
227
+ reporter: 'list',
228
+ timeout: 30_000,
229
+ use: {
230
+ baseURL,
231
+ channel: 'chrome',
232
+ trace: 'on-first-retry',
233
+ screenshot: 'only-on-failure',
234
+ },
235
+ webServer: {
236
+ command: 'pnpm build && pnpm preview --port 4173 --strictPort',
237
+ env: { HAI_E2E: '1' },
238
+ url: baseURL,
239
+ reuseExistingServer: false,
240
+ timeout: 180_000,
241
+ },
242
+ })
243
+ ```
244
+
245
+ ### E2E 工具函数模板
246
+
247
+ ```typescript
248
+ // e2e/helpers.ts
249
+ import type { APIRequestContext, Page } from '@playwright/test'
250
+
251
+ /** 生成唯一测试用户 */
252
+ export function uniqueUser(prefix = 'e2e') {
253
+ const safePrefix = (prefix.replace(/\W/g, '') || 'e2e').slice(0, 8)
254
+ const entropy = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
255
+ const id = entropy.slice(-10)
256
+ const username = `${safePrefix}_${id}`.slice(0, 20)
257
+ return {
258
+ username,
259
+ email: `${safePrefix}_${id}@test.local`,
260
+ password: 'Test1234!@',
261
+ }
262
+ }
263
+
264
+ /** 通过 API 注册用户 */
265
+ export async function registerViaApi(
266
+ request: APIRequestContext,
267
+ user: ReturnType<typeof uniqueUser>,
268
+ ) {
269
+ return request.post('/api/auth/register', {
270
+ data: {
271
+ username: user.username,
272
+ email: user.email,
273
+ password: user.password,
274
+ confirmPassword: user.password,
275
+ },
276
+ })
277
+ }
278
+
279
+ /** 注册并登录,返回已认证的 Page */
280
+ export async function registerAndLogin(
281
+ page: Page,
282
+ request: APIRequestContext,
283
+ prefix = 'e2e',
284
+ ) {
285
+ const user = uniqueUser(prefix)
286
+ await registerViaApi(request, user)
287
+ await page.goto('/login')
288
+ await page.fill('[name="username"]', user.username)
289
+ await page.fill('[name="password"]', user.password)
290
+ await page.click('button[type="submit"]')
291
+ await page.waitForURL('/')
292
+ return user
293
+ }
294
+ ```
295
+
296
+ ### API 端点 E2E 测试模板
297
+
298
+ ```typescript
299
+ // e2e/articles-api.spec.ts
300
+ import { expect, test } from '@playwright/test'
301
+ import { registerAndLogin } from './helpers'
302
+
303
+ test.describe('Articles API', () => {
304
+ test('GET /api/articles 未认证返回 401', async ({ request }) => {
305
+ const response = await request.get('/api/articles')
306
+ expect(response.status()).toBe(401)
307
+ })
308
+
309
+ test('POST /api/articles 创建文章成功', async ({ page, request }) => {
310
+ await registerAndLogin(page, request, 'article')
311
+
312
+ const response = await page.request.post('/api/articles', {
313
+ data: { title: 'E2E Test Article', content: 'Test content' },
314
+ })
315
+ expect(response.status()).toBe(201)
316
+
317
+ const body = await response.json()
318
+ expect(body.success).toBe(true)
319
+ expect(body.data.title).toBe('E2E Test Article')
320
+ })
321
+
322
+ test('POST /api/articles 空标题返回 400', async ({ page, request }) => {
323
+ await registerAndLogin(page, request, 'article')
324
+
325
+ const response = await page.request.post('/api/articles', {
326
+ data: { title: '', content: '' },
327
+ })
328
+ expect(response.status()).toBe(400)
329
+ })
330
+
331
+ test('GET /api/articles 认证后获取列表', async ({ page, request }) => {
332
+ await registerAndLogin(page, request, 'article')
333
+
334
+ const response = await page.request.get('/api/articles')
335
+ expect(response.ok()).toBeTruthy()
336
+
337
+ const body = await response.json()
338
+ expect(body.success).toBe(true)
339
+ expect(Array.isArray(body.data)).toBe(true)
340
+ })
341
+ })
342
+ ```
343
+
344
+ ### 页面 UI E2E 测试模板
345
+
346
+ ```typescript
347
+ // e2e/articles-ui.spec.ts
348
+ import { expect, test } from '@playwright/test'
349
+ import { registerAndLogin } from './helpers'
350
+
351
+ test.describe('Articles Page', () => {
352
+ test('未登录跳转登录页', async ({ page }) => {
353
+ await page.goto('/admin/articles')
354
+ await expect(page).toHaveURL(/login/)
355
+ })
356
+
357
+ test('已登录显示文章列表', async ({ page, request }) => {
358
+ await registerAndLogin(page, request, 'artui')
359
+ await page.goto('/admin/articles')
360
+
361
+ await expect(page.locator('h1, h2').first()).toContainText(/文章|article/i)
362
+ })
363
+
364
+ test('创建文章表单提交', async ({ page, request }) => {
365
+ await registerAndLogin(page, request, 'artui')
366
+ await page.goto('/admin/articles/new')
367
+
368
+ await page.fill('[name="title"]', 'E2E Article')
369
+ await page.fill('[name="content"]', 'E2E content')
370
+ await page.click('button[type="submit"]')
371
+
372
+ // 验证跳转或成功提示
373
+ await expect(page.locator('.alert-success, .toast')).toBeVisible({ timeout: 5000 })
374
+ })
375
+ })
376
+ ```
377
+
378
+ ---
379
+
380
+ ## TDD 用例设计策略
381
+
382
+ ### 从需求到测试用例的转换
383
+
384
+ ```
385
+ 需求:用户可以创建文章,标题必填且不超过 100 字符,需要登录后才能操作
386
+
387
+ ↓ 拆分测试点
388
+
389
+ 单元测试(Vitest):
390
+ ✅ 创建文章成功返回 HaiResult.ok
391
+ ✅ 空标题返回 HaiResult.err + VALIDATION_FAILED
392
+ ✅ 超长标题(>100 字符)返回 HaiResult.err
393
+ ✅ DB 异常时透传错误
394
+
395
+ Schema 测试(Vitest):
396
+ ✅ 合法输入通过校验
397
+ ✅ 空标题被拒绝
398
+ ✅ 超长标题被拒绝
399
+ ✅ 缺少必填字段被拒绝
400
+
401
+ E2E 测试(Playwright):
402
+ ✅ POST /api/articles 未认证 → 401
403
+ ✅ POST /api/articles 合法数据 → 201
404
+ ✅ POST /api/articles 空标题 → 400
405
+ ✅ 页面表单提交成功 → 跳转/提示
406
+ ✅ 未登录访问创建页 → 重定向登录
407
+ ```
408
+
409
+ ---
410
+
411
+ ## 断言规范
412
+
413
+ - 始终校验 `result.success`
414
+ - 失败时校验 `error.code`(不只检查 `success === false`)
415
+ - 使用类型缩窄后访问 data 或 error
416
+ - E2E 断言使用 Playwright 内置 expect 匹配器
417
+
418
+ ```typescript
419
+ // ✅ 单元测试断言
420
+ const result = await service.create(input)
421
+ expect(result.success).toBe(true)
422
+ if (result.success) {
423
+ expect(result.data.id).toBeDefined()
424
+ }
425
+
426
+ // ✅ 单元测试错误断言
427
+ const result = await service.create(invalidInput)
428
+ expect(result.success).toBe(false)
429
+ if (!result.success) {
430
+ expect(result.error.code).toBe('VALIDATION_FAILED')
431
+ }
432
+
433
+ // ✅ E2E API 断言
434
+ const response = await request.post('/api/articles', { data: input })
435
+ expect(response.status()).toBe(201)
436
+ const body = await response.json()
437
+ expect(body.success).toBe(true)
438
+
439
+ // ✅ E2E UI 断言
440
+ await expect(page.locator('.alert-success')).toBeVisible()
441
+ await expect(page).toHaveURL('/admin/articles')
442
+ ```
443
+
444
+ ---
445
+
446
+ ## 质量门禁
447
+
448
+ ```bash
449
+ # 运行单元测试
450
+ pnpm --filter <app-name> test
451
+
452
+ # 运行 E2E 测试
453
+ pnpm --filter <app-name> test:e2e
454
+
455
+ # 覆盖率
456
+ pnpm --filter <app-name> test:coverage
457
+
458
+ # 全量门禁
459
+ pnpm typecheck && pnpm lint && pnpm test
460
+ ```
461
+
462
+ ---
463
+
464
+ ## 相关 Skills
465
+
466
+ - `hai-app-create`:应用功能创建规范(TDD Green 阶段参照)
467
+ - `hai-app-review`:应用代码审查规范(TDD Refactor 阶段参照)
468
+ - `hai-build`:项目架构总览与 TDD 工作流导航
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: hai-audit
3
+ description: 使用 @h-ai/audit 进行审计日志的记录、查询、清理与统计。当需求涉及操作审计、安全事件追踪、用户活动日志时使用。
4
+ ---
5
+
6
+ # @h-ai/audit — 审计日志模块
7
+
8
+ 统一审计日志模块,基于 `@h-ai/reldb` 实现持久化,提供审计日志的记录、分页查询、清理与统计。
9
+
10
+ ## 依赖
11
+
12
+ | 模块 | 用途 | 是否必需 | 初始化要求 |
13
+ | --- | --- | --- | --- |
14
+ | `@h-ai/reldb` | 数据库(审计日志持久化) | **必需** | 需在 `audit.init()` 前初始化 |
15
+
16
+ ## 使用步骤
17
+
18
+ ### 1. 配置
19
+
20
+ 审计模块依赖已初始化的 `@h-ai/reldb` 单例,无需独立配置文件。审计日志固定存储在 `hai_audit_logs` 表中。
21
+
22
+ ### 2. 初始化
23
+
24
+ ```ts
25
+ import { audit } from '@h-ai/audit'
26
+ import { reldb } from '@h-ai/reldb'
27
+
28
+ // 先初始化依赖
29
+ await reldb.init({ type: 'sqlite', database: './data.db' })
30
+
31
+ // 再初始化审计模块(自动使用已初始化的 reldb 单例)
32
+ const result = await audit.init()
33
+ if (!result.success) {
34
+ // 处理错误
35
+ }
36
+ ```
37
+
38
+ 自定义用户表映射:
39
+
40
+ ```ts
41
+ await audit.init({
42
+ userTable: 'hai_iam_users',
43
+ userIdColumn: 'id',
44
+ userNameColumn: 'username',
45
+ })
46
+ ```
47
+
48
+ ### 3. 关闭
49
+
50
+ ```ts
51
+ await audit.close()
52
+ ```
53
+
54
+ ## 核心 API
55
+
56
+ ### 生命周期
57
+
58
+ | 方法 | 签名 | 说明 |
59
+ | --------------------- | ---------------------------------------------------------------- | ---------- |
60
+ | `audit.init` | `(config: AuditInitConfig) => Promise<HaiResult<void>>` | 初始化模块 |
61
+ | `audit.close` | `() => Promise<void>` | 关闭模块 |
62
+ | `audit.isInitialized` | `boolean` | 初始化状态 |
63
+
64
+ ### 日志操作
65
+
66
+ | 方法 | 签名 | 说明 |
67
+ | --------------------- | ----------------------------------------------------------------------------------- | -------------------- |
68
+ | `audit.log` | `(input: CreateAuditLogInput) => Promise<HaiResult<AuditLog>>` | 记录审计日志 |
69
+ | `audit.list` | `(options?: ListAuditLogsOptions) => Promise<HaiResult<{ items, total }>>` | 分页查询(含用户名) |
70
+ | `audit.getUserRecent` | `(userId: string, limit?: number) => Promise<HaiResult<AuditLog[]>>` | 用户最近活动 |
71
+ | `audit.cleanup` | `(olderThanDays?: number) => Promise<HaiResult<number>>` | 清理旧日志 |
72
+ | `audit.getStats` | `(days?: number) => Promise<HaiResult<AuditStatItem[]>>` | 操作统计 |
73
+
74
+ ### 便捷记录器 (`audit.helper`)
75
+
76
+ | 方法 | 说明 |
77
+ | ------------------------------------------------------------------------------ | ------------ |
78
+ | `audit.helper.login(userId, ip?, ua?)` | 登录 |
79
+ | `audit.helper.logout(userId, ip?, ua?)` | 登出 |
80
+ | `audit.helper.register(userId, ip?, ua?)` | 注册 |
81
+ | `audit.helper.passwordResetRequest(email, ip?, ua?)` | 密码重置请求 |
82
+ | `audit.helper.passwordResetComplete(userId, ip?, ua?)` | 密码重置完成 |
83
+ | `audit.helper.crud(input)` | CRUD 操作 |
84
+
85
+ ## 错误码
86
+
87
+ | 错误码 | code | 说明 |
88
+ | --------------------------------- | ---------------- | ------------ |
89
+ | `HaiAuditError.LOG_FAILED` | `hai:audit:001` | 记录失败 |
90
+ | `HaiAuditError.QUERY_FAILED` | `hai:audit:002` | 查询失败 |
91
+ | `HaiAuditError.CLEANUP_FAILED` | `hai:audit:003` | 清理失败 |
92
+ | `HaiAuditError.STATS_FAILED` | `hai:audit:004` | 统计失败 |
93
+ | `HaiAuditError.INIT_IN_PROGRESS` | `hai:audit:005` | 初始化进行中 |
94
+ | `HaiAuditError.NOT_INITIALIZED` | `hai:audit:010` | 模块未初始化 |
95
+ | `HaiAuditError.CONFIG_ERROR` | `hai:audit:012` | 配置错误 |
96
+
97
+ ## 常见模式
98
+
99
+ ### 用户认证审计
100
+
101
+ ```ts
102
+ // 登录
103
+ await audit.helper.login(userId, request.ip, request.headers['user-agent'])
104
+
105
+ // 登出
106
+ await audit.helper.logout(userId, request.ip, request.headers['user-agent'])
107
+ ```
108
+
109
+ ### CRUD 操作审计
110
+
111
+ ```ts
112
+ await audit.helper.crud({ userId, action: 'create', resource: 'users', resourceId: newUser.id, details: { name: newUser.name } })
113
+ await audit.helper.crud({ userId, action: 'update', resource: 'roles', resourceId: roleId, details: { changes } })
114
+ await audit.helper.crud({ userId, action: 'delete', resource: 'permissions', resourceId: permId })
115
+ ```
116
+
117
+ ### 仪表盘数据
118
+
119
+ ```ts
120
+ // 最近日志
121
+ const recentLogs = await audit.list({ pageSize: 10 })
122
+
123
+ // 7 天统计
124
+ const stats = await audit.getStats(7)
125
+ ```
126
+
127
+ ### 定期清理
128
+
129
+ ```ts
130
+ // 清理 90 天前的日志
131
+ const result = await audit.cleanup(90)
132
+ if (result.success) {
133
+ logger.info(`Cleaned up ${result.data} old audit logs`)
134
+ }
135
+ ```
136
+
137
+ ## 相关 Skills
138
+
139
+ - `hai-reldb` — 数据库模块(审计模块依赖)
140
+ - `hai-iam` — 身份认证与权限管理(审计的主要记录对象)
141
+ - `hai-core` — 核心工具(日志、ID 生成、i18n)