FR-092 第 8 棒盤點報告——scripts/ 與 test/ 的死物

第 8 棒:scripts/ 與 test/ 死物盤點

§1

範圍與方法

只盤不修——本棒沒有改動任何程式碼、沒有刪或搬任何檔案、沒有碰 pyproject.toml、沒有連 DB。

項目 內容
盤點對象 test/ 全部 193 支 .py(189 支 test_*.py + 4 支輔助檔);scripts/ 中 2026-08-16 之後新增的檔
不動區 scripts/sql/、scripts/build/(僅查呼叫端,不判去留)、scripts/notion_case.py——照 CM-1236 政策
基準 commit 31160034,branch feature/FR-075
工具 Python 3.11.9、pytest 9.0.3、ast 靜態 import 解析、grep、git log --follow
判定紀律 每條工具命中都開檔核對過動態呼叫(sys.modules 別名、DI wiring、importlib)才列「確定」

結論數字:確定 7 條,疑似 0 條,工具誤判排除 19 條。


§2

一、test/ 部分

1.1 確定條目

# 檔案 測的東西 該東西現況 判定
T-1 test/test_ssp_versioning_service.py(1,178 行、26 個 test) SspVersioningService 第 1 棒刪除清單內(app/oscal/service/ssp_versioning_service.py) 隨第 1 棒刪
T-2 test/test_clone_ssp_objectives_reference_documents.py(10 行、0 個 test) 無——整檔只剩 docstring 說自己被 T-1 取代 T-1 本身要刪,這份轉介也失去對象 隨第 1 棒刪
T-3 test/test_decision_merge.py 第 9 行 merge_by_decisions(活的,兩個 app service 在呼叫) 但它 import 的 ImportDecisions 來自第 1 棒要刪的 domain/oscal/strategy/i_ssp_docx_write_strategy.py 第 1 棒必須改這一行,不可刪整檔
T-4 test/test_oscal_stage_preconditions.py(240 行、14 個 test) app/flow_control/service/oscal_stage_preconditions.py(活的,DI 有 wiring) 測試斷言的是被 FR-038 刻意移除的行為 獨立可刪(或重寫)
T-5 test/decode_salt.py(10 行) 無——是一段硬編碼 bcrypt hex 的解碼小工具 全 codebase 零引用,最後內容異動 2025-07-20 獨立可刪
T-6 test/test_bpmn_generator_basic_flow.py 無——0 bytes 空殼,2026-01-30 建立後未再動 獨立可刪
T-7 test/test_nist_parse.py 無——1 byte(只有一個換行) 空殼,2026-01-01 建立後未再動 獨立可刪

1.2 三條需要展開說明

T-3 是本棒最值得注意的一條,因為它會讓第 1 棒踩坑。 test_decision_merge.py 測的 merge_by_decisions 是活函式,被 ssp_excel_import_app_service.py:646 與 ssp_docx_import_app_service.py:550 呼叫,測試本身有 13 個真實斷言且目前全綠。但它從第 1 棒要刪的檔案 import 了 ImportDecisions 這個 dataclass。第 1 棒若照清單直接刪檔,這支測試會 collect error。 可行的修法:merge_by_decisions 內部用 getattr(decisions, "decisions", None) 取值,也接受純 dict 和 list(decision_merge.py:65-77 的 _decisions_by_control 明文支援三種形態),而測試自己第 175 行就有一個「傳純 dict 也要能吃」的案例。所以把 _decisions() helper 改成回傳純 dict 即可,不需要保留那個 dataclass。這個判斷屬第 1 棒的實作決定,本棒只標出風險。

T-4 是典型的「綠著但沒在守」。 這支測試在模組層被 pytest.skip(allow_module_level=True) 擋掉,理由寫「FR-038 Wave 2A 停用,2B 重建」。我把 skip 拿掉後實跑,14 個 case 全紅,錯誤是 TypeError: check() got an unexpected keyword argument 'ap_uid'——FR-038 已把介面的參數從 ap_uid 改成 round_uid,而且四個被測 class 的 check() 現在一律 return PreconditionResult.ok()(原始碼註解明寫「暫 lenient」)。測試斷言的「未指派時要 fail、要回 unassigned 數量」這些行為,產品端已經不存在了。 被測模組本身是活的(di_containers/flow_control/flow_control_containers.py:21 有 wiring),所以刪的是測試不是模組。要不要改成對 lenient 行為的斷言,屬 FR-038 收口範圍,非本案。

T-1 的 skip 出於同一個 FR-038 2A 理由,但和 T-4 不同:它的被測模組確實在第 1 棒刪除清單上,所以測試跟著走,沒有重寫的問題。

1.3 檢查過但判定「活的」(不刪)

這些是查過之後排除的,記下來免得下一棒重查:

查了什麼 結果
19 支測試 import app.detection_tools.service.*,ast 判定「模組不存在」 工具誤判。app/detection_tools/service/__init__.py 是 FR-069 的 shim,用 pkgutil.walk_packages 把 jedi_detection.app.service 整棵子樹遞迴掛進 sys.modules。實跑 importlib.import_module 七支代表模組全部 OK,指向套件實體檔。靜態解析看不到這種動態別名
4 支「只有 mock 斷言、沒有業務斷言」的檔(test_fr048_workflow_guard_revival / test_fr048_ssp_export_guard / test_fr048_job_comment_guard / test_workflow_execution_service_revert_job_bypass_gate) 活的。粗篩的 awk 條件只數 assert x == y,漏算 pytest.raises。開檔看四支都用 pytest.raises(ForbiddenError) 驗守門真的擋住,是有牙齒的斷言
docstring 提到 v1 / jedi_oscal 的 3 支(test_manual_assignment_merge / test_fr032_excel_device_roundtrip / test_party_reconciliation_v2) 活的。那些字句是在聲明「本檔零 v1 import 所以可以直接接 v2」,是說明不是依賴。實際 import 全指向現存 v2 模組
test/test_stage_advance_service.py 兩個 @pytest.mark.skip 留著。skip 理由已寫明是 FR-038 刻意移除的行為、屬另棒處理,且該檔另外 25 個 case 是綠的。不是「等 X 而 X 早已發生」那種爛掉的 skip
test/test_error_code_mirror_parity.py 的 skipif 留著。條件是「jedi-compliance-audit 未安裝」,屬正常的環境條件守衛
test/fixtures/ 兩個 fixture 檔 都有人用。airasia_cmmc_l1_sample.docx 被 test_ap_report_parser_airasia.py:29 用;airasia_cmmc_l1_ar.xlsx 被 test_fr044_ar_adapter.py 與 test_fr044_ar_import_parse.py 用
test/fixtures_frozen_error_codes.py 活的。被 test_module_boundaries.py:229 用 spec_from_file_location 動態載入
test/cloud_integration/ 21 支 全活。對應的 app/cloud_integration/ 與 infra/cloud_integration/ 都在,無一支 import 到刪除清單
第 2 棒的 17 個 dead file 零測試牽連。用 dotted 路徑與檔案路徑兩種形式在 test/ 全掃,命中 0
第 3 棒的 12 支端點與 subtask_status_history 整模組 零測試牽連。四個 route class 名(ScheduleReportFileListRoute / SspScopedDocxImport / JobExecutionDevice / SubtaskStatusHistory)在 test/ 全無命中
common/code/event_code.py、common/code/local_code.py 刪除後,test_error_code_uniqueness.py 會不會少守 不會。該守衛走訪 common.code 全部子模組收 BaseCode 子類,實跑確認這兩支各貢獻 0 個 class(HIT 的只有 detection_tools_error_code / error_code / flow_control_error_code)。活的那支同名檔是 common/enum/event_code.py,四處在用

1.4 全量測試基準(供刪除後對照)

刪任何檔之前,現況是:

2252 collected, 2243 passed, 3 failed, 8 skipped

三支既有紅燈,與本案無關、不要順手修:

紅燈 原因
test_module_boundaries.py::test_upload_file_endpoint_paths_unchanged ImportError: cannot import name 'FROZEN_URLS' from 'jedi_file_upload.api'
test_error_code_mirror_parity.py::test_mirror_has_same_members 鏡像碼表差一個成員 GRC_REQUEST_BODY_TOO_LARGE
test_license_cross_tenant_duplicate.py::test_same_tenant_historical_license_replaces 測試的 _FakeDomain 缺 replace_current_atomic,套件端已加該方法

八個 skip 之中,四個是「.env 沒有 DB_SECRET 所以跳過 DB 整合測試」,兩個是 T-1/T-4 的模組層 skip,兩個是 test_stage_advance_service 的 case 層 skip。

刪完 T-1/T-2/T-4/T-5/T-6/T-7 之後的預期:collected 2252 → 2212(少掉 T-1 的 26 與 T-4 的 14),skipped 8 → 6,failed 維持 3。


§3

二、scripts/ 部分

2.1 2026-08-16 之後新增的檔

git log --since=2026-08-16 --diff-filter=A --name-status -- scripts/ 列出 73 個新增檔。扣掉不動區(scripts/sql/ 26 支 migration + 1 支 backup、scripts/init/detection-profiles/ 與 framework-pdfs/ 的資料檔),需要查呼叫端的是下列可執行腳本。每一支都查了,全部有活的呼叫端,零支建議搬 archive/。

檔案 呼叫端 判定
scripts/check_env_scoped_migrations.py scripts/check_migration_manifest.sh:87 串接執行 現役
scripts/notion_create_case.py CLAUDE.md 派工鐵則、big-feature-workflow 與 security-scan-lead skill、docs/claude/notion-card-templates.md 現役
scripts/gen_postman_collection.py docs/reference/postman/README.md 明列重產指令,產出物 endpoint-inventory.md 檔頭標「本檔由此腳本產出勿手改」 現役
scripts/deliverables/plugin_metrics.py docs/features/architecture-handbook/package-layers.md:156 定為插件化四指標量尺,FR-080 每棒回寫時重跑 現役
scripts/build/assert_db_current.sh build 管線 現役(不動區)
scripts/build/build_bundle.sh build 管線,scripts/build/README.md 為 canonical 現役(不動區)
scripts/build/build_fe_image.sh 同上 現役(不動區)
scripts/build/build_init_image.sh 同上 現役(不動區)
scripts/build/i18n_probe.py scripts/build/smoke_release.sh:320 直接呼叫(smoke 探針⑦,CM-1419) 現役(不動區)
scripts/init/gen_schema_sql.sh / gen_seed_sql.sh / gen_stamp_sql.sh / init.sh / migrate.sh scripts/init/README.md 為 canonical,installer 與出貨基線流程依賴 現役
scripts/installer/install.sh / guidant / install.conf.example FR-065 落地版 installer 本體 現役

2.2 CM-1236 README 未收錄的兩支

scripts/README.md 是 2026-08-16 的快照,之後新增的檔沒補進去。有兩支值得補:

檔案 情況 建議
scripts/deliverables/plugin_metrics.py 8/16 後新增,README 的 deliverables/ 段落沒提到 補一列。另注意:docs/review/2026-09-11-fr080-arc-review.md:449 記載這支腳本有三個已知缺陷(清單指向已刪的 infra/asset/user_name_resolver.py、docstring 說 15 條實際 24 條、預設 monorepo 路徑指向不存在的目錄),採信它的輸出前要先修。本棒不修,建議首腦另開卡
scripts/deliverables/render_docx.py 早於 8/16 就存在,但 README 的 deliverables/ 段落只列了 render_html / render_doc / render_index,漏了它 補一列。它是活的,test/test_deliverables_renderer.py 整支在測它

scripts/README.md 的分類統計也已過時(寫「現役工具 33 支」,實際根目錄現在是 24 支不變、deliverables/ 從 6 支變 8 支)。本棒不改 README,列出來供首腦決定是否開收尾小卡。

2.3 scripts/archive/ 五支的真實年紀

CM-1236 政策是「只搬不刪,要不要真刪由決策者決定」。git log 直接查會全部顯示 2026-08-16(那是搬移的 commit),用 --follow 追到搬移前的原始異動才是真實年紀:

檔案 搬移前最後一次內容異動 分類 現在還能跑嗎
regenerate_reference_templates.py 2026-05-29(其中 5/24 兩次是實質改動) 一次性已完成 未驗,import 未查
2026-05-04-backfill-framework-version-file-uid.py 2026-07-25(Sonar 批次清理,非功能改動;實質改動停在 2026-05-04) 一次性已完成 未驗
backfill_2026-06-17_inject_job_execution_uid.py 2026-06-17 一次性已完成 未驗
test_compute_engine_integration.py 2026-07-25(Sonar 批次;實質改動停在 2026-02-10) 疑似棄用 不能。見下
test_metadata_builder_integration.py 2026-07-25(同上) 疑似棄用 不能。見下

後兩支現在可以從「疑似棄用」升格為「確定死透」:CM-1236 當時的判定依據是「app/ai_dashboard/service/ 下已無 compute_engine.py」。本棒實查發現情況更徹底——整個 app/ai_dashboard/ 目錄已經不存在(app/ 下現存 25 個子目錄無此項),importlib.import_module('app.ai_dashboard') 直接 ModuleNotFoundError,也沒有任何 shim 把它導向套件。這兩支永遠不可能再跑起來。

建議:其餘三支一次性 backfill 有事後追溯價值,維持 archive;這兩支已無追溯價值(被測模組整個消失),可提請決策者裁真刪。本棒不刪。


§4

三、最值得先處理的三條

  1. T-3(test_decision_merge.py 第 9 行)——唯一會讓第 1 棒直接踩坑的一條。不處理就是 collect error,而且它測的是活功能,不能連檔刪掉。
  2. T-4(test_oscal_stage_preconditions.py)——240 行、14 個 case 假裝在守著六個 precondition 檢查,實際上被測方法全部改成無條件放行了。這種「skip 掛著、看起來有保護」的檔留越久越危險。
  3. archive 兩支 AI 儀表板測試——不影響任何人,但既然查清楚了被測模組整個目錄都不在了,值得把它們從「疑似」結案掉,免得下次盤點的人再查一次。
§5

四、本棒沒做的事

  • 沒有刪或搬任何檔案(含 scripts/archive/ 那兩支確定死透的)
  • 沒有改 scripts/README.md(過時的統計與兩支漏收錄的腳本,列在 §2.2 供裁決)
  • 沒有修 plugin_metrics.py 的三個已知缺陷(arc-review 已記載,建議另開卡)
  • 沒有碰三支既有紅燈
  • 沒有連 DB、沒有動 pyproject.toml