6e67034fbd
build / build (push) Successful in 1m10s
以治理库单一只读事务分开聚合累计、自然周、截止月及当前通讯录使用分母;浏览器不再读取全量长答案来构建看板。 新增迁移52与普通worker周质量冻结,保留真实冻结时点、规则和标量贡献;缺少当时快照留空。提交后冻结头与贡献不可修改、删除或追加;只读接口保留管理员、固定租户、严格JSON与输入冲突保护。 已验证:make verify退出0,686个Go用例和305个前端用例通过,必需跳过0;镜像与组织隔离PostgreSQL竞态回归退出0。真实治理副本浏览器核对第41周累计1885与新增471、十一项和完整PNG下载,390px无外溢、JS异常0;明确模拟503后不伪零且重试恢复。没有修改DobotAI原始问答、真实区域规则或生产发布门禁。
236 lines
9.4 KiB
Go
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")
|
|
}
|
|
}
|