Implement the complete homelab-deploy system with three operational modes: - Listener mode: Runs on NixOS hosts as a systemd service, subscribes to NATS subjects with configurable templates, executes nixos-rebuild on deployment requests with concurrency control - MCP mode: MCP server exposing deploy, deploy_admin, and list_hosts tools for AI assistants with tiered access control - CLI mode: Manual deployment commands with subject alias support via environment variables Key components: - internal/messages: Request/response types with validation - internal/nats: Client wrapper with NKey authentication - internal/deploy: Executor with timeout and lock for concurrency - internal/listener: Subject template expansion and request handling - internal/cli: Deploy logic with alias resolution - internal/mcp: MCP server with mcp-go integration - nixos/module.nix: NixOS module with hardened systemd service Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
110 lines
2.4 KiB
Go
110 lines
2.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
|
|
)
|
|
|
|
func TestDeployResult_AllSucceeded(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
responses []*messages.DeployResponse
|
|
errors []error
|
|
want bool
|
|
}{
|
|
{
|
|
name: "all completed",
|
|
responses: []*messages.DeployResponse{
|
|
{Hostname: "host1", Status: messages.StatusCompleted},
|
|
{Hostname: "host2", Status: messages.StatusCompleted},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "one failed",
|
|
responses: []*messages.DeployResponse{
|
|
{Hostname: "host1", Status: messages.StatusCompleted},
|
|
{Hostname: "host2", Status: messages.StatusFailed},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "one rejected",
|
|
responses: []*messages.DeployResponse{
|
|
{Hostname: "host1", Status: messages.StatusRejected},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "no responses",
|
|
responses: []*messages.DeployResponse{},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "has errors",
|
|
responses: []*messages.DeployResponse{
|
|
{Hostname: "host1", Status: messages.StatusCompleted},
|
|
},
|
|
errors: []error{nil}, // placeholder error
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := &DeployResult{
|
|
Responses: tc.responses,
|
|
Errors: tc.errors,
|
|
}
|
|
got := r.AllSucceeded()
|
|
if got != tc.want {
|
|
t.Errorf("AllSucceeded() = %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeployResult_HostCount(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
responses []*messages.DeployResponse
|
|
want int
|
|
}{
|
|
{
|
|
name: "no responses",
|
|
responses: []*messages.DeployResponse{},
|
|
want: 0,
|
|
},
|
|
{
|
|
name: "unique hosts",
|
|
responses: []*messages.DeployResponse{
|
|
{Hostname: "host1"},
|
|
{Hostname: "host2"},
|
|
{Hostname: "host3"},
|
|
},
|
|
want: 3,
|
|
},
|
|
{
|
|
name: "duplicate hosts",
|
|
responses: []*messages.DeployResponse{
|
|
{Hostname: "host1", Status: messages.StatusStarted},
|
|
{Hostname: "host1", Status: messages.StatusCompleted},
|
|
{Hostname: "host2", Status: messages.StatusStarted},
|
|
{Hostname: "host2", Status: messages.StatusCompleted},
|
|
},
|
|
want: 2,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := &DeployResult{Responses: tc.responses}
|
|
got := r.HostCount()
|
|
if got != tc.want {
|
|
t.Errorf("HostCount() = %d, want %d", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|