Files
member-console/internal/entitlements/cap_timing_test.go
T
cgalo5758 3727ff31d8 Add entitlement set rule change ledger and preview flow
Add an append-only ledger of entitlement set rule changes with per-pool
effect rows, a preview-and-commit rule change flow, and an automatic
drain that settles deferred recomputations. Rules gain a tier reduction
policy, resource keys declare over-limit behavior, and the materializer
now lowers limits when a rule stops applying.
Add entitlement set rule change ledger and preview flow

Add an append-only ledger of entitlement set rule changes with a
preview-and-commit operator flow. Rule writes now go through an enclosed
`core.commit_rule_change` function that files an act row and one
obligation per carrying pool, with a drain workflow settling deferred
recomputations. The preview dry-runs the materializer with a rule
overlay and renders per-pool buckets, reduction-policy disclosures, and
provider over-limit consequences. Materializing transactions take a
shared advisory rendezvous that rule changes hold exclusively, enforced
by a possession assertion. Add History and Entitlement changes surfaces,
a rule-less warning on five product-selection surfaces, and a
`tier_reduction_policy` column that gates FedWiki parking.
2026-09-15 03:53:28 -05:00

111 lines
3.8 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package entitlements_test
import (
"context"
"database/sql"
"os"
"testing"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"github.com/google/uuid"
)
// TestTimedSyncCap is the timed run of task 11.2, which sets
// rematerializeSyncCap. It seeds a population of carrying pools in steps and
// times one atomic-path commit and one exact preview at each size, so the
// cap can be read off the size whose commit still holds the exclusive
// rendezvous for an acceptable time. It seeds hundreds of organizations and
// runs only when ENTITLEMENT_CAP_TIMING is set:
//
// ENTITLEMENT_CAP_TIMING=1 go test -run TestTimedSyncCap -v ./internal/entitlements/
func TestTimedSyncCap(t *testing.T) {
if os.Getenv("ENTITLEMENT_CAP_TIMING") == "" {
t.Skip("ENTITLEMENT_CAP_TIMING not set; this is the task 11.2 timed run, not a suite test")
}
database := testDB(t)
ctx := context.Background()
setupTx, err := entitlements.BeginRuleChange(ctx, database)
if err != nil {
t.Fatal(err)
}
product := createTestProduct(t, ctx, setupTx, "Cap timing "+uuid.New().String()[:8], 6, false)
rule := firstActiveRule(t, ctx, entitlements.New(setupTx), product.setID)
if err := setupTx.Commit(); err != nil {
t.Fatalf("commit product: %v", err)
}
sizes := []int{50, 100, 200, 400, 800}
seeded := 0
value := int64(6)
t.Logf("%8s %12s %12s %12s", "pools", "commit", "per pool", "preview")
for _, size := range sizes {
seedCarryingPools(t, ctx, database, product.productID, size-seeded)
seeded = size
previewStart := time.Now()
preview, err := entitlements.RuleChangePreviewDB(ctx, database, entitlements.RuleChangeInput{
SetID: product.setID,
ChangeKind: entitlements.ChangeKindRuleModified,
RuleID: rule.RuleID,
Rule: entitlements.RuleFields{ResourceValue: sql.NullInt64{Int64: value + 1, Valid: true}},
ActorType: entitlements.ActorTypeSystem,
SyncCap: int32(size),
})
previewTook := time.Since(previewStart)
if err != nil {
t.Fatalf("preview at %d pools: %v", size, err)
}
if !preview.Exact || preview.PoolCount != int64(size) {
t.Fatalf("preview at %d pools: exact=%v pool_count=%d", size, preview.Exact, preview.PoolCount)
}
value++
commitStart := time.Now()
result, err := entitlements.RuleChangeCommit(ctx, database, entitlements.RuleChangeInput{
SetID: product.setID,
ChangeKind: entitlements.ChangeKindRuleModified,
RuleID: rule.RuleID,
Rule: entitlements.RuleFields{ResourceValue: sql.NullInt64{Int64: value, Valid: true}},
ActorType: entitlements.ActorTypeSystem,
SyncCap: int32(size),
})
commitTook := time.Since(commitStart)
if err != nil {
t.Fatalf("commit at %d pools: %v", size, err)
}
if result.SyncPath != "atomic" || result.Recomputed != size {
t.Fatalf("commit at %d pools took path %q with %d recomputed", size, result.SyncPath, result.Recomputed)
}
t.Logf("%8d %12s %12s %12s", size, commitTook.Round(time.Millisecond),
(commitTook / time.Duration(size)).Round(10*time.Microsecond), previewTook.Round(time.Millisecond))
}
}
// seedCarryingPools adds n organizations whose default pools carry the
// product, in batches so one transaction never holds hundreds of pool
// materializations.
func seedCarryingPools(t *testing.T, ctx context.Context, database *sql.DB, productID string, n int) {
t.Helper()
const batch = 50
for done := 0; done < n; {
tx, err := entitlements.BeginMaterializing(ctx, database, nil)
if err != nil {
t.Fatal(err)
}
q := entitlements.New(tx)
for i := 0; i < batch && done < n; i++ {
org := setupTestOrg(t, ctx, tx)
createGrantAndProvision(t, ctx, tx, q, org, productID, 1)
done++
}
if err := tx.Commit(); err != nil {
t.Fatalf("commit seed batch: %v", err)
}
}
}