@roj-ai/sdk 0.1.35 → 0.2.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 (908) hide show
  1. package/dist/bootstrap.d.ts +420 -26
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/bootstrap.js +55 -24
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/bun-platform/fs.d.ts.map +1 -1
  6. package/dist/bun-platform/fs.js +1 -0
  7. package/dist/bun-platform/fs.js.map +1 -1
  8. package/dist/bun-platform/index.d.ts +4 -1
  9. package/dist/bun-platform/index.d.ts.map +1 -1
  10. package/dist/bun-platform/index.js +7 -1
  11. package/dist/bun-platform/index.js.map +1 -1
  12. package/dist/bun-platform/server-lifecycle.d.ts +30 -0
  13. package/dist/bun-platform/server-lifecycle.d.ts.map +1 -0
  14. package/dist/bun-platform/server-lifecycle.js +182 -0
  15. package/dist/bun-platform/server-lifecycle.js.map +1 -0
  16. package/dist/bun-platform/shell.d.ts +33 -0
  17. package/dist/bun-platform/shell.d.ts.map +1 -0
  18. package/dist/bun-platform/shell.js +322 -0
  19. package/dist/bun-platform/shell.js.map +1 -0
  20. package/dist/config.d.ts +34 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +97 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/core/agents/agent.d.ts +42 -7
  25. package/dist/core/agents/agent.d.ts.map +1 -1
  26. package/dist/core/agents/agent.js +209 -90
  27. package/dist/core/agents/agent.js.map +1 -1
  28. package/dist/core/agents/config.d.ts +8 -1
  29. package/dist/core/agents/config.d.ts.map +1 -1
  30. package/dist/core/agents/config.js +5 -1
  31. package/dist/core/agents/config.js.map +1 -1
  32. package/dist/core/agents/retry.d.ts +1 -1
  33. package/dist/core/agents/retry.d.ts.map +1 -1
  34. package/dist/core/agents/retry.js +14 -16
  35. package/dist/core/agents/retry.js.map +1 -1
  36. package/dist/core/agents/state.d.ts +27 -1
  37. package/dist/core/agents/state.d.ts.map +1 -1
  38. package/dist/core/agents/state.js +1 -1
  39. package/dist/core/agents/state.js.map +1 -1
  40. package/dist/core/context/state.d.ts +3 -3
  41. package/dist/core/context/state.d.ts.map +1 -1
  42. package/dist/core/context/state.js +6 -2
  43. package/dist/core/context/state.js.map +1 -1
  44. package/dist/core/events/base-event-store.d.ts.map +1 -1
  45. package/dist/core/events/base-event-store.js +22 -14
  46. package/dist/core/events/base-event-store.js.map +1 -1
  47. package/dist/core/events/file.d.ts +39 -1
  48. package/dist/core/events/file.d.ts.map +1 -1
  49. package/dist/core/events/file.js +159 -20
  50. package/dist/core/events/file.js.map +1 -1
  51. package/dist/core/events/types.d.ts +1 -1
  52. package/dist/core/file-store/containment.d.ts +19 -0
  53. package/dist/core/file-store/containment.d.ts.map +1 -0
  54. package/dist/core/file-store/containment.js +62 -0
  55. package/dist/core/file-store/containment.js.map +1 -0
  56. package/dist/core/file-store/file-store.d.ts +22 -2
  57. package/dist/core/file-store/file-store.d.ts.map +1 -1
  58. package/dist/core/file-store/file-store.js +88 -20
  59. package/dist/core/file-store/file-store.js.map +1 -1
  60. package/dist/core/file-store/types.d.ts +13 -1
  61. package/dist/core/file-store/types.d.ts.map +1 -1
  62. package/dist/core/image/image-processor.d.ts.map +1 -1
  63. package/dist/core/image/image-processor.js +3 -1
  64. package/dist/core/image/image-processor.js.map +1 -1
  65. package/dist/core/image/types.d.ts +2 -0
  66. package/dist/core/image/types.d.ts.map +1 -1
  67. package/dist/core/image/vips-resizer.d.ts.map +1 -1
  68. package/dist/core/image/vips-resizer.js +81 -33
  69. package/dist/core/image/vips-resizer.js.map +1 -1
  70. package/dist/core/llm/anthropic.d.ts +0 -1
  71. package/dist/core/llm/anthropic.d.ts.map +1 -1
  72. package/dist/core/llm/anthropic.js +101 -74
  73. package/dist/core/llm/anthropic.js.map +1 -1
  74. package/dist/core/llm/logger.d.ts +14 -2
  75. package/dist/core/llm/logger.d.ts.map +1 -1
  76. package/dist/core/llm/logger.js +161 -36
  77. package/dist/core/llm/logger.js.map +1 -1
  78. package/dist/core/llm/openrouter.d.ts +0 -1
  79. package/dist/core/llm/openrouter.d.ts.map +1 -1
  80. package/dist/core/llm/openrouter.js +70 -74
  81. package/dist/core/llm/openrouter.js.map +1 -1
  82. package/dist/core/llm/provider-request.d.ts +10 -0
  83. package/dist/core/llm/provider-request.d.ts.map +1 -0
  84. package/dist/core/llm/provider-request.js +41 -0
  85. package/dist/core/llm/provider-request.js.map +1 -0
  86. package/dist/core/llm/provider.d.ts +45 -3
  87. package/dist/core/llm/provider.d.ts.map +1 -1
  88. package/dist/core/llm/provider.js +26 -0
  89. package/dist/core/llm/provider.js.map +1 -1
  90. package/dist/core/llm/state.d.ts +4 -2
  91. package/dist/core/llm/state.d.ts.map +1 -1
  92. package/dist/core/llm/state.js +15 -4
  93. package/dist/core/llm/state.js.map +1 -1
  94. package/dist/core/plugins/plugin-builder.d.ts +57 -3
  95. package/dist/core/plugins/plugin-builder.d.ts.map +1 -1
  96. package/dist/core/plugins/plugin-builder.js +68 -14
  97. package/dist/core/plugins/plugin-builder.js.map +1 -1
  98. package/dist/core/preset/preset-builder.d.ts +3 -2
  99. package/dist/core/preset/preset-builder.d.ts.map +1 -1
  100. package/dist/core/preset/preset-builder.js +7 -16
  101. package/dist/core/preset/preset-builder.js.map +1 -1
  102. package/dist/core/sessions/context.d.ts +23 -5
  103. package/dist/core/sessions/context.d.ts.map +1 -1
  104. package/dist/core/sessions/runtime-activity.d.ts +33 -0
  105. package/dist/core/sessions/runtime-activity.d.ts.map +1 -0
  106. package/dist/core/sessions/runtime-activity.js +55 -0
  107. package/dist/core/sessions/runtime-activity.js.map +1 -0
  108. package/dist/core/sessions/schema.d.ts +23 -2
  109. package/dist/core/sessions/schema.d.ts.map +1 -1
  110. package/dist/core/sessions/schema.js +24 -3
  111. package/dist/core/sessions/schema.js.map +1 -1
  112. package/dist/core/sessions/session-manager.d.ts +83 -6
  113. package/dist/core/sessions/session-manager.d.ts.map +1 -1
  114. package/dist/core/sessions/session-manager.js +517 -118
  115. package/dist/core/sessions/session-manager.js.map +1 -1
  116. package/dist/core/sessions/session-store.d.ts +18 -0
  117. package/dist/core/sessions/session-store.d.ts.map +1 -1
  118. package/dist/core/sessions/session-store.js +33 -0
  119. package/dist/core/sessions/session-store.js.map +1 -1
  120. package/dist/core/sessions/session.d.ts +42 -6
  121. package/dist/core/sessions/session.d.ts.map +1 -1
  122. package/dist/core/sessions/session.js +176 -44
  123. package/dist/core/sessions/session.js.map +1 -1
  124. package/dist/core/sessions/state.d.ts +10 -1
  125. package/dist/core/sessions/state.d.ts.map +1 -1
  126. package/dist/core/sessions/state.js +16 -1
  127. package/dist/core/sessions/state.js.map +1 -1
  128. package/dist/core/system.d.ts +9 -0
  129. package/dist/core/system.d.ts.map +1 -1
  130. package/dist/core/system.js +4 -1
  131. package/dist/core/system.js.map +1 -1
  132. package/dist/core/tools/executor.d.ts +1 -1
  133. package/dist/core/wake-key.d.ts +34 -0
  134. package/dist/core/wake-key.d.ts.map +1 -0
  135. package/dist/core/wake-key.js +51 -0
  136. package/dist/core/wake-key.js.map +1 -0
  137. package/dist/index.d.ts +24 -13
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +22 -8
  140. package/dist/index.js.map +1 -1
  141. package/dist/lib/archive/archive-inspection.d.ts +48 -0
  142. package/dist/lib/archive/archive-inspection.d.ts.map +1 -0
  143. package/dist/lib/archive/archive-inspection.fixtures.d.ts +7 -0
  144. package/dist/lib/archive/archive-inspection.fixtures.d.ts.map +1 -0
  145. package/dist/lib/archive/archive-inspection.fixtures.js +134 -0
  146. package/dist/lib/archive/archive-inspection.fixtures.js.map +1 -0
  147. package/dist/lib/archive/archive-inspection.js +253 -0
  148. package/dist/lib/archive/archive-inspection.js.map +1 -0
  149. package/dist/lib/archive/index.d.ts +3 -0
  150. package/dist/lib/archive/index.d.ts.map +1 -0
  151. package/dist/lib/archive/index.js +2 -0
  152. package/dist/lib/archive/index.js.map +1 -0
  153. package/dist/lib/logger/console.d.ts +5 -12
  154. package/dist/lib/logger/console.d.ts.map +1 -1
  155. package/dist/lib/logger/console.js +5 -47
  156. package/dist/lib/logger/console.js.map +1 -1
  157. package/dist/lib/logger/file.d.ts +8 -11
  158. package/dist/lib/logger/file.d.ts.map +1 -1
  159. package/dist/lib/logger/file.js +26 -37
  160. package/dist/lib/logger/file.js.map +1 -1
  161. package/dist/lib/logger/index.d.ts +3 -1
  162. package/dist/lib/logger/index.d.ts.map +1 -1
  163. package/dist/lib/logger/index.js +3 -1
  164. package/dist/lib/logger/index.js.map +1 -1
  165. package/dist/lib/logger/jsonl.d.ts +21 -0
  166. package/dist/lib/logger/jsonl.d.ts.map +1 -0
  167. package/dist/lib/logger/jsonl.js +29 -0
  168. package/dist/lib/logger/jsonl.js.map +1 -0
  169. package/dist/lib/logger/logger.d.ts +22 -0
  170. package/dist/lib/logger/logger.d.ts.map +1 -1
  171. package/dist/lib/logger/logger.js +37 -0
  172. package/dist/lib/logger/logger.js.map +1 -1
  173. package/dist/lib/logger/session-log.d.ts +21 -0
  174. package/dist/lib/logger/session-log.d.ts.map +1 -0
  175. package/dist/lib/logger/session-log.js +37 -0
  176. package/dist/lib/logger/session-log.js.map +1 -0
  177. package/dist/lib/utils/concurrency.d.ts +1 -1
  178. package/dist/lib/utils/concurrency.d.ts.map +1 -1
  179. package/dist/lib/utils/concurrency.js +30 -7
  180. package/dist/lib/utils/concurrency.js.map +1 -1
  181. package/dist/lib/utils/fs-batch.d.ts +17 -0
  182. package/dist/lib/utils/fs-batch.d.ts.map +1 -0
  183. package/dist/lib/utils/fs-batch.js +38 -0
  184. package/dist/lib/utils/fs-batch.js.map +1 -0
  185. package/dist/lib/utils/result.d.ts +7 -22
  186. package/dist/lib/utils/result.d.ts.map +1 -1
  187. package/dist/lib/utils/result.js +6 -17
  188. package/dist/lib/utils/result.js.map +1 -1
  189. package/dist/lib/utils/sleep.d.ts +10 -0
  190. package/dist/lib/utils/sleep.d.ts.map +1 -0
  191. package/dist/lib/utils/sleep.js +26 -0
  192. package/dist/lib/utils/sleep.js.map +1 -0
  193. package/dist/platform/fs-revision.d.ts +21 -0
  194. package/dist/platform/fs-revision.d.ts.map +1 -0
  195. package/dist/platform/fs-revision.js +9 -0
  196. package/dist/platform/fs-revision.js.map +1 -0
  197. package/dist/platform/fs.d.ts +104 -1
  198. package/dist/platform/fs.d.ts.map +1 -1
  199. package/dist/platform/git.d.ts +59 -0
  200. package/dist/platform/git.d.ts.map +1 -0
  201. package/dist/platform/git.js +8 -0
  202. package/dist/platform/git.js.map +1 -0
  203. package/dist/platform/index.d.ts +40 -1
  204. package/dist/platform/index.d.ts.map +1 -1
  205. package/dist/platform/index.js +1 -1
  206. package/dist/platform/index.js.map +1 -1
  207. package/dist/platform/llm-call-log.d.ts +80 -0
  208. package/dist/platform/llm-call-log.d.ts.map +1 -0
  209. package/dist/platform/llm-call-log.js +12 -0
  210. package/dist/platform/llm-call-log.js.map +1 -0
  211. package/dist/platform/scheduler.d.ts +32 -0
  212. package/dist/platform/scheduler.d.ts.map +1 -0
  213. package/dist/platform/scheduler.js +46 -0
  214. package/dist/platform/scheduler.js.map +1 -0
  215. package/dist/platform/session-log.d.ts +36 -0
  216. package/dist/platform/session-log.d.ts.map +1 -0
  217. package/dist/platform/session-log.js +12 -0
  218. package/dist/platform/session-log.js.map +1 -0
  219. package/dist/platform/shell.d.ts +83 -0
  220. package/dist/platform/shell.d.ts.map +1 -0
  221. package/dist/platform/shell.js +13 -0
  222. package/dist/platform/shell.js.map +1 -0
  223. package/dist/plugins/agent-status/plugin.d.ts.map +1 -1
  224. package/dist/plugins/agent-status/plugin.js +1 -0
  225. package/dist/plugins/agent-status/plugin.js.map +1 -1
  226. package/dist/plugins/agents/plugin.d.ts.map +1 -1
  227. package/dist/plugins/agents/plugin.js +129 -62
  228. package/dist/plugins/agents/plugin.js.map +1 -1
  229. package/dist/plugins/filesystem/listing.d.ts +3 -0
  230. package/dist/plugins/filesystem/listing.d.ts.map +1 -1
  231. package/dist/plugins/filesystem/listing.js +66 -10
  232. package/dist/plugins/filesystem/listing.js.map +1 -1
  233. package/dist/plugins/filesystem/plugin.d.ts.map +1 -1
  234. package/dist/plugins/filesystem/plugin.js +8 -6
  235. package/dist/plugins/filesystem/plugin.js.map +1 -1
  236. package/dist/plugins/git-status/plugin.d.ts +34 -7
  237. package/dist/plugins/git-status/plugin.d.ts.map +1 -1
  238. package/dist/plugins/git-status/plugin.js +168 -64
  239. package/dist/plugins/git-status/plugin.js.map +1 -1
  240. package/dist/plugins/llm-debug/plugin.d.ts.map +1 -1
  241. package/dist/plugins/llm-debug/plugin.js +1 -0
  242. package/dist/plugins/llm-debug/plugin.js.map +1 -1
  243. package/dist/plugins/logs/plugin.d.ts +1 -4
  244. package/dist/plugins/logs/plugin.d.ts.map +1 -1
  245. package/dist/plugins/logs/plugin.js +7 -0
  246. package/dist/plugins/logs/plugin.js.map +1 -1
  247. package/dist/plugins/mailbox/helpers.d.ts +2 -2
  248. package/dist/plugins/mailbox/helpers.js +1 -1
  249. package/dist/plugins/mailbox/plugin.d.ts.map +1 -1
  250. package/dist/plugins/mailbox/plugin.js +38 -43
  251. package/dist/plugins/mailbox/plugin.js.map +1 -1
  252. package/dist/plugins/mailbox/query.d.ts +7 -2
  253. package/dist/plugins/mailbox/query.d.ts.map +1 -1
  254. package/dist/plugins/mailbox/query.js +14 -7
  255. package/dist/plugins/mailbox/query.js.map +1 -1
  256. package/dist/plugins/mailbox/state.d.ts +5 -4
  257. package/dist/plugins/mailbox/state.d.ts.map +1 -1
  258. package/dist/plugins/mailbox/state.js +6 -5
  259. package/dist/plugins/mailbox/state.js.map +1 -1
  260. package/dist/plugins/resources/filename.d.ts +4 -0
  261. package/dist/plugins/resources/filename.d.ts.map +1 -0
  262. package/dist/plugins/resources/filename.js +18 -0
  263. package/dist/plugins/resources/filename.js.map +1 -0
  264. package/dist/plugins/resources/plugin.d.ts +3 -0
  265. package/dist/plugins/resources/plugin.d.ts.map +1 -1
  266. package/dist/plugins/resources/plugin.js +97 -60
  267. package/dist/plugins/resources/plugin.js.map +1 -1
  268. package/dist/plugins/resources/state.d.ts +1 -1
  269. package/dist/plugins/resources/state.js +1 -1
  270. package/dist/plugins/result-eviction/plugin.d.ts.map +1 -1
  271. package/dist/plugins/result-eviction/plugin.js +6 -1
  272. package/dist/plugins/result-eviction/plugin.js.map +1 -1
  273. package/dist/plugins/services/plugin.d.ts +4 -1
  274. package/dist/plugins/services/plugin.d.ts.map +1 -1
  275. package/dist/plugins/services/plugin.js +251 -100
  276. package/dist/plugins/services/plugin.js.map +1 -1
  277. package/dist/plugins/services/prompt.d.ts.map +1 -1
  278. package/dist/plugins/services/prompt.js +1 -4
  279. package/dist/plugins/services/prompt.js.map +1 -1
  280. package/dist/plugins/services/schema.d.ts +15 -5
  281. package/dist/plugins/services/schema.d.ts.map +1 -1
  282. package/dist/plugins/services/service.d.ts +100 -18
  283. package/dist/plugins/services/service.d.ts.map +1 -1
  284. package/dist/plugins/services/service.js +680 -230
  285. package/dist/plugins/services/service.js.map +1 -1
  286. package/dist/plugins/session-lifecycle/plugin.d.ts +21 -1
  287. package/dist/plugins/session-lifecycle/plugin.d.ts.map +1 -1
  288. package/dist/plugins/session-lifecycle/plugin.js +53 -40
  289. package/dist/plugins/session-lifecycle/plugin.js.map +1 -1
  290. package/dist/plugins/session-state/plugin.d.ts +21 -13
  291. package/dist/plugins/session-state/plugin.d.ts.map +1 -1
  292. package/dist/plugins/session-state/plugin.js +214 -79
  293. package/dist/plugins/session-state/plugin.js.map +1 -1
  294. package/dist/plugins/session-stats/plugin.d.ts.map +1 -1
  295. package/dist/plugins/session-stats/plugin.js +1 -0
  296. package/dist/plugins/session-stats/plugin.js.map +1 -1
  297. package/dist/plugins/shell/executor.d.ts +17 -15
  298. package/dist/plugins/shell/executor.d.ts.map +1 -1
  299. package/dist/plugins/shell/executor.js +130 -195
  300. package/dist/plugins/shell/executor.js.map +1 -1
  301. package/dist/plugins/shell/index.d.ts +2 -2
  302. package/dist/plugins/shell/index.d.ts.map +1 -1
  303. package/dist/plugins/shell/index.js +2 -2
  304. package/dist/plugins/shell/index.js.map +1 -1
  305. package/dist/plugins/shell/plugin.d.ts +5 -4
  306. package/dist/plugins/shell/plugin.d.ts.map +1 -1
  307. package/dist/plugins/shell/plugin.js +4 -8
  308. package/dist/plugins/shell/plugin.js.map +1 -1
  309. package/dist/plugins/skills/discovery.d.ts.map +1 -1
  310. package/dist/plugins/skills/discovery.js +10 -8
  311. package/dist/plugins/skills/discovery.js.map +1 -1
  312. package/dist/plugins/skills/index.d.ts +1 -1
  313. package/dist/plugins/skills/index.d.ts.map +1 -1
  314. package/dist/plugins/skills/plugin.d.ts +1 -1
  315. package/dist/plugins/skills/plugin.d.ts.map +1 -1
  316. package/dist/plugins/skills/plugin.js +23 -30
  317. package/dist/plugins/skills/plugin.js.map +1 -1
  318. package/dist/plugins/skills/prompts.d.ts +3 -3
  319. package/dist/plugins/skills/prompts.d.ts.map +1 -1
  320. package/dist/plugins/skills/prompts.js.map +1 -1
  321. package/dist/plugins/skills/schema.d.ts +4 -2
  322. package/dist/plugins/skills/schema.d.ts.map +1 -1
  323. package/dist/plugins/todo/schema.d.ts +1 -1
  324. package/dist/plugins/uploads/plugin.d.ts +11 -4
  325. package/dist/plugins/uploads/plugin.d.ts.map +1 -1
  326. package/dist/plugins/uploads/plugin.js +891 -225
  327. package/dist/plugins/uploads/plugin.js.map +1 -1
  328. package/dist/plugins/uploads/preprocessor.d.ts +14 -0
  329. package/dist/plugins/uploads/preprocessor.d.ts.map +1 -1
  330. package/dist/plugins/uploads/preprocessor.js +11 -0
  331. package/dist/plugins/uploads/preprocessor.js.map +1 -1
  332. package/dist/plugins/uploads/preprocessors/image-classifier.d.ts +1 -1
  333. package/dist/plugins/uploads/preprocessors/image-classifier.d.ts.map +1 -1
  334. package/dist/plugins/uploads/preprocessors/image-classifier.js +51 -26
  335. package/dist/plugins/uploads/preprocessors/image-classifier.js.map +1 -1
  336. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts +2 -2
  337. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts.map +1 -1
  338. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js +41 -16
  339. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js.map +1 -1
  340. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts +1 -1
  341. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts.map +1 -1
  342. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js +29 -14
  343. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js.map +1 -1
  344. package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts +1 -1
  345. package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts.map +1 -1
  346. package/dist/plugins/uploads/preprocessors/zip-preprocessor.js +62 -34
  347. package/dist/plugins/uploads/preprocessors/zip-preprocessor.js.map +1 -1
  348. package/dist/plugins/uploads/schema.d.ts +25 -2
  349. package/dist/plugins/uploads/schema.d.ts.map +1 -1
  350. package/dist/plugins/uploads/schema.js +24 -3
  351. package/dist/plugins/uploads/schema.js.map +1 -1
  352. package/dist/plugins/uploads/state.d.ts +13 -1
  353. package/dist/plugins/uploads/state.d.ts.map +1 -1
  354. package/dist/plugins/uploads/state.js +4 -1
  355. package/dist/plugins/uploads/state.js.map +1 -1
  356. package/dist/plugins/user-chat/plugin.d.ts +12 -5
  357. package/dist/plugins/user-chat/plugin.d.ts.map +1 -1
  358. package/dist/plugins/user-chat/plugin.js +141 -32
  359. package/dist/plugins/user-chat/plugin.js.map +1 -1
  360. package/dist/plugins/workers/context.d.ts +35 -13
  361. package/dist/plugins/workers/context.d.ts.map +1 -1
  362. package/dist/plugins/workers/context.js +82 -32
  363. package/dist/plugins/workers/context.js.map +1 -1
  364. package/dist/plugins/workers/definition.d.ts +7 -0
  365. package/dist/plugins/workers/definition.d.ts.map +1 -1
  366. package/dist/plugins/workers/definition.js.map +1 -1
  367. package/dist/plugins/workers/index.d.ts +4 -4
  368. package/dist/plugins/workers/index.d.ts.map +1 -1
  369. package/dist/plugins/workers/index.js +2 -2
  370. package/dist/plugins/workers/index.js.map +1 -1
  371. package/dist/plugins/workers/plugin.d.ts +10 -57
  372. package/dist/plugins/workers/plugin.d.ts.map +1 -1
  373. package/dist/plugins/workers/plugin.js +385 -123
  374. package/dist/plugins/workers/plugin.js.map +1 -1
  375. package/dist/plugins/workers/state.d.ts +69 -0
  376. package/dist/plugins/workers/state.d.ts.map +1 -0
  377. package/dist/plugins/workers/state.js +52 -0
  378. package/dist/plugins/workers/state.js.map +1 -0
  379. package/dist/plugins/workers/worker.d.ts +15 -4
  380. package/dist/plugins/workers/worker.d.ts.map +1 -1
  381. package/dist/testing/conformance.d.ts +106 -0
  382. package/dist/testing/conformance.d.ts.map +1 -0
  383. package/dist/testing/conformance.js +1463 -0
  384. package/dist/testing/conformance.js.map +1 -0
  385. package/dist/testing/node-platform.d.ts.map +1 -1
  386. package/dist/testing/node-platform.js +5 -0
  387. package/dist/testing/node-platform.js.map +1 -1
  388. package/dist/testing/test-harness.d.ts +11 -1
  389. package/dist/testing/test-harness.d.ts.map +1 -1
  390. package/dist/testing/test-harness.js +17 -37
  391. package/dist/testing/test-harness.js.map +1 -1
  392. package/dist/testing/wait-helpers.d.ts.map +1 -1
  393. package/dist/testing/wait-helpers.js +1 -3
  394. package/dist/testing/wait-helpers.js.map +1 -1
  395. package/dist/transport/adapter/client-adapter.d.ts +6 -0
  396. package/dist/transport/adapter/client-adapter.d.ts.map +1 -1
  397. package/dist/transport/adapter/client-adapter.js +50 -3
  398. package/dist/transport/adapter/client-adapter.js.map +1 -1
  399. package/dist/transport/adapter/heartbeat.d.ts +16 -0
  400. package/dist/transport/adapter/heartbeat.d.ts.map +1 -0
  401. package/dist/transport/adapter/heartbeat.js +41 -0
  402. package/dist/transport/adapter/heartbeat.js.map +1 -0
  403. package/dist/transport/adapter/server-adapter.d.ts.map +1 -1
  404. package/dist/transport/adapter/server-adapter.js +6 -0
  405. package/dist/transport/adapter/server-adapter.js.map +1 -1
  406. package/dist/transport/http/app.d.ts +3 -30
  407. package/dist/transport/http/app.d.ts.map +1 -1
  408. package/dist/transport/http/app.js +3 -7
  409. package/dist/transport/http/app.js.map +1 -1
  410. package/dist/transport/http/context.d.ts +40 -0
  411. package/dist/transport/http/context.d.ts.map +1 -0
  412. package/dist/transport/http/context.js +17 -0
  413. package/dist/transport/http/context.js.map +1 -0
  414. package/dist/transport/http/fetch-guard.d.ts +49 -0
  415. package/dist/transport/http/fetch-guard.d.ts.map +1 -0
  416. package/dist/transport/http/fetch-guard.js +312 -0
  417. package/dist/transport/http/fetch-guard.js.map +1 -0
  418. package/dist/transport/http/index.d.ts +1 -1
  419. package/dist/transport/http/index.d.ts.map +1 -1
  420. package/dist/transport/http/middleware/bearer-auth.d.ts +1 -1
  421. package/dist/transport/http/middleware/bearer-auth.d.ts.map +1 -1
  422. package/dist/transport/http/middleware/error-handler.d.ts +2 -2
  423. package/dist/transport/http/middleware/error-handler.d.ts.map +1 -1
  424. package/dist/transport/http/path-containment.d.ts +20 -0
  425. package/dist/transport/http/path-containment.d.ts.map +1 -0
  426. package/dist/transport/http/path-containment.js +31 -0
  427. package/dist/transport/http/path-containment.js.map +1 -0
  428. package/dist/transport/http/responses.d.ts +30 -0
  429. package/dist/transport/http/responses.d.ts.map +1 -0
  430. package/dist/transport/http/responses.js +5 -0
  431. package/dist/transport/http/responses.js.map +1 -0
  432. package/dist/transport/http/routes/files.d.ts +1 -1
  433. package/dist/transport/http/routes/files.d.ts.map +1 -1
  434. package/dist/transport/http/routes/files.js +54 -155
  435. package/dist/transport/http/routes/files.js.map +1 -1
  436. package/dist/transport/http/routes/resources.d.ts +1 -1
  437. package/dist/transport/http/routes/resources.d.ts.map +1 -1
  438. package/dist/transport/http/routes/resources.js +92 -57
  439. package/dist/transport/http/routes/resources.js.map +1 -1
  440. package/dist/transport/http/routes/rpc.d.ts +6 -1
  441. package/dist/transport/http/routes/rpc.d.ts.map +1 -1
  442. package/dist/transport/http/routes/rpc.js +51 -21
  443. package/dist/transport/http/routes/rpc.js.map +1 -1
  444. package/dist/transport/http/routes/upload.d.ts +1 -1
  445. package/dist/transport/http/routes/upload.d.ts.map +1 -1
  446. package/dist/transport/http/routes/upload.js +256 -169
  447. package/dist/transport/http/routes/upload.js.map +1 -1
  448. package/dist/user-config.d.ts +34 -7
  449. package/dist/user-config.d.ts.map +1 -1
  450. package/dist/user-config.js +71 -0
  451. package/dist/user-config.js.map +1 -1
  452. package/package.json +18 -8
  453. package/src/bootstrap.ts +119 -35
  454. package/src/bun-platform/fs.ts +1 -0
  455. package/src/bun-platform/index.ts +18 -1
  456. package/src/bun-platform/server-lifecycle.ts +218 -0
  457. package/src/bun-platform/shell.ts +362 -0
  458. package/src/config.ts +130 -0
  459. package/src/core/agents/agent.ts +219 -95
  460. package/src/core/agents/config.ts +8 -0
  461. package/src/core/agents/retry.ts +16 -17
  462. package/src/core/agents/state.ts +28 -0
  463. package/src/core/context/state.ts +4 -0
  464. package/src/core/events/base-event-store.ts +24 -16
  465. package/src/core/events/file.ts +198 -22
  466. package/src/core/file-store/containment.ts +75 -0
  467. package/src/core/file-store/file-store.ts +106 -25
  468. package/src/core/file-store/types.ts +14 -1
  469. package/src/core/image/image-processor.ts +3 -1
  470. package/src/core/image/types.ts +2 -0
  471. package/src/core/image/vips-resizer.ts +102 -37
  472. package/src/core/llm/anthropic.ts +127 -82
  473. package/src/core/llm/logger.ts +175 -37
  474. package/src/core/llm/openrouter.ts +82 -81
  475. package/src/core/llm/provider-request.ts +40 -0
  476. package/src/core/llm/provider.ts +72 -3
  477. package/src/core/llm/state.ts +12 -0
  478. package/src/core/plugins/plugin-builder.ts +106 -14
  479. package/src/core/preset/preset-builder.ts +12 -22
  480. package/src/core/sessions/context.ts +18 -0
  481. package/src/core/sessions/runtime-activity.ts +78 -0
  482. package/src/core/sessions/schema.ts +29 -3
  483. package/src/core/sessions/session-manager.ts +560 -140
  484. package/src/core/sessions/session-store.ts +38 -0
  485. package/src/core/sessions/session.ts +187 -47
  486. package/src/core/sessions/state.ts +16 -0
  487. package/src/core/system.ts +15 -0
  488. package/src/core/wake-key.ts +65 -0
  489. package/src/index.ts +31 -12
  490. package/src/lib/archive/archive-inspection.fixtures.ts +138 -0
  491. package/src/lib/archive/archive-inspection.ts +359 -0
  492. package/src/lib/archive/index.ts +17 -0
  493. package/src/lib/logger/console.ts +7 -57
  494. package/src/lib/logger/file.ts +28 -50
  495. package/src/lib/logger/index.ts +3 -1
  496. package/src/lib/logger/jsonl.ts +36 -0
  497. package/src/lib/logger/logger.ts +48 -0
  498. package/src/lib/logger/session-log.ts +42 -0
  499. package/src/lib/utils/concurrency.ts +37 -8
  500. package/src/lib/utils/fs-batch.ts +49 -0
  501. package/src/lib/utils/result.ts +7 -31
  502. package/src/lib/utils/sleep.ts +23 -0
  503. package/src/platform/fs-revision.ts +21 -0
  504. package/src/platform/fs.ts +109 -1
  505. package/src/platform/git.ts +67 -0
  506. package/src/platform/index.ts +51 -1
  507. package/src/platform/llm-call-log.ts +86 -0
  508. package/src/platform/scheduler.ts +71 -0
  509. package/src/platform/session-log.ts +39 -0
  510. package/src/platform/shell.ts +89 -0
  511. package/src/plugins/agent-status/plugin.ts +1 -0
  512. package/src/plugins/agents/plugin.ts +134 -81
  513. package/src/plugins/filesystem/listing.ts +70 -12
  514. package/src/plugins/filesystem/plugin.ts +8 -6
  515. package/src/plugins/git-status/plugin.ts +204 -77
  516. package/src/plugins/llm-debug/plugin.ts +1 -0
  517. package/src/plugins/logs/plugin.ts +9 -0
  518. package/src/plugins/mailbox/plugin.ts +46 -64
  519. package/src/plugins/mailbox/query.ts +15 -7
  520. package/src/plugins/mailbox/state.ts +1 -0
  521. package/src/plugins/resources/filename.ts +19 -0
  522. package/src/plugins/resources/plugin.ts +111 -59
  523. package/src/plugins/result-eviction/plugin.ts +6 -1
  524. package/src/plugins/services/plugin.ts +254 -98
  525. package/src/plugins/services/prompt.ts +1 -6
  526. package/src/plugins/services/schema.ts +17 -3
  527. package/src/plugins/services/service.ts +702 -252
  528. package/src/plugins/session-lifecycle/plugin.ts +56 -41
  529. package/src/plugins/session-state/plugin.ts +242 -88
  530. package/src/plugins/session-stats/plugin.ts +1 -0
  531. package/src/plugins/shell/executor.ts +144 -226
  532. package/src/plugins/shell/index.ts +1 -4
  533. package/src/plugins/shell/plugin.ts +7 -9
  534. package/src/plugins/skills/discovery.ts +10 -8
  535. package/src/plugins/skills/index.ts +1 -1
  536. package/src/plugins/skills/plugin.ts +19 -27
  537. package/src/plugins/skills/prompts.ts +3 -3
  538. package/src/plugins/skills/schema.ts +5 -2
  539. package/src/plugins/uploads/plugin.ts +991 -273
  540. package/src/plugins/uploads/preprocessor.ts +25 -0
  541. package/src/plugins/uploads/preprocessors/image-classifier.ts +56 -25
  542. package/src/plugins/uploads/preprocessors/markitdown-preprocessor.ts +41 -15
  543. package/src/plugins/uploads/preprocessors/pdf-preprocessor.ts +32 -16
  544. package/src/plugins/uploads/preprocessors/zip-preprocessor.ts +67 -38
  545. package/src/plugins/uploads/schema.ts +30 -3
  546. package/src/plugins/uploads/state.ts +12 -0
  547. package/src/plugins/user-chat/plugin.ts +196 -40
  548. package/src/plugins/workers/context.ts +102 -39
  549. package/src/plugins/workers/definition.ts +7 -0
  550. package/src/plugins/workers/index.ts +4 -4
  551. package/src/plugins/workers/plugin.ts +461 -151
  552. package/src/plugins/workers/state.ts +64 -0
  553. package/src/plugins/workers/worker.ts +15 -4
  554. package/src/testing/conformance.ts +1751 -0
  555. package/src/testing/node-platform.ts +5 -0
  556. package/src/testing/test-harness.ts +21 -38
  557. package/src/testing/wait-helpers.ts +1 -4
  558. package/src/transport/adapter/client-adapter.ts +60 -3
  559. package/src/transport/adapter/heartbeat.ts +50 -0
  560. package/src/transport/adapter/server-adapter.ts +6 -0
  561. package/src/transport/http/app.ts +7 -35
  562. package/src/transport/http/context.ts +46 -0
  563. package/src/transport/http/fetch-guard.ts +318 -0
  564. package/src/transport/http/index.ts +1 -1
  565. package/src/transport/http/middleware/bearer-auth.ts +1 -1
  566. package/src/transport/http/middleware/error-handler.ts +1 -1
  567. package/src/transport/http/path-containment.ts +44 -0
  568. package/src/transport/http/responses.ts +20 -0
  569. package/src/transport/http/routes/files.ts +69 -159
  570. package/src/transport/http/routes/resources.ts +124 -93
  571. package/src/transport/http/routes/rpc.ts +63 -26
  572. package/src/transport/http/routes/upload.ts +327 -258
  573. package/src/user-config.ts +93 -7
  574. package/dist/config.test.d.ts +0 -2
  575. package/dist/config.test.d.ts.map +0 -1
  576. package/dist/config.test.js +0 -155
  577. package/dist/config.test.js.map +0 -1
  578. package/dist/core/agent-loop.integration.test.d.ts +0 -2
  579. package/dist/core/agent-loop.integration.test.d.ts.map +0 -1
  580. package/dist/core/agent-loop.integration.test.js +0 -414
  581. package/dist/core/agent-loop.integration.test.js.map +0 -1
  582. package/dist/core/agents/agent-config.test.d.ts +0 -2
  583. package/dist/core/agents/agent-config.test.d.ts.map +0 -1
  584. package/dist/core/agents/agent-config.test.js +0 -194
  585. package/dist/core/agents/agent-config.test.js.map +0 -1
  586. package/dist/core/agents/agent-shutdown.test.d.ts +0 -6
  587. package/dist/core/agents/agent-shutdown.test.d.ts.map +0 -1
  588. package/dist/core/agents/agent-shutdown.test.js +0 -180
  589. package/dist/core/agents/agent-shutdown.test.js.map +0 -1
  590. package/dist/core/agents/agent.test.d.ts +0 -2
  591. package/dist/core/agents/agent.test.d.ts.map +0 -1
  592. package/dist/core/agents/agent.test.js +0 -313
  593. package/dist/core/agents/agent.test.js.map +0 -1
  594. package/dist/core/agents/communicator.d.ts +0 -3
  595. package/dist/core/agents/communicator.d.ts.map +0 -1
  596. package/dist/core/agents/communicator.js +0 -13
  597. package/dist/core/agents/communicator.js.map +0 -1
  598. package/dist/core/agents/handler-events.test.d.ts +0 -5
  599. package/dist/core/agents/handler-events.test.d.ts.map +0 -1
  600. package/dist/core/agents/handler-events.test.js +0 -115
  601. package/dist/core/agents/handler-events.test.js.map +0 -1
  602. package/dist/core/agents/response-sanitizer.test.d.ts +0 -2
  603. package/dist/core/agents/response-sanitizer.test.d.ts.map +0 -1
  604. package/dist/core/agents/response-sanitizer.test.js +0 -101
  605. package/dist/core/agents/response-sanitizer.test.js.map +0 -1
  606. package/dist/core/error-recovery.integration.test.d.ts +0 -2
  607. package/dist/core/error-recovery.integration.test.d.ts.map +0 -1
  608. package/dist/core/error-recovery.integration.test.js +0 -288
  609. package/dist/core/error-recovery.integration.test.js.map +0 -1
  610. package/dist/core/event-sourcing.integration.test.d.ts +0 -2
  611. package/dist/core/event-sourcing.integration.test.d.ts.map +0 -1
  612. package/dist/core/event-sourcing.integration.test.js +0 -154
  613. package/dist/core/event-sourcing.integration.test.js.map +0 -1
  614. package/dist/core/events/file.test.d.ts +0 -2
  615. package/dist/core/events/file.test.d.ts.map +0 -1
  616. package/dist/core/events/file.test.js +0 -284
  617. package/dist/core/events/file.test.js.map +0 -1
  618. package/dist/core/events/memory.test.d.ts +0 -2
  619. package/dist/core/events/memory.test.d.ts.map +0 -1
  620. package/dist/core/events/memory.test.js +0 -502
  621. package/dist/core/events/memory.test.js.map +0 -1
  622. package/dist/core/image/image-processor.test.d.ts +0 -2
  623. package/dist/core/image/image-processor.test.d.ts.map +0 -1
  624. package/dist/core/image/image-processor.test.js +0 -171
  625. package/dist/core/image/image-processor.test.js.map +0 -1
  626. package/dist/core/image/vips-resizer.test.d.ts +0 -2
  627. package/dist/core/image/vips-resizer.test.d.ts.map +0 -1
  628. package/dist/core/image/vips-resizer.test.js +0 -336
  629. package/dist/core/image/vips-resizer.test.js.map +0 -1
  630. package/dist/core/llm/anthropic.test.d.ts +0 -2
  631. package/dist/core/llm/anthropic.test.d.ts.map +0 -1
  632. package/dist/core/llm/anthropic.test.js +0 -464
  633. package/dist/core/llm/anthropic.test.js.map +0 -1
  634. package/dist/core/llm/cache-breakpoints.test.d.ts +0 -2
  635. package/dist/core/llm/cache-breakpoints.test.d.ts.map +0 -1
  636. package/dist/core/llm/cache-breakpoints.test.js +0 -69
  637. package/dist/core/llm/cache-breakpoints.test.js.map +0 -1
  638. package/dist/core/llm/cache-live.test.d.ts +0 -16
  639. package/dist/core/llm/cache-live.test.d.ts.map +0 -1
  640. package/dist/core/llm/cache-live.test.js +0 -137
  641. package/dist/core/llm/cache-live.test.js.map +0 -1
  642. package/dist/core/llm/logger.test.d.ts +0 -7
  643. package/dist/core/llm/logger.test.d.ts.map +0 -1
  644. package/dist/core/llm/logger.test.js +0 -228
  645. package/dist/core/llm/logger.test.js.map +0 -1
  646. package/dist/core/llm/message-sanitization.test.d.ts +0 -2
  647. package/dist/core/llm/message-sanitization.test.d.ts.map +0 -1
  648. package/dist/core/llm/message-sanitization.test.js +0 -181
  649. package/dist/core/llm/message-sanitization.test.js.map +0 -1
  650. package/dist/core/llm/mock.test.d.ts +0 -2
  651. package/dist/core/llm/mock.test.d.ts.map +0 -1
  652. package/dist/core/llm/mock.test.js +0 -318
  653. package/dist/core/llm/mock.test.js.map +0 -1
  654. package/dist/core/llm/openrouter-mapping.test.d.ts +0 -2
  655. package/dist/core/llm/openrouter-mapping.test.d.ts.map +0 -1
  656. package/dist/core/llm/openrouter-mapping.test.js +0 -172
  657. package/dist/core/llm/openrouter-mapping.test.js.map +0 -1
  658. package/dist/core/llm/openrouter.test.d.ts +0 -2
  659. package/dist/core/llm/openrouter.test.d.ts.map +0 -1
  660. package/dist/core/llm/openrouter.test.js +0 -377
  661. package/dist/core/llm/openrouter.test.js.map +0 -1
  662. package/dist/core/llm/provider-integration.test.d.ts +0 -12
  663. package/dist/core/llm/provider-integration.test.d.ts.map +0 -1
  664. package/dist/core/llm/provider-integration.test.js +0 -350
  665. package/dist/core/llm/provider-integration.test.js.map +0 -1
  666. package/dist/core/llm/routing-provider.test.d.ts +0 -2
  667. package/dist/core/llm/routing-provider.test.d.ts.map +0 -1
  668. package/dist/core/llm/routing-provider.test.js +0 -94
  669. package/dist/core/llm/routing-provider.test.js.map +0 -1
  670. package/dist/core/llm/snapshot-middleware.test.d.ts +0 -2
  671. package/dist/core/llm/snapshot-middleware.test.d.ts.map +0 -1
  672. package/dist/core/llm/snapshot-middleware.test.js +0 -144
  673. package/dist/core/llm/snapshot-middleware.test.js.map +0 -1
  674. package/dist/core/multi-agent.integration.test.d.ts +0 -2
  675. package/dist/core/multi-agent.integration.test.d.ts.map +0 -1
  676. package/dist/core/multi-agent.integration.test.js +0 -298
  677. package/dist/core/multi-agent.integration.test.js.map +0 -1
  678. package/dist/core/plugin-hooks.integration.test.d.ts +0 -2
  679. package/dist/core/plugin-hooks.integration.test.d.ts.map +0 -1
  680. package/dist/core/plugin-hooks.integration.test.js +0 -458
  681. package/dist/core/plugin-hooks.integration.test.js.map +0 -1
  682. package/dist/core/session-lifecycle.integration.test.d.ts +0 -2
  683. package/dist/core/session-lifecycle.integration.test.d.ts.map +0 -1
  684. package/dist/core/session-lifecycle.integration.test.js +0 -159
  685. package/dist/core/session-lifecycle.integration.test.js.map +0 -1
  686. package/dist/core/sessions/fork-utils.test.d.ts +0 -2
  687. package/dist/core/sessions/fork-utils.test.d.ts.map +0 -1
  688. package/dist/core/sessions/fork-utils.test.js +0 -129
  689. package/dist/core/sessions/fork-utils.test.js.map +0 -1
  690. package/dist/core/sessions/session-overrides.integration.test.d.ts +0 -2
  691. package/dist/core/sessions/session-overrides.integration.test.d.ts.map +0 -1
  692. package/dist/core/sessions/session-overrides.integration.test.js +0 -154
  693. package/dist/core/sessions/session-overrides.integration.test.js.map +0 -1
  694. package/dist/core/sessions/session.test.d.ts +0 -2
  695. package/dist/core/sessions/session.test.d.ts.map +0 -1
  696. package/dist/core/sessions/session.test.js +0 -1100
  697. package/dist/core/sessions/session.test.js.map +0 -1
  698. package/dist/core/tools/executor.test.d.ts +0 -2
  699. package/dist/core/tools/executor.test.d.ts.map +0 -1
  700. package/dist/core/tools/executor.test.js +0 -144
  701. package/dist/core/tools/executor.test.js.map +0 -1
  702. package/dist/lib/logger/console.test.d.ts +0 -2
  703. package/dist/lib/logger/console.test.d.ts.map +0 -1
  704. package/dist/lib/logger/console.test.js +0 -258
  705. package/dist/lib/logger/console.test.js.map +0 -1
  706. package/dist/lib/never.d.ts +0 -2
  707. package/dist/lib/never.d.ts.map +0 -1
  708. package/dist/lib/never.js +0 -4
  709. package/dist/lib/never.js.map +0 -1
  710. package/dist/lib/utils/concurrency.test.d.ts +0 -2
  711. package/dist/lib/utils/concurrency.test.d.ts.map +0 -1
  712. package/dist/lib/utils/concurrency.test.js +0 -135
  713. package/dist/lib/utils/concurrency.test.js.map +0 -1
  714. package/dist/plugins/agent-status/agent-status.test.d.ts +0 -2
  715. package/dist/plugins/agent-status/agent-status.test.d.ts.map +0 -1
  716. package/dist/plugins/agent-status/agent-status.test.js +0 -136
  717. package/dist/plugins/agent-status/agent-status.test.js.map +0 -1
  718. package/dist/plugins/agents/agents.integration.test.d.ts +0 -2
  719. package/dist/plugins/agents/agents.integration.test.d.ts.map +0 -1
  720. package/dist/plugins/agents/agents.integration.test.js +0 -830
  721. package/dist/plugins/agents/agents.integration.test.js.map +0 -1
  722. package/dist/plugins/agents/supervision.integration.test.d.ts +0 -2
  723. package/dist/plugins/agents/supervision.integration.test.d.ts.map +0 -1
  724. package/dist/plugins/agents/supervision.integration.test.js +0 -215
  725. package/dist/plugins/agents/supervision.integration.test.js.map +0 -1
  726. package/dist/plugins/context-compact/compaction-live.test.d.ts +0 -17
  727. package/dist/plugins/context-compact/compaction-live.test.d.ts.map +0 -1
  728. package/dist/plugins/context-compact/compaction-live.test.js +0 -177
  729. package/dist/plugins/context-compact/compaction-live.test.js.map +0 -1
  730. package/dist/plugins/context-compact/context-compact.integration.test.d.ts +0 -2
  731. package/dist/plugins/context-compact/context-compact.integration.test.d.ts.map +0 -1
  732. package/dist/plugins/context-compact/context-compact.integration.test.js +0 -364
  733. package/dist/plugins/context-compact/context-compact.integration.test.js.map +0 -1
  734. package/dist/plugins/context-compact/context-compactor.test.d.ts +0 -2
  735. package/dist/plugins/context-compact/context-compactor.test.d.ts.map +0 -1
  736. package/dist/plugins/context-compact/context-compactor.test.js +0 -858
  737. package/dist/plugins/context-compact/context-compactor.test.js.map +0 -1
  738. package/dist/plugins/context-compact/history-offloader.test.d.ts +0 -2
  739. package/dist/plugins/context-compact/history-offloader.test.d.ts.map +0 -1
  740. package/dist/plugins/context-compact/history-offloader.test.js +0 -77
  741. package/dist/plugins/context-compact/history-offloader.test.js.map +0 -1
  742. package/dist/plugins/filesystem/filesystem.integration.test.d.ts +0 -2
  743. package/dist/plugins/filesystem/filesystem.integration.test.d.ts.map +0 -1
  744. package/dist/plugins/filesystem/filesystem.integration.test.js +0 -447
  745. package/dist/plugins/filesystem/filesystem.integration.test.js.map +0 -1
  746. package/dist/plugins/filesystem/schema.d.ts +0 -7
  747. package/dist/plugins/filesystem/schema.d.ts.map +0 -1
  748. package/dist/plugins/filesystem/schema.js +0 -2
  749. package/dist/plugins/filesystem/schema.js.map +0 -1
  750. package/dist/plugins/limits-guard/limit-guard.test.d.ts +0 -2
  751. package/dist/plugins/limits-guard/limit-guard.test.d.ts.map +0 -1
  752. package/dist/plugins/limits-guard/limit-guard.test.js +0 -194
  753. package/dist/plugins/limits-guard/limit-guard.test.js.map +0 -1
  754. package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts +0 -2
  755. package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts.map +0 -1
  756. package/dist/plugins/limits-guard/limits-guard.integration.test.js +0 -887
  757. package/dist/plugins/limits-guard/limits-guard.integration.test.js.map +0 -1
  758. package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts +0 -2
  759. package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts.map +0 -1
  760. package/dist/plugins/llm-debug/llm-debug.integration.test.js +0 -157
  761. package/dist/plugins/llm-debug/llm-debug.integration.test.js.map +0 -1
  762. package/dist/plugins/mailbox/mailbox.integration.test.d.ts +0 -2
  763. package/dist/plugins/mailbox/mailbox.integration.test.d.ts.map +0 -1
  764. package/dist/plugins/mailbox/mailbox.integration.test.js +0 -678
  765. package/dist/plugins/mailbox/mailbox.integration.test.js.map +0 -1
  766. package/dist/plugins/resources/resources.integration.test.d.ts +0 -2
  767. package/dist/plugins/resources/resources.integration.test.d.ts.map +0 -1
  768. package/dist/plugins/resources/resources.integration.test.js +0 -62
  769. package/dist/plugins/resources/resources.integration.test.js.map +0 -1
  770. package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts +0 -2
  771. package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts.map +0 -1
  772. package/dist/plugins/result-eviction/result-eviction.integration.test.js +0 -294
  773. package/dist/plugins/result-eviction/result-eviction.integration.test.js.map +0 -1
  774. package/dist/plugins/services/pid-registry.test.d.ts +0 -2
  775. package/dist/plugins/services/pid-registry.test.d.ts.map +0 -1
  776. package/dist/plugins/services/pid-registry.test.js +0 -161
  777. package/dist/plugins/services/pid-registry.test.js.map +0 -1
  778. package/dist/plugins/services/services.integration.test.d.ts +0 -2
  779. package/dist/plugins/services/services.integration.test.d.ts.map +0 -1
  780. package/dist/plugins/services/services.integration.test.js +0 -1057
  781. package/dist/plugins/services/services.integration.test.js.map +0 -1
  782. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts +0 -2
  783. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts.map +0 -1
  784. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js +0 -498
  785. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js.map +0 -1
  786. package/dist/plugins/session-stats/plugin.test.d.ts +0 -2
  787. package/dist/plugins/session-stats/plugin.test.d.ts.map +0 -1
  788. package/dist/plugins/session-stats/plugin.test.js +0 -145
  789. package/dist/plugins/session-stats/plugin.test.js.map +0 -1
  790. package/dist/plugins/shell/shell.integration.test.d.ts +0 -2
  791. package/dist/plugins/shell/shell.integration.test.d.ts.map +0 -1
  792. package/dist/plugins/shell/shell.integration.test.js +0 -234
  793. package/dist/plugins/shell/shell.integration.test.js.map +0 -1
  794. package/dist/plugins/shell/shell.test.d.ts +0 -2
  795. package/dist/plugins/shell/shell.test.d.ts.map +0 -1
  796. package/dist/plugins/shell/shell.test.js +0 -236
  797. package/dist/plugins/shell/shell.test.js.map +0 -1
  798. package/dist/plugins/skills/discovery.test.d.ts +0 -2
  799. package/dist/plugins/skills/discovery.test.d.ts.map +0 -1
  800. package/dist/plugins/skills/discovery.test.js +0 -312
  801. package/dist/plugins/skills/discovery.test.js.map +0 -1
  802. package/dist/plugins/skills/skills.integration.test.d.ts +0 -2
  803. package/dist/plugins/skills/skills.integration.test.d.ts.map +0 -1
  804. package/dist/plugins/skills/skills.integration.test.js +0 -475
  805. package/dist/plugins/skills/skills.integration.test.js.map +0 -1
  806. package/dist/plugins/todo/todo.integration.test.d.ts +0 -2
  807. package/dist/plugins/todo/todo.integration.test.d.ts.map +0 -1
  808. package/dist/plugins/todo/todo.integration.test.js +0 -605
  809. package/dist/plugins/todo/todo.integration.test.js.map +0 -1
  810. package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts +0 -2
  811. package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts.map +0 -1
  812. package/dist/plugins/uploads/preprocessors/image-classifier.test.js +0 -113
  813. package/dist/plugins/uploads/preprocessors/image-classifier.test.js.map +0 -1
  814. package/dist/plugins/uploads/uploads.integration.test.d.ts +0 -2
  815. package/dist/plugins/uploads/uploads.integration.test.d.ts.map +0 -1
  816. package/dist/plugins/uploads/uploads.integration.test.js +0 -639
  817. package/dist/plugins/uploads/uploads.integration.test.js.map +0 -1
  818. package/dist/plugins/user-chat/user-chat.integration.test.d.ts +0 -2
  819. package/dist/plugins/user-chat/user-chat.integration.test.d.ts.map +0 -1
  820. package/dist/plugins/user-chat/user-chat.integration.test.js +0 -754
  821. package/dist/plugins/user-chat/user-chat.integration.test.js.map +0 -1
  822. package/dist/plugins/workers/workers.integration.test.d.ts +0 -2
  823. package/dist/plugins/workers/workers.integration.test.d.ts.map +0 -1
  824. package/dist/plugins/workers/workers.integration.test.js +0 -629
  825. package/dist/plugins/workers/workers.integration.test.js.map +0 -1
  826. package/dist/prompts/macros.test.d.ts +0 -2
  827. package/dist/prompts/macros.test.d.ts.map +0 -1
  828. package/dist/prompts/macros.test.js +0 -80
  829. package/dist/prompts/macros.test.js.map +0 -1
  830. package/dist/testing/test-harness.test.d.ts +0 -2
  831. package/dist/testing/test-harness.test.d.ts.map +0 -1
  832. package/dist/testing/test-harness.test.js +0 -92
  833. package/dist/testing/test-harness.test.js.map +0 -1
  834. package/dist/transport/http/routes/rpc.integration.test.d.ts +0 -2
  835. package/dist/transport/http/routes/rpc.integration.test.d.ts.map +0 -1
  836. package/dist/transport/http/routes/rpc.integration.test.js +0 -189
  837. package/dist/transport/http/routes/rpc.integration.test.js.map +0 -1
  838. package/dist/transport/http/routes/rpc.test.d.ts +0 -2
  839. package/dist/transport/http/routes/rpc.test.d.ts.map +0 -1
  840. package/dist/transport/http/routes/rpc.test.js +0 -316
  841. package/dist/transport/http/routes/rpc.test.js.map +0 -1
  842. package/src/config.test.ts +0 -174
  843. package/src/core/agent-loop.integration.test.ts +0 -503
  844. package/src/core/agents/agent-config.test.ts +0 -240
  845. package/src/core/agents/agent-shutdown.test.ts +0 -236
  846. package/src/core/agents/agent.test.ts +0 -387
  847. package/src/core/agents/communicator.ts +0 -16
  848. package/src/core/agents/handler-events.test.ts +0 -167
  849. package/src/core/agents/response-sanitizer.test.ts +0 -137
  850. package/src/core/error-recovery.integration.test.ts +0 -343
  851. package/src/core/event-sourcing.integration.test.ts +0 -191
  852. package/src/core/events/file.test.ts +0 -436
  853. package/src/core/events/memory.test.ts +0 -741
  854. package/src/core/image/image-processor.test.ts +0 -218
  855. package/src/core/image/vips-resizer.test.ts +0 -388
  856. package/src/core/llm/anthropic.test.ts +0 -546
  857. package/src/core/llm/cache-breakpoints.test.ts +0 -85
  858. package/src/core/llm/cache-live.test.ts +0 -155
  859. package/src/core/llm/logger.test.ts +0 -270
  860. package/src/core/llm/message-sanitization.test.ts +0 -217
  861. package/src/core/llm/mock.test.ts +0 -402
  862. package/src/core/llm/openrouter-mapping.test.ts +0 -210
  863. package/src/core/llm/openrouter.test.ts +0 -499
  864. package/src/core/llm/provider-integration.test.ts +0 -408
  865. package/src/core/llm/routing-provider.test.ts +0 -113
  866. package/src/core/llm/snapshot-middleware.test.ts +0 -168
  867. package/src/core/multi-agent.integration.test.ts +0 -340
  868. package/src/core/plugin-hooks.integration.test.ts +0 -568
  869. package/src/core/session-lifecycle.integration.test.ts +0 -202
  870. package/src/core/sessions/fork-utils.test.ts +0 -158
  871. package/src/core/sessions/session-overrides.integration.test.ts +0 -210
  872. package/src/core/sessions/session.test.ts +0 -1736
  873. package/src/core/tools/executor.test.ts +0 -161
  874. package/src/lib/logger/console.test.ts +0 -348
  875. package/src/lib/never.ts +0 -3
  876. package/src/lib/utils/concurrency.test.ts +0 -169
  877. package/src/plugins/agent-status/agent-status.test.ts +0 -164
  878. package/src/plugins/agents/agents.integration.test.ts +0 -976
  879. package/src/plugins/agents/supervision.integration.test.ts +0 -249
  880. package/src/plugins/context-compact/compaction-live.test.ts +0 -221
  881. package/src/plugins/context-compact/context-compact.integration.test.ts +0 -418
  882. package/src/plugins/context-compact/context-compactor.test.ts +0 -1036
  883. package/src/plugins/context-compact/history-offloader.test.ts +0 -100
  884. package/src/plugins/filesystem/filesystem.integration.test.ts +0 -529
  885. package/src/plugins/filesystem/schema.ts +0 -6
  886. package/src/plugins/limits-guard/limit-guard.test.ts +0 -253
  887. package/src/plugins/limits-guard/limits-guard.integration.test.ts +0 -1012
  888. package/src/plugins/llm-debug/llm-debug.integration.test.ts +0 -192
  889. package/src/plugins/mailbox/mailbox.integration.test.ts +0 -794
  890. package/src/plugins/resources/resources.integration.test.ts +0 -68
  891. package/src/plugins/result-eviction/result-eviction.integration.test.ts +0 -332
  892. package/src/plugins/services/pid-registry.test.ts +0 -180
  893. package/src/plugins/services/services.integration.test.ts +0 -1223
  894. package/src/plugins/session-lifecycle/session-lifecycle.integration.test.ts +0 -639
  895. package/src/plugins/session-stats/plugin.test.ts +0 -160
  896. package/src/plugins/shell/shell.integration.test.ts +0 -273
  897. package/src/plugins/shell/shell.test.ts +0 -314
  898. package/src/plugins/skills/discovery.test.ts +0 -395
  899. package/src/plugins/skills/skills.integration.test.ts +0 -580
  900. package/src/plugins/todo/todo.integration.test.ts +0 -687
  901. package/src/plugins/uploads/preprocessors/image-classifier.test.ts +0 -142
  902. package/src/plugins/uploads/uploads.integration.test.ts +0 -831
  903. package/src/plugins/user-chat/user-chat.integration.test.ts +0 -893
  904. package/src/plugins/workers/workers.integration.test.ts +0 -778
  905. package/src/prompts/macros.test.ts +0 -107
  906. package/src/testing/test-harness.test.ts +0 -114
  907. package/src/transport/http/routes/rpc.integration.test.ts +0 -243
  908. package/src/transport/http/routes/rpc.test.ts +0 -372
@@ -0,0 +1,1751 @@
1
+ /**
2
+ * Executable contract for `Platform`.
3
+ *
4
+ * The ports are interfaces with doc comments, and until now the only thing that
5
+ * said whether an implementation honoured them was the SDK's own test suite —
6
+ * which a host that cannot run the SDK cannot run either. This is the contract
7
+ * on its own: a list of checks, each asserting one clause the doc comments
8
+ * state, runnable against any `Platform`.
9
+ *
10
+ * The checks are data, not `test()` calls. `runPlatformConformance` maps them
11
+ * onto `bun:test`; anything else can execute one directly with
12
+ * `runConformanceCheck` — which is how the suite is itself proven able to fail.
13
+ *
14
+ * Optional ports are probed, never assumed, and a port that is not exercised is
15
+ * reported with the reason. A host that answers nothing does not pass quietly.
16
+ */
17
+
18
+ import { describe, expect, test } from 'bun:test'
19
+ import { sleep } from '~/lib/utils/sleep.js'
20
+ import type {
21
+ FileSystem,
22
+ LLMCallRow,
23
+ Platform,
24
+ ReadFilesEntry,
25
+ ShellConfinement,
26
+ ShellGrant,
27
+ ShellRunOptions,
28
+ WalkEntry,
29
+ WalkOptions,
30
+ } from '~/platform/index.js'
31
+ import { isLiveScheduler } from '~/platform/index.js'
32
+
33
+ // ============================================================================
34
+ // Target
35
+ // ============================================================================
36
+
37
+ /** One platform and the empty directory a check may write in. */
38
+ export interface PlatformInstance {
39
+ platform: Platform
40
+ /** Absolute path to a directory that exists and holds nothing. */
41
+ root: string
42
+ dispose?(): Promise<void> | void
43
+ }
44
+
45
+ export interface ConformanceTarget {
46
+ /** Names the host in the run's output. */
47
+ name: string
48
+
49
+ /** A fresh platform and scratch root. Called once per check, never shared. */
50
+ create(): Promise<PlatformInstance> | PlatformInstance
51
+
52
+ /**
53
+ * Symlink `linkPath` to `targetPath` — the `fs` port has no verb for it, and
54
+ * `realpath` and `walk` both have a clause about links. Defaults to `ln -s`
55
+ * through `platform.process`; checks that need one are skipped where neither
56
+ * works.
57
+ */
58
+ symlink?(instance: PlatformInstance, targetPath: string, linkPath: string): Promise<void>
59
+
60
+ /**
61
+ * Build the fixture repository the `git` checks read, at `dir`. Defaults to
62
+ * `git` through `platform.process`; a host with a stubbed process table
63
+ * supplies its own. See {@link GIT_FIXTURE} for what it must contain.
64
+ */
65
+ buildGitRepo?(instance: PlatformInstance, dir: string): Promise<void>
66
+ }
67
+
68
+ /**
69
+ * What a `git` fixture repository must contain, however it is built:
70
+ * branch `feature`, one commit ahead of `main`, HEAD's subject line `second`,
71
+ * a tracked `note.txt` modified in the worktree, and an untracked `scratch.txt`.
72
+ */
73
+ export const GIT_FIXTURE: {
74
+ branch: string
75
+ base: string
76
+ headSubject: string
77
+ baseSubject: string
78
+ modified: string
79
+ untracked: string
80
+ } = {
81
+ branch: 'feature',
82
+ base: 'main',
83
+ headSubject: 'second',
84
+ baseSubject: 'first',
85
+ modified: 'note.txt',
86
+ untracked: 'scratch.txt',
87
+ }
88
+
89
+ // ============================================================================
90
+ // Ports
91
+ // ============================================================================
92
+
93
+ /**
94
+ * What the suite reports on. Most are ports; the dotted ones are facets a port
95
+ * either has or does not — a scheduler that delivers its own wakes, a shell that
96
+ * confines by paths — and they are named so that not exercising one is visible.
97
+ */
98
+ export type ConformancePort =
99
+ | 'fs'
100
+ | 'fs.symlinks'
101
+ | 'fs.walk'
102
+ | 'fs.readFiles'
103
+ | 'fs.writeFiles'
104
+ | 'fs.rmFiles'
105
+ | 'fs.scopeReads'
106
+ | 'scheduler'
107
+ | 'scheduler.live'
108
+ | 'shell'
109
+ | 'shell.paths'
110
+ | 'shell.host'
111
+ | 'git'
112
+ | 'fsRevision'
113
+ | 'fsRevision.numbered'
114
+ | 'sessionLog'
115
+ | 'llmCallLog'
116
+
117
+ /** Report order, and the order sections are declared in. */
118
+ export const CONFORMANCE_PORTS: readonly ConformancePort[] = [
119
+ 'fs',
120
+ 'fs.symlinks',
121
+ 'fs.walk',
122
+ 'fs.readFiles',
123
+ 'fs.writeFiles',
124
+ 'fs.rmFiles',
125
+ 'fs.scopeReads',
126
+ 'scheduler',
127
+ 'scheduler.live',
128
+ 'shell',
129
+ 'shell.paths',
130
+ 'shell.host',
131
+ 'git',
132
+ 'fsRevision',
133
+ 'fsRevision.numbered',
134
+ 'sessionLog',
135
+ 'llmCallLog',
136
+ ]
137
+
138
+ /** The five optional `FileSystem` members, as the port names that report them. */
139
+ const OPTIONAL_FS_VERBS: readonly ('walk' | 'readFiles' | 'writeFiles' | 'rmFiles' | 'scopeReads')[] = [
140
+ 'walk',
141
+ 'readFiles',
142
+ 'writeFiles',
143
+ 'rmFiles',
144
+ 'scopeReads',
145
+ ]
146
+
147
+ export interface PortSupport {
148
+ port: ConformancePort
149
+ answered: boolean
150
+ /** Why the port is not exercised. Always set when `answered` is false. */
151
+ note?: string
152
+ }
153
+
154
+ // ============================================================================
155
+ // Checks
156
+ // ============================================================================
157
+
158
+ export interface ConformanceContext {
159
+ readonly platform: Platform
160
+ /** Empty directory this check owns. */
161
+ readonly root: string
162
+ /** A path under `root`. */
163
+ path(...segments: string[]): string
164
+ /** Symlink `linkPath` to `targetPath`. Only for checks that need `fs.symlinks`. */
165
+ symlink(targetPath: string, linkPath: string): Promise<void>
166
+ /** The fixture repository, built once per check. Only for `git` checks. */
167
+ gitRepo(): Promise<string>
168
+ }
169
+
170
+ export interface ConformanceCheck {
171
+ /** Section the check is reported under. */
172
+ port: ConformancePort
173
+ /** Further ports it needs; absent ones skip it as the section would. */
174
+ needs?: readonly ConformancePort[]
175
+ name: string
176
+ run(ctx: ConformanceContext): Promise<void>
177
+ }
178
+
179
+ // ============================================================================
180
+ // Helpers
181
+ // ============================================================================
182
+
183
+ function joinPath(base: string, segments: readonly string[]): string {
184
+ const tail = segments.filter((segment) => segment.length > 0).join('/')
185
+ if (tail.length === 0) return base
186
+ return base.endsWith('/') ? `${base}${tail}` : `${base}/${tail}`
187
+ }
188
+
189
+ function baseName(path: string): string {
190
+ const cut = path.lastIndexOf('/')
191
+ return cut === -1 ? path : path.slice(cut + 1)
192
+ }
193
+
194
+ function relativeTo(base: string, path: string): string {
195
+ return path.startsWith(base) ? path.slice(base.length).replace(/^\/+/, '') : path
196
+ }
197
+
198
+ /** The rejection, or a failure of its own when the promise resolved. */
199
+ async function rejection(promise: Promise<unknown>, what: string): Promise<unknown> {
200
+ try {
201
+ await promise
202
+ } catch (error) {
203
+ return error
204
+ }
205
+ throw new Error(`expected ${what} to reject, but it resolved`)
206
+ }
207
+
208
+ /** The code `readFile` threw, the way `ReadFilesEntry.error` names it. */
209
+ function errorCode(error: unknown): string | undefined {
210
+ if (error instanceof Error && 'code' in error && typeof error.code === 'string') return error.code
211
+ return undefined
212
+ }
213
+
214
+ async function waitUntil(predicate: () => boolean, timeoutMs: number, what: string): Promise<void> {
215
+ const deadline = Date.now() + timeoutMs
216
+ while (Date.now() < deadline) {
217
+ if (predicate()) return
218
+ await sleep(5)
219
+ }
220
+ throw new Error(`${what} did not happen within ${timeoutMs}ms`)
221
+ }
222
+
223
+ const textOf = (bytes: Buffer): string => Buffer.from(bytes).toString('utf-8')
224
+
225
+ /** Comparable shape of a walked entry: what the contract fixes, nothing more. */
226
+ interface WalkShape {
227
+ path: string
228
+ type: WalkEntry['type']
229
+ size: number
230
+ }
231
+
232
+ function walkShapes(base: string, entries: readonly WalkEntry[]): WalkShape[] {
233
+ return entries
234
+ .map((entry) => ({ path: relativeTo(base, entry.path), type: entry.type, size: entry.size }))
235
+ .sort((a, b) => a.path.localeCompare(b.path))
236
+ }
237
+
238
+ /**
239
+ * The readdir-and-stat walk `FileSystem.walk` replaces, following the same
240
+ * rules — classifying from the dirent so a link is a link, but sizing from
241
+ * `stat`, which follows it. A broken link is listed at 0, not dropped.
242
+ */
243
+ async function walkWithReaddir(fs: FileSystem, dir: string, options?: WalkOptions): Promise<WalkEntry[]> {
244
+ const excluded = new Set(options?.exclude ?? [])
245
+ const out: WalkEntry[] = []
246
+
247
+ const descend = async (current: string, depthLeft: number): Promise<void> => {
248
+ for (const dirent of await fs.readdir(current, { withFileTypes: true })) {
249
+ if (options?.limit !== undefined && out.length >= options.limit) return
250
+ if (excluded.has(dirent.name)) continue
251
+ if (options?.excludeHidden && dirent.name.startsWith('.')) continue
252
+
253
+ const path = joinPath(current, [dirent.name])
254
+ const type: WalkEntry['type'] = dirent.isSymbolicLink()
255
+ ? 'symlink'
256
+ : dirent.isDirectory()
257
+ ? 'directory'
258
+ : dirent.isFile()
259
+ ? 'file'
260
+ : 'other'
261
+
262
+ const stats = await fs.stat(path).then((value) => value, () => undefined)
263
+ const size = type === 'directory' ? 0 : stats?.size ?? 0
264
+ const mtime = stats?.mtimeMs ?? 0
265
+
266
+ out.push({ path, type, size, mtime })
267
+ if (type === 'directory' && depthLeft > 1) await descend(path, depthLeft - 1)
268
+ }
269
+ }
270
+
271
+ await descend(dir, options?.depth ?? Number.POSITIVE_INFINITY)
272
+ return out
273
+ }
274
+
275
+ /** The per-path `readFile` loop `FileSystem.readFiles` replaces. */
276
+ async function readFilesWithLoop(fs: FileSystem, paths: readonly string[]): Promise<ReadFilesEntry[]> {
277
+ return Promise.all(paths.map(async (path): Promise<ReadFilesEntry> => {
278
+ try {
279
+ return { path, content: await fs.readFile(path) }
280
+ } catch (error) {
281
+ return { path, error: errorCode(error) ?? String(error) }
282
+ }
283
+ }))
284
+ }
285
+
286
+ /**
287
+ * A small tree the walk checks read: two levels, a hidden file, a hidden
288
+ * directory with a child, and a directory the `exclude` check names.
289
+ */
290
+ async function buildWalkTree(ctx: ConformanceContext): Promise<void> {
291
+ const { platform, path } = ctx
292
+ const fs = platform.fs
293
+ await fs.mkdir(path('sub', 'deep'), { recursive: true })
294
+ await fs.mkdir(path('skipme'), { recursive: true })
295
+ await fs.mkdir(path('.hiddendir'), { recursive: true })
296
+ await fs.writeFile(path('a.txt'), 'aaa')
297
+ await fs.writeFile(path('.hidden.txt'), 'h')
298
+ await fs.writeFile(path('sub', 'b.txt'), 'bb')
299
+ await fs.writeFile(path('sub', 'deep', 'c.txt'), 'cccc')
300
+ await fs.writeFile(path('skipme', 'd.txt'), 'd')
301
+ await fs.writeFile(path('.hiddendir', 'inside.txt'), 'i')
302
+ }
303
+
304
+ async function defaultSymlink(instance: PlatformInstance, targetPath: string, linkPath: string): Promise<void> {
305
+ await instance.platform.process.execFile('ln', ['-s', targetPath, linkPath], {})
306
+ }
307
+
308
+ async function defaultBuildGitRepo(instance: PlatformInstance, dir: string): Promise<void> {
309
+ const { platform } = instance
310
+ const identity = ['-c', 'user.email=conformance@roj.local', '-c', 'user.name=Roj Conformance', '-c', 'commit.gpgsign=false']
311
+ const git = async (...args: string[]): Promise<void> => {
312
+ await platform.process.execFile('git', [...identity, ...args], { cwd: dir })
313
+ }
314
+
315
+ await platform.fs.mkdir(dir, { recursive: true })
316
+ await git('init', '-q', '-b', GIT_FIXTURE.base)
317
+ await platform.fs.writeFile(joinPath(dir, [GIT_FIXTURE.modified]), 'one\n')
318
+ await git('add', GIT_FIXTURE.modified)
319
+ await git('commit', '-q', '-m', GIT_FIXTURE.baseSubject)
320
+ await git('checkout', '-q', '-b', GIT_FIXTURE.branch)
321
+ await platform.fs.writeFile(joinPath(dir, [GIT_FIXTURE.modified]), 'two\n')
322
+ await git('commit', '-q', '-a', '-m', `${GIT_FIXTURE.headSubject}\n\nbody paragraph\n`)
323
+ await platform.fs.writeFile(joinPath(dir, [GIT_FIXTURE.modified]), 'three\n')
324
+ await platform.fs.writeFile(joinPath(dir, [GIT_FIXTURE.untracked]), 'scratch\n')
325
+ }
326
+
327
+ function contextFor(target: ConformanceTarget, instance: PlatformInstance): ConformanceContext {
328
+ let repo: Promise<string> | undefined
329
+ return {
330
+ platform: instance.platform,
331
+ root: instance.root,
332
+ path: (...segments) => joinPath(instance.root, segments),
333
+ symlink: (targetPath, linkPath) => (target.symlink ?? defaultSymlink)(instance, targetPath, linkPath),
334
+ gitRepo: () => {
335
+ if (!repo) {
336
+ const dir = joinPath(instance.root, ['fixture-repo'])
337
+ const build = target.buildGitRepo ?? defaultBuildGitRepo
338
+ repo = build(instance, dir).then(() => dir, (error) => {
339
+ throw new Error(
340
+ `could not build the fixture repository: ${error instanceof Error ? error.message : String(error)}. `
341
+ + 'A host that cannot run `git` supplies ConformanceTarget.buildGitRepo.',
342
+ )
343
+ })
344
+ }
345
+ return repo
346
+ },
347
+ }
348
+ }
349
+
350
+ // ============================================================================
351
+ // fs — the required surface
352
+ // ============================================================================
353
+
354
+ const fsChecks: ConformanceCheck[] = [
355
+ {
356
+ port: 'fs',
357
+ name: 'writeFile and readFile round-trip text and bytes',
358
+ async run({ platform, path }) {
359
+ const fs = platform.fs
360
+ const file = path('note.txt')
361
+ await fs.writeFile(file, 'héllo\n')
362
+ expect(await fs.readFile(file, 'utf-8')).toBe('héllo\n')
363
+ expect(textOf(await fs.readFile(file))).toBe('héllo\n')
364
+
365
+ const binary = path('bytes.bin')
366
+ await fs.writeFile(binary, new Uint8Array([0, 1, 254, 255]))
367
+ expect([...Buffer.from(await fs.readFile(binary))]).toEqual([0, 1, 254, 255])
368
+
369
+ await fs.writeFile(file, 'replaced')
370
+ expect(await fs.readFile(file, 'utf-8')).toBe('replaced')
371
+ },
372
+ },
373
+ {
374
+ port: 'fs',
375
+ name: 'readFile rejects for a missing path',
376
+ async run({ platform, path }) {
377
+ await rejection(platform.fs.readFile(path('missing.txt')), 'readFile of a missing path')
378
+ },
379
+ },
380
+ {
381
+ port: 'fs',
382
+ name: 'appendFile creates a file and adds to the end of one',
383
+ async run({ platform, path }) {
384
+ const fs = platform.fs
385
+ const file = path('log.txt')
386
+ await fs.appendFile(file, 'one\n')
387
+ await fs.appendFile(file, 'two\n')
388
+ expect(await fs.readFile(file, 'utf-8')).toBe('one\ntwo\n')
389
+ },
390
+ },
391
+ {
392
+ port: 'fs',
393
+ name: 'mkdir creates one level, and the whole chain when recursive',
394
+ async run({ platform, path }) {
395
+ const fs = platform.fs
396
+ await fs.mkdir(path('one'))
397
+ expect(await fs.exists(path('one'))).toBe(true)
398
+
399
+ await rejection(fs.mkdir(path('deep', 'chain')), 'mkdir of a path whose parent is missing')
400
+ await fs.mkdir(path('deep', 'chain'), { recursive: true })
401
+ expect(await fs.exists(path('deep', 'chain'))).toBe(true)
402
+
403
+ await rejection(fs.mkdir(path('one')), 'mkdir of an existing directory')
404
+ await fs.mkdir(path('one'), { recursive: true })
405
+ },
406
+ },
407
+ {
408
+ port: 'fs',
409
+ name: 'readdir lists names, and classifies them with withFileTypes',
410
+ async run({ platform, path }) {
411
+ const fs = platform.fs
412
+ await fs.mkdir(path('dir'), { recursive: true })
413
+ await fs.writeFile(path('a.txt'), 'a')
414
+ await fs.writeFile(path('b.txt'), 'b')
415
+
416
+ expect((await fs.readdir(path())).sort()).toEqual(['a.txt', 'b.txt', 'dir'])
417
+
418
+ const dirents = await fs.readdir(path(), { withFileTypes: true })
419
+ const classified = dirents
420
+ .map((dirent) => [dirent.name, dirent.isDirectory(), dirent.isFile()])
421
+ .sort()
422
+ expect(classified).toEqual([
423
+ ['a.txt', false, true],
424
+ ['b.txt', false, true],
425
+ ['dir', true, false],
426
+ ])
427
+
428
+ await rejection(fs.readdir(path('missing')), 'readdir of a missing directory')
429
+ },
430
+ },
431
+ {
432
+ port: 'fs',
433
+ name: 'stat reports size, kind and a modification time',
434
+ async run({ platform, path }) {
435
+ const fs = platform.fs
436
+ await fs.writeFile(path('a.txt'), 'abcde')
437
+ await fs.mkdir(path('dir'), { recursive: true })
438
+
439
+ const file = await fs.stat(path('a.txt'))
440
+ expect(file.size).toBe(5)
441
+ expect(file.isFile()).toBe(true)
442
+ expect(file.isDirectory()).toBe(false)
443
+ expect(Number.isFinite(file.mtimeMs)).toBe(true)
444
+
445
+ const dir = await fs.stat(path('dir'))
446
+ expect(dir.isDirectory()).toBe(true)
447
+
448
+ await rejection(fs.stat(path('missing')), 'stat of a missing path')
449
+ },
450
+ },
451
+ {
452
+ port: 'fs',
453
+ name: 'lstat answers as stat does for a path that is not a link',
454
+ async run({ platform, path }) {
455
+ const fs = platform.fs
456
+ await fs.writeFile(path('a.txt'), 'abcde')
457
+ await fs.mkdir(path('dir'), { recursive: true })
458
+
459
+ const file = await fs.lstat(path('a.txt'))
460
+ expect(file.size).toBe(5)
461
+ expect(file.isFile()).toBe(true)
462
+ expect(file.isSymbolicLink()).toBe(false)
463
+ expect((await fs.lstat(path('dir'))).isDirectory()).toBe(true)
464
+
465
+ await rejection(fs.lstat(path('missing')), 'lstat of a missing path')
466
+ },
467
+ },
468
+ {
469
+ port: 'fs',
470
+ needs: ['fs.symlinks'],
471
+ name: 'lstat reports the link itself, and answers for one whose target is gone',
472
+ async run({ platform, path, symlink }) {
473
+ const fs = platform.fs
474
+ await fs.writeFile(path('target.txt'), 'target')
475
+ await symlink(path('target.txt'), path('link.txt'))
476
+ await symlink(path('nowhere.txt'), path('broken.link'))
477
+
478
+ expect((await fs.lstat(path('link.txt'))).isSymbolicLink()).toBe(true)
479
+ expect((await fs.lstat(path('link.txt'))).isFile()).toBe(false)
480
+ expect((await fs.stat(path('link.txt'))).isFile()).toBe(true)
481
+
482
+ // The pair a caller separates "not there" from "there but unresolvable" on.
483
+ expect((await fs.lstat(path('broken.link'))).isSymbolicLink()).toBe(true)
484
+ await rejection(fs.realpath(path('broken.link')), 'realpath of a broken link')
485
+ },
486
+ },
487
+ {
488
+ port: 'fs',
489
+ name: 'access resolves for a reachable path and rejects for a missing one',
490
+ async run({ platform, path }) {
491
+ const fs = platform.fs
492
+ await fs.writeFile(path('a.txt'), 'a')
493
+ await fs.access(path('a.txt'))
494
+ await fs.access(path())
495
+ await rejection(fs.access(path('missing')), 'access of a missing path')
496
+ },
497
+ },
498
+ {
499
+ port: 'fs',
500
+ name: 'exists never throws for a missing path',
501
+ async run({ platform, path }) {
502
+ const fs = platform.fs
503
+ await fs.writeFile(path('a.txt'), 'a')
504
+ await fs.mkdir(path('dir'), { recursive: true })
505
+
506
+ expect(await fs.exists(path('a.txt'))).toBe(true)
507
+ expect(await fs.exists(path('dir'))).toBe(true)
508
+ expect(await fs.exists(path('missing.txt'))).toBe(false)
509
+ // The parent is missing too — the corner that makes a stat-and-rethrow fail.
510
+ expect(await fs.exists(path('no', 'such', 'parent', 'file.txt'))).toBe(false)
511
+ },
512
+ },
513
+ {
514
+ port: 'fs',
515
+ name: 'unlink removes one file and rejects for a missing one',
516
+ async run({ platform, path }) {
517
+ const fs = platform.fs
518
+ await fs.writeFile(path('a.txt'), 'a')
519
+ await fs.unlink(path('a.txt'))
520
+ expect(await fs.exists(path('a.txt'))).toBe(false)
521
+ await rejection(fs.unlink(path('a.txt')), 'unlink of a missing path')
522
+ },
523
+ },
524
+ {
525
+ port: 'fs',
526
+ name: 'rm with force is a no-op on a missing path, and rejects without it',
527
+ async run({ platform, path }) {
528
+ const fs = platform.fs
529
+ await fs.rm(path('missing.txt'), { force: true })
530
+ await fs.rm(path('missing', 'deep'), { recursive: true, force: true })
531
+ await rejection(fs.rm(path('missing.txt')), 'rm of a missing path without force')
532
+ },
533
+ },
534
+ {
535
+ port: 'fs',
536
+ name: 'rm removes a populated directory only when recursive',
537
+ async run({ platform, path }) {
538
+ const fs = platform.fs
539
+ await fs.mkdir(path('tree', 'nested'), { recursive: true })
540
+ await fs.writeFile(path('tree', 'nested', 'leaf.txt'), 'leaf')
541
+
542
+ await rejection(fs.rm(path('tree')), 'rm of a populated directory without recursive')
543
+ expect(await fs.exists(path('tree', 'nested', 'leaf.txt'))).toBe(true)
544
+
545
+ await fs.rm(path('tree'), { recursive: true })
546
+ expect(await fs.exists(path('tree'))).toBe(false)
547
+ },
548
+ },
549
+ {
550
+ port: 'fs',
551
+ name: 'cp copies a file, and a subtree only when recursive',
552
+ async run({ platform, path }) {
553
+ const fs = platform.fs
554
+ await fs.mkdir(path('src', 'nested'), { recursive: true })
555
+ await fs.writeFile(path('src', 'top.txt'), 'top')
556
+ await fs.writeFile(path('src', 'nested', 'leaf.txt'), 'leaf')
557
+
558
+ await fs.cp(path('src', 'top.txt'), path('copy.txt'))
559
+ expect(await fs.readFile(path('copy.txt'), 'utf-8')).toBe('top')
560
+
561
+ await rejection(fs.cp(path('src'), path('flat')), 'cp of a directory without recursive')
562
+
563
+ await fs.cp(path('src'), path('deep'), { recursive: true })
564
+ expect(await fs.readFile(path('deep', 'top.txt'), 'utf-8')).toBe('top')
565
+ expect(await fs.readFile(path('deep', 'nested', 'leaf.txt'), 'utf-8')).toBe('leaf')
566
+ },
567
+ },
568
+ {
569
+ port: 'fs',
570
+ name: 'realpath returns a canonical path it can be asked for again',
571
+ async run({ platform, path }) {
572
+ const fs = platform.fs
573
+ await fs.writeFile(path('a.txt'), 'a')
574
+
575
+ const canonical = await fs.realpath(path('a.txt'))
576
+ expect(baseName(canonical)).toBe('a.txt')
577
+ expect(await fs.exists(canonical)).toBe(true)
578
+ expect(await fs.realpath(canonical)).toBe(canonical)
579
+
580
+ await rejection(fs.realpath(path('missing.txt')), 'realpath of a missing path')
581
+ },
582
+ },
583
+ {
584
+ port: 'fs',
585
+ needs: ['fs.symlinks'],
586
+ name: 'realpath resolves a symlink to its target',
587
+ async run({ platform, path, symlink }) {
588
+ const fs = platform.fs
589
+ await fs.writeFile(path('target.txt'), 'target')
590
+ await symlink(path('target.txt'), path('link.txt'))
591
+
592
+ expect(await fs.realpath(path('link.txt'))).toBe(await fs.realpath(path('target.txt')))
593
+ },
594
+ },
595
+ {
596
+ port: 'fs',
597
+ name: 'open returns the positional-read handle subset',
598
+ async run({ platform, path }) {
599
+ const fs = platform.fs
600
+ const content = 'abcdefghijklmnop'
601
+ await fs.writeFile(path('a.txt'), content)
602
+
603
+ const handle = await fs.open(path('a.txt'))
604
+ try {
605
+ expect((await handle.stat()).size).toBe(content.length)
606
+
607
+ const buffer = Buffer.alloc(8, 0)
608
+ const read = await handle.read(buffer, 2, 4, 6)
609
+ expect(read.bytesRead).toBe(4)
610
+ expect(Buffer.from(read.buffer).subarray(2, 6).toString('utf-8')).toBe(content.slice(6, 10))
611
+
612
+ // A second read at a different position: the handle carries no cursor of its own.
613
+ const again = await handle.read(Buffer.alloc(3, 0), 0, 3, 0)
614
+ expect(Buffer.from(again.buffer).subarray(0, 3).toString('utf-8')).toBe(content.slice(0, 3))
615
+
616
+ expect((await handle.read(Buffer.alloc(4, 0), 0, 4, content.length)).bytesRead).toBe(0)
617
+ } finally {
618
+ await handle.close()
619
+ }
620
+ },
621
+ },
622
+ {
623
+ port: 'fs',
624
+ name: 'tmpDir names a directory that exists',
625
+ async run({ platform }) {
626
+ expect(platform.tmpDir.length).toBeGreaterThan(0)
627
+ expect(await platform.fs.exists(platform.tmpDir)).toBe(true)
628
+ },
629
+ },
630
+ ]
631
+
632
+ // ============================================================================
633
+ // fs — the optional verbs, each against the loop it replaces
634
+ // ============================================================================
635
+
636
+ const walkChecks: ConformanceCheck[] = [
637
+ {
638
+ port: 'fs.walk',
639
+ name: 'agrees with the readdir-and-stat walk it replaces',
640
+ async run(ctx) {
641
+ const fs = ctx.platform.fs
642
+ await buildWalkTree(ctx)
643
+
644
+ const walked = await fs.walk?.(ctx.root)
645
+ const looped = await walkWithReaddir(fs, ctx.root)
646
+ expect(walkShapes(ctx.root, walked ?? [])).toEqual(walkShapes(ctx.root, looped))
647
+
648
+ for (const entry of walked ?? []) {
649
+ expect(entry.path.startsWith(ctx.root)).toBe(true)
650
+ expect(Number.isFinite(entry.mtime)).toBe(true)
651
+ // "0 for a directory" — everything else is whatever stat reports.
652
+ if (entry.type === 'directory') expect(entry.size).toBe(0)
653
+ }
654
+ },
655
+ },
656
+ {
657
+ port: 'fs.walk',
658
+ name: 'honours depth',
659
+ async run(ctx) {
660
+ const fs = ctx.platform.fs
661
+ await buildWalkTree(ctx)
662
+
663
+ const shallow = walkShapes(ctx.root, (await fs.walk?.(ctx.root, { depth: 1 })) ?? [])
664
+ expect(shallow).toEqual(walkShapes(ctx.root, await walkWithReaddir(fs, ctx.root, { depth: 1 })))
665
+ expect(shallow.map((entry) => entry.path)).not.toContain('sub/b.txt')
666
+
667
+ const twoDeep = walkShapes(ctx.root, (await fs.walk?.(ctx.root, { depth: 2 })) ?? [])
668
+ expect(twoDeep.map((entry) => entry.path)).toContain('sub/b.txt')
669
+ expect(twoDeep.map((entry) => entry.path)).not.toContain('sub/deep/c.txt')
670
+ },
671
+ },
672
+ {
673
+ port: 'fs.walk',
674
+ name: 'honours limit, in traversal order',
675
+ async run(ctx) {
676
+ const fs = ctx.platform.fs
677
+ await buildWalkTree(ctx)
678
+
679
+ const all = (await fs.walk?.(ctx.root)) ?? []
680
+ expect(all.length).toBeGreaterThan(3)
681
+
682
+ const limited = (await fs.walk?.(ctx.root, { limit: 3 })) ?? []
683
+ expect(limited.length).toBe(3)
684
+ // "Entries to return at most, in traversal order" — so a prefix, not a sample.
685
+ expect(limited.map((entry) => entry.path)).toEqual(all.slice(0, 3).map((entry) => entry.path))
686
+
687
+ expect(((await fs.walk?.(ctx.root, { limit: all.length + 10 })) ?? []).length).toBe(all.length)
688
+ expect(((await fs.walk?.(ctx.root, { limit: 0 })) ?? []).length).toBe(0)
689
+ },
690
+ },
691
+ {
692
+ port: 'fs.walk',
693
+ name: 'honours exclude, and does not descend into an excluded directory',
694
+ async run(ctx) {
695
+ const fs = ctx.platform.fs
696
+ await buildWalkTree(ctx)
697
+
698
+ const walked = walkShapes(ctx.root, (await fs.walk?.(ctx.root, { exclude: ['skipme'] })) ?? [])
699
+ expect(walked).toEqual(walkShapes(ctx.root, await walkWithReaddir(fs, ctx.root, { exclude: ['skipme'] })))
700
+ expect(walked.map((entry) => entry.path)).not.toContain('skipme')
701
+ expect(walked.map((entry) => entry.path)).not.toContain('skipme/d.txt')
702
+ expect(walked.map((entry) => entry.path)).toContain('a.txt')
703
+ },
704
+ },
705
+ {
706
+ port: 'fs.walk',
707
+ name: 'honours excludeHidden, and does not descend into a hidden directory',
708
+ async run(ctx) {
709
+ const fs = ctx.platform.fs
710
+ await buildWalkTree(ctx)
711
+
712
+ const walked = walkShapes(ctx.root, (await fs.walk?.(ctx.root, { excludeHidden: true })) ?? [])
713
+ expect(walked).toEqual(walkShapes(ctx.root, await walkWithReaddir(fs, ctx.root, { excludeHidden: true })))
714
+ for (const entry of walked) {
715
+ expect(entry.path.split('/').some((segment) => segment.startsWith('.'))).toBe(false)
716
+ }
717
+ expect(walked.map((entry) => entry.path)).toContain('a.txt')
718
+ },
719
+ },
720
+ {
721
+ port: 'fs.walk',
722
+ needs: ['fs.symlinks'],
723
+ name: 'reports a symlink as one, carrying the size stat gives its target',
724
+ async run(ctx) {
725
+ const fs = ctx.platform.fs
726
+ await fs.writeFile(ctx.path('target.txt'), 'abc')
727
+ await ctx.symlink(ctx.path('target.txt'), ctx.path('good.link'))
728
+ await ctx.symlink(ctx.path('nowhere.txt'), ctx.path('broken.link'))
729
+
730
+ const walked = walkShapes(ctx.root, (await fs.walk?.(ctx.root)) ?? [])
731
+ // The loop this replaces stats through the link, so a walk that reported
732
+ // the link's own size would disagree with it for the same tree.
733
+ expect(walked).toEqual([
734
+ { path: 'broken.link', type: 'symlink', size: 0 },
735
+ { path: 'good.link', type: 'symlink', size: 3 },
736
+ { path: 'target.txt', type: 'file', size: 3 },
737
+ ])
738
+ },
739
+ },
740
+ {
741
+ port: 'fs.walk',
742
+ name: 'rejects for a missing directory, as the readdir it replaces would',
743
+ async run(ctx) {
744
+ await rejection(Promise.resolve(ctx.platform.fs.walk?.(ctx.path('missing'))), 'walk of a missing directory')
745
+ },
746
+ },
747
+ ]
748
+
749
+ const readFilesChecks: ConformanceCheck[] = [
750
+ {
751
+ port: 'fs.readFiles',
752
+ name: 'agrees with the readFile loop it replaces, in the order asked',
753
+ async run({ platform, path }) {
754
+ const fs = platform.fs
755
+ await fs.mkdir(path('sub'), { recursive: true })
756
+ await fs.writeFile(path('a.txt'), 'aaa')
757
+ await fs.writeFile(path('sub', 'b.txt'), 'bb')
758
+ await fs.writeFile(path('c.txt'), '')
759
+
760
+ // Deliberately not sorted: the contract fixes the order asked, not a sort.
761
+ const paths = [path('sub', 'b.txt'), path('c.txt'), path('a.txt')]
762
+ const batched = (await fs.readFiles?.(paths)) ?? []
763
+ const looped = await readFilesWithLoop(fs, paths)
764
+
765
+ expect(batched.map((entry) => entry.path)).toEqual(paths)
766
+ expect(batched.map((entry) => (entry.content ? textOf(entry.content) : undefined)))
767
+ .toEqual(looped.map((entry) => (entry.content ? textOf(entry.content) : undefined)))
768
+ expect(batched.map((entry) => entry.error)).toEqual([undefined, undefined, undefined])
769
+ },
770
+ },
771
+ {
772
+ port: 'fs.readFiles',
773
+ name: 'reports a missing path per entry instead of throwing',
774
+ async run({ platform, path }) {
775
+ const fs = platform.fs
776
+ await fs.writeFile(path('a.txt'), 'aaa')
777
+ await fs.writeFile(path('b.txt'), 'bbb')
778
+
779
+ const paths = [path('a.txt'), path('missing.txt'), path('b.txt')]
780
+ const batched = (await fs.readFiles?.(paths)) ?? []
781
+
782
+ expect(batched.map((entry) => entry.path)).toEqual(paths)
783
+ expect(batched[0]?.content && textOf(batched[0].content)).toBe('aaa')
784
+ expect(batched[2]?.content && textOf(batched[2].content)).toBe('bbb')
785
+
786
+ const missing = batched[1]
787
+ expect(missing?.content).toBeUndefined()
788
+ expect(typeof missing?.error).toBe('string')
789
+ expect(missing?.error?.length).toBeGreaterThan(0)
790
+
791
+ // "The code `readFile` would have thrown for this path."
792
+ const thrown = errorCode(await rejection(fs.readFile(path('missing.txt')), 'readFile of a missing path'))
793
+ if (thrown !== undefined) expect(missing?.error).toBe(thrown)
794
+ },
795
+ },
796
+ {
797
+ port: 'fs.readFiles',
798
+ name: 'reads a directory the same way the loop does',
799
+ async run({ platform, path }) {
800
+ const fs = platform.fs
801
+ await fs.mkdir(path('dir'), { recursive: true })
802
+
803
+ const batched = (await fs.readFiles?.([path('dir')])) ?? []
804
+ const looped = await readFilesWithLoop(fs, [path('dir')])
805
+ expect(batched.length).toBe(1)
806
+ expect(batched[0]?.error !== undefined).toBe(looped[0]?.error !== undefined)
807
+ },
808
+ },
809
+ ]
810
+
811
+ const writeFilesChecks: ConformanceCheck[] = [
812
+ {
813
+ port: 'fs.writeFiles',
814
+ name: 'agrees with the writeFile loop it replaces',
815
+ async run({ platform, path }) {
816
+ const fs = platform.fs
817
+ await fs.writeFile(path('c.txt'), 'stale')
818
+
819
+ await fs.writeFiles?.([
820
+ { path: path('a.txt'), content: 'aaa' },
821
+ { path: path('b.txt'), content: new Uint8Array([1, 2, 3]) },
822
+ { path: path('c.txt'), content: 'fresh' },
823
+ ])
824
+
825
+ expect(await fs.readFile(path('a.txt'), 'utf-8')).toBe('aaa')
826
+ expect([...Buffer.from(await fs.readFile(path('b.txt')))]).toEqual([1, 2, 3])
827
+ expect(await fs.readFile(path('c.txt'), 'utf-8')).toBe('fresh')
828
+ },
829
+ },
830
+ {
831
+ port: 'fs.writeFiles',
832
+ name: 'creates parents only when asked to',
833
+ async run({ platform, path }) {
834
+ const fs = platform.fs
835
+
836
+ await rejection(
837
+ Promise.resolve(fs.writeFiles?.([{ path: path('missing', 'a.txt'), content: 'a' }])),
838
+ 'writeFiles into a missing directory without createParents',
839
+ )
840
+ expect(await fs.exists(path('missing'))).toBe(false)
841
+
842
+ await fs.writeFiles?.([{ path: path('made', 'deep', 'a.txt'), content: 'a' }], { createParents: true })
843
+ expect(await fs.readFile(path('made', 'deep', 'a.txt'), 'utf-8')).toBe('a')
844
+ },
845
+ },
846
+ ]
847
+
848
+ const rmFilesChecks: ConformanceCheck[] = [
849
+ {
850
+ port: 'fs.rmFiles',
851
+ name: 'removes every path it is given',
852
+ async run({ platform, path }) {
853
+ const fs = platform.fs
854
+ await fs.writeFile(path('a.txt'), 'a')
855
+ await fs.writeFile(path('b.txt'), 'b')
856
+ await fs.writeFile(path('c.txt'), 'c')
857
+
858
+ await fs.rmFiles?.([path('a.txt'), path('b.txt')])
859
+
860
+ expect(await fs.exists(path('a.txt'))).toBe(false)
861
+ expect(await fs.exists(path('b.txt'))).toBe(false)
862
+ expect(await fs.exists(path('c.txt'))).toBe(true)
863
+ },
864
+ },
865
+ {
866
+ port: 'fs.rmFiles',
867
+ name: 'raises what rm raises for a missing path, and force silences it the same way',
868
+ async run({ platform, path }) {
869
+ const fs = platform.fs
870
+ await fs.writeFile(path('a.txt'), 'a')
871
+
872
+ await rejection(
873
+ Promise.resolve(fs.rmFiles?.([path('missing.txt')])),
874
+ 'rmFiles of a missing path without force',
875
+ )
876
+ await fs.rmFiles?.([path('missing.txt'), path('a.txt')], { force: true })
877
+ expect(await fs.exists(path('a.txt'))).toBe(false)
878
+ },
879
+ },
880
+ {
881
+ port: 'fs.rmFiles',
882
+ name: 'removes a populated directory only when recursive',
883
+ async run({ platform, path }) {
884
+ const fs = platform.fs
885
+ await fs.mkdir(path('tree', 'nested'), { recursive: true })
886
+ await fs.writeFile(path('tree', 'nested', 'leaf.txt'), 'leaf')
887
+
888
+ await rejection(
889
+ Promise.resolve(fs.rmFiles?.([path('tree')])),
890
+ 'rmFiles of a populated directory without recursive',
891
+ )
892
+ expect(await fs.exists(path('tree', 'nested', 'leaf.txt'))).toBe(true)
893
+
894
+ await fs.rmFiles?.([path('tree')], { recursive: true })
895
+ expect(await fs.exists(path('tree'))).toBe(false)
896
+ },
897
+ },
898
+ ]
899
+
900
+ const scopeReadsChecks: ConformanceCheck[] = [
901
+ {
902
+ port: 'fs.scopeReads',
903
+ name: 'returns what the block returns, and the same reads it would answer outside',
904
+ async run({ platform, path }) {
905
+ const fs = platform.fs
906
+ await fs.mkdir(path('sub'), { recursive: true })
907
+ await fs.writeFile(path('a.txt'), 'aaa')
908
+ await fs.writeFile(path('sub', 'b.txt'), 'bb')
909
+
910
+ const outside = [await fs.readFile(path('a.txt'), 'utf-8'), (await fs.readdir(path())).sort().join(',')]
911
+ const inside = await fs.scopeReads?.(async () => [
912
+ await fs.readFile(path('a.txt'), 'utf-8'),
913
+ (await fs.readdir(path())).sort().join(','),
914
+ ])
915
+
916
+ expect(inside).toEqual(outside)
917
+ },
918
+ },
919
+ {
920
+ port: 'fs.scopeReads',
921
+ name: 'propagates the block rejection rather than swallowing it',
922
+ async run({ platform }) {
923
+ const marker = new Error('block failed')
924
+ const caught = await rejection(
925
+ Promise.resolve(platform.fs.scopeReads?.(async () => {
926
+ throw marker
927
+ })),
928
+ 'scopeReads over a block that throws',
929
+ )
930
+ expect(caught).toBe(marker)
931
+ },
932
+ },
933
+ {
934
+ port: 'fs.scopeReads',
935
+ name: 'a write inside the scope is visible to a read after it',
936
+ async run({ platform, path }) {
937
+ const fs = platform.fs
938
+ await fs.scopeReads?.(async () => {
939
+ await fs.writeFile(path('a.txt'), 'written')
940
+ })
941
+ expect(await fs.readFile(path('a.txt'), 'utf-8')).toBe('written')
942
+ },
943
+ },
944
+ ]
945
+
946
+ // ============================================================================
947
+ // scheduler
948
+ // ============================================================================
949
+
950
+ /** Keys a wake was delivered for, in the order they arrived. */
951
+ function collectWakes(platform: Platform): string[] {
952
+ const fired: string[] = []
953
+ if (isLiveScheduler(platform.scheduler)) platform.scheduler.onWake((key) => void fired.push(key))
954
+ return fired
955
+ }
956
+
957
+ const schedulerChecks: ConformanceCheck[] = [
958
+ {
959
+ port: 'scheduler',
960
+ name: 'wake and cancel resolve, and cancel of an unarmed key is a no-op',
961
+ async run({ platform }) {
962
+ await platform.scheduler.cancel('never-armed')
963
+ await platform.scheduler.wake('armed', 20)
964
+ await platform.scheduler.cancel('armed')
965
+ await platform.scheduler.cancel('armed')
966
+ },
967
+ },
968
+ {
969
+ port: 'scheduler.live',
970
+ name: 'a wake fires after its delay, once, with its own key',
971
+ async run({ platform }) {
972
+ const fired = collectWakes(platform)
973
+
974
+ await platform.scheduler.wake('alpha', 40)
975
+ expect(fired).toEqual([])
976
+
977
+ await waitUntil(() => fired.length > 0, 2000, 'the wake for alpha')
978
+ await sleep(80)
979
+ expect(fired).toEqual(['alpha'])
980
+ },
981
+ },
982
+ {
983
+ port: 'scheduler.live',
984
+ name: 'a second wake for the same key replaces the first rather than adding one',
985
+ async run({ platform }) {
986
+ const fired = collectWakes(platform)
987
+
988
+ await platform.scheduler.wake('alpha', 20)
989
+ await platform.scheduler.wake('alpha', 400)
990
+
991
+ // Past the first deadline by a wide margin: the replaced wake must not fire.
992
+ await sleep(150)
993
+ expect(fired).toEqual([])
994
+
995
+ await waitUntil(() => fired.length > 0, 3000, 'the replacing wake for alpha')
996
+ await sleep(80)
997
+ expect(fired).toEqual(['alpha'])
998
+ },
999
+ },
1000
+ {
1001
+ port: 'scheduler.live',
1002
+ name: 'cancel before the delay stops it',
1003
+ async run({ platform }) {
1004
+ const fired = collectWakes(platform)
1005
+
1006
+ await platform.scheduler.wake('alpha', 60)
1007
+ await platform.scheduler.wake('beta', 60)
1008
+ await platform.scheduler.cancel('alpha')
1009
+
1010
+ await waitUntil(() => fired.length > 0, 2000, 'the wake for beta')
1011
+ await sleep(120)
1012
+ expect(fired).toEqual(['beta'])
1013
+ },
1014
+ },
1015
+ {
1016
+ port: 'scheduler.live',
1017
+ name: 'a cancel-then-wake pair issued without awaiting lands in that order',
1018
+ async run({ platform }) {
1019
+ const fired = collectWakes(platform)
1020
+
1021
+ await platform.scheduler.wake('alpha', 500)
1022
+ // Exactly how callers issue it: neither promise awaited before the next call.
1023
+ const cancelled = platform.scheduler.cancel('alpha')
1024
+ const rearmed = platform.scheduler.wake('alpha', 40)
1025
+ await Promise.all([cancelled, rearmed])
1026
+
1027
+ await waitUntil(() => fired.length > 0, 2000, 'the re-armed wake for alpha')
1028
+ await sleep(80)
1029
+ expect(fired).toEqual(['alpha'])
1030
+ },
1031
+ },
1032
+ {
1033
+ port: 'scheduler.live',
1034
+ name: 'onWake replaces any previous handler',
1035
+ async run({ platform }) {
1036
+ const scheduler = platform.scheduler
1037
+ if (!isLiveScheduler(scheduler)) throw new Error('scheduler.live checks need a LiveScheduler')
1038
+
1039
+ const first: string[] = []
1040
+ const second: string[] = []
1041
+ scheduler.onWake((key) => void first.push(key))
1042
+ scheduler.onWake((key) => void second.push(key))
1043
+
1044
+ await scheduler.wake('alpha', 20)
1045
+ await waitUntil(() => second.length > 0, 2000, 'the wake for alpha')
1046
+ await sleep(60)
1047
+
1048
+ expect(second).toEqual(['alpha'])
1049
+ expect(first).toEqual([])
1050
+ },
1051
+ },
1052
+ ]
1053
+
1054
+ // ============================================================================
1055
+ // shell
1056
+ // ============================================================================
1057
+
1058
+ const shellRun = (platform: Platform, options: ShellRunOptions) => {
1059
+ if (!platform.shell) throw new Error('shell checks need platform.shell')
1060
+ return platform.shell.run(options)
1061
+ }
1062
+
1063
+ const shellChecks: ConformanceCheck[] = [
1064
+ {
1065
+ port: 'shell',
1066
+ name: 'declares one of the three confinements',
1067
+ async run({ platform }) {
1068
+ const declared: readonly (ShellConfinement | undefined)[] = ['paths', 'host', 'none']
1069
+ expect(declared).toContain(platform.shell?.confinement)
1070
+ },
1071
+ },
1072
+ {
1073
+ port: 'shell',
1074
+ name: 'a non-zero exit resolves rather than rejects',
1075
+ async run({ platform, root }) {
1076
+ const result = await shellRun(platform, { command: 'exit 3', cwd: root, timeoutMs: 10_000 })
1077
+ expect(result.exitCode).toBe(3)
1078
+ expect(result.timedOut).toBe(false)
1079
+ },
1080
+ },
1081
+ {
1082
+ port: 'shell',
1083
+ name: 'buffers stdout and stderr separately',
1084
+ async run({ platform, root }) {
1085
+ const result = await shellRun(platform, {
1086
+ command: 'printf out; printf err 1>&2',
1087
+ cwd: root,
1088
+ timeoutMs: 10_000,
1089
+ })
1090
+ expect(result.exitCode).toBe(0)
1091
+ expect(result.stdout).toContain('out')
1092
+ expect(result.stderr).toContain('err')
1093
+ expect(result.stdout).not.toContain('err')
1094
+ },
1095
+ },
1096
+ {
1097
+ port: 'shell',
1098
+ name: 'stdin reaches the command',
1099
+ async run({ platform, root }) {
1100
+ const result = await shellRun(platform, {
1101
+ command: 'cat',
1102
+ cwd: root,
1103
+ stdin: 'conformance-stdin\n',
1104
+ timeoutMs: 10_000,
1105
+ })
1106
+ expect(result.exitCode).toBe(0)
1107
+ expect(result.stdout).toContain('conformance-stdin')
1108
+ },
1109
+ },
1110
+ {
1111
+ port: 'shell',
1112
+ name: 'cwd is where the command runs',
1113
+ async run({ platform, root }) {
1114
+ const result = await shellRun(platform, { command: 'pwd', cwd: root, timeoutMs: 10_000 })
1115
+ expect([root, await platform.fs.realpath(root)]).toContain(result.stdout.trim())
1116
+ },
1117
+ },
1118
+ {
1119
+ port: 'shell',
1120
+ name: 'env reaches the command',
1121
+ async run({ platform, root }) {
1122
+ const result = await shellRun(platform, {
1123
+ command: 'printf %s "$ROJ_CONFORMANCE"',
1124
+ cwd: root,
1125
+ env: { ROJ_CONFORMANCE: 'present' },
1126
+ timeoutMs: 10_000,
1127
+ })
1128
+ expect(result.stdout).toContain('present')
1129
+ },
1130
+ },
1131
+ {
1132
+ port: 'shell',
1133
+ name: 'a timeout terminates the command and reports timedOut',
1134
+ async run({ platform, root }) {
1135
+ const started = Date.now()
1136
+ const result = await shellRun(platform, { command: 'sleep 30', cwd: root, timeoutMs: 300 })
1137
+ expect(result.timedOut).toBe(true)
1138
+ expect(Date.now() - started).toBeLessThan(20_000)
1139
+ expect(result.exitCode).not.toBe(0)
1140
+ },
1141
+ },
1142
+ {
1143
+ port: 'shell.paths',
1144
+ name: 'a granted path is reachable and one outside the grants is not',
1145
+ async run({ platform, path }) {
1146
+ const fs = platform.fs
1147
+ await fs.mkdir(path('granted'), { recursive: true })
1148
+ await fs.mkdir(path('ungranted'), { recursive: true })
1149
+ await fs.writeFile(path('granted', 'inside.txt'), 'inside')
1150
+ await fs.writeFile(path('ungranted', 'secret.txt'), 'secret')
1151
+
1152
+ const seen = path('visible')
1153
+ const grants: ShellGrant[] = [{ path: seen, source: path('granted'), mode: 'rw' }]
1154
+
1155
+ const reachable = await shellRun(platform, {
1156
+ command: 'cat inside.txt',
1157
+ cwd: seen,
1158
+ grants,
1159
+ timeoutMs: 20_000,
1160
+ })
1161
+ expect(reachable.exitCode).toBe(0)
1162
+ expect(reachable.stdout).toContain('inside')
1163
+
1164
+ const blocked = await shellRun(platform, {
1165
+ command: `cat ${path('ungranted', 'secret.txt')}`,
1166
+ cwd: seen,
1167
+ grants,
1168
+ timeoutMs: 20_000,
1169
+ })
1170
+ expect(blocked.exitCode).not.toBe(0)
1171
+ expect(blocked.stdout).not.toContain('secret')
1172
+ },
1173
+ },
1174
+ {
1175
+ port: 'shell.paths',
1176
+ name: 'a read-only grant is readable and not writable',
1177
+ async run({ platform, path }) {
1178
+ const fs = platform.fs
1179
+ await fs.mkdir(path('granted'), { recursive: true })
1180
+ await fs.writeFile(path('granted', 'inside.txt'), 'inside')
1181
+
1182
+ const seen = path('visible')
1183
+ const grants: ShellGrant[] = [{ path: seen, source: path('granted'), mode: 'ro' }]
1184
+
1185
+ const read = await shellRun(platform, { command: 'cat inside.txt', cwd: seen, grants, timeoutMs: 20_000 })
1186
+ expect(read.exitCode).toBe(0)
1187
+
1188
+ const write = await shellRun(platform, {
1189
+ command: 'printf x > written.txt',
1190
+ cwd: seen,
1191
+ grants,
1192
+ timeoutMs: 20_000,
1193
+ })
1194
+ expect(write.exitCode).not.toBe(0)
1195
+ expect(await fs.exists(path('granted', 'written.txt'))).toBe(false)
1196
+ },
1197
+ },
1198
+ {
1199
+ port: 'shell.host',
1200
+ name: 'needs no grants — the filesystem holds nothing but this session',
1201
+ async run({ platform, path, root }) {
1202
+ await platform.fs.writeFile(path('inside.txt'), 'inside')
1203
+
1204
+ const result = await shellRun(platform, { command: 'cat inside.txt', cwd: root, timeoutMs: 20_000 })
1205
+ expect(result.exitCode).toBe(0)
1206
+ expect(result.stdout).toContain('inside')
1207
+ },
1208
+ },
1209
+ ]
1210
+
1211
+ // ============================================================================
1212
+ // git
1213
+ // ============================================================================
1214
+
1215
+ const gitChecks: ConformanceCheck[] = [
1216
+ {
1217
+ port: 'git',
1218
+ name: 'status reports the non-clean paths, and nothing else',
1219
+ async run({ platform, gitRepo }) {
1220
+ const dir = await gitRepo()
1221
+ const status = (await platform.git?.status({ dir })) ?? []
1222
+
1223
+ expect([...status].sort((a, b) => a.path.localeCompare(b.path))).toEqual([
1224
+ { path: GIT_FIXTURE.modified, index: ' ', worktree: 'M' },
1225
+ { path: GIT_FIXTURE.untracked, index: ' ', worktree: '?' },
1226
+ ])
1227
+ },
1228
+ },
1229
+ {
1230
+ port: 'git',
1231
+ name: 'log returns commits newest first, in milliseconds, and honours depth',
1232
+ async run({ platform, gitRepo }) {
1233
+ const dir = await gitRepo()
1234
+
1235
+ const head = (await platform.git?.log({ dir, depth: 1 })) ?? []
1236
+ expect(head.length).toBe(1)
1237
+ expect(head[0]?.message.split('\n')[0]).toBe(GIT_FIXTURE.headSubject)
1238
+ expect(head[0]?.oid.length).toBeGreaterThan(0)
1239
+ // Milliseconds, "like every other SDK timestamp" — seconds would be ~1.7e9.
1240
+ expect(head[0]?.committedAt).toBeGreaterThan(1e12)
1241
+
1242
+ const all = (await platform.git?.log({ dir })) ?? []
1243
+ expect(all.length).toBeGreaterThanOrEqual(2)
1244
+ expect(all[0]?.message.split('\n')[0]).toBe(GIT_FIXTURE.headSubject)
1245
+ expect(all[all.length - 1]?.message.split('\n')[0]).toBe(GIT_FIXTURE.baseSubject)
1246
+ expect(new Set(all.map((commit) => commit.oid)).size).toBe(all.length)
1247
+
1248
+ const base = (await platform.git?.log({ dir, ref: GIT_FIXTURE.base, depth: 1 })) ?? []
1249
+ expect(base[0]?.message.split('\n')[0]).toBe(GIT_FIXTURE.baseSubject)
1250
+ },
1251
+ },
1252
+ {
1253
+ port: 'git',
1254
+ name: 'countAhead counts what is in ref and not in base',
1255
+ async run({ platform, gitRepo }) {
1256
+ const dir = await gitRepo()
1257
+
1258
+ expect(await platform.git?.countAhead({ dir, base: GIT_FIXTURE.base })).toBe(1)
1259
+ expect(await platform.git?.countAhead({ dir, base: GIT_FIXTURE.branch })).toBe(0)
1260
+ expect(await platform.git?.countAhead({ dir, base: GIT_FIXTURE.base, ref: GIT_FIXTURE.base })).toBe(0)
1261
+ },
1262
+ },
1263
+ {
1264
+ port: 'git',
1265
+ name: 'defaultBranch answers a branch name or undefined',
1266
+ async run({ platform, gitRepo }) {
1267
+ const dir = await gitRepo()
1268
+ const branch = await platform.git?.defaultBranch({ dir })
1269
+ expect(branch === undefined || (typeof branch === 'string' && branch.length > 0)).toBe(true)
1270
+ },
1271
+ },
1272
+ {
1273
+ port: 'git',
1274
+ name: 'rejects outside a repository, so the caller reads it as no git state',
1275
+ async run({ platform, path }) {
1276
+ const dir = path('not-a-repo')
1277
+ await platform.fs.mkdir(dir, { recursive: true })
1278
+
1279
+ await rejection(Promise.resolve(platform.git?.status({ dir })), 'status outside a repository')
1280
+ await rejection(Promise.resolve(platform.git?.log({ dir })), 'log outside a repository')
1281
+ await rejection(
1282
+ Promise.resolve(platform.git?.countAhead({ dir, base: GIT_FIXTURE.base })),
1283
+ 'countAhead outside a repository',
1284
+ )
1285
+
1286
+ // `defaultBranch` says undefined means unknown, so outside a repo it may answer either way.
1287
+ const branch = await platform.git?.defaultBranch({ dir }).catch(() => undefined)
1288
+ expect(branch).toBeUndefined()
1289
+ },
1290
+ },
1291
+ ]
1292
+
1293
+ // ============================================================================
1294
+ // fsRevision
1295
+ // ============================================================================
1296
+
1297
+ const fsRevisionChecks: ConformanceCheck[] = [
1298
+ {
1299
+ port: 'fsRevision',
1300
+ name: 'current never rejects — a host that cannot answer returns undefined',
1301
+ async run({ platform, path }) {
1302
+ const first = await platform.fsRevision?.current()
1303
+ expect(first === undefined || typeof first === 'number').toBe(true)
1304
+
1305
+ await platform.fs.writeFile(path('a.txt'), 'a')
1306
+ const second = await platform.fsRevision?.current()
1307
+ expect(second === undefined || typeof second === 'number').toBe(true)
1308
+ },
1309
+ },
1310
+ {
1311
+ port: 'fsRevision.numbered',
1312
+ name: 'the number moves after a write',
1313
+ async run({ platform, path }) {
1314
+ const before = await platform.fsRevision?.current()
1315
+ await platform.fs.writeFile(path('a.txt'), 'a')
1316
+ const after = await platform.fsRevision?.current()
1317
+
1318
+ expect(typeof after).toBe('number')
1319
+ expect(after).not.toBe(before)
1320
+ },
1321
+ },
1322
+ {
1323
+ port: 'fsRevision.numbered',
1324
+ name: 'the number stands still without one',
1325
+ async run({ platform, path }) {
1326
+ await platform.fs.writeFile(path('a.txt'), 'a')
1327
+
1328
+ const first = await platform.fsRevision?.current()
1329
+ const second = await platform.fsRevision?.current()
1330
+ await platform.fs.readFile(path('a.txt'))
1331
+ const third = await platform.fsRevision?.current()
1332
+
1333
+ expect(second).toBe(first)
1334
+ expect(third).toBe(first)
1335
+ },
1336
+ },
1337
+ ]
1338
+
1339
+ // ============================================================================
1340
+ // sessionLog
1341
+ // ============================================================================
1342
+
1343
+ const SESSION_A = 'conformance-session-a'
1344
+ const SESSION_B = 'conformance-session-b'
1345
+
1346
+ const sessionLogChecks: ConformanceCheck[] = [
1347
+ {
1348
+ port: 'sessionLog',
1349
+ name: 'append returns nothing and round-trips the lines in order',
1350
+ async run({ platform }) {
1351
+ const store = platform.sessionLog
1352
+ if (!store) throw new Error('sessionLog checks need platform.sessionLog')
1353
+
1354
+ expect(store.append(SESSION_A, '{"n":1}')).toBeUndefined()
1355
+ store.append(SESSION_A, '{"n":2}')
1356
+ store.append(SESSION_A, '{"n":3}')
1357
+
1358
+ const page = await store.read(SESSION_A, 0)
1359
+ expect(page.lines).toEqual(['{"n":1}', '{"n":2}', '{"n":3}'])
1360
+ },
1361
+ },
1362
+ {
1363
+ port: 'sessionLog',
1364
+ name: 'the cursor returns what followed it, and only that',
1365
+ async run({ platform }) {
1366
+ const store = platform.sessionLog
1367
+ if (!store) throw new Error('sessionLog checks need platform.sessionLog')
1368
+
1369
+ store.append(SESSION_A, '{"n":1}')
1370
+ store.append(SESSION_A, '{"n":2}')
1371
+
1372
+ const first = await store.read(SESSION_A, 0)
1373
+ expect(first.lines.length).toBe(2)
1374
+
1375
+ // Nothing new: the same cursor comes back with nothing behind it.
1376
+ expect((await store.read(SESSION_A, first.offset)).lines).toEqual([])
1377
+ expect((await store.read(SESSION_A, first.offset)).offset).toBe(first.offset)
1378
+
1379
+ store.append(SESSION_A, '{"n":3}')
1380
+ const second = await store.read(SESSION_A, first.offset)
1381
+ expect(second.lines).toEqual(['{"n":3}'])
1382
+ // Re-reading the same cursor answers the same — a cursor is a position, not a queue.
1383
+ expect((await store.read(SESSION_A, first.offset)).lines).toEqual(['{"n":3}'])
1384
+ expect((await store.read(SESSION_A, second.offset)).lines).toEqual([])
1385
+ },
1386
+ },
1387
+ {
1388
+ port: 'sessionLog',
1389
+ name: 'a session with nothing in it reads empty',
1390
+ async run({ platform }) {
1391
+ const store = platform.sessionLog
1392
+ if (!store) throw new Error('sessionLog checks need platform.sessionLog')
1393
+
1394
+ expect((await store.read('conformance-never-written', 0)).lines).toEqual([])
1395
+ },
1396
+ },
1397
+ {
1398
+ port: 'sessionLog',
1399
+ name: 'one session does not see another',
1400
+ async run({ platform }) {
1401
+ const store = platform.sessionLog
1402
+ if (!store) throw new Error('sessionLog checks need platform.sessionLog')
1403
+
1404
+ store.append(SESSION_A, '{"who":"a"}')
1405
+ store.append(SESSION_B, '{"who":"b"}')
1406
+
1407
+ expect((await store.read(SESSION_A, 0)).lines).toEqual(['{"who":"a"}'])
1408
+ expect((await store.read(SESSION_B, 0)).lines).toEqual(['{"who":"b"}'])
1409
+ },
1410
+ },
1411
+ {
1412
+ port: 'sessionLog',
1413
+ name: 'delete drops one session and reports how many went',
1414
+ async run({ platform }) {
1415
+ const store = platform.sessionLog
1416
+ if (!store) throw new Error('sessionLog checks need platform.sessionLog')
1417
+
1418
+ store.append(SESSION_A, '{"n":1}')
1419
+ store.append(SESSION_A, '{"n":2}')
1420
+ store.append(SESSION_B, '{"n":1}')
1421
+
1422
+ expect(await store.delete(SESSION_A)).toBe(2)
1423
+ expect((await store.read(SESSION_A, 0)).lines).toEqual([])
1424
+ expect((await store.read(SESSION_B, 0)).lines).toEqual(['{"n":1}'])
1425
+ expect(await store.delete('conformance-never-written')).toBe(0)
1426
+ },
1427
+ },
1428
+ ]
1429
+
1430
+ // ============================================================================
1431
+ // llmCallLog
1432
+ // ============================================================================
1433
+
1434
+ /** Fixed-width and ordered, the way a UUIDv7 is — so `list` can be checked on it. */
1435
+ const callId = (n: number): string => `0198e9c0-0000-7000-8000-${String(n).padStart(12, '0')}`
1436
+
1437
+ function callRow(n: number): LLMCallRow {
1438
+ return {
1439
+ callId: callId(n),
1440
+ agentId: 'conformance-agent',
1441
+ createdAt: 1_700_000_000_000 + n,
1442
+ status: 'running',
1443
+ model: 'test-model',
1444
+ request: JSON.stringify({ n }),
1445
+ }
1446
+ }
1447
+
1448
+ const llmCallLogChecks: ConformanceCheck[] = [
1449
+ {
1450
+ port: 'llmCallLog',
1451
+ name: 'create and get round-trip the row',
1452
+ async run({ platform }) {
1453
+ const store = platform.llmCallLog
1454
+ if (!store) throw new Error('llmCallLog checks need platform.llmCallLog')
1455
+
1456
+ if (store.maxBlobBytes !== undefined) expect(store.maxBlobBytes).toBeGreaterThan(0)
1457
+
1458
+ const row = callRow(1)
1459
+ await store.create(SESSION_A, row)
1460
+
1461
+ expect(await store.get(SESSION_A, row.callId)).toEqual(row)
1462
+ expect(await store.get(SESSION_A, callId(999))).toBeNull()
1463
+ expect(await store.get(SESSION_B, row.callId)).toBeNull()
1464
+ },
1465
+ },
1466
+ {
1467
+ port: 'llmCallLog',
1468
+ name: 'complete attaches the outcome and leaves the request alone',
1469
+ async run({ platform }) {
1470
+ const store = platform.llmCallLog
1471
+ if (!store) throw new Error('llmCallLog checks need platform.llmCallLog')
1472
+
1473
+ const row = callRow(1)
1474
+ await store.create(SESSION_A, row)
1475
+ await store.complete(SESSION_A, row.callId, {
1476
+ status: 'success',
1477
+ completedAt: 1_700_000_005_000,
1478
+ durationMs: 5000,
1479
+ providerRequestId: 'req-1',
1480
+ response: '{"text":"hi"}',
1481
+ metrics: '{"tokens":7}',
1482
+ })
1483
+
1484
+ const stored = await store.get(SESSION_A, row.callId)
1485
+ expect(stored?.status).toBe('success')
1486
+ expect(stored?.completedAt).toBe(1_700_000_005_000)
1487
+ expect(stored?.durationMs).toBe(5000)
1488
+ expect(stored?.providerRequestId).toBe('req-1')
1489
+ expect(stored?.response).toBe('{"text":"hi"}')
1490
+ expect(stored?.metrics).toBe('{"tokens":7}')
1491
+ expect(stored?.request).toBe(row.request)
1492
+ expect(stored?.error).toBeUndefined()
1493
+ },
1494
+ },
1495
+ {
1496
+ port: 'llmCallLog',
1497
+ name: 'complete on an id the store does not hold is a no-op, not a throw',
1498
+ async run({ platform }) {
1499
+ const store = platform.llmCallLog
1500
+ if (!store) throw new Error('llmCallLog checks need platform.llmCallLog')
1501
+
1502
+ await store.create(SESSION_A, callRow(1))
1503
+ await store.complete(SESSION_A, callId(999), {
1504
+ status: 'error',
1505
+ completedAt: 1_700_000_005_000,
1506
+ durationMs: 5000,
1507
+ error: '{"message":"reaped"}',
1508
+ })
1509
+
1510
+ expect(await store.get(SESSION_A, callId(999))).toBeNull()
1511
+ expect((await store.list(SESSION_A, { limit: 10, offset: 0 })).total).toBe(1)
1512
+ },
1513
+ },
1514
+ {
1515
+ port: 'llmCallLog',
1516
+ name: 'list pages newest first by callId, with a total that ignores limit and offset',
1517
+ async run({ platform }) {
1518
+ const store = platform.llmCallLog
1519
+ if (!store) throw new Error('llmCallLog checks need platform.llmCallLog')
1520
+
1521
+ for (const n of [1, 2, 3]) await store.create(SESSION_A, callRow(n))
1522
+ await store.create(SESSION_B, callRow(9))
1523
+
1524
+ const firstPage = await store.list(SESSION_A, { limit: 2, offset: 0 })
1525
+ expect(firstPage.total).toBe(3)
1526
+ expect(firstPage.calls.map((call) => call.callId)).toEqual([callId(3), callId(2)])
1527
+
1528
+ const secondPage = await store.list(SESSION_A, { limit: 2, offset: 2 })
1529
+ expect(secondPage.total).toBe(3)
1530
+ expect(secondPage.calls.map((call) => call.callId)).toEqual([callId(1)])
1531
+
1532
+ expect((await store.list(SESSION_A, { limit: 10, offset: 10 })).calls).toEqual([])
1533
+ expect((await store.list(SESSION_B, { limit: 10, offset: 0 })).total).toBe(1)
1534
+ expect((await store.list('conformance-never-written', { limit: 10, offset: 0 })).total).toBe(0)
1535
+ },
1536
+ },
1537
+ {
1538
+ port: 'llmCallLog',
1539
+ name: 'delete drops one session and reports how many went',
1540
+ async run({ platform }) {
1541
+ const store = platform.llmCallLog
1542
+ if (!store) throw new Error('llmCallLog checks need platform.llmCallLog')
1543
+
1544
+ for (const n of [1, 2]) await store.create(SESSION_A, callRow(n))
1545
+ await store.create(SESSION_B, callRow(9))
1546
+
1547
+ expect(await store.delete(SESSION_A)).toBe(2)
1548
+ expect((await store.list(SESSION_A, { limit: 10, offset: 0 })).total).toBe(0)
1549
+ expect(await store.get(SESSION_A, callId(1))).toBeNull()
1550
+ expect((await store.list(SESSION_B, { limit: 10, offset: 0 })).total).toBe(1)
1551
+ expect(await store.delete('conformance-never-written')).toBe(0)
1552
+ },
1553
+ },
1554
+ ]
1555
+
1556
+ /** Every clause the ports state, as one list. */
1557
+ export const platformConformanceChecks: readonly ConformanceCheck[] = [
1558
+ ...fsChecks,
1559
+ ...walkChecks,
1560
+ ...readFilesChecks,
1561
+ ...writeFilesChecks,
1562
+ ...rmFilesChecks,
1563
+ ...scopeReadsChecks,
1564
+ ...schedulerChecks,
1565
+ ...shellChecks,
1566
+ ...gitChecks,
1567
+ ...fsRevisionChecks,
1568
+ ...sessionLogChecks,
1569
+ ...llmCallLogChecks,
1570
+ ]
1571
+
1572
+ // ============================================================================
1573
+ // Probing what a host answers
1574
+ // ============================================================================
1575
+
1576
+ /** Which ports the target answers, and why it does not answer the rest. */
1577
+ export async function probePlatformPorts(target: ConformanceTarget): Promise<PortSupport[]> {
1578
+ const instance = await target.create()
1579
+ try {
1580
+ return await probeInstance(target, instance)
1581
+ } finally {
1582
+ await instance.dispose?.()
1583
+ }
1584
+ }
1585
+
1586
+ async function probeInstance(target: ConformanceTarget, instance: PlatformInstance): Promise<PortSupport[]> {
1587
+ const { platform } = instance
1588
+ const support: PortSupport[] = []
1589
+ const add = (port: ConformancePort, answered: boolean, note?: string): void => {
1590
+ support.push(answered ? { port, answered } : { port, answered, note: note ?? 'port absent' })
1591
+ }
1592
+
1593
+ add('fs', true)
1594
+ add('fs.symlinks', await canSymlink(target, instance), 'the host cannot make a symlink and the target declares no `symlink`')
1595
+
1596
+ for (const verb of OPTIONAL_FS_VERBS) {
1597
+ add(`fs.${verb}`, typeof platform.fs[verb] === 'function')
1598
+ }
1599
+
1600
+ add('scheduler', true)
1601
+ add('scheduler.live', isLiveScheduler(platform.scheduler), 'the scheduler does not deliver its own wakes')
1602
+
1603
+ const confinement = platform.shell?.confinement
1604
+ add('shell', platform.shell !== undefined)
1605
+ const confines = await probeConfinement(instance)
1606
+ add('shell.paths', confines.ok, confines.note)
1607
+ add('shell.host', confinement === 'host', `confinement is ${confinement ?? 'absent'}`)
1608
+
1609
+ const buildable = platform.git !== undefined && await canBuildGitRepo(target, platform)
1610
+ add('git', buildable, platform.git === undefined ? undefined : 'no way to build a fixture repository — declare ConformanceTarget.buildGitRepo')
1611
+
1612
+ add('fsRevision', platform.fsRevision !== undefined)
1613
+ const numbered = await probeRevision(platform)
1614
+ add('fsRevision.numbered', numbered.ok, numbered.note)
1615
+
1616
+ add('sessionLog', platform.sessionLog !== undefined)
1617
+ add('llmCallLog', platform.llmCallLog !== undefined)
1618
+
1619
+ return support
1620
+ }
1621
+
1622
+ async function canSymlink(target: ConformanceTarget, instance: PlatformInstance): Promise<boolean> {
1623
+ const probePath = joinPath(instance.root, ['.conformance-symlink-probe'])
1624
+ try {
1625
+ await (target.symlink ?? defaultSymlink)(instance, instance.root, probePath)
1626
+ return await instance.platform.fs.exists(probePath)
1627
+ } catch {
1628
+ return false
1629
+ }
1630
+ }
1631
+
1632
+ async function canBuildGitRepo(target: ConformanceTarget, platform: Platform): Promise<boolean> {
1633
+ if (target.buildGitRepo) return true
1634
+ try {
1635
+ await platform.process.execFile('git', ['--version'], {})
1636
+ return true
1637
+ } catch {
1638
+ return false
1639
+ }
1640
+ }
1641
+
1642
+ /** First line of an error, short enough to carry in a test name. */
1643
+ function messageOf(error: unknown): string {
1644
+ const text = (error instanceof Error ? error.message : String(error)).split('\n')[0] ?? ''
1645
+ return text.length > 120 ? `${text.slice(0, 117)}...` : text
1646
+ }
1647
+
1648
+ /**
1649
+ * Whether the host can run a confined command at all — not whether it confines
1650
+ * correctly, which is what the `shell.paths` checks are for.
1651
+ *
1652
+ * Resolving is the signal, whatever the exit code: a runner that ran and answered
1653
+ * wrongly must fail those checks, and only one that cannot run at all is skipped.
1654
+ */
1655
+ async function probeConfinement(instance: PlatformInstance): Promise<{ ok: boolean; note?: string }> {
1656
+ const { platform } = instance
1657
+ const shell = platform.shell
1658
+ if (!shell) return { ok: false, note: 'port absent' }
1659
+ if (shell.confinement !== 'paths') return { ok: false, note: `confinement is ${shell.confinement}` }
1660
+
1661
+ const dir = joinPath(instance.root, ['.conformance-confinement-probe'])
1662
+ try {
1663
+ await platform.fs.mkdir(dir, { recursive: true })
1664
+ await shell.run({ command: 'exit 0', cwd: dir, grants: [{ path: dir, mode: 'rw' }], timeoutMs: 30_000 })
1665
+ return { ok: true }
1666
+ } catch (error) {
1667
+ return { ok: false, note: `declares \`paths\` confinement but cannot run a confined command: ${messageOf(error)}` }
1668
+ }
1669
+ }
1670
+
1671
+ async function probeRevision(platform: Platform): Promise<{ ok: boolean; note?: string }> {
1672
+ if (!platform.fsRevision) return { ok: false, note: 'port absent' }
1673
+ try {
1674
+ const current = await platform.fsRevision.current()
1675
+ return typeof current === 'number' ? { ok: true } : { ok: false, note: 'current() answers undefined' }
1676
+ } catch {
1677
+ return { ok: false, note: 'current() rejected' }
1678
+ }
1679
+ }
1680
+
1681
+ // ============================================================================
1682
+ // Running
1683
+ // ============================================================================
1684
+
1685
+ /** Run one check against a fresh instance of the target. Throws what it asserts. */
1686
+ export async function runConformanceCheck(target: ConformanceTarget, check: ConformanceCheck): Promise<void> {
1687
+ const instance = await target.create()
1688
+ try {
1689
+ await check.run(contextFor(target, instance))
1690
+ } finally {
1691
+ await instance.dispose?.()
1692
+ }
1693
+ }
1694
+
1695
+ /** The checks a host answering `answered` should be held to. */
1696
+ export function checksFor(answered: Iterable<ConformancePort>): ConformanceCheck[] {
1697
+ const ports = new Set(answered)
1698
+ return platformConformanceChecks.filter((check) =>
1699
+ ports.has(check.port) && (check.needs ?? []).every((port) => ports.has(port))
1700
+ )
1701
+ }
1702
+
1703
+ function describeSkip(support: PortSupport): string {
1704
+ return support.note ? `${support.port} (${support.note})` : support.port
1705
+ }
1706
+
1707
+ /**
1708
+ * Declare the suite against `target`, one section per port.
1709
+ *
1710
+ * Ports the host does not answer are declared skipped rather than dropped, and
1711
+ * the two `ports:` tests carry the exercised and skipped sets in their names —
1712
+ * so a host that answers almost nothing says so in the run's output.
1713
+ */
1714
+ export function runPlatformConformance(target: ConformanceTarget): void {
1715
+ describe(`platform conformance: ${target.name}`, async () => {
1716
+ const support = await probePlatformPorts(target)
1717
+ const exercised = support.filter((entry) => entry.answered).map((entry) => entry.port)
1718
+ const answered = new Set(exercised)
1719
+ const skipped = support.filter((entry) => !entry.answered)
1720
+
1721
+ console.log(
1722
+ `[conformance] ${target.name}\n`
1723
+ + ` exercised: ${exercised.join(', ') || 'none'}\n`
1724
+ + ` skipped: ${skipped.map(describeSkip).join(', ') || 'none'}`,
1725
+ )
1726
+
1727
+ for (const port of CONFORMANCE_PORTS) {
1728
+ const checks = platformConformanceChecks.filter((check) => check.port === port)
1729
+ if (checks.length === 0) continue
1730
+
1731
+ describe(port, () => {
1732
+ for (const check of checks) {
1733
+ const missing = [port, ...(check.needs ?? [])].filter((needed) => !answered.has(needed))
1734
+ const declareTest = missing.length === 0 ? test : test.skip
1735
+ const name = missing.length === 0 ? check.name : `${check.name} — needs ${missing.join(', ')}`
1736
+ declareTest(name, () => runConformanceCheck(target, check))
1737
+ }
1738
+ })
1739
+ }
1740
+
1741
+ test(`ports exercised: ${exercised.join(', ') || 'none'}`, async () => {
1742
+ const again = await probePlatformPorts(target)
1743
+ expect(again.filter((entry) => entry.answered).map((entry) => entry.port)).toEqual(exercised)
1744
+ })
1745
+
1746
+ test(`ports skipped: ${skipped.map(describeSkip).join(', ') || 'none'}`, () => {
1747
+ // A skip with no reason is a silent one, which is the thing this suite must not allow.
1748
+ expect(skipped.filter((entry) => entry.note === undefined)).toEqual([])
1749
+ })
1750
+ })
1751
+ }