Fix four FedWiki defects in the code #165 rewrites

- The three active and force-reduced site lists end on site_id, so two
  sites that tie on every other column come back in one order, and the
  sweep parks the same one every pass (#225).
- The quota check answers "no entitlement" only when the pool has no
  entitlement row; any other read failure is returned, and the create
  ends failed with quota_unavailable (#231). The nilerr exception goes.
- The JSON create checks the assembled name's length against the farm
  domain, as the form does.
- A form create whose repeat lookup fails is refused with the address
  kept, instead of going on as a first create.
This commit is contained in:
2026-10-21 23:40:06 -04:00
parent 354a51aaf7
commit 9027c0b1c0
10 changed files with 324 additions and 21 deletions
-4
View File
@@ -56,10 +56,6 @@ linters:
- path: '^internal/integrations/fedwiki/workflows/sync\.go$'
linters: [gocognit]
text: 'cognitive complexity 35 of func `\(\*Activities\)\.SyncSitesToDBActivity`' # #165
# #231: the text also matches the function's last return, which nilerr does not report.
- path: '^internal/integrations/fedwiki/workflows/activities\.go$'
linters: [nilerr]
source: 'return &CheckQuotaOutput\{$' # #231
# #205: each rule pins the calls on one line of source text; the ladder_id text is on two lines, one in each of PostKeep and PostCancel.
- path: '^internal/server/billing\.go$'
linters: [forbidigo]
@@ -151,7 +151,7 @@ WHERE domain = $1;
-- Active sites, most-recently-modified first — the force_reduce keep/park order.
SELECT * FROM fedwiki.sites
WHERE workspace_id = $1 AND status = 'active'
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC;
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC, site_id DESC;
-- name: ListActiveSitesByPool :many
-- The active sites of every workspace whose active primary assignment is the
@@ -162,7 +162,7 @@ WHERE status = 'active' AND workspace_id IN (
SELECT pa.workspace_id FROM core.pool_assignments pa
WHERE pa.pool_id = sqlc.arg(pool_id)::uuid AND pa.is_primary = TRUE AND pa.status = 'active'
)
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC;
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC, site_id DESC;
-- name: ListForceReducedSitesByPool :many
-- Sites a downgrade parked that are still read-only, in every workspace
@@ -174,7 +174,7 @@ WHERE force_reduced_at IS NOT NULL AND status = 'readonly' AND workspace_id IN (
SELECT pa.workspace_id FROM core.pool_assignments pa
WHERE pa.pool_id = sqlc.arg(pool_id)::uuid AND pa.is_primary = TRUE AND pa.status = 'active'
)
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC;
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC, site_id DESC;
-- name: CountActiveSitesByPool :one
-- The active sites of every workspace whose active primary assignment is the
@@ -291,7 +291,7 @@ WHERE status = 'active' AND workspace_id IN (
SELECT pa.workspace_id FROM core.pool_assignments pa
WHERE pa.pool_id = $1::uuid AND pa.is_primary = TRUE AND pa.status = 'active'
)
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC, site_id DESC
`
// The active sites of every workspace whose active primary assignment is the
@@ -334,7 +334,7 @@ func (q *Queries) ListActiveSitesByPool(ctx context.Context, poolID string) ([]S
const listActiveSitesByWorkspace = `-- name: ListActiveSitesByWorkspace :many
SELECT site_id, workspace_id, domain, created_at, updated_at, status, storage_bytes, last_modified_at, force_reduced_at, archived_at FROM fedwiki.sites
WHERE workspace_id = $1 AND status = 'active'
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC, site_id DESC
`
// Active sites, most-recently-modified first — the force_reduce keep/park order.
@@ -626,7 +626,7 @@ WHERE force_reduced_at IS NOT NULL AND status = 'readonly' AND workspace_id IN (
SELECT pa.workspace_id FROM core.pool_assignments pa
WHERE pa.pool_id = $1::uuid AND pa.is_primary = TRUE AND pa.status = 'active'
)
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC
ORDER BY last_modified_at DESC NULLS LAST, created_at DESC, site_id DESC
`
// Sites a downgrade parked that are still read-only, in every workspace
+16 -1
View File
@@ -276,6 +276,21 @@ func sameCreate(held integration.LifecycleRequest, personID string, params reque
(p.IsCustomDomain == nil || *p.IsCustomDomain == *params.IsCustomDomain)
}
// validateHostedName is the refusal of a hosted label, or "" when it stands:
// the label's own rules, then the length of the name it makes under the first
// allowed farm domain, the parent the JSON create always resolves under
// (RFC 1035 §2.3.4). With no allowed domain it refuses
// nothing, because the resolve that follows answers that case.
func validateHostedName(label string, allowed []string) string {
if errMsg := dnsname.ValidateDNSLabel(label); errMsg != "" {
return errMsg
}
if len(allowed) == 0 {
return ""
}
return dnsname.ValidateFQDN(label, allowed[0])
}
// CreateSite handles POST /api/fedwiki/sites - initiates site creation workflow.
func (h *FedWikiHandler) CreateSite(w http.ResponseWriter, r *http.Request) {
session, err := h.getSessionFromRequest(r)
@@ -396,7 +411,7 @@ func (h *FedWikiHandler) CreateSite(w http.ResponseWriter, r *http.Request) {
return
}
// dispositionOwnActive falls through to the create path below.
} else if errMsg := dnsname.ValidateDNSLabel(req.Domain); errMsg != "" {
} else if errMsg := validateHostedName(req.Domain, h.FedWikiAllowedDomains); errMsg != "" {
// Hosted path only: a custom domain is a full FQDN, not a label.
respondError(w, http.StatusBadRequest, errMsg, "")
return
@@ -388,6 +388,48 @@ func TestCreateRepeatTable(t *testing.T) {
}
}
// A create whose repeat lookup fails is refused with the form-level
// sentence and its address kept. The registry is a separate handle, so with
// only the handler's database closed the registry would still answer the
// placed name as taken; the sentence shows the lookup's failure was what
// answered. The same submission with the lookup healthy is accepted.
func TestCreateRepeatLookupFailureRefusesTheCreate(t *testing.T) {
e := newSiteEnv(t, 5)
domain := e.verifiedClaim("lookupfail")
instance, gen := e.createForm()
if rec := e.submitCustomCreate(domain, instance, gen); rec.Code != http.StatusOK || rec.Body.Len() != 0 {
t.Fatalf("custom create = %d %q, want accepted", rec.Code, rec.Body.String())
}
// What the workflow does before the farm call.
if err := workflows.AllocateSiteDomain(e.ctx, e.registry, e.ws, domain, instance); err != nil {
t.Fatal(err)
}
healthy := e.h.Database
e.h.Database = dbtest.Closed(t)
failed := e.submitCustomCreate(domain, instance, gen)
e.h.Database = healthy
if failed.Code != http.StatusUnprocessableEntity || !strings.Contains(failed.Body.String(), "Failed to initiate site creation") {
t.Fatalf("failed lookup = %d %s, want 422 with the form-level sentence", failed.Code, failed.Body.String())
}
if !strings.Contains(failed.Body.String(), domain) {
t.Errorf("the refusal did not keep the typed name")
}
if gotInstance, gotGen := formAddress(t, failed.Body.String()); gotInstance != instance || gotGen != gen {
t.Errorf("the refusal's address = (%s, %d), want the submitted (%s, %d)", gotInstance, gotGen, instance, gen)
}
if e.requestCount(instance) != 1 || len(e.temporal.starts()) != 1 {
t.Errorf("a refused repeat left %d requests and %d workflows, want 1 and 1", e.requestCount(instance), len(e.temporal.starts()))
}
if ok := e.submitCustomCreate(domain, instance, gen); ok.Code != http.StatusOK || !strings.Contains(ok.Header().Get("HX-Trigger"), "fedwikiCreateAccepted") {
t.Errorf("healthy lookup = %d %s, want accepted", ok.Code, ok.Body.String())
}
if e.requestCount(instance) != 1 || len(e.temporal.starts()) != 1 {
t.Errorf("the healthy repeat left %d requests and %d workflows, want 1 and 1", e.requestCount(instance), len(e.temporal.starts()))
}
}
// TestCreateConcurrentDoubleSubmit sends one form's submission twice at once.
func TestCreateConcurrentDoubleSubmit(t *testing.T) {
e := newSiteEnv(t, 5)
@@ -1065,6 +1107,10 @@ func TestNoticeSentences(t *testing.T) {
"recipes.sites.localhost wasn't created. A site with this name already exists."; got != want {
t.Errorf("noticeSentence = %q, want %q", got, want)
}
if got, want := noticeSentence("recipes.sites.localhost", "created", workflows.ReasonQuotaUnavailable),
"recipes.sites.localhost wasn't created. Couldn't check your site limit. Try again later."; got != want {
t.Errorf("noticeSentence = %q, want %q", got, want)
}
}
// TestNoticesPerAction: each row action's declined or failed request is a
@@ -1256,6 +1302,48 @@ func TestAPICreatePolling(t *testing.T) {
}
}
// The JSON create runs the assembled-name length check the form runs: a
// label whose name under the first farm domain passes 253 characters is
// refused before anything is recorded or started, and one that fits is
// accepted under the same long farm domain.
func TestAPICreateRefusesAnAssembledNameOverTheLimit(t *testing.T) {
e := newSiteEnv(t, 5)
long := strings.Join([]string{strings.Repeat("a", 63), strings.Repeat("b", 63), strings.Repeat("c", 63), strings.Repeat("d", 8)}, ".")
if len(long) != 200 {
t.Fatalf("test fixture: farm domain is %d characters, want 200", len(long))
}
e.api.FedWikiAllowedDomains = []string{long}
requests := func() (n int) {
if err := e.db.QueryRowContext(e.ctx,
`SELECT count(*) FROM core.lifecycle_requests WHERE workspace_id = $1`, e.ws).Scan(&n); err != nil {
t.Fatal(err)
}
return n
}
label := strings.Repeat("e", 60)
refused := e.apiCreate(map[string]any{"domain": label})
if refused.Code != http.StatusBadRequest {
t.Fatalf("long name = %d %s, want 400", refused.Code, refused.Body.String())
}
if want := "The full domain name (" + label + "." + long + ") exceeds the 253-character limit"; !strings.Contains(refused.Body.String(), want) {
t.Errorf("body = %s, want it to say %q", refused.Body.String(), want)
}
if n := requests(); n != 0 {
t.Errorf("a refused create recorded %d requests", n)
}
if starts := e.temporal.starts(); len(starts) != 0 {
t.Errorf("a refused create started %v", starts)
}
if ok := e.apiCreate(map[string]any{"domain": "fits" + e.uniq}); ok.Code != http.StatusAccepted {
t.Errorf("a name that fits = %d %s, want 202", ok.Code, ok.Body.String())
}
if n := requests(); n != 1 {
t.Errorf("requests after the accepted create = %d, want 1", n)
}
}
// A client that repeats an unanswered create sends the same instance twice
// at once; both lookups can miss before either records. The second is a
// repeat of the first, so both are answered 202.
@@ -234,7 +234,8 @@ func (h *FedWikiPartialsHandler) CreateSite(w http.ResponseWriter, r *http.Reque
// in the caller's workspace: accepted when it is the same create, and the
// stale-form answer under a fresh address otherwise. It reports whether it
// answered; a form without an address, or an address that holds nothing the
// caller may see, goes on to the checks and the recording.
// caller may see, goes on to the checks and the recording. A lookup that
// fails answers the form-level refusal with the address kept.
func (h *FedWikiPartialsHandler) answerCreateRepeat(w http.ResponseWriter, r *http.Request, session *auth.UserSession,
values forms.Values, errs *forms.Errors, domain, selectedDomain string, isCustomDomain bool) bool {
addr, ok := parseAddressValues(values.String("instance"), values.String("generation"))
@@ -243,9 +244,10 @@ func (h *FedWikiPartialsHandler) answerCreateRepeat(w http.ResponseWriter, r *ht
}
_, held, found, err := integration.LookupLifecycleRequest(r.Context(), h.Database, session.WorkspaceID, addr.Instance, addr.Generation)
if err != nil {
// The recording below reads the address again under its lock.
h.Logger.Warn("failed to look up a create request", slog.Any("error", err))
return false
h.Logger.Error("failed to look up a create request", slog.Any("error", err))
errs.Form("Failed to initiate site creation")
h.renderCreateFormRefusal(w, r, session.WorkspaceID, values, errs, false)
return true
}
if !found {
return false
@@ -407,7 +407,7 @@ func (a *Activities) CheckQuotaActivity(ctx context.Context, input CheckQuotaInp
PoolID: assignment.PoolID,
ResourceKey: "fedwiki_sites",
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// No entitlement means no quota
a.Logger.Info("no sites entitlement found",
slog.String("workspaceID", input.WorkspaceID))
@@ -417,6 +417,9 @@ func (a *Activities) CheckQuotaActivity(ctx context.Context, input CheckQuotaInp
CanCreate: false,
}, nil
}
if err != nil {
return nil, fmt.Errorf("failed to get sites entitlement: %w", err)
}
usage, err := a.EntitlementsQ.GetUsageByPoolAndResource(ctx, entitlements.GetUsageByPoolAndResourceParams{
PoolID: assignment.PoolID,
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package workflows
import (
"context"
"errors"
"testing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/entitlements"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
)
// entitlementReadFails is the entitlements querier whose entitlement read
// answers err; every other read is the real one.
type entitlementReadFails struct {
entitlements.Querier
err error
}
func (q entitlementReadFails) GetNumericEntitlementByPoolAndResource(context.Context, entitlements.GetNumericEntitlementByPoolAndResourceParams) (entitlements.NumericEntitlement, error) {
return entitlements.NumericEntitlement{}, q.err
}
// A pool with no sites entitlement has no quota: the check answers it as
// a refusal to create, not as an error (#231).
func TestCheckQuotaActivityAnswersNoEntitlement_DB(t *testing.T) {
e := newReqEnv(t, 5, 0)
bare := fedwikitest.New(t, e.db).Tenant("NoSites")
out, err := e.acts.CheckQuotaActivity(e.ctx, CheckQuotaInput{WorkspaceID: bare.Workspace.ID})
if err != nil {
t.Fatalf("check quota: %v", err)
}
if out.CanCreate || out.Quota != 0 || out.CurrentCount != 0 {
t.Errorf("check = %+v, want no quota and no create", out)
}
}
// An entitlement read that fails is not "no entitlement": the check returns
// the error (#231).
func TestCheckQuotaActivityReturnsAFailedEntitlementRead_DB(t *testing.T) {
e := newReqEnv(t, 5, 0)
boom := errors.New("entitlement read failed")
e.acts.EntitlementsQ = entitlementReadFails{Querier: e.acts.EntitlementsQ, err: boom}
out, err := e.acts.CheckQuotaActivity(e.ctx, CheckQuotaInput{WorkspaceID: e.ws})
if !errors.Is(err, boom) {
t.Fatalf("check quota = %+v, %v; want the read's error", out, err)
}
}
// A create whose quota check cannot read the entitlement ends failed with
// quota_unavailable, reserves no name and calls no farm.
func TestCreateRequestFailsWhenTheEntitlementReadFails_DB(t *testing.T) {
e := newReqEnv(t, 5, 0)
e.acts.EntitlementsQ = entitlementReadFails{Querier: e.acts.EntitlementsQ, err: errors.New("entitlement read failed")}
in := e.startCreate("unchecked")
env := e.testEnv()
env.ExecuteWorkflow(CreateFedWikiSiteRequestWorkflow, in)
out := runOutput(t, env)
if out.Status != "failed" || out.ReasonCode != ReasonQuotaUnavailable {
t.Errorf("output = %+v", out)
}
if status, reason := e.request(in.InstanceID, in.Generation); status != "failed" || reason != ReasonQuotaUnavailable {
t.Errorf("request = %s/%s", status, reason)
}
if e.placed(in.Domain) {
t.Error("a name was reserved for a create that could not check its limit")
}
if creates, _, _ := e.farm.Counts(); creates != 0 {
t.Errorf("the farm was called %d times", creates)
}
}
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package workflows
import (
"context"
"database/sql"
"fmt"
"slices"
"testing"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/fedwikitest"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
)
// tiedSites creates two sites in one workspace that share every sort column
// of FedWiki's keep-and-park lists (last_modified_at and created_at) and
// returns the workspace, its pool, and the two site ids newest-first, which
// is site_id DESC: the order the lists end on. Lowercase text order equals
// UUID byte order, and the ids are uuidv7, so id order is write order and
// the newest site comes first.
func tiedSites(t *testing.T, database *sql.DB, limit, used int64) (ws, poolID string, byIDDesc []string) {
t.Helper()
env := fedwikitest.New(t, database)
ws = env.Counted("Tester", limit, used).Workspace.ID
at := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
for i := range 2 {
domain := fmt.Sprintf("tie%d-%s.localtest.me", i, env.Sfx)
fedwikitest.SiteModifiedAt(t, database, ws, domain, at)
var id string
if err := database.QueryRowContext(context.Background(),
`UPDATE fedwiki.sites SET created_at = $2 WHERE domain = $1 RETURNING site_id::text`, domain, at).Scan(&id); err != nil {
t.Fatalf("tie created_at of %s: %v", domain, err)
}
byIDDesc = append(byIDDesc, id)
}
slices.Sort(byIDDesc)
slices.Reverse(byIDDesc)
if err := database.QueryRowContext(context.Background(), `
SELECT pool_id::text FROM core.pool_assignments
WHERE workspace_id = $1 AND is_primary AND status = 'active'`, ws).Scan(&poolID); err != nil {
t.Fatalf("pool of %s: %v", ws, err)
}
return ws, poolID, byIDDesc
}
func siteIDs(sites []fwmod.Site) []string {
ids := make([]string, len(sites))
for i, s := range sites {
ids[i] = s.SiteID
}
return ids
}
func siteStatusByID(t *testing.T, database *sql.DB, id string) string {
t.Helper()
var status string
if err := database.QueryRowContext(context.Background(),
`SELECT status FROM fedwiki.sites WHERE site_id = $1`, id).Scan(&status); err != nil {
t.Fatalf("read site %s: %v", id, err)
}
return status
}
// Two sites that tie on every other sort column come back from the three
// lists in site_id DESC order, so the sweep parks and reactivates the same
// site on every run (#225).
func TestKeepAndParkListsBreakTiesOnSiteID(t *testing.T) {
_, database := newReconcileTestEnv(t)
ws, poolID, want := tiedSites(t, database, 2, 2)
ctx := context.Background()
q := fwmod.New(database)
byWorkspace, err := q.ListActiveSitesByWorkspace(ctx, ws)
if err != nil {
t.Fatalf("list active by workspace: %v", err)
}
byPool, err := q.ListActiveSitesByPool(ctx, poolID)
if err != nil {
t.Fatalf("list active by pool: %v", err)
}
if _, err := database.ExecContext(ctx,
`UPDATE fedwiki.sites SET status = 'readonly', force_reduced_at = NOW() WHERE workspace_id = $1`, ws); err != nil {
t.Fatalf("park both: %v", err)
}
forceReduced, err := q.ListForceReducedSitesByPool(ctx, poolID)
if err != nil {
t.Fatalf("list force-reduced by pool: %v", err)
}
for name, got := range map[string][]fwmod.Site{
"ListActiveSitesByWorkspace": byWorkspace,
"ListActiveSitesByPool": byPool,
"ListForceReducedSitesByPool": forceReduced,
} {
if ids := siteIDs(got); !slices.Equal(ids, want) {
t.Errorf("%s = %v, want site_id DESC %v", name, ids, want)
}
}
}
// The sweep keeps the first site of the tied list and parks the last, which
// is the one with the lower site_id.
func TestSweepParksTheLowerSiteIDOfATie(t *testing.T) {
acts, database := newReconcileTestEnv(t)
ws, _, byIDDesc := tiedSites(t, database, 1, 2)
governFedwikiLimit(t, database, ws, "force_reduce")
out, err := acts.ReconcileFedWikiQuotaActivity(context.Background(), ReconcileFedWikiQuotaInput{WorkspaceID: ws})
if err != nil {
t.Fatalf("sweep: %v", err)
}
if out.Parked != 1 {
t.Fatalf("parked = %d, want 1", out.Parked)
}
if got := siteStatusByID(t, database, byIDDesc[0]); got != "active" {
t.Errorf("higher site_id %s is %s, want active", byIDDesc[0], got)
}
if got := siteStatusByID(t, database, byIDDesc[1]); got != "readonly" {
t.Errorf("lower site_id %s is %s, want readonly", byIDDesc[1], got)
}
}
@@ -13,11 +13,11 @@ Each numbered section below is one commit, and its items are that commit's parts
## 2. Ride-along fixes that stand alone
- [ ] 2.1 #225, FedWiki's lists: end `ListActiveSitesByWorkspace`, `ListActiveSitesByPool` and `ListForceReducedSitesByPool` (`store/queries/sites.sql`) on `site_id DESC`, regenerate sqlc, and add a database test with two sites that share every other sort column, which pins the order each list returns and the site the sweep parks.
- [ ] 2.2 #231: `CheckQuotaActivity` (`workflows/activities.go`) answers "no entitlement" only for `sql.ErrNoRows` and returns any other error, which the create workflow already ends `failed` with `quota_unavailable`. Remove the nilerr exception for `return &CheckQuotaOutput{` in `.golangci.yml`. Tests: a querier whose entitlement read fails returns the error; the create workflow with that activity ends the request `failed` with `quota_unavailable` and the notice "<domain> wasn't created. Couldn't check your site limit. Try again later." (copy 3).
- [ ] 2.3 The JSON create runs the assembled-name length check the form runs (design D14). In `web/api.go`, add `validateHostedName(label string, allowed []string) string`: it returns `dnsname.ValidateDNSLabel(label)` when that refuses, then `dnsname.ValidateFQDN(label, allowed[0])` (the parent `resolveCreateDomain` falls back to when the JSON names none), and returns "" when `allowed` is empty, because the resolve that follows answers that case with `msgServiceAvailable`. `(*FedWikiHandler).CreateSite` calls it in place of its `dnsname.ValidateDNSLabel` call on the hosted path and answers 400 with the refusal (copy 7); the call replaces a call, so the function's cognitive complexity stays 48 and its exception in `.golangci.yml` still matches. Test, in `web/requests_db_test.go`: a handler configured with a 200-character farm domain answers a 60-character hosted label with 400 and "The full domain name (<label>.<domain>) exceeds the 253-character limit", records no request, and starts no workflow; a label that fits is still accepted. `TestAPICreatePolling` and the other JSON create tests pass unchanged. No golden moves.
- [ ] 2.4 A failed repeat lookup on the form fails the create (design D14). In `answerCreateRepeat` (`web/site_actions.go`), when `integration.LookupLifecycleRequest` returns an error, log it, add the form error "Failed to initiate site creation" (copy 6), render `renderCreateFormRefusal` with the address kept (`stale` false) and return true, in place of logging a warning and returning false. Test, in `web/requests_db_test.go` beside `TestCreateRepeatTable`: a form create whose repeat lookup fails answers 422 with that sentence, keeps the form's address and records no request; the same submission with the lookup healthy is accepted. No golden moves.
- [ ] 2.5 No golden moves in this section; a golden, replay history or test result that changes is a stop.
- [x] 2.1 #225, FedWiki's lists: end `ListActiveSitesByWorkspace`, `ListActiveSitesByPool` and `ListForceReducedSitesByPool` (`store/queries/sites.sql`) on `site_id DESC`, regenerate sqlc, and add a database test with two sites that share every other sort column, which pins the order each list returns and the site the sweep parks.
- [x] 2.2 #231: `CheckQuotaActivity` (`workflows/activities.go`) answers "no entitlement" only for `sql.ErrNoRows` and returns any other error, which the create workflow already ends `failed` with `quota_unavailable`. Remove the nilerr exception for `return &CheckQuotaOutput{` in `.golangci.yml`. Tests: a querier whose entitlement read fails returns the error; the create workflow with that activity ends the request `failed` with `quota_unavailable` and the notice "<domain> wasn't created. Couldn't check your site limit. Try again later." (copy 3).
- [x] 2.3 The JSON create runs the assembled-name length check the form runs (design D14). In `web/api.go`, add `validateHostedName(label string, allowed []string) string`: it returns `dnsname.ValidateDNSLabel(label)` when that refuses, then `dnsname.ValidateFQDN(label, allowed[0])` (the parent `resolveCreateDomain` falls back to when the JSON names none), and returns "" when `allowed` is empty, because the resolve that follows answers that case with `msgServiceAvailable`. `(*FedWikiHandler).CreateSite` calls it in place of its `dnsname.ValidateDNSLabel` call on the hosted path and answers 400 with the refusal (copy 7); the call replaces a call, so the function's cognitive complexity stays 48 and its exception in `.golangci.yml` still matches. Test, in `web/requests_db_test.go`: a handler configured with a 200-character farm domain answers a 60-character hosted label with 400 and "The full domain name (<label>.<domain>) exceeds the 253-character limit", records no request, and starts no workflow; a label that fits is still accepted. `TestAPICreatePolling` and the other JSON create tests pass unchanged. No golden moves.
- [x] 2.4 A failed repeat lookup on the form fails the create (design D14). In `answerCreateRepeat` (`web/site_actions.go`), when `integration.LookupLifecycleRequest` returns an error, log it, add the form error "Failed to initiate site creation" (copy 6), render `renderCreateFormRefusal` with the address kept (`stale` false) and return true, in place of logging a warning and returning false. Test, in `web/requests_db_test.go` beside `TestCreateRepeatTable`: a form create whose repeat lookup fails answers 422 with that sentence, keeps the form's address and records no request; the same submission with the lookup healthy is accepted. No golden moves.
- [x] 2.5 No golden moves in this section; a golden, replay history or test result that changes is a stop.
## 3. The six interleavings, pinned against today's code