cmd/agent-watcher/main_test.go builds the real binary in TestMain, then
launches it twice with temp configs to exercise the full path:
TestEndToEnd_BothSourcesEmitToInbox
- drops a *.json file via tmp+rename (mirrors Syncthing semantics)
- POSTs a webhook with template variables ({{ .repo }}, {{ .actor }})
- POSTs a urgent alert with empty body and fixed-string template
- asserts 3 JSONL lines land in bob.inbox with exact shape
- confirms each event's source field tracks origin
("drop-folder:drop1.json", "webhook:/forgejo/push")
- hits /health and verifies emitted=2 (one webhook didn't 200, that
counter only counts successful emits)
TestEndToEnd_GracefulShutdown
- SIGTERM after listener up
- asserts process exits within 3s
Total: 43 tests across 5 packages, all passing. Real binary verified
end-to-end on Linux/amd64.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
internal/source/webhook routes inbound POSTs to inbox events via a
configured table. Each route specifies recipient, type, priority, and
a Go text/template payload renderer that consumes the request body
decoded as JSON.
v1 binds loopback only — New() rejects non-loopback addresses at
construction. Caddy + bearer-token reverse-proxy is the v2 upgrade
path per spec §4.
Behavior:
- POST + matched route + valid JSON body → render template, emit, 202
- Missing route → 404
- Wrong method → 405
- Bad JSON → 400
- Template render failure → 500
- Emit failure → 500 (caller responsible for retry; HTTP source has no
durable staging)
- Empty body → empty data map for template (lets fixed-string templates
work without sending {})
- 1 MiB request body cap
GET /health returns JSON Stats{received, emitted, errors, uptime_sec}
on the same listener for journalctl correlation per spec §3.5.
10 tests passing — non-loopback rejection, bad type/template
rejection, route+template happy path, priority defaulting, empty body,
404/400/405/500, health endpoint counters.
31 tests across the three internal packages, all passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
source.Source is the contract every Collector input implements: Name +
Run(ctx, emit). Sources don't own state — they convert external events
into emit calls. Dispatcher routes.
internal/source/dropfolder: watches ~/Nyx/workspace/incoming/ for
*.json drop files. fsnotify-driven with periodic poll fallback (default
30s safety net for missed events). Each file:
1. Parsed against the spec §3.1.2 schema with DisallowUnknownFields.
2. Valid → emitted, then file deleted.
3. Invalid (missing fields, bad type/priority, unknown fields, garbage)
→ moved to .dead-letter/ with a sidecar .reason file for forensics.
4. Emit failure → file retained in place for retry (transient errors
shouldn't be permanent dead-letters).
Also: initial-scan on Run() drains files that landed before the watcher
attached, catching up after a Collector restart.
14 tests in the package — schema validation table for all error cases,
initial-scan, live inotify drop, post-emit delete, dead-letter +
sidecar, emit-failure retention. Plus the 7 inbox tests still passing.
Pinned fsnotify v1.7.0 (Go 1.22 compatible; 1.10.x demanded toolchain
1.23 which isn't in apt yet). go.mod stays at 1.22 to match VPS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Layer 1 keystone. The internal/inbox package writes ping-shaped JSONL
events to recipient inbox files in a format bit-identical to the
agent-ping CLI's output, so the existing UserPromptSubmit hook and the
future MCP Watcher cannot tell whether a line came from `ping` or the
Collector.
- O_APPEND opens for atomic line writes (POSIX guarantees writes <=
PIPE_BUF, our lines are well under).
- Per-recipient sync.Mutex bounds contention; multiple goroutines
writing to one inbox stay correctly serialized.
- 7 tests passing: shape, ID/TS preservation, omitempty for optional
fields, key-set + compactness match against ping CLI's separators=
(",",":") output, 100-goroutine concurrent-write torn-line check,
bad-input rejection, empty-dir rejection.
go.mod at git.botbought.ai/foreman/agent-watcher; module name matches
the public Forgejo path so eventual consumers can `go get` it.
Next milestones:
- Source plugin interface
- Drop folder source (inotify, via fsnotify)
- HTTP webhook source
- Config loader (YAML)
- main.go wiring
- systemd unit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot of official Anthropic Channels docs (channels.md, channels-reference.md,
routines summary) fetched 2026-05-06 from code.claude.com. Bob needs these to
implement the MCP Watcher in Layer 2; the channels-reference.md is the primary
implementation reference.
docs/channels/README.md cross-references the snapshot back to spec §4 and notes
the key facts confirmed: capability key, notification method, lifecycle, the
research-preview --dangerously-load-development-channels caveat, and version
minimums (v2.1.80 for channels, v2.1.81 for permission relay which we are not
using in v1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>