fix: harden unicode safety checks

This commit is contained in:
Affaan Mustafa
2026-03-29 08:59:06 -04:00
parent dd675d4258
commit 866d9ebb53
239 changed files with 3780 additions and 3962 deletions

View File

@@ -101,12 +101,12 @@ c) 依影響排序優先順序
**模式 1型別推論失敗**
```typescript
// 錯誤Parameter 'x' implicitly has an 'any' type
// FAIL: 錯誤Parameter 'x' implicitly has an 'any' type
function add(x, y) {
return x + y
}
// 修復:新增型別註解
// PASS: 修復:新增型別註解
function add(x: number, y: number): number {
return x + y
}
@@ -114,25 +114,25 @@ function add(x: number, y: number): number {
**模式 2Null/Undefined 錯誤**
```typescript
// 錯誤Object is possibly 'undefined'
// FAIL: 錯誤Object is possibly 'undefined'
const name = user.name.toUpperCase()
// 修復:可選串聯
// PASS: 修復:可選串聯
const name = user?.name?.toUpperCase()
// Null 檢查
// PASS:Null 檢查
const name = user && user.name ? user.name.toUpperCase() : ''
```
**模式 3缺少屬性**
```typescript
// 錯誤Property 'age' does not exist on type 'User'
// FAIL: 錯誤Property 'age' does not exist on type 'User'
interface User {
name: string
}
const user: User = { name: 'John', age: 30 }
// 修復:新增屬性到介面
// PASS: 修復:新增屬性到介面
interface User {
name: string
age?: number // 如果不是總是存在則為可選
@@ -141,10 +141,10 @@ interface User {
**模式 4Import 錯誤**
```typescript
// 錯誤Cannot find module '@/lib/utils'
// FAIL: 錯誤Cannot find module '@/lib/utils'
import { formatDate } from '@/lib/utils'
// 修復 1檢查 tsconfig paths 是否正確
// PASS: 修復 1檢查 tsconfig paths 是否正確
{
"compilerOptions": {
"paths": {
@@ -153,22 +153,22 @@ import { formatDate } from '@/lib/utils'
}
}
// 修復 2使用相對 import
// PASS: 修復 2使用相對 import
import { formatDate } from '../lib/utils'
// 修復 3安裝缺少的套件
// PASS: 修復 3安裝缺少的套件
npm install @/lib/utils
```
**模式 5型別不符**
```typescript
// 錯誤Type 'string' is not assignable to type 'number'
// FAIL: 錯誤Type 'string' is not assignable to type 'number'
const age: number = "30"
// 修復:解析字串為數字
// PASS: 修復:解析字串為數字
const age: number = parseInt("30", 10)
// 或:變更型別
// PASS: 或:變更型別
const age: string = "30"
```
@@ -177,34 +177,34 @@ const age: string = "30"
**關鍵:做最小可能的變更**
### 應該做:
在缺少處新增型別註解
在需要處新增 null 檢查
修復 imports/exports
新增缺少的相依性
更新型別定義
修復設定檔
PASS: 在缺少處新增型別註解
PASS: 在需要處新增 null 檢查
PASS: 修復 imports/exports
PASS: 新增缺少的相依性
PASS: 更新型別定義
PASS: 修復設定檔
### 不應該做:
重構不相關的程式碼
變更架構
重新命名變數/函式(除非是錯誤原因)
新增功能
變更邏輯流程(除非是修復錯誤)
優化效能
改善程式碼風格
FAIL: 重構不相關的程式碼
FAIL: 變更架構
FAIL: 重新命名變數/函式(除非是錯誤原因)
FAIL: 新增功能
FAIL: 變更邏輯流程(除非是修復錯誤)
FAIL: 優化效能
FAIL: 改善程式碼風格
**最小差異範例:**
```typescript
// 檔案有 200 行,第 45 行有錯誤
// 錯誤:重構整個檔案
// FAIL: 錯誤:重構整個檔案
// - 重新命名變數
// - 抽取函式
// - 變更模式
// 結果50 行變更
// 正確:只修復錯誤
// PASS: 正確:只修復錯誤
// - 在第 45 行新增型別註解
// 結果1 行變更
@@ -212,12 +212,12 @@ function processData(data) { // 第 45 行 - 錯誤:'data' implicitly has 'any
return data.map(item => item.value)
}
// 最小修復:
// PASS: 最小修復:
function processData(data: any[]) { // 只變更這行
return data.map(item => item.value)
}
// 更好的最小修復(如果知道型別):
// PASS: 更好的最小修復(如果知道型別):
function processData(data: Array<{ value: number }>) {
return data.map(item => item.value)
}
@@ -232,7 +232,7 @@ function processData(data: Array<{ value: number }>) {
**建置目標:** Next.js 生產 / TypeScript 檢查 / ESLint
**初始錯誤:** X
**已修復錯誤:** Y
**建置狀態:** 通過 / 失敗
**建置狀態:** PASS: 通過 / FAIL: 失敗
## 已修復的錯誤
@@ -260,11 +260,11 @@ Parameter 'market' implicitly has an 'any' type.
## 驗證步驟
1. TypeScript 檢查通過:`npx tsc --noEmit`
2. Next.js 建置成功:`npm run build`
3. ESLint 檢查通過:`npx eslint .`
4. 沒有引入新錯誤
5. 開發伺服器執行:`npm run dev`
1. PASS: TypeScript 檢查通過:`npx tsc --noEmit`
2. PASS: Next.js 建置成功:`npm run build`
3. PASS: ESLint 檢查通過:`npx eslint .`
4. PASS: 沒有引入新錯誤
5. PASS: 開發伺服器執行:`npm run dev`
```
## 何時使用此 Agent
@@ -287,13 +287,13 @@ Parameter 'market' implicitly has an 'any' type.
## 成功指標
建置錯誤解決後:
- `npx tsc --noEmit` 以代碼 0 結束
- `npm run build` 成功完成
- 沒有引入新錯誤
- 變更行數最小(< 受影響檔案的 5%
- 建置時間沒有顯著增加
- 開發伺服器無錯誤執行
- 測試仍然通過
- PASS: `npx tsc --noEmit` 以代碼 0 結束
- PASS: `npm run build` 成功完成
- PASS: 沒有引入新錯誤
- PASS: 變更行數最小(< 受影響檔案的 5%
- PASS: 建置時間沒有顯著增加
- PASS: 開發伺服器無錯誤執行
- PASS: 測試仍然通過
---

View File

@@ -81,15 +81,15 @@ model: opus
問題API 金鑰暴露在原始碼中
修復:移至環境變數
const apiKey = "sk-abc123"; // 錯誤
const apiKey = "sk-abc123"; // FAIL: 錯誤
const apiKey = process.env.API_KEY; // ✓ 正確
```
## 批准標準
- 批准:無關鍵或高優先問題
- ⚠️ 警告:僅有中優先問題(可謹慎合併)
- 阻擋:發現關鍵或高優先問題
- PASS: 批准:無關鍵或高優先問題
- WARNING: 警告:僅有中優先問題(可謹慎合併)
- FAIL: 阻擋:發現關鍵或高優先問題
## 專案特定指南(範例)

View File

@@ -109,14 +109,14 @@ c) 資料保護
**影響:** 大表上查詢快 100-1000 倍
```sql
-- 錯誤:外鍵沒有索引
-- FAIL: 錯誤:外鍵沒有索引
CREATE TABLE orders (
id bigint PRIMARY KEY,
customer_id bigint REFERENCES customers(id)
-- 缺少索引!
);
-- 正確:外鍵有索引
-- PASS: 正確:外鍵有索引
CREATE TABLE orders (
id bigint PRIMARY KEY,
customer_id bigint REFERENCES customers(id)
@@ -134,11 +134,11 @@ CREATE INDEX orders_customer_id_idx ON orders (customer_id);
| **Hash** | 僅等於 | `=`(比 B-tree 略快)|
```sql
-- 錯誤JSONB 包含用 B-tree
-- FAIL: 錯誤JSONB 包含用 B-tree
CREATE INDEX products_attrs_idx ON products (attributes);
SELECT * FROM products WHERE attributes @> '{"color": "red"}';
-- 正確JSONB 用 GIN
-- PASS: 正確JSONB 用 GIN
CREATE INDEX products_attrs_idx ON products USING gin (attributes);
```
@@ -147,11 +147,11 @@ CREATE INDEX products_attrs_idx ON products USING gin (attributes);
**影響:** 多欄位查詢快 5-10 倍
```sql
-- 錯誤:分開的索引
-- FAIL: 錯誤:分開的索引
CREATE INDEX orders_status_idx ON orders (status);
CREATE INDEX orders_created_idx ON orders (created_at);
-- 正確:複合索引(等於欄位在前,然後範圍)
-- PASS: 正確:複合索引(等於欄位在前,然後範圍)
CREATE INDEX orders_status_created_idx ON orders (status, created_at);
```
@@ -167,11 +167,11 @@ CREATE INDEX orders_status_created_idx ON orders (status, created_at);
**影響:** 透過避免表查找,查詢快 2-5 倍
```sql
-- 錯誤:必須從表獲取 name
-- FAIL: 錯誤:必須從表獲取 name
CREATE INDEX users_email_idx ON users (email);
SELECT email, name FROM users WHERE email = 'user@example.com';
-- 正確:所有欄位在索引中
-- PASS: 正確:所有欄位在索引中
CREATE INDEX users_email_idx ON users (email) INCLUDE (name, created_at);
```
@@ -180,10 +180,10 @@ CREATE INDEX users_email_idx ON users (email) INCLUDE (name, created_at);
**影響:** 索引小 5-20 倍,寫入和查詢更快
```sql
-- 錯誤:完整索引包含已刪除的列
-- FAIL: 錯誤:完整索引包含已刪除的列
CREATE INDEX users_email_idx ON users (email);
-- 正確:部分索引排除已刪除的列
-- PASS: 正確:部分索引排除已刪除的列
CREATE INDEX users_active_email_idx ON users (email) WHERE deleted_at IS NULL;
```
@@ -196,11 +196,11 @@ CREATE INDEX users_active_email_idx ON users (email) WHERE deleted_at IS NULL;
**影響:** 關鍵 - 資料庫強制的租戶隔離
```sql
-- 錯誤:僅應用程式篩選
-- FAIL: 錯誤:僅應用程式篩選
SELECT * FROM orders WHERE user_id = $current_user_id;
-- Bug 意味著所有訂單暴露!
-- 正確:資料庫強制的 RLS
-- PASS: 正確:資料庫強制的 RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
@@ -220,11 +220,11 @@ CREATE POLICY orders_user_policy ON orders
**影響:** RLS 查詢快 5-10 倍
```sql
-- 錯誤:每列呼叫一次函式
-- FAIL: 錯誤:每列呼叫一次函式
CREATE POLICY orders_policy ON orders
USING (auth.uid() = user_id); -- 1M 列呼叫 1M 次!
-- 正確:包在 SELECT 中(快取,只呼叫一次)
-- PASS: 正確:包在 SELECT 中(快取,只呼叫一次)
CREATE POLICY orders_policy ON orders
USING ((SELECT auth.uid()) = user_id); -- 快 100 倍
@@ -235,10 +235,10 @@ CREATE INDEX orders_user_id_idx ON orders (user_id);
### 3. 最小權限存取
```sql
-- 錯誤:過度寬鬆
-- FAIL: 錯誤:過度寬鬆
GRANT ALL PRIVILEGES ON ALL TABLES TO app_user;
-- 正確:最小權限
-- PASS: 正確:最小權限
CREATE ROLE app_readonly NOLOGIN;
GRANT USAGE ON SCHEMA public TO app_readonly;
GRANT SELECT ON public.products, public.categories TO app_readonly;
@@ -260,36 +260,36 @@ REVOKE ALL ON SCHEMA public FROM public;
**影響:** 批量插入快 10-50 倍
```sql
-- 錯誤:個別插入
-- FAIL: 錯誤:個別插入
INSERT INTO events (user_id, action) VALUES (1, 'click');
INSERT INTO events (user_id, action) VALUES (2, 'view');
-- 1000 次往返
-- 正確:批次插入
-- PASS: 正確:批次插入
INSERT INTO events (user_id, action) VALUES
(1, 'click'),
(2, 'view'),
(3, 'click');
-- 1 次往返
-- 最佳:大資料集用 COPY
-- PASS: 最佳:大資料集用 COPY
COPY events (user_id, action) FROM '/path/to/data.csv' WITH (FORMAT csv);
```
### 2. 消除 N+1 查詢
```sql
-- 錯誤N+1 模式
-- FAIL: 錯誤N+1 模式
SELECT id FROM users WHERE active = true; -- 回傳 100 個 IDs
-- 然後 100 個查詢:
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 2;
-- ... 還有 98 個
-- 正確:用 ANY 的單一查詢
-- PASS: 正確:用 ANY 的單一查詢
SELECT * FROM orders WHERE user_id = ANY(ARRAY[1, 2, 3, ...]);
-- 正確JOIN
-- PASS: 正確JOIN
SELECT u.id, u.name, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
@@ -301,11 +301,11 @@ WHERE u.active = true;
**影響:** 無論頁面深度,一致的 O(1) 效能
```sql
-- 錯誤OFFSET 隨深度變慢
-- FAIL: 錯誤OFFSET 隨深度變慢
SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 199980;
-- 掃描 200,000 列!
-- 正確:游標式(總是快)
-- PASS: 正確:游標式(總是快)
SELECT * FROM products WHERE id > 199980 ORDER BY id LIMIT 20;
-- 使用索引O(1)
```
@@ -313,11 +313,11 @@ SELECT * FROM products WHERE id > 199980 ORDER BY id LIMIT 20;
### 4. UPSERT 用於插入或更新
```sql
-- 錯誤:競態條件
-- FAIL: 錯誤:競態條件
SELECT * FROM settings WHERE user_id = 123 AND key = 'theme';
-- 兩個執行緒都找不到,都插入,一個失敗
-- 正確:原子 UPSERT
-- PASS: 正確:原子 UPSERT
INSERT INTO settings (user_id, key, value)
VALUES (123, 'theme', 'dark')
ON CONFLICT (user_id, key)
@@ -329,27 +329,27 @@ RETURNING *;
## 要標記的反模式
### 查詢反模式
### FAIL: 查詢反模式
- 生產程式碼中用 `SELECT *`
- WHERE/JOIN 欄位缺少索引
- 大表上用 OFFSET 分頁
- N+1 查詢模式
- 非參數化查詢SQL 注入風險)
### 結構描述反模式
### FAIL: 結構描述反模式
- IDs 用 `int`(應用 `bigint`
- 無理由用 `varchar(255)`(應用 `text`
- `timestamp` 沒有時區(應用 `timestamptz`
- 隨機 UUIDs 作為主鍵(應用 UUIDv7 或 IDENTITY
- 需要引號的混合大小寫識別符
### 安全性反模式
### FAIL: 安全性反模式
- `GRANT ALL` 給應用程式使用者
- 多租戶表缺少 RLS
- RLS 政策每列呼叫函式(沒有包在 SELECT 中)
- RLS 政策欄位沒有索引
### 連線反模式
### FAIL: 連線反模式
- 沒有連線池
- 沒有閒置逾時
- Transaction 模式連線池使用 Prepared statements

View File

@@ -220,28 +220,28 @@ test('market search with complex query', async ({ page }) => {
**1. 競態條件**
```typescript
// 不穩定:不要假設元素已準備好
// FAIL: 不穩定:不要假設元素已準備好
await page.click('[data-testid="button"]')
// 穩定:等待元素準備好
// PASS: 穩定:等待元素準備好
await page.locator('[data-testid="button"]').click() // 內建自動等待
```
**2. 網路時序**
```typescript
// 不穩定:任意逾時
// FAIL: 不穩定:任意逾時
await page.waitForTimeout(5000)
// 穩定:等待特定條件
// PASS: 穩定:等待特定條件
await page.waitForResponse(resp => resp.url().includes('/api/markets'))
```
**3. 動畫時序**
```typescript
// 不穩定:在動畫期間點擊
// FAIL: 不穩定:在動畫期間點擊
await page.click('[data-testid="menu-item"]')
// 穩定:等待動畫完成
// PASS: 穩定:等待動畫完成
await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' })
await page.waitForLoadState('networkidle')
await page.click('[data-testid="menu-item"]')
@@ -290,13 +290,13 @@ use: {
## 成功指標
E2E 測試執行後:
- 所有關鍵旅程通過100%
- 總體通過率 > 95%
- 不穩定率 < 5%
- 沒有失敗測試阻擋部署
- 產出物已上傳且可存取
- 測試時間 < 10 分鐘
- HTML 報告已產生
- PASS: 所有關鍵旅程通過100%
- PASS: 總體通過率 > 95%
- PASS: 不穩定率 < 5%
- PASS: 沒有失敗測試阻擋部署
- PASS: 產出物已上傳且可存取
- PASS: 測試時間 < 10 分鐘
- PASS: HTML 報告已產生
---

View File

@@ -146,22 +146,22 @@ e) 驗證測試仍通過
### 1. 未使用的 Imports
```typescript
// 移除未使用的 imports
// FAIL: 移除未使用的 imports
import { useState, useEffect, useMemo } from 'react' // 只有 useState 被使用
// 只保留使用的
// PASS: 只保留使用的
import { useState } from 'react'
```
### 2. 無用程式碼分支
```typescript
// 移除不可達的程式碼
// FAIL: 移除不可達的程式碼
if (false) {
// 這永遠不會執行
doSomething()
}
// 移除未使用的函式
// FAIL: 移除未使用的函式
export function unusedHelper() {
// 程式碼庫中沒有參考
}
@@ -169,18 +169,18 @@ export function unusedHelper() {
### 3. 重複元件
```typescript
// 多個類似元件
// FAIL: 多個類似元件
components/Button.tsx
components/PrimaryButton.tsx
components/NewButton.tsx
// 整合為一個
// PASS: 整合為一個
components/Button.tsx variant prop
```
### 4. 未使用的相依性
```json
// 已安裝但未 import 的套件
// FAIL: 已安裝但未 import 的套件
{
"dependencies": {
"lodash": "^4.17.21", // 沒有在任何地方使用
@@ -261,12 +261,12 @@ components/Button.tsx帶 variant prop
## 成功指標
清理工作階段後:
- 所有測試通過
- 建置成功
- 沒有 console 錯誤
- DELETION_LOG.md 已更新
- Bundle 大小減少
- 生產環境沒有回歸
- PASS: 所有測試通過
- PASS: 建置成功
- PASS: 沒有 console 錯誤
- PASS: DELETION_LOG.md 已更新
- PASS: Bundle 大小減少
- PASS: 生產環境沒有回歸
---

View File

@@ -128,12 +128,12 @@ b) 審查高風險區域
### 1. 寫死密鑰(關鍵)
```javascript
// 關鍵:寫死的密鑰
// FAIL: 關鍵:寫死的密鑰
const apiKey = "sk-proj-xxxxx"
const password = "admin123"
const token = "ghp_xxxxxxxxxxxx"
// 正確:環境變數
// PASS: 正確:環境變數
const apiKey = process.env.OPENAI_API_KEY
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured')
@@ -143,11 +143,11 @@ if (!apiKey) {
### 2. SQL 注入(關鍵)
```javascript
// 關鍵SQL 注入弱點
// FAIL: 關鍵SQL 注入弱點
const query = `SELECT * FROM users WHERE id = ${userId}`
await db.query(query)
// 正確:參數化查詢
// PASS: 正確:參數化查詢
const { data } = await supabase
.from('users')
.select('*')
@@ -157,11 +157,11 @@ const { data } = await supabase
### 3. 命令注入(關鍵)
```javascript
// 關鍵:命令注入
// FAIL: 關鍵:命令注入
const { exec } = require('child_process')
exec(`ping ${userInput}`, callback)
// 正確:使用函式庫,而非 shell 命令
// PASS: 正確:使用函式庫,而非 shell 命令
const dns = require('dns')
dns.lookup(userInput, callback)
```
@@ -169,10 +169,10 @@ dns.lookup(userInput, callback)
### 4. 跨站腳本 XSS
```javascript
// XSS 弱點
// FAIL:XSS 弱點
element.innerHTML = userInput
// 正確:使用 textContent 或清理
// PASS: 正確:使用 textContent 或清理
element.textContent = userInput
// 或
import DOMPurify from 'dompurify'
@@ -182,10 +182,10 @@ element.innerHTML = DOMPurify.sanitize(userInput)
### 5. 伺服器端請求偽造 SSRF
```javascript
// SSRF 弱點
// FAIL:SSRF 弱點
const response = await fetch(userProvidedUrl)
// 正確:驗證和白名單 URL
// PASS: 正確:驗證和白名單 URL
const allowedDomains = ['api.example.com', 'cdn.example.com']
const url = new URL(userProvidedUrl)
if (!allowedDomains.includes(url.hostname)) {
@@ -197,10 +197,10 @@ const response = await fetch(url.toString())
### 6. 不安全的驗證(關鍵)
```javascript
// 關鍵:明文密碼比對
// FAIL: 關鍵:明文密碼比對
if (password === storedPassword) { /* login */ }
// 正確:雜湊密碼比對
// PASS: 正確:雜湊密碼比對
import bcrypt from 'bcrypt'
const isValid = await bcrypt.compare(password, hashedPassword)
```
@@ -208,13 +208,13 @@ const isValid = await bcrypt.compare(password, hashedPassword)
### 7. 授權不足(關鍵)
```javascript
// 關鍵:沒有授權檢查
// FAIL: 關鍵:沒有授權檢查
app.get('/api/user/:id', async (req, res) => {
const user = await getUser(req.params.id)
res.json(user)
})
// 正確:驗證使用者可以存取資源
// PASS: 正確:驗證使用者可以存取資源
app.get('/api/user/:id', authenticateUser, async (req, res) => {
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' })
@@ -227,13 +227,13 @@ app.get('/api/user/:id', authenticateUser, async (req, res) => {
### 8. 財務操作中的競態條件(關鍵)
```javascript
// 關鍵:餘額檢查中的競態條件
// FAIL: 關鍵:餘額檢查中的競態條件
const balance = await getBalance(userId)
if (balance >= amount) {
await withdraw(userId, amount) // 另一個請求可能同時提款!
}
// 正確:帶鎖定的原子交易
// PASS: 正確:帶鎖定的原子交易
await db.transaction(async (trx) => {
const balance = await trx('balances')
.where({ user_id: userId })
@@ -253,13 +253,13 @@ await db.transaction(async (trx) => {
### 9. 速率限制不足(高)
```javascript
// 高:沒有速率限制
// FAIL: 高:沒有速率限制
app.post('/api/trade', async (req, res) => {
await executeTrade(req.body)
res.json({ success: true })
})
// 正確:速率限制
// PASS: 正確:速率限制
import rateLimit from 'express-rate-limit'
const tradeLimiter = rateLimit({
@@ -277,10 +277,10 @@ app.post('/api/trade', tradeLimiter, async (req, res) => {
### 10. 記錄敏感資料(中)
```javascript
// 中:記錄敏感資料
// FAIL: 中:記錄敏感資料
console.log('User login:', { email, password, apiKey })
// 正確:清理日誌
// PASS: 正確:清理日誌
console.log('User login:', {
email: email.replace(/(?<=.).(?=.*@)/g, '*'),
passwordProvided: !!password
@@ -302,7 +302,7 @@ console.log('User login:', {
- **高優先問題:** Y
- **中優先問題:** Z
- **低優先問題:** W
- **風險等級:** 🔴 高 / 🟡 中 / 🟢
- **風險等級:** 高 / 中 / 低
## 關鍵問題(立即修復)
@@ -324,7 +324,7 @@ console.log('User login:', {
**修復:**
```javascript
// 安全的實作
// PASS: 安全的實作
```
**參考:**
@@ -365,13 +365,13 @@ console.log('User login:', {
## 成功指標
安全性審查後:
- 未發現關鍵問題
- 所有高優先問題已處理
- 安全性檢查清單完成
- 程式碼中無密鑰
- 相依性已更新
- 測試包含安全性情境
- 文件已更新
- PASS: 未發現關鍵問題
- PASS: 所有高優先問題已處理
- PASS: 安全性檢查清單完成
- PASS: 程式碼中無密鑰
- PASS: 相依性已更新
- PASS: 測試包含安全性情境
- PASS: 文件已更新
---

View File

@@ -220,26 +220,26 @@ jest.mock('@/lib/openai', () => ({
## 測試異味(反模式)
### 測試實作細節
### FAIL: 測試實作細節
```typescript
// 不要測試內部狀態
expect(component.state.count).toBe(5)
```
### 測試使用者可見的行為
### PASS: 測試使用者可見的行為
```typescript
// 測試使用者看到的
expect(screen.getByText('Count: 5')).toBeInTheDocument()
```
### 測試相互依賴
### FAIL: 測試相互依賴
```typescript
// 不要依賴前一個測試
test('creates user', () => { /* ... */ })
test('updates same user', () => { /* 需要前一個測試 */ })
```
### 獨立測試
### PASS: 獨立測試
```typescript
// 在每個測試中設定資料
test('updates user', () => {