mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 17:36:45 +08:00
* test(server): reduce slow unit test waits Co-authored-by: multica-agent <[email protected]> * test(server): cut slow Go unit test time further Follow-up to the first pass on MUL-7271. Test-only changes plus a few package-private seams whose production values are unchanged. - Re-executed race-built test binaries no longer pay the race runtime's 1s atexit sleep (GORACE=atexit_sleep_ms=0 for child processes only). - pkg/agent: event-driven or shorter test-only bounds for handshake, drain and idle graces; redundant timeout variants merged. - daemon/execenv: git fixtures copied from a template repo built once; tests that re-ran the same worktree lifecycle folded into their supersets; ForkLock-guarded fake-CLI writes so parallel tests cannot hit ETXTBSY. - handler/service: cap and paging tests shrink their caps instead of seeding thousands of rows; lock waits probe pg_locks instead of sleeping; one shared pgxpool per service test binary; cheaper cron and RSA fixtures. - wecom: the phishing-link sweep reuses one parser and derives CRLF from LF with a sampled cross-check; fake sockets acknowledge instead of waiting out the ack timeout. - migrate: the migration corpus is parsed once; isolated DB tests run in parallel. Co-authored-by: multica-agent <[email protected]> * test(server): remove transport timeout unit tests Co-authored-by: multica-agent <[email protected]> * test(server): restore two non-timeout tests and make a bound load-bearing Follow-up to the review of #8318. All test-only; no production code changes. - daemon: restore the three read-liveness cases that the transport-timeout sweep took with it. They assert that a ping, a pong and an application frame each EXTEND the read deadline, so a healthy wakeup socket survives without traffic of our own — the deadline never fires on the happy path, which is why they are not transport-timeout tests. Verified they fail (all three) when the extension calls are removed from wakeup.go, and pass in 0.57s together. The pure "times out without peer traffic" case stays deleted. - pkg/agent: restore the parent-DEADLINE half of the opaque-env redaction pair. codex.go treats a parent deadline and a parent cancellation as one ending; only the Canceled arm was left covered. The fake now writes the secret to stderr before reading the handshake, so the check no longer races our own initialize write, and the budget is 2s against a measured 430-465ms launch. Verified it fails when the !contextEnded guard is removed. - pkg/agent: TestRunCollectQuietDoesNotWaitWhenTheAnswerIsIn had its bound widened to a literal 5s, which the shrunk 750ms drain grace can never reach — the test could no longer fail. It now raises the drain grace to 10s for its own duration and asserts under 3s: healthy runs take 0.19-0.44s, and a broken short-circuit takes 10.19s and fails. - wecom: gofmt the import block in relay_settle_budget_test.go. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: J <[email protected]>
180 lines
6.3 KiB
Go
180 lines
6.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// setWorkspaceDeleteLockTimeoutForTest is a package-private hook used only by
|
|
// the tests below to keep them fast.
|
|
func setWorkspaceDeleteLockTimeoutForTest(t *testing.T, v time.Duration) {
|
|
t.Helper()
|
|
workspaceDeleteLockTimeoutOverride = v
|
|
t.Cleanup(func() { workspaceDeleteLockTimeoutOverride = 0 })
|
|
}
|
|
|
|
// TestDeleteWorkspace_FailsFastWhenRollupLockHeld is the regression test for
|
|
// MUL-5983: "clicking delete workspace does nothing".
|
|
//
|
|
// The teardown transaction takes global advisory lock 4246 so the
|
|
// task_usage_hourly rollup cannot write aggregates for a workspace that is
|
|
// being torn down. That lock is batch-held — a rollup tick, a backfill run, or
|
|
// (before migration 272) a cancelled tick that leaked it — and nothing above
|
|
// this handler has a deadline: the desktop client's fetch has no timeout, so
|
|
// an unbounded wait leaves the delete dialog spinning on "Deleting…" until the
|
|
// app is force quit, and the workspace is never deleted.
|
|
//
|
|
// With SET LOCAL lock_timeout in place the same contention has to surface as a
|
|
// bounded, retryable 503 while the workspace stays intact.
|
|
func TestDeleteWorkspace_FailsFastWhenRollupLockHeld(t *testing.T) {
|
|
if testPool == nil {
|
|
t.Skip("DATABASE_URL not set; skipping live-Postgres workspace delete lock test")
|
|
}
|
|
ctx := context.Background()
|
|
|
|
// Taking 4246 by hand is exactly what the rollup family's cross-binary
|
|
// guard exists to serialise: internal/scheduler runs in parallel against
|
|
// the same database, and its own 4246 tests read the lock's state (MUL-3980).
|
|
lockRollupSingleton(t)
|
|
|
|
// The request waits this budget out in full on 4246, which is never
|
|
// released, so it only has to be long enough to be a real wait.
|
|
setWorkspaceDeleteLockTimeoutForTest(t, 100*time.Millisecond)
|
|
|
|
// Hold 4246 on a pinned connection for the duration of the request, the
|
|
// way a running rollup tick (or a leaked one) holds it in production.
|
|
holder, err := testPool.Acquire(ctx)
|
|
if err != nil {
|
|
t.Fatalf("acquire advisory lock holder: %v", err)
|
|
}
|
|
if _, err := holder.Exec(ctx, `SELECT pg_advisory_lock(4246)`); err != nil {
|
|
holder.Release()
|
|
t.Fatalf("hold advisory lock 4246: %v", err)
|
|
}
|
|
// Idempotent: the deadline branch below releases the holder early to
|
|
// unblock the handler, and the defer still runs afterwards. A second
|
|
// Exec/Release on a returned pgxpool connection panics, which would
|
|
// replace the assertion failure with a stack trace.
|
|
var releaseOnce sync.Once
|
|
releaseHolder := func() {
|
|
releaseOnce.Do(func() {
|
|
_, _ = holder.Exec(context.Background(), `SELECT pg_advisory_unlock(4246)`)
|
|
holder.Release()
|
|
})
|
|
}
|
|
defer releaseHolder()
|
|
|
|
const slug = "handler-tests-delete-lock"
|
|
_, _ = testPool.Exec(ctx, `DELETE FROM workspace WHERE slug = $1`, slug)
|
|
|
|
var wsID string
|
|
if err := testPool.QueryRow(ctx, `
|
|
INSERT INTO workspace (name, slug, description)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id
|
|
`, "Handler Test Delete Lock", slug, "DeleteWorkspace lock timeout test").Scan(&wsID); err != nil {
|
|
t.Fatalf("create workspace: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = testPool.Exec(context.Background(), `DELETE FROM workspace WHERE id = $1`, wsID)
|
|
})
|
|
|
|
if _, err := testPool.Exec(ctx, `
|
|
INSERT INTO member (workspace_id, user_id, role)
|
|
VALUES ($1, $2, 'owner')
|
|
`, wsID, testUserID); err != nil {
|
|
t.Fatalf("create owner member: %v", err)
|
|
}
|
|
|
|
// The handler runs on its own goroutine so a regression shows up as a
|
|
// failed deadline here instead of hanging the whole test binary.
|
|
w := httptest.NewRecorder()
|
|
req := newRequest("DELETE", "/api/workspaces/"+wsID, nil)
|
|
req = withURLParam(req, "id", wsID)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
testHandler.DeleteWorkspace(w, req)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(15 * time.Second):
|
|
// Unblock the handler so the test binary can still shut down cleanly.
|
|
releaseHolder()
|
|
<-done
|
|
t.Fatal("DeleteWorkspace blocked on advisory lock 4246 instead of timing out — this is the hang users see as 'delete workspace does nothing' (MUL-5983)")
|
|
}
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected 503 while advisory lock 4246 is held, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var exists bool
|
|
if err := testPool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM workspace WHERE id = $1)`, wsID).Scan(&exists); err != nil {
|
|
t.Fatalf("verify workspace: %v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatal("workspace must survive a teardown that never got its locks")
|
|
}
|
|
}
|
|
|
|
// TestDeleteWorkspace_SucceedsWhenRollupLockIsFree is the counterpart: the
|
|
// lock timeout must not make an uncontended delete flaky.
|
|
func TestDeleteWorkspace_SucceedsWhenRollupLockIsFree(t *testing.T) {
|
|
if testPool == nil {
|
|
t.Skip("DATABASE_URL not set; skipping live-Postgres workspace delete lock test")
|
|
}
|
|
ctx := context.Background()
|
|
|
|
// The teardown itself waits on 4246 under a 500 ms budget, so a rollup
|
|
// test holding that lock in the parallel internal/scheduler binary would
|
|
// turn this "uncontended" delete into a 503. Same guard, same reason.
|
|
lockRollupSingleton(t)
|
|
|
|
setWorkspaceDeleteLockTimeoutForTest(t, 500*time.Millisecond)
|
|
|
|
const slug = "handler-tests-delete-lock-free"
|
|
_, _ = testPool.Exec(ctx, `DELETE FROM workspace WHERE slug = $1`, slug)
|
|
|
|
var wsID string
|
|
if err := testPool.QueryRow(ctx, `
|
|
INSERT INTO workspace (name, slug, description)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id
|
|
`, "Handler Test Delete Lock Free", slug, "DeleteWorkspace lock timeout test").Scan(&wsID); err != nil {
|
|
t.Fatalf("create workspace: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_, _ = testPool.Exec(context.Background(), `DELETE FROM workspace WHERE id = $1`, wsID)
|
|
})
|
|
|
|
if _, err := testPool.Exec(ctx, `
|
|
INSERT INTO member (workspace_id, user_id, role)
|
|
VALUES ($1, $2, 'owner')
|
|
`, wsID, testUserID); err != nil {
|
|
t.Fatalf("create owner member: %v", err)
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
req := newRequest("DELETE", "/api/workspaces/"+wsID, nil)
|
|
req = withURLParam(req, "id", wsID)
|
|
testHandler.DeleteWorkspace(w, req)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Fatalf("expected 204 from an uncontended delete, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var exists bool
|
|
if err := testPool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM workspace WHERE id = $1)`, wsID).Scan(&exists); err != nil {
|
|
t.Fatalf("verify workspace: %v", err)
|
|
}
|
|
if exists {
|
|
t.Fatal("workspace still exists after a successful delete")
|
|
}
|
|
}
|