Files
knowledge-governance/internal/qaopsorg/store_integration_test.go
T
姚章浩 6e67034fbd
build / build (push) Successful in 1m10s
feat(qa-ops): 交付十一项统计看板与完整图片导出
以治理库单一只读事务分开聚合累计、自然周、截止月及当前通讯录使用分母;浏览器不再读取全量长答案来构建看板。

新增迁移52与普通worker周质量冻结,保留真实冻结时点、规则和标量贡献;缺少当时快照留空。提交后冻结头与贡献不可修改、删除或追加;只读接口保留管理员、固定租户、严格JSON与输入冲突保护。

已验证:make verify退出0,686个Go用例和305个前端用例通过,必需跳过0;镜像与组织隔离PostgreSQL竞态回归退出0。真实治理副本浏览器核对第41周累计1885与新增471、十一项和完整PNG下载,390px无外溢、JS异常0;明确模拟503后不伪零且重试恢复。没有修改DobotAI原始问答、真实区域规则或生产发布门禁。
2026-10-18 23:26:55 +08:00

236 lines
9.4 KiB
Go

package qaopsorg_test
import (
"context"
"errors"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
postgresstore "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/qaopsorg"
)
func TestOrganizationStoreRoundTripAndOptimisticConflict(t *testing.T) {
dsn := os.Getenv("KG_QAOPS_ORG_TEST_DSN")
if dsn == "" {
t.Skip("explicit isolated organization database is not configured")
}
u, err := url.Parse(dsn)
if err != nil || u.Hostname() != "127.0.0.1" || !strings.HasPrefix(u.Path, "/kg_qaops_org_test_") || os.Getenv("KG_QAOPS_ORG_TEST_ISOLATED") != "1" {
t.Fatal("refusing non-isolated organization database")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal("test database connection failed")
}
defer pool.Close()
if err = postgresstore.ApplyMigrations(ctx, pool); err != nil {
t.Fatal(err)
}
store := qaopsorg.NewStore(pool, "fixture-tenant")
empty, err := store.GetView(ctx)
if err != nil || empty.State != "empty" {
t.Fatalf("empty state must be explicit: %+v %v", empty, err)
}
input := qaopsorg.Import{Source: "weekly_report_snapshot", SyncedAt: time.Date(2026, 9, 27, 14, 3, 44, 0, time.UTC), Complete: true, Directory: qaopsorg.Directory{
Departments: []qaopsorg.Department{{ID: "root", Name: "示例公司"}, {ID: "a", ParentID: "root", Name: "国内组"}, {ID: "b", ParentID: "a", Name: "海外组"}},
People: []qaopsorg.Person{{ID: "u1", DepartmentIDs: []string{"a"}}, {ID: "u2", DepartmentIDs: []string{"b"}}},
}}
imported, err := store.ReplaceDirectory(ctx, input)
if err != nil || imported.DirectoryID == "" || imported.View.TotalPeople != 2 {
t.Fatalf("import failed: %+v %v", imported, err)
}
request := qaopsorg.SaveRequest{DirectoryID: imported.DirectoryID, Rules: []qaopsorg.Rule{{DepartmentID: "a", Region: "国内工业"}, {DepartmentID: "b", Region: "APAC"}}}
preview, err := store.PreviewRules(ctx, request)
if err != nil || preview.View.Regions["APAC"] != 1 {
t.Fatalf("preview failed: %+v %v", preview, err)
}
before, _ := store.GetView(ctx)
if before.RevisionID != "" {
t.Fatal("preview must not persist changes")
}
saved, err := store.SaveRules(ctx, request, "fixture-admin")
if err != nil || saved.RevisionID == "" || saved.UpdatedBy != "fixture-admin" {
t.Fatalf("save failed: %+v %v", saved, err)
}
read, err := store.GetView(ctx)
if err != nil || read.RevisionID != saved.RevisionID || read.View.Regions["国内工业"] != 1 {
t.Fatal("saved rules not consumed by read path")
}
projection, projectionErr := store.GetAttribution(ctx)
if projectionErr != nil || projection.Metadata.DirectoryID != read.DirectoryID || projection.Metadata.RevisionID != read.RevisionID || projection.Resolve("u2", "").Region != "APAC" {
t.Fatal("attribution did not consume the saved directory/region revision")
}
if _, err = store.SaveRules(ctx, request, "stale-admin"); !errors.Is(err, qaopsorg.ErrConflict) {
t.Fatalf("stale update overwrote newer rules: %v", err)
}
request.BaseRevision = saved.RevisionID
again, err := store.SaveRules(ctx, request, "fixture-admin")
if err != nil || again.RevisionID != saved.RevisionID {
t.Fatal("unchanged save must not append duplicate revisions")
}
broken := input
broken.Complete = false
if _, err = store.ReplaceDirectory(ctx, broken); !errors.Is(err, qaopsorg.ErrInvalid) {
t.Fatal("partial sync activated")
}
request.Rules = []qaopsorg.Rule{{DepartmentID: "absent", Region: "EMEA"}}
if _, err = store.SaveRules(ctx, request, "fixture-admin"); !errors.Is(err, qaopsorg.ErrInvalid) {
t.Fatal("invalid rule persisted")
}
after, _ := store.GetView(ctx)
if after.RevisionID != saved.RevisionID {
t.Fatal("failed writes changed active revision")
}
other, _ := qaopsorg.NewStore(pool, "another-tenant").GetView(ctx)
if other.State != "empty" {
t.Fatal("directory leaked across tenants")
}
input.Directory.Departments = input.Directory.Departments[:2]
input.Directory.People = input.Directory.People[:1]
input.SyncedAt = input.SyncedAt.Add(time.Hour)
newer, err := store.ReplaceDirectory(ctx, input)
if err != nil || newer.RevisionID != saved.RevisionID || len(newer.View.MissingDepartments) != 1 {
t.Fatalf("directory refresh deleted rules or hid missing department: %+v %v", newer, err)
}
if _, err = store.PreviewRules(ctx, request); !errors.Is(err, qaopsorg.ErrConflict) {
t.Fatal("preview accepted an obsolete directory snapshot")
}
older := input
start := make(chan struct{})
results := make(chan error, 2)
for _, region := range []string{"APAC", "EMEA"} {
go func(region string) {
<-start
_, failure := store.SaveRules(ctx, qaopsorg.SaveRequest{DirectoryID: newer.DirectoryID, BaseRevision: newer.RevisionID, Rules: []qaopsorg.Rule{{DepartmentID: "a", Region: region}}}, "concurrent-admin")
results <- failure
}(region)
}
close(start)
successes, conflicts := 0, 0
for range 2 {
failure := <-results
if failure == nil {
successes++
} else if errors.Is(failure, qaopsorg.ErrConflict) {
conflicts++
} else {
t.Fatal(failure)
}
}
if successes != 1 || conflicts != 1 {
t.Fatalf("concurrent stale writes were not serialized: success=%d conflict=%d", successes, conflicts)
}
older.SyncedAt = older.SyncedAt.Add(-2 * time.Hour)
if _, err = store.ReplaceDirectory(ctx, older); !errors.Is(err, qaopsorg.ErrConflict) {
t.Fatalf("older directory rolled back the active roster: %v", err)
}
var revisions int
if err = pool.QueryRow(ctx, "SELECT count(*) FROM qa_ops_region_revisions WHERE tenant_key=$1", "fixture-tenant").Scan(&revisions); err != nil || revisions != 2 {
t.Fatal("revision history changed unexpectedly")
}
t.Run("custom catalog persists with the mapping and rename advances revision", func(t *testing.T) {
custom := qaopsorg.NewStore(pool, "catalog-fixture")
imported, e := custom.ReplaceDirectory(ctx, input)
if e != nil {
t.Fatal(e)
}
request := qaopsorg.SaveRequest{DirectoryID: imported.DirectoryID, Regions: []qaopsorg.RegionDefinition{{ID: "education", Name: "国内教育"}}, Rules: []qaopsorg.Rule{{DepartmentID: "a", Region: "education"}}}
preview, e := custom.PreviewRules(ctx, request)
if e != nil || preview.View.Regions["education"] != 1 {
t.Fatal("custom preview failed", e)
}
unchanged, _ := custom.GetView(ctx)
if unchanged.RevisionID != "" {
t.Fatal("catalog preview wrote data")
}
saved, e := custom.SaveRules(ctx, request, "catalog-admin")
if e != nil {
t.Fatal(e)
}
fresh, e := pgxpool.New(ctx, dsn)
if e != nil {
t.Fatal(e)
}
defer fresh.Close()
read, e := qaopsorg.NewStore(fresh, "catalog-fixture").GetView(ctx)
if e != nil || len(read.Regions) != 1 || read.Regions[0].Name != "国内教育" || read.Rules[0].Region != "education" {
t.Fatal("catalog was not persisted with its rules", e)
}
request.BaseRevision = saved.RevisionID
request.Regions[0].Name = "国外工业Amercia"
renamed, e := custom.SaveRules(ctx, request, "catalog-admin")
if e != nil || renamed.RevisionID == saved.RevisionID {
t.Fatal("rename did not create an atomic revision", e)
}
projection, e := custom.GetAttribution(ctx)
if e != nil || projection.Resolve("u1", "").Region != "国外工业Amercia" {
t.Fatal("renamed labels not shared by the read paths")
}
if _, e = custom.SaveRules(ctx, request, "stale-admin"); !errors.Is(e, qaopsorg.ErrConflict) {
t.Fatal("rename allowed stale overwrite")
}
request.BaseRevision = renamed.RevisionID
repeated, e := custom.SaveRules(ctx, request, "catalog-admin")
if e != nil || repeated.RevisionID != renamed.RevisionID {
t.Fatal("unchanged catalog appended another revision")
}
// Pin the catalog down test to migration 51, not a later additive feature.
for {
var latest int64
if err := pool.QueryRow(ctx, "SELECT max(version) FROM schema_migrations").Scan(&latest); err != nil {
t.Fatal(err)
}
if latest <= 51 {
break
}
if err := postgresstore.RollbackLatestMigration(ctx, pool); err != nil {
t.Fatal(err)
}
}
if e = postgresstore.RollbackLatestMigration(ctx, pool); e == nil {
t.Fatal("down migration discarded a custom catalog")
}
if _, e = custom.GetView(ctx); e != nil {
t.Fatal("refused down migration damaged last-good catalog")
}
for _, table := range []string{"qa_ops_organization_state", "qa_ops_region_revisions", "qa_ops_directory_snapshots"} {
if _, e = pool.Exec(ctx, "DELETE FROM "+table+" WHERE tenant_key=$1", "catalog-fixture"); e != nil {
t.Fatal(e)
}
}
})
// This test exercises migration 48, not whichever unrelated migration is latest.
for {
var latest int64
if pool.QueryRow(ctx, "SELECT max(version) FROM schema_migrations").Scan(&latest) != nil {
t.Fatal("migration version unavailable")
}
if latest <= 48 {
break
}
if postgresstore.RollbackLatestMigration(ctx, pool) != nil {
t.Fatal("could not reach organization migration")
}
}
if err = postgresstore.RollbackLatestMigration(ctx, pool); err != nil {
t.Fatal(err)
}
var removed bool
if err = pool.QueryRow(ctx, "SELECT to_regclass('qa_ops_organization_state') IS NULL AND to_regclass('qa_ops_region_revisions') IS NULL AND to_regclass('qa_ops_directory_snapshots') IS NULL").Scan(&removed); err != nil || !removed {
t.Fatal("organization down migration left residue")
}
if err = postgresstore.ApplyMigrations(ctx, pool); err != nil {
t.Fatal(err)
}
if state, failure := store.GetView(ctx); failure != nil || state.State != "empty" {
t.Fatal("organization migration could not be reapplied")
}
}