Files
WeiCHEN 9c7a5be209 feat(arch): clear G1 U3/U4 sql_write and path exceptions
Move document_records duty-slot writes into docgov, project ledger
heads through docgov/freedoc ports, and workspace deliverable I/O
through DeliverableStore so arch-check baseline drops to sql_write:1
path:13.
2026-09-22 21:40:12 +08:00

166 lines
5.5 KiB
Go

package docrevision_test
import (
"os"
"path/filepath"
"testing"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/workspace"
)
func TestClassifyProjection_LegacyHardBreakDrift(t *testing.T) {
raw := "标题 \n下一行 \n正文\n"
head := docrevision.LegacyDestructiveCanonicalizeMarkdown(raw)
got := docrevision.ClassifyProjection([]byte(raw), head)
if got.Class != docrevision.ProjectionLegacyCanonicalizationDrift {
t.Fatalf("class=%s detail=%+v", got.Class, got)
}
}
func TestClassifyProjection_AlignedAndFork(t *testing.T) {
raw := "同一正文 \n"
canon := docrevision.CanonicalizeMarkdown(raw)
aligned := docrevision.ClassifyProjection([]byte(raw), canon)
if aligned.Class != docrevision.ProjectionAligned {
t.Fatalf("aligned got %s", aligned.Class)
}
fork := docrevision.ClassifyProjection([]byte("完全不同的正文\n"), canon)
if fork.Class != docrevision.ProjectionContentFork {
t.Fatalf("fork got %s", fork.Class)
}
if missing := docrevision.ClassifyProjection(nil, canon); missing.Class != docrevision.ProjectionMissing {
t.Fatalf("nil raw got %s", missing.Class)
}
if empty := docrevision.ClassifyProjection([]byte{}, canon); empty.Class != docrevision.ProjectionEmpty {
t.Fatalf("empty raw got %s", empty.Class)
}
}
func TestProjectOutputMarkdown_AtomicAndSameBytes(t *testing.T) {
ctx := t.Context()
dir := t.TempDir()
workDir := filepath.Join(dir, "ws")
if err := os.MkdirAll(workDir, 0o755); err != nil {
t.Fatal(err)
}
md := "人工保存 \n保留硬换行\n"
svc := docrevision.NewService(nil)
svc.DataDir = dir
svc.SetDeliverableStore(workspace.DirDeliverable{})
st, err := svc.OpenTemplateVersion(ctx, "doc_proj_write", md, docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
if err != nil {
t.Fatal(err)
}
if err := svc.ProjectOutputMarkdown("doc_proj_write", dir); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(filepath.Join(workDir, "output.md"))
if err != nil {
t.Fatal(err)
}
canon := docrevision.CanonicalizeMarkdown(md)
if string(got) != canon {
t.Fatalf("projected file must be canonical bytes: %q vs %q", got, canon)
}
if docrevision.SHA256Hex(string(got)) != st.HeadSHA256 {
t.Fatalf("file sha %s != head %s", docrevision.SHA256Hex(string(got)), st.HeadSHA256)
}
}
func TestProjectOutputMarkdown_RefusesSilentOverwriteOfLegacyFile(t *testing.T) {
ctx := t.Context()
dir := t.TempDir()
workDir := filepath.Join(dir, "ws")
if err := os.MkdirAll(workDir, 0o755); err != nil {
t.Fatal(err)
}
raw := "原文件 \n硬换行\n"
legacy := docrevision.LegacyDestructiveCanonicalizeMarkdown(raw)
if err := os.WriteFile(filepath.Join(workDir, "output.md"), []byte(raw), 0o644); err != nil {
t.Fatal(err)
}
svc := docrevision.NewService(nil)
svc.DataDir = dir
svc.SetDeliverableStore(workspace.DirDeliverable{})
if _, err := svc.OpenTemplateVersion(ctx, "doc_legacy", legacy, docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir); err != nil {
t.Fatal(err)
}
err := svc.ProjectOutputMarkdownIfSafe("doc_legacy", dir)
if err == nil {
t.Fatal("safe project must refuse legacy drift overwrite")
}
got, _ := os.ReadFile(filepath.Join(workDir, "output.md"))
if string(got) != raw {
t.Fatalf("file must stay untouched, got %q", got)
}
}
func TestMapStoredDataPath_RewritesContainerRoot(t *testing.T) {
mapped, err := docrevision.MapStoredDataPath(
"/app/data/workspaces/bizdev/intake_1",
"/app/data",
"/repo/data/dev",
)
if err != nil {
t.Fatal(err)
}
want := filepath.Join("/repo/data/dev", "workspaces", "bizdev", "intake_1")
if mapped != want {
t.Fatalf("mapped %q want %q", mapped, want)
}
if _, err := docrevision.MapStoredDataPath("/tmp/other", "/app/data", "/repo/data/dev"); err == nil {
t.Fatal("unrelated absolute path must not be reused silently")
}
if _, err := docrevision.MapStoredDataPath("/app/data/x", "/app/data", ""); err == nil {
t.Fatal("half mapping must fail")
}
}
func TestInspectProjection_RefusesUnmappedAbsoluteWorkDir(t *testing.T) {
ctx := t.Context()
host := t.TempDir()
other := t.TempDir()
if err := os.WriteFile(filepath.Join(other, "output.md"), []byte("其他绝对路径\n"), 0o644); err != nil {
t.Fatal(err)
}
svc := docrevision.NewService(nil)
svc.StoredDataRoot = "/app/data"
svc.HostDataRoot = host
svc.SetDeliverableStore(workspace.DirDeliverable{})
if _, err := svc.OpenTemplateVersion(ctx, "doc_abs", "# x\n", docrevision.ActorSnapshot{}, 1, other); err != nil {
t.Fatal(err)
}
if _, err := svc.InspectProjection(ctx, "doc_abs", ""); err == nil {
t.Fatal("inspect must fail closed when work_dir is outside stored-data-root")
}
}
func TestInspectProjection_UsesHostDataRootMapping(t *testing.T) {
ctx := t.Context()
host := t.TempDir()
ws := filepath.Join(host, "workspaces", "intake")
if err := os.MkdirAll(ws, 0o755); err != nil {
t.Fatal(err)
}
raw := "条目 \n结尾\n"
if err := os.WriteFile(filepath.Join(ws, "output.md"), []byte(raw), 0o644); err != nil {
t.Fatal(err)
}
svc := docrevision.NewService(nil)
svc.StoredDataRoot = "/app/data"
svc.HostDataRoot = host
svc.SetDeliverableStore(workspace.DirDeliverable{})
legacy := docrevision.LegacyDestructiveCanonicalizeMarkdown(raw)
if _, err := svc.OpenTemplateVersion(ctx, "doc_map", legacy, docrevision.ActorSnapshot{}, 1, "/app/data/workspaces/intake"); err != nil {
t.Fatal(err)
}
class, err := svc.InspectProjection(ctx, "doc_map", "")
if err != nil {
t.Fatal(err)
}
if class.Class != docrevision.ProjectionLegacyCanonicalizationDrift {
t.Fatalf("mapped inspect class=%s", class.Class)
}
}