agents.default and agents.nodes.<id> in the cluster's settings hold everything agent.yaml can say but the agent's id, control planes, address and certificate, which stay in the file and are refused there. The control plane sends each node its settings on register and, a second apart, when they change; an agent whose settings changed checks they load beside its file, keeps them and starts again on them, engines serving throughout. An edited agent.yaml that loads restarts the agent too. History, audit, ctl config, the console and the portal's settings page cover them as the rest of the settings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
89 lines
2.7 KiB
Go
89 lines
2.7 KiB
Go
package agent
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
llmgwv1 "gitea.mynodes.xyz/ilker/llmgw/api/gen/llmgw/v1"
|
|
)
|
|
|
|
func settingsAgent(t *testing.T, running string) (*Agent, chan struct{}) {
|
|
t.Helper()
|
|
oldFile, oldExit := SettingsFile, exitForUpgrade
|
|
SettingsFile = filepath.Join(t.TempDir(), "agent-settings.yaml")
|
|
exited := make(chan struct{}, 1)
|
|
exitForUpgrade = func() { exited <- struct{}{} }
|
|
t.Cleanup(func() { SettingsFile, exitForUpgrade = oldFile, oldExit })
|
|
cfg := Config{Server: "127.0.0.1:7443", NodeID: "n1", WeightsCache: t.TempDir(), EnginesDir: t.TempDir(), Settings: running}
|
|
cfg.Superv.WorkDir = t.TempDir()
|
|
a, err := New(cfg, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return a, exited
|
|
}
|
|
|
|
// exitedSoon is whether the agent exited within a second: it waits a
|
|
// moment for its log line to leave first.
|
|
func exitedSoon(ch chan struct{}) bool {
|
|
select {
|
|
case <-ch:
|
|
return true
|
|
case <-time.After(time.Second):
|
|
return false
|
|
}
|
|
}
|
|
|
|
func TestAnAgentOnItsSettingsStaysAndOneOffThemStartsAgain(t *testing.T) {
|
|
a, exited := settingsAgent(t, "heartbeat_interval: 5s\n")
|
|
a.applySettings(&llmgwv1.AgentSettings{Yaml: "heartbeat_interval: 5s\n"})
|
|
if exitedSoon(exited) || ReadSettings() != nil {
|
|
t.Error("settings it already runs on restarted it")
|
|
}
|
|
a.applySettings(&llmgwv1.AgentSettings{Yaml: "heartbeat_interval: 2s\n"})
|
|
if !exitedSoon(exited) || string(ReadSettings()) != "heartbeat_interval: 2s\n" {
|
|
t.Errorf("new settings: kept %q", ReadSettings())
|
|
}
|
|
}
|
|
|
|
func TestSettingsThatWouldNotLoadAreNotTaken(t *testing.T) {
|
|
a, exited := settingsAgent(t, "")
|
|
a.SetSettingsCheck(func([]byte) error { return errors.New("fabric ib-a lists no HCAs") })
|
|
a.applySettings(&llmgwv1.AgentSettings{Yaml: "fabrics: [{name: ib-a}]\n"})
|
|
if exitedSoon(exited) || ReadSettings() != nil {
|
|
t.Error("settings that do not load were taken")
|
|
}
|
|
}
|
|
|
|
func TestAChangedAgentYamlThatLoadsStartsTheAgentAgain(t *testing.T) {
|
|
a, exited := settingsAgent(t, "")
|
|
old := watchEvery
|
|
watchEvery = 10 * time.Millisecond
|
|
t.Cleanup(func() { watchEvery = old })
|
|
p := filepath.Join(t.TempDir(), "agent.yaml")
|
|
_ = os.WriteFile(p, []byte("node_id: n1\n"), 0o600)
|
|
var loads atomic.Bool
|
|
check := func() error {
|
|
if loads.Load() {
|
|
return nil
|
|
}
|
|
return errors.New("broken")
|
|
}
|
|
go a.WatchFile(t.Context(), p, check)
|
|
time.Sleep(30 * time.Millisecond)
|
|
// Saved broken: reported, not restarted into.
|
|
_ = os.WriteFile(p, []byte("node_id: [\n"), 0o600)
|
|
if exitedSoon(exited) {
|
|
t.Fatal("a broken agent.yaml restarted the agent")
|
|
}
|
|
loads.Store(true)
|
|
_ = os.WriteFile(p, []byte("node_id: n1\nlabels: {a: b}\n"), 0o600)
|
|
if !exitedSoon(exited) {
|
|
t.Error("a changed agent.yaml did not restart the agent")
|
|
}
|
|
}
|