# AI Brain 回歸測試機制實施計劃

## Overview

本計劃實施 AI Brain Node 的回歸測試機制，重點解決以下核心問題：

1. **平台綁定問題**：`fix_build_errors.md` prompt 硬編碼 STM32 目錄結構，違反泛用設計目標
2. **測試覆蓋缺口**：AI Brain Node 目前 0% 測試覆蓋率，缺少單元測試驗證核心邏輯
3. **可維護性**：為後續統計測試框架（Phase 3）奠定基礎

**實施範圍**：Phase 1（平台配置系統）+ Phase 2（單元測試），**不包含** Phase 3（統計測試框架）

**預估工作量**：4-6 天

## Current State Analysis

### 現有測試架構

FWAuto 採用三層測試金字塔：

```
Story Tests (BDD, 55%)
    ↓
Integration Tests (15%, 使用 tmp_path + Makefile replay)
    ↓
Unit Tests (30%, 重度 Mock)
```

**優勢**：
- pytest + fixture 設計清晰
- `tmp_path` 隔離模式成熟 (`tests/integration/conftest.py:11-46`)
- Mock 策略一致（參考 `test_build_makefile.py`）

**缺口**：
- ✅ `build_node`: 7 tests
- ✅ `flash_node`: 7 tests
- ✅ `log_node`: 3 tests (Story-006)
- ❌ **`ai_brain_node`: 0 tests** ← 本計劃目標

### 平台綁定問題

**位置**：`prompts/fix_build_errors.md:23-31`

```markdown
🚫 **絕對不可修改**：`SYSTEM/`、`CORE/`、`HALLIB/`、`HARDWARE/` 目錄
✅ **只能修改**：`USER/` 目錄
```

**問題核心**：
- 這些目錄名稱**只存在於 STM32 + Keil 專案**
- ESP32 使用 `components/`, `main/`
- Nordic 使用 `sdk/`, `src/`
- 無法支援多平台

**影響**：
- 測試只能覆蓋 STM32 場景
- 後續擴展需要重寫 prompt（破壞性變更）
- 違反 FWAuto「泛用韌體開發 agent」設計目標

### AI Brain Node 架構

**核心流程** (`src/fwauto/nodes/ai_brain.py:36-62`):

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

    # 2. 載入 prompt 模板
    prompt = prompt_manager.get_prompt(task_type, dict(state))

    # 3. 配置 SDK 選項
    options = get_ai_options(state)  # 109-133 行

    # 4. 執行 AI query (async generator)
    async for message in query(prompt=prompt, options=options):
        responses.append(str(message))

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

**可測試的純函數**：
- ✅ `analyze_task_type(state)` - 任務類型判斷邏輯
- ✅ `get_ai_options(state)` - SDK 配置建立
- ✅ `update_state_from_result(state, result)` - 狀態更新邏輯

**需要 Mock 的部分**：
- ⚠️ `query()` - Claude Code SDK (async generator)
- ⚠️ `prompt_manager.get_prompt()` - Jinja2 模板渲染

### Key Discoveries

1. **現有 Mock 模式參考** (`tests/test_story_006_error_handling.py:89-94`):
   ```python
   with patch("src.fwauto.nodes.log.query") as mock_query:
       async def mock_response():
           yield "測試分析結果"
       mock_query.return_value = mock_response()
   ```

2. **FirmwareState 結構** (`src/fwauto/state.py:6-71`):
   - 採用 `TypedDict` 定義
   - 所有欄位都有明確型別標註
   - **缺少 `platform` 欄位** ← Phase 1 需新增

3. **PromptManager 設計** (`src/fwauto/prompt_manager.py:31-73`):
   - 使用 Jinja2 `StrictUndefined` 模式（變數缺失會拋錯）
   - `_build_variables()` 合併系統變數 + State 變數
   - **無平台配置注入邏輯** ← Phase 1 需新增

## Desired End State

### Phase 1: 平台配置系統

**目標**：建立參數化 Prompt 機制，解決平台綁定問題

**成功標準**：
- [x] `FirmwareState` 包含 `platform` 欄位
- [x] `src/fwauto/platforms.py` 定義 `PlatformConfig`
- [x] `PromptManager` 能注入平台配置到模板變數
- [x] `prompts/fix_build_errors.md` 使用 Jinja2 參數化渲染目錄名稱
- [x] 預設平台 `stm32_keil`，保留擴展空間（ESP32、Nordic）

**驗證方式**：
```python
state = {"platform": "stm32_keil", ...}
prompt = prompt_manager.get_prompt("fix_build_errors", state)
assert "SYSTEM/" in prompt  # STM32 特定目錄
assert "USER/" in prompt
```

### Phase 2: AI Brain Node 單元測試

**目標**：建立測試基礎設施，達到 80%+ 測試覆蓋率

**成功標準**：
- [x] `tests/nodes/test_ai_brain.py` 建立
- [x] `analyze_task_type()` 測試覆蓋所有分支（5 種任務類型）
- [x] `get_ai_options()` 測試 SDK 配置正確性
- [x] `update_state_from_result()` 測試狀態更新邏輯
- [x] `ai_brain_node()` Mock SDK 測試成功/失敗路徑
- [x] 測試覆蓋率 ≥ 80%（使用 `pytest-cov` 驗證）

**驗證方式**：
```bash
make test-nodes
pytest tests/nodes/test_ai_brain.py -v --cov=src/fwauto/nodes/ai_brain --cov-report=term
```

## What We're NOT Doing

明確排除以下範圍（避免 scope creep）：

1. ❌ **統計測試框架**（Phase 3）
   - 不實作重複執行機制
   - 不建立成功率計算工具
   - 不設計 A/B 測試框架

2. ❌ **真實 AI 行為測試**
   - 不呼叫實際 Claude Code SDK
   - 不測試 prompt 實際效果
   - 不驗證 AI 修正能力

3. ❌ **多平台完整支援**
   - 只實作 `stm32_keil` 平台配置
   - 不實作 ESP32/Nordic 完整配置（保留擴展接口）

4. ❌ **PromptManager 單元測試**
   - 僅透過 `ai_brain_node` 間接測試
   - 不獨立測試 Jinja2 渲染邏輯

5. ❌ **缺失 Prompt 模板補全**
   - 不實作 `implement_feature.md`
   - 不實作 `fix_bug.md`
   - 不實作 `natural_language.md`
   - 不實作 `general_codegen.md`

## Implementation Approach

### 高層策略

1. **先建立基礎設施，再撰寫測試**
   - Phase 1 優先（平台配置是測試的前提）
   - Phase 2 依賴 Phase 1 的平台配置機制

2. **參考現有測試模式**
   - Mock 策略借鑑 `test_build_makefile.py`
   - Async generator mock 借鑑 `test_story_006_error_handling.py`
   - Fixture 設計借鑑 `tests/integration/conftest.py`

3. **保持 KISS 原則**
   - 最小化變更（不重構現有架構）
   - 使用成熟的測試工具（pytest, unittest.mock）
   - 避免過度抽象

4. **為 Phase 3 預留擴展空間**
   - 平台配置設計支援參數化測試
   - 測試 fixture 設計考慮重複執行場景

---

## Phase 1: 平台配置系統建立

### Overview

建立參數化 Prompt 機制，使 `fix_build_errors.md` 能根據平台動態渲染目錄限制。

**目標**：
- 解決 prompt 平台綁定問題
- 為多平台支援奠定基礎
- 不破壞現有功能（預設 `stm32_keil`）

**預估時間**：1-2 天

---

### Changes Required

#### 1. 新增 `platform` 欄位到 `FirmwareState`

**File**: `src/fwauto/state.py`
**Changes**: 在 `FirmwareState` TypedDict 中新增平台識別欄位

**修改位置**: 第 71 行之後（`firmware_path` 欄位後）

```python
class FirmwareState(TypedDict):
    # ... 現有欄位 ...

    firmware_path: Annotated[
        str | None,
        "Generated firmware hex file path (populated by build_node on success)",
    ]

    # 新增平台欄位
    platform: Annotated[
        str,
        "Platform identifier (e.g., 'stm32_keil', 'esp32_idf', 'nordic_nrf')",
    ]
```

**重要提醒**：
- 此欄位會影響所有使用 `FirmwareState` 的節點
- 需要在初始化 state 時提供預設值 `"stm32_keil"`
- 後續 Phase 3 可透過此欄位實現平台參數化測試

---

#### 2. 建立平台配置模組

**File**: `src/fwauto/platforms.py` (新建檔案)
**Changes**: 定義平台配置結構與查詢接口

```python
"""Platform configuration for multi-platform firmware development."""

from typing import TypedDict


class PlatformConfig(TypedDict):
    """Configuration for a specific firmware development platform."""

    name: str  # 顯示名稱，例如 "STM32 + Keil"
    forbidden_dirs: list[str]  # 禁止修改的目錄
    allowed_dirs: list[str]  # 允許修改的目錄
    stop_conditions: list[str]  # AI 停止修復的條件描述


# 平台配置字典
PLATFORMS: dict[str, PlatformConfig] = {
    "stm32_keil": {
        "name": "STM32 + Keil",
        "forbidden_dirs": ["SYSTEM", "CORE", "HALLIB", "HARDWARE"],
        "allowed_dirs": ["USER"],
        "stop_conditions": [
            "錯誤來源於 SYSTEM/、CORE/、HALLIB/、HARDWARE/ 目錄",
            "缺少標頭檔案（如 *.h not found）",
            "需要下載或安裝任何依賴",
            "需要修改系統配置或編譯器設定",
            "涉及 HAL 庫、BSP 檔案或 SDK 檔案的問題",
        ],
    },
    # 保留擴展空間（Phase 3 可加入）
    # "esp32_idf": {...},
    # "nordic_nrf": {...},
}


def get_platform_config(platform: str) -> PlatformConfig:
    """
    Get platform configuration by platform identifier.

    Args:
        platform: Platform identifier (e.g., 'stm32_keil')

    Returns:
        PlatformConfig dictionary

    Raises:
        ValueError: If platform is not supported
    """
    if platform not in PLATFORMS:
        raise ValueError(
            f"Unsupported platform: {platform}. "
            f"Supported platforms: {', '.join(PLATFORMS.keys())}"
        )
    return PLATFORMS[platform]


def list_platforms() -> list[str]:
    """List all supported platform identifiers."""
    return list(PLATFORMS.keys())
```

**設計考量**：
- 使用 `TypedDict` 保持與 `FirmwareState` 一致
- `PLATFORMS` 字典易於擴展（新增平台只需加一項）
- `get_platform_config()` 提供清晰的錯誤訊息
- 未來可擴展為 Plugin 機制（Phase 3+）

---

#### 3. 修改 `PromptManager` 注入平台配置

**File**: `src/fwauto/prompt_manager.py`
**Changes**: 在 `_build_variables()` 中注入平台配置

**修改位置**: 第 63-73 行

```python
def _build_variables(self, state: dict[str, Any]) -> dict[str, Any]:
    """Build variables dictionary for template rendering."""
    from .platforms import get_platform_config  # 新增 import

    # 取得平台配置（預設 stm32_keil）
    platform = state.get("platform", "stm32_keil")
    try:
        platform_config = get_platform_config(platform)
    except ValueError as e:
        self.logger.warning(f"Platform config error: {e}, using stm32_keil")
        platform_config = get_platform_config("stm32_keil")

    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 變數（直接傳遞）
        **state,
    }
```

**注意事項**：
- `import` 放在函數內避免循環依賴
- 使用 `try-except` 處理不支援的平台（fallback 到 `stm32_keil`）
- `platform.system()` 行改為 `import platform` 模組（避免變數名稱衝突）

**Import 修正**：在檔案頂部修改

```python
import os
import platform as platform_module  # 重命名避免衝突
from datetime import datetime
```

然後在 `_build_variables()` 中使用：

```python
"os_name": platform_module.system(),
```

---

#### 4. 更新 `fix_build_errors.md` 使用參數化

**File**: `prompts/fix_build_errors.md`
**Changes**: 將硬編碼目錄改為 Jinja2 變數渲染

**修改前** (第 1 行):
```markdown
你是 STM32 韌體開發專家，**當前任務：立即自動修復編譯錯誤**
```

**修改後**:
```jinja2
你是 {{platform_config.name}} 韌體開發專家，**當前任務：立即自動修復編譯錯誤**
```

---

**修改前** (第 23-31 行):
```markdown
🚫 **嚴格禁止區域**：
- **絕對不可修改**：`SYSTEM/`、`CORE/`、`HALLIB/`、`HARDWARE/` 目錄下任何檔案

✅ **唯一允許操作**：
- **只能修改**：`USER/` 目錄下的檔案（如 `USER/main.c`）
```

**修改後**:
```jinja2
🚫 **嚴格禁止區域**：
{% for dir in platform_config.forbidden_dirs -%}
- **絕對不可修改**：`{{dir}}/` 目錄下任何檔案
{% endfor %}

✅ **唯一允許操作**：
{% for dir in platform_config.allowed_dirs -%}
- **只能修改**：`{{dir}}/` 目錄下的檔案
{% endfor %}
```

---

**修改前** (第 33-40 行):
```markdown
## ⚠️ 遇到以下情況立即停止修復

如果編譯錯誤涉及以下任一情況，**立即停止並返回錯誤**：
- 錯誤來源於 `SYSTEM/`、`CORE/`、`HALLIB/`、`HARDWARE/` 目錄
- 缺少標頭檔案（如 `*.h` not found）
- 需要下載或安裝任何依賴
```

**修改後**:
```jinja2
## ⚠️ 遇到以下情況立即停止修復

如果編譯錯誤涉及以下任一情況，**立即停止並返回錯誤**：
{% for condition in platform_config.stop_conditions -%}
- {{condition}}
{% endfor %}
```

---

**修改前** (第 50-62 行):
```markdown
## 允許修復的錯誤類型（僅限 USER/ 目錄）

✅ **可以修復**：
- **函數拼寫錯誤**: `delay_ms_xxx` → `delay_ms`
- **語法錯誤**: 缺少分號、括號不匹配等
- **變數未宣告**: 在 `USER/` 檔案中加入宣告
- **邏輯錯誤**: `USER/main.c` 中的程式邏輯問題
```

**修改後**:
```jinja2
## 允許修復的錯誤類型（僅限 {{platform_config.allowed_dirs|join(', ')}} 目錄）

✅ **可以修復**：
- **函數拼寫錯誤**: 例如函數名稱拼寫錯誤
- **語法錯誤**: 缺少分號、括號不匹配等
- **變數未宣告**: 在允許目錄的檔案中加入宣告
- **邏輯錯誤**: 允許目錄中的程式邏輯問題
```

---

### Success Criteria

#### Automated Verification

- [ ] 所有現有測試通過: `make test`
- [ ] 型別檢查通過: `make typecheck`（如果有）
- [ ] Linting 通過: `make lint`（如果有）
- [ ] `platforms.py` 模組可正常 import: `python -c "from src.fwauto.platforms import get_platform_config; print(get_platform_config('stm32_keil'))"`
- [ ] PromptManager 能載入平台配置:
  ```bash
  python -c "
  from src.fwauto.prompt_manager import PromptManager
  pm = PromptManager()
  state = {'platform': 'stm32_keil', 'build_errors': ['test'], 'build_status': 'error'}
  prompt = pm.get_prompt('fix_build_errors', state)
  assert 'STM32 + Keil' in prompt
  assert 'SYSTEM/' in prompt
  assert 'USER/' in prompt
  print('✓ Prompt 參數化驗證通過')
  "
  ```

#### Manual Verification

- [ ] 手動檢查 `prompts/fix_build_errors.md` 語法正確（Jinja2）
- [ ] 使用 `stm32_keil` 平台生成的 prompt 包含正確的目錄名稱
- [ ] 確認預設平台機制生效（state 無 `platform` 時使用 `stm32_keil`）
- [ ] 閱讀生成的 prompt，確認可讀性未降低

**Implementation Note**: 完成 Phase 1 所有自動驗證與手動驗證後，暫停並向用戶確認，然後再進入 Phase 2。

---

## Phase 2: AI Brain Node 單元測試

### Overview

建立 `ai_brain_node` 的單元測試，驗證核心邏輯正確性，達到 80%+ 測試覆蓋率。

**測試策略**：
- 重度 Mock（不呼叫實際 Claude Code SDK）
- 測試純函數邏輯（`analyze_task_type`, `get_ai_options`, `update_state_from_result`）
- Mock async generator 測試整合流程

**預估時間**：3-4 天

---

### Changes Required

#### 1. 建立測試檔案與 Fixture

**File**: `tests/nodes/test_ai_brain.py` (新建檔案)
**Changes**: 建立測試檔案結構與共用 fixture

```python
"""Tests for AI Brain node."""

from unittest.mock import Mock, patch

import pytest

from fwauto.nodes.ai_brain import (
    ai_brain_node,
    analyze_task_type,
    get_ai_options,
    update_state_from_result,
)
from fwauto.state import FirmwareState


@pytest.fixture
def base_state() -> FirmwareState:
    """Create a base state for testing."""
    return {
        "user_prompt": "test ai brain",
        "project_root": "/test/project",
        "project_initialized": True,
        "build_status": "pending",
        "build_errors": [],
        "build_log": "",
        "flash_status": "pending",
        "flash_errors": [],
        "flash_log": "",
        "iteration": 0,
        "max_retries": 3,
        "execution_mode": None,
        "command": None,
        "command_args": {},
        "mode_metadata": None,
        "command_error": None,
        "log_file_path": None,
        "log_analysis_result": None,
        "firmware_path": None,
        "platform": "stm32_keil",  # 新增平台欄位
    }


# 測試函數將在後續步驟中實作...
```

**參考模式**: `tests/nodes/test_build_makefile.py:11-34`

---

#### 2. 測試 `analyze_task_type()` 任務類型判斷

**File**: `tests/nodes/test_ai_brain.py`
**Changes**: 新增測試函數覆蓋所有任務類型分支

```python
class TestAnalyzeTaskType:
    """Tests for analyze_task_type function."""

    def test_detects_fix_build_errors(self, base_state):
        """Test detection of build error fixing task."""
        base_state["build_errors"] = ["error: undefined reference to 'Delay'"]
        base_state["build_status"] = "error"

        result = analyze_task_type(base_state)

        assert result == "fix_build_errors"

    def test_detects_implement_feature(self, base_state):
        """Test detection of feature implementation task."""
        base_state["execution_mode"] = "feat"

        result = analyze_task_type(base_state)

        assert result == "implement_feature"

    def test_detects_fix_bug(self, base_state):
        """Test detection of bug fix task."""
        base_state["execution_mode"] = "fix"

        result = analyze_task_type(base_state)

        assert result == "fix_bug"

    def test_detects_natural_language(self, base_state):
        """Test detection of natural language task."""
        base_state["execution_mode"] = "natural"

        result = analyze_task_type(base_state)

        assert result == "natural_language"

    def test_defaults_to_general_codegen(self, base_state):
        """Test default task type when no specific condition matches."""
        # No special conditions set
        result = analyze_task_type(base_state)

        assert result == "general_codegen"

    def test_prioritizes_build_errors_over_execution_mode(self, base_state):
        """Test that build errors take priority over execution mode."""
        base_state["build_errors"] = ["error"]
        base_state["build_status"] = "error"
        base_state["execution_mode"] = "feat"  # Should be ignored

        result = analyze_task_type(base_state)

        assert result == "fix_build_errors"
```

**覆蓋分支** (對應 `ai_brain.py:83-106`):
- ✅ 編譯錯誤 (L92-94)
- ✅ Feature 模式 (L95-97)
- ✅ Fix 模式 (L98-100)
- ✅ Natural 模式 (L101-103)
- ✅ 預設 (L104-106)
- ✅ 優先順序測試（編譯錯誤優先）

---

#### 3. 測試 `get_ai_options()` SDK 配置

**File**: `tests/nodes/test_ai_brain.py`
**Changes**: 新增測試函數驗證 SDK 配置正確性

```python
class TestGetAIOptions:
    """Tests for get_ai_options function."""

    def test_configures_allowed_tools(self, base_state):
        """Test that allowed tools are correctly configured."""
        options = get_ai_options(base_state)

        assert options.allowed_tools == [
            "Read",
            "Write",
            "Edit",
            "MultiEdit",
            "Glob",
            "Grep",
            "Bash",
        ]

    def test_configures_permission_mode(self, base_state):
        """Test that permission mode bypasses checks."""
        options = get_ai_options(base_state)

        assert options.permission_mode == "bypassPermissions"

    def test_configures_project_root_access(self, base_state):
        """Test that project root is added to accessible directories."""
        base_state["project_root"] = "/test/stm32_project"

        options = get_ai_options(base_state)

        assert options.add_dirs == ["/test/stm32_project"]
        assert options.cwd == "/test/stm32_project"

    def test_handles_missing_project_root(self, base_state):
        """Test behavior when project_root is not set."""
        base_state["project_root"] = None

        options = get_ai_options(base_state)

        # Should not have add_dirs or cwd when project_root is missing
        assert not hasattr(options, "add_dirs") or options.add_dirs is None
        assert not hasattr(options, "cwd") or options.cwd is None
```

**覆蓋場景** (對應 `ai_brain.py:109-133`):
- ✅ 工具列表配置 (L121)
- ✅ 權限模式配置 (L122)
- ✅ 專案目錄存取 (L126-129)
- ✅ 無專案目錄的降級處理 (L130-131)

---

#### 4. 測試 `update_state_from_result()` 狀態更新

**File**: `tests/nodes/test_ai_brain.py`
**Changes**: 新增測試函數驗證狀態更新邏輯

```python
class TestUpdateStateFromResult:
    """Tests for update_state_from_result function."""

    def test_updates_state_on_success(self, base_state):
        """Test state update when AI processing succeeds."""
        result = {
            "success": True,
            "responses": ["Fixed the error"],
            "task_type": "fix_build_errors",
        }

        new_state = update_state_from_result(base_state, result)

        assert new_state["build_status"] == "success"
        assert new_state["build_errors"] == []
        assert "fix_build_errors" in new_state["build_log"]

    def test_updates_state_on_failure(self, base_state):
        """Test state update when AI processing fails."""
        result = {"success": False, "error": "Template not found"}

        new_state = update_state_from_result(base_state, result)

        assert new_state["build_status"] == "error"
        assert "Template not found" in new_state["build_errors"]
        assert "failed" in new_state["build_log"].lower()

    def test_increments_iteration_on_failure(self, base_state):
        """Test that iteration counter increments on failure."""
        base_state["iteration"] = 2
        result = {"success": False, "error": "AI failed"}

        new_state = update_state_from_result(base_state, result)

        assert new_state["iteration"] == 3

    def test_preserves_other_state_fields(self, base_state):
        """Test that other state fields are preserved."""
        base_state["user_prompt"] = "test prompt"
        base_state["project_root"] = "/test/project"
        result = {"success": True, "task_type": "fix_build_errors"}

        new_state = update_state_from_result(base_state, result)

        assert new_state["user_prompt"] == "test prompt"
        assert new_state["project_root"] == "/test/project"
```

**覆蓋場景** (對應 `ai_brain.py:136-160`):
- ✅ 成功路徑 (L144-151)
- ✅ 失敗路徑 (L152-160)
- ✅ 迭代計數器遞增 (L159)
- ✅ State immutability（`{**state, ...}` 模式）

---

#### 5. 測試 `ai_brain_node()` 整合流程

**File**: `tests/nodes/test_ai_brain.py`
**Changes**: Mock SDK 測試整合流程

```python
class TestAIBrainNode:
    """Tests for ai_brain_node function."""

    @patch("fwauto.nodes.ai_brain.query")
    @patch("fwauto.nodes.ai_brain.prompt_manager")
    def test_executes_successful_ai_query(self, mock_pm, mock_query, base_state):
        """Test successful AI query execution."""
        # Mock prompt manager
        mock_pm.get_prompt.return_value = "Test prompt"

        # Mock async generator response
        async def mock_response():
            from claude_code_sdk.types import AssistantMessage, TextBlock

            yield AssistantMessage(content=[TextBlock(text="Fixed the error")])

        mock_query.return_value = mock_response()

        # Execute node
        result = ai_brain_node(base_state)

        # Verify prompt was generated
        mock_pm.get_prompt.assert_called_once()

        # Verify SDK was called
        mock_query.assert_called_once()

        # Verify state update
        assert result["build_status"] == "success"
        assert result["build_errors"] == []

    @patch("fwauto.nodes.ai_brain.query")
    @patch("fwauto.nodes.ai_brain.prompt_manager")
    def test_handles_prompt_loading_error(self, mock_pm, mock_query, base_state):
        """Test handling of prompt loading errors."""
        # Mock prompt manager to raise exception
        mock_pm.get_prompt.side_effect = Exception("Template not found")

        # Execute node
        result = ai_brain_node(base_state)

        # Verify error state
        assert result["build_status"] == "error"
        assert "Template not found" in result["build_errors"][0]

        # Verify SDK was not called
        mock_query.assert_not_called()

    @patch("fwauto.nodes.ai_brain.query")
    @patch("fwauto.nodes.ai_brain.prompt_manager")
    def test_handles_sdk_query_error(self, mock_pm, mock_query, base_state):
        """Test handling of SDK query errors."""
        # Mock prompt manager
        mock_pm.get_prompt.return_value = "Test prompt"

        # Mock async generator to raise exception
        async def mock_error_response():
            raise RuntimeError("SDK connection failed")

        mock_query.return_value = mock_error_response()

        # Execute node
        result = ai_brain_node(base_state)

        # Verify error state
        assert result["build_status"] == "error"
        assert "SDK connection failed" in result["build_errors"][0]

    @patch("fwauto.nodes.ai_brain.query")
    @patch("fwauto.nodes.ai_brain.prompt_manager")
    def test_passes_correct_state_to_prompt_manager(self, mock_pm, mock_query, base_state):
        """Test that state is correctly passed to prompt manager."""
        base_state["build_errors"] = ["test error"]
        base_state["build_status"] = "error"

        mock_pm.get_prompt.return_value = "Test prompt"

        async def mock_response():
            from claude_code_sdk.types import AssistantMessage, TextBlock
            yield AssistantMessage(content=[TextBlock(text="Fixed")])

        mock_query.return_value = mock_response()

        # Execute node
        ai_brain_node(base_state)

        # Verify prompt manager received state dict (not FirmwareState TypedDict)
        call_args = mock_pm.get_prompt.call_args
        assert call_args[0][0] == "fix_build_errors"  # task_type
        assert isinstance(call_args[0][1], dict)  # state as dict
        assert call_args[0][1]["build_errors"] == ["test error"]
```

**參考模式**: `tests/test_story_006_error_handling.py:89-100`

**覆蓋場景**:
- ✅ 成功執行完整流程
- ✅ Prompt 載入錯誤處理
- ✅ SDK query 錯誤處理
- ✅ State 正確傳遞給 PromptManager
- ✅ Async generator Mock（AssistantMessage + TextBlock）

---

### Success Criteria

#### Automated Verification

- [ ] 所有新增測試通過: `pytest tests/nodes/test_ai_brain.py -v`
- [ ] 測試覆蓋率 ≥ 80%: `pytest tests/nodes/test_ai_brain.py --cov=src/fwauto/nodes/ai_brain --cov-report=term`
- [ ] 所有現有測試仍然通過: `make test`
- [ ] 型別檢查通過（如有）: `make typecheck`
- [ ] Linting 通過（如有）: `make lint`

#### Manual Verification

- [ ] 檢視測試覆蓋率報告，確認關鍵分支都有測試
- [ ] 執行單一測試確認 Mock 行為正確: `pytest tests/nodes/test_ai_brain.py::TestAIBrainNode::test_executes_successful_ai_query -v -s`
- [ ] 確認測試失敗時有清晰的錯誤訊息
- [ ] Code review：測試程式碼易讀易維護

**Implementation Note**: 完成 Phase 2 所有自動驗證與手動驗證後，向用戶報告測試覆蓋率與測試結果。

---

## Testing Strategy

### Unit Tests Philosophy

**目標**: 快速、穩定、獨立的測試

**原則**:
1. **Pure Function First**: 優先測試純函數（`analyze_task_type`, `get_ai_options`, `update_state_from_result`）
2. **Mock External Dependencies**: Mock 所有外部依賴（SDK, PromptManager）
3. **Test Behavior, Not Implementation**: 測試行為而非實作細節

### Mock Strategy

**Mock 層級**:

```
ai_brain_node
    ↓ Mock prompt_manager.get_prompt()
    ↓ Mock query() (async generator)
    ↓
[純函數直接測試，不 Mock]
    - analyze_task_type()
    - get_ai_options()
    - update_state_from_result()
```

**Async Generator Mock Pattern** (參考 `test_story_006`):

```python
async def mock_response():
    from claude_code_sdk.types import AssistantMessage, TextBlock
    yield AssistantMessage(content=[TextBlock(text="Response")])

mock_query.return_value = mock_response()
```

### Test Coverage Goals

| 函數 | 目標覆蓋率 | 測試數量 |
|------|----------|---------|
| `analyze_task_type` | 100% | 6 tests |
| `get_ai_options` | 100% | 4 tests |
| `update_state_from_result` | 100% | 4 tests |
| `ai_brain_node` | 80%+ | 4 tests |
| **總計** | **80%+** | **18 tests** |

### Manual Testing Steps

Phase 1 完成後手動驗證:

1. **Prompt 參數化驗證**
   ```bash
   cd /home/kent/dev/playground/fwauto
   python3 << 'EOF'
   from src.fwauto.prompt_manager import PromptManager
   pm = PromptManager()
   state = {
       "platform": "stm32_keil",
       "build_errors": ["test error"],
       "build_status": "error",
       "project_root": "/test",
       "iteration": 1,
       "max_retries": 3,
       "build_log": "test log"
   }
   prompt = pm.get_prompt("fix_build_errors", state)
   print("=== Generated Prompt ===")
   print(prompt[:500])  # 前 500 字元
   assert "STM32 + Keil" in prompt
   assert "SYSTEM/" in prompt
   assert "USER/" in prompt
   print("\n✓ Prompt 參數化驗證通過")
   EOF
   ```

2. **預設平台機制驗證**
   ```bash
   python3 << 'EOF'
   from src.fwauto.prompt_manager import PromptManager
   pm = PromptManager()
   state = {
       # 故意不提供 platform 欄位
       "build_errors": ["test"],
       "build_status": "error"
   }
   prompt = pm.get_prompt("fix_build_errors", state)
   assert "STM32 + Keil" in prompt  # 應使用預設平台
   print("✓ 預設平台機制驗證通過")
   EOF
   ```

Phase 2 完成後手動驗證:

3. **測試覆蓋率檢查**
   ```bash
   pytest tests/nodes/test_ai_brain.py \
     --cov=src/fwauto/nodes/ai_brain \
     --cov-report=html \
     --cov-report=term

   # 打開 HTML 報告檢視
   open htmlcov/index.html  # macOS
   # 或 xdg-open htmlcov/index.html  # Linux
   ```

4. **失敗案例測試**
   ```bash
   # 驗證測試能正確捕捉錯誤
   pytest tests/nodes/test_ai_brain.py::TestAIBrainNode::test_handles_prompt_loading_error -v -s
   ```

---

## Performance Considerations

### Phase 1 效能影響

**預期影響**: 幾乎無（< 1ms）

- `get_platform_config()` 是簡單的字典查詢 (O(1))
- Jinja2 渲染增加的迴圈可忽略（< 10 個項目）
- 無額外 I/O 操作

### Phase 2 測試執行時間

**預期時間**: < 5 秒（18 個測試）

- 純函數測試極快（< 0.1s per test）
- Mock async generator 有輕微開銷（< 0.3s per test）
- 無實際 SDK 呼叫，無網路延遲

**優化策略**:
- 使用 `pytest-xdist` 並行執行: `pytest -n auto`
- Fixture 共用，避免重複初始化

---

## Migration Notes

### 向後相容性

**Phase 1 變更對現有程式碼的影響**:

1. **`FirmwareState` 新增 `platform` 欄位**
   - **影響**: 所有建立 State 的地方需提供此欄位
   - **降級策略**: PromptManager 使用 `state.get("platform", "stm32_keil")` 提供預設值
   - **建議**: 在 `src/fwauto/graph.py` 初始化 State 時加入預設值

2. **`prompts/fix_build_errors.md` 語法變更**
   - **影響**: 舊的 prompt 不相容（如果有版本控制）
   - **降級策略**: Git commit 保留變更歷史
   - **建議**: 無需特殊處理，直接覆蓋

### 資料遷移

**不需要資料遷移**，因為：
- State 不持久化（每次執行重新建立）
- 無資料庫或設定檔變更

### Rollback Plan

如果 Phase 1 出現問題：

```bash
# 回復到 Phase 1 之前的狀態
git revert <phase1-commit-hash>

# 或手動回復關鍵檔案
git checkout HEAD~1 -- src/fwauto/state.py
git checkout HEAD~1 -- prompts/fix_build_errors.md
rm src/fwauto/platforms.py
```

如果 Phase 2 出現問題：

```bash
# 刪除測試檔案即可（不影響產品程式碼）
rm tests/nodes/test_ai_brain.py
```

---

## References

- **原始研究文檔**: `.ai/rpi/research/2025-10-13-ai-behavior-regression-testing.md`
- **問題定義文檔**: `.ai/rpi/problems/2025-10-13-ai-behavior-regression-testing.md`
- **相關實作**:
  - AI Brain Node: `src/fwauto/nodes/ai_brain.py:24-161`
  - Prompt Manager: `src/fwauto/prompt_manager.py:14-93`
  - State 定義: `src/fwauto/state.py:6-71`
  - 測試參考: `tests/nodes/test_build_makefile.py`, `tests/test_story_006_error_handling.py`
- **Jinja2 文檔**: https://jinja.palletsprojects.com/
- **pytest 文檔**: https://docs.pytest.org/
- **unittest.mock 文檔**: https://docs.python.org/3/library/unittest.mock.html

---

## Open Questions

### 關於 Phase 1

1. **平台自動偵測機制**
   - 是否需要實作 `detect_platform(project_root)` 函數？
   - 還是讓使用者手動指定平台？
   - **建議**: Phase 1 暫不實作，保持簡單

2. **平台配置驗證**
   - 是否需要驗證 `forbidden_dirs` 和 `allowed_dirs` 不重疊？
   - 是否需要檢查目錄是否真實存在？
   - **建議**: Phase 1 暫不驗證，Phase 3 再考慮

### 關於 Phase 2

3. **測試資料管理**
   - 是否需要建立 `tests/fixtures/` 目錄存放測試資料？
   - 還是所有測試資料內嵌在測試檔案中？
   - **建議**: 內嵌在測試檔案，保持簡單

4. **PromptManager 獨立測試**
   - 是否需要為 `PromptManager` 建立獨立測試檔案？
   - 還是只透過 `ai_brain_node` 測試間接覆蓋？
   - **建議**: Phase 2 暫不獨立測試，後續 Phase 3 再考慮

### 關於未來擴展

5. **Phase 3 統計測試框架**
   - 何時開始規劃 Phase 3？
   - Phase 3 是否獨立成另一個實施計劃？
   - **建議**: 完成 Phase 1+2 後，基於實際經驗重新評估

6. **多平台支援時程**
   - 何時加入 ESP32/Nordic 平台配置？
   - 是否需要真實 ESP32 專案測試？
   - **建議**: Phase 3 規劃時一併考慮

---

## 實施檢查清單

### Phase 1: 平台配置系統

- [x] **Step 1.1**: 修改 `src/fwauto/state.py` 新增 `platform` 欄位
- [x] **Step 1.2**: 建立 `src/fwauto/platforms.py` 並實作 `PlatformConfig`
- [x] **Step 1.3**: 修改 `src/fwauto/prompt_manager.py` 注入平台配置
- [x] **Step 1.4**: 更新 `prompts/fix_build_errors.md` 使用 Jinja2 參數化
- [x] **Step 1.5**: 執行自動驗證（所有 51 測試通過）
- [x] **Step 1.6**: 執行手動驗證（Prompt 渲染、預設平台）
- [x] **Step 1.7**: 提交 Phase 1 變更（Git commit: a688543）

### Phase 2: AI Brain Node 單元測試

- [x] **Step 2.1**: 建立 `tests/nodes/test_ai_brain.py` 與 `base_state` fixture
- [x] **Step 2.2**: 實作 `TestAnalyzeTaskType` 類別（6 tests）
- [x] **Step 2.3**: 實作 `TestGetAIOptions` 類別（4 tests）
- [x] **Step 2.4**: 實作 `TestUpdateStateFromResult` 類別（4 tests）
- [x] **Step 2.5**: 實作 `TestAIBrainNode` 類別（4 tests）
- [x] **Step 2.6**: 執行測試並檢查覆蓋率（目標 ≥ 80%，實際達成 96%）
- [x] **Step 2.7**: 執行自動驗證（所有 69 測試通過）
- [x] **Step 2.8**: 執行手動驗證（覆蓋率報告、失敗案例）
- [x] **Step 2.9**: 提交 Phase 2 變更（Git commit: 114f80c）

### 最終驗證

- [ ] **Final 1**: 完整測試套件執行 `make test`（或 `pytest`）
- [ ] **Final 2**: Code review 檢查程式碼品質
- [ ] **Final 3**: 更新文檔（如有需要）
- [ ] **Final 4**: 向用戶報告完成狀況與測試結果

---

**計劃版本**: 1.0
**最後更新**: 2025-10-13
**預估總工作量**: 4-6 天
**下一步**: 等待用戶確認計劃後，開始執行 Phase 1
