mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
* fix(deploy): build Go binaries on the runner, ship them, SKIP_BUILD on box The deploy SSHed into the ~458MB droplet and ran `go build` there. The Go linker's peak memory OOM-killed the build — and with it the sshd serving the deploy session — surfacing as "Connection closed by remote host" (exit 255). It was flaky because it tracked momentary memory pressure from the co-resident ergo/forgejo/tor/podman/agentbbs processes (run #25 passed, #26 failed on near-identical code). Build both binaries on the 16GB GitHub runner instead (pure-Go, modernc sqlite, so CGO_ENABLED=0 static cross-build), scp them to the droplet, and run setup.sh with SKIP_BUILD=1 so the box never compiles. Arch is detected from the droplet so amd64/arm64 both work. setup.sh now also skips the Go toolchain download when SKIP_BUILD=1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(members): member directory + store-and-forward messaging A members-only hub plugin (the BBS "who") plus user-to-user messaging: - internal/store: messages table + SendMessage/Inbox/UnreadCount/MarkRead, and OnlineUsers (open sessions) for presence. MarkRead is recipient-scoped so a member can only clear their own mail. - plugins/members: directory with online dots + last-seen, a finger-style profile view, a minimal compose box, and an inbox that marks read on open. - ssh msg@host <user> [text]: scriptable CLI to leave a note (body from args or stdin), mirroring the existing finger route; "msg"/"message" are reserved. - hub: "N unread" badge on login (hubMOTD). plugin.Context gains Host for member homepage URLs. Extends the existing finger@ behavior (ssh <name>@host) rather than replacing it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
2.1 KiB
Go
82 lines
2.1 KiB
Go
package store
|
|
|
|
import "testing"
|
|
|
|
func TestMessagingRoundtrip(t *testing.T) {
|
|
st := openTest(t)
|
|
|
|
_, _ = st.EnsureUser("alice", "member", "SHA256:aaa")
|
|
_, _ = st.EnsureUser("bob", "member", "SHA256:bbb")
|
|
|
|
if n, err := st.UnreadCount("bob"); err != nil || n != 0 {
|
|
t.Fatalf("fresh unread: n=%d err=%v", n, err)
|
|
}
|
|
|
|
if err := st.SendMessage("alice", "bob", "hey, c4 tonight?"); err != nil {
|
|
t.Fatalf("send: %v", err)
|
|
}
|
|
if err := st.SendMessage("alice", "bob", "second note"); err != nil {
|
|
t.Fatalf("send2: %v", err)
|
|
}
|
|
|
|
n, err := st.UnreadCount("bob")
|
|
if err != nil || n != 2 {
|
|
t.Fatalf("unread after send: n=%d err=%v", n, err)
|
|
}
|
|
|
|
inbox, err := st.Inbox("bob", 10)
|
|
if err != nil {
|
|
t.Fatalf("inbox: %v", err)
|
|
}
|
|
if len(inbox) != 2 {
|
|
t.Fatalf("want 2 messages, got %d", len(inbox))
|
|
}
|
|
// Newest first.
|
|
if inbox[0].Body != "second note" || inbox[0].From != "alice" || inbox[0].To != "bob" {
|
|
t.Fatalf("unexpected newest message: %+v", inbox[0])
|
|
}
|
|
|
|
// Mark only the first read; the other stays unread.
|
|
if err := st.MarkRead("bob", []int64{inbox[0].ID}); err != nil {
|
|
t.Fatalf("markread: %v", err)
|
|
}
|
|
if n, _ := st.UnreadCount("bob"); n != 1 {
|
|
t.Fatalf("want 1 unread after partial read, got %d", n)
|
|
}
|
|
|
|
// MarkRead is scoped to the recipient: alice can't clear bob's mail.
|
|
if err := st.MarkRead("alice", []int64{inbox[1].ID}); err != nil {
|
|
t.Fatalf("markread other: %v", err)
|
|
}
|
|
if n, _ := st.UnreadCount("bob"); n != 1 {
|
|
t.Fatalf("cross-user markread leaked: unread=%d", n)
|
|
}
|
|
|
|
// Empty ids is a no-op.
|
|
if err := st.MarkRead("bob", nil); err != nil {
|
|
t.Fatalf("markread empty: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOnlineUsers(t *testing.T) {
|
|
st := openTest(t)
|
|
|
|
u, _ := st.EnsureUser("carol", "member", "SHA256:ccc")
|
|
id, _ := st.RecordSession(u.ID, "carol", "1.2.3.4", "hub")
|
|
|
|
online, err := st.OnlineUsers()
|
|
if err != nil {
|
|
t.Fatalf("online: %v", err)
|
|
}
|
|
if !online["carol"] {
|
|
t.Fatal("carol should be online while her session is open")
|
|
}
|
|
|
|
if err := st.EndSession(id); err != nil {
|
|
t.Fatalf("end: %v", err)
|
|
}
|
|
online, _ = st.OnlineUsers()
|
|
if online["carol"] {
|
|
t.Fatal("carol should be offline after her session ends")
|
|
}
|
|
}
|