前三週先把 Google AI 生態系拆開來認識。
Gemini API 負責模型呼叫,Zod 負責結構化資料,Genkit 負責 AI Flow,Stitch 負責介面生成,Antigravity 則開始接觸 Agentic 開發。
到了第四週,這些東西不再只是獨立的實驗。
DevPulse 正式開始組裝。
不過第一天的任務不是急著把 Gemini 接進來,而是先準備好一組固定的「問題」。
因為後面要讓 AI 找 Bug、產生修復程式碼,最後還要驗證修復結果,前提是必須先有可以穩定重現的 Bug 與測試。
所以 Day 22 先把最基礎的地基做好:
Bug 程式碼
↓
Jest 測試
↓
❌ Fail
↓
後續交給 AI 修復
今天先不追求 AI。
先讓「紅燈」站穩。
先建立新的專案目錄:
mkdir devpulse
cd devpulse
npm init -y
接著安裝 Jest:
npm install -D jest
再把 package.json 的測試指令改成:
{
"scripts": {
"test": "jest"
}
}
專案初期先維持最簡單的結構:
devpulse/
├─ samples/
├─ tests/
├─ package.json
├─ package-lock.json
└─ node_modules/
今天的內容刻意不先加入 Genkit。
按照這幾天的學習節奏,工具不需要一次全部塞進來,而是每一天只增加一個真正需要的能力。
第一個案例是非同步程式碼。
建立:
samples/async-await.cjs
內容:
function sendAll(users, sendMail) {
const results = [];
users.forEach(async (user) => {
const result = await sendMail(user);
results.push(result);
});
return results;
}
module.exports = { sendAll };
表面上看起來很合理。
把所有使用者跑過一次,寄送完成後,把結果放進 results。
但問題在於:
users.forEach(async (user) => {
forEach() 不會等待裡面的 async callback。
因此:
return results;
可能在 sendMail() 真正完成之前就執行。
建立:
tests/async-await.test.cjs
原本的測試是:
const { sendAll } = require("../samples/async-await.cjs");
test("sendAll 應該在全部完成後回傳結果", async () => {
const sendMail = async (user) => user;
const result = await sendAll(["A", "B"], sendMail);
expect(result).toEqual(["A", "B"]);
});
執行:
npm test
結果卻是:
PASS tests/async-await.test.cjs
這裡反而出現了一個很有意思的問題:
Bug 存在,但測試沒有抓到 Bug。
原因是 sendMail() 幾乎立即完成,這個測試的執行時序剛好沒有把問題暴露出來。
因此測試本身也需要重新設計。
把測試改成刻意延遲:
const { sendAll } = require("../samples/async-await.cjs");
test("sendAll 應該在全部完成後回傳結果", async () => {
const sendMail = (user) =>
new Promise((resolve) => {
setTimeout(() => resolve(user), 50);
});
const result = await sendAll(["A", "B"], sendMail);
expect(result).toEqual(["A", "B"]);
});
這時候才會真正出現:
FAIL tests/async-await.test.cjs
也因此得到 Day 22 第一個很重要的觀念:
測試不是為了讓程式 PASS,而是要有能力把 Bug 穩定抓出來。
第二個案例換成資安問題。
建立:
samples/sql.cjs
內容:
function findUser(db, userName) {
const sql =
`SELECT * FROM users WHERE name = '${userName}'`;
return db.query(sql);
}
module.exports = { findUser };
這段程式碼看起來可以正常產生 SQL:
SELECT * FROM users WHERE name = 'Alice'
但問題在於 userName 是直接拼進 SQL 字串。
建立:
tests/sql.test.cjs
內容:
const { findUser } = require("../samples/sql.cjs");
test("findUser 應該使用參數化查詢", () => {
const db = {
query: jest.fn()
};
findUser(db, "Alice");
expect(db.query).toHaveBeenCalledWith(
"SELECT * FROM users WHERE name = ?",
["Alice"]
);
});
執行測試時,Jest 會指出:
Expected:
"SELECT * FROM users WHERE name = ?", ["Alice"]
Received:
"SELECT * FROM users WHERE name = 'Alice'"
這代表測試成功抓到問題。
程式目前雖然可以組出 SQL,但沒有使用參數化查詢。
這個案例也很適合留給後面的 Gemini 做程式碼審查。
最後一個案例處理邊界條件。
建立:
samples/boundary.cjs
內容:
function getUserName(user) {
return user.profile.name.trim();
}
module.exports = { getUserName };
正常資料:
{
profile: {
name: "Alice"
}
}
沒有問題。
但是只要 API 回傳:
{}
這段:
user.profile.name
就會變成:
undefined.name
最後直接拋出:
TypeError: Cannot read properties of undefined
因此測試先定義預期行為:
tests/boundary.test.cjs
const { getUserName } = require("../samples/boundary.cjs");
test("缺少 profile 時應該回傳 Unknown", () => {
expect(getUserName({})).toBe("Unknown");
});
目前程式沒有處理這個邊界情況,因此測試也會亮紅燈。
現在 samples/ 與 tests/ 會形成:
devpulse/
├─ samples/
│ ├─ async-await.cjs
│ ├─ sql.cjs
│ └─ boundary.cjs
│
├─ tests/
│ ├─ async-await.test.cjs
│ ├─ sql.test.cjs
│ └─ boundary.test.cjs
│
├─ package.json
├─ package-lock.json
└─ node_modules/
再執行:
npm test
這次應該得到:
FAIL tests/sql.test.cjs
FAIL tests/boundary.test.cjs
FAIL tests/async-await.test.cjs
Test Suites: 3 failed, 3 total
Tests: 3 failed, 3 total
三個測試全部紅燈。
而且這三個紅燈其實代表三種不同問題:
async-await
↓
非同步流程錯誤
SQL
↓
資安風險
boundary
↓
邊界條件錯誤
這三個案例也正好對應規劃中 Day 22 要準備的「非同步、SQL、邊界」三組微型 Bug。
到了這裡,很容易產生一個錯覺:
「測試不是應該全部通過嗎?」
但 DevPulse 現在的狀態剛好相反。
今天需要的是:
故意有問題的程式
↓
穩定重現的測試
↓
❌ Fail
因為後面真正要做的是:
Day 22
建立 Bug
↓
❌ Fail
Day 23
Gemini 分析
↓
找到問題
Day 24
Gemini 產生 Patch
↓
修復程式
Day 25
重新執行 Jest
↓
✅ Pass
因此現在這三個紅燈,其實就是後面 AI 修復功能的「起跑線」。
沒有紅燈,就沒有辦法證明 AI 修完之後真的變好了。
DevPulse 最終想完成的並不是單純:
貼上程式碼
↓
Gemini 找問題
而是:
Bug 程式碼
↓
Gemini 分析
↓
產生修復版本
↓
執行 Jest
↓
Fail → Pass
這也是為什麼 Day 22 沒有急著把 Genkit 接進來。
先把問題固定,後面的 AI 才有明確的工作目標。
下一篇才會正式讓 Genkit 回到 DevPulse:
sample code
↓
codeReviewFlow
↓
Gemini
↓
漏洞說明
↓
修改建議
今天真正花時間的地方,反而不是建立三個 Bug,而是發現:
一段有問題的程式,不代表測試一定能抓到問題。
最明顯的例子就是 forEach() 搭配 async。
第一次測試竟然 PASS,並不是因為程式沒問題,而是測試沒有製造出足以暴露競態的執行條件。
把 sendMail() 改成具有延遲的 Promise 後,問題才真正被測試抓出來。
這也讓 Day 22 的重點更加清楚:
AI 修 Bug 之前,先建立一個可靠的驗收標準。
這三個紅燈,就是 DevPulse 接下來幾天的起點。
今天完成:
✅ 從零建立 devpulse/
✅ 安裝 Jest
✅ 建立 samples/
✅ 建立 tests/
✅ 非同步 Bug
✅ SQL Bug
✅ 邊界條件 Bug
✅ 3 組測試穩定 Fail
下一篇進入 Day 23:撰寫 codeReviewFlow,讓 Genkit 開始替 DevPulse 分析這三個 Bug。