208 lines
8.0 KiB
Go
208 lines
8.0 KiB
Go
// foldertree_local_test.go — 本機那份樹快照(`inkstone/InkStoneCo#44` 桌面小幫手那半)
|
||
//
|
||
// 這裡測的不是「樹算得對不對」(那在 foldertree_test.go,已驗過、沒動),
|
||
// 而是**落地那一層的三個會讓畫面說謊的情境**:
|
||
//
|
||
// ① 這一輪掃壞了 ⇒ 不准把上一輪的好資料蓋成空的(畫面不該突然說「還沒回報」)
|
||
// ② 使用者移除了資料夾 ⇒ 快照裡那棵要消失(不然他以為沒移掉)
|
||
// ③ 存進去、讀回來,要是同一棵樹(桌面與 portal 看到的是同一份資料)
|
||
package collector
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func sampleTree(root string, synced, total int) FolderTree {
|
||
return FolderTree{
|
||
Library: "lib-" + filepath.Base(root),
|
||
DisplayName: filepath.Base(root),
|
||
Root: root,
|
||
Mode: "all",
|
||
Reason: "整個資料夾都收",
|
||
Nodes: []FolderNode{
|
||
{Path: "", Name: filepath.Base(root), Parent: "-", Depth: 0,
|
||
TotalFiles: total, SyncedFiles: synced, UnsupportedFiles: total - synced},
|
||
{Path: "sub", Name: "sub", Parent: "", Depth: 1, TotalFiles: 2, SyncedFiles: 2},
|
||
},
|
||
TotalNodes: 2,
|
||
GeneratedAt: 1000,
|
||
}
|
||
}
|
||
|
||
// ③ 存-讀來回:一個欄位都不准掉。
|
||
// 🔴 這條的重點不是 JSON 會不會動,是**桌面看到的必須是送上雲端的那一棵**——
|
||
// 少一個欄位就等於本機悄悄長出了第二套資料形狀。
|
||
func TestFolderTreeStoreRoundTrip(t *testing.T) {
|
||
dir := t.TempDir()
|
||
path := FolderTreeStorePath(filepath.Join(dir, "manifest.json"))
|
||
if want := filepath.Join(dir, "folder-trees.json"); path != want {
|
||
t.Fatalf("快照該與 manifest 同目錄:got %s want %s", path, want)
|
||
}
|
||
in := FolderTreeStore{
|
||
UpdatedAt: "2026-08-26T00:00:00Z",
|
||
Trees: map[string]FolderTree{"/a/b": sampleTree("/a/b", 3, 5)},
|
||
}
|
||
if err := SaveFolderTreeStore(path, in); err != nil {
|
||
t.Fatalf("存檔失敗:%v", err)
|
||
}
|
||
out, err := LoadFolderTreeStore(path)
|
||
if err != nil {
|
||
t.Fatalf("讀回失敗:%v", err)
|
||
}
|
||
got, ok := out.Trees["/a/b"]
|
||
if !ok {
|
||
t.Fatalf("讀回來少了那棵樹:%+v", out)
|
||
}
|
||
if got.Library != "lib-b" || got.Reason != "整個資料夾都收" || got.Mode != "all" {
|
||
t.Fatalf("欄位掉了:%+v", got)
|
||
}
|
||
if len(got.Nodes) != 2 || got.Nodes[0].TotalFiles != 5 || got.Nodes[0].SyncedFiles != 3 ||
|
||
got.Nodes[0].Parent != "-" || got.Nodes[1].Depth != 1 {
|
||
t.Fatalf("節點對不上:%+v", got.Nodes)
|
||
}
|
||
}
|
||
|
||
// 讀不到/壞掉的檔一律回可用的零值——桌面小幫手不該因為快照壞掉就整個畫不出來。
|
||
func TestFolderTreeStoreLoadMissingOrBroken(t *testing.T) {
|
||
dir := t.TempDir()
|
||
s, err := LoadFolderTreeStore(filepath.Join(dir, "nope.json"))
|
||
if err == nil {
|
||
t.Fatalf("檔案不存在該回 error")
|
||
}
|
||
if s.Trees == nil {
|
||
t.Fatalf("零值也要能直接用(Trees 不可為 nil)")
|
||
}
|
||
bad := filepath.Join(dir, "bad.json")
|
||
if err := os.WriteFile(bad, []byte("{ not json"), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s2, err := LoadFolderTreeStore(bad)
|
||
if err == nil {
|
||
t.Fatalf("壞檔該回 error")
|
||
}
|
||
if s2.Trees == nil {
|
||
t.Fatalf("壞檔也要回可用的零值")
|
||
}
|
||
}
|
||
|
||
// ①:這一輪某個根沒算出樹(掃描失敗/還沒輪到)⇒ 沿用上一輪的,不清空。
|
||
func TestMergeFolderTreeStoreKeepsPreviousWhenRoundProducedNothing(t *testing.T) {
|
||
now := time.Unix(2000, 0)
|
||
prev := FolderTreeStore{Trees: map[string]FolderTree{
|
||
"/a": sampleTree("/a", 3, 5),
|
||
"/b": sampleTree("/b", 1, 1),
|
||
}}
|
||
// 這一輪只有 /a 算出來,/b 掃失敗(fresh 裡沒有它),但兩個都還在看守清單上。
|
||
out := MergeFolderTreeStore(prev, map[string]FolderTree{"/a": sampleTree("/a", 5, 5)}, []string{"/a", "/b"}, now)
|
||
if len(out.Trees) != 2 {
|
||
t.Fatalf("兩個根都該在:%+v", out.Trees)
|
||
}
|
||
if out.Trees["/a"].Nodes[0].SyncedFiles != 5 {
|
||
t.Fatalf("這一輪算出來的該覆蓋舊的:%+v", out.Trees["/a"].Nodes[0])
|
||
}
|
||
if out.Trees["/b"].Nodes[0].SyncedFiles != 1 {
|
||
t.Fatalf("這一輪沒算出來的該沿用上一輪,不是被清掉:%+v", out.Trees["/b"])
|
||
}
|
||
if out.UpdatedAt == "" {
|
||
t.Fatalf("要記得更新時間,畫面才說得出「這是什麼時候的現況」")
|
||
}
|
||
}
|
||
|
||
// ②:使用者把資料夾從看守清單移掉 ⇒ 快照裡那棵要消失。
|
||
// 留著的話他移除完還看得到那棵樹,會以為移除沒作用(arcrun-rag#46 修過的同一個病)。
|
||
func TestMergeFolderTreeStoreDropsUnwatchedRoots(t *testing.T) {
|
||
now := time.Unix(3000, 0)
|
||
prev := FolderTreeStore{Trees: map[string]FolderTree{
|
||
"/a": sampleTree("/a", 3, 5),
|
||
"/gone": sampleTree("/gone", 1, 1),
|
||
}}
|
||
out := MergeFolderTreeStore(prev, map[string]FolderTree{}, []string{"/a"}, now)
|
||
if _, still := out.Trees["/gone"]; still {
|
||
t.Fatalf("已經不看守的根不該留在快照裡:%+v", out.Trees)
|
||
}
|
||
if _, ok := out.Trees["/a"]; !ok {
|
||
t.Fatalf("還在看守的根不該被一起刪掉:%+v", out.Trees)
|
||
}
|
||
}
|
||
|
||
// ── 接線測試:跑完一輪,本機真的多出那份快照 ────────────────────────────────
|
||
//
|
||
// 🔴 這支存在的理由就是 `arcrun-rag#104` 那條教訓的翻版:
|
||
// 「東西做好了,但**不在會被執行的那條路上**」。
|
||
// 樹早就算得出來(BuildFolderTree 有測、送雲端有測),但只要 RunDirectOnce
|
||
// 沒把它落地,桌面小幫手就永遠一個字都看不到——而單看那些既有的測試,全綠。
|
||
func TestRunDirectOnce把樹落地給桌面小幫手(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
||
}))
|
||
defer srv.Close()
|
||
|
||
base := t.TempDir()
|
||
root := filepath.Join(base, "我的知識庫")
|
||
mustWrite(t, filepath.Join(root, "報銷規則.md"), "# 報銷規則\n內容")
|
||
mustWrite(t, filepath.Join(root, "docs", "流程.md"), "# 流程\n內容")
|
||
mustWrite(t, filepath.Join(root, "docs", "main.go"), "package main")
|
||
|
||
orig := fetchCloudVersion
|
||
fetchCloudVersion = func(string) (string, bool) { return "stub", true }
|
||
defer func() { fetchCloudVersion = orig }()
|
||
|
||
manifest := filepath.Join(base, "manifest.json")
|
||
cfg := &DirectConfig{
|
||
Manifest: manifest,
|
||
Accounts: []AccountConfig{{CypherURL: srv.URL, Namespace: "ns", WatchFolders: []string{root}}},
|
||
MaxRemoved: DefaultMaxRemovedRatio,
|
||
ExtractorExplicit: true, // 隔離變因:本測驗的是落地,不是萃取
|
||
}
|
||
if _, exit, _ := RunDirectOnce(cfg, false); exit != 0 {
|
||
t.Fatalf("同步應成功,exit=%d", exit)
|
||
}
|
||
|
||
store, err := LoadFolderTreeStore(FolderTreeStorePath(manifest))
|
||
if err != nil {
|
||
t.Fatalf("跑完一輪之後應該有快照可讀:%v", err)
|
||
}
|
||
tree, ok := store.Trees[root]
|
||
if !ok {
|
||
t.Fatalf("快照裡沒有這個看守根:%+v", store.Trees)
|
||
}
|
||
// 桌面畫面要的東西一件都不能少:根節點、子資料夾、分母、以及「為什麼沒收」的分類。
|
||
if len(tree.Nodes) < 2 {
|
||
t.Fatalf("樹太扁,畫不出巢狀結構:%+v", tree.Nodes)
|
||
}
|
||
if tree.DisplayName != "我的知識庫" {
|
||
t.Errorf("根的顯示名要是使用者認得的資料夾名,got %q", tree.DisplayName)
|
||
}
|
||
var docs *FolderNode
|
||
for i := range tree.Nodes {
|
||
if tree.Nodes[i].Path == "docs" {
|
||
docs = &tree.Nodes[i]
|
||
}
|
||
}
|
||
if docs == nil {
|
||
t.Fatalf("子資料夾 docs 不在樹上:%+v", tree.Nodes)
|
||
}
|
||
if docs.TotalFiles != 2 {
|
||
t.Errorf("docs 分母=%d,應為 2(使用者在 Finder 裡看得到兩個檔):%+v", docs.TotalFiles, docs)
|
||
}
|
||
if docs.UnsupportedFiles+docs.ExcludedFiles == 0 {
|
||
t.Errorf("main.go 沒被分類到「為什麼沒上去」:%+v", docs)
|
||
}
|
||
|
||
// 🔴 不准併進 status.json:那支每秒被讀一次,樹上限 300 個節點。
|
||
raw, err := os.ReadFile(StatusFilePath(manifest))
|
||
if err != nil {
|
||
t.Fatalf("status.json 該存在:%v", err)
|
||
}
|
||
if strings.Contains(string(raw), "folder_tree") || strings.Contains(string(raw), "\"nodes\"") {
|
||
t.Errorf("樹跑進 status.json 了——那支每秒讀一次,不該扛這個")
|
||
}
|
||
}
|