Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ jobs:
# 不加參數:dart_test.yaml 讓 live 預設跳過(ADR 0015 §決定 3)。
# 身分測試要 cmake,ubuntu runner 內建。插件執行環境的測試跑真的
# QuickJS:test/support/quickjs.dart 預先載入 flutter_js 內附的 .so,
# 不用先建置桌面版;載入失敗會讓測試失敗,不會跳過。
# 不用先建置桌面版;載入失敗會讓測試失敗,不會跳過。插件契約執行器
# (test/plugins/contract/contract_test.dart,ADR 0015 §決定 6)也在
# 這一步:它以 fixture 重播跑 test/fixtures/plugins/ 的每個測試插件。
- name: Unit and widget tests
run: flutter test

Expand Down
61 changes: 58 additions & 3 deletions .trellis/spec/app/plugins/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ lib/plugins/
plugin_installer.dart # PluginInstaller:解析 → 載入 → 寫入 → 註冊
dev_plugin_entry.dart # dev 的開發入口
types/fmp-plugin.d.ts # 給插件作者的 TypeScript 型別

test/plugins/contract/ # 契約執行器(只在測試裡,理由見 PR 9b 的 research/notes.md)
contract_runner.dart # PluginDirectory、runContract/runCheck/recordContract
checks.dart # checks.json、checkShapes、期望的比對
fixture.dart # fixture 格式、fixtureShapes、遮蔽、重播的網址比對
fixture_adapters.dart # ReplayAdapter、RecordingAdapter(dio 最底層的 adapter)
credential_scan.dart # fixture、log、串流 headers 的憑證檢查
contract_test.dart # 重播的入口(FMP_PLUGIN_DIR)
record_test.dart # 錄製(live 測試是真實連線的入口)
```

## 寫一個插件
Expand Down Expand Up @@ -69,7 +78,48 @@ export async function resolveStream({ sourceId, cid, formats }) {
對應表);`fmp.http.request` 自己丟的錯誤(網域不符、限流重試後仍失敗、傳輸錯誤)直接讓它往上拋。
- 要跨重啟的值(匿名 cookie 等)存 `fmp.storage`;`fmp.credentials.get()` 在 M1 一律是 `null`。
- 沒有 `setTimeout`、`fetch`、`require`,也不能 `import` 其他 module:一個檔案就是全部。
- 用 `fmp-test` 當範本:`app/test/fixtures/plugins/test_plugin/test_plugin.js`。
- 用 `fmp-test` 當範本:`app/test/fixtures/plugins/test_plugin/test_plugin.js`;會發請求的範本是
`app/test/fixtures/plugins/http_test_plugin/`。

## 寫檢查案例與 fixture

插件目錄是契約檢查的單位(格式在 `fmp-plugin.d.ts` 的 `FmpChecks`、`FmpFixture`):

```
<插件目錄>/
<名稱>.js # 安裝檔,只能有一個
checks.json # 每能力最多一條
fixtures/<能力>/001.json … # 該案例依序的請求與回應
```

```json
{
"search": {
"input": { "keyword": "tone", "page": 1 },
"expect": { "minItems": 2, "nonEmpty": ["sourceId", "title"] }
},
"resolveStream": {
"input": {
"sourceId": "a1",
"purpose": "playback",
"formats": [{ "container": "mp4", "codec": "aac" }]
},
"expect": { "error": "Unavailable", "reason": "copyright" }
}
}
```

- 成功的案例盡量用錄的:`FMP_PLUGIN_DIR=<絕對路徑> flutter test --run-skipped --tags live
test/plugins/contract/record_test.dart`(`app/` 內;真實連線,照 ADR 0027 §決定 2 回報)。
那個能力原本的 fixture 整組重寫,但只在結果符合 `expect` 時寫(不符就不動原本的檔案並回報);
需要登入的案例錄不了(M1 沒有憑證,會以 `AuthRequired` 失敗)。
- 錯誤案例(風控、下架)多半錄不到:手寫或把錄到的改掉,在 `meta.edited` 寫理由,錄製就不會蓋掉
那個案例。手寫的 fixture 也要是遮過的樣子(值寫 `***`),掃描不會放過。
- 會變的 query 參數(時間戳、簽名)在 fixture 裡寫 `***` 就不比值;遮蔽名單上的參數錄的時候
已經是 `***`。
- 跑:`FMP_PLUGIN_DIR=<絕對路徑> flutter test test/plugins/contract/contract_test.dart`;沒設
`FMP_PLUGIN_DIR` 就是跑 `app/` 內的測試插件(裸 `flutter test` 已包含)。失敗訊息列出每一條
違反,例如 `search: request #1 (GET …) does not match fixtures/search/001.json (GET …)`。

## 在 App 裡試插件

Expand Down Expand Up @@ -124,10 +174,15 @@ export async function resolveStream({ sourceId, cid, formats }) {
回訊息再拋錯),不在正式程式碼留開關。
- 走 Riverpod 的測試(安裝、清單)用 `ProviderContainer(retry: (_, _) => null, ...)`:App 關了重試,
不關的話失敗的 provider 會一直重試到測試逾時。
- 錯誤 fixture 的契約測試、`checks.json` 在 PR 9b。
- 契約執行器本身(`test/plugins/contract/`):改它的檢查時,在 `contract_runner_test.dart` 以
`copyPlugin`/`edit`(`plugin_copy.dart`)在測試插件的副本上造一個會紅的變異,並留一個改無關處
不紅的案例。`edit` 找不到要換的字串會讓測試失敗(Windows checkout 可能是 CRLF,只換單行)。

## Quality Check

- `test/plugins/` 全綠;新的宿主 API 有隔離案例;`type_definitions_test.dart` 綠。
- `test/plugins/` 全綠(含契約執行器);新的宿主 API 有隔離案例;`type_definitions_test.dart` 綠。
- `lib/plugins/` 以外沒有 import `flutter_js`;沒有網址字面值(`fmp_url_literal`)。
- 改了 `fmp` 形狀或 DTO:`fmp-plugin.d.ts` 一起改,`app/AGENTS.md` § 插件若規則變了也改。
- 加了 DTO 欄位:`test/plugins/contract/checks.dart` 的 `trackFields`/`candidateFields` 一起加
(`checks_test.dart` 比對);`SourcePlugin` 加了能力的方法:`checkShapes['FmpChecks']`、
`PluginCheck.run` 與 d.ts 的 `FmpChecks` 一起加。
3 changes: 2 additions & 1 deletion .trellis/spec/app/testing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
| 單元 | 純邏輯、平台層實作(注入路徑與 callback,在暫存目錄上跑) | `test/platform/app_data_directory_test.dart` |
| widget | 畫面;依賴以建構子或 provider override 注入 | `test/app/fmp_app_test.dart` |
| 建置設定 | 原生身分:能執行就執行(`cmake -P`),不能就解析設定檔並附變異案例 | `test/identity/` |
| 插件契約、整合、golden | M1 PR 9、PR 13 與設計系統元件加入時再寫 | — |
| 插件契約 | 插件目錄的 `checks.json` 以 fixture 重播(寫法見 `.trellis/spec/app/plugins/index.md` § 寫檢查案例與 fixture) | `test/plugins/contract/contract_test.dart` |
| 整合、golden | M1 PR 13 與設計系統元件加入時再寫 | — |

- 碰資料庫的測試用 `test/support/memory_database.dart`,寫法見
`.trellis/spec/app/data/index.md` § 測試。
Expand Down
4 changes: 2 additions & 2 deletions .trellis/tasks/09-28-m1-skeleton-tracer/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ app/
i18n/ # slang:zh-TW(base)、zh-CN、en
packages/
fmp_lints/ # analysis_server_plugin;analyzer_testing 測試
plugin_contract/ # 契約執行器(重播 fixture 跑 checks.json)
test/
flutter_test_config.dart # HttpOverrides.global 擋真實 HttpClient
fixtures/plugins/test_plugin/ # 合成資料、播放本機音檔
plugins/contract/ # 契約執行器(重播 fixture 跑 checks.json;9b 改放這裡,ADR 0015 更正)
integration_test/
```

Expand Down Expand Up @@ -135,7 +135,7 @@ M1 的 drift 表:
- `checks.json`。
- 插件以舊專案 `lib/data/sources/bilibili*` 為規格,用 JS 重寫(ADR 0008 檔頭補充)。M1 只需要 `search` 與 `resolveStream` 兩個能力。
- fixture 以真實連線錄製一次(ADR 0027 §決定 2),寫檔前經遮蔽函式;提交前人工確認沒有 cookie 或 token 原值。
- fmp-plugins 的變更以該 repo 自己的 PR 合併,本機跑 `app/packages/plugin_contract` 的執行器驗證。
- fmp-plugins 的變更以該 repo 自己的 PR 合併,本機以 `FMP_PLUGIN_DIR=<插件目錄> flutter test test/plugins/contract/contract_test.dart`(在 `app/` 內)驗證。

## 5. CI(PR 2 起逐步擴充)

Expand Down
23 changes: 16 additions & 7 deletions .trellis/tasks/09-28-m1-skeleton-tracer/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@

## 進度與交接(2026-09-30 更新;compact 後從這裡接)

- **已合併進 `main`**:#173(設計文件)、#174(PR 1 指令檔分家)、#175(PR 2 骨架與 CI)、#177(PR 3 lint)、#178(PR 4 平台層)、#179(PR 5 drift)、#180(PR 6 log 與設定)、#181(PR 7 錯誤模型)、#182(PR 8 網路層)。#176 是 CI 路徑探測,已關閉。
- **已合併進 `main`**:#173(設計文件)、#174(PR 1 指令檔分家)、#175(PR 2 骨架與 CI)、#177(PR 3 lint)、#178(PR 4 平台層)、#179(PR 5 drift)、#180(PR 6 log 與設定)、#181(PR 7 錯誤模型)、#182(PR 8 網路層)、#183(PR 9a JS 執行環境)。#176 是 CI 路徑探測,已關閉。
- **isar/sqlite3 共存探針**:已完成,兩平台共存、全部 16KB 對齊(`research/isar-sqlite3-coexistence.md`,ADR 0010 已補)。
- **PR 9a 完成**(分支 `feat/js-runtime`,子任務已 archive 到 `.trellis/tasks/archive/2026-09/09-30-js-runtime/`):每插件一個背景 isolate 的 QuickJS、宿主 API v1、manifest、從檔案安裝與 dev 開發入口、測試插件 `fmp-test`;數字在該子任務 `research/notes.md` §4。
- **PR 9a 完成**(#183,子任務已 archive 到 `.trellis/tasks/archive/2026-09/09-30-js-runtime/`):每插件一個背景 isolate 的 QuickJS、宿主 API v1、manifest、從檔案安裝與 dev 開發入口、測試插件 `fmp-test`;數字在該子任務 `research/notes.md` §4。
- 實機:Windows dev 開發入口裝上、重啟後從資料庫載入、prod 不理會旗標;Android 模擬器 dev 的前兩項。模擬器上的 `com.personal.fmp` 是舊版 1.11.0,prod 沒裝上去驗;prod 那一段由單元測試守(`devPluginPath` 對 prod 一律回 `null`,有變異驗證)。
- **下一步**:9b(fixture 錄製重播、`checks.json`、契約執行器)→ 9c(建 `1morr/fmp-plugins`、B 站插件、錄一次 fixture)→ YouTube.js 探針(與 10–13 並行)→ 10 播放核心 → 11 verify-on-device → 12 UI → 13 五平台建置與發版 workflow → 里程碑驗收。
- **擁有者決定**:1–7 都在父任務 `prd.md`「擁有者的決定」。9a 期間新增了兩項:
- **PR 9b 待合併**(分支 `feat/plugin-contract`,子任務已 archive 到 `.trellis/tasks/archive/2026-09/09-30-plugin-contract/`):fixture 錄製重播、`checks.json`、契約執行器;擁有者決定 8 讓執行器可在命令列做免登入的錄製(`live` tag),ADR 0015 §決定 7 已補更正。
- **之後**:9c(建 `1morr/fmp-plugins`、B 站插件、錄一次 fixture)→ YouTube.js 探針(與 10–13 並行)→ 10 播放核心 → 11 verify-on-device → 12 UI → 13 五平台建置與發版 workflow → 里程碑驗收。
- **擁有者決定**:1–8 都在父任務 `prd.md`「擁有者的決定」。9a、9b 期間新增了三項:
- 決定 6:插件安裝檔是單一 `.js`,開頭帶 `==FMP Plugin==` manifest;
- 決定 7:插件在背景 isolate 執行;逾時先送存活探測,沒回應才停用到重啟。
- 決定 7:插件在背景 isolate 執行;逾時先送存活探測,沒回應才停用到重啟;
- 決定 8(9b):M1 的 fixture 由契約執行器在命令列錄製,限免登入案例。
- **每個 PR 的固定流程**:
1. 從最新 `main` 開分支;
2. `task.py create … --parent .trellis/tasks/09-28-m1-skeleton-tracer --package app --no-start`;
Expand Down Expand Up @@ -150,7 +152,7 @@
### 9. JS 執行環境與插件(拆成 9a、9b、9c 三個 PR)

- **9a** JS 執行環境、宿主 API v1、manifest 解析(安裝檔格式見 prd 擁有者決定 6)、`SourcePlugin` 轉接、從檔案安裝、測試插件;`flutter_js` 實測(含能否在 `flutter test` 內載入 QuickJS)。
- **9b** fixture 錄製與重播 adapter、`checks.json`、`packages/plugin_contract` 契約執行器、CI 步驟。
- **9b** fixture 錄製與重播 adapter、`checks.json`、契約執行器(放在 `app/test/plugins/contract/`)、CI 步驟。
- **9c** 在 `1morr/fmp-plugins` 建 repo 與 B 站插件(`search`、`resolveStream`),錄一次 fixture;FMP 端無程式碼,或只有文件。

原清單:
Expand All @@ -161,7 +163,7 @@
- `apiVersion` 相容檢查;
- 從檔案安裝。
- [x] TypeScript 型別定義(9a)。
- [ ] `packages/plugin_contract/`:契約執行器、fixture 格式、`checks.json`。
- [x] 契約執行器、fixture 格式、`checks.json`(9b,`app/test/plugins/contract/`)。
- [x] `test/fixtures/plugins/test_plugin/`:合成資料、本機音檔(9a)。
- [ ] 接真實 B 站時觀察:伺服器回不合法的 `Set-Cookie` 是否讓請求變成 `UnexpectedError`(`dio_cookie_manager` 的 `ignoreInvalidCookies` 預設 false;PR 8 檢查提出,沒有重現案例前不改)。
- [ ] 建立 `1morr/fmp-plugins`:
Expand All @@ -181,6 +183,13 @@
- [ ] Android 的跨 isolate 成本:模擬器 debug 下每次宿主呼叫多約 7 ms,不經宿主的 `search` 也比純 Dart 對照慢,差額未查明。有實機時用 profile 模式重量(`integration_test/plugin_runtime_benchmark_test.dart`)。
- [ ] Android 上 prod 的開發入口實機驗證:需要一台沒裝舊版的模擬器或實機(PR 13 或 M9 前)。

9b 留下的後續:

- [ ] 錄製與重播的 adapter 現在在 `app/test/plugins/contract/`;M3 的 App 內開發工具要用時移進 `lib/core/network/`,格式不變。
- [ ] 執行器看不到插件自己吞掉的「網域不在清單」錯誤(網路層照樣不送出),也分不出插件自拋的 `ParseError` 與 DTO 驗證失敗;要看到前者得讓網路層替被拒的請求寫紀錄。
- [ ] 串流候選的網址本身(query 裡的 `access_key` 之類)不在憑證檢查內,現在只查 headers。9c 接 B 站時看要不要加。
- [ ] 9b 審查時有一次 `flutter test` 兩個契約測試檔卡在 loading(編譯階段,20 分鐘無進度),之後 14 次沒重現。CI 若出現同樣的卡住,從 flutter_tools 層查。

### 探針:YouTube.js(擁有者決定 3)

- [ ] 9 合併後開一個子任務,時限約 2–3 個 session。
Expand Down
1 change: 1 addition & 0 deletions .trellis/tasks/09-28-m1-skeleton-tracer/prd.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@

6. **插件安裝檔格式**(2026-09-30):單一 `.js` 檔,開頭以 `/* ==FMP Plugin== … ==/FMP Plugin== */` 包一段 JSON manifest,App 不執行腳本就能讀出 manifest 並先檢查網域與能力(沿用使用者腳本 metadata block 的慣例;MusicFree、LX Music 也是一檔一插件)。插件庫可把 manifest 另存、由腳本合併成安裝檔;圖示只能用網址或內嵌 base64。
7. **插件在背景 isolate 執行**(2026-09-30):`flutter_js` 0.8.7 的 QuickJS 沒有中斷機制,同步無窮迴圈會凍住 UI isolate(PR 9a 實測)。每個插件的 JS 執行環境放在自己的背景 isolate;呼叫逾時先送存活探測:背景 isolate 有回應就只讓這次呼叫以 `NetworkError` 失敗(只是在等網路),沒有回應或 isolate 已結束才把插件標成「沒有回應」並停用到 App 重啟(2026-09-30 擁有者確認);運算重的腳本(YouTube 解簽名)也不會讓畫面卡頓。不換引擎:可中斷的替代套件(`flutter_qjs_next`、`quickjs_engine`)都是單一作者、低採用的新套件。
8. **M1 的 fixture 錄製**(2026-09-30):契約執行器加錄製模式,打 `live` tag、手動執行、只給免登入的案例,寫檔前經遮蔽;需要登入的錄製仍在 App 內(M3)。理由:App 內的插件開發工具在 M3,9c 要先錄 B 站 fixture,過時也要能重錄;ADR 0015 §決定 4 的冒煙測試本來就在命令列用真實連線。

## 需求

Expand Down
3 changes: 2 additions & 1 deletion .trellis/tasks/09-28-m1-skeleton-tracer/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"09-29-logging-settings",
"09-29-error-model",
"09-29-network-layer",
"09-30-js-runtime"
"09-30-js-runtime",
"09-30-plugin-contract"
],
"parent": "09-26-fmp-rewrite",
"relatedFiles": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{"file": "docs/adr/0015-testing-gates-and-dev-environment.md", "reason": "Fixture format, check cases, contract runner, recording correction"}
{"file": "docs/adr/0014-script-source-plugins.md", "reason": "Plugin model, manifest, host API v1, contract tests"}
{"file": "docs/adr/0011-settings-and-logging.md", "reason": "Redaction applied before fixtures are written"}
{"file": ".trellis/spec/app/plugins/index.md", "reason": "How plugins and the runtime are built (9a)"}
{"file": ".trellis/spec/app/network/index.md", "reason": "SourceHttpClient and createAdapter injection point"}
{"file": ".trellis/spec/app/testing/index.md", "reason": "Test conventions, live tag, zero-network guard"}
{"file": ".trellis/spec/app/lints/index.md", "reason": "Lint rules and layer imports"}
{"file": ".trellis/tasks/archive/2026-09/09-30-js-runtime/research/notes.md", "reason": "flutter_js facts, QuickJS in flutter test"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{"file": "docs/adr/0015-testing-gates-and-dev-environment.md", "reason": "Fixture format, check cases, contract runner, recording correction"}
{"file": "docs/adr/0014-script-source-plugins.md", "reason": "Plugin model, manifest, host API v1, contract tests"}
{"file": "docs/adr/0011-settings-and-logging.md", "reason": "Redaction applied before fixtures are written"}
{"file": ".trellis/spec/app/plugins/index.md", "reason": "How plugins and the runtime are built (9a)"}
{"file": ".trellis/spec/app/network/index.md", "reason": "SourceHttpClient and createAdapter injection point"}
{"file": ".trellis/spec/app/testing/index.md", "reason": "Test conventions, live tag, zero-network guard"}
{"file": ".trellis/spec/app/lints/index.md", "reason": "Lint rules and layer imports"}
{"file": ".trellis/tasks/archive/2026-09/09-30-js-runtime/research/notes.md", "reason": "flutter_js facts, QuickJS in flutter test"}
Loading
Loading