gentle-pi 3.7.0 → 4.0.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.
Files changed (316) hide show
  1. package/README.md +37 -6
  2. package/assets/agents/gentle-ai-explore.md +4 -4
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -9
  5. package/assets/orchestrator-delegation.md +40 -41
  6. package/assets/orchestrator-memory.md +1 -22
  7. package/assets/orchestrator-skills.md +1 -1
  8. package/assets/orchestrator.md +12 -24
  9. package/assets/support/strict-tdd-verify.md +4 -266
  10. package/assets/support/strict-tdd.md +8 -360
  11. package/bin/gentle-shell.mjs +254 -29
  12. package/docs/delegated-verification.md +26 -1
  13. package/docs/gentle-agents-activity.md +24 -0
  14. package/docs/gentle-shell.md +92 -28
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +112 -206
  18. package/docs/telemetry.md +1 -1
  19. package/docs/yolo-mode.md +86 -0
  20. package/extensions/child-context.ts +26 -0
  21. package/extensions/child-safety.ts +23 -0
  22. package/extensions/gentle-agents.ts +429 -262
  23. package/extensions/gentle-ai.ts +781 -683
  24. package/extensions/gentle-shell.ts +1375 -88
  25. package/extensions/gentle-stats.ts +101 -0
  26. package/extensions/gentle-todo.ts +18 -7
  27. package/extensions/history/atomic-write.ts +38 -0
  28. package/extensions/history/hide-prompts.ts +183 -0
  29. package/extensions/history/index.ts +1419 -0
  30. package/extensions/history/load-shared-history.ts +39 -0
  31. package/extensions/history/selector-helpers.ts +538 -0
  32. package/extensions/history/session-scan.ts +233 -0
  33. package/extensions/history/store.ts +1119 -0
  34. package/extensions/nan-provider.ts +6 -0
  35. package/extensions/quiet-tools.ts +179 -87
  36. package/extensions/resume-hint.ts +60 -0
  37. package/extensions/skill-registry.ts +16 -12
  38. package/extensions/startup-banner.ts +60 -31
  39. package/lib/agent-assets.ts +604 -0
  40. package/lib/agent-profile-pin.ts +12 -0
  41. package/lib/agents-message-delivery.ts +181 -0
  42. package/lib/agents-protocol.ts +60 -0
  43. package/lib/agents-runner.ts +96 -98
  44. package/lib/agents-view.ts +26 -3
  45. package/lib/agents-widget.ts +16 -9
  46. package/lib/append-system-prompt.ts +21 -0
  47. package/lib/bounded-writer-admission.ts +147 -0
  48. package/lib/card-style-policy.ts +60 -0
  49. package/lib/child-context-files.ts +166 -0
  50. package/lib/codemode-renderer.ts +185 -0
  51. package/lib/command-palette-catalog.ts +3 -9
  52. package/lib/command-palette.ts +25 -14
  53. package/lib/destructive-command-guard.ts +144 -0
  54. package/lib/gentle-ai-elapsed-store.ts +87 -0
  55. package/lib/gentle-ai-renderer.ts +196 -39
  56. package/lib/gentle-shell-launcher.ts +24 -13
  57. package/lib/gentle-shell-resume-hint.ts +176 -0
  58. package/lib/history-capture-policy.ts +95 -0
  59. package/lib/model-routing-authority.ts +5 -1
  60. package/lib/nan-provider.ts +227 -0
  61. package/lib/native-review-cli.ts +49 -108
  62. package/lib/odd-phase-inference.ts +231 -0
  63. package/lib/odd-phase.ts +141 -0
  64. package/lib/overlay-repaint.ts +26 -0
  65. package/lib/pi-tui-keys.ts +53 -0
  66. package/lib/review-candidate-view-owner.ts +67 -17
  67. package/lib/review-candidate-view.ts +112 -25
  68. package/lib/review-reminder-receipt.ts +48 -8
  69. package/lib/review-risk-assessment.ts +156 -11
  70. package/lib/review-sidebar-state.ts +223 -0
  71. package/lib/selection-engine.ts +515 -0
  72. package/lib/session-messaging-grants.ts +135 -0
  73. package/lib/session-worktree-registry.ts +14 -2
  74. package/lib/shell-bar.ts +179 -24
  75. package/lib/shell-card.ts +287 -18
  76. package/lib/shell-changes-view.ts +2 -1
  77. package/lib/shell-prompt.ts +102 -6
  78. package/lib/shell-sidebar-layout.ts +78 -14
  79. package/lib/shell-sidebar.ts +15 -1
  80. package/lib/shell-todo.ts +23 -13
  81. package/lib/shell-usage-view.ts +9 -4
  82. package/lib/shell-usage.ts +66 -10
  83. package/lib/stats-collector.ts +381 -0
  84. package/lib/stats-view.ts +431 -0
  85. package/lib/theme-customization.ts +52 -0
  86. package/lib/vim-editor-adapter.ts +379 -0
  87. package/lib/vim-normal-engine.ts +154 -0
  88. package/lib/vim-operator-engine.ts +416 -0
  89. package/lib/vim-policy.ts +49 -0
  90. package/lib/vim-visual-engine.ts +107 -0
  91. package/lib/visual-customization-policy.ts +108 -0
  92. package/lib/visual-customize-view.ts +330 -0
  93. package/lib/visual-profiles.ts +228 -0
  94. package/lib/yolo-session-policy.ts +240 -0
  95. package/package.json +20 -8
  96. package/runtime/gentle-shell-launcher.mjs +23 -12
  97. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  98. package/runtime/native-review-cli.mjs +49 -108
  99. package/runtime/review-risk-assessment.mjs +154 -9
  100. package/scripts/build-runtime-modules.mjs +1 -0
  101. package/scripts/gentle-ai-installer.mjs +14 -13
  102. package/scripts/mirror-odd-routing.mjs +2 -2
  103. package/scripts/run-test-suite.mjs +76 -0
  104. package/scripts/test-packed-runner.mjs +31 -14
  105. package/scripts/verify-package-files.mjs +11 -22
  106. package/skills/branch-pr/SKILL.md +24 -52
  107. package/skills/chained-pr/SKILL.md +31 -15
  108. package/skills/chained-pr/references/chaining-details.md +31 -20
  109. package/skills/gentle-ai/SKILL.md +9 -15
  110. package/skills/issue-creation/SKILL.md +8 -2
  111. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  112. package/skills/work-unit-commits/SKILL.md +4 -3
  113. package/tests/agent-profiles.test.ts +18 -0
  114. package/tests/agents-fake-child.ts +2 -2
  115. package/tests/agents-message-delivery.test.ts +106 -0
  116. package/tests/agents-protocol.test.ts +40 -0
  117. package/tests/agents-runner.test.ts +378 -89
  118. package/tests/agents-view-thread-identity.test.ts +169 -0
  119. package/tests/agents-view.test.ts +8 -2
  120. package/tests/agents-widget.test.ts +154 -15
  121. package/tests/append-system-prompt-route.test.ts +160 -0
  122. package/tests/append-system-prompt.test.ts +46 -0
  123. package/tests/artifact-language.test.ts +19 -213
  124. package/tests/ask-user-question.test.ts +44 -1
  125. package/tests/asset-installation-runtime.test.ts +5 -16
  126. package/tests/autonomous-guard.test.ts +69 -1
  127. package/tests/bounded-writer-admission.test.ts +95 -0
  128. package/tests/branch-pr-skill.test.ts +43 -0
  129. package/tests/card-style-policy.test.ts +55 -0
  130. package/tests/chained-pr-skill.test.ts +124 -0
  131. package/tests/child-context-files.test.ts +255 -0
  132. package/tests/child-safety.test.ts +82 -0
  133. package/tests/codemode-rendering.test.ts +491 -0
  134. package/tests/command-palette.test.ts +39 -3
  135. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  136. package/tests/destructive-command-guard.test.ts +84 -0
  137. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  138. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  139. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  140. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  145. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  146. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  148. package/tests/generic-agent-tools.test.ts +54 -0
  149. package/tests/gentle-agents.test.ts +1047 -252
  150. package/tests/gentle-ai-binary.test.ts +3 -3
  151. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  152. package/tests/gentle-ai-installer.test.ts +68 -54
  153. package/tests/gentle-ai-renderer.test.ts +487 -8
  154. package/tests/gentle-ai.test.ts +288 -65
  155. package/tests/gentle-card-text.ts +2 -1
  156. package/tests/gentle-shell-bin.test.ts +651 -114
  157. package/tests/gentle-shell-launcher.test.ts +99 -52
  158. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  159. package/tests/gentle-shell.test.ts +3839 -187
  160. package/tests/gentle-stats.test.ts +152 -0
  161. package/tests/gentle-theme.test.ts +4 -1
  162. package/tests/gentle-todo.test.ts +80 -8
  163. package/tests/history-atomic-write.test.ts +57 -0
  164. package/tests/history-capture-policy.test.ts +102 -0
  165. package/tests/history-command-registration.test.ts +164 -0
  166. package/tests/history-dedupe-entries.test.ts +123 -0
  167. package/tests/history-delete-backfill.test.ts +190 -0
  168. package/tests/history-delete-confirm.test.ts +460 -0
  169. package/tests/history-dispatch.test.ts +180 -0
  170. package/tests/history-drain-hidden.test.ts +110 -0
  171. package/tests/history-drain-order.test.ts +98 -0
  172. package/tests/history-expanded-globals.test.ts +62 -0
  173. package/tests/history-gc.test.ts +832 -0
  174. package/tests/history-header-layout.test.ts +265 -0
  175. package/tests/history-hide-prompts.test.ts +275 -0
  176. package/tests/history-lazy-windowing.test.ts +508 -0
  177. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  178. package/tests/history-load-shared-history.test.ts +53 -0
  179. package/tests/history-max-results-cap.test.ts +76 -0
  180. package/tests/history-multi-reader.test.ts +203 -0
  181. package/tests/history-off-path.test.ts +170 -0
  182. package/tests/history-openflow-integration.test.ts +173 -0
  183. package/tests/history-overlay-margin.test.ts +326 -0
  184. package/tests/history-preview-layout.test.ts +93 -0
  185. package/tests/history-registry.test.ts +143 -0
  186. package/tests/history-scope-delete.test.ts +411 -0
  187. package/tests/history-search-caret-keys.test.ts +142 -0
  188. package/tests/history-seed-bootstrap.test.ts +170 -0
  189. package/tests/history-seed-regen.test.ts +129 -0
  190. package/tests/history-selector-windowing.test.ts +94 -0
  191. package/tests/history-session-scan-directory.test.ts +87 -0
  192. package/tests/history-session-scan-extract.test.ts +583 -0
  193. package/tests/history-session-writer.test.ts +351 -0
  194. package/tests/history-store-paths.test.ts +79 -0
  195. package/tests/history-tombstone-exact.test.ts +139 -0
  196. package/tests/history-wheel-mouse.test.ts +242 -0
  197. package/tests/inprocess-reviewer.test.ts +29 -19
  198. package/tests/issue-creation-skill.test.ts +61 -0
  199. package/tests/model-routing-authority.test.ts +16 -0
  200. package/tests/nan-provider.test.ts +471 -0
  201. package/tests/native-review-capability-contract.test.ts +7 -1
  202. package/tests/native-review-cli.test.ts +6 -120
  203. package/tests/native-review-parity-runtime.test.ts +100 -3
  204. package/tests/odd-integration.test.ts +67 -0
  205. package/tests/odd-phase-inference.test.ts +213 -0
  206. package/tests/odd-phase-loader.test.ts +253 -0
  207. package/tests/odd-phase.test.ts +307 -0
  208. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  209. package/tests/odd-routing-contract.test.ts +86 -35
  210. package/tests/orchestrator-budget.test.ts +14 -39
  211. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  212. package/tests/overlay-repaint.test.ts +74 -0
  213. package/tests/package-manifest.test.ts +252 -115
  214. package/tests/packed-runner-owned-path.test.ts +46 -0
  215. package/tests/persona-single-channel.test.ts +6 -6
  216. package/tests/provider-defect-handoff.test.ts +3 -11
  217. package/tests/quiet-bash-runtime.test.ts +76 -0
  218. package/tests/quiet-tool-rendering.test.ts +409 -184
  219. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  220. package/tests/rdd-status-line.test.ts +9 -4
  221. package/tests/resume-hint-extension.test.ts +122 -0
  222. package/tests/review-agent-end-preflight.test.ts +176 -12
  223. package/tests/review-candidate-owner-retry.test.ts +22 -1
  224. package/tests/review-candidate-view.test.ts +298 -0
  225. package/tests/review-contract-prompt.test.ts +108 -43
  226. package/tests/review-controller-lock-status.test.ts +0 -1
  227. package/tests/review-controller-native-routing.test.ts +611 -5
  228. package/tests/review-controller-workspace-root.test.ts +163 -4
  229. package/tests/review-host-relay-routing.test.ts +338 -2
  230. package/tests/review-integration-v2-forward.test.ts +200 -0
  231. package/tests/review-ledger-contract.test.ts +10 -34
  232. package/tests/review-reminder-receipt.test.ts +47 -1
  233. package/tests/review-risk-assessment.test.ts +498 -6
  234. package/tests/review-sidebar-state.test.ts +402 -0
  235. package/tests/run-test-suite.test.ts +124 -0
  236. package/tests/runtime-harness.mjs +145 -786
  237. package/tests/runtime-metrics-children.test.ts +16 -23
  238. package/tests/selection-engine.test.ts +421 -0
  239. package/tests/session-messaging-grants.test.ts +255 -0
  240. package/tests/session-worktree-registry.test.ts +77 -0
  241. package/tests/shell-bar.test.ts +382 -1
  242. package/tests/shell-card.test.ts +353 -1
  243. package/tests/shell-changes-view.test.ts +52 -0
  244. package/tests/shell-prompt.test.ts +94 -2
  245. package/tests/shell-sidebar-layout.test.ts +325 -21
  246. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  247. package/tests/shell-todo.test.ts +87 -1
  248. package/tests/shell-usage-view.test.ts +27 -0
  249. package/tests/shell-usage.test.ts +73 -0
  250. package/tests/skill-registry.test.ts +50 -1
  251. package/tests/startup-banner.test.ts +130 -2
  252. package/tests/stats-collector.test.ts +195 -0
  253. package/tests/stats-view.test.ts +202 -0
  254. package/tests/telemetry-trigger.test.ts +81 -20
  255. package/tests/theme-customization.test.ts +72 -0
  256. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  257. package/tests/vim-editor-adapter.test.ts +804 -0
  258. package/tests/vim-normal-engine.test.ts +101 -0
  259. package/tests/vim-operator-engine.test.ts +215 -0
  260. package/tests/vim-policy.test.ts +19 -0
  261. package/tests/vim-visual-engine.test.ts +52 -0
  262. package/tests/visual-customization-policy.test.ts +110 -0
  263. package/tests/visual-customize-view.test.ts +418 -0
  264. package/tests/visual-profiles.test.ts +87 -0
  265. package/tests/yolo-customize.test.ts +256 -0
  266. package/tests/yolo-mode-runtime.test.ts +161 -0
  267. package/tests/yolo-mode.test.ts +261 -0
  268. package/tests/yolo-session-policy.test.ts +59 -0
  269. package/themes/Gentle.json +2 -1
  270. package/themes/Gentleman-Cute.json +2 -1
  271. package/themes/Gentleman-Sexy.json +2 -1
  272. package/assets/agents/sdd-apply.md +0 -159
  273. package/assets/agents/sdd-archive.md +0 -228
  274. package/assets/agents/sdd-design.md +0 -49
  275. package/assets/agents/sdd-explore.md +0 -48
  276. package/assets/agents/sdd-init.md +0 -56
  277. package/assets/agents/sdd-onboard.md +0 -52
  278. package/assets/agents/sdd-proposal.md +0 -64
  279. package/assets/agents/sdd-remediate.md +0 -37
  280. package/assets/agents/sdd-research.md +0 -49
  281. package/assets/agents/sdd-spec.md +0 -192
  282. package/assets/agents/sdd-status.md +0 -54
  283. package/assets/agents/sdd-tasks.md +0 -108
  284. package/assets/agents/sdd-verify.md +0 -124
  285. package/assets/chains/sdd-full.chain.md +0 -83
  286. package/assets/chains/sdd-plan.chain.md +0 -56
  287. package/assets/chains/sdd-verify.chain.md +0 -43
  288. package/assets/sdd-orchestrator-workflow.md +0 -319
  289. package/assets/support/sdd-status-contract.md +0 -77
  290. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  291. package/extensions/sdd-init.ts +0 -816
  292. package/lib/openspec-deltas.ts +0 -156
  293. package/lib/sdd-preflight.ts +0 -1066
  294. package/lib/sdd-research-capabilities.ts +0 -94
  295. package/lib/sdd-status.ts +0 -26
  296. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  297. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  298. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  299. package/tests/openspec-deltas.test.ts +0 -209
  300. package/tests/sdd-agent-tools.test.ts +0 -156
  301. package/tests/sdd-archive-replay.test.ts +0 -82
  302. package/tests/sdd-classical-continuation.test.ts +0 -74
  303. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  304. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  305. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  306. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  307. package/tests/sdd-odd-integration.test.ts +0 -33
  308. package/tests/sdd-optional-research.test.ts +0 -124
  309. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  310. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  311. package/tests/sdd-preflight.test.ts +0 -541
  312. package/tests/sdd-research-capabilities.test.ts +0 -114
  313. package/tests/sdd-research-live.test.ts +0 -241
  314. package/tests/sdd-selection-transport.test.ts +0 -653
  315. package/tests/sdd-status.test.ts +0 -9
  316. package/tests/sdd-task-truth.test.ts +0 -43
@@ -0,0 +1,351 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import {
7
+ appendSessionCapture,
8
+ openSessionWriter,
9
+ projectHash,
10
+ seedFilePath,
11
+ sessionFilePath,
12
+ } from "../extensions/history/store.ts";
13
+ import promptHistoryExtension, { captureEnabled } from "../extensions/history/index.ts";
14
+ import { writeHistoryCapturePolicy } from "../lib/history-capture-policy.ts";
15
+
16
+ function makeRoot(): string {
17
+ return fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-writer-"));
18
+ }
19
+
20
+ const CWD = "/pi-history-test/project-a";
21
+
22
+ /** An empty Gentle config home: the Customize preference is unset. */
23
+ function makeConfigHome(): string {
24
+ return fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-config-"));
25
+ }
26
+
27
+ function fileTexts(file: string): string[] {
28
+ return fs
29
+ .readFileSync(file, "utf8")
30
+ .split("\n")
31
+ .filter((l) => l.trim().length > 0)
32
+ .map((l) => (JSON.parse(l) as { text: string }).text);
33
+ }
34
+
35
+ function openWriterForTest(root: string, instanceId: string) {
36
+ return openSessionWriter(root, CWD, instanceId);
37
+ }
38
+
39
+ /** Load the extension against a temp root and return its event handlers. */
40
+ function handlersWith(
41
+ env: NodeJS.ProcessEnv,
42
+ root: string,
43
+ configHome: string = makeConfigHome(),
44
+ ): Map<string, (event: unknown) => void> {
45
+ const registered: Array<[string, unknown]> = [];
46
+ const pi = {
47
+ on: (event: string, handler: unknown) => {
48
+ registered.push([event, handler]);
49
+ },
50
+ // Slice-3+ wiring surface: the factory also registers the shortcut,
51
+ // command, and tool_call dismissal; the capture handler stays the
52
+ // first registration, so these no-ops only absorb the extra wiring.
53
+ registerShortcut: () => {},
54
+ registerCommand: () => {},
55
+ };
56
+ promptHistoryExtension(pi as never, {
57
+ env,
58
+ root,
59
+ cwd: CWD,
60
+ instanceId: "inst-entry",
61
+ now: () => 1700000000000,
62
+ agentDir: path.join(root, "agent"),
63
+ sessionsRoot: path.join(root, "sessions"),
64
+ // Never read a developer's real Customize preference.
65
+ gentlePiConfigHome: configHome,
66
+ });
67
+ return new Map(
68
+ registered.map(([event, handler]) => [
69
+ event,
70
+ handler as (event: unknown) => void,
71
+ ]),
72
+ );
73
+ }
74
+
75
+ /** Load the extension against a temp root and return the capture handler. */
76
+ function captureHandlerWith(
77
+ env: NodeJS.ProcessEnv,
78
+ root: string,
79
+ configHome?: string,
80
+ ) {
81
+ const handler = handlersWith(env, root, configHome).get("before_agent_start");
82
+ assert.ok(handler, "the capture handler is registered");
83
+ return handler;
84
+ }
85
+
86
+ /** Fill the project dir past the default GC file threshold (50 files). */
87
+ function fillProjectDir(root: string, files: number): string {
88
+ const dir = path.join(root, "projects", projectHash(CWD));
89
+ fs.mkdirSync(dir, { recursive: true });
90
+ for (let i = 1; i <= files; i++) {
91
+ const file = path.join(dir, `peer-${String(i).padStart(3, "0")}.jsonl`);
92
+ fs.writeFileSync(file, `${JSON.stringify({ v: 1, text: `peer ${i}` })}\n`);
93
+ fs.utimesSync(file, new Date(i * 1000), new Date(i * 1000));
94
+ }
95
+ return dir;
96
+ }
97
+
98
+ test("no file is created until the first capture", () => {
99
+ const root = makeRoot();
100
+ const state = openWriterForTest(root, "sess-1");
101
+ const file = sessionFilePath(root, CWD, "sess-1");
102
+ assert.equal(fs.existsSync(file), false);
103
+ assert.equal(state.lineCount, 0);
104
+ });
105
+
106
+ test("first capture lazily creates the file and appends one line", () => {
107
+ const root = makeRoot();
108
+ const state = openWriterForTest(root, "sess-1");
109
+ appendSessionCapture(state, "hello world", 1234);
110
+ const file = sessionFilePath(root, CWD, "sess-1");
111
+ assert.equal(fs.existsSync(file), true);
112
+ const lines = fs.readFileSync(file, "utf8").trim().split("\n");
113
+ assert.equal(lines.length, 1);
114
+ const parsed = JSON.parse(lines[0]);
115
+ assert.equal(parsed.text, "hello world");
116
+ assert.equal(parsed.ts, 1234);
117
+ assert.equal(parsed.v, 1);
118
+ assert.equal(state.lineCount, 1);
119
+ });
120
+
121
+ test("captures append in order; count tracks", () => {
122
+ const root = makeRoot();
123
+ const state = openWriterForTest(root, "sess-2");
124
+ appendSessionCapture(state, "one");
125
+ appendSessionCapture(state, "two");
126
+ appendSessionCapture(state, "three");
127
+ assert.deepEqual(fileTexts(sessionFilePath(root, CWD, "sess-2")), [
128
+ "one",
129
+ "two",
130
+ "three",
131
+ ]);
132
+ assert.equal(state.lineCount, 3);
133
+ });
134
+
135
+ test("command-like and empty captures are skipped", () => {
136
+ const root = makeRoot();
137
+ const state = openWriterForTest(root, "sess-3");
138
+ appendSessionCapture(state, "/compact");
139
+ appendSessionCapture(state, " ");
140
+ appendSessionCapture(state, "");
141
+ appendSessionCapture(state, "kept");
142
+ assert.deepEqual(fileTexts(sessionFilePath(root, CWD, "sess-3")), ["kept"]);
143
+ assert.equal(state.lineCount, 1);
144
+ });
145
+
146
+ test("two writers own separate files in the same project dir", () => {
147
+ const root = makeRoot();
148
+ const a = openWriterForTest(root, "inst-a");
149
+ const b = openWriterForTest(root, "inst-b");
150
+ appendSessionCapture(a, "from-a");
151
+ appendSessionCapture(b, "from-b");
152
+ const dir = path.join(root, "projects", projectHash(CWD));
153
+ const files = fs.readdirSync(dir).sort();
154
+ assert.deepEqual(files, ["inst-a.jsonl", "inst-b.jsonl"]);
155
+ });
156
+
157
+ test("the extension entry registers exactly the slice-6 wiring surface", () => {
158
+ // Module load must stay side-effect free (importing index.ts parses the
159
+ // whole graph without touching the real ~/.pi store root). Wiring as of
160
+ // slice 6: before_agent_start capture, session_shutdown GC, tool_call
161
+ // overlay dismiss, the ctrl+shift+r shortcut, and the history command.
162
+ const registered: Array<[string, unknown]> = [];
163
+ const shortcuts: Array<[string, unknown]> = [];
164
+ const commands: Array<[string, unknown]> = [];
165
+ const pi = {
166
+ on: (event: string, handler: unknown) => {
167
+ registered.push([event, handler]);
168
+ },
169
+ registerShortcut: (key: string, def: unknown) => {
170
+ shortcuts.push([key, def]);
171
+ },
172
+ registerCommand: (name: string, def: unknown) => {
173
+ commands.push([name, def]);
174
+ },
175
+ };
176
+ // Capture off (explicit env, empty config home) keeps the warm-up from
177
+ // touching the real store root while the defaults are exercised.
178
+ promptHistoryExtension(pi as never, {
179
+ env: { GENTLE_PI_HISTORY_CAPTURE: "off" },
180
+ gentlePiConfigHome: makeConfigHome(),
181
+ });
182
+ assert.deepEqual(
183
+ registered.map(([event]) => event),
184
+ ["before_agent_start", "session_shutdown", "tool_call"],
185
+ );
186
+ assert.deepEqual(shortcuts.map(([key]) => key), ["ctrl+shift+r"]);
187
+ assert.deepEqual(commands.map(([name]) => name), ["history"]);
188
+ // Handlers are callable but are NEVER invoked here: a real invocation
189
+ // would run getWriter() against the user's real ~/.pi/agent/history.
190
+ for (const [, handler] of registered) {
191
+ assert.equal(typeof handler, "function");
192
+ }
193
+ });
194
+
195
+ test("captureEnabled is a strict opt-in", () => {
196
+ // Every call gets an empty config home: an env value that defers to the
197
+ // preference must never read the developer's real Customize setting.
198
+ const configHome = makeConfigHome();
199
+ const enabled = (env: NodeJS.ProcessEnv) => captureEnabled(env, configHome);
200
+ assert.equal(enabled({}), false);
201
+ assert.equal(enabled({ GENTLE_PI_HISTORY_CAPTURE: "0" }), false);
202
+ assert.equal(enabled({ GENTLE_PI_HISTORY_CAPTURE: "false" }), false);
203
+ assert.equal(enabled({ GENTLE_PI_HISTORY_CAPTURE: "off" }), false);
204
+ assert.equal(enabled({ GENTLE_PI_HISTORY_CAPTURE: "yes" }), false);
205
+ assert.equal(enabled({ GENTLE_PI_HISTORY_CAPTURE: " 1 " }), true);
206
+ assert.equal(enabled({ GENTLE_PI_HISTORY_CAPTURE: "TRUE" }), true);
207
+ assert.equal(enabled({ GENTLE_PI_HISTORY_CAPTURE: "On" }), true);
208
+ // The unshipped rename from the contributor branch is not a switch.
209
+ assert.equal(enabled({ [`GENTLE_PI_HISTORY_${"ENABLE"}`]: "1" }), false);
210
+ });
211
+
212
+ test("the capture handler is a no-op unless the user opts in", () => {
213
+ const root = makeRoot();
214
+ const handler = captureHandlerWith({}, root);
215
+ handler({ prompt: "sensitive prompt" });
216
+ handler({ prompt: "another one" });
217
+ // Nothing at all: no capture file, no project dir, no registry entry.
218
+ assert.deepEqual(fs.readdirSync(root), []);
219
+ });
220
+
221
+ test("an opted-in session captures delivered prompts", () => {
222
+ const root = makeRoot();
223
+ const handler = captureHandlerWith({ GENTLE_PI_HISTORY_CAPTURE: "1" }, root);
224
+ handler({ prompt: "hello store" });
225
+ assert.deepEqual(fileTexts(sessionFilePath(root, CWD, "inst-entry")), [
226
+ "hello store",
227
+ ]);
228
+ });
229
+
230
+ test("opted-in capture imports into its own root and defers seed on untrusted tombstones", () => {
231
+ const root = makeRoot();
232
+ const sessions = path.join(root, "sessions", "--pi-history-test-project-a--");
233
+ fs.mkdirSync(sessions, { recursive: true });
234
+ fs.writeFileSync(path.join(sessions, "s.jsonl"), [
235
+ JSON.stringify({ type: "session", version: 3 }),
236
+ JSON.stringify({ type: "message", message: { role: "user", content: "transcript prompt" } }),
237
+ ].join("\n") + "\n");
238
+ const agentDir = path.join(root, "agent");
239
+ fs.mkdirSync(agentDir);
240
+ fs.writeFileSync(path.join(agentDir, "editor-history.jsonl"),
241
+ JSON.stringify({ v: 1, text: "legacy prompt" }) + "\n");
242
+ fs.writeFileSync(path.join(root, "hidden.json"), "{invalid");
243
+ const handler = captureHandlerWith({ GENTLE_PI_HISTORY_CAPTURE: "1" }, root);
244
+ handler({ prompt: "current prompt" });
245
+ assert.deepEqual(fileTexts(path.join(root, "history-global.jsonl")), ["legacy prompt"]);
246
+ assert.equal(fs.existsSync(seedFilePath(root, CWD)), false);
247
+ assert.deepEqual(fileTexts(sessionFilePath(root, CWD, "inst-entry")), ["current prompt"]);
248
+ fs.writeFileSync(path.join(root, "hidden.json"), JSON.stringify(["transcript prompt"]));
249
+ // A new instance retries bootstrap after tombstones become trusted.
250
+ captureHandlerWith({ GENTLE_PI_HISTORY_CAPTURE: "1" }, root)({ prompt: "next prompt" });
251
+ assert.equal(fs.existsSync(seedFilePath(root, CWD)), false);
252
+ });
253
+
254
+ test("disabling capture stops new lines and leaves existing files alone", () => {
255
+ const root = makeRoot();
256
+ const env: NodeJS.ProcessEnv = { GENTLE_PI_HISTORY_CAPTURE: "true" };
257
+ const handler = captureHandlerWith(env, root);
258
+ handler({ prompt: "kept" });
259
+ const file = sessionFilePath(root, CWD, "inst-entry");
260
+ assert.equal(fs.existsSync(file), true);
261
+ delete env.GENTLE_PI_HISTORY_CAPTURE;
262
+ handler({ prompt: "never written" });
263
+ assert.deepEqual(fileTexts(file), ["kept"]);
264
+ });
265
+
266
+ test("the Customize preference enables capture without the env switch and toggles live", () => {
267
+ const root = makeRoot();
268
+ const configHome = makeConfigHome();
269
+ const handler = captureHandlerWith({}, root, configHome);
270
+ handler({ prompt: "before opt-in" });
271
+ assert.deepEqual(fs.readdirSync(root), []);
272
+ writeHistoryCapturePolicy("on", { gentlePiConfigHome: configHome });
273
+ handler({ prompt: "captured" });
274
+ const file = sessionFilePath(root, CWD, "inst-entry");
275
+ assert.deepEqual(fileTexts(file), ["captured"]);
276
+ // Turning the preference off stops new lines and keeps stored history.
277
+ writeHistoryCapturePolicy("off", { gentlePiConfigHome: configHome });
278
+ handler({ prompt: "never written" });
279
+ assert.deepEqual(fileTexts(file), ["captured"]);
280
+ });
281
+
282
+ test("an explicit env value overrides the Customize preference", () => {
283
+ const root = makeRoot();
284
+ const configHome = makeConfigHome();
285
+ writeHistoryCapturePolicy("on", { gentlePiConfigHome: configHome });
286
+ captureHandlerWith({ GENTLE_PI_HISTORY_CAPTURE: "0" }, root, configHome)({ prompt: "forced off" });
287
+ assert.deepEqual(fs.readdirSync(root), []);
288
+ writeHistoryCapturePolicy("off", { gentlePiConfigHome: configHome });
289
+ captureHandlerWith({ GENTLE_PI_HISTORY_CAPTURE: "On" }, root, configHome)({ prompt: "forced on" });
290
+ assert.deepEqual(fileTexts(sessionFilePath(root, CWD, "inst-entry")), ["forced on"]);
291
+ });
292
+
293
+ test("a malformed Customize preference fails closed", () => {
294
+ const root = makeRoot();
295
+ const configHome = makeConfigHome();
296
+ fs.writeFileSync(path.join(configHome, "history-capture.json"), '{"policy":"on"}');
297
+ captureHandlerWith({}, root, configHome)({ prompt: "not captured" });
298
+ assert.deepEqual(fs.readdirSync(root), []);
299
+ assert.equal(captureEnabled({}, configHome), false);
300
+ });
301
+
302
+ test("the extension resolves the preference under GENTLE_PI_CONFIG_HOME by default", () => {
303
+ const root = makeRoot();
304
+ const configHome = makeConfigHome();
305
+ writeHistoryCapturePolicy("on", { gentlePiConfigHome: configHome });
306
+ const registered: Array<[string, unknown]> = [];
307
+ promptHistoryExtension({ on: (event: string, handler: unknown) => registered.push([event, handler]), registerShortcut: () => {}, registerCommand: () => {} } as never, {
308
+ env: { GENTLE_PI_CONFIG_HOME: configHome },
309
+ root,
310
+ cwd: CWD,
311
+ instanceId: "inst-entry",
312
+ now: () => 1700000000000,
313
+ agentDir: path.join(root, "agent"),
314
+ sessionsRoot: path.join(root, "sessions"),
315
+ });
316
+ const capture = registered.find(([event]) => event === "before_agent_start")![1] as (event: unknown) => void;
317
+ capture({ prompt: "from config home" });
318
+ assert.deepEqual(fileTexts(sessionFilePath(root, CWD, "inst-entry")), ["from config home"]);
319
+ });
320
+
321
+ test("session_shutdown GC is a no-op while capture is off", () => {
322
+ const root = makeRoot();
323
+ const dir = fillProjectDir(root, 60);
324
+ const before = fs.readdirSync(dir).sort();
325
+ const shutdown = handlersWith({}, root).get("session_shutdown");
326
+ assert.ok(shutdown, "the shutdown handler is registered");
327
+ shutdown({});
328
+ assert.deepEqual(fs.readdirSync(dir).sort(), before);
329
+ assert.deepEqual(fs.readdirSync(root).sort(), ["projects"]);
330
+ });
331
+
332
+ test("session_shutdown GC compacts the injected root and keeps its own file", () => {
333
+ const root = makeRoot();
334
+ const dir = fillProjectDir(root, 60);
335
+ // This instance's own capture file is the oldest one in the dir.
336
+ const own = sessionFilePath(root, CWD, "inst-entry");
337
+ fs.writeFileSync(own, `${JSON.stringify({ v: 1, text: "own" })}\n`);
338
+ fs.utimesSync(own, new Date(1), new Date(1));
339
+ const shutdown = handlersWith({ GENTLE_PI_HISTORY_CAPTURE: "1" }, root).get(
340
+ "session_shutdown",
341
+ );
342
+ assert.ok(shutdown, "the shutdown handler is registered");
343
+ shutdown({});
344
+ const names = fs.readdirSync(dir);
345
+ // Default policy: the newest 10 peers stay, the other 50 merge into one
346
+ // compact file, and the own file is never a merge candidate.
347
+ assert.equal(names.filter((n) => n.startsWith("compact-")).length, 1);
348
+ assert.equal(names.filter((n) => n.startsWith("peer-")).length, 10);
349
+ assert.equal(fs.existsSync(own), true);
350
+ assert.deepEqual(fileTexts(own), ["own"]);
351
+ });
@@ -0,0 +1,79 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import {
7
+ globalSeedPath,
8
+ projectDir,
9
+ projectHash,
10
+ registryPath,
11
+ seedFilePath,
12
+ sessionFilePath,
13
+ } from "../extensions/history/store.ts";
14
+
15
+ const ROOT = path.join(os.tmpdir(), "pi-history-test-root");
16
+
17
+ test("projectHash returns 16 lowercase hex chars", () => {
18
+ const hash = projectHash("/pi-history-test/project-a");
19
+ assert.match(hash, /^[0-9a-f]{16}$/);
20
+ });
21
+
22
+ test("known vector: stable hash for a fixed path", () => {
23
+ // The literal exists on no machine, so every platform exercises the
24
+ // documented raw-string fallback: sha256(literal), first 16 hex chars.
25
+ assert.equal(
26
+ projectHash("/pi-history-test/project-a"),
27
+ "4be15ec687e9df85",
28
+ );
29
+ });
30
+
31
+ test("distinct paths produce distinct hashes", () => {
32
+ assert.notEqual(
33
+ projectHash("/pi-history-test/project-a"),
34
+ projectHash("/pi-history-test/project-b"),
35
+ );
36
+ });
37
+
38
+ test("symlinked cwd resolves to the same hash as its target", () => {
39
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "paths-sym-"));
40
+ const target = path.join(dir, "real-project");
41
+ fs.mkdirSync(target);
42
+ const link = path.join(dir, "link-project");
43
+ fs.symlinkSync(target, link);
44
+ assert.equal(projectHash(link), projectHash(target));
45
+ });
46
+
47
+ test("trailing slash does not change the identity", () => {
48
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "paths-slash-"));
49
+ assert.equal(projectHash(dir), projectHash(`${dir}/`));
50
+ });
51
+
52
+ test("nonexistent path falls back to hashing the raw string (no throw)", () => {
53
+ const missing = path.join(os.tmpdir(), "paths-missing-does-not-exist");
54
+ const hash = projectHash(missing);
55
+ assert.match(hash, /^[0-9a-f]{16}$/);
56
+ });
57
+
58
+ test("path derivations compose under the root", () => {
59
+ const cwd = "/pi-history-test/project-a";
60
+ const hash = projectHash(cwd);
61
+ assert.equal(projectDir(ROOT, cwd), path.join(ROOT, "projects", hash));
62
+ assert.equal(
63
+ sessionFilePath(ROOT, cwd, "abc-123"),
64
+ path.join(ROOT, "projects", hash, "abc-123.jsonl"),
65
+ );
66
+ assert.equal(
67
+ seedFilePath(ROOT, cwd),
68
+ path.join(ROOT, "projects", hash, "seed.jsonl"),
69
+ );
70
+ assert.equal(globalSeedPath(ROOT), path.join(ROOT, "history-global.jsonl"));
71
+ assert.equal(registryPath(ROOT), path.join(ROOT, "registry.json"));
72
+ });
73
+
74
+ test("two cwds map to sibling project dirs", () => {
75
+ const a = projectDir(ROOT, "/pi-history-test/project-a");
76
+ const b = projectDir(ROOT, "/pi-history-test/project-b");
77
+ assert.notEqual(a, b);
78
+ assert.equal(path.dirname(a), path.dirname(b));
79
+ });
@@ -0,0 +1,139 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import {
7
+ hidePrompt,
8
+ readHiddenPrompts,
9
+ tombstoneKey,
10
+ } from "../extensions/history/hide-prompts.ts";
11
+ import {
12
+ bootstrapProjectSeed,
13
+ drainGlobal,
14
+ drainProject,
15
+ projectHash,
16
+ seedFilePath,
17
+ } from "../extensions/history/store.ts";
18
+
19
+ // Exact tombstones (PR #1393 adaptation): a deletion hides ONLY the exact
20
+ // normalized prompt (whitespace-collapsed, trimmed, case-insensitive) —
21
+ // never every prompt that shares a 120-character prefix. hidden.json keeps
22
+ // its JSON-array-of-strings shape; new entries are hashes, and plaintext
23
+ // entries written by the earlier prefix format stay readable and honored.
24
+
25
+ const CWD = "/pi-history-test/tombstone-exact";
26
+
27
+ function makeRoot(): string {
28
+ return fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-tomb-"));
29
+ }
30
+
31
+ function writeLines(file: string, texts: string[]): void {
32
+ fs.mkdirSync(path.dirname(file), { recursive: true });
33
+ fs.writeFileSync(
34
+ file,
35
+ `${texts.map((t) => JSON.stringify({ v: 1, text: t })).join("\n")}\n`,
36
+ "utf8",
37
+ );
38
+ }
39
+
40
+ function drainedProject(root: string): string[] {
41
+ const drained = drainProject(root, CWD, 1000, root);
42
+ assert.equal(drained.status, "ok");
43
+ if (drained.status !== "ok") throw new Error("unreachable");
44
+ return drained.prompts;
45
+ }
46
+
47
+ const PREFIX = `${"shared context ".repeat(10)}`; // 150 chars, > 120
48
+ const LONG_A = `${PREFIX}then deploy staging`;
49
+ const LONG_B = `${PREFIX}then deploy production`;
50
+
51
+ test("tombstoneKey normalizes whitespace and case but never truncates", () => {
52
+ assert.equal(tombstoneKey("Fix\t the BUILD "), tombstoneKey("fix the build"));
53
+ assert.notEqual(tombstoneKey(LONG_A), tombstoneKey(LONG_B));
54
+ assert.match(tombstoneKey("fix the build"), /^sha256:[0-9a-f]{64}$/);
55
+ });
56
+
57
+ test("hidden.json stores a hash, never the deleted prompt's text", () => {
58
+ const root = makeRoot();
59
+ assert.deepEqual(hidePrompt(root, "my secret token abc123"), {
60
+ status: "written",
61
+ });
62
+ const raw = fs.readFileSync(path.join(root, "hidden.json"), "utf8");
63
+ assert.equal(raw.includes("secret"), false);
64
+ assert.deepEqual(JSON.parse(raw), [tombstoneKey("my secret token abc123")]);
65
+ });
66
+
67
+ test("hiding one long prompt keeps a sibling that shares its 120-char prefix", () => {
68
+ const root = makeRoot();
69
+ const dir = path.join(root, "projects", projectHash(CWD));
70
+ writeLines(path.join(dir, "s.jsonl"), [LONG_A, LONG_B, "short keep"]);
71
+ assert.deepEqual(hidePrompt(root, LONG_A), { status: "written" });
72
+
73
+ const prompts = drainedProject(root);
74
+ assert.equal(prompts.includes(LONG_A), false, "the deleted prompt stays hidden");
75
+ assert.ok(prompts.includes(LONG_B), "the prefix sibling must stay visible");
76
+ assert.ok(prompts.includes("short keep"));
77
+
78
+ const global = drainGlobal(root, 1000, root);
79
+ assert.equal(global.status, "ok");
80
+ if (global.status !== "ok") throw new Error("unreachable");
81
+ assert.equal(global.prompts.includes(LONG_A), false);
82
+ assert.ok(global.prompts.includes(LONG_B));
83
+ });
84
+
85
+ test("a tombstone hides whitespace and case variants of the exact prompt", () => {
86
+ const root = makeRoot();
87
+ const dir = path.join(root, "projects", projectHash(CWD));
88
+ writeLines(path.join(dir, "s.jsonl"), ["Deploy THE api", "deploy the api now"]);
89
+ hidePrompt(root, "deploy the api");
90
+ assert.deepEqual(drainedProject(root), ["deploy the api now"]);
91
+ });
92
+
93
+ test("plaintext entries from the earlier prefix format stay readable and honored", () => {
94
+ const root = makeRoot();
95
+ const dir = path.join(root, "projects", projectHash(CWD));
96
+ writeLines(path.join(dir, "s.jsonl"), ["Legacy Hidden", "visible prompt"]);
97
+ // The earlier writer stored promptDedupKey output: the whitespace-
98
+ // collapsed, 120-char, lowercased text.
99
+ fs.writeFileSync(
100
+ path.join(root, "hidden.json"),
101
+ JSON.stringify(["legacy hidden"]),
102
+ "utf8",
103
+ );
104
+ assert.equal(readHiddenPrompts(root).status, "trusted");
105
+ assert.deepEqual(drainedProject(root), ["visible prompt"]);
106
+
107
+ // A new hide appends a hash entry and preserves the legacy entry.
108
+ hidePrompt(root, "visible prompt");
109
+ const stored = JSON.parse(
110
+ fs.readFileSync(path.join(root, "hidden.json"), "utf8"),
111
+ );
112
+ assert.deepEqual(stored, ["legacy hidden", tombstoneKey("visible prompt")]);
113
+ assert.deepEqual(drainedProject(root), []);
114
+ });
115
+
116
+ test("seed bootstrap skips an exact tombstone but seeds its prefix sibling", () => {
117
+ const root = makeRoot();
118
+ const sessionsRoot = path.join(root, "sessions");
119
+ const sessions = path.join(sessionsRoot, "--pi-history-test-tombstone-exact--");
120
+ fs.mkdirSync(sessions, { recursive: true });
121
+ fs.writeFileSync(
122
+ path.join(sessions, "s.jsonl"),
123
+ [
124
+ JSON.stringify({ type: "session", version: 3 }),
125
+ JSON.stringify({ type: "message", message: { role: "user", content: LONG_A } }),
126
+ JSON.stringify({ type: "message", message: { role: "user", content: LONG_B } }),
127
+ ].join("\n") + "\n",
128
+ );
129
+ hidePrompt(root, LONG_A);
130
+
131
+ const result = bootstrapProjectSeed(root, CWD, sessionsRoot, 500, root);
132
+ assert.deepEqual(result, { seeded: 1, ran: true });
133
+ const seeded = fs
134
+ .readFileSync(seedFilePath(root, CWD), "utf8")
135
+ .trim()
136
+ .split("\n")
137
+ .map((line) => (JSON.parse(line) as { text: string }).text);
138
+ assert.deepEqual(seeded, [LONG_B]);
139
+ });