Files
2026-07-20 17:05:52 +00:00

172 lines
6.1 KiB
Go

//go:build linux
package scenario
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/nezhahq/nezha/integration/agentcompat/internal/agent"
"github.com/nezhahq/nezha/integration/agentcompat/internal/client"
"github.com/nezhahq/nezha/integration/agentcompat/internal/contract"
"github.com/nezhahq/nezha/integration/agentcompat/internal/dashboard"
"github.com/nezhahq/nezha/integration/agentcompat/internal/fixture"
)
const transferScenarioName = "transfer-100mib"
var ErrTransferHashFault = errors.New("transfer scenario: injected hash mismatch")
type TransferInput struct {
Paths contract.Paths
Fault contract.Fault
}
type Transfer struct{}
func (scenario Transfer) Run(ctx context.Context, input TransferInput) (Result, error) {
result, _, err := scenario.RunWithEvidence(ctx, input)
return result, err
}
func (Transfer) RunWithEvidence(ctx context.Context, input TransferInput) (result Result, transferEvidence TransferEvidence, runErr error) {
assertions := NewAssertionSet()
dashboardInstance, err := dashboard.Start(ctx, dashboard.StartConfig{SourceDir: input.Paths.NezhaSource().String(), ReceiptGate: true})
if err != nil {
result, runErr = transferFinish(assertions, err)
return result, transferEvidence, runErr
}
var agentInstance *agent.Agent
var agentWorkspaceRoot string
dashboardWorkspaceRoot := dashboardInstance.WorkspaceRoot()
defer func() {
cleanupContext, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
cleanupErr := stopTransferProcesses(cleanupContext, agentInstance, dashboardInstance)
residueErr := transferWorkspaceResidue(agentWorkspaceRoot, dashboardWorkspaceRoot)
cleanupErr = errors.Join(cleanupErr, residueErr)
assertions.Record("process listener and workspace cleanup completed", cleanupErr == nil, errorText(cleanupErr))
result.CleanupOK = cleanupErr == nil
if cleanupErr != nil {
result, runErr = transferFinish(assertions, errors.Join(runErr, cleanupErr))
result.CleanupOK = false
} else {
result.Assertions = assertions.Results()
}
}()
agentInstance, err = agent.Start(ctx, agent.AgentStartConfig{
SourceDir: input.Paths.AgentSource().String(),
Endpoint: dashboardInstance.Endpoint(),
Secret: dashboardInstance.AgentSecret(),
UUID: "00000000-0000-0000-0000-000000000216",
})
if err != nil {
result, runErr = transferFinish(assertions, err)
return result, transferEvidence, runErr
}
agentWorkspaceRoot = agentInstance.WorkspaceRoot()
if err := dashboardInstance.WaitForReceiptAccepted(ctx); err != nil {
result, runErr = transferFinish(assertions, err)
return result, transferEvidence, runErr
}
if err := dashboardInstance.ReleaseReceipt(ctx); err != nil {
result, runErr = transferFinish(assertions, err)
return result, transferEvidence, runErr
}
readiness, err := agentInstance.WaitReady(ctx, dashboardInstance)
if err != nil {
result, runErr = transferFinish(assertions, err)
return result, transferEvidence, runErr
}
serverID, err := transferServerID(ctx, dashboardInstance.Clients().MCP, readiness.UUID)
if err != nil {
result, runErr = transferFinish(assertions, err)
return result, transferEvidence, runErr
}
root, err := fixture.NewAgentRoot(agentInstance.WorkspaceRoot(), "transfer-files")
if err != nil {
result, runErr = transferFinish(assertions, err)
return result, transferEvidence, runErr
}
sentinels, err := newTransferSentinels(root, agentInstance.WorkspaceRoot())
if err != nil {
result, runErr = transferFinish(assertions, err)
return result, transferEvidence, runErr
}
defer sentinels.close()
execution := transferExecution{
client: dashboardInstance.Clients().MCP,
serverID: serverID,
root: root,
residueScope: transferResidueScope{AgentRoot: agentInstance.WorkspaceRoot(), DashboardPID: dashboardInstance.PID()},
sentinels: sentinels,
}
transferEvidence, err = execution.run(ctx, assertions, input.Fault)
result, runErr = transferFinish(assertions, err)
return result, transferEvidence, runErr
}
func transferWorkspaceResidue(workspaceRoots ...string) error {
var residueErr error
for _, workspaceRoot := range workspaceRoots {
if workspaceRoot == "" {
continue
}
if _, err := os.Stat(workspaceRoot); err == nil {
residueErr = errors.Join(residueErr, fmt.Errorf("workspace remains: %s", workspaceRoot))
} else if !errors.Is(err, os.ErrNotExist) {
residueErr = errors.Join(residueErr, fmt.Errorf("inspect workspace %s: %w", workspaceRoot, err))
}
}
return residueErr
}
func stopTransferProcesses(ctx context.Context, agentInstance *agent.Agent, dashboardInstance *dashboard.Dashboard) error {
var cleanupErr error
if agentInstance != nil {
stopErr := agentInstance.Stop(ctx)
receipt := agentInstance.CleanupReceipt()
if stopErr != nil || !receipt.Passed || receipt.Forced {
cleanupErr = errors.Join(cleanupErr, stopErr, errors.New("agent cleanup receipt failed"))
}
}
stopErr := dashboardInstance.Stop(ctx)
receipt := dashboardInstance.CleanupReceipt()
if stopErr != nil || !receipt.Passed || receipt.Forced {
cleanupErr = errors.Join(cleanupErr, stopErr, errors.New("dashboard cleanup receipt failed"))
}
return cleanupErr
}
func transferFinish(assertions *AssertionSet, runErr error) (Result, error) {
for _, assertion := range assertions.assertions {
if !assertion.Passed && runErr == nil {
runErr = errors.New(assertion.Name + ": " + assertion.Details)
}
}
result := Result{Name: transferScenarioName, Passed: runErr == nil, Assertions: assertions.Results()}
if runErr != nil {
result.Error = errorText(runErr)
}
return result, runErr
}
func transferServerID(ctx context.Context, mcpClient *client.Client, uuid string) (uint64, error) {
servers, err := client.CallTool[client.ServerListArguments, client.ServerListResult](ctx, mcpClient, client.ToolCall[client.ServerListArguments]{
Name: "server.list", Arguments: client.ServerListArguments{OnlineOnly: true},
})
if err != nil {
return 0, fmt.Errorf("list transfer Agents: %w", err)
}
for _, server := range servers.StructuredContent.Servers {
if server.UUID == uuid && server.Online {
return server.ID, nil
}
}
return 0, errors.New("online transfer Agent not found")
}