molecule-core/workspace-server/internal/registry/hibernation_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

148 lines
4.1 KiB
Go

package registry
import (
"context"
"database/sql"
"sync/atomic"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
)
func setupHibernationMock(t *testing.T) sqlmock.Sqlmock {
t.Helper()
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New: %v", err)
}
db.DB = mockDB
t.Cleanup(func() { mockDB.Close() })
return mock
}
// TestHibernateIdleWorkspaces_CallsHandlerForEachCandidate verifies that
// hibernateIdleWorkspaces calls onHibernate once for each workspace row
// returned by the DB query.
func TestHibernateIdleWorkspaces_CallsHandlerForEachCandidate(t *testing.T) {
mock := setupHibernationMock(t)
mock.ExpectQuery(`SELECT id FROM workspaces`).
WillReturnRows(sqlmock.NewRows([]string{"id"}).
AddRow("ws-idle-1").
AddRow("ws-idle-2"))
var called []string
hibernateIdleWorkspaces(context.Background(), func(ctx context.Context, id string) {
called = append(called, id)
})
if len(called) != 2 {
t.Fatalf("expected 2 hibernations, got %d: %v", len(called), called)
}
if called[0] != "ws-idle-1" || called[1] != "ws-idle-2" {
t.Errorf("unexpected IDs: %v", called)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
// TestHibernateIdleWorkspaces_NoRowsNoHandler verifies that no handler is
// called when the query returns zero rows (no idle workspaces).
func TestHibernateIdleWorkspaces_NoRowsNoHandler(t *testing.T) {
mock := setupHibernationMock(t)
mock.ExpectQuery(`SELECT id FROM workspaces`).
WillReturnRows(sqlmock.NewRows([]string{"id"})) // empty
var called int
hibernateIdleWorkspaces(context.Background(), func(_ context.Context, _ string) {
called++
})
if called != 0 {
t.Errorf("expected 0 hibernations, got %d", called)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
// TestHibernateIdleWorkspaces_DBErrorDoesNotPanic verifies that a DB error
// from the query is logged but does not crash the monitor loop.
func TestHibernateIdleWorkspaces_DBErrorDoesNotPanic(t *testing.T) {
mock := setupHibernationMock(t)
mock.ExpectQuery(`SELECT id FROM workspaces`).
WillReturnError(sql.ErrConnDone)
// Should not panic
hibernateIdleWorkspaces(context.Background(), func(_ context.Context, _ string) {
t.Error("handler should not be called on DB error")
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet expectations: %v", err)
}
}
// TestStartHibernationMonitor_TicksAndCallsHandler verifies the monitor loop
// ticks at the configured interval and calls the handler.
func TestStartHibernationMonitor_TicksAndCallsHandler(t *testing.T) {
mock := setupHibernationMock(t)
// Expect at least one DB query (the first tick)
mock.ExpectQuery(`SELECT id FROM workspaces`).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("ws-hibernate-me"))
var callCount int32
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
StartHibernationMonitorWithInterval(ctx, 50*time.Millisecond, func(_ context.Context, id string) {
if id == "ws-hibernate-me" {
atomic.AddInt32(&callCount, 1)
cancel() // stop after first hit
}
})
close(done)
}()
select {
case <-done:
case <-time.After(3 * time.Second):
t.Fatal("monitor did not stop within timeout")
}
if atomic.LoadInt32(&callCount) == 0 {
t.Error("expected handler to be called at least once")
}
}
// TestStartHibernationMonitor_StopsOnContextCancel verifies clean shutdown
// when the context is cancelled before any tick fires.
func TestStartHibernationMonitor_StopsOnContextCancel(t *testing.T) {
_ = setupHibernationMock(t) // no DB calls expected
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
done := make(chan struct{})
go func() {
// Very long interval — only context cancel should stop it
StartHibernationMonitorWithInterval(ctx, 10*time.Minute, func(_ context.Context, _ string) {
// should never be called
})
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("monitor did not stop on context cancel")
}
}