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 干净
133 lines
5.1 KiB
Go
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 }
|