Files
Cyrene_v2/internal/config/document_gc_env_test.go
AskaEth 3ba31b58ff
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(documents): 引用计数 + 文档/侧车 GC(主人 2.2 裁决;S7)
- 引用计数真实维护(documents 表唯一写入面):增减点只有四处 —— history 的 insert / UpdateMessageDocument / DeleteMessage / DeleteSession,且**全在同一事务**内(计数写失败 ⇒ 消息写入整体回滚,有钉住用例);UpdateMessageDocument 按新旧名字**集合差**增减(三阶段回写是常态,重算会重复计数 ⇒ 文件永远回收不掉)。

- GC:候选 = ref_count = 0;判据 = last_referenced_at(NULL 回落 created_at)早于 now − 保留期;动作 = 删 sidecar + 原件 + 元数据行(删行带 AND ref_count = 0 原子再确认);路径双检;汇总日志(含实占字节);单件失败 WARN 并继续;删除顺序 = 先文件后行(失败则行留着下轮重试,不留孤儿)。

- 组合根:启动期清扫一次(不阻塞启动)+ ticker 常驻(可停,close 顺序在关库之前);env DOCUMENT_GC_INTERVAL 默认 1h / DOCUMENT_RETENTION 默认 168h,**任一 ≤0 = 关闭 GC**(RETENTION=0 不解释成立即回收,关闭时记 WARN);.env.example 已同步(AGENTS §10)。

- 主会话独立验收:隔离 worktree(S7 的 18 文件,排除 F2 在途的 mcp/vision)build/vet 绿、全树 59 包 0 FAIL、GC/引用用例 20 PASS、双平台交叉编译绿;**变异亲自复现**(候选查询去掉 ref_count = 0 ⇒ TestGCDoesNotDeleteReferencedDocument 红:candidates = 1, want 0)。

- 未做(任务书允许的「只做后端」):面板手动「清理未引用附件」入口 —— 后端能力已就绪(documents.NewGC + GC.Sweep),接成端点属小切片(含 admin 路由 + 审计 document.gc + 契约登记)。

- 已知窄窗口(已在 gc.go 文件头登记):一轮 GC 与一次落库几乎同时发生时可能删掉刚被引用的文件(行不删、记 WARN);坏 document 列只记 WARN 不阻塞消息写入 ⇒ 计数可能偏低(只会让文件**晚**回收,绝不早删)。
2026-10-04 21:01:30 +08:00

49 lines
1.7 KiB
Go
Raw Permalink 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 config
import (
"strings"
"testing"
"time"
)
// TestLoad_DocumentGCKeys 钉住文档回收(GC)两个新键(切片 S7)的三态:
// 默认(1h / 168h = 7 天)、显式覆盖、以及「0 = 关闭」——后者尤其重要:0 必须被
// 解码成 0(而不是被当成未配置、悄悄回落到默认开启)。
func TestLoad_DocumentGCKeys(t *testing.T) {
t.Setenv("JWT_SECRET", strings.Repeat("s", 40))
cfg, err := Load("")
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if cfg.Server.DocumentGCInterval != time.Hour {
t.Errorf("DocumentGCInterval = %v, want 1h(默认)", cfg.Server.DocumentGCInterval)
}
if cfg.Server.DocumentRetention != 168*time.Hour {
t.Errorf("DocumentRetention = %v, want 168h(默认 7 天)", cfg.Server.DocumentRetention)
}
t.Setenv("DOCUMENT_GC_INTERVAL", "30m")
t.Setenv("DOCUMENT_RETENTION", "24h")
cfg, err = Load("")
if err != nil {
t.Fatalf("Load() with overrides: %v", err)
}
if cfg.Server.DocumentGCInterval != 30*time.Minute || cfg.Server.DocumentRetention != 24*time.Hour {
t.Errorf("覆盖后的取值为 (%v,%v), want (30m,24h)",
cfg.Server.DocumentGCInterval, cfg.Server.DocumentRetention)
}
// 0 = 关闭:必须原样抵达配置(GC 的「关闭」判据是 ≤0)。
t.Setenv("DOCUMENT_GC_INTERVAL", "0")
t.Setenv("DOCUMENT_RETENTION", "0")
cfg, err = Load("")
if err != nil {
t.Fatalf("Load() with zeros: %v", err)
}
if cfg.Server.DocumentGCInterval != 0 || cfg.Server.DocumentRetention != 0 {
t.Errorf("显式 0 后的取值为 (%v,%v), want (0,0)(0 = 关闭 GC,不是「回落默认」)",
cfg.Server.DocumentGCInterval, cfg.Server.DocumentRetention)
}
}