@retaw106/mmc-oc 2.2.1 → 2.4.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 (285) hide show
  1. package/dist/cli.js +14 -3
  2. package/dist/config/deprecated-config.d.ts +4 -0
  3. package/dist/config/deprecated-config.d.ts.map +1 -0
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/project-security.d.ts +2 -2
  6. package/dist/config/schema/magic-context.d.ts +36 -3
  7. package/dist/config/schema/magic-context.d.ts.map +1 -1
  8. package/dist/config-init.js +14 -3
  9. package/dist/{context-mrvm92n8.js → context-c72brxe5.js} +2410 -1801
  10. package/dist/{derive-budgets-rjjz5f7y.js → derive-budgets-s1p37t2g.js} +2 -2
  11. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  12. package/dist/features/magic-context/historian-failure-reason.d.ts +7 -0
  13. package/dist/features/magic-context/historian-failure-reason.d.ts.map +1 -0
  14. package/dist/features/magic-context/memory-maintainer/launcher.d.ts +13 -4
  15. package/dist/features/magic-context/memory-maintainer/launcher.d.ts.map +1 -1
  16. package/dist/features/magic-context/memory-maintainer/manual-commands.d.ts.map +1 -1
  17. package/dist/features/magic-context/memory-maintainer/model-chain.d.ts +47 -0
  18. package/dist/features/magic-context/memory-maintainer/model-chain.d.ts.map +1 -0
  19. package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts +17 -4
  20. package/dist/features/magic-context/memory-maintainer/oc2-adapter.d.ts.map +1 -1
  21. package/dist/features/magic-context/memory-maintainer/pipeline.d.ts +6 -0
  22. package/dist/features/magic-context/memory-maintainer/pipeline.d.ts.map +1 -1
  23. package/dist/features/magic-context/message-index-async.d.ts +21 -3
  24. package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
  25. package/dist/features/magic-context/message-index.d.ts +7 -1
  26. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  27. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  28. package/dist/features/magic-context/pi-content-decisions.d.ts +7 -1
  29. package/dist/features/magic-context/pi-content-decisions.d.ts.map +1 -1
  30. package/dist/features/magic-context/search.d.ts +29 -0
  31. package/dist/features/magic-context/search.d.ts.map +1 -1
  32. package/dist/features/magic-context/storage-clone.d.ts +2 -0
  33. package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
  34. package/dist/features/magic-context/storage-compartment-history-version.d.ts +4 -0
  35. package/dist/features/magic-context/storage-compartment-history-version.d.ts.map +1 -0
  36. package/dist/features/magic-context/storage-db.d.ts +1 -1
  37. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  38. package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -1
  39. package/dist/features/magic-context/storage-meta-persisted.d.ts +23 -5
  40. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  41. package/dist/features/magic-context/storage-meta-session.d.ts +7 -1
  42. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  43. package/dist/features/magic-context/storage-session-tables.d.ts +5 -1
  44. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -1
  45. package/dist/features/magic-context/storage-tag-identity-revision.d.ts +50 -0
  46. package/dist/features/magic-context/storage-tag-identity-revision.d.ts.map +1 -0
  47. package/dist/features/magic-context/storage-tags.d.ts +159 -5
  48. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  49. package/dist/features/magic-context/tagger.d.ts +2 -0
  50. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  51. package/dist/features/magic-context/transform-decision-log.d.ts +60 -2
  52. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts +34 -0
  54. package/dist/hooks/magic-context/__fixtures__/tail-hygiene-refresh.d.ts.map +1 -0
  55. package/dist/hooks/magic-context/active-anthropic-turn.d.ts +2 -0
  56. package/dist/hooks/magic-context/active-anthropic-turn.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/apply-operations.d.ts +10 -1
  58. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  59. package/dist/hooks/magic-context/auto-search-runner.d.ts +2 -2
  60. package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -2
  62. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/compartment-runner.d.ts +4 -0
  64. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/compartment-trigger-error.d.ts +11 -0
  66. package/dist/hooks/magic-context/compartment-trigger-error.d.ts.map +1 -0
  67. package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
  68. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +1 -0
  70. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/degraded-pass-refusal.d.ts +2 -0
  72. package/dist/hooks/magic-context/degraded-pass-refusal.d.ts.map +1 -1
  73. package/dist/hooks/magic-context/edit-marker.d.ts +2 -0
  74. package/dist/hooks/magic-context/edit-marker.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts +2 -0
  76. package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  78. package/dist/hooks/magic-context/history-boundary-repair.d.ts +11 -2
  79. package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
  80. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/lkg-measured-request.d.ts +3 -1
  82. package/dist/hooks/magic-context/lkg-measured-request.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/lkg-persist.d.ts +10 -13
  84. package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts +62 -0
  86. package/dist/hooks/magic-context/lkg-recovery-fence.d.ts.map +1 -0
  87. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts +38 -0
  88. package/dist/hooks/magic-context/lkg-recovery-prefix.d.ts.map +1 -0
  89. package/dist/hooks/magic-context/lkg-replay.d.ts +16 -9
  90. package/dist/hooks/magic-context/lkg-replay.d.ts.map +1 -1
  91. package/dist/hooks/magic-context/lkg-slot.d.ts +141 -2
  92. package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
  93. package/dist/hooks/magic-context/opaque-thinking.d.ts +12 -0
  94. package/dist/hooks/magic-context/opaque-thinking.d.ts.map +1 -0
  95. package/dist/hooks/magic-context/pass-outcome.d.ts +1 -0
  96. package/dist/hooks/magic-context/pass-outcome.d.ts.map +1 -1
  97. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts +14 -0
  98. package/dist/hooks/magic-context/pending-drop-eligibility.d.ts.map +1 -0
  99. package/dist/hooks/magic-context/pending-materialization.d.ts +10 -0
  100. package/dist/hooks/magic-context/pending-materialization.d.ts.map +1 -0
  101. package/dist/hooks/magic-context/postprocess-read-cache.d.ts.map +1 -1
  102. package/dist/hooks/magic-context/provider-visible-parts.d.ts +51 -0
  103. package/dist/hooks/magic-context/provider-visible-parts.d.ts.map +1 -0
  104. package/dist/hooks/magic-context/read-session-chunk.d.ts +22 -1
  105. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  106. package/dist/hooks/magic-context/read-session-db.d.ts +3 -0
  107. package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
  108. package/dist/hooks/magic-context/read-session-formatting.d.ts +5 -0
  109. package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
  110. package/dist/hooks/magic-context/read-session-raw.d.ts +123 -1
  111. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  112. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  113. package/dist/hooks/magic-context/reasoning-budget.d.ts.map +1 -1
  114. package/dist/hooks/magic-context/render-options.d.ts +19 -0
  115. package/dist/hooks/magic-context/render-options.d.ts.map +1 -0
  116. package/dist/hooks/magic-context/sentinel.d.ts +41 -1
  117. package/dist/hooks/magic-context/sentinel.d.ts.map +1 -1
  118. package/dist/hooks/magic-context/strict-thinking-prefix.d.ts +8 -0
  119. package/dist/hooks/magic-context/strict-thinking-prefix.d.ts.map +1 -0
  120. package/dist/hooks/magic-context/strip-content.d.ts +38 -24
  121. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  122. package/dist/hooks/magic-context/supersession-reclaim.d.ts +5 -3
  123. package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
  124. package/dist/hooks/magic-context/system-date-refresh.d.ts +29 -0
  125. package/dist/hooks/magic-context/system-date-refresh.d.ts.map +1 -0
  126. package/dist/hooks/magic-context/system-prompt-dates.d.ts +6 -0
  127. package/dist/hooks/magic-context/system-prompt-dates.d.ts.map +1 -0
  128. package/dist/hooks/magic-context/system-prompt-delivery.d.ts +47 -0
  129. package/dist/hooks/magic-context/system-prompt-delivery.d.ts.map +1 -0
  130. package/dist/hooks/magic-context/system-prompt-hash.d.ts +18 -5
  131. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  132. package/dist/hooks/magic-context/tag-messages.d.ts +19 -0
  133. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  134. package/dist/hooks/magic-context/tag-owner-warmup.d.ts +14 -0
  135. package/dist/hooks/magic-context/tag-owner-warmup.d.ts.map +1 -0
  136. package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts +18 -0
  137. package/dist/hooks/magic-context/tail-hygiene-output-count.d.ts.map +1 -0
  138. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +8 -4
  139. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
  140. package/dist/hooks/magic-context/token-count-exact.d.ts +35 -0
  141. package/dist/hooks/magic-context/token-count-exact.d.ts.map +1 -0
  142. package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
  143. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  144. package/dist/hooks/magic-context/tool-reclaim.d.ts +5 -2
  145. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
  146. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  147. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +9 -0
  148. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  149. package/dist/hooks/magic-context/transform.d.ts +21 -11
  150. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  151. package/dist/hooks/magic-context/unresolved-history-boundary.d.ts +5 -1
  152. package/dist/hooks/magic-context/unresolved-history-boundary.d.ts.map +1 -1
  153. package/dist/{index-g2z2k5m4.js → index-27ddyx39.js} +2 -2
  154. package/dist/{index-btp5tdc8.js → index-48e9znrc.js} +1248 -570
  155. package/dist/{index-5cep2r9n.js → index-5ty9rnbx.js} +60 -258
  156. package/dist/{index-z2g20msd.js → index-azbmnekv.js} +4 -4
  157. package/dist/{index-1j34a56j.js → index-dwh99z66.js} +293 -36
  158. package/dist/{index-qbkzdkk0.js → index-k59af64t.js} +6 -2
  159. package/dist/{index-5b7x80ec.js → index-kahkxcqw.js} +12532 -10348
  160. package/dist/{index-53g38ggt.js → index-kmbme1v1.js} +1 -1
  161. package/dist/{index-amycb7b1.js → index-r7s7d350.js} +3 -3
  162. package/dist/{index-33gprt9g.js → index-yjv8dpht.js} +1 -1
  163. package/dist/index-ykq24x1n.js +1477 -0
  164. package/dist/index.d.ts.map +1 -1
  165. package/dist/index.js +114 -2
  166. package/dist/{message-time-backfill-phrwp2fz.js → message-time-backfill-j96x1sab.js} +3 -3
  167. package/dist/model-resolution-cvyrrcjw.js +8 -0
  168. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  169. package/dist/{read-session-chunk-2xm4sc4y.js → read-session-chunk-t8bg6wkz.js} +6 -4
  170. package/dist/recomp-orchestrator-k760qy1n.js +21 -0
  171. package/dist/{safe-notification-target-ph3xd1ve.js → safe-notification-target-1jkf0m82.js} +2 -2
  172. package/dist/{send-session-notification-rnc4nrsr.js → send-session-notification-njjf3b4s.js} +5 -5
  173. package/dist/shared/auto-search-lifecycle.d.ts +14 -0
  174. package/dist/shared/auto-search-lifecycle.d.ts.map +1 -1
  175. package/dist/shared/logger.d.ts.map +1 -1
  176. package/dist/shared/model-suggestion-retry.d.ts +21 -0
  177. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  178. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  179. package/dist/shared/models-dev-temperature.d.ts +18 -0
  180. package/dist/shared/models-dev-temperature.d.ts.map +1 -0
  181. package/dist/shared/processed-image-eligibility.d.ts +22 -0
  182. package/dist/shared/processed-image-eligibility.d.ts.map +1 -0
  183. package/dist/shared/project-directory-key.d.ts +16 -0
  184. package/dist/shared/project-directory-key.d.ts.map +1 -1
  185. package/dist/shared/prompt-surface-runtime.d.ts +5 -0
  186. package/dist/shared/prompt-surface-runtime.d.ts.map +1 -1
  187. package/dist/shared/reclaim-distance.d.ts +33 -0
  188. package/dist/shared/reclaim-distance.d.ts.map +1 -0
  189. package/dist/shared/recovery-reasoning-prefix.d.ts +9 -0
  190. package/dist/shared/recovery-reasoning-prefix.d.ts.map +1 -0
  191. package/dist/shared/sqlite.d.ts +35 -1
  192. package/dist/shared/sqlite.d.ts.map +1 -1
  193. package/dist/shared/stable-json.d.ts +7 -0
  194. package/dist/shared/stable-json.d.ts.map +1 -1
  195. package/dist/shared/storage-file-mutex.d.ts +9 -0
  196. package/dist/shared/storage-file-mutex.d.ts.map +1 -0
  197. package/dist/shared/storage-permissions.d.ts +6 -0
  198. package/dist/shared/storage-permissions.d.ts.map +1 -1
  199. package/dist/shared/subagent-temperature.d.ts +46 -0
  200. package/dist/shared/subagent-temperature.d.ts.map +1 -0
  201. package/dist/shared/tag-owner-id.d.ts +1 -0
  202. package/dist/shared/tag-owner-id.d.ts.map +1 -1
  203. package/dist/shared/tag-transcript.d.ts +9 -0
  204. package/dist/shared/tag-transcript.d.ts.map +1 -1
  205. package/dist/shared/tail-hygiene-json.d.ts +22 -0
  206. package/dist/shared/tail-hygiene-json.d.ts.map +1 -0
  207. package/dist/shared/user-facing-codes.d.ts +2 -2
  208. package/dist/shared/window-geometry.d.ts +9 -66
  209. package/dist/shared/window-geometry.d.ts.map +1 -1
  210. package/dist/standalone-plugin-marker.d.ts +8 -0
  211. package/dist/standalone-plugin-marker.d.ts.map +1 -0
  212. package/dist/tools/ctx-expand/search-archive.d.ts +4 -0
  213. package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -0
  214. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  215. package/dist/tools/ctx-reduce/constants.d.ts +1 -2
  216. package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
  217. package/dist/tools/ctx-reduce/tools.d.ts +1 -1
  218. package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
  219. package/dist/v2/hidden-completion.d.ts +20 -1
  220. package/dist/v2/hidden-completion.d.ts.map +1 -1
  221. package/dist/v2/hooks/context.d.ts +1 -1
  222. package/dist/v2/hooks/context.d.ts.map +1 -1
  223. package/dist/v2/hooks/copied-tag-notation.d.ts +19 -0
  224. package/dist/v2/hooks/copied-tag-notation.d.ts.map +1 -0
  225. package/dist/v2/hooks/generate.d.ts +9 -2
  226. package/dist/v2/hooks/generate.d.ts.map +1 -1
  227. package/dist/v2/hooks/hidden-child.d.ts +22 -1
  228. package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
  229. package/dist/v2/hooks/payload.d.ts.map +1 -1
  230. package/dist/v2/hooks/system-prompt-response.d.ts +5 -0
  231. package/dist/v2/hooks/system-prompt-response.d.ts.map +1 -0
  232. package/dist/v2/server.d.ts.map +1 -1
  233. package/dist/v2/server.js +26593 -21940
  234. package/dist/v2/store-reader.d.ts +27 -1
  235. package/dist/v2/store-reader.d.ts.map +1 -1
  236. package/dist/web/main.js +401 -23
  237. package/dist/{wrapup-orchestrator-yknks3ad.js → wrapup-orchestrator-b2gswatj.js} +10 -10
  238. package/package.json +1 -1
  239. package/src/config/deprecated-config.ts +18 -0
  240. package/src/config/index.test.ts +13 -37
  241. package/src/config/index.ts +2 -2
  242. package/src/config/latch-permanence-guard.test.ts +4 -0
  243. package/src/config/project-security.test.ts +17 -0
  244. package/src/config/project-security.ts +3 -3
  245. package/src/config/schema/magic-context.test.ts +50 -0
  246. package/src/config/schema/magic-context.ts +44 -7
  247. package/src/shared/auto-search-lifecycle.ts +43 -0
  248. package/src/shared/logger.ts +10 -2
  249. package/src/shared/model-suggestion-retry.ts +5 -1
  250. package/src/shared/models-dev-cache.ts +1 -7
  251. package/src/shared/models-dev-temperature.ts +107 -0
  252. package/src/shared/processed-image-eligibility.test.ts +68 -0
  253. package/src/shared/processed-image-eligibility.ts +41 -0
  254. package/src/shared/processed-image-position-review.json +20 -0
  255. package/src/shared/processed-image-position-review.test.ts +19 -0
  256. package/src/shared/project-directory-key.ts +20 -0
  257. package/src/shared/prompt-surface-runtime.ts +12 -0
  258. package/src/shared/reclaim-distance.test.ts +111 -0
  259. package/src/shared/reclaim-distance.ts +91 -0
  260. package/src/shared/recovery-reasoning-prefix.test.ts +34 -0
  261. package/src/shared/recovery-reasoning-prefix.ts +35 -0
  262. package/src/shared/root-async-context.probe.mjs +36 -0
  263. package/src/shared/root-async-context.test.ts +49 -0
  264. package/src/shared/sqlite-acquisition-routing.test.ts +4 -3
  265. package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
  266. package/src/shared/sqlite-foreground-wait.test.ts +304 -0
  267. package/src/shared/sqlite-pass-scope.test.ts +4 -4
  268. package/src/shared/sqlite-writer-diagnostics.test.ts +52 -12
  269. package/src/shared/sqlite.ts +428 -46
  270. package/src/shared/stable-json.ts +79 -7
  271. package/src/shared/storage-file-mutex.test.ts +50 -0
  272. package/src/shared/storage-file-mutex.ts +130 -0
  273. package/src/shared/storage-permissions.ts +46 -0
  274. package/src/shared/subagent-temperature.ts +110 -0
  275. package/src/shared/tag-owner-id.ts +13 -0
  276. package/src/shared/tag-transcript.ts +118 -0
  277. package/src/shared/tail-hygiene-json.test.ts +117 -0
  278. package/src/shared/tail-hygiene-json.ts +217 -0
  279. package/src/shared/user-facing-codes.ts +2 -2
  280. package/src/shared/window-geometry.test.ts +102 -236
  281. package/src/shared/window-geometry.ts +23 -413
  282. package/dist/index-ramz5kkm.js +0 -514
  283. package/dist/model-resolution-wazv26n0.js +0 -6
  284. package/dist/recomp-orchestrator-frmwj9hz.js +0 -21
  285. package/src/shared/__fixtures__/window-overlay.v1.json +0 -337
@@ -0,0 +1,291 @@
1
+ import { expect, test } from "bun:test";
2
+ import { rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ Database,
7
+ guardSqliteTransformPass,
8
+ SqliteAcquisitionBusyError,
9
+ withSqliteBackgroundWriter,
10
+ withSqliteTransformPass,
11
+ } from "./sqlite";
12
+ import { createTestTempDirFromPath } from "./test-temp-dir";
13
+
14
+ function fixture() {
15
+ const dir = createTestTempDirFromPath(join(tmpdir(), "mc-busy-review-"));
16
+ const path = join(dir, "context.db");
17
+ const db = new Database(path);
18
+ db.exec("PRAGMA busy_timeout=5000; CREATE TABLE result(id INTEGER PRIMARY KEY, n INTEGER)");
19
+ db.exec("INSERT INTO result VALUES (1, 0)");
20
+ return {
21
+ db,
22
+ path,
23
+ close() {
24
+ db.close();
25
+ rmSync(dir, { recursive: true, force: true });
26
+ },
27
+ };
28
+ }
29
+
30
+ async function lock(path: string, mode: "read" | "write" | "exclusive", holdMs: number) {
31
+ const child = Bun.spawn(
32
+ [
33
+ "bun",
34
+ "--eval",
35
+ `import { Database } from "bun:sqlite";
36
+ const db = new Database(process.argv[1]);
37
+ db.exec(process.argv[2] === "read" ? "BEGIN; SELECT * FROM result"
38
+ : process.argv[2] === "write" ? "BEGIN IMMEDIATE" : "BEGIN EXCLUSIVE");
39
+ console.log("LOCKED");
40
+ Bun.sleepSync(Number(process.argv[3]));
41
+ db.exec("COMMIT");
42
+ db.close();`,
43
+ path,
44
+ mode,
45
+ String(holdMs),
46
+ ],
47
+ { stdout: "pipe", stderr: "pipe", windowsHide: true },
48
+ );
49
+ const reader = child.stdout.getReader();
50
+ let output = "";
51
+ while (!output.includes("LOCKED\n")) {
52
+ const chunk = await reader.read();
53
+ if (chunk.done) {
54
+ throw new Error(`locker failed: ${await new Response(child.stderr).text()}`);
55
+ }
56
+ output += new TextDecoder().decode(chunk.value);
57
+ }
58
+ reader.releaseLock();
59
+ return { exited: child.exited };
60
+ }
61
+
62
+ for (const method of ["run", "get", "all", "exec"] as const) {
63
+ test(`a rollback-journal commit BUSY retries ${method} exactly once after the reader leaves`, async () => {
64
+ const { db, path, close } = fixture();
65
+ const sql = "UPDATE result SET n = n + 1 WHERE id = 1";
66
+ const statement = db.prepare(`${sql} RETURNING n`);
67
+ const { exited } = await lock(path, "read", 150);
68
+ try {
69
+ await withSqliteTransformPass(() => {
70
+ if (method === "exec") db.exec(sql);
71
+ else statement[method]();
72
+ });
73
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([{ id: 1, n: 1 }]);
74
+ expect(db.inTransaction).toBe(false);
75
+ } finally {
76
+ expect(await exited).toBe(0);
77
+ close();
78
+ }
79
+ });
80
+ }
81
+
82
+ for (const scope of ["foreground", "background", "unscoped"] as const) {
83
+ test(`exec in ${scope} does not wait on an unrelated attached database schema`, async () => {
84
+ const main = fixture();
85
+ const aux = fixture();
86
+ main.db.prepare("ATTACH DATABASE ? AS aux").run(aux.path);
87
+ const { exited } = await lock(aux.path, "exclusive", 2500);
88
+ try {
89
+ const started = performance.now();
90
+ const operation = () => main.db.exec("UPDATE main.result SET n = n + 1");
91
+ if (scope === "foreground") {
92
+ await withSqliteTransformPass(() => {
93
+ guardSqliteTransformPass({ assert() {}, remainingMs: () => 75 });
94
+ operation();
95
+ });
96
+ } else if (scope === "background") withSqliteBackgroundWriter(operation);
97
+ else operation();
98
+ expect(performance.now() - started).toBeLessThan(1500);
99
+ expect(main.db.prepare("SELECT n FROM main.result").get()).toEqual({ n: 1 });
100
+ } finally {
101
+ expect(await exited).toBe(0);
102
+ main.close();
103
+ aux.close();
104
+ }
105
+ }, 10_000);
106
+ }
107
+
108
+ for (const location of ["main", "aux"] as const) {
109
+ test(`BUSY retries leave single-statement effects atomic in ${location}, including AFTER triggers`, async () => {
110
+ const main = fixture();
111
+ const aux = fixture();
112
+ main.db.prepare("ATTACH DATABASE ? AS aux").run(aux.path);
113
+ main.db.exec("CREATE TABLE audit(n INTEGER)");
114
+ main.db.exec(`CREATE TEMP TRIGGER audit_insert AFTER INSERT ON ${location}.result
115
+ BEGIN INSERT INTO audit VALUES (new.n); END`);
116
+ main.db.exec(`CREATE TEMP TRIGGER audit_update AFTER UPDATE ON ${location}.result
117
+ BEGIN INSERT INTO audit VALUES (new.n); END`);
118
+ main.db.exec(`CREATE TEMP TRIGGER audit_delete AFTER DELETE ON ${location}.result
119
+ BEGIN INSERT INTO audit VALUES (old.n); END`);
120
+ const forms = [
121
+ `INSERT INTO ${location}.result VALUES (2, 10)`,
122
+ `INSERT OR IGNORE INTO ${location}.result VALUES (2, 20)`,
123
+ `INSERT OR REPLACE INTO ${location}.result VALUES (2, 30)`,
124
+ `INSERT INTO ${location}.result VALUES (2, 40) ON CONFLICT(id) DO UPDATE SET n=n+1`,
125
+ `UPDATE ${location}.result SET n=n+1 WHERE id=2`,
126
+ `DELETE FROM ${location}.result WHERE id=2`,
127
+ ];
128
+ try {
129
+ for (const sql of forms) {
130
+ const statement = main.db.prepare(`${sql} RETURNING *`);
131
+ const blocker = new Database(location === "main" ? main.path : aux.path);
132
+ blocker.exec("BEGIN IMMEDIATE");
133
+ main.db.exec("PRAGMA busy_timeout=0");
134
+ const before = main.db
135
+ .prepare(`SELECT * FROM ${location}.result ORDER BY id`)
136
+ .all();
137
+ const beforeAudit = main.db.prepare("SELECT * FROM audit").all();
138
+ try {
139
+ expect(() => statement.all()).toThrow("locked");
140
+ expect(
141
+ main.db.prepare(`SELECT * FROM ${location}.result ORDER BY id`).all(),
142
+ ).toEqual(before);
143
+ expect(main.db.prepare("SELECT * FROM audit").all()).toEqual(beforeAudit);
144
+ expect(main.db.inTransaction).toBe(false);
145
+ } finally {
146
+ blocker.exec("ROLLBACK");
147
+ blocker.close();
148
+ }
149
+ const { exited } = await lock(
150
+ location === "main" ? main.path : aux.path,
151
+ "write",
152
+ 60,
153
+ );
154
+ try {
155
+ await withSqliteTransformPass(() => statement.all());
156
+ } finally {
157
+ expect(await exited).toBe(0);
158
+ }
159
+ }
160
+ expect(main.db.prepare(`SELECT * FROM ${location}.result ORDER BY id`).all()).toEqual([
161
+ { id: 1, n: 0 },
162
+ ]);
163
+ expect(main.db.prepare("SELECT * FROM audit").all()).toEqual([
164
+ { n: 10 },
165
+ { n: 30 },
166
+ { n: 31 },
167
+ { n: 32 },
168
+ { n: 32 },
169
+ ]);
170
+ } finally {
171
+ main.close();
172
+ aux.close();
173
+ }
174
+ }, 10_000);
175
+ }
176
+
177
+ test("an attached DML-only committed prefix is not replayed after a later BUSY", () => {
178
+ const main = fixture();
179
+ const free = fixture();
180
+ const blocked = fixture();
181
+ try {
182
+ main.db.prepare("ATTACH DATABASE ? AS free").run(free.path);
183
+ main.db.prepare("ATTACH DATABASE ? AS blocked").run(blocked.path);
184
+ blocked.db.exec("BEGIN IMMEDIATE");
185
+ withSqliteTransformPass(() => {
186
+ expect(() =>
187
+ main.db.exec("UPDATE free.result SET n=n+1; UPDATE blocked.result SET n=n+1"),
188
+ ).toThrow(SqliteAcquisitionBusyError);
189
+ });
190
+ expect(main.db.prepare("SELECT n FROM free.result").get()).toEqual({ n: 1 });
191
+ expect(main.db.prepare("SELECT n FROM blocked.result").get()).toEqual({ n: 0 });
192
+ } finally {
193
+ blocked.db.exec("ROLLBACK");
194
+ main.close();
195
+ free.close();
196
+ blocked.close();
197
+ }
198
+ });
199
+
200
+ for (const location of ["main", "free"] as const) {
201
+ test(`an ignored AUTOINCREMENT prefix in ${location} is committed only once before a later BUSY`, () => {
202
+ const main = fixture();
203
+ const free = fixture();
204
+ const blocked = fixture();
205
+ try {
206
+ main.db.prepare("ATTACH DATABASE ? AS free").run(free.path);
207
+ main.db.exec(`CREATE TABLE ${location}.numbered(id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT UNIQUE);
208
+ INSERT INTO ${location}.numbered(value) VALUES ('existing')`);
209
+ main.db.prepare("ATTACH DATABASE ? AS blocked").run(blocked.path);
210
+ blocked.db.exec("BEGIN IMMEDIATE");
211
+ const before = main.db.prepare("SELECT total_changes() AS n").get();
212
+ withSqliteTransformPass(() => {
213
+ guardSqliteTransformPass({ assert() {}, remainingMs: () => 50 });
214
+ expect(() =>
215
+ main.db.exec(
216
+ `INSERT OR IGNORE INTO ${location}.numbered(value) VALUES ('existing'); UPDATE blocked.result SET n=n+1`,
217
+ ),
218
+ ).toThrow(SqliteAcquisitionBusyError);
219
+ });
220
+ expect(main.db.prepare("SELECT total_changes() AS n").get()).toEqual(before);
221
+ expect(
222
+ main.db
223
+ .prepare(`SELECT seq FROM ${location}.sqlite_sequence WHERE name='numbered'`)
224
+ .get(),
225
+ ).toEqual({ seq: 2 });
226
+ expect(main.db.prepare(`SELECT * FROM ${location}.numbered`).all()).toEqual([
227
+ { id: 1, value: "existing" },
228
+ ]);
229
+ } finally {
230
+ blocked.db.exec("ROLLBACK");
231
+ main.close();
232
+ free.close();
233
+ blocked.close();
234
+ }
235
+ });
236
+ }
237
+
238
+ test("a WAL BUSY_SNAPSHOT in an explicit read transaction is not retried", () => {
239
+ const { db, path, close } = fixture();
240
+ db.exec("PRAGMA journal_mode=WAL");
241
+ const other = new Database(path);
242
+ try {
243
+ db.exec("BEGIN; SELECT * FROM result");
244
+ other.exec("UPDATE result SET n=n+1");
245
+ withSqliteTransformPass(() => {
246
+ expect(() => db.prepare("UPDATE result SET n=n+10").run()).toThrow("locked");
247
+ });
248
+ expect(db.inTransaction).toBe(true);
249
+ db.exec("ROLLBACK");
250
+ expect(db.prepare("SELECT n FROM result").get()).toEqual({ n: 1 });
251
+ } finally {
252
+ if (db.inTransaction) db.exec("ROLLBACK");
253
+ other.close();
254
+ close();
255
+ }
256
+ });
257
+
258
+ test("a BUSY from an AFTER trigger writing an attached database rolls back the outer statement", async () => {
259
+ const main = fixture();
260
+ const aux = fixture();
261
+ main.db.prepare("ATTACH DATABASE ? AS aux").run(aux.path);
262
+ // The TEMP trigger resolves an unqualified table in main first; give the
263
+ // attached target its own name so the trigger must write the other file.
264
+ main.db.exec("ALTER TABLE aux.result RENAME TO attached_result");
265
+ main.db.exec(`CREATE TEMP TRIGGER update_aux AFTER UPDATE ON main.result
266
+ BEGIN UPDATE attached_result SET n=n+1 WHERE id=1; END`);
267
+ const statement = main.db.prepare("UPDATE main.result SET n=n+1 RETURNING n");
268
+ const blocker = new Database(aux.path);
269
+ try {
270
+ blocker.exec("BEGIN IMMEDIATE");
271
+ main.db.exec("PRAGMA busy_timeout=0");
272
+ expect(() => statement.all()).toThrow("locked");
273
+ expect(main.db.prepare("SELECT n FROM main.result").get()).toEqual({ n: 0 });
274
+ expect(main.db.prepare("SELECT n FROM aux.attached_result").get()).toEqual({ n: 0 });
275
+ expect(main.db.inTransaction).toBe(false);
276
+ blocker.exec("ROLLBACK");
277
+ const { exited } = await lock(aux.path, "write", 100);
278
+ try {
279
+ await withSqliteTransformPass(() => statement.all());
280
+ } finally {
281
+ expect(await exited).toBe(0);
282
+ }
283
+ expect(main.db.prepare("SELECT n FROM main.result").get()).toEqual({ n: 1 });
284
+ expect(main.db.prepare("SELECT n FROM aux.attached_result").get()).toEqual({ n: 1 });
285
+ } finally {
286
+ if (blocker.inTransaction) blocker.exec("ROLLBACK");
287
+ blocker.close();
288
+ main.close();
289
+ aux.close();
290
+ }
291
+ });
@@ -0,0 +1,304 @@
1
+ import { expect, spyOn, test } from "bun:test";
2
+ import { rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { log } from "./logger";
6
+ import {
7
+ Database,
8
+ guardSqliteTransformPass,
9
+ registerSqliteDiagnosticSink,
10
+ SqliteAcquisitionBusyError,
11
+ withPrivilegedWriter,
12
+ withSqliteTransformPass,
13
+ } from "./sqlite";
14
+ import { startSqliteWriteLocker } from "./sqlite-write-locker-test-support";
15
+ import { createTestTempDirFromPath } from "./test-temp-dir";
16
+
17
+ function fixture() {
18
+ const dir = createTestTempDirFromPath(join(tmpdir(), "mc-foreground-wait-"));
19
+ const path = join(dir, "context.db");
20
+ const db = new Database(path);
21
+ db.exec(`
22
+ PRAGMA journal_mode=WAL;
23
+ PRAGMA busy_timeout=5000;
24
+ CREATE TABLE context_privilege_state(id INTEGER PRIMARY KEY, enabled INTEGER);
25
+ CREATE TABLE result(value TEXT);
26
+ `);
27
+ return {
28
+ db,
29
+ path,
30
+ close() {
31
+ db.close();
32
+ rmSync(dir, { recursive: true, force: true });
33
+ },
34
+ };
35
+ }
36
+
37
+ // Upstream also drives two cases through the OC1 messages-transform handler
38
+ // (1 s hold served, 2.5 s hold refused with StorageBusyRefusalError). That
39
+ // adapter is deleted in this fork; the in-pass acquisition-wait behavior those
40
+ // cases cover is exercised here against the core primitives instead, and the
41
+ // 1 s/2.5 s boundaries are additionally covered by scripts/smoke-node-sqlite.ts.
42
+ for (const mode of ["BEGIN", "autocommit"]) {
43
+ test(`foreground ${mode} wait is capped by the pass deadline`, () => {
44
+ const { db, path, close } = fixture();
45
+ const blocker = new Database(path);
46
+ blocker.exec("BEGIN IMMEDIATE");
47
+ const diagnostics: string[] = [];
48
+ registerSqliteDiagnosticSink((line) => diagnostics.push(line));
49
+ try {
50
+ const started = performance.now();
51
+ withSqliteTransformPass(() => {
52
+ guardSqliteTransformPass({ assert() {}, remainingMs: () => 100 });
53
+ expect(() => {
54
+ if (mode === "BEGIN") withPrivilegedWriter(db, () => undefined);
55
+ else db.prepare("INSERT INTO result VALUES ('late')").run();
56
+ }).toThrow(SqliteAcquisitionBusyError);
57
+ });
58
+ expect(performance.now() - started).toBeGreaterThanOrEqual(75);
59
+ expect(performance.now() - started).toBeLessThan(700);
60
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([]);
61
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
62
+ } finally {
63
+ registerSqliteDiagnosticSink(log);
64
+ blocker.exec("ROLLBACK");
65
+ blocker.close();
66
+ close();
67
+ }
68
+ });
69
+ }
70
+
71
+ // Fork shape: upstream drives the "slow uncontended SQL preserves admission"
72
+ // pair through the OC1 messages-transform handler (deleted in this fork — see
73
+ // the note above); the lease invariant it protects is asserted here against
74
+ // the core primitives instead.
75
+ test("uncontended autocommit work does not consume the foreground wait budget", async () => {
76
+ const { db, path, close } = fixture();
77
+ const blocker = new Database(path);
78
+ const timeouts: number[] = [];
79
+ const nativeExec = db.exec.bind(db);
80
+ const exec = spyOn(db, "exec").mockImplementation((sql) => {
81
+ const timeout = /^PRAGMA busy_timeout=(\d+)$/.exec(sql);
82
+ if (timeout) timeouts.push(Number(timeout[1]));
83
+ return nativeExec(sql);
84
+ });
85
+ try {
86
+ await withSqliteTransformPass(async () => {
87
+ guardSqliteTransformPass({ assert() {}, remainingMs: () => 30_000 });
88
+ // Spend ~1.5s inside successful, uncontended SQL. Recursive CTEs
89
+ // keep this independent of CPU speed and UDF APIs.
90
+ const cte =
91
+ "WITH RECURSIVE n(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM n WHERE x<1000000)";
92
+ const slowWrite = db.prepare(
93
+ `${cte} INSERT INTO result SELECT sum(x) FROM n RETURNING value`,
94
+ );
95
+ const slowRead = db.prepare(`${cte} SELECT sum(x) FROM n`);
96
+ const started = performance.now();
97
+ do {
98
+ slowWrite.all();
99
+ slowRead.all();
100
+ } while (performance.now() - started < 1500);
101
+ // The contended retry must still see (nearly) the full budget.
102
+ blocker.exec("BEGIN IMMEDIATE");
103
+ timeouts.length = 0;
104
+ expect(() => db.prepare("INSERT INTO result VALUES ('blocked')").run()).toThrow(
105
+ SqliteAcquisitionBusyError,
106
+ );
107
+ });
108
+ expect(timeouts[0]).toBe(0);
109
+ expect(timeouts[1]).toBeGreaterThanOrEqual(1900);
110
+ } finally {
111
+ exec.mockRestore();
112
+ if (blocker.inTransaction) blocker.exec("ROLLBACK");
113
+ blocker.close();
114
+ close();
115
+ }
116
+ }, 30_000);
117
+
118
+ test("BUSY autocommit probes do not double-apply INSERT OR IGNORE, UPSERT or RETURNING", async () => {
119
+ const { db, path, close } = fixture();
120
+ db.exec(
121
+ "CREATE TABLE counter(id INTEGER PRIMARY KEY, n INTEGER); INSERT INTO counter VALUES (1, 0)",
122
+ );
123
+ const forms = [
124
+ ["INSERT OR IGNORE INTO counter VALUES (2, 1)", "run"],
125
+ ["INSERT INTO counter VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET n = n + 1", "run"],
126
+ ["UPDATE counter SET n = n + 1 WHERE id = 1 RETURNING n", "get"],
127
+ [
128
+ "INSERT INTO counter VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET n = n + 1 RETURNING n",
129
+ "all",
130
+ ],
131
+ ] as const;
132
+ try {
133
+ for (const [sql, method] of forms) {
134
+ const blocker = new Database(path);
135
+ blocker.exec("BEGIN IMMEDIATE");
136
+ db.exec("PRAGMA busy_timeout=0");
137
+ const before = db.prepare("SELECT * FROM counter ORDER BY id").all();
138
+ try {
139
+ expect(() => db.prepare(sql)[method]()).toThrow("locked");
140
+ expect(db.prepare("SELECT * FROM counter ORDER BY id").all()).toEqual(before);
141
+ expect(db.inTransaction).toBe(false);
142
+ } finally {
143
+ blocker.exec("ROLLBACK");
144
+ blocker.close();
145
+ }
146
+ const locker = await startSqliteWriteLocker(path, 100);
147
+ try {
148
+ await withSqliteTransformPass(() => db.prepare(sql)[method]());
149
+ } finally {
150
+ expect(await locker.exited).toBe(0);
151
+ }
152
+ }
153
+ expect(db.prepare("SELECT * FROM counter ORDER BY id").all()).toEqual([
154
+ { id: 1, n: 3 },
155
+ { id: 2, n: 1 },
156
+ ]);
157
+ } finally {
158
+ close();
159
+ }
160
+ }, 30_000);
161
+
162
+ test("many contended autocommit writes share at most two seconds of lock waiting", async () => {
163
+ const { db, path, close } = fixture();
164
+ const blocker = new Database(path);
165
+ blocker.exec("BEGIN IMMEDIATE");
166
+ const timeouts: number[] = [];
167
+ const nativeExec = db.exec.bind(db);
168
+ const exec = spyOn(db, "exec").mockImplementation((sql) => {
169
+ const timeout = /^PRAGMA busy_timeout=(\d+)$/.exec(sql);
170
+ if (timeout) timeouts.push(Number(timeout[1]));
171
+ return nativeExec(sql);
172
+ });
173
+ try {
174
+ await withSqliteTransformPass(() => {
175
+ // Use real BUSY failures but a controlled accounting clock: each
176
+ // retry costs 400ms regardless of runner scheduling. The deadline
177
+ // keeps the real waits short without hiding lease exhaustion.
178
+ guardSqliteTransformPass({ assert() {}, remainingMs: () => 10 });
179
+ let now = 0;
180
+ const clock = spyOn(performance, "now").mockImplementation(() => (now += 400));
181
+ try {
182
+ for (let i = 0; i < 8; i++) {
183
+ expect(() => db.prepare("INSERT INTO result VALUES ('one')").run()).toThrow(
184
+ SqliteAcquisitionBusyError,
185
+ );
186
+ }
187
+ } finally {
188
+ clock.mockRestore();
189
+ }
190
+ // Five charged retries exhaust 2000ms. All later retries must have
191
+ // zero timeout even though the deadline still offers another 10ms.
192
+ expect(timeouts).toEqual([
193
+ ...Array.from({ length: 5 }, () => [0, 10, 5000]).flat(),
194
+ ...Array.from({ length: 3 }, () => [0, 0, 5000]).flat(),
195
+ ]);
196
+ blocker.exec("ROLLBACK");
197
+ db.prepare("INSERT INTO result VALUES ('uncontended')").run();
198
+ });
199
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([{ value: "uncontended" }]);
200
+ } finally {
201
+ exec.mockRestore();
202
+ if (blocker.inTransaction) blocker.exec("ROLLBACK");
203
+ blocker.close();
204
+ close();
205
+ }
206
+ }, 30_000);
207
+
208
+ test("an exec script charges its single attempt without a zero-wait probe", () => {
209
+ const { db, path, close } = fixture();
210
+ const blocker = new Database(path);
211
+ const timeouts: number[] = [];
212
+ const nativeExec = db.exec.bind(db);
213
+ const exec = spyOn(db, "exec").mockImplementation((sql) => {
214
+ const timeout = /^PRAGMA busy_timeout=(\d+)$/.exec(sql);
215
+ if (timeout) timeouts.push(Number(timeout[1]));
216
+ return nativeExec(sql);
217
+ });
218
+ try {
219
+ withSqliteTransformPass(() => {
220
+ let now = 0;
221
+ const clock = spyOn(performance, "now").mockImplementation(() => (now += 2500));
222
+ try {
223
+ db.exec(
224
+ "INSERT INTO result VALUES ('prefix'); INSERT INTO result VALUES ('suffix')",
225
+ );
226
+ } finally {
227
+ clock.mockRestore();
228
+ }
229
+ blocker.exec("BEGIN IMMEDIATE");
230
+ expect(() => db.prepare("INSERT INTO result VALUES ('blocked')").run()).toThrow(
231
+ SqliteAcquisitionBusyError,
232
+ );
233
+ });
234
+ expect(timeouts).toEqual([2000, 5000, 0, 0, 5000]);
235
+ expect(db.prepare("SELECT * FROM result").all()).toEqual([
236
+ { value: "prefix" },
237
+ { value: "suffix" },
238
+ ]);
239
+ } finally {
240
+ exec.mockRestore();
241
+ if (blocker.inTransaction) blocker.exec("ROLLBACK");
242
+ blocker.close();
243
+ close();
244
+ }
245
+ });
246
+
247
+ test("an explicit transaction keeps its timeout and never probes or retries", () => {
248
+ const { db, path, close } = fixture();
249
+ const other = new Database(path);
250
+ db.exec("PRAGMA busy_timeout=0; BEGIN; SELECT * FROM result");
251
+ other.exec("INSERT INTO result VALUES ('other')");
252
+ const exec = spyOn(db, "exec");
253
+ try {
254
+ withSqliteTransformPass(() => {
255
+ expect(() => db.prepare("INSERT INTO result VALUES ('stale')").run()).toThrow("locked");
256
+ });
257
+ expect(exec).not.toHaveBeenCalled();
258
+ expect(db.inTransaction).toBe(true);
259
+ expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 0 });
260
+ } finally {
261
+ exec.mockRestore();
262
+ if (db.inTransaction) db.exec("ROLLBACK");
263
+ other.close();
264
+ close();
265
+ }
266
+ });
267
+
268
+ test("an autocommit exec script never replays a committed prefix after BUSY", () => {
269
+ const main = fixture();
270
+ const auxiliary = fixture();
271
+ const free = fixture();
272
+ try {
273
+ main.db.prepare("ATTACH DATABASE ? AS aux").run(auxiliary.path);
274
+ main.db.prepare("ATTACH DATABASE ? AS free").run(free.path);
275
+ auxiliary.db.exec("BEGIN IMMEDIATE");
276
+ withSqliteTransformPass(() => {
277
+ guardSqliteTransformPass({ assert() {}, remainingMs: () => 100 });
278
+ expect(() =>
279
+ main.db.exec(
280
+ "INSERT INTO main.result VALUES ('prefix'); INSERT INTO aux.result VALUES ('blocked')",
281
+ ),
282
+ ).toThrow(SqliteAcquisitionBusyError);
283
+ expect(() =>
284
+ main.db.exec(
285
+ "CREATE TABLE applied_once(id); INSERT INTO aux.result VALUES ('blocked')",
286
+ ),
287
+ ).toThrow(SqliteAcquisitionBusyError);
288
+ expect(() =>
289
+ main.db.exec(
290
+ "CREATE TABLE free.applied_once(id); INSERT INTO aux.result VALUES ('blocked')",
291
+ ),
292
+ ).toThrow(SqliteAcquisitionBusyError);
293
+ });
294
+ expect(main.db.prepare("SELECT * FROM main.result").all()).toEqual([{ value: "prefix" }]);
295
+ expect(
296
+ main.db.prepare("SELECT name FROM sqlite_schema WHERE name='applied_once'").all(),
297
+ ).toHaveLength(1);
298
+ } finally {
299
+ auxiliary.db.exec("ROLLBACK");
300
+ auxiliary.close();
301
+ main.close();
302
+ free.close();
303
+ }
304
+ });
@@ -100,7 +100,7 @@ test("foreground admission keeps the loop ticking while a separate writer holds
100
100
  }
101
101
  }, 30000);
102
102
 
103
- test("a foreground in-pass writer has bounded 250ms tolerance and restores its timeout", () => {
103
+ test("a foreground in-pass writer has bounded 2000ms tolerance and restores its timeout", () => {
104
104
  const { db, path, close } = fixture();
105
105
  const blocker = new Database(path);
106
106
  blocker.exec("BEGIN IMMEDIATE");
@@ -109,15 +109,15 @@ test("a foreground in-pass writer has bounded 250ms tolerance and restores its t
109
109
  expect(() =>
110
110
  withSqliteTransformPass(() => withPrivilegedWriter(db, () => undefined)),
111
111
  ).toThrow(SqliteAcquisitionBusyError);
112
- expect(performance.now() - start).toBeGreaterThanOrEqual(200);
113
- expect(performance.now() - start).toBeLessThan(400);
112
+ expect(performance.now() - start).toBeGreaterThanOrEqual(1900);
113
+ expect(performance.now() - start).toBeLessThan(2500);
114
114
  expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 5000 });
115
115
  } finally {
116
116
  blocker.exec("ROLLBACK");
117
117
  blocker.close();
118
118
  close();
119
119
  }
120
- });
120
+ }, 30000);
121
121
 
122
122
  test("ready background publication retries on a timer and retains its write", async () => {
123
123
  const { db, path, close } = fixture();