dcdc8d1ddd
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环): - 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口) - 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入 parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截 - 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过): * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id) * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配 * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数 * 回滚 job payload 支持 prior_execution_id(strict 解码) - 三通道发布+回滚演练实测:publish 4/4 verified,检索命中, rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false - 前端 A·Data-console 风格全站统一,120 用例通过 - 门禁:go vet / go test 全包通过,gofmt 干净
170 lines
7.7 KiB
Go
170 lines
7.7 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
func TestListReviewCandidatesReturnsSafeCurrentGateProjection(t *testing.T) {
|
|
now := time.Date(2026, 9, 6, 8, 0, 0, 0, time.UTC)
|
|
db := &recordingDB{
|
|
row: reviewCountRow{count: 1},
|
|
rows: &reviewCandidateRows{values: []reviewCandidateRow{{
|
|
publicationID: "tp-safe", title: "Synthetic review item", sourceType: "manual",
|
|
currentVisibility: "internal", acceptanceRunID: "job-safe", knowledgeState: "passed",
|
|
chunksState: "passed", retrievalState: "manual_required", gateStatus: "ready", gateCode: "ready",
|
|
reviewState: "pending", updatedAt: now,
|
|
}}},
|
|
}
|
|
page, err := NewStore(db).ListReviewCandidates(context.Background(), governance.ReviewCandidateFilter{ReviewState: "pending", Limit: 20, Offset: 0})
|
|
if err != nil {
|
|
t.Fatalf("ListReviewCandidates() error = %v", err)
|
|
}
|
|
if page.Total != 1 || len(page.Items) != 1 {
|
|
t.Fatalf("page = %#v", page)
|
|
}
|
|
item := page.Items[0]
|
|
if item.PublicationID != "tp-safe" || item.GateStatus != governance.ReviewGateReady || item.ReviewState != governance.ReviewPending || item.RetrievalHit != governance.AcceptanceManualRequired {
|
|
t.Fatalf("item = %#v", item)
|
|
}
|
|
joined := strings.ToLower(db.queryRowSQL + "\n" + db.querySQL)
|
|
for _, required := range []string{
|
|
"from acceptance_results", "distinct on (publication_id)", "knowledge_exists", "chunks_present",
|
|
"retrieval_hit", "manual_required", "from publication_reviews", "publication_updated_at", "acceptance_run_id",
|
|
} {
|
|
if !strings.Contains(joined, required) {
|
|
t.Errorf("review queue SQL missing %q", required)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{"knowledge_id", "test_kb_id", "local_spool_ref", "stable_external_id", "canonical_uri_hash", "raw_record_summary", "secret_ref", "base_url"} {
|
|
if strings.Contains(joined, forbidden) {
|
|
t.Errorf("review queue SQL includes forbidden projection %q", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListReviewCandidatesRejectsInvalidFilter(t *testing.T) {
|
|
_, err := NewStore(&recordingDB{}).ListReviewCandidates(context.Background(), governance.ReviewCandidateFilter{ReviewState: "invalid", Limit: 20})
|
|
if !errors.Is(err, ErrInvalidReviewFilter) {
|
|
t.Fatalf("error = %v, want ErrInvalidReviewFilter", err)
|
|
}
|
|
}
|
|
|
|
func TestReviewPublicationAppendsCurrentAcceptanceBoundDecision(t *testing.T) {
|
|
now := time.Date(2026, 9, 6, 8, 10, 0, 0, time.UTC)
|
|
db := &recordingDB{row: publicationReviewRow{
|
|
id: "rv-safe", publicationID: "tp-safe", decision: "approved", visibility: "internal",
|
|
reviewerRef: "engineer-1", decisionCode: "approved_for_release", acceptanceRunID: "job-safe",
|
|
publicationUpdatedAt: now.Add(-time.Minute), createdAt: now,
|
|
}}
|
|
review, err := NewStore(db).ReviewPublication(context.Background(), governance.ReviewPublicationRequest{
|
|
PublicationID: "tp-safe", Decision: governance.ReviewApproved, DecidedVisibility: governance.VisibilityInternal,
|
|
ReviewerRef: "engineer-1", DecisionCode: "approved_for_release", Now: now,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ReviewPublication() error = %v", err)
|
|
}
|
|
if review.ID != "rv-safe" || review.Decision != governance.ReviewApproved || review.AcceptanceRunID != "job-safe" {
|
|
t.Fatalf("review = %#v", review)
|
|
}
|
|
sql := strings.ToLower(db.queryRowSQL)
|
|
for _, required := range []string{
|
|
"insert into publication_reviews", "acceptance_run_id", "publication_updated_at",
|
|
"gate_status = 'ready'", "on conflict", "returning",
|
|
} {
|
|
if !strings.Contains(sql, required) {
|
|
t.Errorf("review SQL missing %q", required)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReviewPublicationRejectsInvalidDecisionContract(t *testing.T) {
|
|
requests := []governance.ReviewPublicationRequest{
|
|
{PublicationID: "tp-safe", Decision: governance.ReviewApproved, DecidedVisibility: governance.VisibilityInternal, ReviewerRef: "engineer-1", DecisionCode: "needs_correction", Now: time.Now()},
|
|
{PublicationID: "tp-safe", Decision: governance.ReviewRejected, DecidedVisibility: governance.VisibilityInternal, ReviewerRef: "engineer-1", DecisionCode: "approved_for_release", Now: time.Now()},
|
|
{PublicationID: "tp-safe", Decision: governance.ReviewApproved, DecidedVisibility: governance.VisibilityInternal, ReviewerRef: "bad\nactor", DecisionCode: "approved_for_release", Now: time.Now()},
|
|
}
|
|
for _, request := range requests {
|
|
if _, err := NewStore(&recordingDB{}).ReviewPublication(context.Background(), request); !errors.Is(err, ErrInvalidPublicationReview) {
|
|
t.Fatalf("request=%#v error=%v, want ErrInvalidPublicationReview", request, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReviewPublicationReturnsNotEligibleWhenGateRejects(t *testing.T) {
|
|
db := &recordingDB{row: sourceScanErrorRow{err: pgx.ErrNoRows}}
|
|
_, err := NewStore(db).ReviewPublication(context.Background(), governance.ReviewPublicationRequest{
|
|
PublicationID: "tp-safe", Decision: governance.ReviewApproved, DecidedVisibility: governance.VisibilityInternal,
|
|
ReviewerRef: "engineer-1", DecisionCode: "approved_for_release", Now: time.Now(),
|
|
})
|
|
if !errors.Is(err, ErrPublicationReviewNotEligible) {
|
|
t.Fatalf("error=%v, want ErrPublicationReviewNotEligible", err)
|
|
}
|
|
}
|
|
|
|
type reviewCountRow struct{ count int }
|
|
|
|
func (r reviewCountRow) Scan(dest ...any) error {
|
|
*dest[0].(*int) = r.count
|
|
return nil
|
|
}
|
|
|
|
type publicationReviewRow struct {
|
|
id, publicationID, decision, visibility, reviewerRef, decisionCode string
|
|
acceptanceRunID string
|
|
publicationUpdatedAt, createdAt time.Time
|
|
}
|
|
|
|
func (r publicationReviewRow) Scan(dest ...any) error {
|
|
*dest[0].(*string), *dest[1].(*string), *dest[2].(*string), *dest[3].(*string) = r.id, r.publicationID, r.decision, r.visibility
|
|
*dest[4].(*string), *dest[5].(*string), *dest[6].(*string) = r.reviewerRef, r.decisionCode, r.acceptanceRunID
|
|
*dest[7].(*time.Time), *dest[8].(*time.Time) = r.publicationUpdatedAt, r.createdAt
|
|
return nil
|
|
}
|
|
|
|
type reviewCandidateRow struct {
|
|
publicationID, title, sourceType, currentVisibility string
|
|
acceptanceRunID, knowledgeState, chunksState string
|
|
retrievalState, gateStatus, gateCode, reviewState string
|
|
decidedVisibility, decisionCode, reviewerRef string
|
|
reviewedAt *time.Time
|
|
updatedAt time.Time
|
|
}
|
|
|
|
type reviewCandidateRows struct {
|
|
values []reviewCandidateRow
|
|
index int
|
|
}
|
|
|
|
func (r *reviewCandidateRows) Close() {}
|
|
func (r *reviewCandidateRows) Err() error { return nil }
|
|
func (r *reviewCandidateRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} }
|
|
func (r *reviewCandidateRows) FieldDescriptions() []pgconn.FieldDescription { return nil }
|
|
func (r *reviewCandidateRows) RawValues() [][]byte { return nil }
|
|
func (r *reviewCandidateRows) Conn() *pgx.Conn { return nil }
|
|
func (r *reviewCandidateRows) Next() bool {
|
|
if r.index >= len(r.values) {
|
|
return false
|
|
}
|
|
r.index++
|
|
return true
|
|
}
|
|
func (r *reviewCandidateRows) Scan(dest ...any) error {
|
|
v := r.values[r.index-1]
|
|
*dest[0].(*string), *dest[1].(*string), *dest[2].(*string), *dest[3].(*string) = v.publicationID, v.title, v.sourceType, v.currentVisibility
|
|
*dest[4].(*string), *dest[5].(*string), *dest[6].(*string), *dest[7].(*string) = v.acceptanceRunID, v.knowledgeState, v.chunksState, v.retrievalState
|
|
*dest[8].(*string), *dest[9].(*string), *dest[10].(*string) = v.gateStatus, v.gateCode, v.reviewState
|
|
*dest[11].(*string), *dest[12].(*string), *dest[13].(*string) = v.decidedVisibility, v.decisionCode, v.reviewerRef
|
|
*dest[14].(**time.Time), *dest[15].(*time.Time) = v.reviewedAt, v.updatedAt
|
|
return nil
|
|
}
|
|
func (r *reviewCandidateRows) Values() ([]any, error) { return nil, nil }
|