fix(docrevision): pg_store JSONB 反序列化失败显式报错(R-5)

hydrateDocLocked 损坏字段不再静默零值化;补 PG 负例测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-18 03:44:08 +00:00
co-authored by Cursor
parent e25a197410
commit 58b69a2b08
2 changed files with 102 additions and 11 deletions
+34 -11
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"chat-agent-system/internal/app/uow"
@@ -65,8 +66,12 @@ func (s *Service) hydrateDocLocked(ctx context.Context, docID string) error {
&sn.SourceKind, &sn.ActorUserID, &sn.ActorRef, &actorRaw, &sn.SourceRunID, &sn.CreatedAt); err != nil {
return err
}
_ = json.Unmarshal(blockRaw, &sn.BlockMap)
_ = json.Unmarshal(actorRaw, &sn.ActorSnapshot)
if err := json.Unmarshal(blockRaw, &sn.BlockMap); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal block_map for doc %s snapshot %d: %w", docID, sn.ID, err)
}
if err := json.Unmarshal(actorRaw, &sn.ActorSnapshot); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal actor_snapshot for doc %s snapshot %d: %w", docID, sn.ID, err)
}
snaps = append(snaps, snapRow{snap: sn})
shas[sn.ContentSHA256] = struct{}{}
}
@@ -141,8 +146,12 @@ func (s *Service) hydrateDocLocked(ctx context.Context, docID string) error {
&ev.EditSessionID, &ev.RequestID, &ev.IdempotencyKey, &ev.CreatedAt); err != nil {
return err
}
_ = json.Unmarshal(actorRaw, &ev.ActorSnapshot)
_ = json.Unmarshal(changeRaw, &ev.ChangeSet)
if err := json.Unmarshal(actorRaw, &ev.ActorSnapshot); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal actor_snapshot for doc %s event %d: %w", docID, ev.ID, err)
}
if err := json.Unmarshal(changeRaw, &ev.ChangeSet); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal change_set for doc %s event %d: %w", docID, ev.ID, err)
}
cp := ev
s.events[cp.ID] = &cp
if cp.IdempotencyKey != "" {
@@ -173,12 +182,24 @@ func (s *Service) hydrateDocLocked(ctx context.Context, docID string) error {
&rep.ModelSnapshot, &rep.PromptVersion, &factRaw, &rep.CorrectsReportID, &rep.RawJSON, &rep.CreatedAt); err != nil {
return err
}
_ = json.Unmarshal(sumRaw, &rep.Summary)
_ = json.Unmarshal(riskRaw, &rep.RiskItems)
_ = json.Unmarshal(projRaw, &rep.ProjectImpacts)
_ = json.Unmarshal(docRaw, &rep.DocumentImpacts)
_ = json.Unmarshal(qRaw, &rep.OpenQuestions)
_ = json.Unmarshal(factRaw, &rep.DeterministicFacts)
if err := json.Unmarshal(sumRaw, &rep.Summary); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal summary for doc %s report %d: %w", docID, rep.ID, err)
}
if err := json.Unmarshal(riskRaw, &rep.RiskItems); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal risk_items for doc %s report %d: %w", docID, rep.ID, err)
}
if err := json.Unmarshal(projRaw, &rep.ProjectImpacts); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal project_impacts for doc %s report %d: %w", docID, rep.ID, err)
}
if err := json.Unmarshal(docRaw, &rep.DocumentImpacts); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal document_impacts for doc %s report %d: %w", docID, rep.ID, err)
}
if err := json.Unmarshal(qRaw, &rep.OpenQuestions); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal open_questions for doc %s report %d: %w", docID, rep.ID, err)
}
if err := json.Unmarshal(factRaw, &rep.DeterministicFacts); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal deterministic_facts for doc %s report %d: %w", docID, rep.ID, err)
}
cp := rep
s.reports[cp.ID] = &cp
if cp.ID > s.nextRepID {
@@ -205,7 +226,9 @@ func (s *Service) hydrateDocLocked(ctx context.Context, docID string) error {
&c.CreatedAt, &c.DecidedAt); err != nil {
return err
}
_ = json.Unmarshal(actorRaw, &c.ActorSnapshot)
if err := json.Unmarshal(actorRaw, &c.ActorSnapshot); err != nil {
return fmt.Errorf("hydrateDocLocked: unmarshal actor_snapshot for doc %s candidate %d: %w", docID, c.ID, err)
}
cp := c
s.candidates[cp.ID] = &cp
if cp.ID > s.nextCandID {
@@ -0,0 +1,68 @@
package docrevision_test
import (
"context"
"strings"
"testing"
"chat-agent-system/internal/db"
"chat-agent-system/internal/docrevision"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestHydrateDocLocked_CorruptJSONBFailsExplicitly_PG(t *testing.T) {
dsn := db.SkipIfNoTestDatabaseURL(t)
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
var n int
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM information_schema.tables WHERE table_name='document_snapshots'`).Scan(&n); err != nil || n == 0 {
t.Skip("document_snapshots table missing; migrate required")
}
docID := "doc_hydrate_corrupt_jsonb"
_, _ = pool.Exec(ctx, `DELETE FROM document_change_events WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_agent_candidates WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_change_reports WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_versions WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_snapshots WHERE doc_id=$1`, docID)
_, err = pool.Exec(ctx, `
INSERT INTO document_snapshots (
doc_id, content_sha256, parent_snapshot_id, block_map, source_kind,
actor_user_id, actor_ref, actor_snapshot, source_run_id, created_at
) VALUES (
$1, 'sha_corrupt_test', NULL, '{not-valid-json'::bytea, 'human',
1, 'tester', '{}'::jsonb, NULL, now()
)
`, docID)
if err != nil {
// Some schemas store block_map as jsonb (rejects invalid at insert). Fall back to casting via text.
_, err = pool.Exec(ctx, `
INSERT INTO document_snapshots (
doc_id, content_sha256, parent_snapshot_id, block_map, source_kind,
actor_user_id, actor_ref, actor_snapshot, source_run_id, created_at
) VALUES (
$1, 'sha_corrupt_test', NULL, convert_to('{not-valid-json', 'UTF8'), 'human',
1, 'tester', '{}'::jsonb, NULL, now()
)
`, docID)
if err != nil {
t.Skipf("cannot insert corrupt block_map for schema: %v", err)
}
}
svc := docrevision.NewService(pool)
_, err = svc.GetRevisionState(ctx, docID)
if err == nil {
t.Fatal("expected hydrate error for corrupt block_map JSONB")
}
if !strings.Contains(err.Error(), "hydrateDocLocked") || !strings.Contains(err.Error(), "block_map") {
t.Fatalf("expected explicit hydrate/block_map error, got: %v", err)
}
}