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.
166 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|