Files
knowledge-governance/internal/platform/postgres/ui_settings_test.go
T
姚章浩 63f3b2c503 feat(governance): policy-driven ingestion from catalog annotations
Annotation in 来源与目录 is now the only import decision surface:
- migration 000033: catalog_import_intents (durable, rescan-proof per-file
  intent with locator snapshot + fetch lifecycle), source_items.policy_state,
  planned publication state, ui_setting_revisions.ingestion_concurrency
- marking fans a folder decision out to per-file intents, materializes
  versioned items, reuses already-verified local bytes (compare, don't pull),
  and plans one publication per routed KB; blocked retires planned rows
  while live publications stay untouched (需下架 shows in the KB diff)
- artifact.fetch worker lane (scripts/artifact_fetch.py): DingTalk export
  tree/live box API, AList /api/fs/get direct download with SSO-redirect
  fail-closed, PP ledger via locator hash; content-level dedupe on sha256
- 开始处理 promotes planned->pending inside the claim-time concurrency budget
  from system settings; claims enforce it in SQL
- monitor overview rebuilt as knowledge-base diff: planned/needs_removal per
  KB + fetch pipeline summary; frontend lanes 待入库/需下架 replace the
  deprecated snapshot change-set confirmation
- spool: images (png/jpg/gif/bmp/webp) accepted, ppt/pptx rejected in favour
  of PDF twins; DingTalk PPT resolves its converted PDF pair
2026-09-30 10:54:07 +08:00

68 lines
3.2 KiB
Go

package postgres
import (
"context"
"errors"
"strings"
"testing"
"time"
"knowledge-governance/internal/governance"
)
func TestUISettingsCandidateAndActiveProjectionAreVersioned(t *testing.T) {
now := time.Date(2026, 9, 10, 6, 0, 0, 0, time.UTC)
row := uiSettingsRow{id: "settings-safe", state: "validated", documentRefresh: 30, resourceRefresh: 60, stalledAfter: 3600, finalizingAfter: 14400, updatedBy: "engineer-1", createdAt: now}
db := &recordingDB{row: row}
candidate, err := NewStore(db).CreateUISettingsCandidate(context.Background(), governance.CreateUISettingsCandidateRequest{DocumentRefreshSeconds: 30, ResourceRefreshSeconds: 60, StalledAfterSeconds: 3600, FinalizingAfterSeconds: 14400, IngestionConcurrency: 2, UpdatedBy: "engineer-1", Now: now})
if err != nil {
t.Fatal(err)
}
if candidate.State != "validated" || candidate.DocumentRefreshSeconds != 30 || candidate.UpdatedBy != "engineer-1" {
t.Fatalf("candidate=%#v", candidate)
}
sql := strings.ToLower(db.queryRowSQL)
for _, required := range []string{"insert into ui_setting_revisions", "'validated'", "returning"} {
if !strings.Contains(sql, required) {
t.Fatalf("candidate SQL missing %q", required)
}
}
for _, forbidden := range []string{"secret", "token", "password", "api_key"} {
if strings.Contains(sql, forbidden) {
t.Fatalf("candidate SQL contains forbidden field %q", forbidden)
}
}
db.row = uiSettingsRow{id: "settings-active", state: "active", documentRefresh: 15, resourceRefresh: 30, stalledAfter: 1800, finalizingAfter: 7200, updatedBy: "system:migration", createdAt: now, activatedAt: &now}
active, err := NewStore(db).GetActiveUISettings(context.Background())
if err != nil || active.State != "active" || active.FinalizingAfterSeconds != 7200 {
t.Fatalf("active=%#v error=%v", active, err)
}
if !strings.Contains(strings.ToLower(db.queryRowSQL), "where state = 'active'") {
t.Fatal("active settings query must select only active revision")
}
}
func TestUISettingsRejectInvalidCandidate(t *testing.T) {
_, err := NewStore(&recordingDB{}).CreateUISettingsCandidate(context.Background(), governance.CreateUISettingsCandidateRequest{DocumentRefreshSeconds: 31, ResourceRefreshSeconds: 60, StalledAfterSeconds: 3600, FinalizingAfterSeconds: 14400, UpdatedBy: "engineer-1", Now: time.Now()})
if !errors.Is(err, ErrInvalidUISettings) {
t.Fatalf("error=%v", err)
}
}
type uiSettingsRow struct {
id, state, updatedBy, errorCode string
documentRefresh, resourceRefresh, stalledAfter, finalizingAfter int
ingestionConcurrency int
createdAt time.Time
activatedAt, retiredAt *time.Time
}
func (row uiSettingsRow) Scan(dest ...any) error {
*dest[0].(*string), *dest[1].(*string) = row.id, row.state
*dest[2].(*int), *dest[3].(*int), *dest[4].(*int), *dest[5].(*int) = row.documentRefresh, row.resourceRefresh, row.stalledAfter, row.finalizingAfter
*dest[6].(*int) = row.ingestionConcurrency
*dest[7].(*string), *dest[8].(*time.Time), *dest[9].(**time.Time), *dest[10].(**time.Time), *dest[11].(*string) = row.updatedBy, row.createdAt, row.activatedAt, row.retiredAt, row.errorCode
return nil
}