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,147 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createTestDb } from "./helpers.js";
|
|
4
|
+
import { handleMcpNarrate, startMcpListen, wakeListenWaiters, } from "../daemon/server.js";
|
|
5
|
+
import { appendEvent } from "../db/events.js";
|
|
6
|
+
import { recordMentions, pendingMentions } from "../db/mentions.js";
|
|
7
|
+
// A read returns what is in range, the caller's own narrations included, but
|
|
8
|
+
// the caller's own narrations never interrupt its waiting. #515 got the second
|
|
9
|
+
// by filtering them out of every forward read, which also lost them for an
|
|
10
|
+
// agent resuming after compaction with a remembered cursor; nothing
|
|
11
|
+
// server-side can tell that read from any other. Now they come back, and only
|
|
12
|
+
// someone else's event is an arrival. The one thing never returned is the event
|
|
13
|
+
// a narrate just wrote, to that narrate (#508's receipt, #509's `omit`).
|
|
14
|
+
const SELF = "claude-aaaaaaaa";
|
|
15
|
+
const OTHER = "claude-bbbbbbbb";
|
|
16
|
+
const BRANE = "brane-home";
|
|
17
|
+
function fakeClient() {
|
|
18
|
+
const sent = [];
|
|
19
|
+
const ws = {
|
|
20
|
+
readyState: 1,
|
|
21
|
+
send: (raw) => {
|
|
22
|
+
const msg = JSON.parse(raw);
|
|
23
|
+
if (msg.type === "tool_result")
|
|
24
|
+
sent.push({ callId: msg.id, result: msg.result });
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
client: { ws: ws, narrativeId: SELF, agentType: "claude", pendingListens: new Map() },
|
|
29
|
+
sent,
|
|
30
|
+
ws,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function db() {
|
|
34
|
+
const d = createTestDb();
|
|
35
|
+
const ins = d.prepare("INSERT INTO narratives (id, brane_id, created_at, status) VALUES (?, ?, 0, 'active')");
|
|
36
|
+
ins.run(SELF, BRANE);
|
|
37
|
+
ins.run(OTHER, BRANE);
|
|
38
|
+
return d;
|
|
39
|
+
}
|
|
40
|
+
const say = (d, text, extra = {}) => handleMcpNarrate({ say: text, ...extra }, d, SELF, () => { });
|
|
41
|
+
const signal = (d, text) => appendEvent(d, SELF, "human_signal", { content: text, source: "web" });
|
|
42
|
+
const listen = (c, d, id, args, waiters = new Map()) => startMcpListen(c, d, id, args, waiters, () => { }, BRANE);
|
|
43
|
+
const wakeAll = (waiters, nid) => {
|
|
44
|
+
for (const w of [...(waiters.get(nid) ?? [])])
|
|
45
|
+
w();
|
|
46
|
+
};
|
|
47
|
+
describe("a read returns the agent's own words", () => {
|
|
48
|
+
it("a non-blocking read hands back an earlier narration", () => {
|
|
49
|
+
const d = db();
|
|
50
|
+
const { client, sent } = fakeClient();
|
|
51
|
+
signal(d, "a"); // 1
|
|
52
|
+
say(d, "b"); // 2
|
|
53
|
+
signal(d, "c"); // 3
|
|
54
|
+
listen(client, d, "1", { after: 1, wait: false });
|
|
55
|
+
assert.deepEqual(sent[0].result.events.self.map((e) => e.content), ["b", "c"]);
|
|
56
|
+
});
|
|
57
|
+
it("even when they are all that is in range", () => {
|
|
58
|
+
const d = db();
|
|
59
|
+
const { client, sent } = fakeClient();
|
|
60
|
+
signal(d, "a"); // 1
|
|
61
|
+
say(d, "b"); // 2
|
|
62
|
+
listen(client, d, "1", { after: 1, wait: false });
|
|
63
|
+
assert.deepEqual(sent[0].result.events.self.map((e) => e.content), ["b"]);
|
|
64
|
+
});
|
|
65
|
+
it("the narrate receipt leaves out only the narration it wrote", () => {
|
|
66
|
+
const d = db();
|
|
67
|
+
signal(d, "a"); // 1
|
|
68
|
+
say(d, "b"); // 2
|
|
69
|
+
signal(d, "c"); // 3
|
|
70
|
+
const r = say(d, "e", { after: 1 }); // 4
|
|
71
|
+
assert.equal(r.seq, 4);
|
|
72
|
+
assert.deepEqual(r.events.self.map((e) => e.content), ["b", "c"]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe("the agent's own words never interrupt its waiting", () => {
|
|
76
|
+
it("a parked listen does not wake on its own narration, and brings it back with the next signal", () => {
|
|
77
|
+
const d = db();
|
|
78
|
+
const { client, sent } = fakeClient();
|
|
79
|
+
const waiters = new Map();
|
|
80
|
+
const notify = (nid) => wakeListenWaiters(waiters, nid);
|
|
81
|
+
signal(d, "go"); // 1
|
|
82
|
+
listen(client, d, "1", { after: 1 }, waiters);
|
|
83
|
+
handleMcpNarrate({ say: "working on it" }, d, SELF, notify); // 2, fires the wakeup
|
|
84
|
+
assert.equal(sent.length, 0, "its own words are not an arrival");
|
|
85
|
+
signal(d, "status?"); // 3
|
|
86
|
+
notify(SELF); // still registered: no 500 ms poller wait
|
|
87
|
+
assert.equal(sent.length, 1);
|
|
88
|
+
assert.deepEqual(sent[0].result.events.self.map((e) => e.seq), [2, 3]);
|
|
89
|
+
});
|
|
90
|
+
it("a blocking listen whose cursor trails its own narration parks instead of returning at once", () => {
|
|
91
|
+
const d = db();
|
|
92
|
+
const { client, sent } = fakeClient();
|
|
93
|
+
signal(d, "question?"); // 1
|
|
94
|
+
say(d, "progress…", { after: 1 }); // 2
|
|
95
|
+
listen(client, d, "1", { after: 1 });
|
|
96
|
+
assert.equal(sent.length, 0);
|
|
97
|
+
});
|
|
98
|
+
it("nor does a waiting narrate: the lagging cursor #508 leaves behind", () => {
|
|
99
|
+
const d = db();
|
|
100
|
+
const { client, sent } = fakeClient();
|
|
101
|
+
signal(d, "question?"); // 1
|
|
102
|
+
say(d, "progress…", { after: 1 }); // 2
|
|
103
|
+
const plan = handleMcpNarrate({ say: "done", after: 1, wait: true }, d, SELF, () => { });
|
|
104
|
+
const { park, ...extra } = plan;
|
|
105
|
+
startMcpListen(client, d, "w", { after: park.after }, new Map(), () => { }, BRANE, { omit: park.omit, extra });
|
|
106
|
+
assert.equal(sent.length, 0, "parked, rather than returning seq 2 at waited_ms 0");
|
|
107
|
+
});
|
|
108
|
+
it("covers foreign narration: what the agent said in another narrative wakes nothing either", () => {
|
|
109
|
+
const d = db();
|
|
110
|
+
const { client, sent } = fakeClient();
|
|
111
|
+
const waiters = new Map();
|
|
112
|
+
listen(client, d, "1", { after: { self: 0, [OTHER]: 0 } }, waiters);
|
|
113
|
+
say(d, "a question for you", { nid: OTHER }); // OTHER 1
|
|
114
|
+
wakeAll(waiters, OTHER);
|
|
115
|
+
assert.equal(sent.length, 0);
|
|
116
|
+
appendEvent(d, OTHER, "narration", { content: "the answer", source: OTHER });
|
|
117
|
+
wakeAll(waiters, OTHER);
|
|
118
|
+
assert.deepEqual(sent[0].result.events[OTHER].map((e) => e.content), ["a question for you", "the answer"]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("a listen whose socket has closed", () => {
|
|
122
|
+
it("does not run its receipt, so a mention is never marked delivered to nobody", () => {
|
|
123
|
+
const d = db();
|
|
124
|
+
const { client, sent, ws } = fakeClient();
|
|
125
|
+
const waiters = new Map();
|
|
126
|
+
signal(d, "go"); // SELF 1
|
|
127
|
+
listen(client, d, "1", { after: { self: 1, [OTHER]: 0 } }, waiters);
|
|
128
|
+
ws.readyState = 3; // CLOSED, close handler not yet run
|
|
129
|
+
const ev = appendEvent(d, OTHER, "narration", { content: `@${SELF} look`, source: OTHER });
|
|
130
|
+
recordMentions(d, { srcNarrative: OTHER, srcSeq: ev.seq, author: OTHER, mentionedNids: [SELF], time: ev.time });
|
|
131
|
+
wakeAll(waiters, OTHER);
|
|
132
|
+
assert.equal(sent.length, 0);
|
|
133
|
+
assert.equal(pendingMentions(d, SELF, 10).total, 1, "still owed: nobody received it");
|
|
134
|
+
});
|
|
135
|
+
it("can be retired: cleanup unregisters its wakeups", () => {
|
|
136
|
+
const d = db();
|
|
137
|
+
const { client } = fakeClient();
|
|
138
|
+
const waiters = new Map();
|
|
139
|
+
listen(client, d, "1", { after: { self: 0, [OTHER]: 0 } }, waiters);
|
|
140
|
+
assert.equal(waiters.get(SELF)?.size, 1);
|
|
141
|
+
for (const p of client.pendingListens.values())
|
|
142
|
+
p.cleanup();
|
|
143
|
+
assert.equal(waiters.get(SELF)?.size, 0);
|
|
144
|
+
assert.equal(waiters.get(OTHER)?.size, 0);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
//# sourceMappingURL=listen-own-words.test.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { listenTimeoutFor } from "../daemon/server.js";
|
|
4
|
+
describe("listenTimeoutFor", () => {
|
|
5
|
+
// claude's stdio idle timeout is raised to 1h by the .mcp.json `timeout`
|
|
6
|
+
// knob (spawn.ts, MCP_TOOL_TIMEOUT_MS), so claude can hold ~1h. 3400s sits
|
|
7
|
+
// under that ceiling and under the 1h prompt-cache TTL.
|
|
8
|
+
it("holds claude for ~1h (3400s)", () => {
|
|
9
|
+
assert.equal(listenTimeoutFor("claude"), 3_400_000);
|
|
10
|
+
});
|
|
11
|
+
// Every other agent keeps the conservative 270s hold until it has been
|
|
12
|
+
// observed honoring its own timeout knob — codex/copilot default to ~60s
|
|
13
|
+
// and would abort a longer hold every idle cycle. A raise must be per-type,
|
|
14
|
+
// never a global flip, so unknown/other types must fall through to default.
|
|
15
|
+
it("keeps non-claude agents at 270s", () => {
|
|
16
|
+
assert.equal(listenTimeoutFor("codex"), 270_000);
|
|
17
|
+
assert.equal(listenTimeoutFor("copilot"), 270_000);
|
|
18
|
+
assert.equal(listenTimeoutFor("goose"), 270_000);
|
|
19
|
+
assert.equal(listenTimeoutFor("gemini"), 270_000);
|
|
20
|
+
assert.equal(listenTimeoutFor("amp"), 270_000);
|
|
21
|
+
});
|
|
22
|
+
it("defaults to 270s for unknown or missing agent type", () => {
|
|
23
|
+
assert.equal(listenTimeoutFor(undefined), 270_000);
|
|
24
|
+
assert.equal(listenTimeoutFor(""), 270_000);
|
|
25
|
+
assert.equal(listenTimeoutFor("some-future-agent"), 270_000);
|
|
26
|
+
});
|
|
27
|
+
it("chosen claude hold stays under the 1h cache TTL and the 1h client ceiling", () => {
|
|
28
|
+
// If this ever fails, an idle self-wake would race the cache TTL / the
|
|
29
|
+
// client's tool-call abort instead of landing safely under both.
|
|
30
|
+
assert.ok(listenTimeoutFor("claude") < 3_600_000);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=listen-timeout.test.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { diagnoseMarkdown } from "../markdown-diagnostics.js";
|
|
4
|
+
import { prepareMathMarkdown, renderMathHtml } from "../shared/markdown-math.js";
|
|
5
|
+
// Every renderer here fails silently on purpose — that is what keeps a stray
|
|
6
|
+
// `$5` from turning into red error markup in the operator's face. These tests
|
|
7
|
+
// pin the other side of that bargain: the author of a narration finds out, and
|
|
8
|
+
// finds out precisely enough to decide whether it is worth correcting.
|
|
9
|
+
function kinds(md) {
|
|
10
|
+
return diagnoseMarkdown(md).map((d) => d.kind);
|
|
11
|
+
}
|
|
12
|
+
describe("diagnoseMarkdown — silence on anything that renders as written", () => {
|
|
13
|
+
it("says nothing about prose with no math in it", () => {
|
|
14
|
+
assert.deepEqual(kinds("Just a paragraph, with a `code span` and a [link](x)."), []);
|
|
15
|
+
});
|
|
16
|
+
it("says nothing about math that parses", () => {
|
|
17
|
+
assert.deepEqual(kinds("Euler: $e^{i\\pi} + 1 = 0$, and $$\\int_0^1 x\\,dx$$"), []);
|
|
18
|
+
});
|
|
19
|
+
/** The guard rejects far more currency and shell syntax than mathematics, so
|
|
20
|
+
* warning on every rejection would bury the real findings under noise. */
|
|
21
|
+
it("says nothing about the dollars that were never math", () => {
|
|
22
|
+
for (const md of ["Costs $5 and $10.", "Set $PATH and ${HOME}.", "awk '{print $1, $9}'"]) {
|
|
23
|
+
assert.deepEqual(kinds(md), [], md);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
it("says nothing about a closed fence, of either flavour", () => {
|
|
27
|
+
assert.deepEqual(kinds("```js\nconst x = 1;\n```\n"), []);
|
|
28
|
+
assert.deepEqual(kinds("~~~\nplain\n~~~"), []);
|
|
29
|
+
assert.deepEqual(kinds("```js\nconst x = 1;\n```"), []);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("diagnoseMarkdown — what it reports", () => {
|
|
33
|
+
it("reports LaTeX KaTeX refused, in KaTeX's own words", () => {
|
|
34
|
+
const [d] = diagnoseMarkdown("Broken: $\\bogusmacro{x}$ here");
|
|
35
|
+
assert.equal(d.kind, "math_parse_error");
|
|
36
|
+
assert.match(d.message, /Undefined control sequence/);
|
|
37
|
+
// KaTeX's prefix and its combining-underline position marker are stripped:
|
|
38
|
+
// one reads as boilerplate, the other as mojibake outside a browser.
|
|
39
|
+
assert.ok(!d.message.startsWith("KaTeX parse error"), d.message);
|
|
40
|
+
assert.ok(!/̲/.test(d.message), "no combining underline");
|
|
41
|
+
});
|
|
42
|
+
it("reports a run that looks like math but was demoted, and why", () => {
|
|
43
|
+
const [d] = diagnoseMarkdown("Spaced: $ \\theta $ there");
|
|
44
|
+
assert.equal(d.kind, "math_demoted");
|
|
45
|
+
assert.match(d.message, /a space sits just inside the delimiters/);
|
|
46
|
+
});
|
|
47
|
+
it("reports TeX delimiters with no partner", () => {
|
|
48
|
+
assert.deepEqual(kinds("Unclosed: \\(x + y and the sentence goes on"), ["math_unclosed"]);
|
|
49
|
+
assert.deepEqual(kinds("Runaway:\n$$ x + y and no closer"), ["math_unclosed"]);
|
|
50
|
+
});
|
|
51
|
+
it("reports an unclosed fence, whose damage is not local", () => {
|
|
52
|
+
const [d] = diagnoseMarkdown("```js\nconst x = 1;\nand the rest of the message");
|
|
53
|
+
assert.equal(d.kind, "fence_unclosed");
|
|
54
|
+
assert.match(d.message, /everything after it renders as one code block/);
|
|
55
|
+
});
|
|
56
|
+
it("stays quiet on `$$` that cannot swallow anything", () => {
|
|
57
|
+
// Found by sweeping the corpus: a price tier, and a mid-sentence pair.
|
|
58
|
+
assert.deepEqual(kinds("- **$$$**: battery emulators, only if serious.\n\nStill prose."), []);
|
|
59
|
+
assert.deepEqual(kinds("costs $$ unless volume)\n\nnext para"), []);
|
|
60
|
+
// ...but at a line start it is a flow fence, list marker or not.
|
|
61
|
+
assert.deepEqual(kinds("- $$ x = 1 and\n\nnext para"), ["math_unclosed"]);
|
|
62
|
+
});
|
|
63
|
+
it("does not let a stray backtick run in prose hide a real fence", () => {
|
|
64
|
+
const md = "The file has its own ```lua block.\n\n````md\n# doc\n\n```lua\nx\n```\n\nend\n````\n\nafter";
|
|
65
|
+
assert.deepEqual(kinds(md), []);
|
|
66
|
+
});
|
|
67
|
+
it("points at the failed run, not an earlier listing of the same LaTeX", () => {
|
|
68
|
+
const [d] = diagnoseMarkdown("```latex\n\\frac{a}{b}\n```\n\nbroken: $\\frac{a}{$ here");
|
|
69
|
+
assert.equal(d.kind, "math_parse_error");
|
|
70
|
+
assert.equal(d.line, 5);
|
|
71
|
+
});
|
|
72
|
+
it("points at the line, so a long narration can be found in", () => {
|
|
73
|
+
const [d] = diagnoseMarkdown("one\ntwo\n\nthree $ \\theta $ four\n");
|
|
74
|
+
assert.equal(d.line, 4);
|
|
75
|
+
});
|
|
76
|
+
it("caps what one narration can produce", () => {
|
|
77
|
+
const md = Array.from({ length: 30 }, () => "$ \\theta $").join("\n\n");
|
|
78
|
+
assert.ok(diagnoseMarkdown(md).length <= 10);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
/** The warning is only worth acting on if it describes the operator's actual
|
|
82
|
+
* view. Both halves of that come from the real pipeline rather than a second
|
|
83
|
+
* scanner written to approximate it — this is the test that they agree. */
|
|
84
|
+
describe("diagnoseMarkdown — the fallback is what will really be shown", () => {
|
|
85
|
+
it("hands back the literal source a failed render falls back to", () => {
|
|
86
|
+
const [d] = diagnoseMarkdown("Broken: $\\bogusmacro{x}$ here");
|
|
87
|
+
assert.equal(d.fallback, "$\\bogusmacro{x}$");
|
|
88
|
+
// …which is exactly what the renderer emits for it, HTML-escaped.
|
|
89
|
+
assert.equal(renderMathHtml("\\bogusmacro{x}", false), "$\\bogusmacro{x}$");
|
|
90
|
+
});
|
|
91
|
+
it("hands back the demoted run as the prose it becomes", () => {
|
|
92
|
+
const [d] = diagnoseMarkdown("Spaced: $ \\theta $ there");
|
|
93
|
+
assert.equal(d.fallback, "$ \\theta $");
|
|
94
|
+
// The opener is escaped, and `\$` renders as a plain `$`.
|
|
95
|
+
assert.ok(prepareMathMarkdown("Spaced: $ \\theta $ there").includes("\\$ \\theta"));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
/** Collecting must not change what anyone sees. The export and the app call
|
|
99
|
+
* these same two functions, and a diagnostic pass that perturbed either would
|
|
100
|
+
* be a rendering bug wearing a linter's clothes. */
|
|
101
|
+
describe("collecting is free of side effects", () => {
|
|
102
|
+
it("leaves prepareMathMarkdown's output identical with and without a collector", () => {
|
|
103
|
+
const sources = [
|
|
104
|
+
"Costs $5 and $10.",
|
|
105
|
+
"Euler: $e^{i\\pi}+1=0$",
|
|
106
|
+
"\\[B = m\\ddot U\\]",
|
|
107
|
+
"```js\nunclosed",
|
|
108
|
+
"$$ runaway",
|
|
109
|
+
"Spaced: $ \\theta $",
|
|
110
|
+
];
|
|
111
|
+
for (const md of sources) {
|
|
112
|
+
assert.equal(prepareMathMarkdown(md, () => { }), prepareMathMarkdown(md), md);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
it("leaves renderMathHtml's output identical with and without an onError", () => {
|
|
116
|
+
for (const [value, display] of [["x^2", false], ["\\bogusmacro{", false], ["\\int_0^1", true]]) {
|
|
117
|
+
assert.equal(renderMathHtml(value, display, () => { }), renderMathHtml(value, display));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=markdown-diagnostics.test.js.map
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { micromark } from "micromark";
|
|
4
|
+
import { gfmTaskListItem, gfmTaskListItemHtml } from "micromark-extension-gfm-task-list-item";
|
|
5
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
6
|
+
import { gfmTaskListItemFromMarkdown } from "mdast-util-gfm-task-list-item";
|
|
7
|
+
import { listIsLoose, listItemRowMarginTop, listParagraphOverride, } from "../shared/markdown-lists.js";
|
|
8
|
+
// Lists are rendered twice by two unrelated pieces of machinery: micromark
|
|
9
|
+
// straight to HTML for the export, and mdast through react-native primitives
|
|
10
|
+
// for the app. Nothing forces them to agree, and the ways they can disagree are
|
|
11
|
+
// invisible in a unit test of either one alone — so the tests below check the
|
|
12
|
+
// app's inputs *against micromark's output*, which is the only shared ground.
|
|
13
|
+
function html(md) {
|
|
14
|
+
return micromark(md, {
|
|
15
|
+
extensions: [gfmTaskListItem()],
|
|
16
|
+
htmlExtensions: [gfmTaskListItemHtml()],
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function tree(md) {
|
|
20
|
+
return fromMarkdown(md, {
|
|
21
|
+
extensions: [gfmTaskListItem()],
|
|
22
|
+
mdastExtensions: [gfmTaskListItemFromMarkdown()],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** The first list in a document, as the app's renderer would receive it. */
|
|
26
|
+
function firstList(md) {
|
|
27
|
+
return tree(md).children.find((n) => n.type === "list");
|
|
28
|
+
}
|
|
29
|
+
/** Whether micromark wrapped this document's list items in paragraphs — its
|
|
30
|
+
* own, independent verdict on looseness, and the thing `listIsLoose` has to
|
|
31
|
+
* reproduce from the tree. */
|
|
32
|
+
function htmlIsLoose(md) {
|
|
33
|
+
return /<li>\n<p>/.test(html(md));
|
|
34
|
+
}
|
|
35
|
+
describe("task list items", () => {
|
|
36
|
+
it("parses the checked state the app renders from", () => {
|
|
37
|
+
const list = firstList("- [x] done\n- [ ] pending\n- plain\n");
|
|
38
|
+
assert.deepEqual(list.children.map((i) => i.checked), [true, false, null]);
|
|
39
|
+
});
|
|
40
|
+
it("leaves `checked` null for an ordinary item, which is what keeps its bullet", () => {
|
|
41
|
+
// The app swaps the marker for a checkbox on `checked != null`. `false` is
|
|
42
|
+
// an unticked box and `null` is a bullet, so the two must not be conflated
|
|
43
|
+
// — a `!checked` test would draw bullets over every unticked task.
|
|
44
|
+
const list = firstList("- plain\n");
|
|
45
|
+
assert.equal(list.children[0].checked, null);
|
|
46
|
+
});
|
|
47
|
+
it("accepts an uppercase X, as GFM does", () => {
|
|
48
|
+
assert.equal(firstList("- [X] done\n").children[0].checked, true);
|
|
49
|
+
});
|
|
50
|
+
it("renders a real checkbox input in the export", () => {
|
|
51
|
+
const out = html("- [x] done\n- [ ] pending\n");
|
|
52
|
+
assert.match(out, /<li><input type="checkbox" disabled="" checked="" \/> done<\/li>/);
|
|
53
|
+
assert.match(out, /<li><input type="checkbox" disabled="" \/> pending<\/li>/);
|
|
54
|
+
});
|
|
55
|
+
it("puts the input inside the paragraph when the list is loose", () => {
|
|
56
|
+
// Both shapes exist, which is why src/export.ts needs two `list-style: none`
|
|
57
|
+
// selectors rather than one.
|
|
58
|
+
assert.match(html("- [x] one\n\n- [ ] two\n"), /<li>\n<p><input type="checkbox"/);
|
|
59
|
+
});
|
|
60
|
+
it("nests", () => {
|
|
61
|
+
const list = firstList("- [ ] parent\n - [x] child\n");
|
|
62
|
+
assert.equal(list.children[0].checked, false);
|
|
63
|
+
const nested = list.children[0].children.find((n) => n.type === "list");
|
|
64
|
+
assert.equal(nested.children[0].checked, true);
|
|
65
|
+
});
|
|
66
|
+
it("does not fire on a bracket that isn't a task marker", () => {
|
|
67
|
+
// `[x]` mid-item, and a link reference — neither is a checkbox.
|
|
68
|
+
assert.equal(firstList("- see [x] here\n").children[0].checked, null);
|
|
69
|
+
assert.doesNotMatch(html("- see [x] here\n"), /input type="checkbox"/);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("listIsLoose", () => {
|
|
73
|
+
// Each case is checked twice: once against micromark's HTML, which is what
|
|
74
|
+
// the export ships, and once against `listIsLoose`, which is what the app
|
|
75
|
+
// decides. A case where the two disagree is the bug this exists to catch.
|
|
76
|
+
const cases = [
|
|
77
|
+
["tight", "- one\n- two\n"],
|
|
78
|
+
["loose", "- one\n\n- two\n"],
|
|
79
|
+
["tight, ordered", "1. one\n2. two\n"],
|
|
80
|
+
["loose, ordered", "1. one\n\n2. two\n"],
|
|
81
|
+
["tight task list", "- [x] one\n- [ ] two\n"],
|
|
82
|
+
["loose task list", "- [x] one\n\n- [ ] two\n"],
|
|
83
|
+
["single item", "- only\n"],
|
|
84
|
+
["item holding two blocks", "- one\n\n still one\n- two\n"],
|
|
85
|
+
];
|
|
86
|
+
for (const [name, md] of cases) {
|
|
87
|
+
it(`agrees with micromark: ${name}`, () => {
|
|
88
|
+
assert.equal(listIsLoose(firstList(md)), htmlIsLoose(md), md);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
it("is a property of the list, not of the item", () => {
|
|
92
|
+
// CommonMark: one blank line anywhere makes the *whole* list loose, so the
|
|
93
|
+
// second item gets paragraph margins even though it is not itself spread.
|
|
94
|
+
// Reading `item.spread` per item — the obvious shortcut — would space the
|
|
95
|
+
// two differently, and differently from the export.
|
|
96
|
+
const list = firstList("- one\n\n still one\n- two\n");
|
|
97
|
+
assert.equal(list.children[1].spread, false);
|
|
98
|
+
assert.equal(listIsLoose(list), true);
|
|
99
|
+
});
|
|
100
|
+
it("falls back to the child count for a hand-built tree", () => {
|
|
101
|
+
assert.equal(listIsLoose({ children: [{ children: [{}, {}] }] }), true);
|
|
102
|
+
assert.equal(listIsLoose({ children: [{ children: [{}] }] }), false);
|
|
103
|
+
});
|
|
104
|
+
it("treats a missing list as tight", () => {
|
|
105
|
+
assert.equal(listIsLoose(undefined), false);
|
|
106
|
+
assert.equal(listIsLoose(null), false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe("list item marker alignment", () => {
|
|
110
|
+
// A list item row is `[marker <Text>][content <View>]`, top-aligned, so a
|
|
111
|
+
// margin *inside* the content column moves the text and leaves the marker
|
|
112
|
+
// behind. These pin the arithmetic that keeps the two on the same line —
|
|
113
|
+
// the defect was a `1.` sitting ~8px above the line it numbered, on every
|
|
114
|
+
// item of every loose list.
|
|
115
|
+
const PARA = 8; // styles.paragraph.marginVertical
|
|
116
|
+
/** Where the marker and the item's first line land, in the row's own
|
|
117
|
+
* coordinates. `beforeFix` models what the renderer used to do: no margin on
|
|
118
|
+
* the row, the full paragraph margin on the first block. */
|
|
119
|
+
function layout(loose, beforeFix = false) {
|
|
120
|
+
const rowMarginTop = beforeFix ? 0 : listItemRowMarginTop(loose, PARA);
|
|
121
|
+
const override = beforeFix
|
|
122
|
+
? loose
|
|
123
|
+
? null
|
|
124
|
+
: { marginVertical: 0 }
|
|
125
|
+
: listParagraphOverride({ inListItem: true, firstBlock: true, loose });
|
|
126
|
+
const paraMarginTop = override && "marginVertical" in override
|
|
127
|
+
? override.marginVertical
|
|
128
|
+
: override && "marginTop" in override
|
|
129
|
+
? override.marginTop
|
|
130
|
+
: PARA;
|
|
131
|
+
return { marker: rowMarginTop, text: rowMarginTop + paraMarginTop };
|
|
132
|
+
}
|
|
133
|
+
for (const loose of [false, true]) {
|
|
134
|
+
it(`puts the marker on its own first line (${loose ? "loose" : "tight"})`, () => {
|
|
135
|
+
const { marker, text } = layout(loose);
|
|
136
|
+
assert.equal(marker, text);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
it("reproduces the defect it fixes", () => {
|
|
140
|
+
// The guard against a "fix" that changes nothing: before, a loose item's
|
|
141
|
+
// text sat a full paragraph margin below its marker.
|
|
142
|
+
const before = layout(true, true);
|
|
143
|
+
assert.equal(before.text - before.marker, PARA);
|
|
144
|
+
// Tight lists were always fine — which is why this looked like a new bug.
|
|
145
|
+
const tight = layout(false, true);
|
|
146
|
+
assert.equal(tight.text - tight.marker, 0);
|
|
147
|
+
});
|
|
148
|
+
for (const loose of [false, true]) {
|
|
149
|
+
it(`leaves the text exactly where it was (${loose ? "loose" : "tight"})`, () => {
|
|
150
|
+
// The margin moved from the paragraph to the row; it was not removed. If
|
|
151
|
+
// this drifts, loose lists have quietly retightened and the app no longer
|
|
152
|
+
// matches the export.
|
|
153
|
+
assert.equal(layout(loose).text, layout(loose, true).text);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
it("only the first block of an item gives up its top margin", () => {
|
|
157
|
+
// `- one\n\n still one` — the second paragraph sits below the marker, not
|
|
158
|
+
// beside it, so it keeps the margin that separates the two blocks.
|
|
159
|
+
assert.deepEqual(listParagraphOverride({ inListItem: true, firstBlock: false, loose: true }), null);
|
|
160
|
+
assert.deepEqual(listParagraphOverride({ inListItem: true, firstBlock: true, loose: true }), { marginTop: 0 });
|
|
161
|
+
});
|
|
162
|
+
it("flattens every paragraph of a tight item, first or not", () => {
|
|
163
|
+
for (const firstBlock of [true, false]) {
|
|
164
|
+
assert.deepEqual(listParagraphOverride({ inListItem: true, firstBlock, loose: false }), { marginVertical: 0 });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
it("leaves a paragraph outside any list alone", () => {
|
|
168
|
+
for (const loose of [true, false]) {
|
|
169
|
+
assert.equal(listParagraphOverride({ inListItem: false, firstBlock: true, loose }), null);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
it("gives a tight row no margin of its own", () => {
|
|
173
|
+
assert.equal(listItemRowMarginTop(false, PARA), 0);
|
|
174
|
+
assert.equal(listItemRowMarginTop(true, PARA), PARA);
|
|
175
|
+
});
|
|
176
|
+
it("keeps the gap between loose items at what two paragraph margins gave", () => {
|
|
177
|
+
// Item N's trailing paragraph margin plus item N+1's row margin. RN does no
|
|
178
|
+
// margin collapsing, so this was 8+8 before and has to stay 8+8.
|
|
179
|
+
assert.equal(PARA + listItemRowMarginTop(true, PARA), PARA + PARA);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
//# sourceMappingURL=markdown-lists.test.js.map
|