brane 0.0.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +362 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +91 -0
- package/DESIGN.md +402 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +205 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +116 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +332 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1094 -0
- package/dist/commands/token.js +153 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6349 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +633 -0
- package/dist/db/tokens.js +583 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +82 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +197 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +49 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +262 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +85 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +137 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +115 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +587 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +581 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +202 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +141 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +53 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +211 -0
- package/docs/token-rotation.md +413 -0
- package/docs/view-urls.md +114 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +81 -23
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +48 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.npmignore +0 -2
- package/.travis.yml +0 -5
- package/index.js +0 -8
- package/index.js~ +0 -12
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTestDb } from "./helpers.js";
|
|
4
|
+
import { startMcpListen, handleMcpNarrate, createMentionRecorder, wakeListenWaiters, } from "../daemon/server.js";
|
|
5
|
+
import { appendEvent } from "../db/events.js";
|
|
6
|
+
import { allMentions, pendingMentions } from "../db/mentions.js";
|
|
7
|
+
// The flows an agent actually runs, end to end through the real pieces: the
|
|
8
|
+
// real markdown scan (so a handle in a fence is inert here for the same reason
|
|
9
|
+
// it is inert on screen), the real recorder with its guards, the real wake
|
|
10
|
+
// path, the real receipt.
|
|
11
|
+
//
|
|
12
|
+
// The other mention suites test parts. This one tests that the parts are wired
|
|
13
|
+
// to each other — which is where a feature like this dies.
|
|
14
|
+
const A = "claude-aaaaaaaa"; // the one doing the mentioning
|
|
15
|
+
const B = "claude-bbbbbbbb"; // the one being mentioned
|
|
16
|
+
const C = "goose-cccccccc"; // a third narrative
|
|
17
|
+
const BRANE = "brane-home";
|
|
18
|
+
/** A whole brane: a db, the real recorder, and a wake fabric. */
|
|
19
|
+
function brane() {
|
|
20
|
+
const db = createTestDb();
|
|
21
|
+
const ins = db.prepare("INSERT INTO narratives (id, brane_id, created_at, status) VALUES (?, ?, 0, 'active')");
|
|
22
|
+
for (const nid of [A, B, C])
|
|
23
|
+
ins.run(nid, BRANE);
|
|
24
|
+
const listenWaiters = new Map();
|
|
25
|
+
const notify = (nid) => wakeListenWaiters(listenWaiters, nid);
|
|
26
|
+
const record = createMentionRecorder(db, notify);
|
|
27
|
+
/** An agent with a bridge whose socket records what it was sent. */
|
|
28
|
+
function agent(nid) {
|
|
29
|
+
const sent = [];
|
|
30
|
+
const client = {
|
|
31
|
+
ws: {
|
|
32
|
+
readyState: 1,
|
|
33
|
+
send: (raw) => {
|
|
34
|
+
const msg = JSON.parse(raw);
|
|
35
|
+
if (msg.type === "tool_result")
|
|
36
|
+
sent.push({ callId: msg.id, result: msg.result });
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
narrativeId: nid,
|
|
40
|
+
agentType: "claude",
|
|
41
|
+
pendingListens: new Map(),
|
|
42
|
+
};
|
|
43
|
+
let calls = 0;
|
|
44
|
+
return {
|
|
45
|
+
nid,
|
|
46
|
+
sent,
|
|
47
|
+
client,
|
|
48
|
+
/** Park on `listen`, or resolve immediately if something is waiting. */
|
|
49
|
+
listen(args = {}) {
|
|
50
|
+
startMcpListen(client, db, `${nid}-${++calls}`, { after: { self: 0 }, ...args }, listenWaiters, () => { }, BRANE);
|
|
51
|
+
},
|
|
52
|
+
/** Say something — into this narrative, or into another's. */
|
|
53
|
+
say(text, into) {
|
|
54
|
+
return handleMcpNarrate({ say: text, ...(into ? { nid: into } : {}) }, db, nid, notify, undefined, record, BRANE);
|
|
55
|
+
},
|
|
56
|
+
last() {
|
|
57
|
+
return sent[sent.length - 1]?.result;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** The operator typing into a narrative's composer. */
|
|
62
|
+
function operatorSignal(into, text) {
|
|
63
|
+
const event = appendEvent(db, into, "human_signal", { content: text, source: "web" });
|
|
64
|
+
notify(into);
|
|
65
|
+
record(into, event, "web");
|
|
66
|
+
}
|
|
67
|
+
return { db, agent, operatorSignal, notify };
|
|
68
|
+
}
|
|
69
|
+
describe("one agent gets another's attention", () => {
|
|
70
|
+
it("wakes a parked listen and hands over a pointer to the message", () => {
|
|
71
|
+
const { agent } = brane();
|
|
72
|
+
const a = agent(A);
|
|
73
|
+
const b = agent(B);
|
|
74
|
+
b.listen(); // B is idle, blocked on listen
|
|
75
|
+
assert.equal(b.sent.length, 0);
|
|
76
|
+
assert.equal(b.client.pendingListens.size, 1);
|
|
77
|
+
a.say(`found the bug in the resync path, @${B} — it's the rAF guard`);
|
|
78
|
+
assert.equal(b.sent.length, 1, "the mention must wake B");
|
|
79
|
+
assert.equal(b.client.pendingListens.size, 0);
|
|
80
|
+
const m = b.last().mentions;
|
|
81
|
+
assert.equal(m.length, 1);
|
|
82
|
+
assert.equal(m[0].src, A, "where to go read");
|
|
83
|
+
assert.equal(m[0].author, A, "who said it");
|
|
84
|
+
assert.equal(m[0].seq, 1);
|
|
85
|
+
assert.equal(b.last().events.self.length, 0, "nothing landed in B's own log");
|
|
86
|
+
});
|
|
87
|
+
it("B reads A, which settles it, and B answers into A's narrative", () => {
|
|
88
|
+
const { db, agent } = brane();
|
|
89
|
+
const a = agent(A);
|
|
90
|
+
const b = agent(B);
|
|
91
|
+
b.listen();
|
|
92
|
+
a.say(`@${B} can you take the ledger tests?`);
|
|
93
|
+
const { src, seq } = b.last().mentions[0];
|
|
94
|
+
// Interpreting a mention means going to read it. The cursor is the ack.
|
|
95
|
+
b.listen({ after: { self: 0, [src]: seq - 1 } });
|
|
96
|
+
const events = b.last().events[src];
|
|
97
|
+
assert.equal(events.length, 1);
|
|
98
|
+
assert.deepEqual(events[0].mentions, [B], "the event says it addresses B");
|
|
99
|
+
assert.equal(pendingMentions(db, B, 10).total, 0, "reading it settled it");
|
|
100
|
+
// B replies the loud way — into A's narrative.
|
|
101
|
+
b.say("on it", A);
|
|
102
|
+
const aEvents = db.prepare("SELECT type, source FROM events WHERE narrative_id = ?").all(A);
|
|
103
|
+
assert.deepEqual(aEvents.map((e) => e.type), ["narration", "agent_signal"]);
|
|
104
|
+
// And B goes back to idle without being re-woken by the settled mention.
|
|
105
|
+
b.listen();
|
|
106
|
+
assert.equal(b.client.pendingListens.size, 1, "B parks again");
|
|
107
|
+
});
|
|
108
|
+
it("an unread mention nags but does not re-wake", () => {
|
|
109
|
+
const { agent } = brane();
|
|
110
|
+
const a = agent(A);
|
|
111
|
+
const b = agent(B);
|
|
112
|
+
b.listen();
|
|
113
|
+
a.say(`@${B} low priority, whenever`);
|
|
114
|
+
assert.equal(b.sent.length, 1);
|
|
115
|
+
// B parks and re-parks without reading A. It is reminded on each *resolved*
|
|
116
|
+
// call, but the unread mention never resolves one by itself again.
|
|
117
|
+
for (let i = 0; i < 5; i++)
|
|
118
|
+
b.listen();
|
|
119
|
+
assert.equal(b.sent.length, 1, "SPIN LOOP: an unread mention re-resolved a listen");
|
|
120
|
+
// A fresh mention does wake it, and the unread one rides along.
|
|
121
|
+
b.listen();
|
|
122
|
+
a.say(`@${B} bumping this`);
|
|
123
|
+
assert.equal(b.sent.length, 2);
|
|
124
|
+
assert.equal(b.last().mentions.length, 2, "both are still owed");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe("mentioning an agent that isn't running", () => {
|
|
128
|
+
it("records without waking anything, and delivers when it comes back", () => {
|
|
129
|
+
const { db, agent } = brane();
|
|
130
|
+
const a = agent(A);
|
|
131
|
+
// B has no bridge at all — nothing is listening.
|
|
132
|
+
a.say(`@${B} when you're back, the migration needs a look`);
|
|
133
|
+
assert.equal(pendingMentions(db, B, 10).total, 1);
|
|
134
|
+
assert.equal(db.prepare("SELECT COUNT(*) c FROM events WHERE narrative_id = ?").get(B).c, 0, "and nothing was written into B's log — no revive, no trace");
|
|
135
|
+
// B restarts and does what a restarted agent does: whoami, then listen.
|
|
136
|
+
const b = agent(B);
|
|
137
|
+
const recovered = allMentions(db, B, 10, BRANE);
|
|
138
|
+
assert.equal(recovered.length, 1);
|
|
139
|
+
assert.equal(recovered[0].src, A);
|
|
140
|
+
b.listen();
|
|
141
|
+
assert.equal(b.sent.length, 1, "the first listen after a restart delivers it");
|
|
142
|
+
assert.equal(b.last().mentions[0].src, A);
|
|
143
|
+
});
|
|
144
|
+
it("survives a restart that loses the agent's cursor entirely", () => {
|
|
145
|
+
const { db, agent } = brane();
|
|
146
|
+
agent(A).say(`@${B} ping`);
|
|
147
|
+
// A restarted agent re-derives its cursor from tail:50 and may well read
|
|
148
|
+
// from 0. The mention must not be lost to that, nor re-delivered forever.
|
|
149
|
+
const b = agent(B);
|
|
150
|
+
b.listen({ tail: { self: 50 } });
|
|
151
|
+
assert.equal(b.sent.length, 1);
|
|
152
|
+
assert.equal(b.last().mentions.length, 1);
|
|
153
|
+
b.listen({ after: { self: 0 } });
|
|
154
|
+
assert.equal(b.sent.length, 1, "a rewound cursor must not resurrect the wake");
|
|
155
|
+
assert.equal(pendingMentions(db, B, 10).total, 1, "but it is still owed until read");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe("the operator mentions an agent", () => {
|
|
159
|
+
it("delivers a human signal's mention the same way", () => {
|
|
160
|
+
const { agent, operatorSignal } = brane();
|
|
161
|
+
const b = agent(B);
|
|
162
|
+
b.listen();
|
|
163
|
+
// Typed into A's composer: "@B — look at this".
|
|
164
|
+
operatorSignal(A, `@${B} see the trace above`);
|
|
165
|
+
assert.equal(b.sent.length, 1);
|
|
166
|
+
assert.equal(b.last().mentions[0].author, "web", "the operator, not an agent");
|
|
167
|
+
assert.equal(b.last().mentions[0].src, A);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe("a third party's narration", () => {
|
|
171
|
+
it("names where to read and who said it, separately", () => {
|
|
172
|
+
const { agent } = brane();
|
|
173
|
+
const a = agent(A);
|
|
174
|
+
const b = agent(B);
|
|
175
|
+
b.listen();
|
|
176
|
+
// A narrates into C's narrative, mentioning B. B must be sent to C to read
|
|
177
|
+
// it, while knowing A wrote it.
|
|
178
|
+
a.say(`@${B} this is the thread`, C);
|
|
179
|
+
const m = b.last().mentions[0];
|
|
180
|
+
assert.equal(m.src, C, "read C");
|
|
181
|
+
assert.equal(m.author, A, "A said it");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe("what must not page anyone", () => {
|
|
185
|
+
it("a handle inside a fenced block in a real narration", () => {
|
|
186
|
+
const { db, agent } = brane();
|
|
187
|
+
const a = agent(A);
|
|
188
|
+
const b = agent(B);
|
|
189
|
+
b.listen();
|
|
190
|
+
a.say(`here's the log I was sent:\n\n\`\`\`\n[12:03] operator: ping @${B}\n\`\`\`\n\nnot sure what to make of it`);
|
|
191
|
+
assert.equal(b.sent.length, 0, "a pasted log must not page anyone");
|
|
192
|
+
assert.equal(b.client.pendingListens.size, 1);
|
|
193
|
+
assert.equal(pendingMentions(db, B, 10).total, 0);
|
|
194
|
+
});
|
|
195
|
+
it("a handle in a code span, a link, or mid-word", () => {
|
|
196
|
+
const { db, agent } = brane();
|
|
197
|
+
const a = agent(A);
|
|
198
|
+
a.say(`run \`brane revive @${B}\` — see [@${B}](https://example.com/x) or mail ben@${B}`);
|
|
199
|
+
assert.equal(pendingMentions(db, B, 10).total, 0);
|
|
200
|
+
});
|
|
201
|
+
it("an agent mentioning itself", () => {
|
|
202
|
+
const { db, agent } = brane();
|
|
203
|
+
const b = agent(B);
|
|
204
|
+
b.say(`note to self: @${B} should check this`);
|
|
205
|
+
assert.equal(pendingMentions(db, B, 10).total, 0);
|
|
206
|
+
});
|
|
207
|
+
it("an author mentioning itself in someone else's narrative", () => {
|
|
208
|
+
const { db, agent } = brane();
|
|
209
|
+
// A narrates into B and signs off `@A`. The words are A's own; a row would
|
|
210
|
+
// wake A's own `narrate({ wait })` at once, pointing at an event A's cursor
|
|
211
|
+
// never reads, so the reminder would recur until dismissed.
|
|
212
|
+
agent(A).say(`answered — ping @${A} if not`, B);
|
|
213
|
+
assert.equal(pendingMentions(db, A, 10).total, 0);
|
|
214
|
+
});
|
|
215
|
+
it("a mention of the narrative being narrated into", () => {
|
|
216
|
+
const { db, agent } = brane();
|
|
217
|
+
// A narrates into B's narrative and mentions B. The message is already in
|
|
218
|
+
// B's log and already woke it; a row would be a duplicate plus a wake.
|
|
219
|
+
agent(A).say(`@${B} here you go`, B);
|
|
220
|
+
assert.equal(pendingMentions(db, B, 10).total, 0);
|
|
221
|
+
assert.equal(db.prepare("SELECT COUNT(*) c FROM events WHERE narrative_id = ?").get(B).c, 1, "the agent_signal itself still landed");
|
|
222
|
+
});
|
|
223
|
+
it("a handle that names no narrative", () => {
|
|
224
|
+
const { db, agent } = brane();
|
|
225
|
+
agent(A).say("@claude-deadbeef are you there?");
|
|
226
|
+
assert.equal(db.prepare("SELECT COUNT(*) c FROM mentions").get().c, 0);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe("the loop stays exactly as it was when no one is mentioned", () => {
|
|
230
|
+
it("an ordinary narrate/listen exchange is byte-identical", () => {
|
|
231
|
+
const { agent, operatorSignal } = brane();
|
|
232
|
+
const a = agent(A);
|
|
233
|
+
const b = agent(B);
|
|
234
|
+
const narrated = a.say("working on the resync heartbeat");
|
|
235
|
+
assert.equal("mentions" in narrated, false);
|
|
236
|
+
assert.equal("mentions_pending" in narrated, false);
|
|
237
|
+
b.listen();
|
|
238
|
+
operatorSignal(B, "how's it going?");
|
|
239
|
+
assert.equal(b.sent.length, 1);
|
|
240
|
+
assert.deepEqual(Object.keys(b.last()).sort(), ["events", "timed_out", "waited_ms"]);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe("two agents mentioning each other", () => {
|
|
244
|
+
it("each wake is earned by a new mention, never by an old one", () => {
|
|
245
|
+
const { agent } = brane();
|
|
246
|
+
const a = agent(A);
|
|
247
|
+
const b = agent(B);
|
|
248
|
+
// The shape that would ping-pong if an unread mention could re-resolve.
|
|
249
|
+
for (let round = 0; round < 5; round++) {
|
|
250
|
+
a.listen();
|
|
251
|
+
b.listen();
|
|
252
|
+
a.say(`round ${round} @${B}`);
|
|
253
|
+
b.say(`round ${round} @${A}`);
|
|
254
|
+
}
|
|
255
|
+
// Five mentions each way, five wakes each way — one per new mention, and
|
|
256
|
+
// never a sixth from the pile of unread ones.
|
|
257
|
+
assert.equal(a.sent.length, 5);
|
|
258
|
+
assert.equal(b.sent.length, 5);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
//# sourceMappingURL=mentions-e2e.test.js.map
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTestDb } from "./helpers.js";
|
|
4
|
+
import { recordMentions, pendingMentions, allMentions, hasUnsentMentions, ackMentionsByRead, dismissMentions, markSent, mentionsInEvents, } from "../db/mentions.js";
|
|
5
|
+
// The ledger makes two guarantees survive restarts, crashes and re-scans: a
|
|
6
|
+
// mention wakes an agent exactly once, and a mention the agent never actually
|
|
7
|
+
// read keeps coming back. Those pull in opposite directions, which is why each
|
|
8
|
+
// row carries two marks — `sent_at` for the first, `seen_at` for the second.
|
|
9
|
+
// Most of what follows pins that split, and one test pins the reason they are
|
|
10
|
+
// per-row rather than high-water marks per narrative.
|
|
11
|
+
const A = "claude-aaaaaaaa";
|
|
12
|
+
const B = "claude-bbbbbbbb";
|
|
13
|
+
const C = "goose-cccccccc";
|
|
14
|
+
function db() {
|
|
15
|
+
const d = createTestDb();
|
|
16
|
+
const ins = d.prepare("INSERT INTO narratives (id, brane_id, created_at, status) VALUES (?, ?, 0, 'active')");
|
|
17
|
+
ins.run(A, "brane-home");
|
|
18
|
+
ins.run(B, "brane-home");
|
|
19
|
+
ins.run(C, "brane-far");
|
|
20
|
+
return d;
|
|
21
|
+
}
|
|
22
|
+
/** B mentions A at `seq`. */
|
|
23
|
+
function mention(d, seq, src = B, mentioned = A) {
|
|
24
|
+
return recordMentions(d, {
|
|
25
|
+
mentionedNids: [mentioned],
|
|
26
|
+
srcNarrative: src,
|
|
27
|
+
srcSeq: seq,
|
|
28
|
+
author: src,
|
|
29
|
+
time: 1000 + seq,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
describe("recording", () => {
|
|
33
|
+
it("returns the rising edge, so only new mentions wake anyone", () => {
|
|
34
|
+
const d = db();
|
|
35
|
+
assert.deepEqual(mention(d, 5), [A]);
|
|
36
|
+
assert.deepEqual(mention(d, 5), [], "a re-scan of the same event wakes nobody");
|
|
37
|
+
assert.deepEqual(mention(d, 6), [A]);
|
|
38
|
+
});
|
|
39
|
+
it("dedups a handle repeated inside one event", () => {
|
|
40
|
+
const d = db();
|
|
41
|
+
const fresh = recordMentions(d, {
|
|
42
|
+
mentionedNids: [A, A, C],
|
|
43
|
+
srcNarrative: B,
|
|
44
|
+
srcSeq: 1,
|
|
45
|
+
author: B,
|
|
46
|
+
time: 1,
|
|
47
|
+
});
|
|
48
|
+
assert.deepEqual(fresh, [A, C]);
|
|
49
|
+
const { c } = d.prepare("SELECT COUNT(*) c FROM mentions").get();
|
|
50
|
+
assert.equal(c, 2);
|
|
51
|
+
});
|
|
52
|
+
it("is idempotent under a full re-scan, which is what makes rebuild safe", () => {
|
|
53
|
+
const d = db();
|
|
54
|
+
for (const seq of [1, 2, 3])
|
|
55
|
+
mention(d, seq);
|
|
56
|
+
markSent(d, A);
|
|
57
|
+
const before = d.prepare("SELECT id, src_seq, sent_at FROM mentions ORDER BY id").all();
|
|
58
|
+
for (const seq of [1, 2, 3])
|
|
59
|
+
mention(d, seq);
|
|
60
|
+
const after = d.prepare("SELECT id, src_seq, sent_at FROM mentions ORDER BY id").all();
|
|
61
|
+
// Same rows, same ids, and — the part that matters — delivery state
|
|
62
|
+
// untouched. A rebuild that reassigned ids or cleared these would
|
|
63
|
+
// re-deliver mentions the agent already handled.
|
|
64
|
+
assert.deepEqual(after, before);
|
|
65
|
+
});
|
|
66
|
+
it("keeps mentions of different narratives apart", () => {
|
|
67
|
+
const d = db();
|
|
68
|
+
recordMentions(d, { mentionedNids: [A, C], srcNarrative: B, srcSeq: 1, author: B, time: 1 });
|
|
69
|
+
assert.equal(pendingMentions(d, A, 10).total, 1);
|
|
70
|
+
assert.equal(pendingMentions(d, C, 10).total, 1);
|
|
71
|
+
assert.equal(pendingMentions(d, B, 10).total, 0);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe("the two marks", () => {
|
|
75
|
+
it("delivery stops the wake but not the reminder", () => {
|
|
76
|
+
const d = db();
|
|
77
|
+
mention(d, 5);
|
|
78
|
+
assert.ok(hasUnsentMentions(d, A), "undelivered: should wake");
|
|
79
|
+
const { rows } = pendingMentions(d, A, 10);
|
|
80
|
+
markSent(d, A); // what delivery does
|
|
81
|
+
assert.equal(hasUnsentMentions(d, A), false, "delivered: must not wake again");
|
|
82
|
+
assert.equal(pendingMentions(d, A, 10).total, 1, "unread: must still be shown");
|
|
83
|
+
assert.equal(rows[0].src, B);
|
|
84
|
+
});
|
|
85
|
+
it("a new mention wakes again even while an old one is still unread", () => {
|
|
86
|
+
const d = db();
|
|
87
|
+
mention(d, 5);
|
|
88
|
+
markSent(d, A);
|
|
89
|
+
assert.equal(hasUnsentMentions(d, A), false);
|
|
90
|
+
mention(d, 6);
|
|
91
|
+
assert.ok(hasUnsentMentions(d, A));
|
|
92
|
+
assert.equal(pendingMentions(d, A, 10).total, 2);
|
|
93
|
+
});
|
|
94
|
+
it("neither mark can rewind, because there is no value that would", () => {
|
|
95
|
+
// A high-water integer can be written backwards by a cursor that reset.
|
|
96
|
+
// NULL -> timestamp cannot: re-running either transition is a no-op.
|
|
97
|
+
const d = db();
|
|
98
|
+
for (const seq of [1, 2, 3])
|
|
99
|
+
mention(d, seq);
|
|
100
|
+
markSent(d, A);
|
|
101
|
+
markSent(d, A);
|
|
102
|
+
assert.equal(hasUnsentMentions(d, A), false);
|
|
103
|
+
ackMentionsByRead(d, A, { [B]: [1, 2, 3, 5] });
|
|
104
|
+
ackMentionsByRead(d, A, { [B]: [1, 2, 3, 5] });
|
|
105
|
+
assert.equal(pendingMentions(d, A, 10).total, 0);
|
|
106
|
+
});
|
|
107
|
+
it("caps the receipt without leaving a remainder that re-wakes", () => {
|
|
108
|
+
const d = db();
|
|
109
|
+
for (let seq = 1; seq <= 25; seq++)
|
|
110
|
+
mention(d, seq);
|
|
111
|
+
const { rows, total } = pendingMentions(d, A, 10);
|
|
112
|
+
assert.equal(rows.length, 10);
|
|
113
|
+
assert.equal(total, 25);
|
|
114
|
+
// markSent covers everything pending, not just the slice shown.
|
|
115
|
+
markSent(d, A);
|
|
116
|
+
assert.equal(hasUnsentMentions(d, A), false, "the uncapped remainder must not re-wake");
|
|
117
|
+
assert.equal(pendingMentions(d, A, 10).total, 25, "but it is still owed to the agent");
|
|
118
|
+
});
|
|
119
|
+
it("shows the most recent when capped, oldest-first within the slice", () => {
|
|
120
|
+
const d = db();
|
|
121
|
+
for (let seq = 1; seq <= 5; seq++)
|
|
122
|
+
mention(d, seq);
|
|
123
|
+
const { rows } = pendingMentions(d, A, 3);
|
|
124
|
+
assert.deepEqual(rows.map((r) => r.seq), [3, 4, 5]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe("receiving the event is the acknowledgment", () => {
|
|
128
|
+
it("settles exactly the events delivered, not a range", () => {
|
|
129
|
+
const d = db();
|
|
130
|
+
for (const seq of [5, 7, 9])
|
|
131
|
+
mention(d, seq);
|
|
132
|
+
assert.equal(pendingMentions(d, A, 10).total, 3);
|
|
133
|
+
// The read returned seq 7 and 9 — a `before` bound, or a `tail`, can skip
|
|
134
|
+
// what lies between. Settling on "seq > 5" would mark all three read.
|
|
135
|
+
assert.equal(ackMentionsByRead(d, A, { [B]: [7, 9] }), 2);
|
|
136
|
+
const rest = pendingMentions(d, A, 10);
|
|
137
|
+
assert.equal(rest.total, 1);
|
|
138
|
+
assert.equal(rest.rows[0].seq, 5, "the event that was never shown is still owed");
|
|
139
|
+
});
|
|
140
|
+
it("settles nothing when the delivered events carry no mention", () => {
|
|
141
|
+
const d = db();
|
|
142
|
+
mention(d, 5);
|
|
143
|
+
assert.equal(ackMentionsByRead(d, A, { [B]: [4, 6, 99] }), 0);
|
|
144
|
+
assert.equal(pendingMentions(d, A, 10).total, 1);
|
|
145
|
+
});
|
|
146
|
+
it("ignores a read of an unrelated narrative", () => {
|
|
147
|
+
const d = db();
|
|
148
|
+
mention(d, 5);
|
|
149
|
+
assert.equal(ackMentionsByRead(d, A, { [C]: [5] }), 0);
|
|
150
|
+
assert.equal(pendingMentions(d, A, 10).total, 1);
|
|
151
|
+
});
|
|
152
|
+
it("ignores an empty delivery", () => {
|
|
153
|
+
const d = db();
|
|
154
|
+
mention(d, 5);
|
|
155
|
+
assert.equal(ackMentionsByRead(d, A, { [B]: [] }), 0);
|
|
156
|
+
assert.equal(ackMentionsByRead(d, A, {}), 0);
|
|
157
|
+
assert.equal(pendingMentions(d, A, 10).total, 1);
|
|
158
|
+
});
|
|
159
|
+
it("reading your own log is not evidence you looked at someone else's", () => {
|
|
160
|
+
const d = db();
|
|
161
|
+
mention(d, 5, A, A); // pathological, but the guard must hold
|
|
162
|
+
assert.equal(ackMentionsByRead(d, A, { [A]: [5] }), 0);
|
|
163
|
+
});
|
|
164
|
+
it("settles one source without touching another, in either recording order", () => {
|
|
165
|
+
// The bug that forced per-row marks. A single high-water over a shared id
|
|
166
|
+
// space settles every mention with a lower id when one is acknowledged —
|
|
167
|
+
// including one from a narrative the agent never looked at. Recording C
|
|
168
|
+
// *first* is what exposes it, so both orders are pinned.
|
|
169
|
+
for (const [first] of [[B], [C]]) {
|
|
170
|
+
const d = db();
|
|
171
|
+
mention(d, 5, first);
|
|
172
|
+
mention(d, 5, first === B ? C : B);
|
|
173
|
+
assert.equal(pendingMentions(d, A, 10).total, 2);
|
|
174
|
+
ackMentionsByRead(d, A, { [B]: [5] });
|
|
175
|
+
const rest = pendingMentions(d, A, 10);
|
|
176
|
+
assert.equal(rest.total, 1, `reading B settled C's mention (${first} recorded first)`);
|
|
177
|
+
assert.equal(rest.rows[0].src, C);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
it("is idempotent", () => {
|
|
181
|
+
const d = db();
|
|
182
|
+
mention(d, 5);
|
|
183
|
+
assert.equal(ackMentionsByRead(d, A, { [B]: [5] }), 1);
|
|
184
|
+
assert.equal(ackMentionsByRead(d, A, { [B]: [5] }), 0);
|
|
185
|
+
assert.equal(pendingMentions(d, A, 10).total, 0);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
describe("annotating delivered events", () => {
|
|
189
|
+
it("says what each event addresses, keyed by nid:seq", () => {
|
|
190
|
+
const d = db();
|
|
191
|
+
recordMentions(d, { mentionedNids: [A, C], srcNarrative: B, srcSeq: 5, author: B, time: 1 });
|
|
192
|
+
const ann = mentionsInEvents(d, { [B]: [4, 5, 6] });
|
|
193
|
+
assert.deepEqual(Object.keys(ann), [`${B}:5`]);
|
|
194
|
+
assert.deepEqual(ann[`${B}:5`].sort(), [A, C].sort());
|
|
195
|
+
});
|
|
196
|
+
it("is empty for events that mention nobody, so the common case costs nothing", () => {
|
|
197
|
+
const d = db();
|
|
198
|
+
mention(d, 5);
|
|
199
|
+
assert.deepEqual(mentionsInEvents(d, { [B]: [1, 2, 3] }), {});
|
|
200
|
+
assert.deepEqual(mentionsInEvents(d, {}), {});
|
|
201
|
+
});
|
|
202
|
+
it("reports regardless of either mark", () => {
|
|
203
|
+
const d = db();
|
|
204
|
+
mention(d, 5);
|
|
205
|
+
markSent(d, A);
|
|
206
|
+
ackMentionsByRead(d, A, { [B]: [5] });
|
|
207
|
+
// The event still mentions A; it just isn't owed any more.
|
|
208
|
+
assert.deepEqual(mentionsInEvents(d, { [B]: [5] }), { [`${B}:5`]: [A] });
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe("recovery and dismissal", () => {
|
|
212
|
+
it("allMentions ignores both marks and moves neither", () => {
|
|
213
|
+
const d = db();
|
|
214
|
+
mention(d, 5);
|
|
215
|
+
markSent(d, A);
|
|
216
|
+
ackMentionsByRead(d, A, { [B]: [1, 2, 3, 5] });
|
|
217
|
+
assert.equal(pendingMentions(d, A, 10).total, 0);
|
|
218
|
+
// Delivery is at-most-once — a tool result can be dropped by a socket that
|
|
219
|
+
// closes mid-flight — so "settled" must never mean "unreachable".
|
|
220
|
+
assert.equal(allMentions(d, A, 10).length, 1);
|
|
221
|
+
assert.equal(pendingMentions(d, A, 10).total, 0, "reading recovery settles nothing");
|
|
222
|
+
assert.equal(hasUnsentMentions(d, A), false, "and wakes nobody");
|
|
223
|
+
});
|
|
224
|
+
it("dismiss settles everything currently recorded, and nothing later", () => {
|
|
225
|
+
const d = db();
|
|
226
|
+
mention(d, 5);
|
|
227
|
+
mention(d, 6);
|
|
228
|
+
assert.equal(dismissMentions(d, A), 2);
|
|
229
|
+
assert.equal(pendingMentions(d, A, 10).total, 0);
|
|
230
|
+
mention(d, 7);
|
|
231
|
+
assert.equal(pendingMentions(d, A, 10).total, 1, "a later mention is unaffected");
|
|
232
|
+
assert.ok(hasUnsentMentions(d, A), "and still wakes");
|
|
233
|
+
});
|
|
234
|
+
it("dismiss on an empty ledger is a no-op", () => {
|
|
235
|
+
const d = db();
|
|
236
|
+
assert.equal(dismissMentions(d, A), 0);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
describe("cross-brane", () => {
|
|
240
|
+
it("stamps brane_id only when it differs from the reader's", () => {
|
|
241
|
+
const d = db();
|
|
242
|
+
mention(d, 1, C); // C lives in brane-far
|
|
243
|
+
mention(d, 1, B); // B is home
|
|
244
|
+
const rows = pendingMentions(d, A, 10, "brane-home").rows;
|
|
245
|
+
const fromFar = rows.find((r) => r.src === C);
|
|
246
|
+
const fromHome = rows.find((r) => r.src === B);
|
|
247
|
+
assert.equal(fromFar?.brane_id, "brane-far");
|
|
248
|
+
assert.equal(fromHome?.brane_id, undefined);
|
|
249
|
+
});
|
|
250
|
+
it("carries the author separately from the source narrative", () => {
|
|
251
|
+
const d = db();
|
|
252
|
+
// C narrated into B's narrative, mentioning A: read B, but C said it.
|
|
253
|
+
recordMentions(d, {
|
|
254
|
+
mentionedNids: [A],
|
|
255
|
+
srcNarrative: B,
|
|
256
|
+
srcSeq: 3,
|
|
257
|
+
author: C,
|
|
258
|
+
time: 1,
|
|
259
|
+
});
|
|
260
|
+
const row = pendingMentions(d, A, 10).rows[0];
|
|
261
|
+
assert.equal(row.src, B, "where to read");
|
|
262
|
+
assert.equal(row.author, C, "who said it");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
//# sourceMappingURL=mentions-ledger.test.js.map
|