Files
MyGoNavi/internal/jvm/agent_provider_test.go
Syngnat 6f14e827ab feat(jvm): 完成资源治理与诊断增强
- 新增 JMX/Endpoint/Agent 三种 JVM 连接模式与配置归一化链路
- 支持资源浏览、变更预览、写入应用、审计记录与只读约束
- 接入 AI 结构化写入计划与诊断计划回填能力
- 新增 Agent Bridge、Arthas Tunnel、JMX Helper 诊断传输实现
- 增加诊断控制台、命令模板、输出历史与自动补全交互
- 补齐前后端契约、运行夹具与 JVM 相关回归测试
2026-04-24 16:45:34 +08:00

306 lines
8.7 KiB
Go

package jvm
import (
"bufio"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"GoNavi-Wails/internal/connection"
)
func TestAgentProviderListResourcesBuildsRequestAndDecodesResponse(t *testing.T) {
provider := NewAgentProvider()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Fatalf("expected GET request, got %s", r.Method)
}
if r.URL.Path != "/gonavi/agent/jvm/resources" {
t.Fatalf("expected path /gonavi/agent/jvm/resources, got %s", r.URL.Path)
}
if got := r.URL.Query().Get("parentPath"); got != "/runtime/cache" {
t.Fatalf("expected parentPath /runtime/cache, got %q", got)
}
if got := r.Header.Get("X-API-Key"); got != "secret-token" {
t.Fatalf("expected X-API-Key header to pass through, got %q", got)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode([]ResourceSummary{{
ID: "agent.cache",
Kind: "folder",
Name: "Agent Cache",
Path: "/runtime/cache",
ProviderMode: ModeAgent,
CanRead: true,
CanWrite: true,
HasChildren: true,
}})
}))
defer server.Close()
items, err := provider.ListResources(context.Background(), newAgentProviderTestConfig(server.URL+"/gonavi/agent/jvm", 3), "/runtime/cache")
if err != nil {
t.Fatalf("ListResources returned error: %v", err)
}
if len(items) != 1 {
t.Fatalf("expected 1 resource, got %#v", items)
}
if items[0].ProviderMode != ModeAgent || items[0].Path != "/runtime/cache" {
t.Fatalf("unexpected resource payload: %#v", items[0])
}
}
func TestAgentProviderRealAgentRoundTrip(t *testing.T) {
if _, err := exec.LookPath("java"); err != nil {
t.Skipf("java 不可用,跳过真实 Agent 集成测试: %v", err)
}
if _, err := exec.LookPath("javac"); err != nil {
t.Skipf("javac 不可用,跳过真实 Agent 集成测试: %v", err)
}
if _, err := exec.LookPath("jar"); err != nil {
t.Skipf("jar 不可用,跳过真实 Agent 集成测试: %v", err)
}
provider := NewAgentProvider()
fixture := startAgentFixture(t)
cfg := newAgentProviderTestConfig(fixture.baseURL+"/gonavi/agent/jvm", 5)
waitForTest(t, 10*time.Second, func() error {
return provider.TestConnection(context.Background(), cfg)
})
caps, err := provider.ProbeCapabilities(context.Background(), cfg)
if err != nil {
t.Fatalf("ProbeCapabilities returned error: %v", err)
}
if len(caps) != 1 || !caps[0].CanBrowse || !caps[0].CanWrite || !caps[0].CanPreview {
t.Fatalf("unexpected capabilities: %#v", caps)
}
root, err := provider.ListResources(context.Background(), cfg, "")
if err != nil {
t.Fatalf("ListResources(root) returned error: %v", err)
}
if len(root) != 1 || root[0].Name != "Agent Cache" {
t.Fatalf("unexpected root resources: %#v", root)
}
children, err := provider.ListResources(context.Background(), cfg, root[0].Path)
if err != nil {
t.Fatalf("ListResources(cache) returned error: %v", err)
}
if len(children) != 1 || children[0].Name != "user:1001" {
t.Fatalf("unexpected child resources: %#v", children)
}
entry := children[0]
before, err := provider.GetValue(context.Background(), cfg, entry.Path)
if err != nil {
t.Fatalf("GetValue(before) returned error: %v", err)
}
valueMap, ok := before.Value.(map[string]any)
if !ok {
t.Fatalf("expected JSON object snapshot, got %#v", before.Value)
}
if valueMap["status"] != "cold" {
t.Fatalf("expected initial status cold, got %#v", before.Value)
}
preview, err := provider.PreviewChange(context.Background(), cfg, ChangeRequest{
ProviderMode: ModeAgent,
ResourceID: entry.Path,
Action: "put",
Reason: "预热用户缓存",
ExpectedVersion: before.Version,
Payload: map[string]any{
"status": "warm",
"score": 99,
},
})
if err != nil {
t.Fatalf("PreviewChange returned error: %v", err)
}
if !preview.Allowed || preview.After.ResourceID != entry.Path {
t.Fatalf("unexpected preview payload: %#v", preview)
}
result, err := provider.ApplyChange(context.Background(), cfg, ChangeRequest{
ProviderMode: ModeAgent,
ResourceID: entry.Path,
Action: "put",
Reason: "预热用户缓存",
ExpectedVersion: before.Version,
Payload: map[string]any{
"status": "warm",
"score": 99,
},
})
if err != nil {
t.Fatalf("ApplyChange returned error: %v", err)
}
if result.Status != "applied" {
t.Fatalf("unexpected apply payload: %#v", result)
}
after, err := provider.GetValue(context.Background(), cfg, entry.Path)
if err != nil {
t.Fatalf("GetValue(after) returned error: %v", err)
}
afterMap, ok := after.Value.(map[string]any)
if !ok {
t.Fatalf("expected JSON object snapshot after apply, got %#v", after.Value)
}
if afterMap["status"] != "warm" {
t.Fatalf("expected status warm after apply, got %#v", after.Value)
}
}
type agentFixtureProcess struct {
port int
baseURL string
cmd *exec.Cmd
}
func startAgentFixture(t *testing.T) agentFixtureProcess {
t.Helper()
javaBin, err := exec.LookPath("java")
if err != nil {
t.Fatalf("look up java failed: %v", err)
}
javacBin, err := exec.LookPath("javac")
if err != nil {
t.Fatalf("look up javac failed: %v", err)
}
jarBin, err := exec.LookPath("jar")
if err != nil {
t.Fatalf("look up jar failed: %v", err)
}
classesDir := filepath.Join(t.TempDir(), "agent-fixture-classes")
sourceRoot := filepath.Join(testRepoRoot(t), "internal", "jvm", "testdata", "agentfixture", "src")
javaFiles, err := filepath.Glob(filepath.Join(sourceRoot, "com", "gonavi", "fixture", "*.java"))
if err != nil {
t.Fatalf("glob agent fixture sources failed: %v", err)
}
if len(javaFiles) == 0 {
t.Fatalf("expected agent fixture java files under %s", sourceRoot)
}
compileCmd := exec.Command(javacBin, append([]string{"-d", classesDir}, javaFiles...)...)
output, err := compileCmd.CombinedOutput()
if err != nil {
t.Fatalf("compile agent fixture failed: %v\n%s", err, strings.TrimSpace(string(output)))
}
manifestPath := filepath.Join(t.TempDir(), "agent-manifest.mf")
manifest := strings.Join([]string{
"Premain-Class: com.gonavi.fixture.GoNaviTestAgent",
"Agent-Class: com.gonavi.fixture.GoNaviTestAgent",
"Can-Redefine-Classes: false",
"Can-Retransform-Classes: false",
"",
}, "\n")
if err := os.WriteFile(manifestPath, []byte(manifest), 0o644); err != nil {
t.Fatalf("write agent manifest failed: %v", err)
}
agentJar := filepath.Join(t.TempDir(), "gonavi-test-agent.jar")
jarCmd := exec.Command(jarBin, "cmf", manifestPath, agentJar, "-C", classesDir, "com")
output, err = jarCmd.CombinedOutput()
if err != nil {
t.Fatalf("package agent jar failed: %v\n%s", err, strings.TrimSpace(string(output)))
}
port := reserveTCPPort(t)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cmd := exec.CommandContext(
ctx,
javaBin,
fmt.Sprintf("-javaagent:%s=port=%d,token=secret-token", agentJar, port),
"-cp",
classesDir,
"com.gonavi.fixture.AgentHostApp",
)
stdout, err := cmd.StdoutPipe()
if err != nil {
t.Fatalf("agent fixture stdout pipe failed: %v", err)
}
if err := cmd.Start(); err != nil {
t.Fatalf("start agent fixture failed: %v", err)
}
t.Cleanup(func() {
cancel()
_ = cmd.Wait()
})
ready := make(chan error, 1)
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
if strings.TrimSpace(scanner.Text()) == "AGENT_READY" {
ready <- nil
return
}
}
if err := scanner.Err(); err != nil {
ready <- fmt.Errorf("agent fixture readiness read failed: %w", err)
return
}
ready <- fmt.Errorf("agent fixture terminated before readiness signal")
}()
select {
case err := <-ready:
if err != nil {
t.Fatalf("wait agent fixture ready failed: %v", err)
}
case <-time.After(20 * time.Second):
t.Fatal("agent fixture did not become ready within 20s")
}
waitForTest(t, 10*time.Second, func() error {
conn, dialErr := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 500*time.Millisecond)
if dialErr != nil {
return dialErr
}
_ = conn.Close()
return nil
})
return agentFixtureProcess{
port: port,
baseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
cmd: cmd,
}
}
func newAgentProviderTestConfig(baseURL string, timeoutSeconds int) connection.ConnectionConfig {
readOnly := false
return connection.ConnectionConfig{
Type: "jvm",
Timeout: timeoutSeconds,
JVM: connection.JVMConfig{
ReadOnly: &readOnly,
AllowedModes: []string{ModeAgent},
PreferredMode: ModeAgent,
Agent: connection.JVMAgentConfig{
BaseURL: baseURL,
APIKey: "secret-token",
TimeoutSeconds: timeoutSeconds,
},
},
}
}