mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
442 lines
15 KiB
Go
442 lines
15 KiB
Go
package automation
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
goruntime "runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"ant-chrome/backend/internal/config"
|
|
)
|
|
|
|
func TestEnsureInstalledDownloadsAndExtractsRuntime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nodeArchive, nodeSHA := buildTestNodeZip(t)
|
|
playwrightArchive, playwrightSHA := buildTestPlaywrightTGZ(t)
|
|
|
|
mux := http.NewServeMux()
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v22.15.1/SHASUMS256.txt":
|
|
_, _ = w.Write([]byte(nodeSHA + " node-v22.15.1-win-x64.zip\n"))
|
|
case "/v22.15.1/node-v22.15.1-win-x64.zip":
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
_, _ = w.Write(nodeArchive)
|
|
case "/playwright-core/1.59.0":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"dist": map[string]any{
|
|
"tarball": server.URL + "/tarballs/playwright-core-1.59.0.tgz",
|
|
"shasum": playwrightSHA,
|
|
},
|
|
})
|
|
case "/tarballs/playwright-core-1.59.0.tgz":
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
_, _ = w.Write(playwrightArchive)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Automation.Enabled = true
|
|
cfg.Automation.NodeSource = config.AutomationNodeSourceBundled
|
|
cfg.Automation.NodeVersion = "22.15.1"
|
|
cfg.Automation.PlaywrightCoreVersion = "1.59.0"
|
|
cfg.Automation.RuntimeVersion = config.DefaultAutomationRuntimeVersion(cfg.Automation.NodeVersion, cfg.Automation.PlaywrightCoreVersion)
|
|
|
|
manager := NewManager(t.TempDir(), cfg, nil, Options{
|
|
NodeDistBaseURL: server.URL,
|
|
NPMRegistryBaseURL: server.URL,
|
|
TargetOS: "windows",
|
|
TargetArch: "amd64",
|
|
})
|
|
|
|
if err := manager.EnsureInstalled(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInstalled returned error: %v", err)
|
|
}
|
|
|
|
state := manager.CurrentState()
|
|
if !state.Installed || !state.Ready {
|
|
t.Fatalf("runtime should be ready after install, got %+v", state)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(state.RuntimeDir, "node", "node.exe")); err != nil {
|
|
t.Fatalf("expected node executable to exist: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(state.RuntimeDir, "node_modules", "playwright-core", "package.json")); err != nil {
|
|
t.Fatalf("expected playwright-core package.json to exist: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(state.RuntimeDir, runnerScriptFileName)); err != nil {
|
|
t.Fatalf("expected runner script to exist: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureInstalledUsesSystemNodeAndSkipsBundledDownload(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nodeExecPath := lookupNodeExecutable(t)
|
|
playwrightArchive, playwrightSHA := buildTestPlayablePlaywrightTGZ(t, "1.59.0")
|
|
|
|
var nodeRequests atomic.Int32
|
|
|
|
mux := http.NewServeMux()
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/v22.15.1/") {
|
|
nodeRequests.Add(1)
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
switch r.URL.Path {
|
|
case "/playwright-core/1.59.0":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"dist": map[string]any{
|
|
"tarball": server.URL + "/tarballs/playwright-core-1.59.0.tgz",
|
|
"shasum": playwrightSHA,
|
|
},
|
|
})
|
|
case "/tarballs/playwright-core-1.59.0.tgz":
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
_, _ = w.Write(playwrightArchive)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Automation.Enabled = true
|
|
cfg.Automation.NodeSource = config.AutomationNodeSourceSystem
|
|
cfg.Automation.SystemNodePath = nodeExecPath
|
|
cfg.Automation.NodeVersion = "22.15.1"
|
|
cfg.Automation.PlaywrightCoreVersion = "1.59.0"
|
|
cfg.Automation.RuntimeVersion = config.DefaultAutomationRuntimeVersion(cfg.Automation.NodeVersion, cfg.Automation.PlaywrightCoreVersion)
|
|
|
|
manager := NewManager(t.TempDir(), cfg, nil, Options{
|
|
NodeDistBaseURL: server.URL,
|
|
NPMRegistryBaseURL: server.URL,
|
|
TargetOS: goruntime.GOOS,
|
|
TargetArch: goruntime.GOARCH,
|
|
})
|
|
|
|
if err := manager.EnsureInstalled(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInstalled returned error: %v", err)
|
|
}
|
|
|
|
state := manager.CurrentState()
|
|
if !state.Installed || !state.Ready {
|
|
t.Fatalf("runtime should be ready after install, got %+v", state)
|
|
}
|
|
if state.NodeSource != config.AutomationNodeSourceSystem {
|
|
t.Fatalf("expected system node source, got %q", state.NodeSource)
|
|
}
|
|
if filepath.Clean(state.NodePath) != filepath.Clean(nodeExecPath) {
|
|
t.Fatalf("expected system node path %q, got %q", nodeExecPath, state.NodePath)
|
|
}
|
|
if nodeRequests.Load() != 0 {
|
|
t.Fatalf("expected bundled node download to be skipped, got %d node requests", nodeRequests.Load())
|
|
}
|
|
if _, err := os.Stat(filepath.Join(state.RuntimeDir, "node_modules", "playwright-core", "package.json")); err != nil {
|
|
t.Fatalf("expected playwright-core package.json to exist: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(state.RuntimeDir, runnerScriptFileName)); err != nil {
|
|
t.Fatalf("expected runner script to exist: %v", err)
|
|
}
|
|
if _, err := os.Stat(manager.nodeExecutablePath(state.RuntimeDir)); !os.IsNotExist(err) {
|
|
t.Fatalf("expected bundled node to be absent, got err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestProbeSystemNodeUsesExplicitPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nodeExecPath := lookupNodeExecutable(t)
|
|
manager := NewManager(t.TempDir(), config.DefaultConfig(), nil, Options{})
|
|
|
|
result, err := manager.ProbeSystemNode(context.Background(), nodeExecPath)
|
|
if err != nil {
|
|
t.Fatalf("ProbeSystemNode returned error: %v", err)
|
|
}
|
|
if !result.OK {
|
|
t.Fatalf("expected probe result to be ok, got %+v", result)
|
|
}
|
|
if filepath.Clean(result.Path) != filepath.Clean(nodeExecPath) {
|
|
t.Fatalf("expected probe path %q, got %q", nodeExecPath, result.Path)
|
|
}
|
|
if strings.TrimSpace(result.Version) == "" {
|
|
t.Fatalf("expected probe version to be set, got %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestProbeSystemNodeMissingReturnsError(t *testing.T) {
|
|
t.Setenv("PATH", t.TempDir())
|
|
|
|
manager := NewManager(t.TempDir(), config.DefaultConfig(), nil, Options{})
|
|
_, err := manager.ProbeSystemNode(context.Background(), filepath.Join(t.TempDir(), "missing-node.exe"))
|
|
if err == nil {
|
|
t.Fatalf("expected ProbeSystemNode to fail for missing node path")
|
|
}
|
|
}
|
|
|
|
func TestCurrentStateReportsBundledFallbackReasonWhenSystemNodeMissing(t *testing.T) {
|
|
t.Setenv("PATH", t.TempDir())
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Automation.Enabled = true
|
|
cfg.Automation.NodeSource = config.AutomationNodeSourceAuto
|
|
cfg.Automation.SystemNodePath = filepath.Join(t.TempDir(), "missing-node.exe")
|
|
|
|
manager := NewManager(t.TempDir(), cfg, nil, Options{
|
|
TargetOS: "windows",
|
|
TargetArch: "amd64",
|
|
})
|
|
|
|
state := manager.CurrentState()
|
|
if state.NodeSource != config.AutomationNodeSourceBundled {
|
|
t.Fatalf("expected bundled node source, got %q", state.NodeSource)
|
|
}
|
|
if !strings.Contains(state.NodeResolution, "回退") {
|
|
t.Fatalf("expected fallback resolution message, got %q", state.NodeResolution)
|
|
}
|
|
if strings.TrimSpace(state.SystemNodeError) == "" {
|
|
t.Fatalf("expected system node error to be set, got %+v", state)
|
|
}
|
|
}
|
|
|
|
func TestCurrentStateReportsSystemResolutionWhenExplicitNodeSucceeds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nodeExecPath := lookupNodeExecutable(t)
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Automation.Enabled = true
|
|
cfg.Automation.NodeSource = config.AutomationNodeSourceAuto
|
|
cfg.Automation.SystemNodePath = nodeExecPath
|
|
|
|
manager := NewManager(t.TempDir(), cfg, nil, Options{
|
|
TargetOS: goruntime.GOOS,
|
|
TargetArch: goruntime.GOARCH,
|
|
})
|
|
|
|
state := manager.CurrentState()
|
|
if state.NodeSource != config.AutomationNodeSourceSystem {
|
|
t.Fatalf("expected system node source, got %q", state.NodeSource)
|
|
}
|
|
if !strings.Contains(state.NodeResolution, "配置的系统 Node 路径") {
|
|
t.Fatalf("expected explicit system node resolution, got %q", state.NodeResolution)
|
|
}
|
|
}
|
|
|
|
func TestEnsureInstalledRepairsBrokenReadyAutoRuntime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nodeExecPath := lookupNodeExecutable(t)
|
|
playwrightArchive, playwrightSHA := buildTestPlayablePlaywrightTGZ(t, "1.59.0")
|
|
|
|
mux := http.NewServeMux()
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/playwright-core/1.59.0":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"dist": map[string]any{
|
|
"tarball": server.URL + "/tarballs/playwright-core-1.59.0.tgz",
|
|
"shasum": playwrightSHA,
|
|
},
|
|
})
|
|
case "/tarballs/playwright-core-1.59.0.tgz":
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
_, _ = w.Write(playwrightArchive)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Automation.Enabled = true
|
|
cfg.Automation.NodeSource = config.AutomationNodeSourceAuto
|
|
cfg.Automation.SystemNodePath = nodeExecPath
|
|
cfg.Automation.NodeVersion = "22.15.1"
|
|
cfg.Automation.PlaywrightCoreVersion = "1.59.0"
|
|
cfg.Automation.RuntimeVersion = config.DefaultAutomationRuntimeVersion(cfg.Automation.NodeVersion, cfg.Automation.PlaywrightCoreVersion)
|
|
|
|
manager := NewManager(t.TempDir(), cfg, nil, Options{
|
|
NPMRegistryBaseURL: server.URL,
|
|
TargetOS: goruntime.GOOS,
|
|
TargetArch: goruntime.GOARCH,
|
|
})
|
|
|
|
initialState := manager.CurrentState()
|
|
if err := writeRunnerScript(initialState.RunnerPath); err != nil {
|
|
t.Fatalf("write runner script failed: %v", err)
|
|
}
|
|
if err := writeBrokenPlaywrightModule(initialState.RuntimeDir, cfg.Automation.PlaywrightCoreVersion); err != nil {
|
|
t.Fatalf("write broken playwright module failed: %v", err)
|
|
}
|
|
|
|
readyState := manager.CurrentState()
|
|
if !readyState.Ready {
|
|
t.Fatalf("expected broken runtime to appear ready before verification, got %+v", readyState)
|
|
}
|
|
|
|
if err := manager.EnsureInstalled(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInstalled returned error: %v", err)
|
|
}
|
|
|
|
check, err := manager.SelfCheck(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("SelfCheck returned error after repair: %v", err)
|
|
}
|
|
if !check.OK {
|
|
t.Fatalf("expected repaired runtime to pass self-check, got %+v", check)
|
|
}
|
|
}
|
|
|
|
func TestEnsureInstalledAutoFallsBackToBundledWhenSystemNodeMissing(t *testing.T) {
|
|
t.Setenv("PATH", t.TempDir())
|
|
|
|
nodeArchive, nodeSHA := buildTestNodeZip(t)
|
|
playwrightArchive, playwrightSHA := buildTestPlaywrightTGZ(t)
|
|
|
|
mux := http.NewServeMux()
|
|
server := httptest.NewServer(mux)
|
|
defer server.Close()
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v22.15.1/SHASUMS256.txt":
|
|
_, _ = w.Write([]byte(nodeSHA + " node-v22.15.1-win-x64.zip\n"))
|
|
case "/v22.15.1/node-v22.15.1-win-x64.zip":
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
_, _ = w.Write(nodeArchive)
|
|
case "/playwright-core/1.59.0":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"dist": map[string]any{
|
|
"tarball": server.URL + "/tarballs/playwright-core-1.59.0.tgz",
|
|
"shasum": playwrightSHA,
|
|
},
|
|
})
|
|
case "/tarballs/playwright-core-1.59.0.tgz":
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
_, _ = w.Write(playwrightArchive)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Automation.Enabled = true
|
|
cfg.Automation.NodeSource = config.AutomationNodeSourceAuto
|
|
cfg.Automation.SystemNodePath = filepath.Join(t.TempDir(), "missing-node.exe")
|
|
cfg.Automation.NodeVersion = "22.15.1"
|
|
cfg.Automation.PlaywrightCoreVersion = "1.59.0"
|
|
cfg.Automation.RuntimeVersion = config.DefaultAutomationRuntimeVersion(cfg.Automation.NodeVersion, cfg.Automation.PlaywrightCoreVersion)
|
|
|
|
manager := NewManager(t.TempDir(), cfg, nil, Options{
|
|
NodeDistBaseURL: server.URL,
|
|
NPMRegistryBaseURL: server.URL,
|
|
TargetOS: "windows",
|
|
TargetArch: "amd64",
|
|
})
|
|
|
|
if err := manager.EnsureInstalled(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInstalled returned error: %v", err)
|
|
}
|
|
|
|
state := manager.CurrentState()
|
|
if !state.Installed || !state.Ready {
|
|
t.Fatalf("runtime should be ready after install, got %+v", state)
|
|
}
|
|
if state.NodeSource != config.AutomationNodeSourceBundled {
|
|
t.Fatalf("expected bundled node source after fallback, got %q", state.NodeSource)
|
|
}
|
|
}
|
|
|
|
func TestEnsureInstalledSystemSourceFailsWhenSystemNodeMissing(t *testing.T) {
|
|
t.Setenv("PATH", t.TempDir())
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Automation.Enabled = true
|
|
cfg.Automation.NodeSource = config.AutomationNodeSourceSystem
|
|
cfg.Automation.SystemNodePath = filepath.Join(t.TempDir(), "missing-node.exe")
|
|
cfg.Automation.NodeVersion = "22.15.1"
|
|
cfg.Automation.PlaywrightCoreVersion = "1.59.0"
|
|
cfg.Automation.RuntimeVersion = config.DefaultAutomationRuntimeVersion(cfg.Automation.NodeVersion, cfg.Automation.PlaywrightCoreVersion)
|
|
|
|
manager := NewManager(t.TempDir(), cfg, nil, Options{
|
|
TargetOS: "windows",
|
|
TargetArch: "amd64",
|
|
})
|
|
|
|
err := manager.EnsureInstalled(context.Background())
|
|
if err == nil {
|
|
t.Fatalf("expected EnsureInstalled to fail when system node is missing")
|
|
}
|
|
if !strings.Contains(err.Error(), "系统 Node 不可用") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureInstalledRefreshesExistingRunnerScript(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.Automation.Enabled = true
|
|
cfg.Automation.NodeSource = config.AutomationNodeSourceBundled
|
|
cfg.Automation.NodeVersion = "22.15.1"
|
|
cfg.Automation.PlaywrightCoreVersion = "1.59.0"
|
|
cfg.Automation.RuntimeVersion = config.DefaultAutomationRuntimeVersion(cfg.Automation.NodeVersion, cfg.Automation.PlaywrightCoreVersion)
|
|
|
|
manager := NewManager(t.TempDir(), cfg, nil, Options{
|
|
TargetOS: "windows",
|
|
TargetArch: "amd64",
|
|
})
|
|
|
|
state := manager.CurrentState()
|
|
if err := os.MkdirAll(filepath.Dir(state.NodePath), 0o755); err != nil {
|
|
t.Fatalf("create node dir failed: %v", err)
|
|
}
|
|
if err := os.WriteFile(state.NodePath, []byte("fake-node-runtime"), 0o755); err != nil {
|
|
t.Fatalf("write fake node failed: %v", err)
|
|
}
|
|
playwrightPkgPath := filepath.Join(state.RuntimeDir, "node_modules", "playwright-core", "package.json")
|
|
if err := os.MkdirAll(filepath.Dir(playwrightPkgPath), 0o755); err != nil {
|
|
t.Fatalf("create playwright dir failed: %v", err)
|
|
}
|
|
if err := os.WriteFile(playwrightPkgPath, []byte(`{"name":"playwright-core","version":"1.59.0"}`), 0o644); err != nil {
|
|
t.Fatalf("write fake playwright package.json failed: %v", err)
|
|
}
|
|
if err := os.WriteFile(state.RunnerPath, []byte("old-runner"), 0o755); err != nil {
|
|
t.Fatalf("write stale runner failed: %v", err)
|
|
}
|
|
|
|
if err := manager.EnsureInstalled(context.Background()); err != nil {
|
|
t.Fatalf("EnsureInstalled returned error: %v", err)
|
|
}
|
|
|
|
runnerData, err := os.ReadFile(state.RunnerPath)
|
|
if err != nil {
|
|
t.Fatalf("read refreshed runner failed: %v", err)
|
|
}
|
|
if string(runnerData) != string(runnerScriptContent) {
|
|
t.Fatalf("expected runner script to be refreshed")
|
|
}
|
|
}
|