# M1 End-to-End Verification Report ## 測試環境 - macOS: 14.7.6 (BuildVersion 23H626) - 架構: x86_64 - 檔案系統: APFS(case-insensitive,預設) - 日期: 2026-04-11 - 測試方式: 複製 `.app` 到 `/tmp/` 模擬全新機器,清空 `~/Library/Application Support/visiona-local` ## 結論(TL;DR) **M1 未通過。** `.dmg` 本身可正常 mount、`.app` bundle 結構正確、codesign 驗證通過,但**應用程式一啟動就失敗**。發現 **2 個 P0 阻斷 bug**: 1. **資料目錄遷移邏輯在 case-insensitive APFS 上壞掉** — 預設 Mac 根本啟動不了。 2. **Wails 端呼叫 server binary 時用了不存在的 flag `--python`** — 就算繞過 bug 1,server 子行程也會立刻 crash。 Mock 模式無法在全新 Mac 上運作,**瀏覽器無法看到主 UI**,M1 的核心承諾全部跳票。 --- ## 前置清理 啟動前發現系統上 legacy `edge-ai-server` 持續被 launchd 重啟(`~/Library/LaunchAgents/com.innovedus.edge-ai-server.plist`),已執行: ``` launchctl unload ~/Library/LaunchAgents/com.innovedus.edge-ai-server.plist pkill -9 -f edge-ai-server ``` 之後 port 3721 淨空。**注意:這是測試環境前置清理,不是 M1 驗收的一部分。但這提醒:使用者第一次安裝若曾裝過 edge-ai-platform,升級 visiona-local 可能 port 衝突。** --- ## 驗證步驟結果 ### Step 1: .dmg 檔案 ✅ - 路徑:`/Users/jimchen/visionA/local-tool/dist/visiona-local.dmg` - 大小:70 MB - 檔案類型:`zlib compressed data` ### Step 2: Mount ✅ `hdiutil attach` 成功,檢查碼全部通過: - 掛載點:`/Volumes/visionA-local` - 可見 `visiona-local.app` ### Step 3: .app 結構 ✅ - `Contents/Info.plist` ✓ - `Contents/MacOS/visiona-local`(9.8 MB,Mach-O 64-bit x86_64)✓ - `Contents/Resources/bin/visiona-local-server`(30 MB)✓ - `Contents/Resources/data/models.json`、`data/nef/` ✓ - `Contents/Resources/iconfile.icns`、`scripts/` ✓ - `codesign --verify --verbose`:**valid on disk, satisfies Designated Requirement** ✓ ### Step 4: 全新機器模擬 ✅ `.app` 複製到 `/tmp/visiona-local.app`,DMG 已 detach。 ### Step 5: 資料目錄清理 ✅ 確認 `~/Library/Application Support/` 下無任何 `vision*` 殘留。 ### Step 6 + 7: 啟動 → Server 起來 ❌❌ #### 第一次啟動(走正常流程) ``` $ /tmp/visiona-local.app/Contents/MacOS/visiona-local [visiona-local] 遷移 /Users/jimchen/Library/Application Support/visionA-local → /Users/jimchen/Library/Application Support/visiona-local 失敗: rename ...: no such file or directory visiona-local already running ``` **Wails app 直接 exit(0)。** 沒有 server、沒有 UI、什麼都沒有。 **根因分析(P0 Bug #1):** `visiona-local/app.go:572 migrateOldDataDirs` 的邏輯在 case-insensitive APFS 上會自我毀滅。 實際流程: 1. `startup()` 先 `MkdirAll(newDir)` 建立 `visiona-local/`。 2. `migrateOldDataDirs` 迭代舊候選路徑,其中包含 `visionA-local`(大寫 A)。 3. 在 case-insensitive APFS 上,`os.Stat(visionA-local)` **會 hit 剛建立的 `visiona-local`**(同 inode),所以 Stat 成功。 4. 程式碼接著檢查 newDir 是否存在(成功)、是否為空(剛建出來的,當然是空)→ 執行 `os.Remove(newDir)`。 5. `os.Remove(newDir)` 把那個目錄刪了,但在 case-insensitive FS 上這等於同時刪掉 `visionA-local`(同 inode)。 6. `os.Rename(visionA-local → newDir)` → 來源不存在 → `ENOENT`。 7. 更致命:newDir 已經被刪掉了,後續 `acquireSingleInstance` 嘗試在裡面建 lock 檔,`os.OpenFile` 回 `ENOENT`(父目錄不存在)。 8. `ENOENT` 不是 `IsExist`,函式直接回錯。外層的 handler 把「任何錯誤」都當成「already running」→ `os.Exit(0)`。 我用 Python 實測確認: ```python os.makedirs('~/Library/Application Support/visiona-local') os.path.exists('~/Library/Application Support/visionA-local') # → True ``` **這個 bug 在任何預設設定的 Mac 上都會觸發(APFS 預設 case-insensitive),等於 M1 在大部分 Mac 上根本跑不起來。** #### 第二次啟動(workaround:預先建立非空 dataDir) 為了繼續測後面的步驟,我先 `mkdir visiona-local && touch .keep`。這樣 `migrateOldDataDirs` 的「newDir 已有內容 → 拒絕遷移」分支會 kick in,繞過 bug 1。 結果:Wails app 進程活著(PID 77828),lock file 建好了,寫了 `visiona-local.ipc-port = 3721`。 但 **port 3721 並沒有人 listen**: ``` $ lsof -i :3721 (空) $ curl http://127.0.0.1:3721/api/system/health (連線失敗) ``` 檢查 `~/Library/Application Support/visiona-local/logs/server.stderr.log`: ``` flag provided but not defined: -python Usage of .../visiona-local-server: -python-mode string ... (列出所有合法 flag,其中沒有 -python) ``` **P0 Bug #2:** `visiona-local/app.go:234` 組 server 參數時: ```go args := []string{ "--host", "127.0.0.1", "--port", strconv.Itoa(port), "--data-dir", a.dataDir, "--python-mode", string(pyMode), } if pyBin != "" { args = append(args, "--python", pyBin) // ← server 沒有這個 flag } ``` Server binary 只認 `--python-mode`,**不認 `--python`**。當系統有 python3(我這台 `/usr/local/bin/python3`)時,`ensurePythonRuntime` 會回傳 pyBin,於是 `--python /usr/local/bin/python3` 被加到參數列。Server 一啟動就 `flag provided but not defined: -python` → `os.Exit(2)`。 Wails app 沒有偵測到 server 掛了(沒有 health check wait,或者 check 失敗但沒有 abort),繼續掛在前景。 **組合效應:** 就算使用者手動繞過 bug 1,只要這台 Mac 上有安裝過 python3(絕大多數開發者 / macOS 自帶),就會撞到 bug 2。Mock 模式理論上不該碰 python,但目前的 code path 還是會把 pyBin 傳下去。 ### Step 8: 主 UI 可訪問 ❌ 因為 server 沒起來,`curl http://127.0.0.1:3721/` 得到 `HTTP 000`(連線拒絕)。**瀏覽器看不到任何東西。** ### Step 9: 資料目錄 ⚠️ 在 workaround 下資料目錄內容: ``` .keep(我手動建的) logs/ server.stderr.log ← 記錄 server 啟動失敗 server.stdout.log ← 空 visiona-local.ipc-port ← 內容 "3721"(但 port 根本沒在 listen) visiona-local.lock ← 有 PID ``` `.ipc-port` 檔寫的是**預期 port**,不是**實際 port**,這本身也是潛在問題(但相比前兩個 bug 是次要的)。 ### Step 10: 乾淨退出 ✅ `pkill -9 -f visiona-local` 後 `pgrep -fl visiona-local` 為空,`.app` 複本、資料目錄都清除乾淨。 ### Step 11: make clean + make dmg 從頭跑 ⏭️ 略過 前面已經是阻斷級問題,重新 build 也不會修掉這兩個 bug。略過本步驟,等修好再驗。 --- ## M1 驗收結論 | 核心承諾 | 結果 | |---------|------| | 1. 雙擊 .dmg → mount → .app 存在 | ✅ | | 2. 啟動 → Mock 模式跑起來 | ❌ Wails app 秒退(bug 1)或 server 立刻 crash(bug 2) | | 3. API (`/api/system/health`、`/api/system/info`) 可訪問 | ❌ port 沒有人 listen | | 4. 主 UI 可在瀏覽器看到 | ❌ 同上 | | 5. 乾淨退出 | ✅(但因為根本沒真的「運作」過,退出也沒什麼好清的) | **核心承諾 2 / 3 / 4 全部沒達成。M1 未通過。** --- ## 發現的問題 ### 🔴 P0 阻斷 #### P0-1: `migrateOldDataDirs` 在 case-insensitive APFS 上自我毀滅 - **位置**:`visiona-local/app.go:572-598` + `oldDataDirCandidates` 清單中的 `visionA-local` - **影響**:任何預設 APFS 的 Mac(絕大多數使用者),**第一次啟動必失敗**。 - **修法建議**: - (A) 用 `os.Stat` 比對 inode:如果舊路徑和新路徑指向同一個 inode,代表 case-insensitive FS 且實際上是同一個目錄,直接 `continue`,不要嘗試遷移。 - (B) 或者,在 candidates 裡**移除** `visionA-local` — 反正新版是 `visiona-local`,如果之前用的也是 `visiona-local`(case-insensitive FS 下等效),根本不需要遷移。只有在真正 case-sensitive FS(少數使用者自選)才需要這個 candidate,那就用 FS 類型偵測決定要不要加。 - (C) 額外修正:`startup()` 裡對 `acquireSingleInstance` 的錯誤處理要分辨「真的有別的 instance 在跑」vs「其他錯誤(例如資料目錄不見了)」。現在任何錯誤都印 "already running" 會誤導 debug。 #### P0-2: Wails 傳給 server 的 `--python` flag 不存在 - **位置**:`visiona-local/app.go:234` - **影響**:只要系統上有 python(macOS 幾乎都有),server 子行程啟動即死。Mock 模式也一樣會死。 - **修法建議**: - 改成 `--python-bin`(並在 server 端加對應 flag),或 - 把 pyBin 放在環境變數 `VISIONA_PYTHON_BIN` 傳,或 - 在 Mock 模式下完全不傳 pyBin 相關參數(根本用不到)。 - **額外**:Wails 端應該在 `startServer()` spawn 後做一次 health check(例如 500ms 內 poll `/api/system/health` 幾次),失敗就 `reportFatal`,不要讓 app 繼續假裝有在運作。 ### 🟡 需修(非阻斷) #### M-1: `.ipc-port` 寫的是預期 port 不是實際 port Server 啟動前就寫好 port file,但如果 server 實際沒起來(bug 2 的情況),前端或 IPC 呼叫者會連到一個不存在的 port。應該在 server 真的 `LISTEN` 成功後才寫 ipc-port 檔(或由 server 自己寫)。 #### M-2: 系統級 launchd agent 殘留 `~/Library/LaunchAgents/com.innovedus.edge-ai-server.plist` 會自動重啟 legacy daemon,搶 3721 port。M1 uninstall / migration 文件應提醒使用者:若曾安裝 edge-ai-platform,需要手動 `launchctl unload` + 刪 plist。或在首次啟動時偵測到同 port 衝突時給出提示。 ### 🟢 建議 - **測試環境 matrix**:目前所有開發測試都在同一台有 python、有舊 edge-ai-platform、有 legacy launchd 的 Mac 上跑。這兩個 bug(尤其 bug 1)在「乾淨 Mac」上才暴露。建議納入「新 macOS VM / 沒有 python 的 Mac / case-sensitive APFS 的 Mac」三種情境做 CI-level 驗證。 - `codesign` 目前是 ad-hoc/self-signed,上線前需要 Apple Developer ID 簽章 + notarize,否則 Gatekeeper 會擋。本次測試沒觸發這個議題(因為 `.app` 是本機 build 直接 run),但那是因為本機沒有 quarantine attr。從 `.dmg` 真的雙擊打開時會有不同行為,建議補 Gatekeeper 測試。 - Legacy LaunchAgent 議題(上方 M-2)也要在交付文件 (`07-delivery/launch-checklist.md`) 列成「升級安裝注意事項」。 --- ## 結論:**M1 未通過,需修** 兩個 P0 bug 都必須在 ship 前修掉。建議流程: 1. **Architect Agent** 決定 bug 1 的修法(inode 比對 vs. 移除候選 vs. FS 偵測),以及 bug 2 的 flag 契約(改名 / 環境變數 / mock 模式不傳)。 2. **Backend Agent** 實作修復(`app.go` 兩處改動 + 可能的 server flag 新增)。 3. **Reviewer** 審查。 4. 重新 `make clean && make dmg`,**我再跑一次完整 E2E**。 5. 本次的前置清理步驟(unload launchd)也應寫進 launch-checklist,提醒升級使用者。 --- ## 附錄:本次使用的清理指令 ```bash # 卸下 legacy launchd agent(若有) launchctl unload ~/Library/LaunchAgents/com.innovedus.edge-ai-server.plist pkill -9 -f edge-ai-server pkill -9 -f visiona-local # 清資料目錄(模擬全新使用者) rm -rf "$HOME/Library/Application Support/visiona-local" rm -rf "$HOME/Library/Application Support/visionA-local" # 注意:APFS 上這會和上一行等效 # 卸載 .dmg(若還掛著) hdiutil detach /Volumes/visionA-local ```