Files
BrowserOS/packages/browseros-agent/apps/cli/integration_test.go
shivammittal274 a85f94de40 feat(cli): add strata commands for Klavis MCP integrations (#700)
Expose the 7 Klavis Strata MCP tools as CLI subcommands under
`browseros-cli strata`, so CLI users (claude-code, gemini-cli) can
discover and execute actions on 40+ external services.

Commands: check, discover, actions, details, exec, search, auth.
Includes discovery flow guidance in help text, integration tests,
and an "Integrations:" group in the root help output.
2026-04-14 17:32:05 +05:30

354 lines
9.4 KiB
Go

//go:build integration
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
var (
cliBinary string
serverURL string
)
func TestMain(m *testing.M) {
serverURL = os.Getenv("BROWSEROS_URL")
if serverURL == "" {
serverURL = "http://127.0.0.1:9105"
}
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(serverURL + "/health")
if err != nil {
fmt.Fprintf(os.Stderr, "Skipping integration tests: server not reachable at %s\n", serverURL)
os.Exit(0)
}
resp.Body.Close()
tmpDir, err := os.MkdirTemp("", "browseros-cli-test-*")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create temp dir: %v\n", err)
os.Exit(1)
}
cliBinary = filepath.Join(tmpDir, "browseros-cli")
buildCmd := exec.Command("go", "build", "-o", cliBinary, ".")
buildCmd.Stderr = os.Stderr
if err := buildCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to build CLI: %v\n", err)
os.RemoveAll(tmpDir)
os.Exit(1)
}
code := m.Run()
os.RemoveAll(tmpDir)
os.Exit(code)
}
type runResult struct {
Stdout string
Stderr string
ExitCode int
}
func run(t *testing.T, args ...string) runResult {
t.Helper()
fullArgs := append([]string{"--server", serverURL}, args...)
cmd := exec.Command(cliBinary, fullArgs...)
var stdout, stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
code := 0
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
code = exitErr.ExitCode()
} else {
t.Fatalf("exec error: %v", err)
}
}
return runResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
ExitCode: code,
}
}
func runJSON(t *testing.T, args ...string) map[string]any {
t.Helper()
fullArgs := append([]string{"--json"}, args...)
r := run(t, fullArgs...)
if r.ExitCode != 0 {
t.Fatalf("command %v exited %d: %s%s", args, r.ExitCode, r.Stdout, r.Stderr)
}
var data map[string]any
if err := json.Unmarshal([]byte(strings.TrimSpace(r.Stdout)), &data); err != nil {
t.Fatalf("invalid JSON output for %v: %v\nraw: %s", args, err, r.Stdout)
}
return data
}
func TestHealth(t *testing.T) {
data := runJSON(t, "health")
status, ok := data["status"].(string)
if !ok || status != "ok" {
t.Errorf("expected status ok, got %v", data["status"])
}
}
func TestVersion(t *testing.T) {
r := run(t, "--version")
if r.ExitCode != 0 {
t.Fatalf("--version exited %d", r.ExitCode)
}
if !strings.Contains(r.Stdout, "browseros-cli") {
t.Errorf("expected version output to contain 'browseros-cli', got: %s", r.Stdout)
}
}
func TestPageLifecycle(t *testing.T) {
// List existing pages
pagesBefore := runJSON(t, "pages")
countBefore, _ := pagesBefore["count"].(float64)
if countBefore < 1 {
t.Log("Warning: no pages found before test, server may not have a browser connected")
}
// Open a new page
openData := runJSON(t, "open", "https://example.com")
pageIDFloat, ok := openData["pageId"].(float64)
if !ok {
t.Fatalf("expected pageId in open response, got: %v", openData)
}
pageID := int(pageIDFloat)
t.Logf("Opened page %d", pageID)
pageArg := fmt.Sprintf("-p=%d", pageID)
// Verify page count increased
pagesAfter := runJSON(t, "pages")
countAfter, _ := pagesAfter["count"].(float64)
if countAfter <= countBefore {
t.Errorf("expected page count to increase: before=%v after=%v", countBefore, countAfter)
}
// Wait for page to load
time.Sleep(2 * time.Second)
// Text extraction
t.Run("text", func(t *testing.T) {
data := runJSON(t, "text", pageArg)
// structuredContent may have a "text" key or the content items have text
// With --json, the output is structuredContent if present
raw, _ := json.Marshal(data)
if !strings.Contains(strings.ToLower(string(raw)), "example") {
t.Errorf("expected page content to mention 'example', got: %s", string(raw))
}
})
// Snapshot
t.Run("snap", func(t *testing.T) {
r := run(t, "--json", "snap", pageArg)
if r.ExitCode != 0 {
t.Fatalf("snap exited %d: %s%s", r.ExitCode, r.Stdout, r.Stderr)
}
if len(r.Stdout) < 10 {
t.Errorf("snapshot output too short: %s", r.Stdout)
}
})
// Eval
t.Run("eval", func(t *testing.T) {
r := run(t, "--json", "eval", pageArg, "document.title")
if r.ExitCode != 0 {
t.Fatalf("eval exited %d: %s%s", r.ExitCode, r.Stdout, r.Stderr)
}
out := strings.TrimSpace(r.Stdout)
if !strings.Contains(strings.ToLower(out), "example") {
t.Errorf("expected eval result to contain 'example', got: %s", out)
}
})
// Screenshot
t.Run("screenshot", func(t *testing.T) {
r := run(t, "--json", "ss", pageArg)
if r.ExitCode != 0 {
t.Fatalf("ss exited %d: %s%s", r.ExitCode, r.Stdout, r.Stderr)
}
out := strings.TrimSpace(r.Stdout)
// JSON output should contain image data or mimeType
if !strings.Contains(out, "image") && !strings.Contains(out, "data") {
t.Errorf("expected screenshot output to contain image data, got: %s", out[:min(len(out), 200)])
}
})
// Navigate
t.Run("nav", func(t *testing.T) {
r := run(t, "--json", "nav", pageArg, "https://example.com/nav-test")
if r.ExitCode != 0 {
t.Fatalf("nav exited %d: %s%s", r.ExitCode, r.Stdout, r.Stderr)
}
})
// Reload
t.Run("reload", func(t *testing.T) {
r := run(t, "--json", "reload", pageArg)
if r.ExitCode != 0 {
t.Fatalf("reload exited %d: %s%s", r.ExitCode, r.Stdout, r.Stderr)
}
})
// Close the page (cleanup)
closeR := run(t, "--json", "close", fmt.Sprintf("%d", pageID))
if closeR.ExitCode != 0 {
t.Errorf("close exited %d: %s%s", closeR.ExitCode, closeR.Stdout, closeR.Stderr)
}
}
func TestActivePage(t *testing.T) {
data := runJSON(t, "active")
page, ok := data["page"].(map[string]any)
if !ok {
t.Fatalf("expected active page response to contain page object, got: %v", data)
}
if _, ok := page["pageId"].(float64); !ok {
t.Fatalf("expected active page response to contain numeric pageId, got: %v", page)
}
}
func TestSnapWithoutExplicitPage(t *testing.T) {
activeData := runJSON(t, "active")
page, ok := activeData["page"].(map[string]any)
if !ok {
t.Fatalf("expected active page response to contain page object, got: %v", activeData)
}
if _, ok := page["pageId"].(float64); !ok {
t.Fatalf("expected active page response to contain numeric pageId, got: %v", page)
}
r := run(t, "--json", "snap")
if r.ExitCode != 0 {
t.Fatalf("snap exited %d: %s%s", r.ExitCode, r.Stdout, r.Stderr)
}
if len(strings.TrimSpace(r.Stdout)) < 10 {
t.Fatalf("snapshot output too short: %s", r.Stdout)
}
}
func TestInfo(t *testing.T) {
r := run(t, "--json", "info")
if r.ExitCode != 0 {
t.Fatalf("info exited %d: %s%s", r.ExitCode, r.Stdout, r.Stderr)
}
if len(r.Stdout) < 5 {
t.Errorf("info output too short: %s", r.Stdout)
}
}
func TestEvalError(t *testing.T) {
// Open a page for eval
openData := runJSON(t, "open", "about:blank")
pageID := int(openData["pageId"].(float64))
defer run(t, "close", fmt.Sprintf("%d", pageID))
r := run(t, "--json", "eval", fmt.Sprintf("-p=%d", pageID), "throw new Error('test-error')")
if r.ExitCode == 0 {
t.Errorf("expected eval with throw to exit non-zero")
}
}
func TestInvalidPage(t *testing.T) {
r := run(t, "--json", "snap", "-p=999999")
if r.ExitCode == 0 {
t.Errorf("expected snap with invalid page ID to exit non-zero")
}
}
func TestStrataCheck(t *testing.T) {
r := run(t, "--json", "strata", "check", "Gmail")
// Klavis may not be configured — accept success or structured error
out := strings.TrimSpace(r.Stdout + r.Stderr)
if out == "" {
t.Fatal("strata check produced no output")
}
if r.ExitCode == 0 {
var data map[string]any
if err := json.Unmarshal([]byte(strings.TrimSpace(r.Stdout)), &data); err != nil {
t.Fatalf("strata check returned non-JSON: %s", r.Stdout)
}
}
}
func TestStrataDiscover(t *testing.T) {
r := run(t, "--json", "strata", "discover", "send email", "Gmail")
out := strings.TrimSpace(r.Stdout + r.Stderr)
if out == "" {
t.Fatal("strata discover produced no output")
}
if r.ExitCode == 0 {
var data map[string]any
if err := json.Unmarshal([]byte(strings.TrimSpace(r.Stdout)), &data); err != nil {
t.Fatalf("strata discover returned non-JSON: %s", r.Stdout)
}
}
}
func TestStrataSearch(t *testing.T) {
r := run(t, "--json", "strata", "search", "send email", "Gmail")
out := strings.TrimSpace(r.Stdout + r.Stderr)
if out == "" {
t.Fatal("strata search produced no output")
}
if r.ExitCode == 0 {
var data map[string]any
if err := json.Unmarshal([]byte(strings.TrimSpace(r.Stdout)), &data); err != nil {
t.Fatalf("strata search returned non-JSON: %s", r.Stdout)
}
}
}
func TestStrataActions(t *testing.T) {
r := run(t, "--json", "strata", "actions", "Gmail")
out := strings.TrimSpace(r.Stdout + r.Stderr)
if out == "" {
t.Fatal("strata actions produced no output")
}
}
func TestStrataDetails(t *testing.T) {
r := run(t, "--json", "strata", "details", "Gmail", "send_email")
out := strings.TrimSpace(r.Stdout + r.Stderr)
if out == "" {
t.Fatal("strata details produced no output")
}
}
func TestStrataAuth(t *testing.T) {
r := run(t, "--json", "strata", "auth", "Gmail")
out := strings.TrimSpace(r.Stdout + r.Stderr)
if out == "" {
t.Fatal("strata auth produced no output")
}
}
func TestStrataExecMissingArgs(t *testing.T) {
r := run(t, "strata", "exec")
if r.ExitCode == 0 {
t.Error("expected strata exec without args to exit non-zero")
}
}
func TestStrataCheckMissingArgs(t *testing.T) {
r := run(t, "strata", "check")
if r.ExitCode == 0 {
t.Error("expected strata check without args to exit non-zero")
}
}