mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-21 10:40:13 +00:00
test(agentcompat): add agent runtime harness
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
//go:build linux && agentcompat
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/client"
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/dashboard"
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/fixture"
|
||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/testpaths"
|
||||
)
|
||||
|
||||
func TestAgent_BecomesOnlineOverH2C(t *testing.T) {
|
||||
// Given
|
||||
dashboardInstance := startTestDashboardWithReceiptGate(t)
|
||||
agentInstance := startTestAgent(t, dashboardInstance, AgentStartConfig{UUID: "00000000-0000-0000-0000-000000000081"})
|
||||
receiptAccepted := make(chan error, 1)
|
||||
go func() { receiptAccepted <- dashboardInstance.WaitForReceiptAccepted(t.Context()) }()
|
||||
select {
|
||||
case err := <-receiptAccepted:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("timed out waiting for withheld state receipt")
|
||||
}
|
||||
serverBeforeRelease := requireOnlineServer(t, dashboardInstance, agentInstance.UUID())
|
||||
require.NotZero(t, serverBeforeRelease.LastActive)
|
||||
stateGeneration := dashboardInstance.StateGeneration(serverBeforeRelease.ID, agentInstance.UUID())
|
||||
require.NotZero(t, stateGeneration)
|
||||
require.NoError(t, dashboardInstance.WaitForStateGeneration(t.Context(), serverBeforeRelease.ID, agentInstance.UUID(), stateGeneration, 1))
|
||||
stateTwoBeforeRelease, cancelStateTwo := context.WithTimeout(t.Context(), 1500*time.Millisecond)
|
||||
require.ErrorIs(t, dashboardInstance.WaitForStateGeneration(stateTwoBeforeRelease, serverBeforeRelease.ID, agentInstance.UUID(), stateGeneration, 2), context.DeadlineExceeded)
|
||||
cancelStateTwo()
|
||||
require.Equal(t, uint64(1), dashboardInstance.ReceiptAcceptedCount())
|
||||
require.NoError(t, dashboardInstance.ReleaseReceipt(t.Context()))
|
||||
secondState := make(chan error, 1)
|
||||
go func() { secondState <- dashboardInstance.WaitForSecondState(t.Context()) }()
|
||||
|
||||
// When
|
||||
readiness, err := agentInstance.WaitReady(t.Context(), dashboardInstance)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, readiness.ServerID)
|
||||
require.Equal(t, serverBeforeRelease.ID, readiness.ServerID)
|
||||
require.Equal(t, agentInstance.UUID(), readiness.UUID)
|
||||
require.Equal(t, "v2.1.0", readiness.Version)
|
||||
require.True(t, readiness.VersionObserved)
|
||||
require.True(t, readiness.RequestTaskEstablished)
|
||||
require.True(t, readiness.StateReceiptObserved)
|
||||
require.NoError(t, <-secondState)
|
||||
require.Equal(t, uint64(2), dashboardInstance.ReceiptAcceptedCount())
|
||||
require.NoError(t, dashboardInstance.WaitForInfo2(t.Context(), serverBeforeRelease.ID, agentInstance.UUID()))
|
||||
require.NotNil(t, readiness.Host)
|
||||
require.NotNil(t, readiness.State)
|
||||
require.True(t, readiness.Online)
|
||||
}
|
||||
|
||||
func TestAgent_BecomesOnlineOverVerifiedTLS(t *testing.T) {
|
||||
// Given
|
||||
dashboardInstance := startTestDashboard(t, true)
|
||||
agentInstance := startTestAgent(t, dashboardInstance, AgentStartConfig{
|
||||
UUID: "00000000-0000-0000-0000-000000000082",
|
||||
TLS: true,
|
||||
CAFilePath: dashboardInstance.TLSCACertificatePath(),
|
||||
})
|
||||
|
||||
// When
|
||||
readiness, err := agentInstance.WaitReady(t.Context(), dashboardInstance)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
require.True(t, readiness.Online)
|
||||
require.NotNil(t, readiness.Host)
|
||||
require.NotNil(t, readiness.State)
|
||||
require.WithinDuration(t, time.Now(), readiness.LastActive, 30*time.Second)
|
||||
var host struct {
|
||||
Platform string `json:"platform"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(readiness.Host, &host))
|
||||
require.Equal(t, "v2.1.0", host.Version)
|
||||
require.NotEmpty(t, host.Platform)
|
||||
var state map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(readiness.State, &state))
|
||||
require.Contains(t, state, "uptime")
|
||||
}
|
||||
|
||||
func TestAgent_RejectsUnknownTLSAuthority(t *testing.T) {
|
||||
// Given
|
||||
dashboardInstance := startTestDashboard(t, true)
|
||||
wrongCAPath := filepath.Join(t.TempDir(), "wrong-ca.crt")
|
||||
wrongFixture, fixtureErr := fixture.NewLocalTLSFixture(time.Now())
|
||||
require.NoError(t, fixtureErr)
|
||||
require.NoError(t, os.WriteFile(wrongCAPath, wrongFixture.CAPEM(), 0o600))
|
||||
agentInstance := startTestAgent(t, dashboardInstance, AgentStartConfig{
|
||||
UUID: "00000000-0000-0000-0000-000000000086",
|
||||
TLS: true,
|
||||
Debug: true,
|
||||
CAFilePath: wrongCAPath,
|
||||
})
|
||||
|
||||
// When
|
||||
readinessContext, cancel := context.WithTimeout(t.Context(), 8*time.Second)
|
||||
defer cancel()
|
||||
_, err := agentInstance.WaitReady(readinessContext, dashboardInstance)
|
||||
|
||||
// Then
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
logData, logErr := os.ReadFile(agentInstance.LogPath())
|
||||
require.NoError(t, logErr)
|
||||
require.Contains(t, string(logData), "x509: certificate signed by unknown authority")
|
||||
config, readErr := os.ReadFile(agentInstance.ConfigPath())
|
||||
require.NoError(t, readErr)
|
||||
stopContext, stopCancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
require.NoError(t, agentInstance.Stop(stopContext))
|
||||
stopCancel()
|
||||
require.Contains(t, string(config), "insecure_tls: false")
|
||||
require.NotContains(t, string(config), "insecure_tls: true")
|
||||
}
|
||||
|
||||
func TestAgent_AssertNeverOnlineFailsClosedWhenDashboardUnavailable(t *testing.T) {
|
||||
// Given
|
||||
dashboardInstance := startTestDashboard(t, false)
|
||||
agentInstance := startTestAgent(t, dashboardInstance, AgentStartConfig{
|
||||
UUID: "00000000-0000-0000-0000-000000000087",
|
||||
Secret: "wrong-agent-secret",
|
||||
})
|
||||
require.NoError(t, dashboardInstance.Stop(context.Background()))
|
||||
|
||||
// When
|
||||
err := agentInstance.AssertNeverOnline(t.Context(), dashboardInstance, time.Second)
|
||||
|
||||
// Then
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAgent_RejectsInvalidSecret(t *testing.T) {
|
||||
// Given
|
||||
dashboardInstance := startTestDashboard(t, false)
|
||||
agentInstance := startTestAgent(t, dashboardInstance, AgentStartConfig{
|
||||
UUID: "00000000-0000-0000-0000-000000000083",
|
||||
Secret: "wrong-agent-secret",
|
||||
})
|
||||
|
||||
// When
|
||||
err := agentInstance.AssertNeverOnline(t.Context(), dashboardInstance, 2*time.Second)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
stopContext, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, agentInstance.Stop(stopContext))
|
||||
}
|
||||
|
||||
func TestAgent_StopsCleanly(t *testing.T) {
|
||||
// Given
|
||||
dashboardInstance := startTestDashboard(t, false)
|
||||
agentInstance := startTestAgent(t, dashboardInstance, AgentStartConfig{UUID: "00000000-0000-0000-0000-000000000084"})
|
||||
require.NoError(t, waitForAgentReady(t, agentInstance, dashboardInstance))
|
||||
|
||||
// When
|
||||
stopContext, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
err := agentInstance.Stop(stopContext)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
require.True(t, agentInstance.CleanupReceipt().Passed)
|
||||
require.False(t, agentInstance.CleanupReceipt().Forced)
|
||||
}
|
||||
|
||||
func TestAgent_StopRemovesAgentFromOnlineOnlyList(t *testing.T) {
|
||||
// Given
|
||||
dashboardInstance := startTestDashboard(t, false)
|
||||
agentInstance := startTestAgent(t, dashboardInstance, AgentStartConfig{UUID: "00000000-0000-0000-0000-000000000088"})
|
||||
require.NoError(t, waitForAgentReady(t, agentInstance, dashboardInstance))
|
||||
stopContext, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, agentInstance.Stop(stopContext))
|
||||
|
||||
// When
|
||||
list, err := client.CallTool[serverListArguments, serverListResult](
|
||||
t.Context(), dashboardInstance.Clients().MCP,
|
||||
client.ToolCall[serverListArguments]{Name: "server.list", Arguments: serverListArguments{OnlineOnly: true}},
|
||||
)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
foundOnline := false
|
||||
for _, server := range list.StructuredContent.Servers {
|
||||
if server.UUID == agentInstance.UUID() {
|
||||
foundOnline = true
|
||||
}
|
||||
}
|
||||
require.False(t, foundOnline)
|
||||
}
|
||||
|
||||
func startTestDashboard(t *testing.T, enableTLS bool) *dashboard.Dashboard {
|
||||
t.Helper()
|
||||
sourceDir, err := testpaths.NezhaSource(t.Name())
|
||||
require.NoError(t, err)
|
||||
instance, err := dashboard.Start(t.Context(), dashboard.StartConfig{SourceDir: sourceDir, EnableTLS: enableTLS, ReadinessTimeout: readinessBudget})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
cleanupContext, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, instance.Stop(cleanupContext))
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
func startTestDashboardWithReceiptGate(t *testing.T) *dashboard.Dashboard {
|
||||
t.Helper()
|
||||
sourceDir, err := testpaths.NezhaSource(t.Name())
|
||||
require.NoError(t, err)
|
||||
instance, err := dashboard.Start(t.Context(), dashboard.StartConfig{SourceDir: sourceDir, ReceiptGate: true, ReadinessTimeout: readinessBudget})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
cleanupContext, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, instance.Stop(cleanupContext))
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
func startTestAgent(t *testing.T, dashboardInstance *dashboard.Dashboard, config AgentStartConfig) *Agent {
|
||||
t.Helper()
|
||||
if config.Secret == "" {
|
||||
config.Secret = dashboardInstance.AgentSecret()
|
||||
}
|
||||
if config.TLS {
|
||||
config.Endpoint = dashboardInstance.TLSEndpoint()
|
||||
} else {
|
||||
config.Endpoint = dashboardInstance.Endpoint()
|
||||
}
|
||||
instance, err := Start(t.Context(), AgentStartConfig{
|
||||
SourceDir: testAgentSourceDir(t),
|
||||
Endpoint: config.Endpoint,
|
||||
Secret: config.Secret,
|
||||
UUID: config.UUID,
|
||||
TLS: config.TLS,
|
||||
Debug: config.Debug,
|
||||
CAFilePath: config.CAFilePath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
cleanupContext, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
require.NoError(t, instance.Stop(cleanupContext))
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
func testAgentSourceDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
if sourceDir := os.Getenv("AGENT_SOURCE"); sourceDir != "" {
|
||||
return sourceDir
|
||||
}
|
||||
nezhaSource, err := testpaths.NezhaSource(t.Name())
|
||||
require.NoError(t, err)
|
||||
agentSourceDir, err := testpaths.AgentSource(nezhaSource)
|
||||
require.NoError(t, err)
|
||||
return agentSourceDir
|
||||
}
|
||||
|
||||
func requireOnlineServer(t *testing.T, dashboardInstance *dashboard.Dashboard, uuid string) serverListItem {
|
||||
t.Helper()
|
||||
list, err := client.CallTool[serverListArguments, serverListResult](t.Context(), dashboardInstance.Clients().MCP, client.ToolCall[serverListArguments]{Name: "server.list", Arguments: serverListArguments{OnlineOnly: true}})
|
||||
require.NoError(t, err)
|
||||
for _, server := range list.StructuredContent.Servers {
|
||||
if server.UUID == uuid {
|
||||
return server
|
||||
}
|
||||
}
|
||||
t.Fatalf("server %q is not online", uuid)
|
||||
return serverListItem{}
|
||||
}
|
||||
|
||||
func waitForAgentReady(t *testing.T, instance *Agent, dashboardInstance *dashboard.Dashboard) error {
|
||||
t.Helper()
|
||||
readinessContext, cancel := context.WithTimeout(t.Context(), readinessBudget)
|
||||
defer cancel()
|
||||
_, err := instance.WaitReady(readinessContext, dashboardInstance)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user