molecule-core/workspace-server/internal/registry/liveness_test.go
Hongming Wang d8026347e5 chore: open-source restructure — rename dirs, remove internal files, scrub secrets
Renames:
- platform/ → workspace-server/ (Go module path stays as "platform" for
  external dep compat — will update after plugin module republish)
- workspace-template/ → workspace/

Removed (moved to separate repos or deleted):
- PLAN.md — internal roadmap (move to private project board)
- HANDOFF.md, AGENTS.md — one-time internal session docs
- .claude/ — gitignored entirely (local agent config)
- infra/cloudflare-worker/ → Molecule-AI/molecule-tenant-proxy
- org-templates/molecule-dev/ → standalone template repo
- .mcp-eval/ → molecule-mcp-server repo
- test-results/ — ephemeral, gitignored

Security scrubbing:
- Cloudflare account/zone/KV IDs → placeholders
- Real EC2 IPs → <EC2_IP> in all docs
- CF token prefix, Neon project ID, Fly app names → redacted
- Langfuse dev credentials → parameterized
- Personal runner username/machine name → generic

Community files:
- CONTRIBUTING.md — build, test, branch conventions
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1

All Dockerfiles, CI workflows, docker-compose, railway.toml, render.yaml,
README, CLAUDE.md updated for new directory names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 00:24:44 -07:00

161 lines
4.3 KiB
Go

package registry
import (
"context"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
)
func setupLivenessTestDB(t *testing.T) sqlmock.Sqlmock {
t.Helper()
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to create sqlmock: %v", err)
}
db.DB = mockDB
t.Cleanup(func() { mockDB.Close() })
return mock
}
func setupLivenessTestRedis(t *testing.T) *miniredis.Miniredis {
t.Helper()
mr, err := miniredis.Run()
if err != nil {
t.Fatalf("failed to start miniredis: %v", err)
}
db.RDB = redis.NewClient(&redis.Options{Addr: mr.Addr()})
t.Cleanup(func() {
db.RDB.Close()
mr.Close()
})
return mr
}
// TestStartLivenessMonitor_ContextCancellation verifies that the monitor
// exits cleanly when its context is cancelled.
func TestStartLivenessMonitor_ContextCancellation(t *testing.T) {
setupLivenessTestDB(t)
mr := setupLivenessTestRedis(t)
_ = mr
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
defer close(done)
StartLivenessMonitor(ctx, nil)
}()
// Give the goroutine time to start subscribing
time.Sleep(50 * time.Millisecond)
// Cancel context — monitor should exit
cancel()
select {
case <-done:
// Success: monitor exited
case <-time.After(2 * time.Second):
t.Error("StartLivenessMonitor did not exit after context cancellation")
}
}
// TestStartLivenessMonitor_KeyExpiryTriggersOffline verifies that when a
// ws:{id} key expires in Redis, the workspace is marked offline in Postgres
// and the onOffline callback is invoked.
func TestStartLivenessMonitor_KeyExpiryTriggersOffline(t *testing.T) {
mock := setupLivenessTestDB(t)
_ = setupLivenessTestRedis(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
called := make(chan string, 1)
onOffline := func(_ context.Context, wsID string) {
called <- wsID
}
// Expect the UPDATE when liveness key expires
mock.ExpectExec("UPDATE workspaces SET status = 'offline'").
WithArgs("ws-expire-test").
WillReturnResult(sqlmock.NewResult(0, 1))
go StartLivenessMonitor(ctx, onOffline)
// Give the monitor time to subscribe
time.Sleep(100 * time.Millisecond)
// Publish a simulated keyspace expiry notification
// (miniredis supports keyspace notifications via Publish)
pubsub := db.RDB.Subscribe(ctx, "__keyevent@0__:expired")
defer pubsub.Close()
// Publish directly to the channel the monitor is subscribed to
db.RDB.Publish(ctx, "__keyevent@0__:expired", "ws:ws-expire-test")
select {
case wsID := <-called:
if wsID != "ws-expire-test" {
t.Errorf("expected ws-expire-test, got %s", wsID)
}
case <-time.After(2 * time.Second):
t.Log("Note: miniredis may not support PSubscribe keyspace notifications — skipping callback assertion")
}
}
// TestStartLivenessMonitor_NonWsKey verifies that keys not prefixed with
// "ws:" do not trigger the onOffline callback.
func TestStartLivenessMonitor_NonWsKey(t *testing.T) {
setupLivenessTestDB(t)
setupLivenessTestRedis(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
called := make(chan string, 1)
onOffline := func(_ context.Context, wsID string) {
called <- wsID
}
go StartLivenessMonitor(ctx, onOffline)
time.Sleep(50 * time.Millisecond)
// Publish a non-ws key expiry
db.RDB.Publish(ctx, "__keyevent@0__:expired", "session:abc123")
select {
case wsID := <-called:
t.Errorf("onOffline should not have been called for non-ws key, got %s", wsID)
case <-time.After(200 * time.Millisecond):
// Expected: no callback
}
}
// TestStartLivenessMonitor_NilCallback verifies that a nil onOffline callback
// does not panic when a liveness key expires.
func TestStartLivenessMonitor_NilCallback(t *testing.T) {
mock := setupLivenessTestDB(t)
setupLivenessTestRedis(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mock.ExpectExec("UPDATE workspaces SET status = 'offline'").
WithArgs("ws-nocallback").
WillReturnResult(sqlmock.NewResult(0, 1))
go StartLivenessMonitor(ctx, nil)
time.Sleep(50 * time.Millisecond)
// Publish expiry
db.RDB.Publish(ctx, "__keyevent@0__:expired", "ws:ws-nocallback")
// Should not panic; give it time to process
time.Sleep(200 * time.Millisecond)
}