---
date: 2025-10-13T19:54:31+08:00
researcher: claude
git_commit: b38fd6945b09a1cde23982ff4f58c106d6035d33
branch: main
repository: fwauto
topic: "AI Brain Node 回歸測試機制研究"
tags: [research, codebase, testing, ai-brain-node, regression, non-deterministic, claude-code-sdk, prompt-platform-compatibility]
status: complete
last_updated: 2025-10-13
last_updated_by: claude
last_updated_note: "Added follow-up research for prompt platform compatibility issue"
---

# Research: AI Brain Node 回歸測試機制

**Date**: 2025-10-13T19:54:31+08:00
**Researcher**: claude
**Git Commit**: b38fd6945b09a1cde23982ff4f58c106d6035d33
**Branch**: main
**Repository**: fwauto

## Research Question

如何為 `ai_brain_node` 建立回歸測試機制，以驗證 AI（基於 Claude Code SDK）在各種編譯錯誤情境下的修正能力，並量化 prompt 改進效果？

**核心挑戰**:
1. AI 行為不確定（同樣錯誤有時成功有時失敗）
2. 需要在真實專案結構上測試，但要避免污染開發環境
3. 缺乏明確的「測試通過」標準和 prompt 效能基準線

## Summary

經過全面研究，發現 FWAuto 具備建立 AI 回歸測試的基礎設施：

**已有的優勢**:
- ✅ 完善的測試架構（pytest + tmp_path 隔離）
- ✅ 錨點模式（`.fwauto/`）天然支援多專案隔離
- ✅ 清晰的 State 管理（immutable 更新模式）
- ✅ Prompt 模板化系統（Jinja2）
- ✅ 真實專案可用（STM32F407 實例1-61）

**關鍵挑戰**:
- ⚠️ `ai_brain_node` 無單元測試（0% 覆蓋率）
- ⚠️ Claude Code SDK 難以 Mock（async generator + 檔案副作用）
- ⚠️ 缺少目錄複製/清理工具（`shutil.copytree`, `rmtree`）
- ⚠️ 4 個 prompt 模板缺失（`implement_feature`, `fix_bug`, `natural_language`, `general_codegen`）
- ⚠️ 無統計測試框架（重複執行、成功率計算）

**可行方向**:
- 💡 擴展現有 `tmp_path` + `test_project_root` 模式
- 💡 使用「專案副本 + Makefile replay」隔離真實環境
- 💡 借鑑 `log_node` 測試模式 Mock SDK
- 💡 設計統計測試框架（pytest parametrize + 多次執行）

## Detailed Findings

### 1. 現有測試架構分析

#### 測試組織結構

```mermaid
graph TD
    A[tests/] --> B[nodes/<br/>單元測試]
    A --> C[integration/<br/>整合測試]
    A --> D[test_story_*<br/>BDD 測試]

    B --> B1[test_build_makefile.py<br/>7 tests]
    B --> B2[test_flash_makefile.py<br/>7 tests]

    C --> C1[conftest.py<br/>test_project_root fixture]
    C --> C2[test_build_integration.py<br/>3 tests]
    C --> C3[test_flash_integration.py<br/>3 tests]

    D --> D1[test_story_001_state_expansion.py]
    D --> D2[test_story_003_prompt_template.py]
    D --> D3[test_story_004_log_analysis_node.py]
    D --> D4[test_story_006_error_handling.py]

    style B fill:#e1f5e1
    style C fill:#e3f2fd
    style D fill:#fff3e0
```

**統計數據**:
- 測試檔案: 10 個
- 測試函數: 52 個
- Story 測試佔比: 55%（BDD 風格為主）
- 單元測試佔比: 30%
- 整合測試佔比: 15%

#### 環境隔離機制

**Integration Test Pattern** (`tests/integration/conftest.py:11-46`):

```python
@pytest.fixture
def test_project_root(tmp_path):
    """建立測試用專案目錄結構 (pytest 自動清理)."""
    project_root = tmp_path / "test_project"
    project_root.mkdir()

    fwauto_dir = project_root / ".fwauto"
    fwauto_dir.mkdir()
    (fwauto_dir / "build").mkdir()
    (fwauto_dir / "flash").mkdir()
    (fwauto_dir / "logs").mkdir()

    # 動態生成測試用 Makefile...
    return project_root
```

**特性**:
| 特性 | 實作方式 | 優勢 | 限制 |
|------|---------|------|------|
| 隔離性 | `tmp_path` fixture | 每個測試獨立目錄 | 無法測試真實專案結構 |
| 清理 | pytest 自動清理 | 無需手動 teardown | 測試失敗時無法保留現場 |
| 重複性 | 動態生成專案結構 | 100% 可重複 | 設定成本高 |
| 真實性 | Makefile replay 技術 | 使用真實工具輸出 | 無法測試實際編譯 |

#### Mock 策略

**重度 Mock（Unit Tests）**:

```python
@patch("fwauto.nodes.build.load_project_config")
@patch("fwauto.utils.subprocess.Popen")
def test_build_node_success(mock_popen, mock_load_config, base_state):
    # Mock config
    mock_config = Mock()
    mock_config.build_makefile = ".fwauto/build/Makefile"
    mock_load_config.return_value = mock_config

    # Mock process
    mock_process = Mock()
    mock_process.returncode = 0
    mock_process.communicate.return_value = ("Build successful\n", None)
    mock_popen.return_value = mock_process
```

**最小 Mock（Integration Tests）**:
- 使用真實檔案系統（`tmp_path`）
- 使用測試用 Makefile（重播真實輸出）
- 只 Mock 無法模擬的硬體（ST-LINK）

#### 發現的問題

**Import 路徑不一致** (`tests/test_story_*.py` vs `tests/nodes/*.py`):
```python
# Story 測試
from src.fwauto.state import FirmwareState

# Node/Integration 測試
from fwauto.state import FirmwareState
```

**影響**: 部分測試無法執行（`ModuleNotFoundError`）

---

### 2. AI Brain Node 實作分析

#### 架構設計

```mermaid
graph LR
    A[FirmwareState] --> B[ai_brain_node]
    B --> C[analyze_task_type]
    C --> D{Task Type}

    D -->|fix_build_errors| E1[PromptManager]
    D -->|implement_feature| E2[❌ 缺少 prompt]
    D -->|fix_bug| E3[❌ 缺少 prompt]
    D -->|natural_language| E4[❌ 缺少 prompt]
    D -->|general_codegen| E5[❌ 缺少 prompt]

    E1 --> F[get_prompt]
    F --> G[Jinja2 渲染]
    G --> H[Claude Code SDK]
    H --> I[query: async generator]
    I --> J[responses: list]
    J --> K[update_state_from_result]
    K --> L[返回新 State]

    style E2 fill:#ffebee
    style E3 fill:#ffebee
    style E4 fill:#ffebee
    style E5 fill:#ffebee
    style H fill:#e1f5e1
```

#### SDK 呼叫細節

**位置**: `src/fwauto/nodes/ai_brain.py:36-62`

```python
async def run_ai_brain():
    # 1. 分析任務類型
    task_type = analyze_task_type(state)

    # 2. 生成 prompt（從模板）
    prompt = prompt_manager.get_prompt(task_type, dict(state))

    # 3. 配置 SDK 選項
    options = get_ai_options(state)

    # 4. 執行 AI query（async generator）
    responses = []
    async for message in query(prompt=prompt, options=options):
        logger.debug(f"🧠 AI Brain: {message}")
        _print_message(message)
        responses.append(str(message))

    return {"success": True, "responses": responses, "task_type": task_type}
```

**SDK 配置** (`ai_brain.py:109-133`):

| 參數 | 值 | 用途 |
|------|---|------|
| `allowed_tools` | `["Read", "Write", "Edit", "MultiEdit", "Glob", "Grep", "Bash"]` | 允許 AI 使用的工具 |
| `permission_mode` | `"bypassPermissions"` | 跳過權限檢查（等同 `--dangerously-skip-permissions`） |
| `add_dirs` | `[project_root]` | 給 SDK 存取 STM32 專案目錄 |
| `cwd` | `project_root` | 工作目錄設定為專案根目錄 |

#### 輸入輸出格式

**輸入** (從 `FirmwareState`):
```python
{
    "build_errors": ["error: undefined reference to 'Delay'"],
    "build_status": "error",
    "build_log": "完整編譯日誌...",
    "project_root": "/path/to/stm32_project",
    "iteration": 1,
    "max_retries": 3,
    "execution_mode": "fix"
}
```

**輸出** (返回新 State):
```python
# 成功時
{
    **state,
    "build_status": "success",
    "build_errors": [],
    "build_log": "🧠 AI Brain completed: fix_build_errors"
}

# 失敗時
{
    **state,
    "build_status": "error",
    "build_errors": [result["error"]],
    "build_log": f"🧠 AI Brain failed: {result['error']}",
    "iteration": state.get("iteration", 0) + 1
}
```

#### 測試挑戰

**挑戰 1: Async Generator Mock**

```python
# 參考 test_story_006 的做法
with patch("src.fwauto.nodes.log.query") as mock_query:
    async def mock_response():
        yield "測試分析結果"

    mock_query.return_value = mock_response()
    result = log_node(test_state)
```

**挑戰 2: 副作用隔離**

SDK 實際會執行：
- 讀取/寫入檔案（Read, Write, Edit, MultiEdit）
- 執行 Bash 命令
- 搜尋檔案（Glob, Grep）

**挑戰 3: 不確定性**

同樣 prompt + state，AI 可能產生不同結果。

---

### 3. 測試環境隔離策略

#### 專案管理機制

**錨點模式** (`src/fwauto/config.py:84-109`):

```mermaid
graph TD
    A[當前目錄] --> B{".fwauto/" 存在?}
    B -->|是| C[返回此目錄為 project_root]
    B -->|否| D[向上一層目錄]
    D --> E{是根目錄?}
    E -->|是| F[返回 None]
    E -->|否| B

    style C fill:#e1f5e1
    style F fill:#ffebee
```

**特性**:
- 類似 Git 的 `.git/` 搜尋機制
- 自動發現專案邊界
- 多專案並存，互不干擾

#### 現有功能盤點

| 功能 | 可用性 | 實作位置 | 說明 |
|------|--------|---------|------|
| **專案搜尋** | ✅ 完整 | `config.py:find_project_root()` | 錨點模式，向上搜尋 `.fwauto/` |
| **專案初始化** | ✅ 完整 | `cli_commands/init.py` | 建立 `.fwauto/` 結構 |
| **專案載入** | ✅ 完整 | `nodes/init.py:init_project_node()` | 驗證和載入專案 |
| **檔案複製** | ⚠️ 部分 | `shutil.copy2()` | 單檔案複製，**無目錄複製** |
| **目錄建立** | ✅ 完整 | `Path.mkdir()` | 遞迴建立目錄 |
| **臨時目錄** | ❌ 無 | - | 無 `tempfile` 使用 |
| **目錄複製** | ❌ 無 | - | 無 `shutil.copytree()` |
| **清理機制** | ❌ 無 | - | 無主動清理邏輯 |
| **Git 操作** | ❌ 無 | - | 不涉及版本控制 |

#### State 路徑管理

```python
# FirmwareState 中的路徑欄位
class FirmwareState(TypedDict):
    project_root: Annotated[str, "Absolute path to the project root"]
    project_initialized: Annotated[bool, "Flag indicating if project paths are validated"]
    log_file_path: Annotated[str | None, "Path to UART log file"]
    firmware_path: Annotated[str | None, "Generated firmware hex file path"]
```

**特性**:
- `project_root` 是**絕對路徑字串**（非 `Path` 物件）
- 所有節點都需轉換為 `Path` 使用
- State 採用 immutable 更新模式（`{**state, ...}`）

#### 隔離方向建議

**方向 A: 擴展 tmp_path 模式**
```python
@pytest.fixture
def isolated_stm32_project(tmp_path, stm32_template):
    """複製完整 STM32 專案到臨時目錄"""
    project = tmp_path / "stm32_project"
    shutil.copytree(stm32_template, project)  # ⚠️ 需新增

    # 建立 .fwauto/ 結構
    (project / ".fwauto").mkdir()
    # ...

    return project
```

**方向 B: 快照與恢復**
```python
@pytest.fixture
def project_snapshot(real_project_root):
    """備份專案狀態"""
    snapshot = {
        'config': (real_project_root / ".fwauto/config.toml").read_text(),
        'main_c': (real_project_root / "USER/main.c").read_text(),
    }

    yield real_project_root

    # 恢復
    for file, content in snapshot.items():
        file.write_text(content)
```

**方向 C: Mock Makefile + 虛擬專案**
- 最小化測試專案（只有 `.fwauto/` + 幾個測試檔案）
- 使用 Mock Makefile 模擬編譯輸出
- 完全控制測試環境

---

### 4. STM32 專案結構分析

#### 專案總覽

**位置**: `/home/kent/dev/playground/STM32F407/examples/`

**數量**: 61 個實例專案

**類別分布**:

```mermaid
pie title STM32F407 實例專案分類
    "基礎週邊(LED,KEY,UART)" : 13
    "定時器與PWM" : 5
    "顯示(OLED,LCD)" : 8
    "通訊協定(IIC,SPI,CAN)" : 12
    "感測器" : 7
    "儲存(SD,FATFS)" : 6
    "進階功能(USB,網路,RTOS)" : 10
```

#### 標準目錄結構

```
實例X XXX實例/
├── CORE/                    # ARM Cortex-M4 核心檔案
│   └── startup_stm32f407xx.s
├── HALLIB/                  # STM32 HAL 庫
│   └── STM32F4xx_HAL_Driver/
├── HARDWARE/                # ⚠️ 專案特定硬體驅動（測試關鍵）
│   ├── LED/
│   ├── KEY/
│   └── [其他週邊]/
├── SYSTEM/                  # 系統支援函式
│   ├── delay/
│   ├── sys/
│   └── usart/
├── USER/                    # ⚠️ 使用者程式碼（測試目標）
│   ├── *.uvprojx           # Keil 專案檔
│   ├── main.c              # 主程式
│   ├── stm32f4xx_hal_conf.h
│   └── stm32f4xx_it.c
└── OBJ/                     # 編譯輸出（測試驗證）
```

#### 最適合測試的專案

🏆 **推薦: 實例1 - 跑馬燈實例**

| 評估標準 | 分數 | 說明 |
|---------|------|------|
| **結構簡單** | ⭐⭐⭐⭐⭐ | 僅需 LED 驅動，無複雜週邊 |
| **容易驗證** | ⭐⭐⭐⭐⭐ | LED 閃爍視覺明顯，成功/失敗清晰 |
| **修改風險** | ⭐⭐⭐⭐⭐ | 邏輯單純（GPIO toggle + delay），不涉及中斷 |
| **編譯速度** | ⭐⭐⭐⭐⭐ | 檔案數量少（214 個），編譯快速 |
| **行為可預測** | ⭐⭐⭐⭐⭐ | LED0/LED1 交替閃爍，無外部依賴 |

**主要程式碼** (`USER/main.c`):
```c
while(1) {
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_RESET);  // LED0 亮
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_10, GPIO_PIN_SET);   // LED1 滅
    delay_ms(500);
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_SET);    // LED0 滅
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_10, GPIO_PIN_RESET); // LED1 亮
    delay_ms(500);
}
```

**常見編譯錯誤案例** (可製造的測試場景):

| 錯誤類型 | 製造方式 | AI 修復難度 |
|---------|---------|------------|
| Function 名稱錯誤 | `delay_ms` → `Delay_ms` | ⭐ 簡單 |
| 缺少 include | 移除 `#include "delay.h"` | ⭐⭐ 中等 |
| 型別錯誤 | `uint16_t` → `uint8_t` | ⭐⭐ 中等 |
| 變數未定義 | 使用未宣告的變數 | ⭐ 簡單 |
| 語法錯誤 | 缺少分號、括號 | ⭐ 簡單 |

---

### 5. Prompt 系統設計

#### 系統架構

```mermaid
graph TD
    A[ai_brain_node] --> B[analyze_task_type]
    B --> C{Task Type}
    C --> D[PromptManager]

    D --> E[_build_variables]
    E --> F1[系統變數<br/>cwd, username, os_name, timestamp]
    E --> F2[State 變數<br/>FirmwareState 所有欄位]

    F1 --> G[合併變數字典]
    F2 --> G

    G --> H[Jinja2 Environment]
    H --> I[載入模板<br/>prompts/task_type.md]
    I --> J[template.render]
    J --> K[渲染後的 prompt]

    style I fill:#e1f5e1
```

#### Prompt 模板現況

**已實作的模板**:

| 檔案 | 大小 | Task Type | 使用變數 | 功能 |
|------|------|-----------|---------|------|
| `fix_build_errors.md` | 2.6KB | `fix_build_errors` | `project_root`, `cwd`, `iteration`, `max_retries`, `build_status`, `build_errors`, `build_log` | 編譯錯誤自動修復 |
| `log.md` | 524B | `log` | `log_file_path`, `user_prompt`, `cwd` | UART 日誌分析 |

**缺失的模板** (⚠️ 會導致 `TemplateNotFound` 錯誤):

| Task Type | 觸發條件 | 影響功能 |
|-----------|---------|---------|
| `implement_feature` | `execution_mode == "feat"` | `fwauto --feat` 模式 |
| `fix_bug` | `execution_mode == "fix"` | `fwauto --fix` 模式 |
| `natural_language` | `execution_mode == "natural"` | `fwauto --natural` 模式 |
| `general_codegen` | 預設情況 | 一般程式碼生成 |

#### Jinja2 配置

**模板引擎設定** (`prompt_manager.py:22-26`):
```python
self.env = Environment(
    loader=FileSystemLoader(self.prompts_dir),
    undefined=StrictUndefined,  # ⚠️ 嚴格模式：變數缺失會拋出異常
)
```

**影響**:
- ✅ 優點：及早發現模板錯誤（拼字錯誤、變數遺漏）
- ⚠️ 風險：如果 `FirmwareState` 缺少欄位，直接拋出 `UndefinedError`

#### fix_build_errors.md 分析

**核心限制** (Prompt 內容摘要):

```markdown
## ⚠️ 絕對限制（違反即立即停止）

🚫 **禁止修改**:
- SYSTEM/, CORE/, HALLIB/, HARDWARE/ 目錄

🚫 **禁止執行**:
- 編譯命令、下載工具

✅ **唯一允許**:
- 使用 Read, Edit, MultiEdit 工具修改 USER/ 目錄

## 停止條件
- 錯誤來自系統目錄
- 缺少標頭檔案
- 需要下載依賴
- 涉及 HAL 庫或 BSP 檔案
```

**可修復的錯誤類型**:
- 函數拼寫錯誤
- 語法錯誤（缺少分號、括號）
- 變數未宣告
- 邏輯錯誤

---

## Code References

| 檔案路徑 | 行號 | 說明 |
|---------|------|------|
| `src/fwauto/nodes/ai_brain.py` | 36-62 | AI Brain 核心執行邏輯（async query） |
| `src/fwauto/nodes/ai_brain.py` | 83-106 | `analyze_task_type` 函數 |
| `src/fwauto/nodes/ai_brain.py` | 109-133 | `get_ai_options` SDK 配置 |
| `src/fwauto/nodes/ai_brain.py` | 136-160 | `update_state_from_result` 狀態更新 |
| `src/fwauto/prompt_manager.py` | 31-61 | `get_prompt` 模板載入與渲染 |
| `src/fwauto/prompt_manager.py` | 63-73 | `_build_variables` 變數建立 |
| `src/fwauto/config.py` | 84-109 | `find_project_root` 錨點模式搜尋 |
| `src/fwauto/state.py` | - | `FirmwareState` TypedDict 定義 |
| `tests/integration/conftest.py` | 11-46 | `test_project_root` fixture |
| `tests/test_story_006_error_handling.py` | - | `log_node` 錯誤處理測試（參考模式） |
| `prompts/fix_build_errors.md` | - | 編譯錯誤修復 prompt 模板 |

## Architecture Insights

### 測試分層設計

FWAuto 採用三層測試金字塔：

```
              Story Tests (BDD)
             /                  \
        Integration Tests    Unit Tests
       (最小 Mock)          (重度 Mock)
```

**設計優勢**:
- Story Tests 驗證業務需求（Given-When-Then）
- Integration Tests 驗證節點協作（真實檔案系統 + Makefile replay）
- Unit Tests 驗證節點邏輯（完全 Mock 外部依賴）

**缺口**:
- `ai_brain_node` 無任何測試（0/3 層）
- PromptManager 無獨立測試

### 錨點模式的擴展空間

`.fwauto/` 錨點模式提供了天然的測試隔離基礎：

**當前用途**:
- 專案邊界標記
- 配置檔案目錄
- 日誌檔案目錄

**可擴展方向**:
- `.fwauto/tests/` - 測試專用目錄
- `.fwauto/snapshots/` - 專案快照備份
- `.fwauto/fixtures/` - 測試固定數據

### Prompt 版本管理缺失

**當前狀況**:
- 僅透過 Git commit 追蹤變更
- 無 prompt 內部版本號
- 無變更日誌（CHANGELOG）

**可能方向**:
- 在 prompt 模板中嵌入版本 metadata
- 建立 prompt 版本相容性檢查
- 追蹤 prompt 效能變化（成功率 baseline）

### Claude Code SDK 整合模式

**發現的模式**:
```python
# 1. 配置 SDK 選項（限制工具、設定工作目錄）
options = ClaudeCodeOptions(
    allowed_tools=[...],
    permission_mode="bypassPermissions",
    add_dirs=[project_root],
    cwd=project_root
)

# 2. Async generator 執行
async for message in query(prompt=prompt, options=options):
    # 處理 streaming response
    responses.append(str(message))

# 3. 結果處理
return {"success": True, "responses": responses}
```

**測試挑戰**:
- SDK 是黑盒（內部實作不透明）
- Async generator 難以 Mock
- 副作用難以驗證（檔案修改、Bash 執行）

**可行測試策略**:
- Mock `query` 函數（參考 `log_node` 測試）
- 驗證 SDK 配置正確性（不執行實際 query）
- 使用隔離專案測試實際行為（E2E 測試）

## Technical Feasibility

### 方向 1: 擴展現有測試模式

**基於現有的 `tmp_path` + `test_project_root` 模式**

**可行性**: ⭐⭐⭐⭐⭐ (高)

**需要補充的功能**:
1. **目錄複製**: 使用 `shutil.copytree()` 複製 STM32 專案
2. **專案模板**: 建立 `tests/fixtures/stm32_templates/` 目錄
3. **錯誤製造器**: 工具函數動態修改檔案製造錯誤

**範例架構**:
```python
@pytest.fixture
def stm32_project_template():
    """STM32 實例1 (跑馬燈) 路徑"""
    return Path("/home/kent/dev/playground/STM32F407/examples/實例1 跑馬燈實例")

@pytest.fixture
def isolated_stm32_project(tmp_path, stm32_project_template):
    """複製 STM32 專案到隔離環境"""
    project = tmp_path / "stm32_test_project"
    shutil.copytree(stm32_project_template, project)

    # 建立 .fwauto/ 結構
    init_fwauto_structure(project)

    return project

def inject_error(project_root, error_type):
    """製造特定類型的編譯錯誤"""
    main_c = project_root / "USER" / "main.c"
    content = main_c.read_text()

    if error_type == "function_name":
        content = content.replace("delay_ms", "Delay_ms")
    elif error_type == "missing_include":
        content = content.replace('#include "delay.h"', "")
    # ...

    main_c.write_text(content)
```

**優勢**:
- 利用現有測試基礎設施
- pytest 自動清理
- 完全隔離

**限制**:
- 複製大型專案耗時（可能 1-2 秒）
- 需要真實的 Keil 編譯器（或 Mock Makefile）

---

### 方向 2: 統計測試框架

**使用 pytest parametrize + 重複執行**

**可行性**: ⭐⭐⭐⭐ (中高)

**設計概念**:
```python
@pytest.mark.parametrize("error_type", [
    "function_name_typo",
    "missing_include",
    "type_mismatch",
    "undefined_variable",
    "syntax_error",
])
@pytest.mark.parametrize("run_number", range(10))  # 每個案例跑 10 次
def test_ai_brain_fixes_error(isolated_stm32_project, error_type, run_number):
    """測試 AI 修復特定錯誤類型（重複執行以量化成功率）"""
    # 1. 製造錯誤
    inject_error(isolated_stm32_project, error_type)

    # 2. 初始化 State
    state = create_test_state(isolated_stm32_project)
    state["build_status"] = "error"
    state["build_errors"] = get_expected_errors(error_type)

    # 3. 執行 AI Brain
    result = ai_brain_node(state)

    # 4. 重新編譯驗證
    build_result = build_node(result)

    # 5. 記錄結果（不直接 assert，而是收集統計）
    record_result(error_type, run_number, build_result["build_status"])

# 測試後分析
def test_ai_brain_success_rate_baseline():
    """驗證整體成功率 >= 70%"""
    results = load_test_results()
    success_rate = calculate_success_rate(results)
    assert success_rate >= 0.70, f"成功率 {success_rate:.1%} 低於基準線 70%"
```

**優勢**:
- 量化 AI 行為（成功率統計）
- 可比較不同 prompt 版本
- 可追蹤效能趨勢

**限制**:
- 測試時間長（10 錯誤類型 × 10 次 = 100 次 AI 執行）
- 需要設計結果記錄機制
- 需要統計分析工具

---

### 方向 3: Mock SDK + 單元測試

**先測試 ai_brain_node 的邏輯，不測試 SDK 實際行為**

**可行性**: ⭐⭐⭐⭐⭐ (高)

**可測試的部分**:

1. **`analyze_task_type` 函數** (純函數):
```python
def test_analyze_task_type_build_errors():
    state = {"build_errors": ["error"], "build_status": "error"}
    assert analyze_task_type(state) == "fix_build_errors"

def test_analyze_task_type_implement_feature():
    state = {"execution_mode": "feat"}
    assert analyze_task_type(state) == "implement_feature"
```

2. **`update_state_from_result` 函數** (純函數):
```python
def test_update_state_from_result_success():
    state = {"build_status": "error", "build_errors": ["e1"]}
    result = {"success": True, "task_type": "fix_build_errors"}

    new_state = update_state_from_result(state, result)

    assert new_state["build_status"] == "success"
    assert new_state["build_errors"] == []
```

3. **SDK 配置**:
```python
def test_get_ai_options_with_project_root():
    state = {"project_root": "/test/project"}
    options = get_ai_options(state)

    assert options.allowed_tools == ["Read", "Write", "Edit", "MultiEdit", "Glob", "Grep", "Bash"]
    assert options.permission_mode == "bypassPermissions"
    assert options.add_dirs == ["/test/project"]
    assert options.cwd == "/test/project"
```

4. **Mock SDK 執行**:
```python
@patch("src.fwauto.nodes.ai_brain.query")
def test_ai_brain_node_calls_sdk(mock_query, base_state):
    async def mock_response():
        yield AssistantMessage(content=[TextBlock(text="Fixed")])

    mock_query.return_value = mock_response()

    result = ai_brain_node(base_state)

    assert result["build_status"] == "success"
    mock_query.assert_called_once()
```

**優勢**:
- 快速執行（無實際 AI 呼叫）
- 驗證核心邏輯正確性
- 可獨立於 SDK 開發

**限制**:
- 無法測試實際 AI 行為
- 需要額外的 E2E 測試補充

---

### 方向 4: E2E 測試（手動或半自動）

**定期執行完整工作流程測試**

**可行性**: ⭐⭐⭐ (中)

**流程**:
1. 準備測試專案（STM32 實例1）
2. 製造錯誤（手動或腳本）
3. 執行 `fwauto` 完整流程
4. 人工驗證結果
5. 記錄成功/失敗統計

**優勢**:
- 最真實的測試
- 驗證完整工作流程
- 可發現整合問題

**限制**:
- 耗時長（需要實際編譯）
- 難以自動化
- 結果不穩定（AI 不確定性）

---

## Historical Context

### 測試演進歷史

**Git 歷史分析**:
```
e6b61c4 make file works
9947424 test: ✅ fix all failing tests and refactor mock strategies
044fe52 log node
e81a6de remove prompts        ← 曾經移除過 prompts
244359f feat: ✨ implement comprehensive logging system
```

**觀察**:
1. 測試架構在 `9947424` 進行過大規模重構
2. Prompt 系統曾被移除後重新加入
3. 日誌系統（`log_node`）是最近加入的功能
4. `ai_brain_node` 始終沒有測試覆蓋

### 相關研究

無相關的先前研究文件（`.ai/rpi/research/` 目錄為空）。

## Open Questions

### 1. 測試環境隔離的最佳策略？

**問題**: 應該使用哪種方式隔離測試環境？

**選項**:
- A. 完整複製 STM32 專案到 `tmp_path`（最隔離，但慢）
- B. 只複製 `USER/` 目錄 + Mock Makefile（快速，但不真實）
- C. 快照 + 還原機制（真實環境，但有污染風險）
- D. Docker 容器（完全隔離，但複雜）

**需要驗證**:
- 各方案的執行時間
- 複製 214 個檔案需要多久
- pytest-xdist 並行執行可行性

---

### 2. 如何定義「測試通過」？

**問題**: AI 不確定性下，如何判斷測試成功？

**當前建議**: 70% 成功率基準

**需要驗證**:
- 70% 是否合理？（基於什麼數據？）
- 需要跑多少次才有統計意義？（10次？50次？100次？）
- 如何處理邊緣案例（49% vs 51%）？
- 是否需要信賴區間（Confidence Interval）？

---

### 3. Prompt 版本如何比較？

**問題**: 修改 prompt 後，如何量化改進效果？

**挑戰**:
- 需要固定的測試案例集
- 需要多次執行（減少隨機性）
- 需要統計檢驗（χ² test, t-test）

**範例場景**:
```
Prompt v1.0: 7/10 成功（70%）
Prompt v1.1: 8/10 成功（80%）
問題：這個差異有統計意義嗎？還是運氣？
```

**需要設計**:
- Prompt 版本管理機制
- A/B 測試框架
- 統計分析工具

---

### 4. 如何處理 SDK 副作用？

**問題**: Claude Code SDK 可能執行的操作：
- 修改檔案（Write, Edit）
- 執行命令（Bash）
- 搜尋檔案（Glob, Grep）

**挑戰**:
- 如何驗證 SDK「做了正確的事」？
- 如何檢測「危險行為」（例如：網路下載、修改系統目錄）？
- 如何在測試中重現 SDK 的實際行為？

**可能方向**:
- 設計 SDK 行為監控機制
- 記錄所有 SDK 工具呼叫
- 建立「允許/禁止」操作白名單

---

### 5. 測試案例覆蓋率？

**問題**: 需要多少種錯誤類型才足夠？

**當前建議**: 10+ 個案例

**需要驗證**:
- STM32 + Keil 的常見編譯錯誤有哪些？
- 哪些錯誤是 AI 「應該能修」的？
- 哪些錯誤是 AI 「不應該碰」的（需要停止條件）？

**範例分類**:
| 錯誤類型 | AI 應該 | 優先級 |
|---------|--------|-------|
| Function 拼寫錯誤 | 能修 | P0 |
| 缺少 include | 能修 | P0 |
| 型別錯誤 | 能修 | P1 |
| 缺少 HAL 函式庫 | 停止 | P0（安全） |
| 記憶體不足 | 停止 | P1（安全） |

---

### 6. 長期維護策略？

**問題**: 測試本身需要維護嗎？

**考量因素**:
- STM32 專案結構變更（HAL 庫升級）
- FWAuto 架構變更（State schema 變更）
- Prompt 變更（可能破壞測試假設）
- SDK 變更（API 變更）

**需要設計**:
- 測試相容性檢查
- 測試資料版本管理
- 測試失敗的根因分析流程

---

## 總結

### 核心發現

1. **FWAuto 具備良好的測試基礎設施**
   - pytest + fixture 設計清晰
   - `tmp_path` 隔離模式成熟
   - Makefile replay 技術創新

2. **`ai_brain_node` 是測試空白區**
   - 0% 測試覆蓋率
   - 無單元測試、無整合測試、無 Story 測試
   - PromptManager 也無獨立測試

3. **環境隔離機制可擴展**
   - 錨點模式提供天然基礎
   - 缺少目錄複製/清理工具
   - 可借鑑現有 Integration Test 模式

4. **STM32 實例1 是理想測試專案**
   - 結構最簡單、容易驗證
   - 可製造多種編譯錯誤
   - 編譯快速、行為可預測

5. **Prompt 系統需要補完**
   - 4 個模板缺失（會導致執行時錯誤）
   - 無版本管理機制
   - 無效能基準線

### 技術可行性評估

| 測試方向 | 可行性 | 優先級 | 實施成本 |
|---------|--------|-------|---------|
| Mock SDK 單元測試 | ⭐⭐⭐⭐⭐ | P0 | 低（1-2天） |
| 擴展 tmp_path 模式 | ⭐⭐⭐⭐⭐ | P0 | 中（3-5天） |
| 統計測試框架 | ⭐⭐⭐⭐ | P1 | 中高（5-7天） |
| Prompt 版本比較 | ⭐⭐⭐ | P1 | 高（7-10天） |
| E2E 測試自動化 | ⭐⭐⭐ | P2 | 高（10-14天） |

### 建議優先順序

**Phase 1: 基礎建設（P0）**
1. 補齊缺失的 prompt 模板（避免執行時錯誤）
2. 為 `ai_brain_node` 增加單元測試（Mock SDK）
3. 為 `PromptManager` 增加單元測試
4. 增加 `shutil.copytree()` / `rmtree()` 封裝

**Phase 2: 環境隔離（P0-P1）**
5. 設計 `isolated_stm32_project` fixture
6. 建立錯誤製造工具（`inject_error` 函數）
7. 實作 Mock Makefile 或整合真實編譯

**Phase 3: 統計測試（P1）**
8. 設計統計測試框架（parametrize + 重複執行）
9. 建立結果記錄機制（JSON 或資料庫）
10. 實作成功率分析工具

**Phase 4: 持續改進（P2）**
11. Prompt 版本管理機制
12. A/B 測試框架
13. 長期效能追蹤

---

**研究完成時間**: 2025-10-13T19:54:31+08:00
**研究範圍**: 全面分析 FWAuto 測試基礎設施、AI Brain Node 架構、環境隔離機制、STM32 專案結構、Prompt 系統設計
**主要結論**: 技術可行，建議採用「擴展 tmp_path 模式 + Mock SDK 單元測試」作為起點，逐步建立統計測試框架

---

## Follow-up Research [2025-10-13T20:26:46+08:00]

### 新發現：Prompt 平台綁定問題

#### 問題識別

在研究 Prompt 系統設計時，發現了一個架構層級的關鍵問題：

**當前 `fix_build_errors.md` 的平台綁定**:

```markdown
## ⚠️ 絕對限制（違反即立即停止）

🚫 **禁止修改**:
- SYSTEM/, CORE/, HALLIB/, HARDWARE/ 目錄

✅ **允許修改**:
- USER/ 目錄
```

**問題核心**:
- 這些目錄名稱（`SYSTEM/`, `CORE/`, `HALLIB/`, `HARDWARE/`, `USER/`）**只存在於 STM32 + Keil 專案結構**
- ESP32 + ESP-IDF 使用 `components/`, `main/` 等完全不同的目錄
- Nordic SDK、Arduino、Zephyr 等平台各有不同結構
- **違反 FWAuto 的「泛用韌體開發 agent」設計目標**

**影響範圍**:
- 測試設計：如果測試基於 STM32 結構，無法驗證多平台支援
- Prompt 效能：當支援新平台時，需要重寫 prompt（破壞性變更）
- 技術債：早期綁定會導致後期重構成本高

---

#### 解決方案評估

經過分析，識別出以下可行方案：

##### 方案 A：分層 Prompt 設計 ⭐⭐⭐⭐⭐

**架構**:
```
prompts/
├── base/
│   └── fix_build_errors_base.md      # 通用邏輯和原則
├── platforms/
│   ├── stm32_keil.md                 # STM32 特定限制
│   ├── esp32_idf.md                  # ESP32 特定限制
│   ├── nordic_segger.md              # Nordic 特定限制
│   └── arduino.md                    # Arduino 特定限制
└── fix_build_errors.md               # 入口（動態組合）
```

**實作方式**：Jinja2 Include

`prompts/fix_build_errors.md`:
```jinja2
你是韌體開發專家，**當前任務：立即自動修復編譯錯誤**

## 專案環境
- **平台**: {{platform}}
- **專案路徑**: `{{project_root}}`

{# 動態載入平台特定限制 #}
{% include "platforms/" + platform + ".md" %}

## 通用原則
- 只修復明確的編譯錯誤
- 保持最小化修改
- 不下載外部依賴
```

**優勢**:
- ✅ 通用邏輯只寫一次（DRY 原則）
- ✅ 平台差異明確隔離
- ✅ 新增平台只需加一個檔案
- ✅ 易於維護和測試
- ✅ Prompt 可追蹤性強（每個平台獨立版本）

**挑戰**:
- ⚠️ 需要在 `FirmwareState` 增加 `platform` 欄位
- ⚠️ Jinja2 include 路徑管理需要小心

**長期架構價值**: ⭐⭐⭐⭐⭐

---

##### 方案 B：參數化 Prompt ⭐⭐⭐⭐⭐ (立即可行)

**架構**：在 Python 中定義平台配置，透過變數注入 prompt

`src/fwauto/platforms.py` (新增檔案):
```python
from typing import TypedDict

class PlatformConfig(TypedDict):
    name: str
    forbidden_dirs: list[str]
    allowed_dirs: list[str]
    stop_conditions: list[str]

PLATFORMS = {
    "stm32_keil": {
        "name": "STM32 + Keil",
        "forbidden_dirs": ["SYSTEM", "CORE", "HALLIB", "HARDWARE"],
        "allowed_dirs": ["USER"],
        "stop_conditions": [
            "錯誤來自系統目錄",
            "缺少 HAL 標頭檔案",
            "涉及 HAL 庫或 BSP 檔案",
        ],
    },
    "esp32_idf": {
        "name": "ESP32 + ESP-IDF",
        "forbidden_dirs": ["components", "build"],
        "allowed_dirs": ["main"],
        "stop_conditions": [
            "錯誤來自 ESP-IDF 框架",
            "缺少 ESP-IDF 元件",
        ],
    },
    "nordic_nrf": {
        "name": "Nordic nRF + Segger",
        "forbidden_dirs": ["sdk", "config"],
        "allowed_dirs": ["src"],
        "stop_conditions": [
            "錯誤來自 Nordic SDK",
            "缺少 SoftDevice 相關檔案",
        ],
    },
}
```

`prompts/fix_build_errors.md` (修改):
```jinja2
你是 {{platform_config.name}} 韌體開發專家

## ⚠️ 絕對限制（違反即立即停止）

🚫 **禁止修改**:
{% for dir in platform_config.forbidden_dirs -%}
- `{{dir}}/` 目錄
{% endfor %}

✅ **允許修改**:
{% for dir in platform_config.allowed_dirs -%}
- `{{dir}}/` 目錄
{% endfor %}

## 停止條件
{% for condition in platform_config.stop_conditions -%}
- {{condition}}
{% endfor %}
```

`src/fwauto/prompt_manager.py` (修改):
```python
def _build_variables(self, state: dict[str, Any]) -> dict[str, Any]:
    from .platforms import get_platform_config

    platform = state.get("platform", "stm32_keil")  # 預設平台
    platform_config = get_platform_config(platform)

    return {
        # 系統變數
        "cwd": os.getcwd(),
        "username": os.getenv("USERNAME", os.getenv("USER", "unknown")),
        "os_name": platform.system(),
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),

        # 平台配置
        "platform": platform,
        "platform_config": platform_config,

        # 狀態變數
        **state,
    }
```

**優勢**:
- ✅ 最小改動（只需新增一個檔案）
- ✅ 平台規則在 Python 中定義（型別安全、易測試）
- ✅ Prompt 模板保持乾淨
- ✅ 可程式化驗證規則
- ✅ **立即可實施**（1-2 天）

**挑戰**:
- ⚠️ 規則與 prompt 分離，可能降低可讀性
- ⚠️ 需要在 `FirmwareState` 增加 `platform` 欄位

**實施成本**: 低（1-2 天）

---

##### 方案 C：Per-Platform Prompt ⭐⭐ (不推薦)

**架構**：每個平台一個完整 prompt

```
prompts/
├── fix_build_errors_stm32_keil.md
├── fix_build_errors_esp32_idf.md
├── fix_build_errors_nordic_nrf.md
└── fix_build_errors_arduino.md
```

**優勢**:
- ✅ 最簡單直接

**缺點**:
- ❌ 違反 DRY 原則（大量重複內容）
- ❌ 維護困難（改一個邏輯要改 N 個檔案）
- ❌ 容易產生不一致性
- ❌ 測試成本高

**不推薦理由**：技術債過高

---

#### 建議採用：方案 B（參數化 Prompt）

**理由**:

1. **立即可行**：最小改動，1-2 天可完成
2. **易於測試**：平台配置在 Python 中，可直接單元測試
3. **漸進式演進**：未來可平滑升級到方案 A（分層設計）
4. **符合測試需求**：參數化設計天然支援測試參數化

**實施步驟**:

**Step 1**: 增加 `FirmwareState.platform` 欄位
```python
class FirmwareState(TypedDict):
    # ... 現有欄位
    platform: Annotated[str, "Platform identifier (stm32_keil, esp32_idf, etc.)"]
```

**Step 2**: 建立 `src/fwauto/platforms.py`
- 定義 `PlatformConfig` TypedDict
- 建立 `PLATFORMS` 字典（至少包含 `stm32_keil`, `esp32_idf`）
- 實作 `get_platform_config(platform: str) -> PlatformConfig`

**Step 3**: 修改 `PromptManager._build_variables()`
- 從 state 取得 `platform`（預設 `"stm32_keil"`）
- 載入對應的 `platform_config`
- 注入到變數字典

**Step 4**: 更新 `prompts/fix_build_errors.md`
- 移除硬編碼的目錄名稱
- 使用 Jinja2 迴圈渲染 `platform_config`

**Step 5**: 更新測試架構
- 測試需要參數化（`@pytest.mark.parametrize("platform", [...])`）
- 每個平台需要獨立的 fixture

**預估工作量**：1-2 天

---

#### 對測試的影響

##### 測試複雜度增加

**原本**：只測試 STM32 場景
```python
def test_ai_brain_fixes_function_typo():
    # 固定使用 STM32 專案
    ...
```

**改為**：參數化測試多平台
```python
@pytest.mark.parametrize("platform", ["stm32_keil", "esp32_idf", "nordic_nrf"])
def test_ai_brain_fixes_function_typo(platform, platform_project):
    """測試 AI 修復函數拼寫錯誤（跨平台）"""
    inject_error(platform_project, "function_typo", platform)

    state = create_test_state(platform_project)
    state["platform"] = platform

    result = ai_brain_node(state)
    # ...
```

##### 需要平台專用 Fixture

```python
@pytest.fixture
def platform_project(platform, tmp_path):
    """根據平台建立測試專案"""
    if platform == "stm32_keil":
        return create_stm32_project(tmp_path)
    elif platform == "esp32_idf":
        return create_esp32_project(tmp_path)
    elif platform == "nordic_nrf":
        return create_nordic_project(tmp_path)
    # ...
```

##### 錯誤製造器需要平台感知

```python
def inject_error(project_root, error_type, platform):
    """根據平台製造特定類型的編譯錯誤"""
    if platform == "stm32_keil":
        target_file = project_root / "USER" / "main.c"
    elif platform == "esp32_idf":
        target_file = project_root / "main" / "main.c"
    # ...
```

##### 測試優先順序

**Phase 1**（P0）：先完成 STM32 測試
- 建立基礎測試框架
- 驗證參數化設計可行性

**Phase 2**（P1）：加入第二個平台（ESP32）
- 驗證跨平台擴展性
- 識別共通測試模式

**Phase 3**（P2）：全面多平台支援
- 加入 Nordic、Arduino 等

---

#### 架構改進建議

##### 建議 1：平台自動偵測

```python
# src/fwauto/platforms.py
def detect_platform(project_root: Path) -> str:
    """自動偵測專案平台"""
    if (project_root / "USER").exists() and (project_root / "CORE").exists():
        return "stm32_keil"
    elif (project_root / "main" / "CMakeLists.txt").exists():
        return "esp32_idf"
    elif (project_root / "Makefile.posix").exists():
        return "nordic_nrf"
    else:
        return "unknown"
```

##### 建議 2：平台註冊機制

```python
from typing import Protocol

class Platform(Protocol):
    name: str
    forbidden_dirs: list[str]
    allowed_dirs: list[str]

    def detect(self, project_root: Path) -> bool:
        """偵測專案是否屬於此平台"""
        ...

# 動態註冊
register_platform(STM32Platform())
register_platform(ESP32Platform())
```

##### 建議 3：Prompt 版本與平台關聯

```yaml
# prompts/metadata.yaml
fix_build_errors:
  version: "1.0.0"
  platforms:
    stm32_keil:
      min_version: "1.0.0"
      validated: true
    esp32_idf:
      min_version: "1.0.0"
      validated: false  # 尚未充分測試
```

---

#### 開放問題

1. **平台識別的優先順序？**
   - 使用者明確指定 vs 自動偵測
   - 當自動偵測失敗時的 fallback 策略

2. **平台配置的驗證？**
   - 如何確保 `forbidden_dirs` 設定正確？
   - 如何測試平台配置的有效性？

3. **跨平台測試的成本？**
   - 是否需要所有測試都跨平台執行？
   - 還是只測試核心邏輯跨平台，邊緣案例單平台？

4. **Prompt 一致性？**
   - 如何確保不同平台的 prompt 行為一致？
   - 需要建立「Prompt 行為規範」嗎？

---

#### 結論與下一步

**核心發現**：
- `fix_build_errors.md` 的平台綁定是**架構層級的設計債**
- 不解決會嚴重影響 FWAuto 的擴展性和測試完整性
- 方案 B（參數化 Prompt）提供了**立即可行且低風險**的解決路徑

**建議行動**：
1. **優先執行方案 B**（參數化 Prompt）- 1-2 天工作量
2. **更新測試策略**：加入平台參數化測試設計
3. **長期規劃**：當支援 3+ 平台時，考慮升級到方案 A（分層 Prompt）

**對原始研究的影響**：
- **Phase 1 優先順序調整**：「補齊缺失 prompt」改為「建立平台配置系統」
- **測試設計**：所有新測試都應考慮平台參數化
- **成功標準**：需要定義「跨平台成功率」指標

---

**Follow-up 完成時間**: 2025-10-13T20:26:46+08:00
**新增研究範圍**: Prompt 平台適配策略、參數化 Prompt 設計、跨平台測試架構
**主要建議**: 採用方案 B（參數化 Prompt），1-2 天可完成，為後續多平台支援奠定基礎
