fix(docrevision): pg_store JSONB 反序列化失败显式报错(R-5)
hydrateDocLocked 损坏字段不再静默零值化;补 PG 负例测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user