brane 0.1.1 → 0.3.1

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.
Files changed (352) hide show
  1. package/AGENTS.md +367 -0
  2. package/CHANGELOG.md +66 -0
  3. package/CONTRIBUTING.md +101 -0
  4. package/DESIGN.md +405 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +213 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +133 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +360 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1101 -0
  37. package/dist/commands/token.js +154 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6353 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +643 -0
  58. package/dist/db/tokens.js +624 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/app-origin.js +26 -0
  96. package/dist/shared/block-glyphs.js +114 -0
  97. package/dist/shared/diagram-css.js +17 -0
  98. package/dist/shared/export-selection.js +101 -0
  99. package/dist/shared/find-in-text.js +110 -0
  100. package/dist/shared/glow-hues.js +225 -0
  101. package/dist/shared/hex-dump.js +62 -0
  102. package/dist/shared/hex-pages.js +52 -0
  103. package/dist/shared/hit-drum.js +79 -0
  104. package/dist/shared/known-nids.js +58 -0
  105. package/dist/shared/list-scroll-keys.js +56 -0
  106. package/dist/shared/markdown-lists.js +72 -0
  107. package/dist/shared/markdown-math.js +485 -0
  108. package/dist/shared/markdown-mentions.js +79 -0
  109. package/dist/shared/markdown-nids.js +41 -0
  110. package/dist/shared/markdown-protocols.js +53 -0
  111. package/dist/shared/markdown-refs.js +123 -0
  112. package/dist/shared/membrane-invasion.js +342 -0
  113. package/dist/shared/membrane-motion.js +83 -0
  114. package/dist/shared/membrane-palette.js +253 -0
  115. package/dist/shared/membrane-terrain.js +212 -0
  116. package/dist/shared/message-selection.js +123 -0
  117. package/dist/shared/mo-glyph.js +57 -0
  118. package/dist/shared/narrative-format.js +237 -0
  119. package/dist/shared/narrative-menu.js +84 -0
  120. package/dist/shared/narrative-sort.js +47 -0
  121. package/dist/shared/narrative-status.js +52 -0
  122. package/dist/shared/no-pan.js +85 -0
  123. package/dist/shared/pane-layout.js +50 -0
  124. package/dist/shared/payload-inline.js +83 -0
  125. package/dist/shared/speaker-color.js +61 -0
  126. package/dist/shared/token-rotation.js +14 -0
  127. package/dist/shared/view-url.js +216 -0
  128. package/dist/shared/wiki-links.js +70 -0
  129. package/dist/shared/wiki-window.js +4 -0
  130. package/dist/tailscale.js +55 -0
  131. package/dist/test/absorb-claude.test.js +892 -0
  132. package/dist/test/agent-scope.test.js +193 -0
  133. package/dist/test/agents.test.js +55 -0
  134. package/dist/test/api-request.test.js +122 -0
  135. package/dist/test/app-host-literal.test.js +53 -0
  136. package/dist/test/awaiting-glow.test.js +54 -0
  137. package/dist/test/awaiting.test.js +122 -0
  138. package/dist/test/bind-token.test.js +88 -0
  139. package/dist/test/block-glyphs.test.js +68 -0
  140. package/dist/test/cert-manager.test.js +262 -0
  141. package/dist/test/claude-live-session.test.js +110 -0
  142. package/dist/test/codex-tool-log.test.js +568 -0
  143. package/dist/test/config.test.js +71 -0
  144. package/dist/test/copilot-tool-log.test.js +93 -0
  145. package/dist/test/cors-preflight.test.js +56 -0
  146. package/dist/test/cost-linger.test.js +31 -0
  147. package/dist/test/cost-pricing.test.js +72 -0
  148. package/dist/test/cost-reprice.test.js +139 -0
  149. package/dist/test/cost-sessions.test.js +301 -0
  150. package/dist/test/crush-tool-log.test.js +289 -0
  151. package/dist/test/daemon-client.test.js +109 -0
  152. package/dist/test/daemon-socket.test.js +293 -0
  153. package/dist/test/diagram-arrows.test.js +205 -0
  154. package/dist/test/diagram-export.test.js +210 -0
  155. package/dist/test/diagram-labels.test.js +313 -0
  156. package/dist/test/diagram-parse.test.js +191 -0
  157. package/dist/test/diagram-render.test.js +175 -0
  158. package/dist/test/diagram-review.test.js +288 -0
  159. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  160. package/dist/test/embedder.test.js +84 -0
  161. package/dist/test/events.test.js +123 -0
  162. package/dist/test/export-selection.test.js +102 -0
  163. package/dist/test/export.test.js +505 -0
  164. package/dist/test/files-browse.test.js +393 -0
  165. package/dist/test/files-find.test.js +272 -0
  166. package/dist/test/find-in-text.test.js +86 -0
  167. package/dist/test/first-paint-viewport.test.js +59 -0
  168. package/dist/test/fixtures/embedding-golden.js +23 -0
  169. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  170. package/dist/test/glow-hues.test.js +202 -0
  171. package/dist/test/harness-cost.test.js +423 -0
  172. package/dist/test/helpers.js +21 -0
  173. package/dist/test/hex-pages.test.js +68 -0
  174. package/dist/test/highlight.test.js +69 -0
  175. package/dist/test/hit-drum.test.js +71 -0
  176. package/dist/test/hook-config.test.js +299 -0
  177. package/dist/test/json-gzip.test.js +70 -0
  178. package/dist/test/key-routing.test.js +58 -0
  179. package/dist/test/known-nids.test.js +106 -0
  180. package/dist/test/link-preview.test.js +67 -0
  181. package/dist/test/list-scroll-keys.test.js +66 -0
  182. package/dist/test/listen-own-words.test.js +147 -0
  183. package/dist/test/listen-timeout.test.js +33 -0
  184. package/dist/test/markdown-diagnostics.test.js +121 -0
  185. package/dist/test/markdown-lists.test.js +182 -0
  186. package/dist/test/markdown-math.test.js +256 -0
  187. package/dist/test/markdown-nids.test.js +146 -0
  188. package/dist/test/markdown-protocols.test.js +139 -0
  189. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  190. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  191. package/dist/test/mcp-narrative.test.js +193 -0
  192. package/dist/test/mcp.test.js +548 -0
  193. package/dist/test/meld.test.js +143 -0
  194. package/dist/test/membrane-invasion.test.js +482 -0
  195. package/dist/test/membrane-motion.test.js +101 -0
  196. package/dist/test/membrane-palette.test.js +166 -0
  197. package/dist/test/membrane-terrain.test.js +170 -0
  198. package/dist/test/memory-log.test.js +202 -0
  199. package/dist/test/mentions-e2e.test.js +261 -0
  200. package/dist/test/mentions-ledger.test.js +265 -0
  201. package/dist/test/mentions-listen.test.js +265 -0
  202. package/dist/test/mentions.test.js +238 -0
  203. package/dist/test/message-selection.test.js +114 -0
  204. package/dist/test/mo-glyph.test.js +22 -0
  205. package/dist/test/model-resolution.test.js +166 -0
  206. package/dist/test/narrate-validate.test.js +77 -0
  207. package/dist/test/narrate-wait.test.js +160 -0
  208. package/dist/test/narrative-format.test.js +219 -0
  209. package/dist/test/narrative-menu.test.js +150 -0
  210. package/dist/test/narrative-sort.test.js +75 -0
  211. package/dist/test/no-pan.test.js +60 -0
  212. package/dist/test/pane-layout.test.js +153 -0
  213. package/dist/test/paths.test.js +37 -0
  214. package/dist/test/payload-inline.test.js +91 -0
  215. package/dist/test/precache-fonts.test.js +72 -0
  216. package/dist/test/presence-gate.test.js +440 -0
  217. package/dist/test/presence.test.js +160 -0
  218. package/dist/test/read-events.test.js +220 -0
  219. package/dist/test/remote-meld.test.js +128 -0
  220. package/dist/test/revive-cli.test.js +93 -0
  221. package/dist/test/search-floor.test.js +206 -0
  222. package/dist/test/search-fusion.test.js +168 -0
  223. package/dist/test/search.test.js +205 -0
  224. package/dist/test/security.test.js +48 -0
  225. package/dist/test/selection-wiring.test.js +61 -0
  226. package/dist/test/session-affinity.test.js +295 -0
  227. package/dist/test/signal-source.test.js +32 -0
  228. package/dist/test/socket-account.test.js +268 -0
  229. package/dist/test/spawn-toml.test.js +72 -0
  230. package/dist/test/speaker-color.test.js +90 -0
  231. package/dist/test/tailscale-rebind.test.js +79 -0
  232. package/dist/test/terminal-cursor.test.js +61 -0
  233. package/dist/test/terminal-resync.test.js +23 -0
  234. package/dist/test/terminal-size.test.js +36 -0
  235. package/dist/test/thin-cli.test.js +382 -0
  236. package/dist/test/token-bootstrap.test.js +272 -0
  237. package/dist/test/token-devices.test.js +646 -0
  238. package/dist/test/token-lapse.test.js +94 -0
  239. package/dist/test/token-ring.test.js +188 -0
  240. package/dist/test/token-rotate-cli.test.js +58 -0
  241. package/dist/test/token-routes.test.js +95 -0
  242. package/dist/test/tool-uses.test.js +1006 -0
  243. package/dist/test/usage-ledger.test.js +126 -0
  244. package/dist/test/view-url.test.js +627 -0
  245. package/dist/test/wiki-links.test.js +57 -0
  246. package/dist/test/wiki-sources.test.js +85 -0
  247. package/dist/test/wiki-window.test.js +31 -0
  248. package/dist/test/wiki.test.js +399 -0
  249. package/dist/test/wordpiece.test.js +68 -0
  250. package/dist/test/wrangler-route.test.js +27 -0
  251. package/dist/tls.js +148 -0
  252. package/dist/tmux.js +213 -0
  253. package/dist/user-agent.js +46 -0
  254. package/docker-entrypoint.sh +154 -0
  255. package/docs/README.md +3 -0
  256. package/docs/absorb.md +231 -0
  257. package/docs/cost-ledger.md +125 -0
  258. package/docs/daemon-api.md +189 -0
  259. package/docs/diagrams.md +163 -0
  260. package/docs/doctor.md +36 -0
  261. package/docs/export-scope.md +69 -0
  262. package/docs/files-view.md +133 -0
  263. package/docs/link-previews.md +38 -0
  264. package/docs/math.md +32 -0
  265. package/docs/mcp.md +69 -0
  266. package/docs/memory-search.md +75 -0
  267. package/docs/mentions.md +101 -0
  268. package/docs/multi-brane.md +14 -0
  269. package/docs/passkeys.md +165 -0
  270. package/docs/presence.md +202 -0
  271. package/docs/releasing.md +104 -0
  272. package/docs/rendering-warnings.md +22 -0
  273. package/docs/request-layer.md +72 -0
  274. package/docs/selection-store.md +50 -0
  275. package/docs/session-affinity.md +61 -0
  276. package/docs/terminal-mirroring.md +243 -0
  277. package/docs/token-rotation.md +431 -0
  278. package/docs/view-urls.md +134 -0
  279. package/docs/web-app.md +23 -0
  280. package/docs/wiki.md +43 -0
  281. package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
  282. package/expo/dist/apple-touch-icon.png +0 -0
  283. package/expo/dist/favicon.svg +3 -0
  284. package/expo/dist/index.html +177 -0
  285. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  301. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  302. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  303. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  304. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  305. package/expo/dist/katex/katex.min.css +1 -0
  306. package/expo/dist/manifest.json +16 -0
  307. package/expo/dist/metadata.json +1 -0
  308. package/expo/dist/og/membrane.v1.png +0 -0
  309. package/expo/dist/pdf.min.mjs +28 -0
  310. package/expo/dist/pdf.worker.min.mjs +28 -0
  311. package/expo/dist/sw.js +154 -0
  312. package/package.json +79 -27
  313. package/protocols/LIVE_loop.md +407 -0
  314. package/protocols/bebrief.md +41 -0
  315. package/protocols/digest.md +231 -0
  316. package/protocols/fly.md +266 -0
  317. package/protocols/idle.md +81 -0
  318. package/protocols/keeper.md +829 -0
  319. package/protocols/markdown.md +80 -0
  320. package/protocols/memory.md +419 -0
  321. package/protocols/mentions.md +93 -0
  322. package/protocols/payloads.md +53 -0
  323. package/protocols/prelude.md +91 -0
  324. package/protocols/ramify.md +105 -0
  325. package/protocols/review.md +356 -0
  326. package/protocols/rrr.md +232 -0
  327. package/protocols/scribe.md +429 -0
  328. package/protocols/summarize.md +203 -0
  329. package/protocols/twin.md +174 -0
  330. package/protocols/wiki.md +438 -0
  331. package/protocols/worker.md +38 -0
  332. package/wiki/shared/append-only-event-log.md +148 -0
  333. package/wiki/shared/index.md +53 -0
  334. package/wiki/shared/owning-your-narratives.md +161 -0
  335. package/wiki/shared/premature-ontologies.md +435 -0
  336. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  337. package/wiki/shared/sequestering-the-narrative.md +143 -0
  338. package/wiki/shared/why-wikis-work.md +216 -0
  339. package/.travis.yml +0 -7
  340. package/lib/brane.cjs.js +0 -12
  341. package/lib/brane.cjs.js.map +0 -1
  342. package/lib/brane.d.ts +0 -11
  343. package/lib/brane.js +0 -6
  344. package/lib/brane.js.map +0 -1
  345. package/lib/tests.d.ts +0 -1
  346. package/lib/tests.js +0 -8
  347. package/lib/tests.js.map +0 -1
  348. package/rollup.config.js +0 -21
  349. package/scripts/test.sh +0 -8
  350. package/src/brane.ts +0 -21
  351. package/src/tests.ts +0 -8
  352. package/tsconfig.json +0 -16
@@ -0,0 +1,423 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import Database from "better-sqlite3";
7
+ import { createTestDb } from "./helpers.js";
8
+ import { discoverGooseSession, readGooseUsage, discoverCrushSession, readCrushUsage, readClineSessionUsage, readCopilotUsage, getAgentAdapter, } from "../agents.js";
9
+ import { maybeCaptureSessionId, getSessionIds } from "../daemon/server.js";
10
+ import { priceMessage } from "../cost/pricing.js";
11
+ const scratch = () => mkdtempSync(join(tmpdir(), "brane-harness-cost-"));
12
+ // ---------------------------------------------------------------------------
13
+ // Pricing
14
+ // ---------------------------------------------------------------------------
15
+ describe("Fireworks K3 pricing", () => {
16
+ const oneMEach = {
17
+ input: 1e6,
18
+ output: 1e6,
19
+ cacheRead: 0,
20
+ cacheWrite5m: 0,
21
+ cacheWrite1h: 0,
22
+ };
23
+ it("resolves the fast router to the fast tier, not the base tier", () => {
24
+ // Regression guard for the table's insertion order: priceForModel returns
25
+ // the FIRST substring hit, and "accounts/fireworks/routers/kimi-k3-fast"
26
+ // contains "kimi-k3" too. Listed the wrong way round this silently prices
27
+ // the fast router a third cheap, with nothing to notice it by.
28
+ assert.equal(priceMessage("accounts/fireworks/routers/kimi-k3-fast", oneMEach), 4.5 + 22.5);
29
+ assert.equal(priceMessage("accounts/fireworks/models/kimi-k3", oneMEach), 3 + 15);
30
+ });
31
+ it("prices cached input at Fireworks' 0.1x rate", () => {
32
+ const cacheOnly = {
33
+ input: 0,
34
+ output: 0,
35
+ cacheRead: 1e6,
36
+ cacheWrite5m: 0,
37
+ cacheWrite1h: 0,
38
+ };
39
+ // $0.30/M against a $3.00/M input price.
40
+ assert.equal(priceMessage("accounts/fireworks/models/kimi-k3", cacheOnly), 0.3);
41
+ });
42
+ });
43
+ // ---------------------------------------------------------------------------
44
+ // goose — one shared SQLite store, a usage ledger brane has to price itself
45
+ // ---------------------------------------------------------------------------
46
+ function gooseFixture() {
47
+ const path = join(scratch(), "sessions.db");
48
+ const db = new Database(path);
49
+ db.exec(`
50
+ CREATE TABLE sessions (
51
+ id TEXT PRIMARY KEY, name TEXT NOT NULL DEFAULT '', working_dir TEXT NOT NULL,
52
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
53
+ );
54
+ CREATE TABLE usage_ledger (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL,
56
+ created_timestamp INTEGER NOT NULL, model TEXT,
57
+ input_tokens INTEGER, output_tokens INTEGER, total_tokens INTEGER,
58
+ cache_read_tokens INTEGER, cache_write_tokens INTEGER,
59
+ cost REAL, cost_source TEXT, is_compaction INTEGER DEFAULT 0
60
+ );
61
+ `);
62
+ const sess = db.prepare("INSERT INTO sessions (id, name, working_dir, created_at) VALUES (?,?,?,?)");
63
+ sess.run("20260816_1", "goose-alpha", "/work/fold", "2026-08-16 15:00:00");
64
+ sess.run("20260816_2", "goose-beta", "/work/fold", "2026-08-16 15:25:32");
65
+ // A second row sharing goose-alpha's name, created later — `name` carries no
66
+ // UNIQUE constraint, so newest must win.
67
+ sess.run("20260817_1", "goose-alpha", "/work/fold", "2026-08-17 09:00:00");
68
+ const led = db.prepare(`INSERT INTO usage_ledger
69
+ (session_id, created_timestamp, model, input_tokens, output_tokens,
70
+ cache_read_tokens, cache_write_tokens, cost, cost_source, is_compaction)
71
+ VALUES (?,?,?,?,?,?,?,NULL,NULL,?)`);
72
+ // input_tokens is INCLUSIVE of cache_read_tokens, as goose records it.
73
+ led.run("20260816_2", 1, "accounts/fireworks/models/kimi-k3", 100_000, 1_000, 90_000, 0, 0);
74
+ led.run("20260816_2", 2, "accounts/fireworks/models/kimi-k3", 50_000, 500, 40_000, 0, 1); // compaction
75
+ led.run("20260816_2", 3, "accounts/fireworks/routers/kimi-k3-fast", 10_000, 200, 0, 0, 0);
76
+ db.close();
77
+ return path;
78
+ }
79
+ describe("goose session discovery", () => {
80
+ it("looks the session up by the narrative id brane launched it under", () => {
81
+ const path = gooseFixture();
82
+ assert.equal(discoverGooseSession(path, "goose-beta"), "20260816_2");
83
+ });
84
+ it("takes the newest row when a name somehow repeats", () => {
85
+ const path = gooseFixture();
86
+ assert.equal(discoverGooseSession(path, "goose-alpha"), "20260817_1");
87
+ });
88
+ it("returns undefined for an unknown narrative or an unreadable store", () => {
89
+ const path = gooseFixture();
90
+ assert.equal(discoverGooseSession(path, "goose-nobody"), undefined);
91
+ assert.equal(discoverGooseSession(join(scratch(), "absent.db"), "goose-beta"), undefined);
92
+ });
93
+ });
94
+ describe("goose usage reading", () => {
95
+ it("prices the ledger, subtracting cache reads out of the inclusive input", () => {
96
+ const reading = readGooseUsage(gooseFixture(), "20260816_2");
97
+ // Rows 1+2 are base-tier kimi-k3, row 3 is the fast router — priced
98
+ // separately because the reader groups by model.
99
+ const base = priceMessage("accounts/fireworks/models/kimi-k3", {
100
+ input: 100_000 - 90_000 + (50_000 - 40_000),
101
+ output: 1_500,
102
+ cacheRead: 130_000,
103
+ cacheWrite5m: 0,
104
+ cacheWrite1h: 0,
105
+ });
106
+ const fast = priceMessage("accounts/fireworks/routers/kimi-k3-fast", {
107
+ input: 10_000,
108
+ output: 200,
109
+ cacheRead: 0,
110
+ cacheWrite5m: 0,
111
+ cacheWrite1h: 0,
112
+ });
113
+ assert.equal(reading.amount, Math.round((base + fast) * 1e4) / 1e4);
114
+ assert.equal(reading.unit, "USD");
115
+ });
116
+ it("counts compaction rows — they are real spend", () => {
117
+ // Row 2 is the only is_compaction row; dropping it would cost this much.
118
+ const reading = readGooseUsage(gooseFixture(), "20260816_2");
119
+ const withoutCompaction = priceMessage("accounts/fireworks/models/kimi-k3", {
120
+ input: 10_000,
121
+ output: 500,
122
+ cacheRead: 40_000,
123
+ cacheWrite5m: 0,
124
+ cacheWrite1h: 0,
125
+ });
126
+ assert.ok(withoutCompaction > 0);
127
+ assert.ok(reading.amount > withoutCompaction);
128
+ });
129
+ it("reports goose's own inclusive input count in the token breakdown", () => {
130
+ const reading = readGooseUsage(gooseFixture(), "20260816_2");
131
+ assert.deepEqual(reading.tokens, {
132
+ input: 160_000,
133
+ output: 1_700,
134
+ cacheRead: 130_000,
135
+ cacheWrite: 0,
136
+ });
137
+ });
138
+ it("returns null for an unknown session and a missing database", () => {
139
+ const path = gooseFixture();
140
+ assert.equal(readGooseUsage(path, "no-such-session"), null);
141
+ assert.equal(readGooseUsage(join(scratch(), "absent.db"), "20260816_2"), null);
142
+ });
143
+ });
144
+ // ---------------------------------------------------------------------------
145
+ // copilot — one local SQLite store with nano-AIU billing
146
+ // ---------------------------------------------------------------------------
147
+ function copilotFixture() {
148
+ const path = join(scratch(), "session-store.db");
149
+ const db = new Database(path);
150
+ db.exec(`
151
+ CREATE TABLE assistant_usage_events (
152
+ session_id TEXT NOT NULL, input_tokens INTEGER, output_tokens INTEGER,
153
+ cache_read_tokens INTEGER, cache_write_tokens INTEGER, total_nano_aiu INTEGER
154
+ );
155
+ `);
156
+ const usage = db.prepare("INSERT INTO assistant_usage_events VALUES (?, ?, ?, ?, ?, ?)");
157
+ usage.run("copilot-1", 100, 20, 80, 5, 1_250_000_000);
158
+ usage.run("copilot-1", 200, 30, 150, 10, 2_500_000_000);
159
+ db.close();
160
+ return path;
161
+ }
162
+ describe("copilot usage", () => {
163
+ it("converts nano AIU to AIC and retains Copilot's token breakdown", () => {
164
+ assert.deepEqual(readCopilotUsage(copilotFixture(), "copilot-1"), {
165
+ amount: 3.75,
166
+ unit: "AIC",
167
+ tokens: { input: 300, output: 50, cacheRead: 230, cacheWrite: 15 },
168
+ });
169
+ });
170
+ });
171
+ // ---------------------------------------------------------------------------
172
+ // crush — per-fold store, cost pre-computed by the harness, second-precision
173
+ // ---------------------------------------------------------------------------
174
+ const SPAWN_MS = 1786899518031; // narrative created_at, milliseconds
175
+ const SPAWN_S = 1786899518;
176
+ function crushFixture(createdAt = (s) => s) {
177
+ const path = join(scratch(), "crush.db");
178
+ const db = new Database(path);
179
+ db.exec(`
180
+ CREATE TABLE sessions (
181
+ id TEXT PRIMARY KEY, parent_session_id TEXT, title TEXT NOT NULL,
182
+ message_count INTEGER NOT NULL DEFAULT 0,
183
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
184
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
185
+ cost REAL NOT NULL DEFAULT 0.0,
186
+ updated_at INTEGER NOT NULL, created_at INTEGER NOT NULL
187
+ );
188
+ `);
189
+ const ins = db.prepare("INSERT INTO sessions (id, parent_session_id, title, prompt_tokens, completion_tokens, cost, updated_at, created_at) VALUES (?,?,?,?,?,?,?,?)");
190
+ const at = (s) => createdAt(s);
191
+ // Predates the spawn — a previous narrative sharing this fold.
192
+ ins.run("older-session", null, "old", 10, 10, 9.99, at(SPAWN_S - 500), at(SPAWN_S - 500));
193
+ // Ours.
194
+ ins.run("real-session", null, "ours", 67422, 118, 1.25, at(SPAWN_S + 2), at(SPAWN_S + 2));
195
+ // Synthetic children of ours: an agent-tool session and a title generator.
196
+ ins.run("abc$$agentic_fetch_68", "real-session", "sub", 19960, 1335, 0.5, at(SPAWN_S + 9), at(SPAWN_S + 9));
197
+ ins.run("title-real-session", null, "titler", 100, 20, 0.25, at(SPAWN_S + 3), at(SPAWN_S + 3));
198
+ // A later unrelated session in the same fold.
199
+ ins.run("newer-session", null, "later", 5, 5, 7.5, at(SPAWN_S + 9999), at(SPAWN_S + 9999));
200
+ db.close();
201
+ return path;
202
+ }
203
+ describe("crush session discovery", () => {
204
+ it("normalizes a millisecond `since` against crush's second-precision column", () => {
205
+ // The trap this exists for: crush's schema annotates created_at as
206
+ // milliseconds but writes seconds, so comparing the narrative's ms spawn
207
+ // time directly matches nothing at all — forever, and silently.
208
+ assert.equal(discoverCrushSession(crushFixture(), SPAWN_MS), "real-session");
209
+ });
210
+ it("works too if crush ever starts honouring its own millisecond comment", () => {
211
+ assert.equal(discoverCrushSession(crushFixture((s) => s * 1000), SPAWN_MS), "real-session");
212
+ });
213
+ it("takes the earliest session after the spawn, not the newest in the fold", () => {
214
+ assert.equal(discoverCrushSession(crushFixture(), SPAWN_MS), "real-session");
215
+ });
216
+ it("skips crush's synthetic title and agent-tool session rows", () => {
217
+ // With `since` dropped, the earliest real row wins and neither the
218
+ // `title-` nor the `$$` row is ever a candidate.
219
+ assert.equal(discoverCrushSession(crushFixture()), "older-session");
220
+ });
221
+ it("returns undefined when nothing started after the spawn, or the db is absent", () => {
222
+ assert.equal(discoverCrushSession(crushFixture(), (SPAWN_S + 99999) * 1000), undefined);
223
+ assert.equal(discoverCrushSession(join(scratch(), "absent.db"), SPAWN_MS), undefined);
224
+ });
225
+ });
226
+ describe("crush usage reading", () => {
227
+ it("sums the session's own cost with its agent-tool and title children", () => {
228
+ const reading = readCrushUsage(crushFixture(), "real-session");
229
+ assert.equal(reading.amount, 1.25 + 0.5 + 0.25);
230
+ assert.equal(reading.unit, "USD");
231
+ });
232
+ it("omits tokens — crush's columns are last-request snapshots, not totals", () => {
233
+ // prompt_tokens/completion_tokens are overwritten per request to gauge
234
+ // context size, so surfacing them as a session total would be a lie.
235
+ assert.equal(readCrushUsage(crushFixture(), "real-session").tokens, undefined);
236
+ });
237
+ it("reports a genuine $0 rather than null when the model is unpriced", () => {
238
+ // Crush defaults a custom provider's models to zero cost, so $0 means
239
+ // "config declares no rates", which is different from "unreadable".
240
+ const path = join(scratch(), "crush.db");
241
+ const db = new Database(path);
242
+ db.exec("CREATE TABLE sessions (id TEXT PRIMARY KEY, parent_session_id TEXT, cost REAL NOT NULL DEFAULT 0.0, created_at INTEGER NOT NULL)");
243
+ db.prepare("INSERT INTO sessions (id, parent_session_id, cost, created_at) VALUES ('s',NULL,0.0,1)").run();
244
+ db.close();
245
+ assert.deepEqual(readCrushUsage(path, "s"), { amount: 0, unit: "USD" });
246
+ });
247
+ it("returns null for an unknown session and a missing database", () => {
248
+ assert.equal(readCrushUsage(crushFixture(), "no-such-session"), null);
249
+ assert.equal(readCrushUsage(join(scratch(), "absent.db"), "real-session"), null);
250
+ });
251
+ });
252
+ // ---------------------------------------------------------------------------
253
+ // cline — per-narrative store, cost pre-computed per message
254
+ // ---------------------------------------------------------------------------
255
+ function clineLog(dir, sessionId, messages) {
256
+ const sdir = join(dir, sessionId);
257
+ mkdirSync(sdir, { recursive: true });
258
+ const file = join(sdir, `${sessionId}.messages.json`);
259
+ writeFileSync(file, JSON.stringify({ sessionId, messages }));
260
+ return file;
261
+ }
262
+ const msg = (id, cost, input = 100, output = 10, cacheRead = 0) => ({
263
+ id,
264
+ role: "assistant",
265
+ metrics: { cost, inputTokens: input, outputTokens: output, cacheReadTokens: cacheRead, cacheWriteTokens: 0 },
266
+ });
267
+ describe("cline usage reading", () => {
268
+ it("sums per-message cost and the full token breakdown", () => {
269
+ const file = clineLog(scratch(), "1786899559349_f9dpo", [
270
+ { id: "u1", role: "user" }, // no metrics — skipped
271
+ msg("m1", 0.25, 1000, 30, 900),
272
+ msg("m2", 0.5, 2000, 40, 1800),
273
+ ]);
274
+ assert.deepEqual(readClineSessionUsage(file), {
275
+ amount: 0.75,
276
+ unit: "USD",
277
+ tokens: { input: 3000, output: 70, cacheRead: 2700, cacheWrite: 0 },
278
+ });
279
+ });
280
+ it("counts an errored turn's cost, which metadata.totalCost would drop", () => {
281
+ // Cline accumulates metadata.totalCost only on the turn-success path, so a
282
+ // session with a failed turn persists that message's metrics but not its
283
+ // cost. Summing the messages is what makes the badge whole.
284
+ const file = clineLog(scratch(), "sess-err", [msg("ok", 0.25), msg("failed", 0.28)]);
285
+ assert.equal(readClineSessionUsage(file).amount, 0.53);
286
+ });
287
+ it("dedupes a message id repeated within one file", () => {
288
+ const file = clineLog(scratch(), "sess-dup", [msg("m1", 0.25), msg("m1", 0.25), msg("m2", 0.5)]);
289
+ assert.equal(readClineSessionUsage(file).amount, 0.75);
290
+ });
291
+ it("returns null for a missing or malformed log", () => {
292
+ const dir = scratch();
293
+ assert.equal(readClineSessionUsage(join(dir, "gone.messages.json")), null);
294
+ const bad = join(dir, "bad.messages.json");
295
+ writeFileSync(bad, "{not json");
296
+ assert.equal(readClineSessionUsage(bad), null);
297
+ });
298
+ });
299
+ describe("cline adapter over its private per-narrative store", () => {
300
+ // The store lives at a fixed /tmp/brane-cline-<nid> path (brane writes it at
301
+ // spawn), so these use a throwaway narrative id under the same root.
302
+ const nid = `cline-test-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
303
+ const sessionsDir = join("/tmp", `brane-cline-${nid}`, "data", "sessions");
304
+ const cline = getAgentAdapter("cline");
305
+ it("discovers the newest session in the narrative's own directory", () => {
306
+ clineLog(sessionsDir, "1786898522696_gg36a", []);
307
+ clineLog(sessionsDir, "1786899559349_f9dpo", [msg("m1", 2.0)]);
308
+ assert.equal(cline.discoverSessionId("/work/fold", 0, nid), "1786899559349_f9dpo");
309
+ });
310
+ it("sums sessions present on disk that brane never recorded", () => {
311
+ // Every dir under the narrative's private store is that narrative's spend.
312
+ // The five dead 401-revive sessions observed live are exactly this case:
313
+ // real launches whose ids the daemon never captured.
314
+ clineLog(sessionsDir, "1786898667854_rf6n9", [msg("m2", 0.5)]);
315
+ const reading = cline.readUsageMany({ sessionIds: [], foldPath: "/work/fold", narrativeId: nid });
316
+ assert.equal(reading.amount, 2.5);
317
+ });
318
+ it("excludes sessions that predate `since`, so a revive can't re-adopt the incumbent", () => {
319
+ // Cline ids carry their own start time (`<epochMs>_<rand5>`). Without this
320
+ // filter, discovery in the window between a revive and the new session dir
321
+ // appearing returns the previous session — which then reads as the
322
+ // successor and pins the pointer to a session that stopped spending.
323
+ const afterEverything = 1786899559350;
324
+ assert.equal(cline.discoverSessionId("/work/fold", afterEverything, nid), undefined);
325
+ assert.equal(cline.discoverSessionId("/work/fold", 1786899559349, nid), "1786899559349_f9dpo");
326
+ });
327
+ it("still honours recorded ids, and needs a narrative id to read anything", () => {
328
+ const both = cline.readUsageMany({
329
+ sessionIds: ["1786899559349_f9dpo"],
330
+ foldPath: "/work/fold",
331
+ narrativeId: nid,
332
+ });
333
+ assert.equal(both.amount, 2.5); // union, not double-counted
334
+ assert.equal(cline.readUsageMany({ sessionIds: [], foldPath: "/work/fold" }), null);
335
+ assert.equal(cline.readUsage({ sessionId: "x", foldPath: "/work/fold" }), null);
336
+ });
337
+ });
338
+ // ---------------------------------------------------------------------------
339
+ // Revive re-capture
340
+ // ---------------------------------------------------------------------------
341
+ describe("maybeCaptureSessionId across revives", () => {
342
+ function seed(id, agent, sessionId) {
343
+ const db = createTestDb();
344
+ db.prepare(`INSERT INTO narratives (id, brane_id, agent_type, role, created_at, status, fold_path, agent_session_id)
345
+ VALUES (?, 'brane-test', ?, 'worker', 1000, 'active', '~/work', ?)`).run(id, agent, sessionId);
346
+ return db;
347
+ }
348
+ const restart = (db, id, seq, time) => db
349
+ .prepare("INSERT INTO events (narrative_id, seq, time, type, content) VALUES (?,?,?,'restarted','r')")
350
+ .run(id, seq, time);
351
+ it("captures on the first bind, anchored at the narrative's spawn time", () => {
352
+ const db = seed("crush-a", "crush", null);
353
+ const seen = [];
354
+ const got = maybeCaptureSessionId(db, "crush-a", "crush", "~/work", 1000, null, (_fp, since) => {
355
+ seen.push(since);
356
+ return "sess-1";
357
+ });
358
+ assert.equal(got, "sess-1");
359
+ assert.deepEqual(seen, [1000]);
360
+ assert.deepEqual(getSessionIds(db, "crush-a"), ["sess-1"]);
361
+ });
362
+ it("leaves a captured pointer alone while the narrative has never been revived", () => {
363
+ const db = seed("crush-b", "crush", "sess-1");
364
+ let called = false;
365
+ const got = maybeCaptureSessionId(db, "crush-b", "crush", "~/work", 1000, "sess-1", () => {
366
+ called = true;
367
+ return "sess-2";
368
+ });
369
+ assert.equal(got, undefined);
370
+ assert.equal(called, false, "discovery must not even run for a settled pointer");
371
+ });
372
+ it("re-anchors at the last revive and adopts the session that launch minted", () => {
373
+ // Crush has no resume and cline writes a fresh session dir per start, so
374
+ // without this every post-revive dollar goes unread against a pointer that
375
+ // names a session which stopped spending.
376
+ const db = seed("crush-c", "crush", "sess-1");
377
+ restart(db, "crush-c", 1, 5000);
378
+ restart(db, "crush-c", 2, 9000);
379
+ const seen = [];
380
+ const got = maybeCaptureSessionId(db, "crush-c", "crush", "~/work", 1000, "sess-1", (_fp, since) => {
381
+ seen.push(since);
382
+ return "sess-3";
383
+ });
384
+ assert.equal(got, "sess-3");
385
+ assert.deepEqual(seen, [9000], "anchored at the latest revive, not the spawn");
386
+ // Both sessions are retained, so cost spans the whole narrative.
387
+ assert.deepEqual(getSessionIds(db, "crush-c"), ["sess-1", "sess-3"]);
388
+ });
389
+ it("is idempotent when the agent resumed its session in place", () => {
390
+ // Goose re-attaches to the row named after the narrative, so discovery
391
+ // returns the same id and nothing is written.
392
+ const db = seed("goose-d", "goose", "20260816_2");
393
+ restart(db, "goose-d", 1, 5000);
394
+ const got = maybeCaptureSessionId(db, "goose-d", "goose", "~/work", 1000, "20260816_2", () => "20260816_2");
395
+ assert.equal(got, undefined);
396
+ assert.deepEqual(getSessionIds(db, "goose-d"), ["20260816_2"]);
397
+ });
398
+ it("does not let the discovery cache pin a stale pointer after a revive", () => {
399
+ // Exercises the real (uninjected) discovery path, so the production cache
400
+ // is live — the injected-`discover` tests above bypass it by design.
401
+ // Sequence: revive, then a tick landing before the new session dir exists,
402
+ // then the dir appears. The first tick must not cache the incumbent
403
+ // against the new anchor, or the successor is never adopted.
404
+ const nid = `cline-cache-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
405
+ const dir = join("/tmp", `brane-cline-${nid}`, "data", "sessions");
406
+ const before = "1000000000000_aaaaa";
407
+ const after = "1000000009000_bbbbb";
408
+ clineLog(dir, before, [msg("m1", 1)]);
409
+ const db = seed(nid, "cline", before);
410
+ restart(db, nid, 1, 1000000005000);
411
+ // Tick one: only the pre-revive session exists.
412
+ assert.equal(maybeCaptureSessionId(db, nid, "cline", "~/work", 1, before), undefined, "the incumbent is not re-adopted as its own successor");
413
+ // Tick two: cline has now written the session this revive minted.
414
+ clineLog(dir, after, [msg("m2", 2)]);
415
+ assert.equal(maybeCaptureSessionId(db, nid, "cline", "~/work", 1, before), after);
416
+ assert.deepEqual(getSessionIds(db, nid), [before, after]);
417
+ });
418
+ it("ignores agents whose session id the daemon assigns or does not own", () => {
419
+ const db = seed("claude-e", "claude", null);
420
+ assert.equal(maybeCaptureSessionId(db, "claude-e", "claude", "~/work", 1000, null, () => "nope"), undefined);
421
+ });
422
+ });
423
+ //# sourceMappingURL=harness-cost.test.js.map
@@ -0,0 +1,21 @@
1
+ import Database from "better-sqlite3";
2
+ import { SCHEMA_SQL, applyMigrations } from "../db/schema.js";
3
+ /**
4
+ * Create a fresh in-memory database with the brane schema applied.
5
+ * Each test gets its own isolated DB instance.
6
+ *
7
+ * Built the same way `openDatabase()` builds the real one — `SCHEMA_SQL` then
8
+ * the migration ladder — so a column added by migration (cost_amount, archived,
9
+ * …) is present here too, and tests can't pass against a shape production never
10
+ * has.
11
+ */
12
+ export function createTestDb() {
13
+ const db = new Database(":memory:");
14
+ db.pragma("journal_mode = WAL");
15
+ db.pragma("foreign_keys = ON");
16
+ db.exec(SCHEMA_SQL);
17
+ applyMigrations(db);
18
+ db.prepare("INSERT INTO meta (key, value) VALUES ('brane_id', 'brane-test')").run();
19
+ return db;
20
+ }
21
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1,68 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { hexColumnOf, hexDumpRows, hexRows } from "../shared/hex-dump.js";
4
+ import { hexSegments, pageOf, pagesOfHit, rowSpans } from "../shared/hex-pages.js";
5
+ // The hex view draws whichever pages are loaded, in place, and 🔎 paints a
6
+ // hit by row and column. Each rule here is one a highlight or a gap row would
7
+ // be visibly wrong without.
8
+ describe("hexSegments", () => {
9
+ it("is one page for a file that fits in one", () => {
10
+ assert.deepEqual(hexSegments([{ offset: 0, length: 100, eof: true }], 100), [{ kind: "page", offset: 0 }]);
11
+ });
12
+ it("puts a gap between pages that don't touch, and after the last until the end", () => {
13
+ const segs = hexSegments([{ offset: 32, length: 16, eof: false }, { offset: 0, length: 16, eof: false }], 100);
14
+ assert.deepEqual(segs, [
15
+ { kind: "page", offset: 0 },
16
+ { kind: "gap", from: 16, to: 32 },
17
+ { kind: "page", offset: 32 },
18
+ { kind: "gap", from: 48, to: 100 },
19
+ ]);
20
+ });
21
+ it("joins adjacent pages with no gap", () => {
22
+ const segs = hexSegments([{ offset: 0, length: 16, eof: false }, { offset: 16, length: 4, eof: true }], 20);
23
+ assert.deepEqual(segs, [{ kind: "page", offset: 0 }, { kind: "page", offset: 16 }]);
24
+ });
25
+ it("leaves the end open while the size is unknown, and closes it at eof", () => {
26
+ assert.deepEqual(hexSegments([{ offset: 0, length: 16, eof: false }], null).at(-1), { kind: "gap", from: 16, to: null });
27
+ assert.deepEqual(hexSegments([{ offset: 0, length: 16, eof: false }, { offset: 48, length: 3, eof: true }], null).at(-1), { kind: "page", offset: 48 });
28
+ });
29
+ it("opens with a gap when the first page isn't loaded", () => {
30
+ assert.deepEqual(hexSegments([{ offset: 64, length: 16, eof: false }], 200)[0], { kind: "gap", from: 0, to: 64 });
31
+ });
32
+ });
33
+ describe("page and row arithmetic", () => {
34
+ it("places a byte on its page, and a hit on every page it touches", () => {
35
+ assert.equal(pageOf(0, 16), 0);
36
+ assert.equal(pageOf(31, 16), 16);
37
+ assert.deepEqual(pagesOfHit(14, 4, 16), [0, 16]);
38
+ assert.deepEqual(pagesOfHit(16, 16, 16), [16]);
39
+ assert.deepEqual(pagesOfHit(5, 0, 16), [0]);
40
+ });
41
+ it("splits a hit that wraps a row into one span per row", () => {
42
+ assert.deepEqual(rowSpans(14, 4, 16), [{ row: 0, first: 14, last: 15 }, { row: 16, first: 0, last: 1 }]);
43
+ assert.deepEqual(rowSpans(3, 2, 8), [{ row: 0, first: 3, last: 4 }]);
44
+ assert.deepEqual(rowSpans(6, 20, 8), [
45
+ { row: 0, first: 6, last: 7 },
46
+ { row: 8, first: 0, last: 7 },
47
+ { row: 16, first: 0, last: 7 },
48
+ { row: 24, first: 0, last: 1 },
49
+ ]);
50
+ });
51
+ it("finds every byte's digits where hexColumnOf says, at both widths", () => {
52
+ const bytes = Uint8Array.from({ length: 16 }, (_, i) => 0xa0 + i);
53
+ for (const rowBytes of [16, 8]) {
54
+ const [row] = hexRows(bytes, 0, rowBytes);
55
+ for (let j = 0; j < rowBytes; j++) {
56
+ const at = hexColumnOf(j, rowBytes);
57
+ assert.equal(row.hex.slice(at, at + 2), (0xa0 + j).toString(16), `byte ${j} at ${rowBytes}/row`);
58
+ }
59
+ assert.equal(row.ascii.length, rowBytes);
60
+ }
61
+ });
62
+ it("composes to the same row hexDumpRows always drew", () => {
63
+ const bytes = Buffer.from("Hello, world!\n\x00\x01ABC");
64
+ const composed = hexRows(bytes, 0x20).map((r) => `${r.offset} ${r.hex} ${r.ascii}`);
65
+ assert.deepEqual(composed, hexDumpRows(bytes, 0x20));
66
+ });
67
+ });
68
+ //# sourceMappingURL=hex-pages.test.js.map
@@ -0,0 +1,69 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { detectLanguage, resolveLang } from "../highlight.js";
4
+ describe("detectLanguage", () => {
5
+ it("maps extensions that differ from the shiki language id", () => {
6
+ assert.equal(detectLanguage("src/daemon/server.ts"), "typescript");
7
+ assert.equal(detectLanguage("main.rs"), "rust");
8
+ assert.equal(detectLanguage("script.py"), "python");
9
+ });
10
+ it("falls back to the bare extension when shiki knows it as a language", () => {
11
+ // The bug this covers: `.lean` was absent from EXT_LANG, so a Lean file
12
+ // rendered as plain monochrome text even though shiki bundles the grammar.
13
+ assert.equal(detectLanguage("Mathlib/Order/Basic.lean"), "lean");
14
+ assert.equal(detectLanguage("build.zig"), "zig");
15
+ assert.equal(detectLanguage("paper.tex"), "tex");
16
+ });
17
+ it("prefers the explicit map over the extension fallback", () => {
18
+ // Shiki bundles no SVG grammar at all, so `.svg` has to name xml itself —
19
+ // the bare-extension fallback would find nothing. Same reason ```svg went
20
+ // unhighlighted in chat until resolveLang existed.
21
+ assert.equal(detectLanguage("logo.svg"), "xml");
22
+ });
23
+ it("returns null for markdown, which the client renders itself", () => {
24
+ assert.equal(detectLanguage("README.md"), null);
25
+ assert.equal(detectLanguage("notes.mdx"), null);
26
+ });
27
+ it("returns null for unknown and extensionless files", () => {
28
+ assert.equal(detectLanguage("data.unknownext"), null);
29
+ assert.equal(detectLanguage("LICENSE"), null);
30
+ });
31
+ it("matches basename rules, including suffixed variants", () => {
32
+ assert.equal(detectLanguage("Dockerfile"), "docker");
33
+ assert.equal(detectLanguage("deploy/Dockerfile.dev"), "docker");
34
+ assert.equal(detectLanguage("Makefile"), "make");
35
+ });
36
+ });
37
+ describe("resolveLang", () => {
38
+ it("passes through the ids and aliases shiki already knows", () => {
39
+ assert.equal(resolveLang("typescript"), "typescript");
40
+ assert.equal(resolveLang("js"), "js");
41
+ assert.equal(resolveLang("yml"), "yml");
42
+ });
43
+ it("resolves a tag shiki lacks by reading it as a file extension", () => {
44
+ // The bug: ```svg rendered as plain text in chat while the identical file
45
+ // highlighted in the Files tab, because only that path consulted EXT_LANG.
46
+ assert.equal(resolveLang("svg"), "xml");
47
+ });
48
+ it("covers the few tags that are neither grammars nor extensions", () => {
49
+ assert.equal(resolveLang("golang"), "go");
50
+ assert.equal(resolveLang("patch"), "diff");
51
+ assert.equal(resolveLang("plist"), "xml");
52
+ });
53
+ it("normalizes case and stray whitespace", () => {
54
+ assert.equal(resolveLang("SVG"), "xml");
55
+ assert.equal(resolveLang(" TypeScript "), "typescript");
56
+ });
57
+ it("returns null for nothing, blanks, and words no grammar answers to", () => {
58
+ assert.equal(resolveLang(null), null);
59
+ assert.equal(resolveLang(undefined), null);
60
+ assert.equal(resolveLang(" "), null);
61
+ assert.equal(resolveLang("nonsenselang"), null);
62
+ });
63
+ it("does not let markdown's client-side rendering leak into fences", () => {
64
+ // detectLanguage returns null for .md so FilesView renders it itself, but
65
+ // ```md in a chat bubble is a code listing and shiki has the grammar.
66
+ assert.equal(resolveLang("md"), "md");
67
+ });
68
+ });
69
+ //# sourceMappingURL=highlight.test.js.map
@@ -0,0 +1,71 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { BUMP_VELOCITY, RUBBER_BAND_LIMIT, clampIndex, compactCount, releaseVelocity, rubberBand, springAtRest, springStep, throwTarget, } from "../shared/hit-drum.js";
4
+ describe("hit drum", () => {
5
+ it("rests on the nearest hit, never past either end", () => {
6
+ assert.equal(clampIndex(2.4, 17), 2);
7
+ assert.equal(clampIndex(2.6, 17), 3);
8
+ assert.equal(clampIndex(-3, 17), 0);
9
+ assert.equal(clampIndex(40, 17), 16);
10
+ assert.equal(clampIndex(3, 0), -1);
11
+ });
12
+ it("resists more the further past an end it is pulled", () => {
13
+ assert.equal(rubberBand(5.5, 17), 5.5);
14
+ const a = 16 - rubberBand(16 - 0, 17);
15
+ assert.equal(a, 0);
16
+ const one = rubberBand(17, 17) - 16;
17
+ const four = rubberBand(20, 17) - 16;
18
+ assert.ok(one > 0 && one < 1, `one hit of pull stretches less than a hit (${one})`);
19
+ assert.ok(four > one && four < RUBBER_BAND_LIMIT, `and never reaches the limit (${four})`);
20
+ assert.ok(rubberBand(-1, 17) < 0 && rubberBand(-1, 17) > -RUBBER_BAND_LIMIT);
21
+ });
22
+ it("bumps a quarter-detent or so into the wall and settles back", () => {
23
+ let s = { x: 16, v: BUMP_VELOCITY };
24
+ let max = 16;
25
+ for (let i = 0; i < 120 && !springAtRest(s.x, s.v, 16); i++) {
26
+ s = springStep(s.x, s.v, 16, 1 / 60);
27
+ max = Math.max(max, s.x);
28
+ }
29
+ const bump = max - 16;
30
+ assert.ok(bump > 0.15 && bump < 0.5, `visible but small (${bump})`);
31
+ assert.ok(springAtRest(s.x, s.v, 16), "and comes to rest on the end");
32
+ });
33
+ it("lands on its target without overshooting", () => {
34
+ let s = { x: 0, v: 0 };
35
+ let max = 0;
36
+ let frames = 0;
37
+ while (!springAtRest(s.x, s.v, 1) && frames < 600) {
38
+ s = springStep(s.x, s.v, 1, 1 / 60);
39
+ max = Math.max(max, s.x);
40
+ frames++;
41
+ }
42
+ assert.ok(frames < 60, `settles within a second (took ${frames} frames)`);
43
+ assert.ok(max <= 1.002, `no overshoot (peaked at ${max})`);
44
+ });
45
+ it("carries a flick a few hits, stopping at the ends", () => {
46
+ assert.equal(throwTarget(3.2, 0, 17), 3);
47
+ assert.equal(throwTarget(3, 10, 17), 7);
48
+ assert.equal(throwTarget(3, -12, 17), 0);
49
+ assert.equal(throwTarget(3, 1000, 17), 16);
50
+ });
51
+ it("measures release velocity over the last moments only", () => {
52
+ const samples = [
53
+ { t: 0, y: 0 },
54
+ { t: 100, y: -100 },
55
+ { t: 150, y: -140 },
56
+ { t: 180, y: -160 },
57
+ ];
58
+ assert.ok(Math.abs(releaseVelocity(samples, 180) - -60 / 80) < 1e-9);
59
+ assert.equal(releaseVelocity(samples, 400), 0, "a finger that paused before lifting throws nothing");
60
+ });
61
+ it("shortens totals past 999, rounding down", () => {
62
+ assert.equal(compactCount(0), "0");
63
+ assert.equal(compactCount(999), "999");
64
+ assert.equal(compactCount(1000), "1k");
65
+ assert.equal(compactCount(1234), "1.2k");
66
+ assert.equal(compactCount(1999), "1.9k");
67
+ assert.equal(compactCount(5000), "5k");
68
+ assert.equal(compactCount(12345), "12k");
69
+ });
70
+ });
71
+ //# sourceMappingURL=hit-drum.test.js.map