/messages reads GET /api/messages/stream. A history select picks the window (live only up to 24 hours) and a filter keeps the messages whose sender or recipient contains the text. Each message is a row of the terminal list an agent's terminal uses: one summary line with the time, the two parties and the start of the body, and the whole body under it. The list follows the newest message and holds the last 2000. The feed has no event id, so a reconnect sends the history again. The page empties its list each time the connection opens and fills it from the replay, where the terminal stream hook, which keeps its rows across a reconnect, would list every message twice. The stream URL, filter, cap and row logic is in its own module with tests, and docs/swarm/ui.md describes the page.
87 lines
3 KiB
JavaScript
87 lines
3 KiB
JavaScript
// The messages page's logic: the stream URL, the filter, the cap, and how a
|
|
// message becomes a terminal row. Run by `npm test`, which Node runs with its
|
|
// built-in test runner and type stripping, so the `.ts` module is imported as
|
|
// it is.
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import {
|
|
clock,
|
|
filterMessages,
|
|
messageRow,
|
|
messagesUrl,
|
|
newest,
|
|
snippet,
|
|
} from "../src/pages/messages/messages.ts";
|
|
|
|
const msg = (from, to, body = "hi", extra = {}) => ({
|
|
ts: "2026-10-18T12:00:00Z",
|
|
from,
|
|
to,
|
|
body,
|
|
...extra,
|
|
});
|
|
|
|
test("the stream URL carries the history window", () => {
|
|
assert.equal(messagesUrl(60), "/api/messages/stream?since_minutes=60");
|
|
assert.equal(messagesUrl(0), "/api/messages/stream?since_minutes=0");
|
|
});
|
|
|
|
test("a blank filter keeps every message", () => {
|
|
const all = [msg("a", "b"), msg("c", "d")];
|
|
assert.deepEqual(filterMessages(all, ""), all);
|
|
assert.deepEqual(filterMessages(all, " "), all);
|
|
});
|
|
|
|
test("the filter matches sender or recipient, ignoring case", () => {
|
|
const all = [msg("atlas", "iris"), msg("iris", "coder"), msg("a", "b")];
|
|
assert.deepEqual(
|
|
filterMessages(all, "IRIS").map((m) => `${m.from}>${m.to}`),
|
|
["atlas>iris", "iris>coder"],
|
|
);
|
|
assert.deepEqual(filterMessages(all, "cod").length, 1);
|
|
assert.deepEqual(filterMessages(all, "nobody"), []);
|
|
});
|
|
|
|
test("the filter does not search the body", () => {
|
|
assert.deepEqual(filterMessages([msg("a", "b", "talk to iris")], "iris"), []);
|
|
});
|
|
|
|
test("the cap keeps the newest messages and leaves a short list alone", () => {
|
|
const all = [1, 2, 3, 4, 5].map((n) => msg("a", "b", String(n)));
|
|
assert.deepEqual(
|
|
newest(all, 3).map((m) => m.body),
|
|
["3", "4", "5"],
|
|
);
|
|
assert.equal(newest(all, 5), all);
|
|
assert.equal(newest(all, 10), all);
|
|
});
|
|
|
|
test("a snippet is the first non-blank line, cut with an ellipsis", () => {
|
|
assert.equal(snippet("\n\n hello there \nsecond", 80), "hello there");
|
|
assert.equal(snippet("abcdefghij", 5), "abcd…");
|
|
assert.equal(snippet("abcde", 5), "abcde");
|
|
assert.equal(snippet(" \n ", 5), "");
|
|
});
|
|
|
|
test("the clock reads hours, minutes and seconds, and passes a non-date through", () => {
|
|
assert.match(clock("2026-10-18T12:00:05Z"), /^\d\d:\d\d:\d\d$/);
|
|
assert.equal(clock("not a date"), "not a date");
|
|
});
|
|
|
|
test("a message becomes an info row with its parties in the summary", () => {
|
|
const row = messageRow(msg("atlas", "iris", "ready\nmore"), "k1");
|
|
assert.equal(row.key, "k1");
|
|
assert.equal(row.level, "info");
|
|
assert.equal(row.body, "ready\nmore");
|
|
assert.equal(row.fromHistory, false);
|
|
assert.match(row.summary, /atlas → iris — ready$/);
|
|
});
|
|
|
|
test("the body format rides on the row, and an empty body leaves no dash", () => {
|
|
assert.equal(
|
|
messageRow(msg("a", "b", "x", { body_format: "diff" }), "k").body_format,
|
|
"diff",
|
|
);
|
|
assert.equal(messageRow(msg("a", "b", "x"), "k").body_format, undefined);
|
|
assert.doesNotMatch(messageRow(msg("a", "b", ""), "k").summary, / — /);
|
|
});
|