Files
arcrun-collector/scan_skipped_test.go
T
Leo 9cd0adab22 G-6.2:讀不了的檔案不再安靜消失——首頁當場說出來
服務 J-1 / S6「我丟進去的檔案,查得到」的考題 G-6.2:
「要嘛查得到,要嘛**當場被告知這種檔案還不支援**,不准安靜地略過。」
本次只做後半句(轉檔本體另有人閘,等 leo 裁「那段 Go 住哪裡」)。

## 真實現況比記載更糟
t16 寫的「PDF 被靜默略過」已不成立(t73 的轉檔層把 pdf/docx/xlsx/csv/pptx
都接上了,實測 2 頁中文 PDF 完整抽出)。**真正還在沉默的是別的東西**:
副檔名不在 allowedExt 的檔案在 scan.go 直接 `return nil`——
不進事件、不進 manifest、不進 status、不進畫面。
使用者丟一份 .doc 進去,從頭到尾一個字都沒有。

實測基線(真檔):丟 .pdf/.md/.doc/.key/.jpg 進資料夾,
`collector scan` 只吐出 pdf 與 md 兩個事件,另外三個檔沒留下任何痕跡。

## 改了什麼
- scan.go:白名單閘不再是死巷。像文件的(.doc/.xls/.ppt/.pages/.key/
  .numbers/.odt/.ods/.odp/.rtf/.epub/.wpd/.msg/.eml)逐檔留名;
  其餘(圖片/影音/程式碼)只計總數——**避免 Obsidian 附件庫炸出幾百行噪音**。
- 兩個新欄位標 `json:"-"`:collector-trigger schema 是
  additionalProperties:false,且 BuildSendablePayload 是淺拷貝
  ⇒ 有 tag 就會漏到雲端被擋。這是給本機使用者看的,不上 wire。
- direct.go → status.json → App 首頁一張卡:講檔名與格式(「舊版報告.doc
  (舊版 Word)」),並告訴他不用重丟、也給替代路(另存成 PDF/.docx)。
- 刻意不進 manifest、不走 CarryForwardActivity:每輪由檔案系統重算,
  不製造 t195 那種「跨輪欄位漏 carry 就靜默歸零」的債。

## 順手修掉一個會讓驗收失效的回歸
同綑的 arcrun-collector 一路自稱 `dev`:退役 fyne 版
(arcrun-tray/build-mac.sh:24,t150/t72)本來就有版本注入,
t194 換 Wails 時沒帶過來,Mac 與 Windows 兩邊都掉。
**幹活的是 collector**——它不報版本,就沒人能判斷修復有沒有到使用者手上。

## 實測
- go test ./... 135 過 0 敗(新增 10 條:scan 5+首頁文案 5)
- check-cis / check-render / check-tray 三閘全過;淺色深色都抓過畫面
- 從 **DMG 裡那支** collector 實跑:Info.plist 0.18.6、
  collector 回報 v0.18.6 (build 20260806-0101)、
  status.json 吐出 skipped_docs=[簡報.key, 舊版報告.doc]、other=1

⚠️ 未出貨:推 bundles repo 在 GitHub,要 leo 開 D20 閘。
DMG 已備妥 dist/Arcrun-v0.18.6.dmg。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 01:04:25 +08:00

142 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// scan_skipped_test.go — G-6.2「不准安靜地略過」(J-1/S62026-08-06)。
//
// 考題:「Given 我丟進去的是 PDF 或 Word / When 我搜它的內容 /
// Then 我一樣找得到——**或當場被告知這種檔案還不支援**,不准安靜地略過。」
//
// 改動前的實測基線(真檔跑過):把 .doc/.key/.jpg 丟進資料夾,`collector scan`
// 吐出的 events 只有 .pdf 與 .md,另外三個檔**沒有在任何輸出裡留下一個字**。
// 本檔把「那三個檔要留下名字」釘住。
import (
"encoding/json"
"strings"
"testing"
)
// 讀不了的文件要逐檔點名;純粹不是文件的只計數;**能處理的檔完全不受影響**(回歸)。
func TestScanReportsSkippedFiles(t *testing.T) {
root := t.TempDir()
// 支援的(必須照常產生事件)
writeFile(t, root, "筆記.md", "# 內容", baseTime)
writeFile(t, root, "報表.csv", "a,b\n1,2", baseTime)
// 像文件、但還讀不了的(必須逐檔點名)
writeFile(t, root, "舊版報告.doc", "\xd0\xcf\x11\xe0binary", baseTime)
writeFile(t, root, "提案/簡報.key", "binary", baseTime)
// 不是文件的(只計數,不點名——避免附件庫炸出幾百行噪音)
writeFile(t, root, "照片.jpg", "\xff\xd8\xff", baseTime)
writeFile(t, root, "封存.zip", "PK", baseTime)
m := newTestManifest()
p := mustScan(t, root, m)
// ① 回歸:支援的格式照舊,一個不多一個不少。
added := eventsOfType(p, "added")
if len(added) != 2 {
t.Fatalf("支援的檔應產生 2 個 added 事件,實得 %d%+v", len(added), added)
}
for _, ev := range added {
if strings.HasSuffix(ev.Path, ".doc") || strings.HasSuffix(ev.Path, ".key") ||
strings.HasSuffix(ev.Path, ".jpg") || strings.HasSuffix(ev.Path, ".zip") {
t.Fatalf("讀不了的檔不該被送進管線(會被當二進位餵給模型):%s", ev.Path)
}
}
// ② 讀不了的文件必須留名——這就是「不准安靜地略過」。
if len(p.Skipped) != 2 {
t.Fatalf("應點名 2 個讀不了的文件,實得 %d:%+v", len(p.Skipped), p.Skipped)
}
got := map[string]string{}
for _, s := range p.Skipped {
got[s.Path] = s.Ext
}
if got["舊版報告.doc"] != ".doc" {
t.Errorf("舊版報告.doc 沒被點名:%+v", got)
}
if got["提案/簡報.key"] != ".key" {
t.Errorf("子目錄裡的 .key 沒被點名(相對路徑要保留):%+v", got)
}
// ③ 非文件檔只給總數。
if p.SkippedOther != 2 {
t.Errorf("非文件檔應計數 2jpg+zip),實得 %d", p.SkippedOther)
}
}
// manifest 不該因為「有讀不了的檔」而被污染——它們沒進管線,就不該有帳本條目。
func TestSkippedFilesStayOutOfManifest(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "好的.md", "x", baseTime)
writeFile(t, root, "壞的.doc", "x", baseTime)
m := newTestManifest()
mustScan(t, root, m)
if _, ok := m.Entries["壞的.doc"]; ok {
t.Error("讀不了的檔不該進 manifest(會被誤當成已收錄)")
}
if _, ok := m.Entries["好的.md"]; !ok {
t.Error("支援的檔應照常進 manifest")
}
}
// 🔴 schema 保護:collector-trigger.v1.schema.json 頂層是 additionalProperties:false
// 多帶一個欄位上線就會被雲端擋掉。Skipped 是**給本機使用者看的**,不准漏到 wire 上。
// BuildSendablePayload 是 `sendable := *p` 淺拷貝 ⇒ 有 json tag 就會一起送出去。)
func TestSkippedNeverGoesOnTheWire(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "壞的.doc", "x", baseTime)
writeFile(t, root, "照片.jpg", "x", baseTime)
p := mustScan(t, root, newTestManifest())
if len(p.Skipped) == 0 {
t.Fatal("前提不成立:這輪應該要有被略過的檔")
}
blob, err := json.Marshal(p)
if err != nil {
t.Fatal(err)
}
for _, forbidden := range []string{"skipped", "壞的.doc"} {
if strings.Contains(string(blob), forbidden) {
t.Errorf("送雲端的 payload 不該出現 %q%s", forbidden, blob)
}
}
}
// 隱藏檔/被排除的目錄不該被點名——那些是系統與產品自己的東西,
// 使用者從來沒把它們當成「我丟進去的檔案」。報了只會變噪音。
func TestSkippedIgnoresHiddenAndExcluded(t *testing.T) {
root := t.TempDir()
writeFile(t, root, ".DS_Store", "x", baseTime)
writeFile(t, root, ".hidden.doc", "x", baseTime)
writeFile(t, root, ".obsidian/workspace.key", "x", baseTime)
writeFile(t, root, "system-dev/舊稿.doc", "x", baseTime)
writeFile(t, root, "真的.doc", "x", baseTime)
m := newTestManifest()
p, err := Scan(root, m, ScanOptions{SkipDirNames: map[string]bool{"system-dev": true}})
if err != nil {
t.Fatal(err)
}
if len(p.Skipped) != 1 || p.Skipped[0].Path != "真的.doc" {
t.Fatalf("只有使用者自己的檔該被點名,實得 %+v", p.Skipped)
}
if p.SkippedOther != 0 {
t.Errorf("隱藏檔不該進非文件計數,實得 %d", p.SkippedOther)
}
}
// 沒有任何讀不了的檔時,兩個欄位都該是零值 ⇒ UI 才不會憑空冒出一張卡。
func TestNoSkippedWhenEverythingSupported(t *testing.T) {
root := t.TempDir()
writeFile(t, root, "a.md", "x", baseTime)
writeFile(t, root, "b.txt", "y", baseTime)
p := mustScan(t, root, newTestManifest())
if len(p.Skipped) != 0 || p.SkippedOther != 0 {
t.Errorf("全部都讀得了時不該有略過紀錄:%+v / %d", p.Skipped, p.SkippedOther)
}
}