# Test Plan: Spec 3 — AP 套用流程（Flow Template Binding）

> **產出日期**：2026-05-13
> **對應 spec**：[design.md](./design.md) v2.1, [implementation-plan.md](./implementation-plan.md) v1.1
> **Branch**：`feature/project-flow-engine-integrate`
> **HEAD（plan commit）**：`c0e26bd`
> **跨 repo 範圍**：BE (`compliance-manager-be`) + FE (`compliance-manager-fe`) + E2E (`compliance-manager-test`)

---

## 0. 文件定位與分工

本 test-plan **不重複** implementation-plan.md 內已展開的 unit test bodies（M2/M3/M4 each task 都已有 `pytest -v` 範例 + assertion）。本文件補的是：

| 範圍 | 來源 |
|------|------|
| Acceptance criteria 拆解 | design.md §五需求摘要 + §六 in-scope + §十一 error handling |
| Traceability matrix AC ↔ test case | 本文件 §5 |
| 整合測試 / lifecycle / 隔離 case 補強 | 本文件 §3 |
| 效能驗證（N+1 防範） | 本文件 §3.5 |
| 回歸驗證（Spec 2 不被破壞） | 本文件 §3.6 |
| FE E2E BDD scenarios（含 Phase E fixture 重做） | 本文件 §4 |
| Manual smoke checklist（DB SQL + curl 範例） | 本文件 §6 |
| 負向 / RLS / DI missing scenario | 本文件 §3.7 |

implementation-plan.md 內已有的 unit test（M2.1 FlowTemplateEnricher 4 tests、M2.2 batch query 3 tests、M3.1–M3.5 carry-forward 5 service tests、M4.1–M4.4 service + integration tests）以「**引用**」方式列入追溯矩陣，不重貼程式碼。

---

## 1. Acceptance Criteria 清單

從 design.md §五（需求摘要 5 條）+ §六 in-scope（11 條 BE/FE 改動）+ §十一 error handling（6 條情境）拆出可驗證條件。

### 1.1 核心需求（design.md §五）

| AC ID | 條件 | 來源 |
|-------|------|------|
| **AC-01** | 建專案時 user 可選流程範本，AP1 (第一輪) 自動綁該範本 snapshot | design.md §五.1 / §七 |
| **AC-02** | 新增稽核計畫 (新一輪 AP) 時 user 可選流程範本，dropdown default 帶**最新一輪 AP 的 master uid** | design.md §五.2 / §10.2 |
| **AC-03** | AP 建立同時 clone master BPMN 成 per-AP `workflow_template` snapshot row（engine 表） | design.md §五.3 / §八 data flow |
| **AC-04** | 後續修改 / 軟刪 master 不影響進行中 AP 的 snapshot.xml（snapshot 不可變） | design.md §五.4 / §四 |
| **AC-05** | 多 AP 互不影響 — 兩 AP 即使選同一 master，也是各自獨立 snapshot row + 獨立 `workflow_execution` | design.md §五.5 / §四 |

### 1.2 BE in-scope（design.md §六 / §九）

| AC ID | 條件 | 來源 |
|-------|------|------|
| **AC-06** | `POST /oscal/projects/start` request schema 含 `flow_template_uid: UUID (required)`；缺欄位回 400 | design.md §9.5 / §十一 |
| **AC-07** | `POST /grc/project/<uid>/launch-new-round` request schema 同上 | design.md §9.5 / §十一 |
| **AC-08** | `_bind_main_workflow_to_ap` 簽章改為接 `flow_template_uid: str (required)`，拔除 default lookup | design.md §9.3 |
| **AC-09** | `launch_new_round` 補做 main workflow snapshot（呼叫 `_bind_main_workflow_to_ap`）— Spec 2 漏蓋 | design.md §三 / §9.2 |
| **AC-10** | `FlowTemplateDomainService.DEFAULT_MAIN_WORKFLOW_TEMPLATE_NAME` / `get_default_main_workflow_template()` / `IFlowTemplateRepo.get_builtin_by_name()` 全刪 | design.md §9.4 |
| **AC-11** | `GET /grc/project/<uid>/assessment-plans/menu`（列表）回 `flow_template: {snapshot_uid, master_uid, master_name, is_master_active}` | design.md §9.6 |
| **AC-12** | `GET /grc/project/<uid>/ap/<ap_uid>`（詳情）同上 enrichment 欄位 | design.md §9.6 |
| **AC-13** | 列表 enrichment 為 batch（三段 query：ext / engine / master），不出現 N+1 | design.md §9.6 效能段 |
| **AC-14** | Enrichment lenient 行為：master 軟刪 → `is_master_active=false` 不擋 API；ext 缺 → `flow_template=null` 不擋 API | design.md §9.6 |
| **AC-15** | `GRC_FLOW_MAIN_TEMPLATE_NOT_FOUND` (412022) error code 刪除 | design.md §9.7 |
| **AC-16** | `GRC_AR_DATA_MISSING` 編號從 500001 改 412024 | design.md §9.7 |

### 1.3 FE in-scope（design.md §十）

| AC ID | 條件 | 來源 |
|-------|------|------|
| **AC-17** | `ProjectCreateView` 含流程範本 dropdown + 📋 詳細選擇按鈕 + 預覽 Dialog（reuse `FlowTemplatePreviewDialog.vue`） | design.md §10.1 / §10.3 |
| **AC-18** | `ProjectCreateView` 流程範本必填驗證；未選時送出顯示錯誤訊息 | design.md §10.1 vee-validate rule |
| **AC-19** | `ProjectApListView`「新增稽核計畫」Dialog 含 dropdown + 預覽 + 沿用上一輪預設 hint | design.md §10.2 / §10.5 i18n |
| **AC-20** | i18n key `project.flowTemplate.*` zh-tw / en 兩語系皆補齊 | design.md §10.5 |

### 1.4 Error handling（design.md §十一）

| AC ID | 條件 | 來源 |
|-------|------|------|
| **AC-21** | 缺 `flow_template_uid` → 400 (webargs schema) | design.md §十一 |
| **AC-22** | `flow_template_uid` 非 UUID 格式 → 400 | design.md §十一 |
| **AC-23** | UUID 形式正確但 master 不存在 / 軟刪 (`is_active=false`) → 404 `GRC_FLOW_TEMPLATE_NOT_FOUND` | design.md §十一 |
| **AC-24** | Cross-tenant master uid → 404（RLS 自動 filter） | design.md §十一 |
| **AC-25** | Engine snapshot 或 `start_workflow_execution` 失敗 → 整 `@transaction` rollback，不留半成品 AP | design.md §十一 |
| **AC-26** | DI 缺失（`_flow_template_domain_service` 或 `_workflow_template_snapshot_service` 是 None）→ 412 `GRC_FLOW_BINDING_DI_MISSING` | implementation-plan.md M4.4 §Step 3 註解 |

**AC 總數 = 26 條**

---

## 2. 測試金字塔（範圍 & 工具）

| 層 | 工具 | 位置 | 範圍 |
|----|------|------|------|
| BE Unit | pytest + unittest.mock | `compliance-manager-be/test/test_*.py` | Service / domain / repo / enricher / handler / precondition |
| BE Integration（API + DB） | pytest + Flask test client + 真 DB | `compliance-manager-be/test/test_*_api.py` | Route → service → repo → ORM → DB (RLS) lifecycle |
| FE 單元（Vitest） | Vitest | `compliance-manager-fe/src/**/__tests__/*` | Dropdown component / form validation（非本 plan 主軸，列入但不展開） |
| E2E BDD | Cucumber.js + Playwright | `compliance-manager-test/features/regression/project-v2/*` | 完整使用者旅程（建專案 → AP1 lifecycle → 建新一輪 → AP2 lifecycle） |
| Manual Smoke | curl + psql | （本機） | M7 必過 checklist（design.md §12.3） |

---

## 3. BE 測試（pytest）

> 路徑：`compliance-manager-be/test/`
> 帳號：`blsit / Billows@123!`（pytest 用，conftest.py 已設好 `app_client` / `blsit_headers`）
> Response 格式：`{"status": true/false, "data": {...}}`（注意：分頁 meta 在頂層、非 data 內）

### 3.1 Unit Tests（plan 已展開，引用即可）

| Test File | 來源 plan task | Tests 數 | 覆蓋 AC |
|-----------|---------------|---------|---------|
| `test_flow_template_enricher.py` | M2.1 | 4 | AC-11, AC-13, AC-14 |
| `test_flow_template_domain_service.py` (補 batch) | M2.2 | 3 | AC-13 |
| `test_ap_extension_domain_service.py` (補 batch) | M2.2 | 2 | AC-13 |
| `test_workflow_template_service.py` (補 batch) | M2.2 | 1 | AC-13 |
| `test_workflow_template_snapshot_service.py` | M3.1 | 3 | AC-03, AC-05 |
| `test_stage_advance_service.py` | M3.2 | ≥3 | (regression) |
| `test_oscal_stage_preconditions.py` | M3.3 | ≥6 (3 fn × 2) | (regression) |
| `test_oscal_stage_handlers.py` | M3.4 | ≥6 (3 handler × 2) | (regression) |
| `test_assessment_plan_extension_repo_impl.py` | M3.5 | 4 | AC-09 |
| `test_oscal_project_service_flow_template.py` | M4.1, M4.4 | 4 | AC-06, AC-08, AC-09, AC-23 |

**小計：≥36 個 unit test 案例**（plan 已展開 bodies；本 plan 不重貼）

### 3.2 整合測試補強（本 plan 新增）

| Test File | Test Method | 目的 | 覆蓋 AC |
|-----------|-------------|------|---------|
| `test_post_oscal_projects_start_flow_template.py` | `test_post_201_creates_ap1_with_correct_snapshot` | start_oscal_project 帶 valid uid → API 200 + ap_extensions 一列 + snapshot row 一列 + workflow_execution 一列 | AC-01, AC-03, AC-06 |
| 同檔 | `test_post_400_missing_flow_template_uid` | 缺欄位 → webargs schema 擋 400 | AC-06, AC-21 |
| 同檔 | `test_post_400_invalid_uuid_format` | uid 非 UUID 格式 → 400 | AC-22 |
| 同檔 | `test_post_404_template_not_found` | uid 隨機 → 404 `GRC_FLOW_TEMPLATE_NOT_FOUND` | AC-23 |
| 同檔 | `test_post_404_template_soft_deleted` | uid 對應 master `is_active=false` → 404 | AC-23 |
| 同檔 | `test_post_404_cross_tenant_template` | uid 屬於 tenant B，user 在 tenant A → 404（RLS） | AC-24 |
| 同檔 | `test_post_rolls_back_when_engine_fails` | mock `start_workflow_execution` raise → AP / ext / snapshot 都不留 | AC-25 |
| `test_post_launch_new_round_flow_template.py` | `test_launch_new_round_creates_ap2_independent_snapshot` | 同 project 建第二輪 AP → snapshot uid 不同 / workflow_execution uid 不同 | AC-02, AC-05, AC-09 |
| 同檔 | `test_launch_new_round_400_missing_flow_template_uid` | 缺欄位 → 400 | AC-07, AC-21 |
| 同檔 | `test_launch_new_round_404_template_soft_deleted` | 軟刪 master uid → 404 | AC-23 |
| 同檔 | `test_launch_new_round_rolls_back_on_engine_failure` | helper 中段失敗 → AP1 ext 不變、AP2 完全不留 | AC-25 |
| `test_grc_assessment_plan_api_enrichment.py` | `test_menu_returns_flow_template_for_every_ap` | 列表 N 個 AP → N 個 `flow_template` payload，欄位齊全 | AC-11 |
| 同檔 | `test_menu_returns_null_when_ext_missing` | 舊資料 ext 不存在 → `flow_template=null`，API 仍 200 | AC-14 |
| 同檔 | `test_menu_returns_is_master_active_false_when_soft_deleted` | master 軟刪 → 欄位齊全但 `is_master_active=false` | AC-14 |
| 同檔 | `test_detail_returns_flow_template` | 詳情 endpoint 同 enrichment | AC-12 |
| 同檔 | `test_menu_uses_batch_queries_no_n_plus_1` | 5 個 AP → 三段 query 各只觸發 1 次（用 `sqlalchemy.event` listen `before_cursor_execute` 計數，或 mock domain service `assert_called_once`） | AC-13 |
| `test_grc_error_code_uniqueness.py` (擴) | `test_grc_flow_main_template_not_found_removed` | grep `GRC_FLOW_MAIN_TEMPLATE_NOT_FOUND` 在 `grc_error_code.py` 不存在 | AC-15 |
| 同檔 | `test_grc_ar_data_missing_code_is_412024` | `GrcErrorCode.GRC_AR_DATA_MISSING == ("...", "GRC_412024")` | AC-16 |

**小計：18 個新整合測試**

### 3.3 Lifecycle 端到端 pytest（最重要的補強）

> 模擬 manual smoke 的 SQL 驗證自動化版，BE 內全程跑通。

`test_spec3_lifecycle.py`：

| Test Method | 目的 | 覆蓋 AC |
|-------------|------|---------|
| `test_full_lifecycle_two_rounds_two_templates` | 建專案選範本 A → 推 AP1 4 stage 到 closed → launch_new_round 選範本 B → 推 AP2 3 stage 到 closed（B 跳 poam）→ 驗 DB：兩列 ext / 兩列 snapshot row / 兩 workflow_execution 互不引用 | AC-01, AC-02, AC-03, AC-05, AC-09 |
| `test_snapshot_immutable_after_master_xml_change` | 建 AP1 → 直接 SQL `UPDATE compliance.flow_templates SET bpmn_xml = '...modified...' WHERE name='A'` → 重查 `public.workflow_templates` snapshot row → xml 仍是原 build 時內容 | AC-04 |
| `test_snapshot_survives_master_soft_delete` | 建 AP1 → `UPDATE flow_templates SET is_active=false WHERE name='A'` → AP1 推進 stage 仍正常運作；但 `POST /oscal/projects/start` 再選同 uid → 404 | AC-04, AC-23 |
| `test_two_aps_same_master_get_different_snapshots` | 建專案選 A → 再 launch_new_round 又選 A → 兩 ap_extensions 行的 `flow_template_snapshot_uid` 不同 | AC-05 |
| `test_launch_new_round_default_value_path` | 建 AP1 選 A → 呼叫 `GET /grc/project/<uid>/assessment-plans/menu` → response `data[0].flow_template.master_uid == A.uid`（FE default 帶值依賴此） | AC-02, AC-11 |

**小計：5 個 lifecycle 級 integration test**

### 3.4 Tenant 隔離 / RLS（補強）

`test_spec3_rls_isolation.py`：

| Test Method | 目的 | 覆蓋 AC |
|-------------|------|---------|
| `test_cross_tenant_master_returns_404` | Tenant A user 用 tenant B 的 `flow_template_uid` → 404（RLS 自動 filter，與「master 不存在」同碼，不漏資訊） | AC-24 |
| `test_cross_tenant_ap_list_does_not_enrich` | Tenant A user 撈 tenant B project 的 AP list → RLS 直接擋 project 查詢（不會進入 enricher） | AC-24 |

**小計：2 個 RLS test**

### 3.5 效能驗證（N+1 防範）

`test_flow_template_enricher_perf.py`：

| Test Method | 方法 | 覆蓋 AC |
|-------------|------|---------|
| `test_enrich_many_n_aps_three_queries_max` | 用 `sqlalchemy.event.listen('before_cursor_execute')` 計 SQL 次數；建 10 個 AP → enrich 應只觸發 ≤3 個 SELECT（一段 ext / 一段 engine / 一段 master） | AC-13 |
| `test_menu_api_query_count_constant_in_ap_count` | 整 API call 跑 N=1 / N=5 / N=20 → enrichment 段 SQL 數量不隨 N 線性成長 | AC-13 |

> **實作備註**：若 sqlalchemy event hook 太重，退而求其次用 mock `domain_service.get_many_by_uids.assert_called_once()` 驗 batch path（plan M2.1 Step 2 第 4 個 test case 已用此 pattern）。

**小計：2 個效能 test**

### 3.6 回歸驗證（Spec 2 不被破壞）

`test_spec2_regression_under_spec3.py`：

| Test Method | 目的 | 覆蓋 |
|-------------|------|------|
| `test_single_round_full_lifecycle_still_works` | 建專案選範本 A → AP1 推 4 stage 到 closed（不建新一輪）→ Spec 2 既有功能不退化 | Spec 2 regression |
| `test_stage_advance_uses_per_ap_snapshot_not_master` | AP1 推進時，BPMN parsing 從 snapshot.xml 撈 next node（非 master.xml）— 改 master 不影響推進邏輯 | AC-04, Spec 2 regression |
| `test_remove_default_lookup_no_caller_left` | grep 整個 codebase 確認 `get_default_main_workflow_template` / `DEFAULT_MAIN_WORKFLOW_TEMPLATE_NAME` / `get_builtin_by_name` 無 hit | AC-10 |

> Test 3 用 subprocess 跑 `grep -rn` 並 assert 0 hit，當作 lint-style guard。

**小計：3 個 regression test**

### 3.7 負向 / Edge case 補強

`test_spec3_negative_cases.py`：

| Test Method | 目的 | 覆蓋 AC |
|-------------|------|---------|
| `test_di_missing_returns_412` | mock `OscalProjectService` 三個 dep 中任一為 `None`，直接 invoke `_bind_main_workflow_to_ap` → 412 `GRC_FLOW_BINDING_DI_MISSING` | AC-26 |
| `test_master_has_empty_bpmn_xml` | master.bpmn_xml = '' → clone 應 raise（engine `start_workflow_execution` 會炸 BPMN parser），整 transaction rollback | AC-25 |
| `test_concurrent_launch_new_round_race` | 同一 project 兩個 request 同時 launch_new_round（threading 或 pytest-asyncio）→ 至少一個成功、另一個應該有明確錯誤（不能 silent corrupt） | edge case |
| `test_old_round1_ap_with_no_ext_still_appears_in_list` | 模擬升級前的舊資料 — AP1 沒對應 ext row → menu API 仍回該 AP，`flow_template=null` | AC-14 |

**小計：4 個 negative test**

### BE 測試總計

| 類別 | 數量 |
|------|------|
| Unit (plan 已展開引用) | ≥36 |
| Integration 補強 | 18 |
| Lifecycle | 5 |
| RLS | 2 |
| 效能 | 2 |
| 回歸 | 3 |
| Negative / edge | 4 |
| **BE 總計** | **≥70 個 test case** |

---

## 4. FE E2E 測試（Cucumber + Playwright）

> 路徑：`~/Projects/Billows/Audit-Manager/compliance-manager-test/`
> 技術：Cucumber.js + Playwright + Node.js（BDD 三層分離：feature → step → page object）
> 範圍：本 spec 涉及修改 + 新增的 scenarios 一律在 `compliance-manager-test/`，**不在** BE 或 FE 主專案內加 e2e。

### 4.1 既有 fixture 修補（Phase E 4 bugs，對應 plan M8）

> 來源：`docs/issues/pending/2026-05-13-test-fixture-broken-by-spec2-phase-d-and-fe-refactor.md`
> 本 spec 加 dropdown 後，wizard 結構變更，既有 fixture 必須適配。

| Fixture / Method | 檔案 | 修補內容 | 阻擋 scenario |
|------------------|------|---------|---------------|
| `ProjectCreatePage.addFourParticipantsWithRoles` | `pages/project-v2/ProjectCreatePage.js` | 適配「基本資訊」step 新增的流程範本 dropdown — 確認 step 順序、`page.click('[data-test=step-next]')` 數量、locator selector | 01-project-create.feature 大部分 |
| `launchProjectViaUi` | `pages/project-v2/ProjectOverviewPage.js`（或 helper） | M4 補齊 main workflow binding 後，preparing → active 推進按鈕應該不再 disabled；驗 `await expect(button).toBeEnabled()` | 07-project-overview.feature Banner scenarios |
| `launchAuditViaUi` | 同上 | active → auditing 推進；同上邏輯 | 同上 |
| `launchRemediationViaUi` | 同上 | auditing → remediation 推進 | 09-poam.feature 入口 |

### 4.2 Feature 檔案新增 / 修改

#### 4.2.1 新增 `features/regression/project-v2/01-project-create.feature`（加 scenario）

```gherkin
@regression @project-v2 @create @flow-template @P0
Scenario: 建專案時必須選擇流程範本
  Given 我以專案管理者身份登入系統
  And 我前往建立專案頁面
  When 我在 Step0 填寫基本資訊但不選流程範本
  And 我點擊下一步
  Then 應顯示「請選擇流程範本」必填錯誤訊息
  And 頁面應停留在 Step0

@regression @project-v2 @create @flow-template @P0
Scenario: 建專案選擇「完整稽核流程」範本並完成建立
  Given 我以專案管理者身份登入系統
  And 我前往建立專案頁面
  When 我在 Step0 填寫基本資訊並選擇流程範本「完整稽核流程」
  And 我完成 Step1–Step3 流程
  And 我點擊建立專案按鈕
  Then 應出現成功 Toast 且導航至專案總覽頁面
  And 第一輪 AP 的 flow_template.master_name 應為「完整稽核流程」

@regression @project-v2 @create @flow-template @P1
Scenario: 預覽 Dialog 顯示流程範本詳細階段
  Given 我以專案管理者身份登入系統
  And 我前往建立專案頁面
  When 我在 Step0 選擇流程範本「完整稽核流程」
  And 我點擊「📋 詳細選擇」按鈕
  Then 應彈出流程範本預覽 Dialog
  And Dialog 應顯示 BPMN 圖以及階段清單（規劃 / 執行 / 稽核 / 改善 / 結束）
```

**對應 step / page**：

| Step file | 新增 step | Page object method |
|-----------|----------|-------------------|
| `steps/project-v2/project-create.steps.js` | `When 我在 Step0 填寫基本資訊並選擇流程範本 {string}` | `ProjectCreatePage.fillStep0WithTemplate(name)` |
| 同上 | `When 我點擊「📋 詳細選擇」按鈕` | `ProjectCreatePage.openFlowTemplatePreview()` |
| 同上 | `Then Dialog 應顯示 BPMN 圖以及階段清單 ...` | `ProjectCreatePage.assertPreviewDialogContent()` |

#### 4.2.2 新增 / 修改 `features/regression/project-v2/04-project-planning.feature`

> 「稽核計畫頁新增稽核計畫 Dialog」對應的 page object 是 `ProjectApListPage.js`

```gherkin
@regression @project-v2 @planning @flow-template @P0
Scenario: 新增稽核計畫 Dialog 流程範本必填
  Given 我以專案管理者身份登入系統
  And 我前往專案稽核計畫頁
  When 我點擊「新增稽核計畫」
  Then Dialog 應開啟，且流程範本 dropdown 預設帶最新一輪 AP 的範本
  When 我手動清空流程範本選擇並送出
  Then 應顯示「請選擇流程範本」必填錯誤

@regression @project-v2 @planning @flow-template @P0
Scenario: 新增稽核計畫選不同範本建立第二輪 AP
  Given 我以專案管理者身份登入系統
  And 我已建立第一輪 AP 並選擇範本「完整稽核流程」並推進至 closed
  And 我前往專案稽核計畫頁
  When 我點擊「新增稽核計畫」
  And 我改選流程範本「自我評估稽核」
  And 我填寫計畫名稱並送出
  Then 應出現成功 Toast
  And AP 列表應包含兩列，且其 flow_template.master_name 分別為「完整稽核流程」與「自我評估稽核」

@regression @project-v2 @planning @flow-template @P1
Scenario: 新增稽核計畫 dropdown default 帶最新一輪範本
  Given 我以專案管理者身份登入系統
  And 專案已有兩輪 AP，最新一輪使用範本「自我評估稽核」
  When 我點擊「新增稽核計畫」
  Then Dialog dropdown 預設應為「自我評估稽核」
  And 應顯示「預設沿用上一輪稽核計畫的流程範本」提示
```

**對應 step / page**：

| Step file | 新增 step | Page object method |
|-----------|----------|-------------------|
| `steps/project-v2/project-planning.steps.js` | `When 我點擊「新增稽核計畫」` | `ProjectApListPage.openNewRoundDialog()` |
| 同上 | `Then Dialog dropdown 預設應為 {string}` | `ProjectApListPage.assertNewRoundDefaultTemplate(name)` |
| 同上 | `When 我改選流程範本 {string}` | `ProjectApListPage.selectFlowTemplate(name)` |
| 同上 | `Then 應顯示「預設沿用上一輪稽核計畫的流程範本」提示` | `ProjectApListPage.assertInheritedHintVisible()` |

#### 4.2.3 新增 `features/regression/project-v2/10-spec3-full-lifecycle.feature`（端到端 lifecycle）

> 整合 4.2.1 + 4.2.2 + 推進 stage 的最完整旅程；可被視為 spec 3 P0 smoke 自動化版（呼應 design.md §12.3 / plan M7）。

```gherkin
Feature: Spec 3 — AP 套用流程完整 lifecycle
  作為專案管理者，我希望兩個 AP 能各自綁定不同流程範本且互不干擾

  @regression @project-v2 @spec3 @P0 @lifecycle
  Scenario: 建專案 A (範本「完整稽核流程」) → 推 4 stage → 建新一輪 (範本「自我評估稽核」) → 推 3 stage
    Given 我以專案管理者身份登入系統
    # Round 1 — 建專案 + 4 stage 推進
    When 我建立專案並選擇流程範本「完整稽核流程」
    Then 第一輪 AP Banner 應顯示 stage 為「規劃」
    When 我推進 stage「啟動專案」
    Then AP1 狀態應為 active 且 Banner stage 為「執行」
    When 我推進 stage「啟動稽核」
    Then AP1 狀態應為 auditing 且 Banner stage 為「稽核」
    When 我推進 stage「提交稽核」
    Then AP1 狀態應為 remediation 且 Banner stage 為「改善」
    When 我推進 stage「完成改善」
    Then AP1 狀態應為 closed
    # Round 2 — 建新一輪 + 3 stage 推進（跳 poam）
    When 我新增稽核計畫並選擇流程範本「自我評估稽核」
    Then 第二輪 AP Banner 應顯示 stage 為「規劃」
    When 我推進 stage「啟動專案」
    Then AP2 狀態應為 active
    When 我推進 stage「啟動稽核」
    Then AP2 狀態應為 auditing
    When 我推進 stage「提交稽核」
    Then AP2 狀態應為 closed
    # Snapshot 隔離驗證（透過 API 比對）
    Then AP1 與 AP2 的 flow_template.snapshot_uid 應不同
    And AP1 與 AP2 的 flow_template.master_uid 應分別為兩個不同範本
```

**對應新 step**：

| Step file | Step | Page object |
|-----------|------|-------------|
| `steps/project-v2/spec3-lifecycle.steps.js`（新建） | `When 我建立專案並選擇流程範本 {string}` | reuse `ProjectCreatePage.fillStep0WithTemplate` + 推進至 Step4 提交 |
| 同上 | `When 我推進 stage {string}` | `ProjectOverviewPage.advanceStage(buttonLabel)` |
| 同上 | `Then AP1 與 AP2 的 flow_template.snapshot_uid 應不同` | helper 呼叫 BE API `GET /grc/project/<uid>/assessment-plans/menu` 比對 |

### 4.3 Page Object 新增 / 修改清單

| Page Object | 路徑 | 修改 / 新增 method |
|-------------|------|------------------|
| `ProjectCreatePage` | `pages/project-v2/ProjectCreatePage.js` | + `fillStep0WithTemplate(templateName)`；+ `openFlowTemplatePreview()`；+ `assertPreviewDialogContent()`；改 `addFourParticipantsWithRoles` 適配新 step |
| `ProjectApListPage` | `pages/project-v2/ProjectApListPage.js` | + `openNewRoundDialog()`；+ `selectFlowTemplate(name)`；+ `assertNewRoundDefaultTemplate(name)`；+ `assertInheritedHintVisible()` |
| `ProjectOverviewPage` | `pages/project-v2/ProjectOverviewPage.js` | + `advanceStage(buttonLabel)`；驗證 Banner stage code 與 AP status；reuse for stage 推進 scenarios |
| `BasePage`（如需） | `pages/BasePage.js` | 若需共用 BE API helper 撈 ext / snapshot uid，加 `fetchAssessmentPlansMenu(projectUid)` |

### 4.4 E2E 測試總計

| 類別 | 數量 |
|------|------|
| Project create scenarios（新增） | 3 |
| Project planning scenarios（新增） | 3 |
| Spec 3 full lifecycle scenario | 1 |
| Phase E fixture 修補（影響既有 scenarios） | 4 (Bug A/B/C/D) |
| **E2E 新增 scenarios** | **7 scenarios** |

---

## 5. Traceability Matrix（AC ↔ Test Case）

| AC ID | 條件 | BE Unit | BE Integration | E2E | Manual Smoke | 備註 |
|-------|------|---------|---------------|-----|--------------|------|
| **AC-01** | 建專案選範本 → AP1 綁正確 snapshot | M4.4 unit (`test_oscal_project_service_flow_template`) | `test_post_201_creates_ap1_with_correct_snapshot` / `test_full_lifecycle_two_rounds_two_templates` | 建專案選「完整稽核流程」scenario | §6 Step 2 | |
| **AC-02** | 新一輪 AP dropdown default 帶最新範本 | — | `test_launch_new_round_default_value_path` | dropdown default scenario | §6 Step 4 | FE-heavy，BE 只保證 enrichment 對 |
| **AC-03** | per-AP snapshot clone | M3.1 (`test_clone_master_as_snapshot_returns_new_uid`) | `test_post_201_creates_ap1_with_correct_snapshot` | （透過 lifecycle 間接覆蓋） | §6 Step 6 DB 驗 | |
| **AC-04** | 改 master 不影響進行中 AP | — | `test_snapshot_immutable_after_master_xml_change` / `test_snapshot_survives_master_soft_delete` / `test_stage_advance_uses_per_ap_snapshot_not_master` | — | §6 Step 7 | E2E 不重複（pytest 覆蓋足） |
| **AC-05** | 多 AP 互不影響 | M3.1 (`test_clone_two_times_returns_different_uids`) | `test_two_aps_same_master_get_different_snapshots` / `test_launch_new_round_creates_ap2_independent_snapshot` | full lifecycle scenario `... snapshot_uid 應不同` | §6 Step 6 DB 驗 | |
| **AC-06** | start_oscal_project 必填 uid | M4.1 (`test_start_oscal_project_raises_when_flow_template_uid_empty`) | `test_post_400_missing_flow_template_uid` | create scenario「必須選擇流程範本」 | — | |
| **AC-07** | launch_new_round 必填 uid | — | `test_launch_new_round_400_missing_flow_template_uid` | planning scenario「流程範本必填」 | — | |
| **AC-08** | `_bind_main_workflow_to_ap` 簽章重做 | M4.4 unit tests | `test_post_201_creates_ap1_with_correct_snapshot` | — | — | |
| **AC-09** | launch_new_round 補綁 main workflow（Spec 2 漏蓋） | M3.5 (`test_assessment_plan_extension_repo_impl`) | `test_launch_new_round_creates_ap2_independent_snapshot` | full lifecycle scenario | §6 Step 5（AP2 Banner 不再 disabled） | |
| **AC-10** | 拔除 default lookup hardcode | — | `test_remove_default_lookup_no_caller_left`（grep guard） | — | — | |
| **AC-11** | AP 列表 enrichment | M2.1 (`test_enrich_returns_full_payload_when_all_layers_exist`) | `test_menu_returns_flow_template_for_every_ap` | — | §6 Step 6 curl menu | |
| **AC-12** | AP 詳情 enrichment | — | `test_detail_returns_flow_template` | — | §6 Step 6 curl detail | |
| **AC-13** | Batch query 防 N+1 | M2.1 (`test_enrich_uses_batch_queries`) + M2.2 batch tests | `test_menu_uses_batch_queries_no_n_plus_1` / `test_enrich_many_n_aps_three_queries_max` / `test_menu_api_query_count_constant_in_ap_count` | — | — | |
| **AC-14** | Lenient enrichment | M2.1 (`test_enrich_returns_none_when_ext_missing` / `test_enrich_marks_master_inactive_when_soft_deleted`) | `test_menu_returns_null_when_ext_missing` / `test_menu_returns_is_master_active_false_when_soft_deleted` / `test_old_round1_ap_with_no_ext_still_appears_in_list` | — | — | |
| **AC-15** | 412022 刪除 | — | `test_grc_flow_main_template_not_found_removed` | — | grep 確認 | |
| **AC-16** | 500001 → 412024 | — | `test_grc_ar_data_missing_code_is_412024` | — | — | |
| **AC-17** | FE create dropdown + 預覽 | — | — | create scenario「選擇『完整稽核流程』範本」+「預覽 Dialog 顯示」 | §6 Step 2 | |
| **AC-18** | FE create dropdown 必填驗證 | — | — | create scenario「必須選擇流程範本」 | §6 Step 2 | |
| **AC-19** | FE planning dropdown + default | — | — | planning scenario「dropdown default 帶最新一輪」 | §6 Step 4 | |
| **AC-20** | i18n 兩語系 | — | — | ⚠️ E2E 未覆蓋（單 locale 跑） | manual 切英文目視 | `feature-development-workflow.md` 預設不跑 i18n 切換 E2E |
| **AC-21** | 缺欄位 400 | — | `test_post_400_missing_flow_template_uid` / `test_launch_new_round_400_missing_flow_template_uid` | (necessary subset of FE scenarios) | — | |
| **AC-22** | 非 UUID 格式 400 | — | `test_post_400_invalid_uuid_format` | — | — | |
| **AC-23** | master 不存在 / 軟刪 404 | M4.1 invalid uid test | `test_post_404_template_not_found` / `test_post_404_template_soft_deleted` / `test_launch_new_round_404_template_soft_deleted` | — | — | |
| **AC-24** | Cross-tenant 404 | — | `test_post_404_cross_tenant_template` / `test_cross_tenant_master_returns_404` / `test_cross_tenant_ap_list_does_not_enrich` | — | — | RLS 自動，pytest 需建 2 tenant fixture |
| **AC-25** | Engine 失敗 rollback | — | `test_post_rolls_back_when_engine_fails` / `test_launch_new_round_rolls_back_on_engine_failure` / `test_master_has_empty_bpmn_xml` | — | — | mock + 驗 DB |
| **AC-26** | DI missing 412 | — | `test_di_missing_returns_412` | — | — | |

**覆蓋率小結**：26 條 AC，**25 條有自動化測試覆蓋**；AC-20（i18n 兩語系）僅 manual smoke（feature-development-workflow 預設不跑 i18n E2E）。

---

## 6. Manual Smoke Checklist（必過項，呼應 plan M7）

> 用途：BE + FE 都 commit 完後跑一次端到端，當 lifecycle pytest 漏抓的最後一道網。
> 帳號：`blsadmin / Billows@123!`
> 環境：本機 BE port 8000 + FE port 5173 + DB `192.168.50.188:25432 / guidant_ai_dev`

### Step 1 — 起環境

```bash
# BE
cd ~/Projects/Billows/Audit-Manager/compliance-manager-be
pkill -9 -f main_socketio.py
python main_socketio.py > /dev/null 2>&1 &
tail -f log/app.log &  # 另開 terminal 監看

# FE
cd ~/Projects/Billows/Audit-Manager/compliance-manager-fe
npm run dev
```

### Step 2 — 建專案 A（範本「完整稽核流程」）

- [ ] 瀏覽器開 `http://localhost:5173/project/projects/new`
- [ ] Step0 基本資訊填齊 + 選範本「完整稽核流程」
- [ ] 點「📋 詳細選擇」→ 預覽 Dialog 跳出 → BPMN 圖渲染 + 階段清單顯示
- [ ] Step1-3 走完 + 建立專案
- [ ] AP1 Banner 顯示 stage = 規劃

```bash
# 驗 BE response
curl -s -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <tid>" \
  http://localhost:8000/grc/project/<proj_uid>/assessment-plans/menu \
  | jq '.data[0].flow_template'
# Expect: {snapshot_uid, master_uid, master_name: "完整稽核流程", is_master_active: true}
```

### Step 3 — AP1 推 4 stage 到 closed

- [ ] 點 Banner「啟動專案」按鈕 → AP.status preparing → active；Banner stage 規劃 → 執行
- [ ] 點「啟動稽核」→ active → auditing；Banner 執行 → 稽核
- [ ] 點「提交稽核」→ auditing → remediation；Banner 稽核 → 改善
- [ ] 點「完成改善」→ remediation → closed；Banner 改善 → 結束 ✓

### Step 4 — launch_new_round（範本「自我評估稽核」）

- [ ] 開「新增稽核計畫」Dialog
- [ ] **驗 dropdown default = 「完整稽核流程」**（最新一輪 master）
- [ ] 改選「自我評估稽核」（少 poam stage）
- [ ] 提交 → AP2 出現

### Step 5 — AP2 推 3 stage 到 closed

- [ ] 「啟動專案」→ active
- [ ] 「啟動稽核」→ auditing
- [ ] 「提交稽核」→ **直接 closed**（跳 remediation，B 範本無 poam）

### Step 6 — DB 驗（cmmgr 帳號連線）

```sql
-- 連線
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev
-- 密碼 jedi@123!

-- 抓 project / ap id
SELECT p.id AS pid, ap.id AS apid, ap.title
FROM compliance.projects p JOIN oscal.assessment_plans ap ON ap.project_id = p.id
WHERE p.uid = '<project_uid>'
ORDER BY ap.id;
-- 預期：兩 row，ap1 / ap2

-- 兩列 ap_extensions，flow_template_snapshot_uid 不同
SELECT assessment_plan_id, flow_template_snapshot_uid, workflow_execution_uid
FROM compliance.assessment_plan_extensions
WHERE assessment_plan_id IN (<ap1_id>, <ap2_id>);
-- Expect: 2 rows，snapshot_uid 互不相同，workflow_execution_uid 也不同 ✓

-- 兩列 snapshot
SELECT id, uid, name, source_template_uid, provider, version
FROM public.workflow_templates
WHERE uid IN (
  SELECT flow_template_snapshot_uid FROM compliance.assessment_plan_extensions
  WHERE assessment_plan_id IN (<ap1_id>, <ap2_id>)
);
-- Expect: 2 rows，provider='snapshot'，source_template_uid 分別對 master A/B uid

-- 兩個 workflow_executions
SELECT uid, name, template_id, status
FROM public.workflow_executions
WHERE uid IN (
  SELECT workflow_execution_uid FROM compliance.assessment_plan_extensions
  WHERE assessment_plan_id IN (<ap1_id>, <ap2_id>)
);
-- Expect: 2 rows，名稱 AP-<ap1uid>-main / AP-<ap2uid>-main，互不引用
```

### Step 7 — Snapshot 隔離驗證

```sql
-- 模擬 admin 改 master A 的 BPMN
UPDATE compliance.flow_templates
SET bpmn_xml = '<bpmn:definitions xmlns:bpmn="..."><!-- modified --></bpmn:definitions>'
WHERE name = '完整稽核流程';

-- 驗 snapshot row 不變
SELECT xml FROM public.workflow_templates WHERE uid = '<ap1_snapshot_uid>';
-- Expect: 仍是 build 時的原 XML，xml 字串中沒有 'modified'
```

```sql
-- 模擬軟刪 master B
UPDATE compliance.flow_templates SET is_active = false WHERE name = '自我評估稽核';

-- AP2 推進若仍進行中應正常（snapshot 仍在）；但 List enrichment 應顯示 is_master_active=false
```

```bash
# 驗 enrichment lenient
curl -s -H "Authorization: ..." http://localhost:8000/grc/project/<proj_uid>/assessment-plans/menu \
  | jq '.data[] | {ap: .title, ft: .flow_template}'
# Expect: AP2.flow_template.is_master_active == false（其他欄位齊全），API 不擋
```

### Step 8 — 負向 manual

- [ ] FE create 不選 range → 送出 → 顯示「請選擇流程範本」必填錯誤
- [ ] FE planning Dialog 強制清空 dropdown → 送出 → 同上錯誤
- [ ] DevTool 改 POST body 把 `flow_template_uid` 改成 invalid UUID → BE 回 400
- [ ] DevTool 改成不存在的 UUID → BE 回 404，msg 含「流程範本不存在」

### Step 9 — 若任一驗證 fail

按 CLAUDE.md「重大決策完成後自動歸檔思考過程」開 `docs/analysis/2026-05-13-spec3-smoke-failure-root-cause.md`，並到 changelog 補一筆 fix（如根因落在 Spec 2 service → 同時補 M3 5 個 service test gap）。

---

## 7. 執行順序與閘門

```
┌─────────────────────────────────────────────────────────────────────────┐
│  M0 Pre-flight grep verify                                              │
└─────────────────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  M1 → M2 → M3 (BE)                                                      │
│  - M1 error code 整理                                                    │
│  - M2 enrichment + 4 unit tests + 2 integration tests 必綠               │
│  - M3 carry-forward 5 service test gap 必綠（保 M4 安全網）              │
└─────────────────────────────────────────────────────────────────────────┘
                            │  ◀── 閘門 A：M1+M2+M3 pytest 全綠
                            ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  M4 BE Core (簽章 + helper 改造 + 拔 hardcode)                          │
│  - §3.2 18 個 integration test + §3.3 5 個 lifecycle test 必綠           │
│  - §3.6 regression 3 test 必綠（Spec 2 不退化）                          │
└─────────────────────────────────────────────────────────────────────────┘
                            │  ◀── 閘門 B：BE 端 ≥70 test 全綠
                            ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  M5 + M6 FE                                                              │
│  - Vitest 元件測試（如有）+ FE manual smoke 跑得通                       │
└─────────────────────────────────────────────────────────────────────────┘
                            │  ◀── 閘門 C：FE smoke 通主流程
                            ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  M7 Manual Smoke Checklist (§6 Step 1-9)                                │
└─────────────────────────────────────────────────────────────────────────┘
                            │  ◀── 閘門 D：8 個 step 全 ✓ 才能 merge
                            ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  M8 (事後) Phase E fixture 修補 + E2E §4.2 7 個 scenario 全綠            │
└─────────────────────────────────────────────────────────────────────────┘
                            │  ◀── 閘門 E：E2E 7 scenario + 4 fixture bug 修完
                            ▼
                       Spec 3 task arc 收尾
                  (changelog 收口 / conversation history / summary)
```

**閘門政策**：
- 閘門 A 失敗 → 不進 M4（避免改 service 找不到 regression 來源）
- 閘門 B 失敗 → 不進 FE（避免 FE 接到壞 API）
- 閘門 D 失敗 → 不 merge；按 §6 Step 9 開 analysis + 補 test
- 閘門 E 是「事後追補」，不阻 spec 3 main task arc merge，但要在 issue 追蹤

---

## 8. 不在本次測試範圍

明確列出，避免無限擴張：

- ❌ **跨瀏覽器相容**（Chrome only；Firefox/Safari 由現有 regression suite 涵蓋）
- ❌ **i18n 切換測試**（AC-20 只 manual 目視；不寫切英文跑全套 E2E）
- ❌ **壓力測試 / load test**（spec 沒要求；snapshot clone 是 O(1) 不會出問題）
- ❌ **安全測試（XSS / SQLi）**（marshmallow + SQLAlchemy parameterized 自動防護）
- ❌ **FE 元件單元測試完整鋪**（FE 沒有 vitest baseline；只在新增的 dropdown wrapper 加 1 個 vitest happy path 就好）
- ❌ **「中途調整 AP 流程」UI**（design.md §六 out-of-scope，未來 spec）
- ❌ **跨 tenant 範本共享**（design.md §六 out-of-scope；v1 純 tenant 隔離）
- ❌ **舊資料 backfill**（design.md M4 changelog 註：dev DB 既有 round-1 AP 已有效運作，不 backfill）

---

## 9. 風險與待釐清

| 風險 | 影響 | 緩解 |
|------|------|------|
| **`WorkflowTemplateService` 加 `get_many_by_uids` 是 jedi-flow-engine 套件異動** | 套件 API 變動需 path mode dev + 發版前 bump | M2.2 plan 已標：dev 走 path mode，feature 完才 bump（user 確認） |
| **M3 carry-forward 5 service test 若揭露 Spec 2 既存 bug** | M4 改動會踩到 → smoke 過不去 | M3 順序在 M4 前；fail 就先停下處理 |
| **AP list enrichment N+1 在 RLS 下無法用 `sqlalchemy.event` 計準 query 數** | §3.5 效能 test 假陽 | 退到 mock domain service `assert_called_once` pattern（M2.1 已示範） |
| **DB sequence 權限漏給 cm_app**（CLAUDE.md SQL Migration 規範） | 新建 table 寫入 silent fail | 本 spec 無新 table，零風險；但若 M3 補 carry-forward test 揭露 ext table 權限漏 → 補 GRANT |
| **Phase E 4 fixture bug 修不完** | M8 拖延，BDD scenario 跑不起來 | M8 標為「事後追補」不阻 M1-M7 merge；但 issue 追蹤直到 close |
| **FE dropdown loading 時 user 已點下一步** | dropdown 為空時表單可能誤判已填 | M5 加 `:loading="loadingTemplates"`；空 list 時必填驗證仍擋 |
| **AC-25 rollback 測試難以模擬 engine 失敗** | mock engine service raise 即可，但要確認 SQLAlchemy nested transaction 行為 | 已在 `test_post_rolls_back_when_engine_fails` 用 monkeypatch + 事後查 DB 0 row 驗證 |
| **AC-20 i18n 兩語系僅 manual 覆蓋** | 翻譯 typo 不被自動化抓到 | 接受風險；i18n 文字以 PR review 為主防線 |

---

## 10. 完成檢查（DoD）

- [ ] BE pytest 全綠（≥70 個 test case）— 閘門 A + B
- [ ] FE manual smoke 通過（Step 2-5）— 閘門 C
- [ ] §6 Manual Smoke Checklist 9 個 step 全 ✓ — 閘門 D
- [ ] E2E §4.2 7 個 scenario 全綠 + Phase E 4 fixture bug 修完 — 閘門 E（事後）
- [ ] 追溯矩陣 §5：26 條 AC 對應 test case 已 mapping（AC-20 manual 目視可接受）
- [ ] Spec 2 hardcode default 完全清除（grep guard test pass）
- [ ] 至少 3 個 changelog（tweak / feat / feat）
- [ ] 若 jedi-flow-engine 動到 → 套件 bump + Nexus（user 確認後執行）

---

## 11. 文件版本

| 版本 | 日期 | 變更 |
|------|------|------|
| v1 | 2026-05-13 | 初版 — 對應 design.md v2.1 + implementation-plan.md v1.1；26 條 AC；≥70 BE test + 7 E2E scenarios + Phase E 4 fixture bug 修補 |
