fix(docgov): map zero duty ids to NULL on CreateOrGetDocument

Optional reviewer/approver duties leave UserID=0; writing 0 into
document_records FK columns failed with SQLSTATE 23503. Align create
path with docduty nullIfZero and surface ensureUser failures in logs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-17 06:08:35 +00:00
co-authored by Cursor
parent feefc322a7
commit c2efcdbacd
3 changed files with 98 additions and 7 deletions
+21 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"sync"
"time"
@@ -13,6 +14,15 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
)
// nullIfZero maps unset duty holders (0) to SQL NULL so nullable FK columns
// (author_id/reviewer_id/approver_id → users.id) accept optional duties.
func nullIfZero(id int64) any {
if id <= 0 {
return nil
}
return id
}
// ModificationCheckResult 智能体或人员发起修改前的认领与授权判定结果
type ModificationCheckResult struct {
AllowedDirectEdit bool `json:"allowed_direct_edit"` // 是否允许直接执行修改
@@ -153,12 +163,15 @@ func (e *GovernanceEngine) CreateOrGetDocument(
}
ensureUser := func(uid int64, uname, urole string) {
if uid > 0 {
_, _ = e.pool.Exec(ctx, `
INSERT INTO users (id, account_id, name, role)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO NOTHING
`, uid, fmt.Sprintf("acc_%d", uid), uname, urole)
if uid <= 0 {
return
}
if _, err := e.pool.Exec(ctx, `
INSERT INTO users (id, account_id, name, role)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO NOTHING
`, uid, fmt.Sprintf("acc_%d", uid), uname, urole); err != nil {
log.Printf("[docgov] ensureUser id=%d role=%s failed: %v", uid, urole, err)
}
}
ensureUser(doc.AuthorID, doc.AuthorName, "engineer")
@@ -185,7 +198,8 @@ func (e *GovernanceEngine) CreateOrGetDocument(
err := e.pool.QueryRow(ctx, query,
doc.DocID, doc.ProjectID, doc.TaskKey, doc.Title, doc.Stage, doc.Version,
string(doc.CurrentState), doc.IsLocked, doc.IsTemplateOnly, doc.WorkDir,
doc.AuthorID, doc.AuthorName, doc.ReviewerID, doc.ReviewerName, doc.ApproverID, doc.ApproverName,
nullIfZero(doc.AuthorID), doc.AuthorName, nullIfZero(doc.ReviewerID), doc.ReviewerName,
nullIfZero(doc.ApproverID), doc.ApproverName,
cfgJSON, doc.CreatedAt, doc.UpdatedAt,
).Scan(&id, &currState, &isLocked, &isTmpl, &authClaim, &revClaim, &appClaim)
if err != nil {
+61
View File
@@ -97,3 +97,64 @@ func TestDocGov_DatabaseIntegration(t *testing.T) {
t.Fatalf("expected doc locked in reviewing")
}
}
// TestDocGov_CreateOrGetDocument_OptionalDutiesNullFK ensures reviewer/approver=0
// persist as SQL NULL (optional DutyMode) instead of FK-violating 0.
func TestDocGov_CreateOrGetDocument_OptionalDutiesNullFK(t *testing.T) {
cfg, err := config.Load(".env")
if err != nil || cfg == nil || cfg.ContextDbHost == "" {
t.Skip("Skipping DB integration test: context DB not configured")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, cfg.ContextDSN())
if err != nil || pool.Ping(ctx) != nil {
t.Skipf("Skipping DB integration test: cannot connect to %s: %v", cfg.ContextDSN(), err)
}
defer pool.Close()
engine := docgov.NewGovernanceEngine(pool)
testDocID := fmt.Sprintf("doc_opt_duty_%d", time.Now().UnixNano())
projectID := time.Now().UnixNano()%900000 + 100000
doc, err := engine.CreateOrGetDocument(
ctx,
testDocID,
projectID,
"intake",
"可选职责空审核人",
"V0.5_feasibility",
"",
9, "系统管理员",
0, "",
0, "",
)
if err != nil {
t.Fatalf("CreateOrGetDocument with reviewer/approver=0 failed: %v", err)
}
if doc.DocID != testDocID {
t.Fatalf("unexpected docID: %s", doc.DocID)
}
var reviewerNull, approverNull bool
var authorID int64
err = pool.QueryRow(ctx, `
SELECT author_id,
reviewer_id IS NULL,
approver_id IS NULL
FROM document_records WHERE doc_id = $1
`, testDocID).Scan(&authorID, &reviewerNull, &approverNull)
if err != nil {
t.Fatalf("query document_records: %v", err)
}
if authorID != 9 {
t.Fatalf("author_id=%d want 9", authorID)
}
if !reviewerNull || !approverNull {
t.Fatalf("expected NULL reviewer/approver, got reviewerNull=%v approverNull=%v", reviewerNull, approverNull)
}
_, _ = pool.Exec(ctx, `DELETE FROM document_records WHERE doc_id = $1`, testDocID)
}
+16
View File
@@ -0,0 +1,16 @@
package docgov
import "testing"
func TestNullIfZero(t *testing.T) {
t.Parallel()
if got := nullIfZero(0); got != nil {
t.Fatalf("nullIfZero(0)=%v want nil", got)
}
if got := nullIfZero(-1); got != nil {
t.Fatalf("nullIfZero(-1)=%v want nil", got)
}
if got := nullIfZero(9); got != int64(9) {
t.Fatalf("nullIfZero(9)=%v want 9", got)
}
}