The second half of leadership. A standby that refuses to place but still holds an agent's stream is worse than useless: commands travel down the stream the agent opened, so the leader's placements fail with "no agent connected" for a node that is plainly connected. So a standby refuses the stream, before reading the opening message, and says why. And an agent takes a list rather than an address, trying each in turn — a standby refusing, a stopped control plane refusing the connection and a crashed one never answering are the same thing from a node, and the answer to all three is somewhere else. `server` and `servers` are both accepted, with `server` first when both are given: every agent configuration written before there could be a second control plane names one address, and an operator adding a standby expects the one already working to stay preferred. With a single address the behaviour is exactly what it was, including the backoff — which is skipped only while there is another address that has not just been tried, because the delay exists to stop hammering one control plane and the next one has not been hammered. Removed a check that asked whether one field was empty, which is how the first run of these tests failed: the list was populated and `server` was not, and an agent configured entirely correctly was refused by its own validation. The tests run a real agent against a real service over h2c. The first attempt used a plain HTTP server, and the refusal arrived as an HTTP/2 framing error that said nothing about why — which is exactly what an operator would have seen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
91 lines
2.7 KiB
Go
91 lines
2.7 KiB
Go
package agent
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/tlsconf"
|
|
)
|
|
|
|
func normalized(t *testing.T, c Config) Config {
|
|
t.Helper()
|
|
if err := c.normalizeServerURL(); err != nil {
|
|
t.Fatalf("normalising %+v: %v", c, err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func TestOneServerStillWorksExactlyAsBefore(t *testing.T) {
|
|
// Every agent configuration written before there could be a second control
|
|
// plane names one address, and none of them should have to change.
|
|
c := normalized(t, Config{Server: "10.0.0.140:7443"})
|
|
if !slices.Equal(c.Servers, []string{"http://10.0.0.140:7443"}) {
|
|
t.Errorf("servers = %v", c.Servers)
|
|
}
|
|
if c.Server != "http://10.0.0.140:7443" {
|
|
t.Errorf("server = %q", c.Server)
|
|
}
|
|
}
|
|
|
|
func TestTheAddressAlreadyThereStaysPreferred(t *testing.T) {
|
|
// An operator adding a standby to an existing file expects the control
|
|
// plane that was already working to keep being tried first.
|
|
c := normalized(t, Config{
|
|
Server: "first:7443",
|
|
Servers: []string{"second:7443", "third:7443"},
|
|
})
|
|
want := []string{"http://first:7443", "http://second:7443", "http://third:7443"}
|
|
if !slices.Equal(c.Servers, want) {
|
|
t.Errorf("servers = %v, want %v", c.Servers, want)
|
|
}
|
|
}
|
|
|
|
func TestADuplicateAddressIsNotTriedTwiceInARow(t *testing.T) {
|
|
// Which would look exactly like the rotation not working.
|
|
c := normalized(t, Config{
|
|
Server: "a:7443",
|
|
Servers: []string{"http://a:7443", "b:7443"},
|
|
})
|
|
want := []string{"http://a:7443", "http://b:7443"}
|
|
if !slices.Equal(c.Servers, want) {
|
|
t.Errorf("servers = %v, want %v", c.Servers, want)
|
|
}
|
|
}
|
|
|
|
func TestEveryAddressIsCheckedAgainstTheTLSSetting(t *testing.T) {
|
|
// The check existed for one address and has to hold for all of them: a
|
|
// standby reached over plaintext while the leader is authenticated would
|
|
// be a way around mutual TLS, not a fallback.
|
|
c := Config{
|
|
Servers: []string{"https://a:7443", "http://b:7443"},
|
|
TLS: tlsconf.Config{CA: "ca.pem", Cert: "c.pem", Key: "k.pem"},
|
|
}
|
|
err := c.normalizeServerURL()
|
|
if err == nil {
|
|
t.Fatal("a plaintext standby was accepted alongside TLS")
|
|
}
|
|
if !strings.Contains(err.Error(), "b:7443") {
|
|
t.Errorf("the error does not say which address: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNoServerAtAllIsStillAnError(t *testing.T) {
|
|
c := Config{}
|
|
if err := c.normalizeServerURL(); err == nil {
|
|
t.Error("an agent with no control plane configured was accepted")
|
|
}
|
|
}
|
|
|
|
func TestSchemeIsAddedPerAddress(t *testing.T) {
|
|
c := normalized(t, Config{
|
|
Servers: []string{"a:7443", "b:7443"},
|
|
TLS: tlsconf.Config{CA: "ca.pem", Cert: "c.pem", Key: "k.pem"},
|
|
})
|
|
for _, s := range c.Servers {
|
|
if !strings.HasPrefix(s, "https://") {
|
|
t.Errorf("%q did not get the secure scheme", s)
|
|
}
|
|
}
|
|
}
|