---
title: "Kobako：用 Coding Agent 時測試能做多少防護"
date: 2026-06-03T00:00:00+08:00
publishDate: 2026-06-03T00:00:00+08:00
lastmod: 2026-06-01T21:24:44+08:00
tags: ["LLM","AI","經驗","Ruby","Harness Engineering"]
toc: true
permalink: "https://blog.aotoki.me/posts/2026/06/03/tests-as-safety-net-with-coding-agent/"
language: "zh-tw"
---



Kobako 的開發過程中有蠻多有趣的案例，延續上一篇 [Kobako：用 AI 開發終究會翻車？](https://blog.aotoki.me/posts/2026/05/27/ai-development-hits-old-challenges/)的經歷，後續又再次撞到新的問題，而且是過去開發很擔心看到的 Segmentation Fault 錯誤，這表示在 Rust 和 WebAssembly 這段非 Ruby 的地方，很高機率做錯。

<!--more-->

## 不穩定測試{#flaky-test}

Kobako 大概在 0.5.0 左右開始減少功能擴充收窄，核心功能完善後就該朝穩定版本前進，沒想到反而出現了不穩定測試（Flaky Test）的狀況，測試在沒有任何改動下有機率出錯。

因為在對功能收尾，也就繼續往下開發，很快的測試就沒有出現 Segmentation Fault 的錯誤訊息，這反而讓我更加緊張，因為如果不能重現那要捕捉到問題會變得困難很多。

使用 Coding Agent 開發的風險確實如此，即使我們認為 Rust 是記憶體安全的語言，整個過程也花費不少力氣減少 `unsafe` 使用把大部分 mruby C API 封裝起來，最終還是遇到這樣的狀況。

但是，至少我們還有測試，這比完全沒有任何測試，到正式被使用或者釋出的時候才被捕捉到，已經好上非常多。

## 垃圾回收{#garbage-collection}

原本對排除問題不太期待，至少以過去的經驗來說是這樣。沒想到問題比預期還快被捕捉到，讓 Opus 4.7 跑幾次測試後，很快就確認是可以重現，而且給出「可能是垃圾回收造成」這樣的判斷。

而且運氣非常好，在 0.4.x 也就是上一個預定的版本中，並不會發生問題。只需要鎖定 0.4.x ~ Head 這一個範圍的提交，而這個提交剛好是針對 RPC -> Transport 重構所造成的問題，在 Ruby 和 Ruby Extension 這個邊界上，剛好出現一個物件標記的失誤。

在 Rust 我們對 Runtime 設計了一個 `#on_dispatch` 的 Handler 來處理從 Guest 向 Host 的方法呼叫，像是下面的情境

```ruby
Utils::Time.now # sandbox.define(:Utils).bind(:Time, Time)
# Kobako::Transport::Proxy -> #method_missing -> __kobako__dispatch
```

簡單來說，會透過 C API 從 WebAssembly 穿透，呼叫 Host Linker 來尋找符合 `Utils::Time.now` 這樣的方法，此時 `#on_dispatch` 負責處理如何呼叫。

但是，初始化順序是 Sandbox -> Runtime + Transport + Catalog 來進行的，對 Runtime 並不能事先知道 Transport 上的 Dispatcher 方法是什麼，也就需要借助 Sandbox 來幫忙[安裝](https://github.com/elct9620/kobako/blob/1e7b34339dfa1d8b9217150bcf50d68a7b07c8b8/lib/kobako/sandbox.rb#L219-L224)大致上如下

```ruby
    def install_dispatch_proc!
      @runtime.on_dispatch = lambda do |request_bytes|
        ...
      end
    end
```

當時 Rust 的實作是這樣處理 `#on_dispatch=` 的行為

```rust
pub(crate) fn set_on_dispatch(&self, proc_value: Value) -> Result<(), MagnusError> {
	let mut store_ref = self.store.borrow_mut();
	store_ref
		.data_mut()
		.bind_on_dispatch(Opaque::from(proc_value));
	Ok(())
}
```

簡單來說，我們在 Rust 將 `lambda do ... end` 轉成一個指標，然後保存在 Rust 的特定物件上，當 Ruby 的垃圾回收啟動時，因為沒有任何 Ruby 變數持有這個 Lambda 物件，所以就被判斷成「可以回收」清理掉，當 mruby 真正呼叫時，就變成對空指標操作，進而觸發 Segmentation Fault 的問題。

這個用法對於 Magnus 來說很常見，大多數狀況也都不會出錯，只要是跟 Ruby 互動就不太會有問題，但我們剛好需要一些跟 WebAssembly 整合的邊界，這就讓 LLM（大型語言模型）訓練資料中覺得「不常見」而沒有採用另一種方式處理，在這個地方用了錯誤的設計。

至少 Opus 4.7 在定位到問題後，搭配 WebSearch 確認文件很快的就理解到 Magnus 提供的 `Opaque::from` 不會做垃圾回收的標記，那麼對 Ruby 來說就是可回收的物件。

解決方法也不複雜，我們將這個物件的生命週期跟 Runtime 綁定，當 Runtime 被拋棄後就自動釋放掉 `#on_dispatch=` 設定的 Ruby 變數，而透過 `#on_dispatch=` 的 Ruby 變數則標記為不可回收，那麼問題就被排除。

## 測試的防護力{#border-of-tests}

這次剛好是一個蠻少見，但確實又是寫測試可以防禦到的案例。但也不是有測試就能完美的防禦，這就很看測試案例的設計，以及怎樣去覆蓋到關鍵的節點。

在過去的軟體開發流程上，測試大多還是一種成本，所以會優先做理想路線（Happy Path）的測試，確保在預期內行為的使用不太會出錯，如果還有餘力就會去覆蓋邊界情境（Edge Cases）來進一步加強。

到了使用 Coding Agent 後，寫程式碼本身的成本非常低，那就很適合用大量的測試做覆蓋。但覆蓋的範圍如果都是破碎、不完整的，實際上也不一定有用。像是只有單元測試，如果只測 Ruby 和 Rust 各自的物件、方法，這次的 Rust 保存 Ruby 變數的情境，就不會被捕捉到。

也因此，選擇「覆蓋的路徑」是一個很重要的判斷，走單元測試跟整合測試都能讓測試覆蓋到達 90% 以上，但是意義卻不同，中間只要有一條路線沒有完整跑完，就有機會漏掉「整合過程中的問題」而產生新的風險。

如果環境許可，我會推薦把端對端測試（End-to-End Testing）都覆蓋進去更理想，走 Behavior-Driven Development（行為驅動開發）也是不錯的方式，即使沒有要這樣做，也推薦設定理想路線的行為定義，像是 Kobako 的 [docs/behavior.md](https://github.com/elct9620/kobako/blob/main/docs/behavior.md) 就有提供每一種使用情境的規範，這次的問題也是這樣捕捉到的。

總而言之，不要太怕麻煩而覺得不做，這類文件交給 AI 處理的成本已經非常低，但是有沒有測試覆蓋跟覆蓋在脆弱的地方，會對這類 AI 驅動開發的軟體穩定性和產出的可信度有很大的影響。
