migration_repair is not a human revision. HashUpdater now loads from DB and retries still sync. Content reads return a structured drift conflict instead of the destructive old head. Co-authored-by: Cursor <cursoragent@cursor.com>
75 lines
3.0 KiB
Go
75 lines
3.0 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/docrevision"
|
|
)
|
|
|
|
func TestWriteControlledContentError_MapsTypedCodes(t *testing.T) {
|
|
cases := []struct {
|
|
err error
|
|
status int
|
|
code string
|
|
}{
|
|
{&docgov.ContentError{Code: docgov.ErrCodeDocumentNotFound, Message: "missing record"}, http.StatusNotFound, docgov.ErrCodeDocumentNotFound},
|
|
{&docgov.ContentError{Code: docgov.ErrCodeProjectionMissing, Message: "no file", NeedsAdmin: true, Retryable: true}, http.StatusConflict, docgov.ErrCodeProjectionMissing},
|
|
{&docgov.ContentError{Code: docgov.ErrCodeProjectionEmpty, Message: "empty", NeedsAdmin: true}, http.StatusConflict, docgov.ErrCodeProjectionEmpty},
|
|
{&docgov.ContentError{Code: docgov.ErrCodeIntegrityMismatch, Message: "mismatch", NeedsAdmin: true}, http.StatusConflict, docgov.ErrCodeIntegrityMismatch},
|
|
{&docgov.ContentError{Code: docgov.ErrCodeProjectionDrift, Message: "drift", NeedsAdmin: true, Class: "LEGACY_CANONICALIZATION_DRIFT"}, http.StatusConflict, docgov.ErrCodeProjectionDrift},
|
|
}
|
|
for _, tc := range cases {
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/docgov/documents/x/content", nil)
|
|
req.Header.Set("X-Request-ID", "corr-1")
|
|
writeControlledContentError(rec, req, tc.err)
|
|
if rec.Code != tc.status {
|
|
t.Fatalf("%s status %d want %d", tc.code, rec.Code, tc.status)
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body["error_code"] != tc.code {
|
|
t.Fatalf("code %v", body["error_code"])
|
|
}
|
|
if body["correlation_id"] != "corr-1" {
|
|
t.Fatalf("correlation %v", body["correlation_id"])
|
|
}
|
|
if tc.err.(*docgov.ContentError).Class != "" && body["projection_class"] != tc.err.(*docgov.ContentError).Class {
|
|
t.Fatalf("projection_class %v", body["projection_class"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestServeLedgerHeadOrConflict_RefusesDestructiveHead(t *testing.T) {
|
|
raw := "条目一 \n条目二 \n"
|
|
head := docrevision.LegacyDestructiveCanonicalizeMarkdown(raw)
|
|
class := docrevision.ClassifyProjection([]byte(raw), head)
|
|
if class.Class != docrevision.ProjectionLegacyCanonicalizationDrift {
|
|
t.Fatalf("class=%s", class.Class)
|
|
}
|
|
_, err := serveLedgerHeadOrConflict("doc_x", 1, false, head, docrevision.SHA256Hex(head), docrevision.SHA256Hex(head), class)
|
|
var ce *docgov.ContentError
|
|
if !errors.As(err, &ce) || ce.Code != docgov.ErrCodeProjectionDrift {
|
|
t.Fatalf("want projection drift, got %v", err)
|
|
}
|
|
if ce.Class != docrevision.ProjectionLegacyCanonicalizationDrift {
|
|
t.Fatalf("class %s", ce.Class)
|
|
}
|
|
|
|
aligned := docrevision.ClassifyProjection([]byte(raw), docrevision.CanonicalizeMarkdown(raw))
|
|
got, err := serveLedgerHeadOrConflict("doc_x", 1, false, docrevision.CanonicalizeMarkdown(raw), aligned.CanonSHA, aligned.CanonSHA, aligned)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Markdown != docrevision.CanonicalizeMarkdown(raw) {
|
|
t.Fatalf("aligned must serve preserved-hard-break head, got %q", got.Markdown)
|
|
}
|
|
}
|