Files
weichenandCursor ed1e232022 feat(docrevision): persist ledger in PostgreSQL with bootstrap
Hydrate/mutate against schema 00028, ensure first-access bootstrap, and build deterministic change reports instead of static fiction.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 08:48:32 +00:00

95 lines
3.1 KiB
Go

package docrevision_test
import (
"context"
"testing"
"chat-agent-system/internal/docrevision"
)
func TestEnsureBootstrapIdempotentAndReadable(t *testing.T) {
svc := docrevision.NewService(nil)
ctx := context.Background()
st, err := svc.EnsureBootstrap(ctx, docrevision.BootstrapRequest{
DocID: "doc_proj143_srs", Kind: "controlled", Markdown: "# SRS\n\nbody\n",
ProjectID: 143, WorkDir: "workspaces/p143/srs", LifecycleState: "drafting",
Actor: docrevision.ActorSnapshot{DisplayName: "bootstrap"},
})
if err != nil {
t.Fatal(err)
}
if st.CurrentVersionNo != 1 || st.HeadRevisionNo != 0 {
t.Fatalf("unexpected state %+v", st)
}
st2, err := svc.EnsureBootstrap(ctx, docrevision.BootstrapRequest{
DocID: "doc_proj143_srs", Markdown: "# other\n", ProjectID: 143,
})
if err != nil {
t.Fatal(err)
}
if st2.HeadSHA256 != st.HeadSHA256 {
t.Fatalf("bootstrap should be idempotent")
}
got, err := svc.GetRevisionState(ctx, "doc_proj143_srs")
if err != nil || got.ETag == "" {
t.Fatalf("get after bootstrap: %v %+v", err, got)
}
}
func TestEnsureBootstrapNeedsContent(t *testing.T) {
svc := docrevision.NewService(nil)
_, err := svc.EnsureBootstrap(context.Background(), docrevision.BootstrapRequest{DocID: "doc-upload-x"})
if err != docrevision.ErrNeedsContent {
t.Fatalf("want ErrNeedsContent got %v", err)
}
}
func TestDeterministicChangeReportNoStaticFiction(t *testing.T) {
before := docrevision.CanonicalizeMarkdown("# A\n\n旧文\n")
after := docrevision.CanonicalizeMarkdown("# A\n\n新文 REQ-1\n")
fb := docrevision.BuildBlockMap(before, nil)
ab := docrevision.BuildBlockMap(after, fb)
diff := docrevision.ComputeDiff(before, after, fb, ab)
from := docrevision.SHA256Hex(before)
to := docrevision.SHA256Hex(after)
raw := docrevision.BuildDeterministicChangeReportJSON(from, to, docrevision.RiskHigh, nil, diff)
if raw == "" {
t.Fatal("empty report")
}
rep, err := docrevision.ValidateChangeReportJSON(raw, from, to, docrevision.RiskHigh, nil, diff.Hunks)
if err != nil {
t.Fatal(err)
}
if len(rep.Summary) == 0 {
t.Fatal("expected deterministic summary")
}
for _, s := range rep.Summary {
if s == "智能体交付物已生成" || s == "系统需求规格已更新" {
t.Fatalf("static fiction summary leaked: %q", s)
}
}
}
func TestHumanSaveAfterBootstrap(t *testing.T) {
svc := docrevision.NewService(nil)
ctx := context.Background()
st, _ := svc.EnsureBootstrap(ctx, docrevision.BootstrapRequest{
DocID: "doc_edit", Markdown: "# A\n", ProjectID: 1, LifecycleState: "drafting",
})
r, err := svc.CommitHumanRevision(ctx, docrevision.HumanRevisionRequest{
DocID: "doc_edit", Markdown: "# A\n\nedit\n",
ExpectedHeadSnapshot: &st.HeadSnapshotID, ExpectedRevisionNo: &st.HeadRevisionNo,
IdempotencyKey: "e1", ActorUserID: 9, LifecycleState: "drafting",
})
if err != nil {
t.Fatal(err)
}
if r.State.HeadRevisionNo != 1 {
t.Fatalf("want r1 got %d", r.State.HeadRevisionNo)
}
ov, err := svc.ManualOverlay(ctx, "doc_edit")
if err != nil || ov == nil || (ov.Summary.Modifies+ov.Summary.Inserts) == 0 {
t.Fatalf("manual overlay empty: %+v %v", ov, err)
}
}