Files
knowledge-governance/internal/platform/postgres/source_connections_test.go
T
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(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 干净
2026-09-30 10:54:06 +08:00

133 lines
5.1 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 TestListSourceConnectionsReturnsSafeOperationalProjection(t *testing.T) {
now := time.Date(2026, 9, 6, 1, 0, 0, 0, time.UTC)
horizon := now.Add(time.Hour)
rows := &sourceConnectionRows{items: []sourceConnectionRow{
{id: "conn-pp", sourceType: "pp", name: "PP历史公开知识", enabled: false, scheduleConfigured: false, health: "healthy", itemCount: 613, updatedAt: now, lastScan: true},
{id: "conn-manual", sourceType: "manual", name: "本地手工验收", enabled: false, scheduleConfigured: false, health: "unknown", itemCount: 5, updatedAt: now},
{id: "conn-alist", sourceType: "alist", name: "AList只读源", enabled: true, scheduleConfigured: true, health: "healthy", itemCount: 0, updatedAt: now, nextRefreshAt: &horizon},
}}
db := &recordingDB{rows: rows}
page, err := NewStore(db).ListSourceConnections(context.Background())
if err != nil {
t.Fatalf("ListSourceConnections() error = %v", err)
}
if len(page.Items) != 3 || page.Items[0].ID != "conn-pp" || page.Items[0].ItemCount != 613 || page.Items[2].HealthStatus != governance.SourceHealthHealthy {
t.Fatalf("items = %#v", page.Items)
}
if page.Items[0].LastScan == nil || page.Items[0].LastScan.State != governance.SourceScanCompleted || page.Items[0].LastScan.ObservedItems != 613 || page.Items[0].LastScan.OverLimitItems != 54 {
t.Fatalf("last scan = %#v", page.Items[0].LastScan)
}
// V1: only the scheduled source exposes a next-refresh horizon.
if page.Items[0].NextRefreshAt != nil || page.Items[1].NextRefreshAt != nil {
t.Fatalf("unscheduled sources leak horizons: %v %v", page.Items[0].NextRefreshAt, page.Items[1].NextRefreshAt)
}
if page.Items[2].NextRefreshAt == nil || !page.Items[2].NextRefreshAt.Equal(horizon) {
t.Fatalf("alist horizon = %v want %v", page.Items[2].NextRefreshAt, horizon)
}
wantSummary := governance.SourceConnectionSummary{Total: 3, Enabled: 1, Disabled: 2, Healthy: 2, Unknown: 1}
if page.Summary != wantSummary {
t.Fatalf("summary = %#v, want %#v", page.Summary, wantSummary)
}
query := strings.ToLower(db.querySQL)
for _, required := range []string{"count(i.id)", "schedule <> '{}'::jsonb", "last_health", "group by", "left join lateral", "from source_scans", "limit 1", "c.next_refresh_at"} {
if !strings.Contains(query, required) {
t.Errorf("source connection query missing %q", required)
}
}
for _, forbidden := range []string{"base_url", "secret_ref"} {
if strings.Contains(query, forbidden) {
t.Errorf("source connection query selected sensitive field %q", forbidden)
}
}
}
type sourceConnectionRow struct {
id string
sourceType string
name string
enabled bool
scheduleConfigured bool
health string
itemCount int
updatedAt time.Time
nextRefreshAt *time.Time
lastScan bool
}
func (row sourceConnectionRow) Scan(dest ...any) error {
*(dest[0].(*string)) = row.id
*(dest[1].(*string)) = row.sourceType
*(dest[2].(*string)) = row.name
*(dest[3].(*bool)) = row.enabled
*(dest[4].(*bool)) = row.scheduleConfigured
*(dest[5].(*string)) = row.health
*(dest[6].(*int)) = row.itemCount
*(dest[7].(*time.Time)) = row.updatedAt
// V1: next_refresh_at pointer, present only for scheduled sources.
*(dest[8].(**time.Time)) = row.nextRefreshAt
if !row.lastScan {
for index := 9; index <= 20; index++ {
switch pointer := dest[index].(type) {
case **string:
*pointer = nil
case **int:
*pointer = nil
case **time.Time:
*pointer = nil
}
}
return nil
}
id, state, errorCode := "scan-1", "completed", ""
observed, unchanged, added, updated, overLimit, missing, planned := 613, 559, 0, 0, 54, 0, 0
createdAt, completedAt := row.updatedAt.Add(-time.Second), row.updatedAt
*(dest[9].(**string)) = &id
*(dest[10].(**string)) = &state
for index, value := range []*int{&observed, &unchanged, &added, &updated, &overLimit, &missing, &planned} {
*(dest[11+index].(**int)) = value
}
*(dest[18].(**string)) = &errorCode
*(dest[19].(**time.Time)) = &createdAt
*(dest[20].(**time.Time)) = &completedAt
return nil
}
type sourceConnectionRows struct {
items []sourceConnectionRow
index int
}
func (rows *sourceConnectionRows) Close() {}
func (rows *sourceConnectionRows) Err() error { return nil }
func (rows *sourceConnectionRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} }
func (rows *sourceConnectionRows) FieldDescriptions() []pgconn.FieldDescription { return nil }
func (rows *sourceConnectionRows) Next() bool {
if rows.index >= len(rows.items) {
return false
}
rows.index++
return true
}
func (rows *sourceConnectionRows) Scan(dest ...any) error {
return rows.items[rows.index-1].Scan(dest...)
}
func (rows *sourceConnectionRows) Values() ([]any, error) { return nil, errors.New("not implemented") }
func (rows *sourceConnectionRows) RawValues() [][]byte { return nil }
func (rows *sourceConnectionRows) Conn() *pgx.Conn { return nil }