mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-29 03:50:18 +00:00
138 lines
4.3 KiB
Go
138 lines
4.3 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestFileTools_Registry(t *testing.T) {
|
|
exec := NewPulseToolExecutor(ExecutorConfig{})
|
|
exec.registerFileTools()
|
|
tools := exec.registry.ListTools(ControlLevelControlled)
|
|
|
|
found := false
|
|
for _, tool := range tools {
|
|
if tool.Name == "pulse_file_edit" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
}
|
|
|
|
func TestExecuteFileEdit_Validation(t *testing.T) {
|
|
exec := NewPulseToolExecutor(ExecutorConfig{})
|
|
|
|
tests := []struct {
|
|
name string
|
|
args map[string]interface{}
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "Missing Path",
|
|
args: map[string]interface{}{"action": "read", "target_host": "h1"},
|
|
wantErr: "path is required",
|
|
},
|
|
{
|
|
name: "Missing TargetHost",
|
|
args: map[string]interface{}{"action": "read", "path": "/etc/config"},
|
|
wantErr: "target_host is required",
|
|
},
|
|
{
|
|
name: "Relative Path",
|
|
args: map[string]interface{}{"action": "read", "path": "etc/config", "target_host": "h1"},
|
|
wantErr: "path must be absolute",
|
|
},
|
|
{
|
|
name: "Invalid Docker Container",
|
|
args: map[string]interface{}{"action": "read", "path": "/f", "target_host": "h1", "docker_container": "bad name!"},
|
|
wantErr: "invalid character",
|
|
},
|
|
{
|
|
name: "Unknown Action",
|
|
args: map[string]interface{}{"action": "dance", "path": "/f", "target_host": "h1"},
|
|
wantErr: "unknown action",
|
|
},
|
|
{
|
|
name: "Append Missing Content",
|
|
args: map[string]interface{}{"action": "append", "path": "/f", "target_host": "h1"},
|
|
wantErr: "content is required",
|
|
},
|
|
{
|
|
name: "Write Missing Content",
|
|
args: map[string]interface{}{"action": "write", "path": "/f", "target_host": "h1"},
|
|
wantErr: "content is required",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
res, err := exec.executeFileEdit(context.Background(), tc.args)
|
|
// Implementation returns error result (NewErrorResult), err is usually nil
|
|
assert.NoError(t, err)
|
|
assert.True(t, res.IsError)
|
|
assert.NotEmpty(t, res.Content)
|
|
assert.Contains(t, res.Content[0].Text, tc.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateWriteExecutionContext_Blocked(t *testing.T) {
|
|
// This tests the security invariant: don't write to host when target is LXC
|
|
|
|
state := models.StateSnapshot{
|
|
Containers: []models.Container{
|
|
{VMID: 100, Name: "my-lxc", Node: "pve1", Status: "running"},
|
|
},
|
|
Nodes: []models.Node{
|
|
{Name: "pve1"},
|
|
},
|
|
}
|
|
|
|
stateProvider := &mockStateProvider{state: state}
|
|
agentServer := &mockAgentServer{}
|
|
agentServer.agents = []agentexec.ConnectedAgent{
|
|
{AgentID: "agent-1", Hostname: "my-lxc"}, // Agent hostname matches LXC name! Risks confusion.
|
|
}
|
|
|
|
exec := NewPulseToolExecutor(ExecutorConfig{
|
|
AgentServer: agentServer,
|
|
StateProvider: stateProvider,
|
|
})
|
|
|
|
// We expect resolveTargetForCommandFull to map "my-lxc" to agent-1
|
|
// But since agent-1 matches hostname "my-lxc" directly, it might be resolved as "direct".
|
|
// Wait, resolveTargetForCommandFull logic logic isn't mocked, it's real code in executor.go using state.
|
|
|
|
// If ResolveResource says it's an LXC on pve1.
|
|
// And FindAgent says agent-1 (hostname=my-lxc) exists.
|
|
// The fallback logic in resolveTarget might pick agent-1 if it matches hostname directly?
|
|
|
|
// Actually, `resolveTargetForCommandFull` calls `ResolveResource`.
|
|
// Then checks if agent exists for that resource.
|
|
|
|
// If I want to trigger the BLOCK, I need:
|
|
// 1. ResolveResource returns LXC.
|
|
// 2. Routing returns "direct" transport on "host" type.
|
|
|
|
// This happens if `findAgent` returns an agent that is NOT the node agent?
|
|
// Or if the node agent is found via hostname match of the LXC name?
|
|
|
|
// Let's manually invoke validateWriteExecutionContext to test logic in isolation.
|
|
|
|
err := exec.validateWriteExecutionContext("my-lxc", CommandRoutingResult{
|
|
Transport: "direct",
|
|
TargetType: "host", // Agent matched directly, assuming it's a host
|
|
AgentHostname: "my-lxc",
|
|
AgentID: "agent-1",
|
|
ResolvedKind: "lxc", // But resolving matched LXC
|
|
ResolvedNode: "pve1",
|
|
})
|
|
|
|
assert.NotNil(t, err)
|
|
assert.Contains(t, err.Message, "write would execute on the Proxmox node instead of inside the lxc")
|
|
}
|