mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-06 20:03:05 +08:00
- 新增 JMX/Endpoint/Agent 三种 JVM 连接模式与配置归一化链路 - 支持资源浏览、变更预览、写入应用、审计记录与只读约束 - 接入 AI 结构化写入计划与诊断计划回填能力 - 新增 Agent Bridge、Arthas Tunnel、JMX Helper 诊断传输实现 - 增加诊断控制台、命令模板、输出历史与自动补全交互 - 补齐前后端契约、运行夹具与 JVM 相关回归测试
306 lines
8.7 KiB
Go
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,
|
|
},
|
|
},
|
|
}
|
|
}
|