Files
Agent-System/internal/server/project_doc_content_error_test.go
weichenandCursor c754f7743d feat(docquery): serve ledger when projection is missing
Authorize first, then return immutable ledger markdown with typed consistency so a missing or drifted projection is no longer a 404/409. GET stays read-only and FAV degraded read stays off.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 14:07:05 +00:00

73 lines
2.9 KiB
Go

package server
import (
"encoding/json"
"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_ServesLedgerOnDrift(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)
}
gotDrift, err := serveLedgerHeadOrConflict("doc_x", 1, false, head, docrevision.SHA256Hex(head), docrevision.SHA256Hex(head), class)
if err != nil {
t.Fatal(err)
}
if gotDrift.Consistency != "drift" || gotDrift.Markdown != head {
t.Fatalf("drift must still serve ledger: %+v", gotDrift)
}
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)
}
}