Files
multica/server/internal/handler/runtime_binding_lock_order_test.go
m2keloopandm2keloop d908e4882d MUL-6710: fix(runtimes): revoke access when making runtime private (#7580)
* fix(runtimes): revoke access when making runtime private

* fix(runtimes): regenerate queries for workflow step

---------

Co-authored-by: m2keloop <[email protected]>
2026-10-20 12:49:37 +08:00

97 lines
3.3 KiB
Go

package handler
import (
"context"
"net/http"
"testing"
"time"
"github.com/multica-ai/multica/server/internal/testutil"
db "github.com/multica-ai/multica/server/pkg/db/generated"
)
func TestRuntimeBindingLocksWorkspaceBeforeRuntime(t *testing.T) {
if testHandler == nil {
t.Fatal("database-backed handler fixture is required")
}
for _, tt := range []struct {
entry string
wantStatus int
}{
{entry: "user create", wantStatus: http.StatusCreated},
{entry: "user rebind", wantStatus: http.StatusOK},
{entry: "mika create", wantStatus: http.StatusCreated},
{entry: "builder create", wantStatus: http.StatusCreated},
{entry: "builder switch", wantStatus: http.StatusOK},
} {
t.Run(tt.entry, func(t *testing.T) {
h := *testHandler
fx, runtimeID, handler, req := runtimeBindingVisibilityRequest(t, &h, tt.entry)
fx.Cleanup(t, `DELETE FROM agent WHERE workspace_id = $1`, fx.WorkspaceID)
fx.Cleanup(t, `DELETE FROM chat_session WHERE workspace_id = $1`, fx.WorkspaceID)
ctx, cancel := context.WithTimeout(req.Context(), 20*time.Second)
defer cancel()
req = req.WithContext(ctx)
holder, err := testPool.Begin(ctx)
if err != nil {
t.Fatalf("begin workspace delete lock: %v", err)
}
defer holder.Rollback(context.Background())
if _, err := db.New(holder).LockWorkspaceForDelete(ctx, parseUUID(fx.WorkspaceID)); err != nil {
t.Fatalf("lock workspace as deletion would: %v", err)
}
holderPID := holderBackendPID(t, ctx, holder)
responses := make(chan *testutil.Response, 1)
done := make(chan struct{})
go func() {
defer close(done)
responses <- testutil.Call(t, handler, req)
}()
defer func() {
cancel()
_ = holder.Rollback(context.Background())
select {
case <-done:
case <-time.After(5 * time.Second):
t.Error("binding request did not exit after cancellation")
}
}()
if !waitForWaiterBlockedBy(t, holderPID, 10*time.Second) {
select {
case response := <-responses:
t.Fatalf("binding returned %d while workspace was locked: %s", response.Code, response.Text())
default:
t.Fatal("binding did not wait for the workspace lock")
}
}
// While the binding waits for the workspace, another transaction
// must still be able to lock the runtime. Otherwise deletion can
// hold workspace -> wait runtime while binding does the reverse.
probe, err := testPool.Begin(ctx)
if err != nil {
t.Fatalf("begin runtime lock probe: %v", err)
}
defer probe.Rollback(context.Background())
if _, err := probe.Exec(ctx, `SELECT id FROM agent_runtime WHERE id = $1 FOR NO KEY UPDATE NOWAIT`, runtimeID); err != nil {
t.Fatalf("runtime was locked before binding acquired workspace: %v", err)
}
if err := probe.Rollback(ctx); err != nil {
t.Fatalf("release runtime lock probe: %v", err)
}
if err := holder.Commit(ctx); err != nil {
t.Fatalf("release workspace lock: %v", err)
}
select {
case response := <-responses:
response.Want(tt.wantStatus)
case <-ctx.Done():
t.Fatal("binding did not finish after workspace lock was released")
}
if got := fx.Count(t, `SELECT count(*) FROM agent WHERE runtime_id = $1`, runtimeID); got != 1 {
t.Fatalf("successful binding left %d agents on the runtime, want 1", got)
}
})
}
}