Collector milestone 4: config loader + main wiring (binary builds)
internal/config loads ~/.config/agent-watcher/collector.yaml with strict validation (KnownFields=true, so typos fail loud), applies sensible defaults, and expands ~/ in path fields. Either source can be omitted but at least one must be configured. cmd/agent-watcher is the entry point: load config, build inbox.Writer, build configured sources, run them concurrently with a shared Emit closure, wait for SIGINT/SIGTERM, shut down. Logs to stderr — text by default, JSON via --json-log for journald structured fields per spec §3.4. SIGHUP reload is a v2 item; for now restart the systemd unit to pick up config changes. 10 config tests passing — full happy path, defaults applied, ~/ expansion, and a table of 9 invalid configs that must all reject (missing agent, no sources, empty webhook routes, route missing recipient/type/template, route path without leading /, unknown top-level field, negative poll seconds). Binary builds clean: 10.8M single static binary on Linux/amd64. go.mod stays at Go 1.22 to match VPS toolchain. Total: 41 tests across 4 packages, all passing. Build clean. go vet clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ba6db7c82f
commit
2183850c03
5 changed files with 502 additions and 1 deletions
168
internal/config/config_test.go
Normal file
168
internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParse_FullValid(t *testing.T) {
|
||||
body := []byte(`
|
||||
agent: foreman
|
||||
inbox_dir: /tmp/inbox
|
||||
sources:
|
||||
webhook:
|
||||
listen: 127.0.0.1:18790
|
||||
routes:
|
||||
/forgejo/push:
|
||||
recipient: bob
|
||||
type: INFO
|
||||
payload_template: "push to {{ .repo }}"
|
||||
/alert:
|
||||
recipient: bob
|
||||
type: NEEDS-RESPONSE
|
||||
priority: urgent
|
||||
payload_template: "alert"
|
||||
drop_folder:
|
||||
path: /tmp/incoming
|
||||
poll_fallback_seconds: 60
|
||||
`)
|
||||
c, err := parse(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Agent != "foreman" {
|
||||
t.Errorf("agent = %q", c.Agent)
|
||||
}
|
||||
if c.InboxDir != "/tmp/inbox" {
|
||||
t.Errorf("inbox = %q", c.InboxDir)
|
||||
}
|
||||
if c.Sources.Webhook == nil || len(c.Sources.Webhook.Routes) != 2 {
|
||||
t.Errorf("webhook routes: %+v", c.Sources.Webhook)
|
||||
}
|
||||
if c.Sources.DropFolder == nil || c.Sources.DropFolder.Path != "/tmp/incoming" {
|
||||
t.Errorf("dropfolder: %+v", c.Sources.DropFolder)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_AppliesDefaults(t *testing.T) {
|
||||
body := []byte(`
|
||||
agent: foreman
|
||||
sources:
|
||||
drop_folder:
|
||||
path: /tmp/x
|
||||
`)
|
||||
c, err := parse(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.InboxDir == "" {
|
||||
t.Error("inbox_dir default not applied")
|
||||
}
|
||||
if c.Sources.DropFolder.PollFallbackSeconds != DefaultPollFallbackSeconds {
|
||||
t.Errorf("poll default = %d", c.Sources.DropFolder.PollFallbackSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_HomeExpansion(t *testing.T) {
|
||||
body := []byte(`
|
||||
agent: foreman
|
||||
sources:
|
||||
drop_folder:
|
||||
path: ~/foo/bar
|
||||
`)
|
||||
c, err := parse(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.HasPrefix(c.Sources.DropFolder.Path, "~") {
|
||||
t.Errorf("~ not expanded: %q", c.Sources.DropFolder.Path)
|
||||
}
|
||||
if !strings.HasSuffix(c.Sources.DropFolder.Path, "/foo/bar") {
|
||||
t.Errorf("expansion lost suffix: %q", c.Sources.DropFolder.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse_RejectsInvalid(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"missing agent": `sources:\n webhook:\n routes: {}`,
|
||||
"no sources": `agent: x`,
|
||||
"empty webhook routes": `
|
||||
agent: x
|
||||
sources:
|
||||
webhook:
|
||||
listen: 127.0.0.1:18790
|
||||
routes: {}
|
||||
`,
|
||||
"route missing recipient": `
|
||||
agent: x
|
||||
sources:
|
||||
webhook:
|
||||
routes:
|
||||
/x:
|
||||
type: INFO
|
||||
payload_template: y
|
||||
`,
|
||||
"route missing type": `
|
||||
agent: x
|
||||
sources:
|
||||
webhook:
|
||||
routes:
|
||||
/x:
|
||||
recipient: r
|
||||
payload_template: y
|
||||
`,
|
||||
"route missing template": `
|
||||
agent: x
|
||||
sources:
|
||||
webhook:
|
||||
routes:
|
||||
/x:
|
||||
recipient: r
|
||||
type: INFO
|
||||
`,
|
||||
"route path no slash": `
|
||||
agent: x
|
||||
sources:
|
||||
webhook:
|
||||
routes:
|
||||
bad:
|
||||
recipient: r
|
||||
type: INFO
|
||||
payload_template: y
|
||||
`,
|
||||
"unknown top-level field": `
|
||||
agent: x
|
||||
typo: oops
|
||||
sources:
|
||||
drop_folder:
|
||||
path: /x
|
||||
`,
|
||||
"poll negative": `
|
||||
agent: x
|
||||
sources:
|
||||
drop_folder:
|
||||
path: /x
|
||||
poll_fallback_seconds: -1
|
||||
`,
|
||||
}
|
||||
for name, body := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := parse([]byte(body)); err == nil {
|
||||
t.Errorf("expected error for: %s", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToWebhookRouteMap(t *testing.T) {
|
||||
w := &WebhookConfig{
|
||||
Routes: map[string]WebhookRoute{
|
||||
"/x": {Recipient: "r", Type: "INFO", Priority: "urgent", PayloadTemplate: "p"},
|
||||
},
|
||||
}
|
||||
got := w.ToWebhookRouteMap()
|
||||
r := got["/x"]
|
||||
if r.Recipient != "r" || r.Type != "INFO" || r.Priority != "urgent" || r.PayloadTemplate != "p" {
|
||||
t.Errorf("conversion lost data: %+v", r)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue