functionalscript 0.45.0 → 0.46.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 (296) hide show
  1. package/README.md +5 -3
  2. package/fjs/asn.1/module.f.mjs +8 -1
  3. package/fjs/asn.1/proof.f.d.mts +10 -0
  4. package/fjs/asn.1/proof.f.mjs +16 -0
  5. package/fjs/basen/base128/module.f.mjs +22 -5
  6. package/fjs/bnf/data/module.f.d.mts +32 -6
  7. package/fjs/bnf/data/module.f.mjs +112 -7
  8. package/fjs/bnf/data/proof.f.d.mts +2 -0
  9. package/fjs/bnf/data/proof.f.mjs +74 -3
  10. package/fjs/bnf/data/types.d.ts +20 -2
  11. package/fjs/bnf/descent/module.f.d.mts +28 -14
  12. package/fjs/bnf/descent/module.f.mjs +122 -77
  13. package/fjs/bnf/descent/proof.f.d.mts +2 -0
  14. package/fjs/bnf/descent/proof.f.mjs +117 -31
  15. package/fjs/bnf/descent/types.d.ts +12 -14
  16. package/fjs/bnf/ll1/module.f.d.mts +64 -22
  17. package/fjs/bnf/ll1/module.f.mjs +214 -154
  18. package/fjs/bnf/ll1/proof.f.d.mts +15 -2
  19. package/fjs/bnf/ll1/proof.f.mjs +323 -149
  20. package/fjs/bnf/ll1/types.d.ts +16 -24
  21. package/fjs/bnf/matcher/module.f.d.mts +66 -0
  22. package/fjs/bnf/matcher/module.f.mjs +81 -0
  23. package/fjs/bnf/matcher/proof.f.d.mts +10 -0
  24. package/fjs/bnf/matcher/proof.f.mjs +79 -0
  25. package/fjs/bnf/matcher/types.d.ts +54 -0
  26. package/fjs/bnf/testlib.f.d.mts +31 -0
  27. package/fjs/bnf/testlib.f.mjs +80 -0
  28. package/fjs/cas/cli/module.f.d.mts +1 -1
  29. package/fjs/cas/cli/module.f.mjs +14 -20
  30. package/fjs/cas/cli/proof.f.d.mts +1 -3
  31. package/fjs/cas/cli/proof.f.mjs +44 -33
  32. package/fjs/cas/evo/module.f.d.mts +64 -18
  33. package/fjs/cas/evo/module.f.mjs +148 -70
  34. package/fjs/cas/evo/proof.f.d.mts +10 -1
  35. package/fjs/cas/evo/proof.f.mjs +305 -223
  36. package/fjs/cas/evo/types.d.ts +45 -9
  37. package/fjs/cas/module.f.d.mts +18 -32
  38. package/fjs/cas/module.f.mjs +129 -128
  39. package/fjs/cas/proof.f.d.mts +11 -8
  40. package/fjs/cas/proof.f.mjs +259 -147
  41. package/fjs/cas/types.d.ts +24 -10
  42. package/fjs/ci/config/module.f.d.mts +2 -2
  43. package/fjs/ci/config/module.f.mjs +2 -2
  44. package/fjs/ci/module.f.d.mts +5 -5
  45. package/fjs/ci/module.f.mjs +8 -7
  46. package/fjs/ci/nix/module.f.d.mts +7 -5
  47. package/fjs/ci/nix/module.f.mjs +13 -12
  48. package/fjs/ci/nix/proof.f.mjs +2 -2
  49. package/fjs/ci/proof.f.mjs +9 -5
  50. package/fjs/cli/module.f.d.mts +4 -6
  51. package/fjs/cli/module.f.mjs +4 -8
  52. package/fjs/cli/proof.f.mjs +17 -16
  53. package/fjs/cli/types.d.ts +2 -3
  54. package/fjs/common/monoid/types.d.ts +1 -1
  55. package/fjs/crypto/hmac/module.f.mjs +2 -2
  56. package/fjs/crypto/sha2/module.f.mjs +3 -1
  57. package/fjs/crypto/sha2/proof.f.d.mts +1 -0
  58. package/fjs/crypto/sha2/proof.f.mjs +24 -0
  59. package/fjs/crypto/sha2/types.d.ts +11 -0
  60. package/fjs/crypto/sign/module.f.mjs +2 -2
  61. package/fjs/dev/module.f.d.mts +13 -4
  62. package/fjs/dev/module.f.mjs +56 -27
  63. package/fjs/dev/update/module.f.d.mts +9 -4
  64. package/fjs/dev/update/module.f.mjs +14 -10
  65. package/fjs/dev/update/proof.f.d.mts +1 -3
  66. package/fjs/dev/update/proof.f.mjs +10 -5
  67. package/fjs/djs/module.f.d.mts +13 -5
  68. package/fjs/djs/module.f.mjs +31 -16
  69. package/fjs/djs/parser/module.f.d.mts +13 -3
  70. package/fjs/djs/parser/module.f.mjs +117 -16
  71. package/fjs/djs/parser/proof.f.d.mts +5 -1
  72. package/fjs/djs/parser/proof.f.mjs +274 -12
  73. package/fjs/djs/parser/types.d.ts +7 -1
  74. package/fjs/djs/proof.f.d.mts +18 -2
  75. package/fjs/djs/proof.f.mjs +187 -12
  76. package/fjs/djs/serializer/module.f.d.mts +25 -8
  77. package/fjs/djs/serializer/module.f.mjs +61 -15
  78. package/fjs/djs/serializer/proof.f.d.mts +5 -0
  79. package/fjs/djs/serializer/proof.f.mjs +23 -1
  80. package/fjs/djs/tokenizer/module.f.d.mts +17 -5
  81. package/fjs/djs/tokenizer/module.f.mjs +99 -60
  82. package/fjs/djs/tokenizer/proof.f.d.mts +1 -1
  83. package/fjs/djs/tokenizer/proof.f.mjs +74 -67
  84. package/fjs/djs/transpiler/module.f.d.mts +12 -7
  85. package/fjs/djs/transpiler/module.f.mjs +82 -56
  86. package/fjs/djs/transpiler/types.d.ts +7 -3
  87. package/fjs/djs/types.d.ts +7 -1
  88. package/fjs/effects/list/module.f.d.mts +14 -11
  89. package/fjs/effects/list/module.f.mjs +13 -11
  90. package/fjs/effects/list/types.d.ts +27 -7
  91. package/fjs/effects/memory/module.f.d.mts +2 -1
  92. package/fjs/effects/memory/module.f.mjs +3 -4
  93. package/fjs/effects/memory/proof.f.mjs +13 -7
  94. package/fjs/effects/memory/types.d.ts +4 -3
  95. package/fjs/effects/mock/module.f.d.mts +20 -4
  96. package/fjs/effects/mock/module.f.mjs +37 -5
  97. package/fjs/effects/mock/types.d.ts +10 -1
  98. package/fjs/effects/module.d.mts +4 -2
  99. package/fjs/effects/module.f.d.mts +466 -280
  100. package/fjs/effects/module.f.mjs +537 -299
  101. package/fjs/effects/module.mjs +2 -1
  102. package/fjs/effects/node/memory/module.d.mts +4 -2
  103. package/fjs/effects/node/memory/module.mjs +6 -3
  104. package/fjs/effects/node/memory/proof.mjs +10 -4
  105. package/fjs/effects/node/module.d.mts +4 -3
  106. package/fjs/effects/node/module.f.d.mts +168 -33
  107. package/fjs/effects/node/module.f.mjs +256 -52
  108. package/fjs/effects/node/module.mjs +57 -34
  109. package/fjs/effects/node/proof.f.d.mts +28 -2
  110. package/fjs/effects/node/proof.f.mjs +161 -42
  111. package/fjs/effects/node/types.d.ts +106 -18
  112. package/fjs/effects/node/virtual/module.f.d.mts +18 -4
  113. package/fjs/effects/node/virtual/module.f.mjs +110 -68
  114. package/fjs/effects/node/virtual/proof.f.d.mts +28 -2
  115. package/fjs/effects/node/virtual/proof.f.mjs +190 -9
  116. package/fjs/effects/proof.f.d.mts +69 -37
  117. package/fjs/effects/proof.f.mjs +410 -130
  118. package/fjs/effects/types.d.ts +161 -33
  119. package/fjs/emergent_testing/module.f.d.mts +19 -12
  120. package/fjs/emergent_testing/module.f.mjs +93 -33
  121. package/fjs/emergent_testing/proof.f.d.mts +21 -7
  122. package/fjs/emergent_testing/proof.f.mjs +166 -32
  123. package/fjs/emergent_testing/types.d.ts +22 -4
  124. package/fjs/fsm/module.f.d.mts +14 -4
  125. package/fjs/fsm/module.f.mjs +54 -37
  126. package/fjs/fsm/proof.f.d.mts +2 -0
  127. package/fjs/fsm/proof.f.mjs +83 -114
  128. package/fjs/js/keywords/module.f.d.mts +52 -0
  129. package/fjs/js/keywords/module.f.mjs +72 -0
  130. package/fjs/js/keywords/proof.f.d.mts +3 -0
  131. package/fjs/js/keywords/proof.f.mjs +13 -0
  132. package/fjs/js/tokenizer/module.f.d.mts +26 -6
  133. package/fjs/js/tokenizer/module.f.mjs +145 -159
  134. package/fjs/js/tokenizer/proof.f.d.mts +1 -0
  135. package/fjs/js/tokenizer/proof.f.mjs +54 -24
  136. package/fjs/js/tokenizer/types.d.ts +33 -24
  137. package/fjs/mcp/cas/module.f.d.mts +1 -6
  138. package/fjs/mcp/cas/module.f.mjs +55 -51
  139. package/fjs/mcp/cas/proof.f.d.mts +15 -0
  140. package/fjs/mcp/cas/proof.f.mjs +174 -0
  141. package/fjs/mcp/evo/module.f.d.mts +19 -10
  142. package/fjs/mcp/evo/module.f.mjs +48 -27
  143. package/fjs/mcp/evo/proof.f.d.mts +6 -1
  144. package/fjs/mcp/evo/proof.f.mjs +115 -31
  145. package/fjs/mcp/module.f.d.mts +4 -4
  146. package/fjs/mcp/module.f.mjs +6 -6
  147. package/fjs/mcp/proof.f.d.mts +5 -3
  148. package/fjs/mcp/proof.f.mjs +112 -46
  149. package/fjs/media/html/module.f.mjs +1 -1
  150. package/fjs/media/json/extended/module.f.d.mts +82 -0
  151. package/fjs/media/json/extended/module.f.mjs +153 -0
  152. package/fjs/media/json/extended/proof.f.d.mts +42 -0
  153. package/fjs/media/json/extended/proof.f.mjs +127 -0
  154. package/fjs/media/json/extended/types.d.ts +23 -0
  155. package/fjs/media/json/module.f.d.mts +8 -2
  156. package/fjs/media/json/module.f.mjs +43 -41
  157. package/fjs/media/json/number/module.f.d.mts +59 -0
  158. package/fjs/media/json/number/module.f.mjs +136 -0
  159. package/fjs/media/json/number/proof.f.d.mts +24 -0
  160. package/fjs/media/json/number/proof.f.mjs +86 -0
  161. package/fjs/media/json/number/types.d.ts +28 -0
  162. package/fjs/media/json/parser/module.f.d.mts +25 -13
  163. package/fjs/media/json/parser/module.f.mjs +114 -70
  164. package/fjs/media/json/parser/proof.f.d.mts +5 -0
  165. package/fjs/media/json/parser/proof.f.mjs +31 -1
  166. package/fjs/media/json/parser/types.d.ts +33 -14
  167. package/fjs/media/json/rtti/module.f.d.mts +1 -1
  168. package/fjs/media/json/rtti/module.f.mjs +1 -1
  169. package/fjs/media/json/rtti/proof.f.mjs +9 -9
  170. package/fjs/media/json/schema/module.f.mjs +3 -13
  171. package/fjs/media/json/schema/proof.f.d.mts +0 -1
  172. package/fjs/media/json/schema/proof.f.mjs +1 -2
  173. package/fjs/media/json/serializer/module.f.d.mts +32 -1
  174. package/fjs/media/json/serializer/module.f.mjs +64 -2
  175. package/fjs/media/json/tokenizer/module.f.mjs +7 -3
  176. package/fjs/media/json/tokenizer/proof.f.d.mts +6 -0
  177. package/fjs/media/json/tokenizer/proof.f.mjs +62 -21
  178. package/fjs/media/json/types.d.ts +36 -10
  179. package/fjs/media/lock/module.f.d.mts +100 -0
  180. package/fjs/media/lock/module.f.mjs +125 -0
  181. package/fjs/media/lock/proof.f.d.mts +33 -0
  182. package/fjs/media/lock/proof.f.mjs +196 -0
  183. package/fjs/media/lock/types.d.ts +15 -0
  184. package/fjs/media/module.f.d.mts +6 -5
  185. package/fjs/media/module.f.mjs +8 -7
  186. package/fjs/media/note/module.f.d.mts +121 -0
  187. package/fjs/media/note/module.f.mjs +131 -0
  188. package/fjs/media/note/proof.f.d.mts +29 -0
  189. package/fjs/media/note/proof.f.mjs +150 -0
  190. package/fjs/media/note/types.d.ts +10 -0
  191. package/fjs/media/proof.f.d.mts +4 -1
  192. package/fjs/media/proof.f.mjs +40 -21
  193. package/fjs/media/revision/module.f.d.mts +78 -7
  194. package/fjs/media/revision/module.f.mjs +119 -12
  195. package/fjs/media/revision/proof.f.d.mts +10 -0
  196. package/fjs/media/revision/proof.f.mjs +88 -0
  197. package/fjs/media/revision/types.d.ts +34 -5
  198. package/fjs/media/type/module.f.d.mts +33 -15
  199. package/fjs/media/type/module.f.mjs +35 -29
  200. package/fjs/media/type/proof.f.d.mts +2 -1
  201. package/fjs/media/type/proof.f.mjs +30 -10
  202. package/fjs/module.f.mjs +29 -8
  203. package/fjs/nanvm/proof.f.mjs +3 -3
  204. package/fjs/nanvm/rust/module.f.mjs +1 -1
  205. package/fjs/nanvm/update/module.f.d.mts +4 -4
  206. package/fjs/nanvm/update/module.f.mjs +8 -9
  207. package/fjs/nanvm/update/proof.f.mjs +4 -3
  208. package/fjs/proof.f.d.mts +3 -3
  209. package/fjs/proof.f.mjs +36 -10
  210. package/fjs/protocol/json_rpc/module.f.d.mts +2 -2
  211. package/fjs/protocol/json_rpc/module.f.mjs +3 -3
  212. package/fjs/protocol/json_rpc/proof.f.mjs +4 -4
  213. package/fjs/protocol/mcp/module.f.d.mts +10 -13
  214. package/fjs/protocol/mcp/module.f.mjs +74 -58
  215. package/fjs/protocol/mcp/proof.f.d.mts +13 -4
  216. package/fjs/protocol/mcp/proof.f.mjs +193 -61
  217. package/fjs/protocol/mcp/stdio/module.f.d.mts +15 -7
  218. package/fjs/protocol/mcp/stdio/module.f.mjs +38 -25
  219. package/fjs/protocol/mcp/stdio/proof.f.d.mts +4 -2
  220. package/fjs/protocol/mcp/stdio/proof.f.mjs +45 -12
  221. package/fjs/protocol/mcp/stdio/types.d.ts +1 -1
  222. package/fjs/protocol/mcp/types.d.ts +17 -6
  223. package/fjs/sul/id/module.f.d.mts +0 -1
  224. package/fjs/sul/id/module.f.mjs +2 -3
  225. package/fjs/sul/level/hash/module.f.mjs +2 -1
  226. package/fjs/sul/level/hash/proof.f.mjs +2 -2
  227. package/fjs/sul/module.f.mjs +18 -13
  228. package/fjs/text/code_point/module.f.d.mts +8 -0
  229. package/fjs/text/code_point/module.f.mjs +8 -1
  230. package/fjs/text/code_point/proof.f.d.mts +1 -0
  231. package/fjs/text/code_point/proof.f.mjs +11 -0
  232. package/fjs/text/sgr/module.f.d.mts +4 -6
  233. package/fjs/text/sgr/module.f.mjs +6 -7
  234. package/fjs/text/utf16/module.f.mjs +7 -2
  235. package/fjs/text/utf16/proof.f.mjs +3 -3
  236. package/fjs/text/utf8/module.f.mjs +4 -2
  237. package/fjs/types/array/module.f.mjs +14 -2
  238. package/fjs/types/bit_vec/module.f.d.mts +0 -2
  239. package/fjs/types/bit_vec/module.f.mjs +46 -32
  240. package/fjs/types/bit_vec/proof.f.mjs +2 -2
  241. package/fjs/types/btree/remove/module.f.mjs +1 -1
  242. package/fjs/types/btree/set/module.f.mjs +9 -11
  243. package/fjs/types/btree/set/proof.f.mjs +12 -0
  244. package/fjs/types/byte_set/module.f.d.mts +11 -4
  245. package/fjs/types/byte_set/module.f.mjs +14 -6
  246. package/fjs/types/byte_set/proof.f.mjs +7 -7
  247. package/fjs/types/function/compare/module.f.mjs +10 -3
  248. package/fjs/types/list/module.f.d.mts +1 -1
  249. package/fjs/types/list/module.f.mjs +1 -1
  250. package/fjs/types/nullable/module.f.d.mts +18 -4
  251. package/fjs/types/nullable/module.f.mjs +21 -4
  252. package/fjs/types/nullable/proof.f.d.mts +4 -0
  253. package/fjs/types/nullable/proof.f.mjs +15 -0
  254. package/fjs/types/object/module.f.d.mts +12 -2
  255. package/fjs/types/object/module.f.mjs +11 -1
  256. package/fjs/types/patricia_trie/module.f.mjs +26 -11
  257. package/fjs/types/result/module.f.d.mts +3 -3
  258. package/fjs/types/result/module.f.mjs +3 -3
  259. package/fjs/types/rtti/common/module.f.d.mts +25 -28
  260. package/fjs/types/rtti/common/module.f.mjs +36 -32
  261. package/fjs/types/rtti/common/proof.f.mjs +5 -5
  262. package/fjs/types/rtti/data/module.f.d.mts +14 -0
  263. package/fjs/types/rtti/data/module.f.mjs +25 -2
  264. package/fjs/types/rtti/data/proof.f.d.mts +1 -0
  265. package/fjs/types/rtti/data/proof.f.mjs +25 -2
  266. package/fjs/types/rtti/parse/module.f.d.mts +24 -14
  267. package/fjs/types/rtti/parse/module.f.mjs +37 -28
  268. package/fjs/types/rtti/parse/proof.f.d.mts +3 -2
  269. package/fjs/types/rtti/parse/proof.f.mjs +33 -14
  270. package/fjs/types/rtti/proof.f.mjs +3 -1
  271. package/fjs/types/rtti/ts/module.f.mjs +13 -17
  272. package/fjs/types/rtti/ts/types.d.ts +14 -1
  273. package/fjs/types/rtti/validate/module.f.d.mts +81 -19
  274. package/fjs/types/rtti/validate/module.f.mjs +107 -61
  275. package/fjs/types/rtti/validate/proof.f.d.mts +13 -10
  276. package/fjs/types/rtti/validate/proof.f.mjs +186 -197
  277. package/fjs/types/sorted_set/module.f.d.mts +18 -0
  278. package/fjs/types/sorted_set/module.f.mjs +22 -0
  279. package/fjs/types/sorted_set/proof.f.d.mts +1 -0
  280. package/fjs/types/sorted_set/proof.f.mjs +16 -1
  281. package/fjs/types/uint8array/module.f.d.mts +1 -1
  282. package/fjs/types/uint8array/module.f.mjs +1 -1
  283. package/fjs/website/module.f.d.mts +3 -3
  284. package/fjs/website/module.f.mjs +4 -7
  285. package/fjs/website/proof.f.mjs +2 -1
  286. package/package.json +2 -2
  287. package/fjs/dev/package_json/module.f.d.mts +0 -39
  288. package/fjs/dev/package_json/module.f.mjs +0 -40
  289. package/fjs/dev/package_json/proof.f.d.mts +0 -6
  290. package/fjs/dev/package_json/proof.f.mjs +0 -32
  291. package/fjs/effects/eff/module.f.d.mts +0 -20
  292. package/fjs/effects/eff/module.f.mjs +0 -70
  293. package/fjs/effects/eff/proof.f.d.mts +0 -15
  294. package/fjs/effects/eff/proof.f.mjs +0 -69
  295. package/fjs/effects/eff/types.d.ts +0 -71
  296. package/fjs/types/rtti/validate/types.d.ts +0 -6
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * @import { Vec } from '../types/bit_vec/types.ts'
3
3
  * @import { FileCasOperation } from './types.ts'
4
+ * @import { IoChannel, IoResult, Mkdir, ReadFile, WriteFile } from '../effects/node/types.ts'
4
5
  * @import { Effect } from '../effects/types.ts'
5
- * @import { ReadFile, WriteFile, Rm, Mkdir, IoResult } from '../effects/node/types.ts'
6
- * @import { Ok } from '../types/result/types.ts'
6
+ * @import { Ok, Result } from '../types/result/types.ts'
7
7
  * @import { List } from '../effects/list/types.ts'
8
8
  */
9
9
 
10
10
  import { length, maxLength, msb, vec, vec8 } from '../types/bit_vec/module.f.mjs'
11
11
  import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.mjs'
12
12
  import { computeSync, sha256 } from '../crypto/sha2/module.f.mjs'
13
- import { fileCas, casAddFile, collectRead, casUpload } from './module.f.mjs'
14
- import { match, pure, runPure, step } from '../effects/module.f.mjs'
15
- import { mkdir, writeFile, rm, readFile, access } from '../effects/node/module.f.mjs'
16
- import { error, ok } from '../types/result/module.f.mjs'
13
+ import { fileCas, casAddFile, collectRead } from './module.f.mjs'
14
+ import { match, runPure } from '../effects/module.f.mjs'
15
+ import { mapStep as ioMapStep, pureError, pureOk, step as ioStep } from '../effects/module.f.mjs'
16
+ import { ioError, mkdir, writeFile, readFile, access } from '../effects/node/module.f.mjs'
17
+ import { error, ok, unwrap as unwrapResult } from '../types/result/module.f.mjs'
17
18
  import { emptyState, virtual } from '../effects/node/virtual/module.f.mjs'
18
19
  import { join } from '../path/module.f.mjs'
19
20
  import { nonEmpty, empty } from '../effects/list/module.f.mjs'
@@ -21,7 +22,7 @@ import { assert, assertEq, assertNotNullish } from '../asserts/module.f.mjs'
21
22
 
22
23
  const testDir = './test-cas-cli'
23
24
 
24
- /** @typedef {FileCasOperation | WriteFile | ReadFile | Rm | Mkdir} _TestOp */
25
+ /** @typedef {FileCasOperation | WriteFile | ReadFile | Mkdir} _TestOp */
25
26
 
26
27
  // Names the command a `FileCasOperation` effect stops at, so a proof can assert
27
28
  // on it and resume the continuation without reading the `Do` layout. The map
@@ -41,8 +42,98 @@ const casCommand = match({
41
42
  writeBytes: () => 'writeBytes',
42
43
  })
43
44
 
45
+ // A harmless "always succeeds" response for a command, used by `drive` once a
46
+ // test's overrides for that command are exhausted — good enough to let the
47
+ // rest of `write`'s pipeline run to completion without ever touching a real
48
+ // filesystem.
49
+ /** @type {(cmd: string) => unknown} */
50
+ const casDefaultResponse = cmd => {
51
+ switch (cmd) {
52
+ case 'now': return ok(0)
53
+ case 'randomInt': return ok(0)
54
+ case 'mkdir': case 'createExclusive': case 'rename': case 'rm':
55
+ case 'writeBytes': case 'access':
56
+ return ok(undefined)
57
+ case 'readdir': return ok([])
58
+ case 'stat': return ok({ size: 0 })
59
+ default: return ok(undefined)
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Drives a `FileCasOperation` effect to completion with synthetic op
65
+ * responses instead of a filesystem. `overrides[cmd]` is a queue consumed in
66
+ * call order; once a command's queue is empty (or was never given),
67
+ * `casDefaultResponse` supplies an always-succeeds value.
68
+ *
69
+ * `write`'s op-failure branches — a `writeBytes`/`rename` call failing
70
+ * mid-stream, or the final `stat` reporting a mismatched size — are real only
71
+ * under a race (another writer, a failing disk) between two of `write`'s own
72
+ * steps. The virtual harness has no such race to offer, so this reaches them
73
+ * directly instead: `overrides` names exactly the one call that fails, and
74
+ * every other call in the pipeline succeeds around it.
75
+ *
76
+ * Also returns the ordered log of command names issued, so a caller can
77
+ * confirm not just the returned error but *that cleanup ran* — e.g. that the
78
+ * failure path's own `rm` of the partial staging file was actually called,
79
+ * not just that some code path returned the right error tag.
80
+ *
81
+ * @type {(overrides: Partial<Record<string, unknown[]>>) => (e: Effect<FileCasOperation, unknown, unknown>) => readonly [Result<unknown, unknown>, readonly string[]]}
82
+ */
83
+ const drive = overrides => {
84
+ /** @type {string[]} */
85
+ const log = []
86
+ /** @type {(cmd: string) => unknown} */
87
+ const next = cmd => {
88
+ log.push(cmd)
89
+ const queue = overrides[cmd]
90
+ return queue !== undefined && queue.length > 0 ? queue.shift() : casDefaultResponse(cmd)
91
+ }
92
+ const handlers = {
93
+ access: () => next('access'),
94
+ createExclusive: () => next('createExclusive'),
95
+ mkdir: () => next('mkdir'),
96
+ now: () => next('now'),
97
+ randomInt: () => next('randomInt'),
98
+ readBytes: () => next('readBytes'),
99
+ readdir: () => next('readdir'),
100
+ rename: () => next('rename'),
101
+ rm: () => next('rm'),
102
+ stat: () => next('stat'),
103
+ writeBytes: () => next('writeBytes'),
104
+ }
105
+ const matcher = match(handlers)
106
+ /** @type {(e: Effect<FileCasOperation, unknown, unknown>) => Result<unknown, unknown>} */
107
+ const run_ = e => {
108
+ const m = matcher(e)
109
+ return m[0] === 'done' ? m[1] : run_(m[2](m[1]))
110
+ }
111
+ return e => [run_(e), log]
112
+ }
113
+
44
114
  // Create a 128 KiB big file content (at the max Vec size limit)
45
115
  // This tests the boundary where files are at the chunk size limit
116
+ /**
117
+ * Asserts that a channel error is a host failure carrying `message`.
118
+ * @type {(e: unknown, message: string) => void}
119
+ */
120
+ const assertIoMessage = (e, message) => {
121
+ assert(e instanceof Array && e[0] === 'ioError', ['expected an ioError', e])
122
+ assertEq(e[1].message, message)
123
+ }
124
+
125
+ /**
126
+ * The error a `drive`d effect answered with. The tuple shape is the driver's
127
+ * declared result type now, so only the tag is checked — and it is still
128
+ * checked, because these proofs exist to establish that a write fails closed.
129
+ *
130
+ * @type {(result: Result<unknown, unknown>) => unknown}
131
+ */
132
+ const errorMessage = result => {
133
+ assert(result[0] === 'error', ['expected an error result', result])
134
+ return result[1]
135
+ }
136
+
46
137
  /** @type {() => Vec} */
47
138
  const createBigFileContent = () => {
48
139
  const byteCount = 128n * 1024n // 128 KiB
@@ -50,107 +141,89 @@ const createBigFileContent = () => {
50
141
  return vec(byteCount * 8n)(0x42424242n)
51
142
  }
52
143
 
53
- // Test adding a big file and verifying the hash
54
- /** @type {() => Effect<_TestOp, void>} */
144
+ // Test adding a big file and verifying the hash.
145
+ //
146
+ // Each link is an `ioStep`, so a failure short-circuits the rest instead of
147
+ // being asserted away link by link: the continuations below see values, and
148
+ // the proof checks the one result at the end.
149
+ //
150
+ // Neither this chain nor the next one ends in cleanup any more. Both used to
151
+ // finish with an `rm(testDir)` whose result the raw `step` then discarded —
152
+ // and the virtual filesystem cannot remove a *non-empty* directory, so that
153
+ // `rm` had been failing on every run without anything noticing. There is also
154
+ // nothing to clean: each run interprets against a fresh `emptyState`.
155
+ /** @type {() => Effect<_TestOp, void, IoChannel>} */
55
156
  const testAddBigFile = () => {
56
157
  const bigFilePath = `${testDir}/big-file.bin`
57
158
  const cas = fileCas(sha256)(testDir)
58
- const x0 = step(
159
+ const x0 = ioStep(
59
160
  mkdir(testDir, { recursive: true }),
60
161
  () => writeFile(bigFilePath, createBigFileContent())
61
162
  )
62
- const x1 = step(
63
- x0,
64
- writeRes => {
65
- assert(writeRes[0] === 'ok', ['failed to write test file', writeRes])
66
- return casAddFile(cas)(bigFilePath)
67
- }
68
- )
69
- const x2 = step(
70
- x1,
71
- addRes => {
72
- assert(addRes[0] === 'ok', ['failed to add file to CAS', addRes])
73
- const hash = addRes[1]
74
- // Verify hash is 256 bits (SHA-256)
75
- assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)])
76
- // Verify hash can be encoded/decoded
77
- assertNotNullish(cBase32ToVec(vecToCBase32(hash)), 'failed to decode hash from cBase32')
78
- return rm(testDir)
79
- })
80
- return step(
81
- x2,
82
- () => pure(undefined)
83
- )
163
+ const x1 = ioStep(x0, () => casAddFile(cas)(bigFilePath))
164
+ return ioMapStep(x1, hash => {
165
+ // Verify hash is 256 bits (SHA-256)
166
+ assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)])
167
+ // Verify hash can be encoded/decoded
168
+ assertNotNullish(cBase32ToVec(vecToCBase32(hash)), 'failed to decode hash from cBase32')
169
+ })
84
170
  }
85
171
 
86
172
  // Test adding and retrieving a big file
87
- /** @type {() => Effect<_TestOp, void>} */
173
+ /** @type {() => Effect<_TestOp, void, IoChannel>} */
88
174
  const testAddAndGetBigFile = () => {
89
175
  const bigContent = createBigFileContent()
90
176
  const bigFilePath = `${testDir}/big-file.bin`
91
177
  const cas = fileCas(sha256)(testDir)
92
- const x0 = step(
178
+ const x0 = ioStep(
93
179
  mkdir(testDir, { recursive: true }),
94
180
  () => writeFile(bigFilePath, bigContent)
95
181
  )
96
- const x1 = step(
97
- x0,
98
- writeRes => {
99
- assert(writeRes[0] === 'ok', ['failed to write test file', writeRes])
100
- return casAddFile(cas)(bigFilePath)
101
- }
102
- )
182
+ const x1 = ioStep(x0, () => casAddFile(cas)(bigFilePath))
103
183
  // Verify the file is stored at the expected location
104
- const x2 = step(
105
- x1,
106
- addRes => {
107
- assert(addRes[0] === 'ok', ['failed to add file to CAS', addRes])
108
- return readFile(cas.url(addRes[1]))
109
- }
110
- )
111
- const x3 = step(
112
- x2,
113
- readRes => {
114
- assert(readRes[0] === 'ok', ['failed to read stored file', readRes])
115
- // Verify content is the same size as original
116
- assertEq(length(readRes[1]), length(bigContent), 'stored content size mismatch')
117
- return rm(testDir)
118
- })
119
- return step(
120
- x3,
121
- () => pure(undefined)
122
- )
184
+ const x2 = ioStep(x1, hash => readFile(cas.url(hash)))
185
+ return ioMapStep(x2, stored => {
186
+ // Verify content is the same size as original
187
+ assertEq(length(stored), length(bigContent), 'stored content size mismatch')
188
+ })
123
189
  }
124
190
 
125
191
  export const proof = {
126
- // Both effects must be interpreted, not merely built: an `Effect` is inert
192
+ // Both effects must be interpreted, not merely built: an effect is inert
127
193
  // data, so returning one from a proof runs none of its continuations and
128
- // asserts nothing.
129
- addBigFile: () => { virtual(emptyState)(testAddBigFile()) },
130
- addAndGetBigFile: () => { virtual(emptyState)(testAddAndGetBigFile()) },
194
+ // asserts nothing. The result is checked too — an error short-circuits the
195
+ // chain, so a run that never reached the asserts inside it would otherwise
196
+ // pass silently.
197
+ addBigFile: () => {
198
+ const [, r] = virtual(emptyState)(testAddBigFile())
199
+ assert(r[0] === 'ok', ['expected the big-file round trip to succeed', r])
200
+ },
201
+ addAndGetBigFile: () => {
202
+ const [, r] = virtual(emptyState)(testAddAndGetBigFile())
203
+ assert(r[0] === 'ok', ['expected the big-file read-back to succeed', r])
204
+ },
131
205
  //
132
206
  casWriteRead: () => {
133
207
  // Round-trip a single-chunk payload through the real streaming CAS: `write` returns
134
208
  // the content hash, and `read` streams the same bytes back as `ok` chunk items.
135
209
  const content = vec8(0x2An)
136
210
  const c = fileCas(sha256)('.')
137
- /** @type {List<FileCasOperation, IoResult<Vec>>} */
138
- const payload = nonEmpty(ok(content), empty())
211
+ /** @type {List<FileCasOperation, Vec, IoChannel>} */
212
+ const payload = nonEmpty(content, empty())
139
213
  const [state1, writeResult] = virtual(emptyState)(c.write(payload))
140
214
  assert(writeResult[0] === 'ok', ['expected write ok', writeResult])
141
215
  const hash = writeResult[1]
142
216
  assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)])
143
217
  assertEq(msb.cmp(hash)(computeSync(sha256)([content])), 0, 'write hash mismatch')
144
- /** @type {(acc: readonly Vec[]) => (stream: List<FileCasOperation, IoResult<Vec>>) => Effect<FileCasOperation, IoResult<readonly Vec[]>>} */
218
+ /** @type {(acc: readonly Vec[]) => (stream: List<FileCasOperation, Vec, IoChannel>) => Effect<FileCasOperation, readonly Vec[], IoChannel>} */
145
219
  const drain = acc =>
146
220
  stream =>
147
- step(
221
+ ioStep(
148
222
  stream,
149
223
  (node) => {
150
- if (node === undefined) { return pure(ok(acc)) }
224
+ if (node === undefined) { return pureOk(acc) }
151
225
  const { first, tail } = node
152
- if (first[0] === 'error') { return pure(first) }
153
- return drain([...acc, first[1]])(tail)
226
+ return drain([...acc, first])(tail)
154
227
  },
155
228
  )
156
229
  const [, readResult] = virtual(state1)(drain([])(c.read(hash)))
@@ -158,12 +231,12 @@ export const proof = {
158
231
  assertEq(msb.cmp(msb.listToVec(readResult[1]))(content), 0, 'read content mismatch')
159
232
  },
160
233
  casReadMissingShard: () => {
161
- // A missing shard surfaces as an explicit error *item*, never as end-of-stream.
234
+ // A missing shard fails the stream; it is never the `ok(undefined)` that
235
+ // ends one, which is the confusion a per-item `Result` used to allow.
162
236
  const c = fileCas(sha256)('.')
163
237
  const hash = computeSync(sha256)([vec8(0x2An)])
164
- const node = virtual(emptyState)(c.read(hash))[1]
165
- assert(node !== undefined, 'missing shard must not be EOF')
166
- assert(node.first[0] === 'error', ['expected error item', node.tail])
238
+ const cell = virtual(emptyState)(c.read(hash))[1]
239
+ assert(cell[0] === 'error', ['missing shard must fail the stream', cell])
167
240
  },
168
241
  casWriteMultiChunk: () => {
169
242
  // A multi-chunk payload streams through `writeBytes` chunk-by-chunk (the lease is
@@ -171,24 +244,23 @@ export const proof = {
171
244
  // and read streams the same content back.
172
245
  const chunks = /** @type {const} */ ([vec8(0x11n), vec8(0x22n), vec8(0x33n)])
173
246
  const c = fileCas(sha256)('.')
174
- /** @type {List<FileCasOperation, IoResult<Vec>>} */
247
+ /** @type {List<FileCasOperation, Vec, IoChannel>} */
175
248
  const payload = chunks.reduceRight(
176
- (tail, chunk) => nonEmpty(ok(chunk), tail),
177
- /** @type {List<FileCasOperation, IoResult<Vec>>} */ (empty()))
249
+ (tail, chunk) => nonEmpty(chunk, tail),
250
+ /** @satisfies {List<never, Vec, IoChannel>} */ (empty()))
178
251
  const [state1, writeResult] = virtual(emptyState)(c.write(payload))
179
252
  assert(writeResult[0] === 'ok', ['expected write ok', writeResult])
180
253
  const hash = writeResult[1]
181
254
  assertEq(msb.cmp(hash)(computeSync(sha256)(chunks)), 0, 'multi-chunk write hash mismatch')
182
- /** @type {(acc: readonly Vec[]) => (stream: List<FileCasOperation, IoResult<Vec>>) => Effect<FileCasOperation, IoResult<readonly Vec[]>>} */
255
+ /** @type {(acc: readonly Vec[]) => (stream: List<FileCasOperation, Vec, IoChannel>) => Effect<FileCasOperation, readonly Vec[], IoChannel>} */
183
256
  const drain = acc =>
184
257
  stream =>
185
- step(
258
+ ioStep(
186
259
  stream,
187
260
  (node) => {
188
- if (node === undefined) { return pure(ok(acc)) }
261
+ if (node === undefined) { return pureOk(acc) }
189
262
  const { first, tail } = node
190
- if (first[0] === 'error') { return pure(first) }
191
- return drain([...acc, first[1]])(tail)
263
+ return drain([...acc, first])(tail)
192
264
  },
193
265
  )
194
266
  const [, readResult] = virtual(state1)(drain([])(c.read(hash)))
@@ -201,28 +273,26 @@ export const proof = {
201
273
  // first, leaving exactly one shard in the store.
202
274
  const content = vec8(0x2An)
203
275
  const c = fileCas(sha256)('.')
204
- /** @type {() => List<FileCasOperation, IoResult<Vec>>} */
205
- const payload = () => nonEmpty(ok(content), empty())
276
+ /** @type {() => List<FileCasOperation, Vec, IoChannel>} */
277
+ const payload = () => nonEmpty(content, empty())
206
278
  const [state1, w1] = virtual(emptyState)(c.write(payload()))
207
279
  const [state2, w2] = virtual(state1)(c.write(payload()))
208
280
  assert(!(w1[0] !== 'ok' || w2[0] !== 'ok'), ['expected both writes ok', w1, w2])
209
281
  assertEq(msb.cmp(w1[1])(w2[1]), 0, 'dedup hash mismatch')
210
- const [, hashes] = virtual(state2)(c.list())
282
+ const [, listed] = virtual(state2)(c.list())
283
+ const hashes = unwrapResult(listed)
211
284
  assertEq(hashes.length, 1, ['expected one shard after dedup', hashes.length])
212
285
  },
213
- casWriteErrorItemAborts: () => {
214
- // An error item mid-stream deletes the partial staging file and fails; nothing is
286
+ casWriteStreamFailureAborts: () => {
287
+ // A stream that fails mid-way deletes the partial staging file and fails; nothing is
215
288
  // published, so the store stays empty.
216
289
  const c = fileCas(sha256)('.')
217
- /** @type {IoResult<Vec>} */
218
- const okItem = ok(vec8(0x11n))
219
- /** @type {IoResult<Vec>} */
220
- const errItem = error({ code: 'BOOM' })
221
- /** @type {List<FileCasOperation, IoResult<Vec>>} */
222
- const payload = nonEmpty(okItem, nonEmpty(errItem, /** @type {List<FileCasOperation, IoResult<Vec>>} */ (empty())))
290
+ /** @type {List<FileCasOperation, Vec, IoChannel>} */
291
+ const payload = nonEmpty(vec8(0x11n), pureError(ioError({ code: 'BOOM', message: 'boom' })))
223
292
  const [state1, result] = virtual(emptyState)(c.write(payload))
224
293
  assert(result[0] === 'error', ['expected write error', result])
225
- const [, hashes] = virtual(state1)(c.list())
294
+ const [, listed] = virtual(state1)(c.list())
295
+ const hashes = unwrapResult(listed)
226
296
  assertEq(hashes.length, 0, ['expected nothing published on abort', hashes])
227
297
  },
228
298
  casWriteReadExceedsMaxLength: () => {
@@ -235,25 +305,24 @@ export const proof = {
235
305
  const tail = vec8(0x2An) // one more byte ⇒ total > maxLength
236
306
  const chunks = /** @type {const} */ ([big, tail])
237
307
  const c = fileCas(sha256)('.')
238
- /** @type {List<FileCasOperation, IoResult<Vec>>} */
308
+ /** @type {List<FileCasOperation, Vec, IoChannel>} */
239
309
  const payload = chunks.reduceRight(
240
- (tl, chunk) => nonEmpty(ok(chunk), tl),
241
- /** @type {List<FileCasOperation, IoResult<Vec>>} */ (empty()))
310
+ (tl, chunk) => nonEmpty(chunk, tl),
311
+ /** @satisfies {List<never, Vec, IoChannel>} */ (empty()))
242
312
  const [state1, w] = virtual(emptyState)(c.write(payload))
243
313
  assert(w[0] === 'ok', ['expected write ok', w])
244
314
  const hash = w[1]
245
315
  assertEq(msb.cmp(hash)(computeSync(sha256)(chunks)), 0, 'oversized write hash mismatch')
246
316
  // Fold the read stream straight into a fresh SHA-2 state — never one `Vec`.
247
- /** @type {(state: typeof sha256.init) => (stream: List<FileCasOperation, IoResult<Vec>>) => Effect<FileCasOperation, IoResult<Vec>>} */
317
+ /** @type {(state: typeof sha256.init) => (stream: List<FileCasOperation, Vec, IoChannel>) => Effect<FileCasOperation, Vec, IoChannel>} */
248
318
  const rehash = state =>
249
319
  stream =>
250
- step(
320
+ ioStep(
251
321
  stream,
252
322
  (node) => {
253
- if (node === undefined) { return pure(ok(sha256.end(state))) }
323
+ if (node === undefined) { return pureOk(sha256.end(state)) }
254
324
  const { first, tail } = node
255
- if (first[0] === 'error') { return pure(first) }
256
- return rehash(sha256.append(first[1])(state))(tail)
325
+ return rehash(sha256.append(first)(state))(tail)
257
326
  }
258
327
  )
259
328
  const [, readBack] = virtual(state1)(rehash(sha256.init)(c.read(hash)))
@@ -271,12 +340,32 @@ export const proof = {
271
340
  }
272
341
  const content = vec8(0x2An)
273
342
  const c = fileCas(sha256)('.')
274
- const x = c.write(nonEmpty(ok(content), /** @type {List<never, Ok<Vec>>} */ (empty())))
343
+ const x = c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (empty())))
275
344
  const [state1, w] = virtual(state0)(x)
276
345
  assert(w[0] === 'ok', ['expected write ok', w])
277
346
  const [, present] = virtual(state1)(access(stalePath))
278
347
  assert(present[0] === 'error', 'expected GC to reclaim the expired staging file')
279
348
  },
349
+ casWriteGcRmFailureDoesNotFailTheUpload: () => {
350
+ // The sweep is best-effort, and `gcStage`'s error channel is `never`
351
+ // because it says so: an `rm` that fails on one expired file recovers
352
+ // per item, so the upload it is piggy-backed on still succeeds.
353
+ const stale = '0000000000000000000-stale'
354
+ const c = fileCas(sha256)('.')
355
+ // An empty payload: the driver's default `stat` reports size 0, so the
356
+ // publish check passes and the sweep is the only thing under test.
357
+ /** @type {List<never, Vec, IoChannel>} */
358
+ const payload = empty()
359
+ const [result, log] = drive({
360
+ // Only the first `now` is the sweep's, so every deadline after it
361
+ // stays 0 and the staging file below reads as expired.
362
+ now: [ok(1)],
363
+ readdir: [ok([{ name: stale, parentPath: '_stage', isFile: true }])],
364
+ rm: [error(ioError({ message: 'busy' }))],
365
+ })(c.write(payload))
366
+ assert(result[0] === 'ok', ['expected write ok', result])
367
+ assertEq(log[0], 'now', ['expected the sweep to run first', log])
368
+ },
280
369
  casWriteGcSkipsLiveLease: () => {
281
370
  // A staging file whose deadline is still in the future is left alone by the GC
282
371
  // that `write` runs before staging its own file.
@@ -288,55 +377,80 @@ export const proof = {
288
377
  }
289
378
  const content = vec8(0x2An)
290
379
  const c = fileCas(sha256)('.')
291
- const x = c.write(nonEmpty(ok(content), /** @type {List<never, Ok<Vec>>} */ (empty())))
380
+ const x = c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (empty())))
292
381
  const [state1, w] = virtual(state0)(x)
293
382
  assert(w[0] === 'ok', ['expected write ok', w])
294
383
  const [, present] = virtual(state1)(access(livePath))
295
384
  assert(present[0] === 'ok', 'expected GC to leave the live staging file alone')
296
385
  },
297
- casUploadSuccess: () => {
298
- // A successful upload returns the hash and deletes the source file from cas_upload/.
299
- const content = vec8(0x2An)
300
- const state0 = { ...emptyState, root: { 'cas_upload': { 'myfile': [content] } } }
301
- const [state1, result] = virtual(state0)(casUpload('.')('myfile'))
302
- assert(result[0] === 'ok', ['expected casUpload ok', result])
303
- assertEq(length(result[1]), 256n, ['expected 256-bit hash', length(result[1])])
304
- // Source must be deleted after successful publish.
305
- const srcPath = join('.', 'cas_upload', 'myfile')
306
- const [, srcAccess] = virtual(state1)(access(srcPath))
307
- assert(srcAccess[0] === 'error', 'expected source to be deleted after successful upload')
386
+ casWriteBytesErrorAborts: () => {
387
+ // A `writeBytes` call failing mid-stream (disk full, permissions) fails closed:
388
+ // the partial staging file is deleted and the error is returned, without ever
389
+ // reaching a real filesystem here.
390
+ const c = fileCas(sha256)('.')
391
+ /** @type {List<never, Vec, IoChannel>} */
392
+ const payload = nonEmpty(vec8(0x11n), empty())
393
+ const [result, log] = drive({ writeBytes: [error(ioError({ message: 'disk full' }))] })(c.write(payload))
394
+ assertIoMessage(errorMessage(result), 'disk full')
395
+ // The cleanup `rm` of the partial staging file must actually run, not just be
396
+ // implied by the returned error tag.
397
+ assertEq(log[log.length - 1], 'rm', ['expected cleanup rm to run', log])
398
+ },
399
+ casWriteLeaseRenewalRenameErrorAborts: () => {
400
+ // The lease-renewal `rename` (after every chunk) failing fails the same way as a
401
+ // `writeBytes` failure: the partial staging file is deleted, error returned.
402
+ const c = fileCas(sha256)('.')
403
+ /** @type {List<never, Vec, IoChannel>} */
404
+ const payload = nonEmpty(vec8(0x11n), empty())
405
+ const [result, log] = drive({ rename: [error(ioError({ message: 'rename failed' }))] })(c.write(payload))
406
+ assertIoMessage(errorMessage(result), 'rename failed')
407
+ assertEq(log[log.length - 1], 'rm', ['expected cleanup rm to run', log])
408
+ },
409
+ casWritePublishSizeMismatchErrors: () => {
410
+ // `publish`'s three filesystem steps (mkdir/rename/rm) run best-effort with their
411
+ // results ignored; success is decided afterward by the final `stat`. A `stat`
412
+ // that reports `ok` but with a size other than what was written — a race with a
413
+ // concurrent writer — surfaces as a "publish size mismatch" error instead of a
414
+ // false "ok". Pins the size half of the check: a `stat` that returns `ok` with
415
+ // any size but the expected one must still fail, even though the tag alone says
416
+ // success.
417
+ const c = fileCas(sha256)('.')
418
+ /** @type {List<never, Vec, IoChannel>} */
419
+ const payload = nonEmpty(vec8(0x11n), empty())
420
+ const [result] = drive({ stat: [ok({ size: 999 })] })(c.write(payload))
421
+ assertIoMessage(errorMessage(result), 'publish size mismatch')
308
422
  },
309
- casUploadFailureKeepsSource: () => {
310
- // A missing source file causes write to fail; casUpload returns error and the
311
- // source is left in place (trivially: it was never there, but the upload is not published).
312
- const state0 = { ...emptyState, root: {} }
313
- const [state1, result] = virtual(state0)(casUpload('.')('nonexistent'))
314
- assert(result[0] === 'error', ['expected casUpload to fail on missing source', result])
315
- // Nothing published to the store.
423
+ casWritePublishStatErrorErrorsEvenWithMatchingSize: () => {
424
+ // Pins the tag half of the same check: a `stat` that fails outright must still
425
+ // fail `write`, even in the degenerate case where its error payload happens to
426
+ // carry a `.size` field equal to the expected offset — otherwise a check that
427
+ // only compared `.size` (dropping the `st[0] === 'ok'` tag check) would slip
428
+ // through undetected by the size-mismatch case above, which never sees a
429
+ // coincidentally-matching size on a failed `stat`.
316
430
  const c = fileCas(sha256)('.')
317
- const [, hashes] = virtual(state1)(c.list())
318
- assertEq(hashes.length, 0, ['expected nothing published on failed upload', hashes])
431
+ /** @type {List<never, Vec, IoChannel>} */
432
+ const payload = nonEmpty(vec8(0x11n), empty())
433
+ const [result] = drive({ stat: [error({ size: 1 })] })(c.write(payload))
434
+ assertIoMessage(errorMessage(result), 'publish size mismatch')
319
435
  },
320
436
  collectReadDrainsChunks: () => {
321
437
  // The common path: every chunk is `ok`, so collectRead concatenates them all
322
438
  // and returns the whole blob as one `Vec`.
323
- /** @type {List<never, IoResult<Vec>>} */
324
- const stream = nonEmpty(ok(vec8(0x11n)), nonEmpty(ok(vec8(0x22n)), empty()))
439
+ /** @type {List<never, Vec, IoChannel>} */
440
+ const stream = nonEmpty(vec8(0x11n), nonEmpty(vec8(0x22n), empty()))
325
441
  const o = runPure(collectRead(stream))
326
442
  assert(o.length === 1, 'expected collectRead to finish without issuing a command')
327
443
  assertEq(o[0][0], 'ok')
328
444
  },
329
- collectReadPropagatesErrorItem: () => {
330
- // An error item mid-stream short-circuits collectRead with that same error.
331
- /** @type {IoResult<Vec>} */
332
- const boom = error('boom')
333
- /** @type {List<never, IoResult<Vec>>} */
334
- const stream = nonEmpty(ok(vec8(0x11n)), nonEmpty(boom, /** @type {List<never, IoResult<Vec>>} */ (empty())))
445
+ collectReadPropagatesStreamFailure: () => {
446
+ // A stream that fails mid-way short-circuits collectRead with that same
447
+ // failure `step` does it, so `collectRead` has no branch for it.
448
+ /** @type {List<never, Vec, IoChannel>} */
449
+ const stream = nonEmpty(vec8(0x11n), pureError(ioError({ message: 'boom' })))
335
450
  const o = runPure(collectRead(stream))
336
451
  assert(o.length === 1, 'expected collectRead to finish without issuing a command')
337
452
  const [r] = o
338
- assertEq(r[0], 'error')
339
- assertEq(r[1], 'boom')
453
+ assert(r[0] === 'error' && r[1][0] === 'ioError' && r[1][1].message === 'boom', r)
340
454
  },
341
455
  // A single `Vec` cannot exceed `maxLength` bits — feed a pure stream whose second
342
456
  // chunk pushes the running total just over the limit so the overflow guard fires
@@ -345,27 +459,25 @@ export const proof = {
345
459
  const half = maxLength / 2n
346
460
  const v1 = vec(half)(0n)
347
461
  const v2 = vec(half + 1n)(0n)
348
- /** @type {List<never, IoResult<Vec>>} */
349
- const stream = nonEmpty(ok(v1), nonEmpty(ok(v2), empty()))
462
+ /** @type {List<never, Vec, IoChannel>} */
463
+ const stream = nonEmpty(v1, nonEmpty(v2, empty()))
350
464
  const o = runPure(collectRead(stream))
351
465
  assert(o.length === 1, 'expected collectRead to finish without issuing a command')
352
466
  assertEq(o[0][0], 'error')
353
467
  },
354
468
  casListPropagatesNonNotFoundAccessError: () => {
355
469
  // A non-ENOENT `access` failure (permissions, corruption) is a genuine storage
356
- // error and must propagate out of `list`, not be swallowed as an empty store.
470
+ // error and must propagate out of `list` as an `error`, neither swallowed as an
471
+ // empty store nor thrown: the caller decides what an unreadable store means.
357
472
  const c = fileCas(sha256)('.')
358
- const boom = { code: 'EACCES' }
473
+ const boom = ioError({ code: 'EACCES', message: 'permission denied' })
359
474
  const r = casCommand(c.list())
360
475
  assert(r[0] === 'cont', 'expected list() to issue an access command first')
361
476
  assertEq(r[1], 'access')
362
- /** @type {unknown} */
363
- let threw
364
- try {
365
- r[2](error(boom))
366
- } catch (e) {
367
- threw = e
368
- }
369
- assertEq(threw, boom)
477
+ const answered = runPure(r[2](error(boom)))
478
+ assert(answered.length === 1, ['expected list() to answer without another command', answered])
479
+ const result = answered[0]
480
+ assert(result[0] === 'error', ['expected the access failure to propagate', result])
481
+ assertEq(result[1], boom)
370
482
  },
371
483
  }