feat: full feature parity for all 42 SDKs + comprehensive tests

All SDKs now implement 58+ functions matching C reference (un.h):
- Execution (8): execute, execute_async, wait_job, get_job, cancel_job, list_jobs, get_languages, detect_language
- Sessions (9): list, get, create, destroy, freeze, unfreeze, boost, unboost, execute
- Services (17): list, get, create, destroy, freeze, unfreeze, lock, unlock, set_unfreeze_on_demand, redeploy, logs, execute, env_get/set/delete/export, resize
- Snapshots (9): list, get, session, service, restore, delete, lock, unlock, clone
- Images (13): list, get, publish, delete, lock, unlock, set_visibility, grant/revoke_access, list_trusted, transfer, spawn, clone
- PaaS Logs (2): fetch, stream
- Utilities (5): validate_keys, hmac_sign, health_check, version, last_error

Test suites created for all SDKs with unit, integration, and functional tests.

Languages: AWK, Bash, C++, C#, Clojure, COBOL, Crystal, D, Dart, .NET, Elixir, Erlang, F#, Forth, Fortran, Go, Groovy, Haskell, Java, JavaScript, Julia, Kotlin, Lisp, Lua, Nim, Objective-C, OCaml, Perl, PHP, PowerShell, Prolog, Python, R, Raku, Ruby, Rust, Scheme, Swift, Tcl, TypeScript, V, Zig
This commit is contained in:
russell@unturf.com 2026-02-05 16:45:02 -05:00
parent a5155aed4f
commit 6e746ace44
90 changed files with 28955 additions and 3028 deletions

View file

@ -1096,6 +1096,20 @@ func ExecuteInService(creds *Credentials, serviceID, command string) (map[string
return makeRequest("POST", fmt.Sprintf("/services/%s/execute", serviceID), creds, data)
}
// ResizeService changes the vCPU count for a running service.
//
// Args:
//
// creds: API credentials
// serviceID: Service ID
// vcpu: New vCPU count (1-8)
func ResizeService(creds *Credentials, serviceID string, vcpu int) (map[string]interface{}, error) {
data := map[string]interface{}{
"vcpu": vcpu,
}
return makeRequest("POST", fmt.Sprintf("/services/%s/resize", serviceID), creds, data)
}
// ============================================================================
// Additional Snapshot Operations
// ============================================================================
@ -1480,6 +1494,190 @@ func CloneImage(creds *Credentials, imageID string, opts *CloneImageOptions) (ma
return makeRequest("POST", fmt.Sprintf("/images/%s/clone", imageID), creds, data)
}
// ============================================================================
// PaaS Logs API
// ============================================================================
// LogsFetchOptions contains options for fetching logs.
type LogsFetchOptions struct {
Lines int // Number of lines (1-10000)
Since string // Time window ("1m", "5m", "1h", "1d")
Grep string // Optional filter pattern
}
// LogsFetch fetches batch logs from the portal.
//
// Args:
//
// creds: API credentials
// source: Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
// opts: Fetch options (can be nil for defaults)
//
// Returns:
//
// JSON response with log entries
func LogsFetch(creds *Credentials, source string, opts *LogsFetchOptions) (map[string]interface{}, error) {
path := "/paas/logs"
params := []string{}
if source != "" {
params = append(params, fmt.Sprintf("source=%s", source))
}
if opts != nil {
if opts.Lines > 0 {
params = append(params, fmt.Sprintf("lines=%d", opts.Lines))
}
if opts.Since != "" {
params = append(params, fmt.Sprintf("since=%s", opts.Since))
}
if opts.Grep != "" {
params = append(params, fmt.Sprintf("grep=%s", opts.Grep))
}
}
if len(params) > 0 {
path = path + "?" + strings.Join(params, "&")
}
return makeRequest("GET", path, creds, nil)
}
// LogCallback is called for each log line received during streaming.
type LogCallback func(source, line string)
// LogsStream streams logs via Server-Sent Events.
// This function blocks until the stream is closed or an error occurs.
//
// Args:
//
// creds: API credentials
// source: Log source ("all", "api", "portal", "pool/cammy", "pool/ai")
// grep: Optional filter pattern (empty string for no filter)
// callback: Function called for each log line
//
// Returns:
//
// nil on clean shutdown, error on failure
func LogsStream(creds *Credentials, source, grep string, callback LogCallback) error {
path := "/paas/logs/stream"
params := []string{}
if source != "" {
params = append(params, fmt.Sprintf("source=%s", source))
}
if grep != "" {
params = append(params, fmt.Sprintf("grep=%s", grep))
}
if len(params) > 0 {
path = path + "?" + strings.Join(params, "&")
}
url := APIBase + path
timestamp := time.Now().Unix()
message := fmt.Sprintf("%d:GET:%s:", timestamp, path)
mac := hmac.New(sha256.New, []byte(creds.SecretKey))
mac.Write([]byte(message))
signature := hex.EncodeToString(mac.Sum(nil))
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+creds.PublicKey)
req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
req.Header.Set("X-Signature", signature)
req.Header.Set("Accept", "text/event-stream")
client := &http.Client{Timeout: 0} // No timeout for streaming
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("stream error (HTTP %d): %s", resp.StatusCode, string(body))
}
reader := bufio.NewReader(resp.Body)
currentSource := source
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
return nil // Clean shutdown
}
return err
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Parse SSE format
if strings.HasPrefix(line, "event:") {
// New source from event type
currentSource = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
} else if strings.HasPrefix(line, "data:") {
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
if callback != nil && data != "" {
callback(currentSource, data)
}
}
}
}
// ============================================================================
// Utility Functions
// ============================================================================
// SDKVersion is the version of this SDK.
const SDKVersion = "4.2.0"
// HmacSign computes an HMAC-SHA256 signature for the given message using the secret key.
// Returns the signature as a lowercase hex string.
func HmacSign(secretKey, message string) string {
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(message))
return hex.EncodeToString(mac.Sum(nil))
}
// HealthCheck checks if the API is reachable and responding.
// Returns true if healthy, false otherwise.
func HealthCheck() bool {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(APIBase + "/health")
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == 200
}
// Version returns the SDK version string.
func Version() string {
return SDKVersion
}
// lastError holds the most recent error message for thread-safe access.
var lastError string
// SetLastError sets the last error message (internal use).
func SetLastError(msg string) {
lastError = msg
}
// LastError returns the most recent error message from the SDK.
func LastError() string {
return lastError
}
// ============================================================================
// CLI Implementation
// ============================================================================

View file

@ -0,0 +1,363 @@
// Tests for the Go unsandbox SDK
// Run with: go test -v ./tests/
package un
import (
"os"
"testing"
)
// ============================================================================
// Unit Tests - Test exported library functions
// ============================================================================
func TestDetectLanguage(t *testing.T) {
tests := []struct {
filename string
expected string
}{
{"script.py", "python"},
{"script.js", "javascript"},
{"script.ts", "typescript"},
{"script.rb", "ruby"},
{"script.go", "go"},
{"script.rs", "rust"},
{"script.c", "c"},
{"script.cpp", "cpp"},
{"script.d", "d"},
{"script.zig", "zig"},
{"script.sh", "bash"},
{"script.lua", "lua"},
{"script.php", "php"},
{"script.unknown", ""},
{"script", ""},
}
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
result := DetectLanguage(tt.filename)
if result != tt.expected {
t.Errorf("DetectLanguage(%q) = %q, want %q", tt.filename, result, tt.expected)
}
})
}
}
func TestHmacSign(t *testing.T) {
// Test with known values
secretKey := "test-secret"
message := "test-message"
result := HmacSign(secretKey, message)
// Should return a 64-character hex string
if len(result) != 64 {
t.Errorf("HmacSign returned %d characters, want 64", len(result))
}
// Should be deterministic
result2 := HmacSign(secretKey, message)
if result != result2 {
t.Error("HmacSign is not deterministic")
}
// Different inputs should produce different outputs
result3 := HmacSign(secretKey, "different-message")
if result == result3 {
t.Error("HmacSign returned same result for different inputs")
}
}
func TestVersion(t *testing.T) {
version := Version()
if version == "" {
t.Error("Version() returned empty string")
}
// Should be in semver format
if len(version) < 5 { // At minimum "0.0.0"
t.Errorf("Version() = %q, expected semver format", version)
}
}
func TestLastError(t *testing.T) {
// Set an error
SetLastError("test error message")
// Retrieve it
err := LastError()
if err != "test error message" {
t.Errorf("LastError() = %q, want %q", err, "test error message")
}
// Clear it
SetLastError("")
err = LastError()
if err != "" {
t.Errorf("LastError() after clear = %q, want empty", err)
}
}
func TestCredentialsNew(t *testing.T) {
pk := "unsb-pk-test-test-test-test"
sk := "unsb-sk-test1-test2-test3-test4"
creds := &Credentials{
PublicKey: pk,
SecretKey: sk,
}
if creds.PublicKey != pk {
t.Errorf("PublicKey = %q, want %q", creds.PublicKey, pk)
}
if creds.SecretKey != sk {
t.Errorf("SecretKey = %q, want %q", creds.SecretKey, sk)
}
}
// ============================================================================
// Integration Tests - Test SDK internal consistency
// ============================================================================
func TestSignRequest(t *testing.T) {
secretKey := "test-secret-key"
timestamp := int64(1704067200) // 2024-01-01 00:00:00 UTC
method := "POST"
path := "/execute"
body := `{"language":"python","code":"print(1)"}`
signature := signRequest(secretKey, timestamp, method, path, []byte(body))
// Should return a 64-character hex string
if len(signature) != 64 {
t.Errorf("signRequest returned %d characters, want 64", len(signature))
}
// Should be deterministic
signature2 := signRequest(secretKey, timestamp, method, path, []byte(body))
if signature != signature2 {
t.Error("signRequest is not deterministic")
}
// Different timestamps should produce different signatures
signature3 := signRequest(secretKey, timestamp+1, method, path, []byte(body))
if signature == signature3 {
t.Error("signRequest returned same result for different timestamps")
}
}
func TestResolveCredentialsFromEnv(t *testing.T) {
// Save original env vars
origPK := os.Getenv("UNSANDBOX_PUBLIC_KEY")
origSK := os.Getenv("UNSANDBOX_SECRET_KEY")
// Set test env vars
testPK := "unsb-pk-test-test-test-test"
testSK := "unsb-sk-test1-test2-test3-test4"
os.Setenv("UNSANDBOX_PUBLIC_KEY", testPK)
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
// Test
creds, err := ResolveCredentials("", "")
if err != nil {
t.Fatalf("ResolveCredentials failed: %v", err)
}
if creds.PublicKey != testPK {
t.Errorf("PublicKey = %q, want %q", creds.PublicKey, testPK)
}
if creds.SecretKey != testSK {
t.Errorf("SecretKey = %q, want %q", creds.SecretKey, testSK)
}
// Restore original env vars
if origPK != "" {
os.Setenv("UNSANDBOX_PUBLIC_KEY", origPK)
} else {
os.Unsetenv("UNSANDBOX_PUBLIC_KEY")
}
if origSK != "" {
os.Setenv("UNSANDBOX_SECRET_KEY", origSK)
} else {
os.Unsetenv("UNSANDBOX_SECRET_KEY")
}
}
func TestResolveCredentialsFromArgs(t *testing.T) {
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
creds, err := ResolveCredentials(testPK, testSK)
if err != nil {
t.Fatalf("ResolveCredentials failed: %v", err)
}
if creds.PublicKey != testPK {
t.Errorf("PublicKey = %q, want %q", creds.PublicKey, testPK)
}
if creds.SecretKey != testSK {
t.Errorf("SecretKey = %q, want %q", creds.SecretKey, testSK)
}
}
// ============================================================================
// Functional Tests - Test against real API (requires credentials)
// ============================================================================
func getTestCredentials(t *testing.T) *Credentials {
creds, err := ResolveCredentials("", "")
if err != nil {
t.Skip("No credentials available for functional tests")
}
return creds
}
func TestHealthCheck(t *testing.T) {
if testing.Short() {
t.Skip("Skipping functional test in short mode")
}
healthy := HealthCheck()
if !healthy {
t.Log("API health check returned unhealthy (API may be unreachable)")
}
}
func TestGetLanguages(t *testing.T) {
if testing.Short() {
t.Skip("Skipping functional test in short mode")
}
creds := getTestCredentials(t)
languages, err := GetLanguages(creds)
if err != nil {
t.Fatalf("GetLanguages failed: %v", err)
}
if len(languages) == 0 {
t.Error("GetLanguages returned empty list")
}
// Should include common languages
hasPython := false
hasJavascript := false
for _, lang := range languages {
if lang == "python" {
hasPython = true
}
if lang == "javascript" {
hasJavascript = true
}
}
if !hasPython {
t.Error("GetLanguages missing 'python'")
}
if !hasJavascript {
t.Error("GetLanguages missing 'javascript'")
}
}
func TestValidateKeys(t *testing.T) {
if testing.Short() {
t.Skip("Skipping functional test in short mode")
}
creds := getTestCredentials(t)
result, err := ValidateKeys(creds)
if err != nil {
t.Fatalf("ValidateKeys failed: %v", err)
}
if result == nil {
t.Error("ValidateKeys returned nil")
}
}
func TestExecuteCode(t *testing.T) {
if testing.Short() {
t.Skip("Skipping functional test in short mode")
}
creds := getTestCredentials(t)
result, err := ExecuteCode(creds, "python", "print('hello from go test')")
if err != nil {
t.Fatalf("ExecuteCode failed: %v", err)
}
if result == nil {
t.Error("ExecuteCode returned nil")
}
// Check for stdout in result
if stdout, ok := result["stdout"].(string); ok {
if stdout == "" {
t.Error("ExecuteCode returned empty stdout")
}
}
}
func TestListSessions(t *testing.T) {
if testing.Short() {
t.Skip("Skipping functional test in short mode")
}
creds := getTestCredentials(t)
sessions, err := ListSessions(creds)
if err != nil {
t.Fatalf("ListSessions failed: %v", err)
}
// Should return a list (possibly empty)
if sessions == nil {
t.Error("ListSessions returned nil")
}
}
func TestListServices(t *testing.T) {
if testing.Short() {
t.Skip("Skipping functional test in short mode")
}
creds := getTestCredentials(t)
services, err := ListServices(creds)
if err != nil {
t.Fatalf("ListServices failed: %v", err)
}
// Should return a list (possibly empty)
if services == nil {
t.Error("ListServices returned nil")
}
}
func TestListSnapshots(t *testing.T) {
if testing.Short() {
t.Skip("Skipping functional test in short mode")
}
creds := getTestCredentials(t)
snapshots, err := ListSnapshots(creds)
if err != nil {
t.Fatalf("ListSnapshots failed: %v", err)
}
// Should return a list (possibly empty)
if snapshots == nil {
t.Error("ListSnapshots returned nil")
}
}
func TestListImages(t *testing.T) {
if testing.Short() {
t.Skip("Skipping functional test in short mode")
}
creds := getTestCredentials(t)
images, err := ListImages(creds, "")
if err != nil {
t.Fatalf("ListImages failed: %v", err)
}
// Should return a list (possibly empty)
if images == nil {
t.Error("ListImages returned nil")
}
}