Files
Agent-System/internal/server/document_revision_patch_order_test.go
T
weichenandCursor 7b48ca2ecc fix(docgov): assert edit duty before lease; project via SaveDocumentContent
Unauthorized actors no longer grab leases; controlled PATCH projects through
DocGov so author_last_edited and audit survive, with idempotent retry on failure.

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

154 lines
5.5 KiB
Go

package server
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"chat-agent-system/internal/docrevision"
)
func TestDocRevision_PatchOrder_AssertBeforeLedger_ProjectionFailureKeepsLedger(t *testing.T) {
srv := newTestServer(t)
ctx := context.Background()
dataRoot := srv.dataRoot()
// WorkDir whose parent is a regular file → SaveDocumentContent MkdirAll fails after ledger commit.
blocker := filepath.Join(dataRoot, "proj-block")
if err := os.WriteFile(blocker, []byte("not-a-dir"), 0o644); err != nil {
t.Fatal(err)
}
workDir := filepath.Join(blocker, "ws-proj")
docID := "doc_proj1_patch_order"
if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
t.Fatal(err)
}
st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
if err != nil {
t.Fatal(err)
}
beforeRev := st.HeadRevisionNo
beforeSnap := st.HeadSnapshotID
token := loginTestAdmin(t, srv)
body := `{"markdown":"# base\n\nbody\n\nedited by human\n"}`
req := httptest.NewRequest(http.MethodPatch, "/api/v1/documents/"+docID+"/content", bytes.NewReader([]byte(body)))
withBearerAuth(req, token)
req.Header.Set("Idempotency-Key", "patch-order-k1")
req.Header.Set("If-Match", `"`+strconv.FormatInt(st.HeadSnapshotID, 10)+":"+strconv.Itoa(st.HeadRevisionNo)+`"`)
w := httptest.NewRecorder()
srv.Routes().ServeHTTP(w, req)
if w.Code != http.StatusInternalServerError {
t.Fatalf("projection failure want 500 got %d body=%s", w.Code, w.Body.String())
}
var payload map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["code"] != "DOCUMENT_PROJECTION_FAILED" {
t.Fatalf("code want DOCUMENT_PROJECTION_FAILED got %#v", payload["code"])
}
after, err := srv.svc.DocRevision().GetRevisionState(ctx, docID)
if err != nil {
t.Fatal(err)
}
if after.HeadRevisionNo <= beforeRev && after.HeadSnapshotID == beforeSnap {
t.Fatalf("ledger should commit before projection; before rev=%d snap=%d after=%+v", beforeRev, beforeSnap, after)
}
}
func TestDocRevision_PatchOrder_LeaseDeniedDoesNotAdvanceLedger(t *testing.T) {
srv := newTestServer(t)
ctx := context.Background()
docID := "doc_proj1_lease_denied"
workDir := filepath.Join(srv.dataRoot(), "ws-lease")
_ = os.MkdirAll(workDir, 0o755)
// Author is user 99; login admin may bypass route author gate but AssertHumanCanEdit (RoleAuthor) still fails.
if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 99, "other", 2, "r", 3, "a"); err != nil {
t.Fatal(err)
}
st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# L\n\nx\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
if err != nil {
t.Fatal(err)
}
before := st.HeadRevisionNo
token := loginTestAdmin(t, srv)
req := httptest.NewRequest(http.MethodPatch, "/api/v1/documents/"+docID+"/content",
bytes.NewReader([]byte(`{"markdown":"# L\n\nchanged\n"}`)))
withBearerAuth(req, token)
req.Header.Set("Idempotency-Key", "lease-denied-k1")
req.Header.Set("If-Match", `"`+strconv.FormatInt(st.HeadSnapshotID, 10)+":"+strconv.Itoa(st.HeadRevisionNo)+`"`)
w := httptest.NewRecorder()
srv.Routes().ServeHTTP(w, req)
if w.Code == http.StatusOK {
t.Fatalf("non-author must not save successfully: %s", w.Body.String())
}
after, err := srv.svc.DocRevision().GetRevisionState(ctx, docID)
if err != nil {
t.Fatal(err)
}
if after.HeadRevisionNo != before {
t.Fatalf("ledger must not advance when AssertHumanCanEdit fails: before=%d after=%d", before, after.HeadRevisionNo)
}
doc, err := srv.svc.DocGov().GetDocument(ctx, docID)
if err != nil {
t.Fatal(err)
}
if doc.HasActiveLease(time.Now()) && doc.LeaseHolderID == "1" {
t.Fatal("failed AssertHumanCanEdit must not leave non-author admin holding the lease")
}
}
func TestDocRevision_PatchOrder_SaveDocumentContentSetsAuthorAudit(t *testing.T) {
srv := newTestServer(t)
ctx := context.Background()
workDir := filepath.Join(srv.dataRoot(), "ws-audit")
_ = os.MkdirAll(workDir, 0o755)
docID := "doc_proj1_patch_audit"
if _, err := srv.svc.DocGov().CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", workDir, 1, "admin", 2, "r", 3, "a"); err != nil {
t.Fatal(err)
}
st, err := srv.svc.DocRevision().OpenTemplateVersion(ctx, docID, "# base\n\nbody\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, workDir)
if err != nil {
t.Fatal(err)
}
token := loginTestAdmin(t, srv)
md := "# base\n\nbody\n\nhuman edit\n"
req := httptest.NewRequest(http.MethodPatch, "/api/v1/documents/"+docID+"/content",
bytes.NewReader([]byte(`{"markdown":`+strconv.Quote(md)+`}`)))
withBearerAuth(req, token)
req.Header.Set("Idempotency-Key", "patch-audit-k1")
req.Header.Set("If-Match", `"`+strconv.FormatInt(st.HeadSnapshotID, 10)+":"+strconv.Itoa(st.HeadRevisionNo)+`"`)
w := httptest.NewRecorder()
srv.Routes().ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("want 200 got %d %s", w.Code, w.Body.String())
}
doc, err := srv.svc.DocGov().GetDocument(ctx, docID)
if err != nil {
t.Fatal(err)
}
if doc.AuthorLastEditedAt == nil {
t.Fatal("SaveDocumentContent projection must set author_last_edited_at")
}
if !doc.AuthorClaimed {
t.Fatal("author_claimed should be true after human save projection")
}
if _, err := os.Stat(filepath.Join(workDir, "output.md")); err != nil {
t.Fatalf("output.md should exist after SaveDocumentContent: %v", err)
}
}