Files
osmedeus/internal/functions/tmux_functions_test.go
T
j3ssie 8448791c3c feat: propagate run cancellation to tmux sessions and remote SSH processes
- Track tmux sessions in ActiveRun and kill them on run cancel via new
  TmuxHooks indirection; expose killed_tmux_sessions in CLI and API responses
- Add ExecuteSSHCommand with remote pidfile + process-group kill watcher so
  cancelling a run actually terminates remote scans (not just the local session)
- Route ssh_exec/ssh_rsync/sync_* through the run's cancellable context via
  new RunContextHooks
- Switch docker-publish to sequential per-arch buildx builds + imagetools
  manifest to avoid OOM on multi-arch builds; add docker-buildx-setup target
- Cross-compile Dockerfile via BUILDPLATFORM/TARGETOS/TARGETARCH and retry
  SAST binary installs to survive QEMU-flaky downloads
- Bump version to v5.0.3
2026-05-26 00:06:26 +08:00

353 lines
8.8 KiB
Go

package functions
import (
"os/exec"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
// toStringSlice converts a goja export result to []string.
// Handles both []string and []interface{} from the VM.
func toStringSlice(v interface{}) []string {
switch s := v.(type) {
case []string:
return s
case []interface{}:
out := make([]string, len(s))
for i, item := range s {
out[i] = item.(string)
}
return out
default:
return nil
}
}
func TestGenerateTmuxSessionName(t *testing.T) {
name := generateTmuxSessionName()
assert.True(t, strings.HasPrefix(name, "bosm-"), "should have bosm- prefix")
// "bosm-" (5) + 8 random chars = 13
assert.Equal(t, 13, len(name), "should be 13 characters total")
}
func TestGenerateTmuxSessionNameUniqueness(t *testing.T) {
seen := make(map[string]bool)
for i := 0; i < 100; i++ {
name := generateTmuxSessionName()
assert.False(t, seen[name], "should generate unique names, got duplicate: %s", name)
seen[name] = true
}
}
func TestFindTmuxBin(t *testing.T) {
path := findTmuxBin()
// Just verify it returns a non-empty string if tmux is installed,
// or empty string if not. Either way is valid.
if path != "" {
assert.True(t, strings.Contains(path, "tmux"), "path should contain 'tmux'")
}
}
func TestTmuxSessionExistsNonexistent(t *testing.T) {
tmuxBin := findTmuxBin()
if tmuxBin == "" {
t.Skip("tmux not installed")
}
assert.False(t, tmuxSessionExists(tmuxBin, "nonexistent-session-bosm-test-999"))
}
// Integration tests - require tmux to be installed
func tmuxAvailable() bool {
_, err := exec.LookPath("tmux")
return err == nil
}
func TestTmuxRunAndKill(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
// Run a session
result, err := rt.Execute(`tmux_run("sleep 60")`, nil)
assert.NoError(t, err)
sessionName, ok := result.(string)
assert.True(t, ok, "should return string")
assert.True(t, strings.HasPrefix(sessionName, "bosm-"), "should have bosm- prefix")
// Verify session exists
tmuxBin := findTmuxBin()
assert.True(t, tmuxSessionExists(tmuxBin, sessionName))
// Kill the session
killResult, err := rt.Execute(`tmux_kill("`+sessionName+`")`, nil)
assert.NoError(t, err)
assert.Equal(t, true, killResult)
// Verify session is gone
assert.False(t, tmuxSessionExists(tmuxBin, sessionName))
}
// TestTmuxRunInvokesHooksWithRunUUID verifies that tmux_run triggers the
// registered TmuxHooks with the current run's UUID, which is how run
// cancellation finds and tears down detached sessions.
func TestTmuxRunInvokesHooksWithRunUUID(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
type event struct {
runUUID string
session string
}
var createdMu sync.Mutex
var created []event
var destroyed []event
RegisterTmuxHooks(&TmuxHooks{
OnSessionCreated: func(runUUID, sessionName string) {
createdMu.Lock()
created = append(created, event{runUUID, sessionName})
createdMu.Unlock()
},
OnSessionDestroyed: func(runUUID, sessionName string) {
createdMu.Lock()
destroyed = append(destroyed, event{runUUID, sessionName})
createdMu.Unlock()
},
})
defer UnregisterTmuxHooks()
rt := NewGojaRuntime()
ctx := map[string]interface{}{"RunUUID": "hook-test-run"}
result, err := rt.Execute(`tmux_run("sleep 60")`, ctx)
assert.NoError(t, err)
sessionName, _ := result.(string)
assert.True(t, strings.HasPrefix(sessionName, "bosm-"))
createdMu.Lock()
if assert.Len(t, created, 1) {
assert.Equal(t, "hook-test-run", created[0].runUUID)
assert.Equal(t, sessionName, created[0].session)
}
createdMu.Unlock()
_, err = rt.Execute(`tmux_kill("`+sessionName+`")`, ctx)
assert.NoError(t, err)
createdMu.Lock()
if assert.Len(t, destroyed, 1) {
assert.Equal(t, "hook-test-run", destroyed[0].runUUID)
assert.Equal(t, sessionName, destroyed[0].session)
}
createdMu.Unlock()
}
func TestTmuxRunWithCustomName(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
customName := "test-tmux-custom-bosm"
result, err := rt.Execute(`tmux_run("sleep 60", "`+customName+`")`, nil)
assert.NoError(t, err)
assert.Equal(t, customName, result)
// Cleanup
_, _ = rt.Execute(`tmux_kill("`+customName+`")`, nil)
}
func TestTmuxCapture(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
// Create a long-lived bash session
result, err := rt.Execute(`tmux_run("bash")`, nil)
assert.NoError(t, err)
sessionName := result.(string)
// Send echo command to the session
_, err = rt.Execute(`tmux_send("`+sessionName+`", "echo 'hello-tmux-test'")`, nil)
assert.NoError(t, err)
// Give the command a moment to execute
_, _ = rt.Execute(`sleep(1)`, nil)
// Capture output
captureResult, err := rt.Execute(`tmux_capture("`+sessionName+`")`, nil)
assert.NoError(t, err)
captured, ok := captureResult.(string)
assert.True(t, ok)
assert.Contains(t, captured, "hello-tmux-test")
// Cleanup
_, _ = rt.Execute(`tmux_kill("`+sessionName+`")`, nil)
}
func TestTmuxSend(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
// Create a session with bash
result, err := rt.Execute(`tmux_run("bash")`, nil)
assert.NoError(t, err)
sessionName := result.(string)
// Send a command
sendResult, err := rt.Execute(`tmux_send("`+sessionName+`", "echo 'sent-test'")`, nil)
assert.NoError(t, err)
assert.Equal(t, true, sendResult)
// Cleanup
_, _ = rt.Execute(`tmux_kill("`+sessionName+`")`, nil)
}
func TestTmuxRunEmptyCommand(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
result, err := rt.Execute(`tmux_run("")`, nil)
assert.NoError(t, err)
assert.Equal(t, "", result)
}
func TestTmuxCaptureNonexistent(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
result, err := rt.Execute(`tmux_capture("nonexistent-bosm-999")`, nil)
assert.NoError(t, err)
assert.Equal(t, "", result)
}
func TestTmuxSendNonexistent(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
result, err := rt.Execute(`tmux_send("nonexistent-bosm-999", "echo hi")`, nil)
assert.NoError(t, err)
assert.Equal(t, false, result)
}
func TestTmuxKillNonexistent(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
result, err := rt.Execute(`tmux_kill("nonexistent-bosm-999")`, nil)
assert.NoError(t, err)
assert.Equal(t, false, result)
}
func TestTmuxList(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
// Create two sessions
result1, err := rt.Execute(`tmux_run("sleep 60", "bosm-list-test-1")`, nil)
assert.NoError(t, err)
assert.Equal(t, "bosm-list-test-1", result1)
result2, err := rt.Execute(`tmux_run("sleep 60", "bosm-list-test-2")`, nil)
assert.NoError(t, err)
assert.Equal(t, "bosm-list-test-2", result2)
// List sessions
listResult, err := rt.Execute(`tmux_list()`, nil)
assert.NoError(t, err)
names := toStringSlice(listResult)
assert.Contains(t, names, "bosm-list-test-1")
assert.Contains(t, names, "bosm-list-test-2")
// Cleanup
_, _ = rt.Execute(`tmux_kill("bosm-list-test-1")`, nil)
_, _ = rt.Execute(`tmux_kill("bosm-list-test-2")`, nil)
}
func TestTmuxListEmpty(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
// tmux_list() should return an array (possibly with other sessions from the system)
listResult, err := rt.Execute(`tmux_list()`, nil)
assert.NoError(t, err)
// Should be a slice type (even if empty)
names := toStringSlice(listResult)
assert.NotNil(t, names, "should return a slice")
}
func TestTmuxCaptureAll(t *testing.T) {
if !tmuxAvailable() {
t.Skip("tmux not installed")
}
rt := NewGojaRuntime()
// Create two bash sessions with distinct output
_, err := rt.Execute(`tmux_run("bash", "bosm-capall-1")`, nil)
assert.NoError(t, err)
_, err = rt.Execute(`tmux_run("bash", "bosm-capall-2")`, nil)
assert.NoError(t, err)
// Send distinct echo commands
_, err = rt.Execute(`tmux_send("bosm-capall-1", "echo 'MARKER_ALPHA_123'")`, nil)
assert.NoError(t, err)
_, err = rt.Execute(`tmux_send("bosm-capall-2", "echo 'MARKER_BETA_456'")`, nil)
assert.NoError(t, err)
// Wait for commands to execute
_, _ = rt.Execute(`sleep(1)`, nil)
// Capture all
captureResult, err := rt.Execute(`tmux_capture("all")`, nil)
assert.NoError(t, err)
captured, ok := captureResult.(string)
assert.True(t, ok)
// Verify both session headers and outputs are present
assert.Contains(t, captured, "=== session: bosm-capall-1 ===")
assert.Contains(t, captured, "=== session: bosm-capall-2 ===")
assert.Contains(t, captured, "MARKER_ALPHA_123")
assert.Contains(t, captured, "MARKER_BETA_456")
// Cleanup
_, _ = rt.Execute(`tmux_kill("bosm-capall-1")`, nil)
_, _ = rt.Execute(`tmux_kill("bosm-capall-2")`, nil)
}