@ic-reactor/mcp 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1041) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +108 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +77 -0
  4. package/.github/actions/setup-rust-wasm/action.yml +25 -0
  5. package/.github/dependabot.yml +107 -0
  6. package/.github/workflows/auth-latest.yml +67 -0
  7. package/.github/workflows/b3pay-site.yml +49 -0
  8. package/.github/workflows/ci.yml +430 -0
  9. package/.github/workflows/codeql.yml +41 -0
  10. package/.github/workflows/dependency-review.yml +68 -0
  11. package/.github/workflows/docs-check.yml +98 -0
  12. package/.github/workflows/docs.yml +265 -0
  13. package/.github/workflows/e2e.yml +90 -0
  14. package/.github/workflows/examples-published.yml +97 -0
  15. package/.github/workflows/release.yml +326 -0
  16. package/.husky/pre-commit +12 -0
  17. package/.prettierignore +109 -0
  18. package/.prettierrc +6 -0
  19. package/AGENTS.md +103 -0
  20. package/CHANGELOG.md +3268 -0
  21. package/CLAUDE.md +93 -0
  22. package/CNAME +1 -0
  23. package/CODE_OF_CONDUCT.md +7 -0
  24. package/CONTRIBUTING.md +433 -0
  25. package/LICENSE +21 -0
  26. package/README.md +2 -2
  27. package/SECURITY.md +74 -0
  28. package/b3pay-module.json +194 -0
  29. package/docs/.remarkrc.json +10 -0
  30. package/docs/README.md +64 -0
  31. package/docs/astro.config.mjs +205 -0
  32. package/docs/favicon.svg +15 -0
  33. package/docs/package.json +39 -0
  34. package/docs/public/favicon.svg +15 -0
  35. package/docs/public/ic-reactor-social-card.png +0 -0
  36. package/docs/scripts/b3pay-static.mjs +22 -0
  37. package/docs/scripts/check-example-docs.mjs +214 -0
  38. package/docs/scripts/check-links.mjs +212 -0
  39. package/docs/scripts/fix-dist-links.js +125 -0
  40. package/docs/scripts/fix-frontmatter.js +71 -0
  41. package/docs/scripts/remove-redundant-h1.cjs +48 -0
  42. package/docs/scripts/rewrite-typedoc-links.js +110 -0
  43. package/docs/scripts/split-index.js +69 -0
  44. package/docs/src/assets/ic-reactor-hero-dark.svg +23 -0
  45. package/docs/src/assets/ic-reactor-hero-light.svg +23 -0
  46. package/docs/src/assets/ic-reactor-lockup-dark.svg +15 -0
  47. package/docs/src/assets/ic-reactor-lockup-light.svg +15 -0
  48. package/docs/src/assets/ic-reactor-mark-dark.svg +14 -0
  49. package/docs/src/assets/ic-reactor-mark-light.svg +14 -0
  50. package/docs/src/assets/layers/candid-core-layer-icon-dark.svg +4 -0
  51. package/docs/src/assets/layers/candid-core-layer-icon-light.svg +4 -0
  52. package/docs/src/assets/layers/candid-profile-layer-icon-dark.svg +4 -0
  53. package/docs/src/assets/layers/candid-profile-layer-icon-light.svg +4 -0
  54. package/docs/src/assets/layers/ic-reactor-layer-icon-dark.svg +4 -0
  55. package/docs/src/assets/layers/ic-reactor-layer-icon-light.svg +4 -0
  56. package/docs/src/components/LibraryCards.astro +172 -0
  57. package/docs/src/components/LibraryIcon.astro +28 -0
  58. package/docs/src/components/SiteTitle.astro +241 -0
  59. package/docs/src/content/docs/404.mdx +19 -0
  60. package/docs/src/content/docs/build/agent-tools.mdx +593 -0
  61. package/docs/src/content/docs/build/icrc1-wallet.mdx +619 -0
  62. package/docs/src/content/docs/build/ssr-per-user.mdx +552 -0
  63. package/docs/src/content/docs/examples/canister-studio.mdx +95 -0
  64. package/docs/src/content/docs/examples/icrc-ledger.mdx +45 -0
  65. package/docs/src/content/docs/examples/index.mdx +38 -0
  66. package/docs/src/content/docs/examples/next-ssr.mdx +73 -0
  67. package/docs/src/content/docs/examples/node-agent-tool.mdx +80 -0
  68. package/docs/src/content/docs/examples/vite-wallet.mdx +71 -0
  69. package/docs/src/content/docs/guides/auth.mdx +1181 -0
  70. package/docs/src/content/docs/guides/client.mdx +551 -0
  71. package/docs/src/content/docs/guides/deploy.mdx +180 -0
  72. package/docs/src/content/docs/guides/errors.mdx +475 -0
  73. package/docs/src/content/docs/guides/getting-started.mdx +620 -0
  74. package/docs/src/content/docs/guides/reads.mdx +586 -0
  75. package/docs/src/content/docs/guides/ssr.mdx +529 -0
  76. package/docs/src/content/docs/guides/testing.mdx +401 -0
  77. package/docs/src/content/docs/guides/values.mdx +261 -0
  78. package/docs/src/content/docs/guides/vite-plugin.mdx +690 -0
  79. package/docs/src/content/docs/guides/writes.mdx +572 -0
  80. package/docs/src/content/docs/ic-reactor/index.mdx +167 -0
  81. package/docs/src/content/docs/index.mdx +65 -0
  82. package/docs/src/content/docs/migrating-from-3.mdx +535 -0
  83. package/docs/src/content/docs/packages/core.mdx +186 -0
  84. package/docs/src/content/docs/packages/mcp.mdx +163 -0
  85. package/docs/src/content/docs/packages/react.mdx +207 -0
  86. package/docs/src/content/docs/packages/vite-plugin.mdx +73 -0
  87. package/docs/src/content/docs/versioning.mdx +104 -0
  88. package/docs/src/content.config.ts +7 -0
  89. package/docs/src/styles/custom.css +189 -0
  90. package/docs/tsconfig.typedoc.json +26 -0
  91. package/docs/typedoc-entry.ts +17 -0
  92. package/docs/typedoc.json +37 -0
  93. package/e2e/Cargo.lock +646 -0
  94. package/e2e/Cargo.toml +3 -0
  95. package/e2e/README.md +101 -0
  96. package/e2e/frontends/build.mjs +94 -0
  97. package/e2e/frontends/index.html +10 -0
  98. package/e2e/global-setup.ts +65 -0
  99. package/e2e/icp.yaml +38 -0
  100. package/e2e/package.json +40 -0
  101. package/e2e/setup.ts +6 -0
  102. package/e2e/src/actor/Cargo.toml +14 -0
  103. package/e2e/src/actor/hello_actor.did +10 -0
  104. package/e2e/src/actor/src/lib.rs +73 -0
  105. package/e2e/src/calls.test.ts +132 -0
  106. package/e2e/src/declarations/hello_actor.envelope.json +175 -0
  107. package/e2e/src/declarations/hello_actor.ts +15 -0
  108. package/e2e/src/errors.test.ts +107 -0
  109. package/e2e/src/frontend-headers.test.ts +253 -0
  110. package/e2e/src/identity.test.ts +61 -0
  111. package/e2e/src/react.test.tsx +59 -0
  112. package/e2e/src/replica.ts +70 -0
  113. package/e2e/src/vite-env.test.ts +108 -0
  114. package/e2e/test.sh +47 -0
  115. package/e2e/tsconfig.json +20 -0
  116. package/e2e/tsconfig.typecheck.json +9 -0
  117. package/e2e/vitest.config.ts +22 -0
  118. package/eslint.config.mjs +100 -0
  119. package/evals/PREREGISTRATION.md +1346 -0
  120. package/evals/README.md +756 -0
  121. package/evals/conditions/thin/condition.json +8 -0
  122. package/evals/conditions/thin/docs/candid-core-cli.README.md +90 -0
  123. package/evals/conditions/thin/docs/candid-core-schema.README.md +339 -0
  124. package/evals/conditions/thin/package.json +25 -0
  125. package/evals/conditions/thin/starter/src/generated/icrc1.ts +57 -0
  126. package/evals/conditions/thin-guide/condition.json +10 -0
  127. package/evals/conditions/thin-guide/docs/llms.txt +292 -0
  128. package/evals/conditions/v3/condition.json +9 -0
  129. package/evals/conditions/v3/docs/ic-reactor-core.llms.txt +232 -0
  130. package/evals/conditions/v3/docs/ic-reactor-react.llms.txt +280 -0
  131. package/evals/conditions/v3/docs/llms-full.txt +1053 -0
  132. package/evals/conditions/v3/package.json +26 -0
  133. package/evals/conditions/v3/starter/src/declarations/icrc1.did.d.ts +95 -0
  134. package/evals/conditions/v3/starter/src/declarations/icrc1.did.js +140 -0
  135. package/evals/conditions/v4/PORTING.md +384 -0
  136. package/evals/conditions/v4/README.md +106 -0
  137. package/evals/conditions/v4/condition.json +5 -0
  138. package/evals/conditions/v4/docs/llms.txt +323 -0
  139. package/evals/conditions/v4/package.json +27 -0
  140. package/evals/conditions/v4/starter/src/generated/icrc1.ts +78 -0
  141. package/evals/conditions/v4-proto/condition.json +6 -0
  142. package/evals/conditions/v4-proto/docs/llms.txt +291 -0
  143. package/evals/conditions/v4-proto/lib/SHIM-NOTES.md +20 -0
  144. package/evals/conditions/v4-proto/lib/package.json +27 -0
  145. package/evals/conditions/v4-proto/lib/src/client.ts +471 -0
  146. package/evals/conditions/v4-proto/lib/src/errors.ts +214 -0
  147. package/evals/conditions/v4-proto/lib/src/index.ts +36 -0
  148. package/evals/conditions/v4-proto/lib/src/principal.ts +175 -0
  149. package/evals/conditions/v4-proto/lib/src/service.ts +69 -0
  150. package/evals/conditions/v4-proto/lib/src/types.ts +174 -0
  151. package/evals/conditions/v4-proto/lib/test/runtime.test.ts +170 -0
  152. package/evals/conditions/v4-proto/lib/test/tsconfig.json +10 -0
  153. package/evals/conditions/v4-proto/lib/test/types.test-d.ts +145 -0
  154. package/evals/conditions/v4-proto/lib/test/vitest.config.mjs +8 -0
  155. package/evals/conditions/v4-proto/lib/tsconfig.json +17 -0
  156. package/evals/conditions/v4-proto/package.json +26 -0
  157. package/evals/conditions/v4-proto/starter/src/generated/icrc1.service.ts +19 -0
  158. package/evals/conditions/v4-proto/starter/src/generated/icrc1.ts +57 -0
  159. package/evals/drive.mjs +1221 -0
  160. package/evals/gate.mjs +161 -0
  161. package/evals/harness/aggregate.mjs +534 -0
  162. package/evals/harness/aggregate.test.mjs +344 -0
  163. package/evals/harness/assemble.mjs +216 -0
  164. package/evals/harness/assemble.test.mjs +143 -0
  165. package/evals/harness/auth.mjs +91 -0
  166. package/evals/harness/check-docs.mjs +157 -0
  167. package/evals/harness/declarations/icrc1.did.d.ts +95 -0
  168. package/evals/harness/declarations/icrc1.did.js +140 -0
  169. package/evals/harness/drive-auth.test.mjs +584 -0
  170. package/evals/harness/drive.test.mjs +509 -0
  171. package/evals/harness/fake-auth.ts +67 -0
  172. package/evals/harness/fake-ledger.ts +178 -0
  173. package/evals/harness/gate-plan.mjs +104 -0
  174. package/evals/harness/gate-plan.test.mjs +243 -0
  175. package/evals/harness/icrc1.did +88 -0
  176. package/evals/harness/judge.mjs +63 -0
  177. package/evals/harness/judge.test.mjs +81 -0
  178. package/evals/harness/leak-scan.mjs +1373 -0
  179. package/evals/harness/leak-scan.test.mjs +1300 -0
  180. package/evals/harness/probes/amounts.ts +14 -0
  181. package/evals/harness/probes/node-tool.probe.ts +48 -0
  182. package/evals/harness/probes/probe.mjs +149 -0
  183. package/evals/harness/probes/react-wallet.probe.ts +96 -0
  184. package/evals/harness/public/support/auth.ts +38 -0
  185. package/evals/harness/public/support/fake-replica.d.ts +28 -0
  186. package/evals/harness/public/support/fake-replica.js +434 -0
  187. package/evals/harness/public/support/ledger.idl.d.ts +95 -0
  188. package/evals/harness/public/support/ledger.idl.js +140 -0
  189. package/evals/harness/public/support/world.ts +179 -0
  190. package/evals/harness/runs.mjs +139 -0
  191. package/evals/harness/sandbox.mjs +88 -0
  192. package/evals/harness/sandbox.test.mjs +134 -0
  193. package/evals/harness/ship.mjs +264 -0
  194. package/evals/harness/ship.test.mjs +552 -0
  195. package/evals/harness/world.ts +291 -0
  196. package/evals/package.json +26 -0
  197. package/evals/pnpm-lock.yaml +1537 -0
  198. package/evals/pnpm-workspace.yaml +12 -0
  199. package/evals/results/2026-09-30T14-22-05-924Z/pilot.json +384 -0
  200. package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.json +404 -0
  201. package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.rescored.json +404 -0
  202. package/evals/results/2026-09-30T14-22-05-924Z/plan.json +240 -0
  203. package/evals/results/2026-09-30T14-22-05-924Z/summary.json +1800 -0
  204. package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.json +1843 -0
  205. package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.rescored.json +2100 -0
  206. package/evals/results/2026-09-30T15-48-23-343Z/pilot.json +404 -0
  207. package/evals/results/2026-09-30T15-48-23-343Z/pilot.rescored.json +404 -0
  208. package/evals/results/2026-09-30T15-48-23-343Z/plan.json +242 -0
  209. package/evals/results/2026-09-30T15-48-23-343Z/summary.json +1843 -0
  210. package/evals/results/2026-09-30T15-48-23-343Z/summary.rescored.json +2195 -0
  211. package/evals/results/2026-10-03-beta1-gate/pilot.json +162 -0
  212. package/evals/results/2026-10-03-beta1-gate/plan.json +141 -0
  213. package/evals/results/2026-10-03-beta1-gate/summary.json +983 -0
  214. package/evals/results/2026-10-05-ga-gate/contaminated-run.json +55 -0
  215. package/evals/results/2026-10-05-ga-gate/pilot.json +162 -0
  216. package/evals/results/2026-10-05-ga-gate/plan.json +457 -0
  217. package/evals/results/2026-10-05-ga-gate/summary.json +1037 -0
  218. package/evals/results/2026-10-05-ga-gate/summary.rescanned.json +1037 -0
  219. package/evals/results/README.md +31 -0
  220. package/evals/runs-pilot.log +150 -0
  221. package/evals/runs-pilot2.log +150 -0
  222. package/evals/score.mjs +256 -0
  223. package/evals/setup.mjs +547 -0
  224. package/evals/tasks/node-tool/faulty/thin-accepts-exponent/meta.json +5 -0
  225. package/evals/tasks/node-tool/faulty/thin-accepts-exponent/src/index.ts +121 -0
  226. package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/meta.json +5 -0
  227. package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/src/index.ts +116 -0
  228. package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/meta.json +9 -0
  229. package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/src/index.ts +118 -0
  230. package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/meta.json +5 -0
  231. package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/src/index.ts +118 -0
  232. package/evals/tasks/node-tool/faulty/thin-ignores-root-key/meta.json +5 -0
  233. package/evals/tasks/node-tool/faulty/thin-ignores-root-key/src/index.ts +117 -0
  234. package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/meta.json +9 -0
  235. package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/src/index.ts +119 -0
  236. package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/meta.json +9 -0
  237. package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/src/index.ts +118 -0
  238. package/evals/tasks/node-tool/faulty/thin-number-amount/meta.json +10 -0
  239. package/evals/tasks/node-tool/faulty/thin-number-amount/src/index.ts +117 -0
  240. package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/meta.json +5 -0
  241. package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/src/index.ts +120 -0
  242. package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/meta.json +5 -0
  243. package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/src/index.ts +118 -0
  244. package/evals/tasks/node-tool/faulty/v3-retry-call-errors/meta.json +11 -0
  245. package/evals/tasks/node-tool/faulty/v3-retry-call-errors/src/index.ts +149 -0
  246. package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/meta.json +5 -0
  247. package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/src/index.ts +71 -0
  248. package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/meta.json +9 -0
  249. package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/src/index.ts +84 -0
  250. package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/meta.json +9 -0
  251. package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/src/index.ts +72 -0
  252. package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/meta.json +5 -0
  253. package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/src/index.ts +72 -0
  254. package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/meta.json +5 -0
  255. package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/src/index.ts +84 -0
  256. package/evals/tasks/node-tool/hidden/node-tool.test.ts +312 -0
  257. package/evals/tasks/node-tool/prompt.md +55 -0
  258. package/evals/tasks/node-tool/prompt.minimal.md +30 -0
  259. package/evals/tasks/node-tool/solutions/thin/reference/src/index.ts +118 -0
  260. package/evals/tasks/node-tool/solutions/thin/reference-module-scope/src/index.ts +145 -0
  261. package/evals/tasks/node-tool/solutions/thin-guide/reference/src/index.ts +118 -0
  262. package/evals/tasks/node-tool/solutions/thin-guide/reference-module-scope/src/index.ts +145 -0
  263. package/evals/tasks/node-tool/solutions/v3/reference/src/index.ts +140 -0
  264. package/evals/tasks/node-tool/solutions/v3/reference-module-scope/src/index.ts +153 -0
  265. package/evals/tasks/node-tool/solutions/v4/reference/src/index.ts +84 -0
  266. package/evals/tasks/node-tool/solutions/v4/reference-module-scope/src/index.ts +105 -0
  267. package/evals/tasks/node-tool/solutions/v4-proto/reference/src/index.ts +72 -0
  268. package/evals/tasks/node-tool/solutions/v4-proto/reference-module-scope/src/index.ts +87 -0
  269. package/evals/tasks/node-tool/starter/src/contract.ts +31 -0
  270. package/evals/tasks/node-tool/starter/src/index.ts +7 -0
  271. package/evals/tasks/node-tool/starter/test/smoke.test.ts +22 -0
  272. package/evals/tasks/node-tool/starter/tsconfig.json +17 -0
  273. package/evals/tasks/node-tool/task.json +63 -0
  274. package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/meta.json +5 -0
  275. package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/src/Wallet.tsx +216 -0
  276. package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/meta.json +5 -0
  277. package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/src/Wallet.tsx +212 -0
  278. package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/meta.json +5 -0
  279. package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/src/Wallet.tsx +210 -0
  280. package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/meta.json +8 -0
  281. package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/src/Wallet.tsx +213 -0
  282. package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/meta.json +9 -0
  283. package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/src/Wallet.tsx +213 -0
  284. package/evals/tasks/react-wallet/faulty/thin-number-balance/meta.json +5 -0
  285. package/evals/tasks/react-wallet/faulty/thin-number-balance/src/Wallet.tsx +213 -0
  286. package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/meta.json +5 -0
  287. package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/src/Wallet.tsx +254 -0
  288. package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/meta.json +5 -0
  289. package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/src/Wallet.tsx +215 -0
  290. package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/meta.json +5 -0
  291. package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/src/Wallet.tsx +214 -0
  292. package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/meta.json +5 -0
  293. package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/src/Wallet.tsx +224 -0
  294. package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/meta.json +12 -0
  295. package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/src/Wallet.tsx +225 -0
  296. package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/meta.json +11 -0
  297. package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/src/Wallet.tsx +182 -0
  298. package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/meta.json +5 -0
  299. package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/src/Wallet.tsx +160 -0
  300. package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/meta.json +5 -0
  301. package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/src/Wallet.tsx +166 -0
  302. package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/meta.json +5 -0
  303. package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/src/Wallet.tsx +161 -0
  304. package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/meta.json +5 -0
  305. package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/src/Wallet.tsx +163 -0
  306. package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/meta.json +12 -0
  307. package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/src/Wallet.tsx +162 -0
  308. package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/meta.json +5 -0
  309. package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/src/Wallet.tsx +163 -0
  310. package/evals/tasks/react-wallet/faulty/v4-retry-spread/meta.json +12 -0
  311. package/evals/tasks/react-wallet/faulty/v4-retry-spread/src/Wallet.tsx +163 -0
  312. package/evals/tasks/react-wallet/faulty/v4-status-not-idle/meta.json +5 -0
  313. package/evals/tasks/react-wallet/faulty/v4-status-not-idle/src/Wallet.tsx +162 -0
  314. package/evals/tasks/react-wallet/hidden/react-wallet.test.ts +536 -0
  315. package/evals/tasks/react-wallet/prompt.md +88 -0
  316. package/evals/tasks/react-wallet/prompt.minimal.md +51 -0
  317. package/evals/tasks/react-wallet/solutions/thin/reference/src/Wallet.tsx +213 -0
  318. package/evals/tasks/react-wallet/solutions/thin/reference-module-scope/src/Wallet.tsx +227 -0
  319. package/evals/tasks/react-wallet/solutions/thin-guide/reference/src/Wallet.tsx +213 -0
  320. package/evals/tasks/react-wallet/solutions/thin-guide/reference-module-scope/src/Wallet.tsx +227 -0
  321. package/evals/tasks/react-wallet/solutions/v3/reference/src/Wallet.tsx +224 -0
  322. package/evals/tasks/react-wallet/solutions/v3/reference-module-scope/src/Wallet.tsx +230 -0
  323. package/evals/tasks/react-wallet/solutions/v4/reference/src/Wallet.tsx +160 -0
  324. package/evals/tasks/react-wallet/solutions/v4/reference-module-scope/src/Wallet.tsx +178 -0
  325. package/evals/tasks/react-wallet/solutions/v4-proto/reference/src/Wallet.tsx +161 -0
  326. package/evals/tasks/react-wallet/solutions/v4-proto/reference-module-scope/src/Wallet.tsx +161 -0
  327. package/evals/tasks/react-wallet/starter/src/Wallet.tsx +12 -0
  328. package/evals/tasks/react-wallet/starter/src/auth.ts +16 -0
  329. package/evals/tasks/react-wallet/starter/src/config.ts +14 -0
  330. package/evals/tasks/react-wallet/starter/src/ii-auth.ts +43 -0
  331. package/evals/tasks/react-wallet/starter/src/main.tsx +15 -0
  332. package/evals/tasks/react-wallet/starter/test/smoke.test.ts +34 -0
  333. package/evals/tasks/react-wallet/starter/tsconfig.json +18 -0
  334. package/evals/tasks/react-wallet/task.json +84 -0
  335. package/examples/canister-studio/.stackblitzrc +4 -0
  336. package/examples/canister-studio/README.md +183 -0
  337. package/examples/canister-studio/did/ckbtc_minter.did +806 -0
  338. package/examples/canister-studio/did/icp_ledger.did +586 -0
  339. package/examples/canister-studio/did/icrc1.did +88 -0
  340. package/examples/canister-studio/package.json +45 -0
  341. package/examples/canister-studio/scripts/smoke.ts +188 -0
  342. package/examples/canister-studio/src/calls/CacheKeyPanel.tsx +104 -0
  343. package/examples/canister-studio/src/calls/ConfirmWrite.tsx +265 -0
  344. package/examples/canister-studio/src/calls/ErrorPanel.test.tsx +113 -0
  345. package/examples/canister-studio/src/calls/ErrorPanel.tsx +214 -0
  346. package/examples/canister-studio/src/calls/MethodRunner.test.tsx +354 -0
  347. package/examples/canister-studio/src/calls/MethodRunner.tsx +57 -0
  348. package/examples/canister-studio/src/calls/ReadRunner.tsx +244 -0
  349. package/examples/canister-studio/src/calls/ResendPanel.tsx +165 -0
  350. package/examples/canister-studio/src/calls/ShareLink.tsx +59 -0
  351. package/examples/canister-studio/src/calls/WriteRunner.test.tsx +678 -0
  352. package/examples/canister-studio/src/calls/WriteRunner.tsx +670 -0
  353. package/examples/canister-studio/src/calls/dedupe.ts +189 -0
  354. package/examples/canister-studio/src/calls/error-info.ts +168 -0
  355. package/examples/canister-studio/src/calls/history.test.ts +284 -0
  356. package/examples/canister-studio/src/calls/history.ts +287 -0
  357. package/examples/canister-studio/src/calls/kinds.ts +68 -0
  358. package/examples/canister-studio/src/calls/prepare.ts +32 -0
  359. package/examples/canister-studio/src/calls/query-options.ts +30 -0
  360. package/examples/canister-studio/src/calls/read.ts +47 -0
  361. package/examples/canister-studio/src/calls/readback.ts +100 -0
  362. package/examples/canister-studio/src/calls/record.ts +123 -0
  363. package/examples/canister-studio/src/calls/safe-calls.test.ts +299 -0
  364. package/examples/canister-studio/src/calls/share.ts +46 -0
  365. package/examples/canister-studio/src/calls/testing/doubles.tsx +108 -0
  366. package/examples/canister-studio/src/calls/testing/fixtures.ts +185 -0
  367. package/examples/canister-studio/src/calls/useCanisterHandle.ts +35 -0
  368. package/examples/canister-studio/src/canisters/ckbtc_minter.envelope.json +4228 -0
  369. package/examples/canister-studio/src/canisters/ckbtc_minter.ts +868 -0
  370. package/examples/canister-studio/src/canisters/icp_ledger.envelope.json +3454 -0
  371. package/examples/canister-studio/src/canisters/icp_ledger.ts +584 -0
  372. package/examples/canister-studio/src/canisters/icrc1.envelope.json +840 -0
  373. package/examples/canister-studio/src/canisters/icrc1.ts +78 -0
  374. package/examples/canister-studio/src/components/ActionButton.tsx +32 -0
  375. package/examples/canister-studio/src/components/AppHeader.tsx +83 -0
  376. package/examples/canister-studio/src/components/AppShell.tsx +29 -0
  377. package/examples/canister-studio/src/components/AuthButton.tsx +79 -0
  378. package/examples/canister-studio/src/components/Badge.tsx +79 -0
  379. package/examples/canister-studio/src/components/CanisterHeader.tsx +200 -0
  380. package/examples/canister-studio/src/components/CanisterJump.tsx +130 -0
  381. package/examples/canister-studio/src/components/Card.tsx +64 -0
  382. package/examples/canister-studio/src/components/Code.tsx +101 -0
  383. package/examples/canister-studio/src/components/CopyButton.tsx +126 -0
  384. package/examples/canister-studio/src/components/Disclosure.tsx +34 -0
  385. package/examples/canister-studio/src/components/EmptyState.tsx +34 -0
  386. package/examples/canister-studio/src/components/MethodList.test.tsx +137 -0
  387. package/examples/canister-studio/src/components/MethodList.tsx +244 -0
  388. package/examples/canister-studio/src/components/Notice.tsx +36 -0
  389. package/examples/canister-studio/src/components/ProvenanceBadge.tsx +44 -0
  390. package/examples/canister-studio/src/components/RouteError.tsx +42 -0
  391. package/examples/canister-studio/src/components/SectionBoundary.tsx +76 -0
  392. package/examples/canister-studio/src/components/Spinner.tsx +33 -0
  393. package/examples/canister-studio/src/components/Tabs.tsx +96 -0
  394. package/examples/canister-studio/src/components/canister/CanisterLoadFailure.test.tsx +56 -0
  395. package/examples/canister-studio/src/components/canister/CanisterLoadFailure.tsx +165 -0
  396. package/examples/canister-studio/src/components/canister/CanisterNotFound.tsx +59 -0
  397. package/examples/canister-studio/src/components/canister/CanisterPending.tsx +33 -0
  398. package/examples/canister-studio/src/components/canister/MethodOverview.tsx +93 -0
  399. package/examples/canister-studio/src/components/canister/MethodWorkspace.test.tsx +201 -0
  400. package/examples/canister-studio/src/components/canister/MethodWorkspace.tsx +187 -0
  401. package/examples/canister-studio/src/components/canister/shared-call.test.ts +186 -0
  402. package/examples/canister-studio/src/components/canister/shared-call.ts +133 -0
  403. package/examples/canister-studio/src/components/canister/shared-link-loader.test.ts +116 -0
  404. package/examples/canister-studio/src/components/canister/use-canister-page.ts +44 -0
  405. package/examples/canister-studio/src/components/canister/workspace-key.test.ts +27 -0
  406. package/examples/canister-studio/src/components/canister/workspace-key.ts +36 -0
  407. package/examples/canister-studio/src/components/canister-id-input.ts +69 -0
  408. package/examples/canister-studio/src/components/canister-names.ts +11 -0
  409. package/examples/canister-studio/src/components/download.ts +34 -0
  410. package/examples/canister-studio/src/components/fallbacks.tsx +33 -0
  411. package/examples/canister-studio/src/components/helpers.test.ts +107 -0
  412. package/examples/canister-studio/src/components/history/HistoryList.tsx +284 -0
  413. package/examples/canister-studio/src/components/home/FeaturedLedger.tsx +149 -0
  414. package/examples/canister-studio/src/components/home/OpenCanister.tsx +99 -0
  415. package/examples/canister-studio/src/components/home/featured.test.tsx +92 -0
  416. package/examples/canister-studio/src/components/home/featured.ts +64 -0
  417. package/examples/canister-studio/src/components/interface/CandidTab.tsx +38 -0
  418. package/examples/canister-studio/src/components/interface/CompatReport.tsx +92 -0
  419. package/examples/canister-studio/src/components/interface/CompatibilityTab.tsx +190 -0
  420. package/examples/canister-studio/src/components/interface/ProjectTab.tsx +196 -0
  421. package/examples/canister-studio/src/components/interface/TypeScriptTab.tsx +114 -0
  422. package/examples/canister-studio/src/components/interface/tools.test.tsx +169 -0
  423. package/examples/canister-studio/src/components/modes.ts +31 -0
  424. package/examples/canister-studio/src/components/playground/MethodPicker.tsx +67 -0
  425. package/examples/canister-studio/src/components/playground/PlaygroundForms.tsx +112 -0
  426. package/examples/canister-studio/src/components/playground/PlaygroundTools.tsx +219 -0
  427. package/examples/canister-studio/src/components/playground/sample.ts +43 -0
  428. package/examples/canister-studio/src/components/playground/target.test.tsx +86 -0
  429. package/examples/canister-studio/src/components/primitives.test.tsx +121 -0
  430. package/examples/canister-studio/src/components/provenance.ts +38 -0
  431. package/examples/canister-studio/src/components/route-render.test.tsx +71 -0
  432. package/examples/canister-studio/src/components/use-task.ts +56 -0
  433. package/examples/canister-studio/src/components/wallet/BalancePanel.tsx +110 -0
  434. package/examples/canister-studio/src/components/wallet/LedgerSummary.tsx +63 -0
  435. package/examples/canister-studio/src/components/wallet/MinterPanel.tsx +255 -0
  436. package/examples/canister-studio/src/components/wallet/SendPanel.test.tsx +297 -0
  437. package/examples/canister-studio/src/components/wallet/SendPanel.tsx +446 -0
  438. package/examples/canister-studio/src/components/wallet/ledgers.test.ts +36 -0
  439. package/examples/canister-studio/src/components/wallet/ledgers.ts +111 -0
  440. package/examples/canister-studio/src/forms/JsonPanel.tsx +157 -0
  441. package/examples/canister-studio/src/forms/MethodForm.test.tsx +369 -0
  442. package/examples/canister-studio/src/forms/MethodForm.tsx +368 -0
  443. package/examples/canister-studio/src/forms/TypedForm.tsx +55 -0
  444. package/examples/canister-studio/src/forms/context.ts +62 -0
  445. package/examples/canister-studio/src/forms/convert.ts +131 -0
  446. package/examples/canister-studio/src/forms/edge.test.ts +252 -0
  447. package/examples/canister-studio/src/forms/engine.test.ts +358 -0
  448. package/examples/canister-studio/src/forms/engine.ts +1492 -0
  449. package/examples/canister-studio/src/forms/fields/BytesField.tsx +210 -0
  450. package/examples/canister-studio/src/forms/fields/Field.tsx +106 -0
  451. package/examples/canister-studio/src/forms/fields/FieldShell.tsx +101 -0
  452. package/examples/canister-studio/src/forms/fields/Group.tsx +74 -0
  453. package/examples/canister-studio/src/forms/fields/ListField.tsx +256 -0
  454. package/examples/canister-studio/src/forms/fields/Segmented.tsx +78 -0
  455. package/examples/canister-studio/src/forms/fields/TimestampField.tsx +108 -0
  456. package/examples/canister-studio/src/forms/fields/UnitsField.tsx +119 -0
  457. package/examples/canister-studio/src/forms/fields/composites.tsx +389 -0
  458. package/examples/canister-studio/src/forms/fields/leaves.tsx +350 -0
  459. package/examples/canister-studio/src/forms/fields.test.tsx +396 -0
  460. package/examples/canister-studio/src/forms/helpers.test.ts +225 -0
  461. package/examples/canister-studio/src/forms/hints.ts +128 -0
  462. package/examples/canister-studio/src/forms/json.ts +119 -0
  463. package/examples/canister-studio/src/forms/parsing.test.ts +217 -0
  464. package/examples/canister-studio/src/forms/useMethodForm.ts +251 -0
  465. package/examples/canister-studio/src/interface/browser.test.ts +23 -0
  466. package/examples/canister-studio/src/interface/browser.ts +153 -0
  467. package/examples/canister-studio/src/interface/cache.ts +68 -0
  468. package/examples/canister-studio/src/interface/cli-cache.test.ts +155 -0
  469. package/examples/canister-studio/src/interface/cli-cache.ts +125 -0
  470. package/examples/canister-studio/src/interface/compile.server.test.ts +198 -0
  471. package/examples/canister-studio/src/interface/compile.server.ts +262 -0
  472. package/examples/canister-studio/src/interface/compile.test.ts +108 -0
  473. package/examples/canister-studio/src/interface/compile.ts +305 -0
  474. package/examples/canister-studio/src/interface/docs.test.ts +75 -0
  475. package/examples/canister-studio/src/interface/docs.ts +100 -0
  476. package/examples/canister-studio/src/interface/functions.test.ts +42 -0
  477. package/examples/canister-studio/src/interface/functions.ts +200 -0
  478. package/examples/canister-studio/src/interface/live-candid.server.ts +91 -0
  479. package/examples/canister-studio/src/interface/live-loader.test.ts +515 -0
  480. package/examples/canister-studio/src/interface/live-loader.ts +364 -0
  481. package/examples/canister-studio/src/interface/load.test.ts +328 -0
  482. package/examples/canister-studio/src/interface/load.ts +249 -0
  483. package/examples/canister-studio/src/interface/read-candid.test.ts +185 -0
  484. package/examples/canister-studio/src/interface/read-candid.ts +233 -0
  485. package/examples/canister-studio/src/interface/server-result.ts +42 -0
  486. package/examples/canister-studio/src/interface/typed.ts +68 -0
  487. package/examples/canister-studio/src/interface/types.ts +121 -0
  488. package/examples/canister-studio/src/lib/format.test.ts +107 -0
  489. package/examples/canister-studio/src/lib/format.ts +139 -0
  490. package/examples/canister-studio/src/lib/hex.test.ts +43 -0
  491. package/examples/canister-studio/src/lib/hex.ts +33 -0
  492. package/examples/canister-studio/src/lib/ids.test.ts +70 -0
  493. package/examples/canister-studio/src/lib/ids.ts +93 -0
  494. package/examples/canister-studio/src/lib/reactor.test.ts +55 -0
  495. package/examples/canister-studio/src/lib/reactor.ts +55 -0
  496. package/examples/canister-studio/src/lib/registry.test.ts +74 -0
  497. package/examples/canister-studio/src/lib/registry.ts +238 -0
  498. package/examples/canister-studio/src/lib/schema.ts +148 -0
  499. package/examples/canister-studio/src/routeTree.gen.ts +227 -0
  500. package/examples/canister-studio/src/router.test.ts +290 -0
  501. package/examples/canister-studio/src/router.tsx +127 -0
  502. package/examples/canister-studio/src/routes/__root.tsx +58 -0
  503. package/examples/canister-studio/src/routes/canister.$canisterId.index.tsx +172 -0
  504. package/examples/canister-studio/src/routes/canister.$canisterId.interface.tsx +98 -0
  505. package/examples/canister-studio/src/routes/canister.$canisterId.tsx +91 -0
  506. package/examples/canister-studio/src/routes/history.tsx +55 -0
  507. package/examples/canister-studio/src/routes/index.tsx +169 -0
  508. package/examples/canister-studio/src/routes/playground.tsx +286 -0
  509. package/examples/canister-studio/src/routes/safety.tsx +57 -0
  510. package/examples/canister-studio/src/routes/wallet.tsx +213 -0
  511. package/examples/canister-studio/src/safety/LabTransferForm.tsx +84 -0
  512. package/examples/canister-studio/src/safety/RequestLog.tsx +79 -0
  513. package/examples/canister-studio/src/safety/SafetyLab.test.tsx +89 -0
  514. package/examples/canister-studio/src/safety/SafetyLab.tsx +140 -0
  515. package/examples/canister-studio/src/safety/ScenarioCard.tsx +262 -0
  516. package/examples/canister-studio/src/safety/lab-run.ts +283 -0
  517. package/examples/canister-studio/src/safety/mock-ledger.test.ts +99 -0
  518. package/examples/canister-studio/src/safety/mock-ledger.ts +263 -0
  519. package/examples/canister-studio/src/safety/scenarios.test.ts +157 -0
  520. package/examples/canister-studio/src/safety/scenarios.ts +924 -0
  521. package/examples/canister-studio/src/styles.css +3453 -0
  522. package/examples/canister-studio/src/styles.test.ts +46 -0
  523. package/examples/canister-studio/src/values/CallPreview.test.tsx +86 -0
  524. package/examples/canister-studio/src/values/CallPreview.tsx +148 -0
  525. package/examples/canister-studio/src/values/ErrValueView.tsx +66 -0
  526. package/examples/canister-studio/src/values/Inspector.tsx +133 -0
  527. package/examples/canister-studio/src/values/ReplyView.test.tsx +341 -0
  528. package/examples/canister-studio/src/values/ReplyView.tsx +164 -0
  529. package/examples/canister-studio/src/values/candid-text.test.ts +516 -0
  530. package/examples/canister-studio/src/values/candid-text.ts +479 -0
  531. package/examples/canister-studio/src/values/hints.ts +58 -0
  532. package/examples/canister-studio/src/values/json.test.ts +66 -0
  533. package/examples/canister-studio/src/values/json.ts +31 -0
  534. package/examples/canister-studio/src/values/leaves.tsx +309 -0
  535. package/examples/canister-studio/src/values/schema.test.ts +123 -0
  536. package/examples/canister-studio/src/values/schema.ts +78 -0
  537. package/examples/canister-studio/src/values/test-router.tsx +21 -0
  538. package/examples/canister-studio/src/values/tree.tsx +670 -0
  539. package/examples/canister-studio/src/values/use-sha256.ts +29 -0
  540. package/examples/canister-studio/tsconfig.json +20 -0
  541. package/examples/canister-studio/vite.config.ts +50 -0
  542. package/examples/canister-studio/vitest.config.ts +20 -0
  543. package/examples/icrc-ledger/.stackblitzrc +4 -0
  544. package/examples/icrc-ledger/README.md +100 -0
  545. package/examples/icrc-ledger/ckbtc_minter.did +12 -0
  546. package/examples/icrc-ledger/icp_ledger.did +147 -0
  547. package/examples/icrc-ledger/icrc1.did +88 -0
  548. package/examples/icrc-ledger/index.html +13 -0
  549. package/examples/icrc-ledger/package.json +40 -0
  550. package/examples/icrc-ledger/scripts/smoke.ts +151 -0
  551. package/examples/icrc-ledger/src/App.tsx +71 -0
  552. package/examples/icrc-ledger/src/ErrorPanel.tsx +105 -0
  553. package/examples/icrc-ledger/src/MainnetTab.tsx +450 -0
  554. package/examples/icrc-ledger/src/SandboxTab.test.tsx +379 -0
  555. package/examples/icrc-ledger/src/SandboxTab.tsx +761 -0
  556. package/examples/icrc-ledger/src/blocks.test.ts +247 -0
  557. package/examples/icrc-ledger/src/blocks.ts +140 -0
  558. package/examples/icrc-ledger/src/canisters/ckbtc_minter.envelope.json +96 -0
  559. package/examples/icrc-ledger/src/canisters/ckbtc_minter.ts +16 -0
  560. package/examples/icrc-ledger/src/canisters/icp_ledger.envelope.json +690 -0
  561. package/examples/icrc-ledger/src/canisters/icp_ledger.ts +223 -0
  562. package/examples/icrc-ledger/src/canisters/icrc1.envelope.json +840 -0
  563. package/examples/icrc-ledger/src/canisters/icrc1.ts +78 -0
  564. package/examples/icrc-ledger/src/format.test.ts +41 -0
  565. package/examples/icrc-ledger/src/format.ts +68 -0
  566. package/examples/icrc-ledger/src/ledger.ts +15 -0
  567. package/examples/icrc-ledger/src/main.tsx +10 -0
  568. package/examples/icrc-ledger/src/minter.ts +16 -0
  569. package/examples/icrc-ledger/src/sandbox.test.ts +464 -0
  570. package/examples/icrc-ledger/src/sandbox.ts +281 -0
  571. package/examples/icrc-ledger/src/styles.css +391 -0
  572. package/examples/icrc-ledger/src/transfer-form.test.ts +62 -0
  573. package/examples/icrc-ledger/src/transfer-form.ts +97 -0
  574. package/examples/icrc-ledger/tsconfig.json +19 -0
  575. package/examples/icrc-ledger/vite.config.ts +44 -0
  576. package/examples/icrc-ledger/vitest.config.ts +12 -0
  577. package/examples/next-ssr/.stackblitzrc +4 -0
  578. package/examples/next-ssr/README.md +112 -0
  579. package/examples/next-ssr/icrc1.did +88 -0
  580. package/examples/next-ssr/next.config.ts +10 -0
  581. package/examples/next-ssr/package.json +41 -0
  582. package/examples/next-ssr/scripts/smoke.ts +169 -0
  583. package/examples/next-ssr/src/app/account/CertifiedBalances.tsx +58 -0
  584. package/examples/next-ssr/src/app/account/account.test.tsx +165 -0
  585. package/examples/next-ssr/src/app/account/page.tsx +82 -0
  586. package/examples/next-ssr/src/app/api/balance/[ledger]/[principal]/route.ts +10 -0
  587. package/examples/next-ssr/src/app/globals.css +278 -0
  588. package/examples/next-ssr/src/app/layout.tsx +34 -0
  589. package/examples/next-ssr/src/app/page.tsx +46 -0
  590. package/examples/next-ssr/src/app/providers.browser.test.tsx +127 -0
  591. package/examples/next-ssr/src/app/providers.test.tsx +73 -0
  592. package/examples/next-ssr/src/app/providers.tsx +60 -0
  593. package/examples/next-ssr/src/canisters/icrc1.envelope.json +840 -0
  594. package/examples/next-ssr/src/canisters/icrc1.ts +78 -0
  595. package/examples/next-ssr/src/components/BalanceTable.tsx +43 -0
  596. package/examples/next-ssr/src/components/LedgerCard.test.tsx +96 -0
  597. package/examples/next-ssr/src/components/LedgerCard.tsx +230 -0
  598. package/examples/next-ssr/src/components/LedgerError.tsx +32 -0
  599. package/examples/next-ssr/src/components/LedgerSections.tsx +22 -0
  600. package/examples/next-ssr/src/components/MyBalances.test.tsx +253 -0
  601. package/examples/next-ssr/src/components/MyBalances.tsx +129 -0
  602. package/examples/next-ssr/src/components/SessionBadge.tsx +18 -0
  603. package/examples/next-ssr/src/components/hydration.test.tsx +259 -0
  604. package/examples/next-ssr/src/components/server-data.ts +10 -0
  605. package/examples/next-ssr/src/format.ts +46 -0
  606. package/examples/next-ssr/src/ledgers.ts +38 -0
  607. package/examples/next-ssr/src/server/balance-route.test.ts +151 -0
  608. package/examples/next-ssr/src/server/balance-route.ts +157 -0
  609. package/examples/next-ssr/src/server/error-summary.test.ts +34 -0
  610. package/examples/next-ssr/src/server/error-summary.ts +37 -0
  611. package/examples/next-ssr/src/server/parse-owner.test.ts +39 -0
  612. package/examples/next-ssr/src/server/parse-owner.ts +34 -0
  613. package/examples/next-ssr/src/server/prefetch-ledgers.test.ts +137 -0
  614. package/examples/next-ssr/src/server/prefetch-ledgers.ts +70 -0
  615. package/examples/next-ssr/src/server/read-balances.test.ts +103 -0
  616. package/examples/next-ssr/src/server/read-balances.ts +78 -0
  617. package/examples/next-ssr/src/server/request-client.rsc.test.tsx +110 -0
  618. package/examples/next-ssr/src/server/request-client.ts +24 -0
  619. package/examples/next-ssr/src/testing/mock-ledgers.ts +115 -0
  620. package/examples/next-ssr/src/testing/react-server-dom-turbopack.d.ts +15 -0
  621. package/examples/next-ssr/src/testing/setup.ts +10 -0
  622. package/examples/next-ssr/tsconfig.json +31 -0
  623. package/examples/next-ssr/vitest.config.ts +59 -0
  624. package/examples/node-agent-tool/.stackblitzrc +4 -0
  625. package/examples/node-agent-tool/README.md +165 -0
  626. package/examples/node-agent-tool/icp.yaml +11 -0
  627. package/examples/node-agent-tool/icrc1.did +88 -0
  628. package/examples/node-agent-tool/package.json +32 -0
  629. package/examples/node-agent-tool/src/canisters/icrc1.envelope.json +840 -0
  630. package/examples/node-agent-tool/src/canisters/icrc1.ts +78 -0
  631. package/examples/node-agent-tool/src/cli.test.ts +64 -0
  632. package/examples/node-agent-tool/src/cli.ts +23 -0
  633. package/examples/node-agent-tool/src/command-line.ts +159 -0
  634. package/examples/node-agent-tool/src/commands/balance.ts +73 -0
  635. package/examples/node-agent-tool/src/commands/demo.test.ts +51 -0
  636. package/examples/node-agent-tool/src/commands/demo.ts +359 -0
  637. package/examples/node-agent-tool/src/commands/info.ts +69 -0
  638. package/examples/node-agent-tool/src/commands/reads.test.ts +133 -0
  639. package/examples/node-agent-tool/src/commands/resend.test.ts +363 -0
  640. package/examples/node-agent-tool/src/commands/transfer.test.ts +313 -0
  641. package/examples/node-agent-tool/src/commands/transfer.ts +415 -0
  642. package/examples/node-agent-tool/src/commands/watch.test.ts +159 -0
  643. package/examples/node-agent-tool/src/commands/watch.ts +135 -0
  644. package/examples/node-agent-tool/src/commands/whoami.ts +36 -0
  645. package/examples/node-agent-tool/src/context.ts +20 -0
  646. package/examples/node-agent-tool/src/dispatch.ts +70 -0
  647. package/examples/node-agent-tool/src/failure.test.ts +59 -0
  648. package/examples/node-agent-tool/src/failure.ts +176 -0
  649. package/examples/node-agent-tool/src/identity.test.ts +242 -0
  650. package/examples/node-agent-tool/src/identity.ts +228 -0
  651. package/examples/node-agent-tool/src/input.ts +110 -0
  652. package/examples/node-agent-tool/src/ledgers.test.ts +74 -0
  653. package/examples/node-agent-tool/src/ledgers.ts +62 -0
  654. package/examples/node-agent-tool/src/mock-ledger.ts +211 -0
  655. package/examples/node-agent-tool/src/network.test.ts +128 -0
  656. package/examples/node-agent-tool/src/network.ts +101 -0
  657. package/examples/node-agent-tool/src/output.test.ts +208 -0
  658. package/examples/node-agent-tool/src/output.ts +163 -0
  659. package/examples/node-agent-tool/src/run.ts +73 -0
  660. package/examples/node-agent-tool/src/test-kit.ts +167 -0
  661. package/examples/node-agent-tool/tsconfig.json +18 -0
  662. package/examples/node-agent-tool/vitest.config.ts +8 -0
  663. package/examples/vite-wallet/.stackblitzrc +4 -0
  664. package/examples/vite-wallet/Cargo.lock +646 -0
  665. package/examples/vite-wallet/Cargo.toml +5 -0
  666. package/examples/vite-wallet/README.md +107 -0
  667. package/examples/vite-wallet/backend/Cargo.toml +12 -0
  668. package/examples/vite-wallet/backend/backend.did +37 -0
  669. package/examples/vite-wallet/backend/src/lib.rs +133 -0
  670. package/examples/vite-wallet/icp.yaml +44 -0
  671. package/examples/vite-wallet/index.html +13 -0
  672. package/examples/vite-wallet/ledger.did +54 -0
  673. package/examples/vite-wallet/package.json +46 -0
  674. package/examples/vite-wallet/public/.ic-assets.json5 +26 -0
  675. package/examples/vite-wallet/scripts/faucet-args.test.ts +30 -0
  676. package/examples/vite-wallet/scripts/faucet-args.ts +41 -0
  677. package/examples/vite-wallet/scripts/faucet.ts +41 -0
  678. package/examples/vite-wallet/src/AddressBook.test.tsx +191 -0
  679. package/examples/vite-wallet/src/AddressBook.tsx +147 -0
  680. package/examples/vite-wallet/src/App.tsx +53 -0
  681. package/examples/vite-wallet/src/Balance.test.tsx +57 -0
  682. package/examples/vite-wallet/src/Balance.tsx +58 -0
  683. package/examples/vite-wallet/src/EnvironmentPanel.test.tsx +83 -0
  684. package/examples/vite-wallet/src/EnvironmentPanel.tsx +103 -0
  685. package/examples/vite-wallet/src/ErrorNote.tsx +70 -0
  686. package/examples/vite-wallet/src/Profile.test.tsx +145 -0
  687. package/examples/vite-wallet/src/Profile.tsx +92 -0
  688. package/examples/vite-wallet/src/SendIcp.test.tsx +231 -0
  689. package/examples/vite-wallet/src/SendIcp.tsx +244 -0
  690. package/examples/vite-wallet/src/SendToContact.test.tsx +113 -0
  691. package/examples/vite-wallet/src/SendToContact.tsx +102 -0
  692. package/examples/vite-wallet/src/SignIn.test.tsx +106 -0
  693. package/examples/vite-wallet/src/SignIn.tsx +120 -0
  694. package/examples/vite-wallet/src/Wallet.test.tsx +89 -0
  695. package/examples/vite-wallet/src/account-switch.test.tsx +108 -0
  696. package/examples/vite-wallet/src/auth/dev-accounts.test.ts +113 -0
  697. package/examples/vite-wallet/src/auth/dev-accounts.ts +216 -0
  698. package/examples/vite-wallet/src/auth/local-page.test.ts +28 -0
  699. package/examples/vite-wallet/src/auth/local-page.ts +20 -0
  700. package/examples/vite-wallet/src/auth/wallet-auth.test.ts +159 -0
  701. package/examples/vite-wallet/src/auth/wallet-auth.ts +131 -0
  702. package/examples/vite-wallet/src/canisters/backend.envelope.json +275 -0
  703. package/examples/vite-wallet/src/canisters/backend.ts +58 -0
  704. package/examples/vite-wallet/src/canisters/ledger.envelope.json +451 -0
  705. package/examples/vite-wallet/src/canisters/ledger.ts +56 -0
  706. package/examples/vite-wallet/src/client.ts +45 -0
  707. package/examples/vite-wallet/src/main.tsx +18 -0
  708. package/examples/vite-wallet/src/optimistic-contacts.ts +113 -0
  709. package/examples/vite-wallet/src/refusal.ts +17 -0
  710. package/examples/vite-wallet/src/styles.css +282 -0
  711. package/examples/vite-wallet/src/test/test-wallet.tsx +322 -0
  712. package/examples/vite-wallet/src/token.ts +50 -0
  713. package/examples/vite-wallet/src/transfer.test.ts +178 -0
  714. package/examples/vite-wallet/src/transfer.ts +110 -0
  715. package/examples/vite-wallet/src/use-canisters.ts +49 -0
  716. package/examples/vite-wallet/tsconfig.json +19 -0
  717. package/examples/vite-wallet/vite.config.ts +47 -0
  718. package/examples/vite-wallet/vitest.config.ts +17 -0
  719. package/package.json +9 -4
  720. package/packages/core/.size-limit.js +47 -0
  721. package/packages/core/LICENSE +21 -0
  722. package/packages/core/README.md +84 -0
  723. package/packages/core/llms.txt +610 -0
  724. package/packages/core/package.json +89 -0
  725. package/packages/core/src/call.ts +961 -0
  726. package/packages/core/src/canister.ts +259 -0
  727. package/packages/core/src/client.ts +2061 -0
  728. package/packages/core/src/errors.ts +1079 -0
  729. package/packages/core/src/hydration.ts +239 -0
  730. package/packages/core/src/index.ts +23 -0
  731. package/packages/core/src/keys.ts +108 -0
  732. package/packages/core/src/management.ts +138 -0
  733. package/packages/core/src/network.ts +1020 -0
  734. package/packages/core/src/options.ts +1000 -0
  735. package/packages/core/src/runtime.ts +50 -0
  736. package/packages/core/src/testing/fake-replica.ts +1166 -0
  737. package/packages/core/src/testing/index.ts +16 -0
  738. package/packages/core/src/testing/test-auth.ts +340 -0
  739. package/packages/core/src/testing/test-client.ts +559 -0
  740. package/packages/core/src/types.ts +541 -0
  741. package/packages/core/src/units.ts +299 -0
  742. package/packages/core/src/version.ts +4 -0
  743. package/packages/core/tests/canister-helpers.ts +187 -0
  744. package/packages/core/tests/canister-id.test.ts +158 -0
  745. package/packages/core/tests/canister-unwrap.test.ts +94 -0
  746. package/packages/core/tests/canister.test-d.ts +406 -0
  747. package/packages/core/tests/canister.test.ts +1921 -0
  748. package/packages/core/tests/client-account-change.test.ts +131 -0
  749. package/packages/core/tests/client-agents.test.ts +402 -0
  750. package/packages/core/tests/client-auth-network.test.ts +719 -0
  751. package/packages/core/tests/client-disposed.test.ts +403 -0
  752. package/packages/core/tests/client-helpers.ts +136 -0
  753. package/packages/core/tests/client-hydration.test.ts +166 -0
  754. package/packages/core/tests/client-pinned.test.ts +626 -0
  755. package/packages/core/tests/client-session.test.ts +348 -0
  756. package/packages/core/tests/client.test-d.ts +181 -0
  757. package/packages/core/tests/client.test.ts +1162 -0
  758. package/packages/core/tests/encode.test.ts +171 -0
  759. package/packages/core/tests/env-canister-id.test.ts +666 -0
  760. package/packages/core/tests/env-cookie-copies.test.ts +182 -0
  761. package/packages/core/tests/error-guard.test.ts +222 -0
  762. package/packages/core/tests/errors.test-d.ts +280 -0
  763. package/packages/core/tests/errors.test.ts +1359 -0
  764. package/packages/core/tests/fixtures/README.md +28 -0
  765. package/packages/core/tests/fixtures/accounts.did +21 -0
  766. package/packages/core/tests/fixtures/accounts.envelope.json +367 -0
  767. package/packages/core/tests/fixtures/accounts.ts +26 -0
  768. package/packages/core/tests/fixtures/archive.did +31 -0
  769. package/packages/core/tests/fixtures/archive.envelope.json +259 -0
  770. package/packages/core/tests/fixtures/archive.ts +55 -0
  771. package/packages/core/tests/fixtures/icrc1.did +88 -0
  772. package/packages/core/tests/fixtures/icrc1.envelope.json +840 -0
  773. package/packages/core/tests/fixtures/icrc1.ts +78 -0
  774. package/packages/core/tests/fixtures/management.did +724 -0
  775. package/packages/core/tests/fixtures/management.envelope.json +4455 -0
  776. package/packages/core/tests/fixtures/management.ts +486 -0
  777. package/packages/core/tests/fixtures/shapes.did +33 -0
  778. package/packages/core/tests/fixtures/shapes.envelope.json +440 -0
  779. package/packages/core/tests/fixtures/shapes.ts +56 -0
  780. package/packages/core/tests/fixtures/skippable.did +15 -0
  781. package/packages/core/tests/fixtures/skippable.envelope.json +182 -0
  782. package/packages/core/tests/fixtures/skippable.ts +33 -0
  783. package/packages/core/tests/func.test.ts +154 -0
  784. package/packages/core/tests/identity-attributes.test.ts +214 -0
  785. package/packages/core/tests/keys.test.ts +289 -0
  786. package/packages/core/tests/management.test.ts +206 -0
  787. package/packages/core/tests/network-helpers.ts +121 -0
  788. package/packages/core/tests/network.test-d.ts +78 -0
  789. package/packages/core/tests/network.test.ts +1051 -0
  790. package/packages/core/tests/options.test.ts +1594 -0
  791. package/packages/core/tests/redirect-sign-in.test.ts +393 -0
  792. package/packages/core/tests/resend.test-d.ts +81 -0
  793. package/packages/core/tests/resend.test.ts +723 -0
  794. package/packages/core/tests/skip.test-d.ts +125 -0
  795. package/packages/core/tests/testing-entry.test.ts +73 -0
  796. package/packages/core/tests/testing-fake-replica.test.ts +1242 -0
  797. package/packages/core/tests/testing-test-auth.test-d.ts +84 -0
  798. package/packages/core/tests/testing-test-auth.test.ts +483 -0
  799. package/packages/core/tests/testing-test-client.test-d.ts +184 -0
  800. package/packages/core/tests/testing-test-client.test.ts +1272 -0
  801. package/packages/core/tests/time-realm.test.ts +63 -0
  802. package/packages/core/tests/time.test.ts +238 -0
  803. package/packages/core/tests/traps/README.md +39 -0
  804. package/packages/core/tests/traps/anonymous-write.test.ts +145 -0
  805. package/packages/core/tests/traps/classification-table.test.ts +256 -0
  806. package/packages/core/tests/traps/err-arm.test.ts +144 -0
  807. package/packages/core/tests/traps/fetch-spy.ts +39 -0
  808. package/packages/core/tests/traps/invalidation-after-write.test.ts +178 -0
  809. package/packages/core/tests/traps/ledger.ts +248 -0
  810. package/packages/core/tests/traps/root-key-off-local.test.ts +130 -0
  811. package/packages/core/tests/traps/stale-data-across-users.test.ts +173 -0
  812. package/packages/core/tests/traps/unresolved-name.test.ts +155 -0
  813. package/packages/core/tests/traps/update-as-query.test.ts +244 -0
  814. package/packages/core/tests/traps/update-not-resent.test.ts +126 -0
  815. package/packages/core/tests/traps/update-resent-when-never-delivered.test.ts +159 -0
  816. package/packages/core/tests/traps.test-d.ts +476 -0
  817. package/packages/core/tests/units.test-d.ts +118 -0
  818. package/packages/core/tests/units.test.ts +634 -0
  819. package/packages/core/tsconfig.json +19 -0
  820. package/packages/core/tsconfig.typecheck.json +16 -0
  821. package/packages/core/vitest.config.ts +13 -0
  822. package/packages/mcp/LICENSE +21 -0
  823. package/packages/mcp/README.md +189 -0
  824. package/packages/mcp/package.json +62 -0
  825. package/packages/mcp/src/candid/candid-text.test.ts +455 -0
  826. package/packages/mcp/src/candid/candid-text.ts +501 -0
  827. package/packages/mcp/src/candid/contract.test.ts +398 -0
  828. package/packages/mcp/src/candid/formats.ts +155 -0
  829. package/packages/mcp/src/candid/hex.ts +29 -0
  830. package/packages/mcp/src/candid/json-schema.test.ts +758 -0
  831. package/packages/mcp/src/candid/json-schema.ts +454 -0
  832. package/packages/mcp/src/candid/json.test.ts +1207 -0
  833. package/packages/mcp/src/candid/json.ts +1053 -0
  834. package/packages/mcp/src/candid/schema-walk.ts +189 -0
  835. package/packages/mcp/src/candid/type-text.test.ts +179 -0
  836. package/packages/mcp/src/candid/type-text.ts +238 -0
  837. package/packages/mcp/src/cli.ts +134 -0
  838. package/packages/mcp/src/identity.ts +188 -0
  839. package/packages/mcp/src/interface/cache.test.ts +48 -0
  840. package/packages/mcp/src/interface/cache.ts +68 -0
  841. package/packages/mcp/src/interface/cli-cache.test.ts +150 -0
  842. package/packages/mcp/src/interface/cli-cache.ts +122 -0
  843. package/packages/mcp/src/interface/compile-worker.test.ts +246 -0
  844. package/packages/mcp/src/interface/compile-worker.ts +290 -0
  845. package/packages/mcp/src/interface/compile.test.ts +155 -0
  846. package/packages/mcp/src/interface/compile.ts +281 -0
  847. package/packages/mcp/src/interface/docs.test.ts +105 -0
  848. package/packages/mcp/src/interface/docs.ts +86 -0
  849. package/packages/mcp/src/interface/fixtures/icrc1.did +88 -0
  850. package/packages/mcp/src/interface/hex.ts +8 -0
  851. package/packages/mcp/src/interface/ids.test.ts +71 -0
  852. package/packages/mcp/src/interface/ids.ts +87 -0
  853. package/packages/mcp/src/interface/loader.test.ts +804 -0
  854. package/packages/mcp/src/interface/loader.ts +519 -0
  855. package/packages/mcp/src/interface/read-candid.test.ts +300 -0
  856. package/packages/mcp/src/interface/read-candid.ts +259 -0
  857. package/packages/mcp/src/interface/types.ts +113 -0
  858. package/packages/mcp/src/network.ts +122 -0
  859. package/packages/mcp/src/options.ts +149 -0
  860. package/packages/mcp/src/server/approval.ts +159 -0
  861. package/packages/mcp/src/server/naming.ts +43 -0
  862. package/packages/mcp/src/server/results.ts +102 -0
  863. package/packages/mcp/src/server/server.ts +797 -0
  864. package/packages/mcp/src/server/tools.ts +116 -0
  865. package/packages/mcp/src/usage.ts +40 -0
  866. package/packages/mcp/tests/approval.test.ts +60 -0
  867. package/packages/mcp/tests/identity.test.ts +234 -0
  868. package/packages/mcp/tests/naming.test.ts +62 -0
  869. package/packages/mcp/tests/options.test.ts +140 -0
  870. package/packages/mcp/tests/server.test.ts +997 -0
  871. package/packages/mcp/tsconfig.json +19 -0
  872. package/packages/mcp/tsconfig.typecheck.json +10 -0
  873. package/packages/mcp/vitest.config.ts +10 -0
  874. package/packages/react/LICENSE +21 -0
  875. package/packages/react/README.md +328 -0
  876. package/packages/react/llms.txt +95 -0
  877. package/packages/react/package.json +95 -0
  878. package/packages/react/src/index.tsx +678 -0
  879. package/packages/react/tests/abandoned-render.test.tsx +816 -0
  880. package/packages/react/tests/auth.test.tsx +315 -0
  881. package/packages/react/tests/caller-keys.test.tsx +2334 -0
  882. package/packages/react/tests/client-time.test.ts +40 -0
  883. package/packages/react/tests/entry.test.ts +52 -0
  884. package/packages/react/tests/helpers.tsx +291 -0
  885. package/packages/react/tests/hydration.test.tsx +262 -0
  886. package/packages/react/tests/ownership.test.tsx +260 -0
  887. package/packages/react/tests/provider.server.test.tsx +169 -0
  888. package/packages/react/tests/query-client.test.tsx +107 -0
  889. package/packages/react/tests/strict-mode.test.tsx +288 -0
  890. package/packages/react/tests/suspense.test-d.tsx +357 -0
  891. package/packages/react/tests/two-query-cores.test.tsx +135 -0
  892. package/packages/react/tests/types.test-d.tsx +63 -0
  893. package/packages/react/tsconfig.json +23 -0
  894. package/packages/react/tsconfig.test.json +8 -0
  895. package/packages/react/tsconfig.typecheck.json +16 -0
  896. package/packages/react/vitest.config.ts +39 -0
  897. package/packages/vite-plugin/LICENSE +21 -0
  898. package/packages/vite-plugin/README.md +532 -0
  899. package/packages/vite-plugin/package.json +76 -0
  900. package/packages/vite-plugin/src/describe-value.test.ts +43 -0
  901. package/packages/vite-plugin/src/describe-value.ts +167 -0
  902. package/packages/vite-plugin/src/dev-environment.test.ts +747 -0
  903. package/packages/vite-plugin/src/dev-environment.ts +510 -0
  904. package/packages/vite-plugin/src/env.ts +431 -0
  905. package/packages/vite-plugin/src/fetch.ts +348 -0
  906. package/packages/vite-plugin/src/file-identity.ts +106 -0
  907. package/packages/vite-plugin/src/generate.ts +691 -0
  908. package/packages/vite-plugin/src/index.test.ts +1243 -0
  909. package/packages/vite-plugin/src/index.ts +2565 -0
  910. package/packages/vite-plugin/src/interface.ts +339 -0
  911. package/packages/vite-plugin/src/local-host.ts +34 -0
  912. package/packages/vite-plugin/src/options.ts +641 -0
  913. package/packages/vite-plugin/src/principal-text.test.ts +87 -0
  914. package/packages/vite-plugin/src/principal-text.ts +98 -0
  915. package/packages/vite-plugin/tests/close.test.ts +141 -0
  916. package/packages/vite-plugin/tests/environment-mode.test.ts +136 -0
  917. package/packages/vite-plugin/tests/fake-ic.ts +214 -0
  918. package/packages/vite-plugin/tests/fetch-sdk.test.ts +27 -0
  919. package/packages/vite-plugin/tests/fetch.test.ts +1196 -0
  920. package/packages/vite-plugin/tests/fixtures/fake-cli/bin.js +90 -0
  921. package/packages/vite-plugin/tests/fixtures/fake-cli/package.json +8 -0
  922. package/packages/vite-plugin/tests/fixtures/icrc1.did +88 -0
  923. package/packages/vite-plugin/tests/generation.test.ts +1036 -0
  924. package/packages/vite-plugin/tests/icp-command.test.ts +194 -0
  925. package/packages/vite-plugin/tests/icp-timeout.test.ts +83 -0
  926. package/packages/vite-plugin/tests/no-process-exit.test.ts +83 -0
  927. package/packages/vite-plugin/tests/on-generated.test.ts +323 -0
  928. package/packages/vite-plugin/tests/output-collision.test.ts +329 -0
  929. package/packages/vite-plugin/tests/package.test.ts +62 -0
  930. package/packages/vite-plugin/tests/project.test.ts +2993 -0
  931. package/packages/vite-plugin/tests/support.ts +339 -0
  932. package/packages/vite-plugin/tests/timeout.test.ts +107 -0
  933. package/packages/vite-plugin/tsconfig.json +23 -0
  934. package/packages/vite-plugin/tsconfig.typecheck.json +16 -0
  935. package/packages/vite-plugin/vitest.config.ts +9 -0
  936. package/pnpm-workspace.yaml +23 -0
  937. package/scripts/ai-context-files.js +72 -0
  938. package/scripts/assemble-docs-site.mjs +485 -0
  939. package/scripts/assemble-docs-site.test.mjs +568 -0
  940. package/scripts/build-examples.js +58 -0
  941. package/scripts/check-ai-context.js +963 -0
  942. package/scripts/check-ai-context.test.mjs +811 -0
  943. package/scripts/check-exports.mjs +767 -0
  944. package/scripts/check-exports.test.mjs +1136 -0
  945. package/scripts/check-snippets/ambient.d.ts +428 -0
  946. package/scripts/check-snippets/app/generated/backend.did +7 -0
  947. package/scripts/check-snippets/app/generated/backend.ts +15 -0
  948. package/scripts/check-snippets/app/generated/icrc1.did +88 -0
  949. package/scripts/check-snippets/app/generated/icrc1.ts +78 -0
  950. package/scripts/check-snippets/app/globals.ts +26 -0
  951. package/scripts/check-snippets/docs/guides/App.tsx +21 -0
  952. package/scripts/check-snippets/docs/guides/app/providers.tsx +23 -0
  953. package/scripts/check-snippets/docs/guides/auth-client.ts +47 -0
  954. package/scripts/check-snippets/docs/guides/canisters/backend.ts +3 -0
  955. package/scripts/check-snippets/docs/guides/canisters/icrc1.ts +3 -0
  956. package/scripts/check-snippets/docs/guides/components/LedgerCard.tsx +26 -0
  957. package/scripts/check-snippets/docs/guides/components/shell.tsx +11 -0
  958. package/scripts/check-snippets/docs/guides/components/sign-in-page.tsx +29 -0
  959. package/scripts/check-snippets/docs/guides/components/slow-section.tsx +5 -0
  960. package/scripts/check-snippets/docs/guides/generated/accounts.ts +95 -0
  961. package/scripts/check-snippets/docs/guides/generated/ckbtc_minter.ts +40 -0
  962. package/scripts/check-snippets/docs/guides/generated/icp_ledger.ts +267 -0
  963. package/scripts/check-snippets/docs/guides/ledger.ts +10 -0
  964. package/scripts/check-snippets/docs/guides/server/request-client.ts +8 -0
  965. package/scripts/check-snippets.mjs +993 -0
  966. package/scripts/check-snippets.test.mjs +338 -0
  967. package/scripts/check-stack.mjs +501 -0
  968. package/scripts/check-stack.test.mjs +800 -0
  969. package/scripts/clean-node-modules.js +67 -0
  970. package/scripts/example-projects.js +58 -0
  971. package/scripts/export-budget.mjs +145 -0
  972. package/scripts/faults.json +6749 -0
  973. package/scripts/lib/fault-workspace.mjs +210 -0
  974. package/scripts/lib/io.mjs +29 -0
  975. package/scripts/lib/io.test.mjs +38 -0
  976. package/scripts/lib/markdown-fences.mjs +56 -0
  977. package/scripts/lib/markdown-fences.test.mjs +84 -0
  978. package/scripts/lib/package-exports.mjs +54 -0
  979. package/scripts/lib/package-exports.test.mjs +66 -0
  980. package/scripts/lib/release-pr-steps.mjs +47 -0
  981. package/scripts/release-tag.mjs +95 -0
  982. package/scripts/release-tag.test.mjs +278 -0
  983. package/scripts/release-workflow.test.mjs +98 -0
  984. package/scripts/release.js +167 -0
  985. package/scripts/release.test.mjs +347 -0
  986. package/scripts/removed-v3-names.js +125 -0
  987. package/scripts/size-app/.size-limit.js +90 -0
  988. package/scripts/size-app/core.js +3 -0
  989. package/scripts/size-app/package.json +24 -0
  990. package/scripts/size-app/react.js +4 -0
  991. package/scripts/size-app.test.mjs +73 -0
  992. package/scripts/sync-ai-context-versions.js +102 -0
  993. package/scripts/sync-example-versions.js +151 -0
  994. package/scripts/test-examples-published.mjs +1585 -0
  995. package/scripts/test-examples-published.test.mjs +1586 -0
  996. package/scripts/traps/any-method-vars-not-a-list.json +5 -0
  997. package/scripts/traps/canister-method-unknown.json +5 -0
  998. package/scripts/traps/client-says-who-calls-once.json +5 -0
  999. package/scripts/traps/client-says-who-calls.json +5 -0
  1000. package/scripts/traps/err-only-on-canister-err.json +14 -0
  1001. package/scripts/traps/format-units-refuses-missing-value.json +5 -0
  1002. package/scripts/traps/format-units-refuses-number.json +5 -0
  1003. package/scripts/traps/format-units-unknown-option.json +5 -0
  1004. package/scripts/traps/guard-err-is-the-methods.json +5 -0
  1005. package/scripts/traps/guard-false-keeps-every-kind.json +5 -0
  1006. package/scripts/traps/internals-not-exported.json +9 -0
  1007. package/scripts/traps/network-refuses-unknown-name.json +5 -0
  1008. package/scripts/traps/never-err-has-no-type.json +5 -0
  1009. package/scripts/traps/options-method-unknown.json +5 -0
  1010. package/scripts/traps/options-vars-wrong-type.json +5 -0
  1011. package/scripts/traps/parse-units-refuses-number.json +5 -0
  1012. package/scripts/traps/parse-units-unknown-option.json +5 -0
  1013. package/scripts/traps/principal-is-not-plain-text.json +14 -0
  1014. package/scripts/traps/query-options-refuses-oneway.json +5 -0
  1015. package/scripts/traps/query-options-refuses-update.json +5 -0
  1016. package/scripts/traps/refuse-next-unknown-option.json +5 -0
  1017. package/scripts/traps/resend-key-reads-the-methods-arg.json +5 -0
  1018. package/scripts/traps/resend-states-its-dedupe-key.json +5 -0
  1019. package/scripts/traps/suspense-refuses-argument-skip.json +5 -0
  1020. package/scripts/traps/suspense-refuses-skippable-read.json +5 -0
  1021. package/scripts/traps/suspense-refuses-vars-skip-fits.json +5 -0
  1022. package/scripts/traps/test-handler-method-unknown.json +5 -0
  1023. package/scripts/traps/test-handler-nat-is-bigint.json +5 -0
  1024. package/scripts/traps/test-handler-takes-the-domain-value.json +5 -0
  1025. package/scripts/traps/vars-of-two-arguments-are-the-tuple.json +5 -0
  1026. package/scripts/typecheck-examples.js +122 -0
  1027. package/scripts/verify-faults.mjs +375 -0
  1028. package/scripts/verify-faults.test.mjs +402 -0
  1029. package/scripts/verify-package-artifacts.mjs +590 -0
  1030. package/scripts/verify-peer-floors.js +399 -0
  1031. package/scripts/verify-peer-floors.test.mjs +422 -0
  1032. package/scripts/verify-test-fails-rules.mjs +104 -0
  1033. package/scripts/verify-test-fails-rules.test.mjs +215 -0
  1034. package/scripts/verify-test-fails.js +371 -0
  1035. package/scripts/verify-traps.mjs +505 -0
  1036. package/scripts/verify-traps.test.mjs +521 -0
  1037. package/scripts/verify-typescript-floor.js +239 -0
  1038. package/skill-packages/ic-reactor/SKILL.md +103 -0
  1039. package/stack.json +8 -0
  1040. package/tsconfig.base.json +23 -0
  1041. package/tsconfig.json +8 -0
@@ -0,0 +1,2993 @@
1
+ /**
2
+ * Phase 2 of fetching: `methods`, which writes a fetched `didFile` as the
3
+ * projection of the live interface to the methods the app calls, and the
4
+ * live check (`verify: "live"`, `IC_REACTOR_VERIFY`), which checks a
5
+ * `didFile` on disk against the live canister. Both run the real
6
+ * `@meantjs/cli` (`project`, `check`) under a real `vite build`.
7
+ *
8
+ * The canisters live on the fake replica of tests/fake-ic.ts, on a local
9
+ * port, which signs its certificates with a root key of its own.
10
+ */
11
+ import { spawn } from "node:child_process"
12
+ import fs from "node:fs"
13
+ import { createRequire } from "node:module"
14
+ import path from "node:path"
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
16
+ import type { Plugin } from "vite"
17
+ import { icReactor, type IcReactorPluginOptions } from "../src/index.js"
18
+ import { startFakeIc, type Canister } from "./fake-ic.js"
19
+ import {
20
+ createApp,
21
+ LEDGER_DID,
22
+ PING_DID,
23
+ recordingLogger,
24
+ runBuild,
25
+ spawnedExited,
26
+ startDev,
27
+ type App,
28
+ } from "./support.js"
29
+
30
+ // The real `spawn`, watched, so that each test waits for the CLI processes it
31
+ // started to exit before its app is removed (#905).
32
+ vi.mock("node:child_process", async (importOriginal) => {
33
+ const actual = await importOriginal<typeof import("node:child_process")>()
34
+ return { ...actual, spawn: vi.fn(actual.spawn) }
35
+ })
36
+
37
+ const LEDGER = fs.readFileSync(LEDGER_DID, "utf-8")
38
+ const LEDGER_ID = "ryjl3-tyaaa-aaaaa-aaaba-cai"
39
+ const OTHER_ID = "mqygn-kiaaa-aaaar-qaadq-cai"
40
+
41
+ const closers: Array<() => Promise<unknown>> = []
42
+ const apps: App[] = []
43
+
44
+ beforeEach(() => {
45
+ vi.mocked(spawn).mockClear()
46
+ })
47
+
48
+ afterEach(async () => {
49
+ vi.unstubAllGlobals()
50
+ vi.unstubAllEnvs()
51
+ for (const close of closers.splice(0).reverse()) await close()
52
+ // A test can end once a process wrote its file and before it exits. On
53
+ // Windows that process holds the app directory, its working directory, and
54
+ // removing the app fails with EBUSY.
55
+ await spawnedExited(vi.mocked(spawn))
56
+ for (const app of apps.splice(0)) app.cleanup()
57
+ })
58
+
59
+ function newApp(files: Record<string, string>) {
60
+ const app = createApp(files, "real")
61
+ apps.push(app)
62
+ return app
63
+ }
64
+
65
+ async function fakeIc(canisters: Parameters<typeof startFakeIc>[0]) {
66
+ const ic = await startFakeIc(canisters)
67
+ closers.push(ic.close)
68
+ return ic
69
+ }
70
+
71
+ /** A `fetch` that refuses every request and remembers what it was asked. */
72
+ function refuseNetwork(): string[] {
73
+ const asked: string[] = []
74
+ vi.stubGlobal(
75
+ "fetch",
76
+ vi.fn((input: unknown) => {
77
+ asked.push(String(input instanceof Request ? input.url : input))
78
+ return Promise.reject(
79
+ new TypeError("fetch failed: the test refuses the network")
80
+ )
81
+ })
82
+ )
83
+ return asked
84
+ }
85
+
86
+ const read = (app: App, file: string) =>
87
+ fs.readFileSync(path.join(app.root, file), "utf-8")
88
+ const exists = (app: App, file: string) =>
89
+ fs.existsSync(path.join(app.root, file))
90
+
91
+ /** What a build threw, as text, and what it logged. */
92
+ async function buildError(app: App, options: IcReactorPluginOptions) {
93
+ const { result, lines } = runBuild(app.root, options)
94
+ const message = await result.then(
95
+ () => "",
96
+ (error: unknown) => (error instanceof Error ? error.message : String(error))
97
+ )
98
+ return { message, lines }
99
+ }
100
+
101
+ /** The line of a failure message that names `name`. */
102
+ const lineOf = (message: string, name: string) =>
103
+ message.split("\n").find((line) => line.includes(`- ${name} (`)) ?? ""
104
+
105
+ // `as const`: the option takes a readonly list.
106
+ const METHODS = ["icrc1_transfer", "icrc1_balance_of"] as const
107
+
108
+ describe("methods", () => {
109
+ it("writes a missing didFile as the projection of the live interface to them, and a second build makes no request", async () => {
110
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: LEDGER } })
111
+ const app = newApp({})
112
+ const options: IcReactorPluginOptions = {
113
+ canisters: {
114
+ ledger: {
115
+ didFile: "did/ledger.did",
116
+ canisterId: LEDGER_ID,
117
+ network: { host: ic.host },
118
+ methods: METHODS,
119
+ },
120
+ },
121
+ }
122
+
123
+ const first = runBuild(app.root, options)
124
+ await first.result
125
+ const did = read(app, "did/ledger.did")
126
+ expect(did).toContain("icrc1_balance_of : (Account) -> (Tokens) query;")
127
+ expect(did).toContain("icrc1_transfer : (TransferArg) -> (TransferResult);")
128
+ // Nothing the two methods do not reach: no other method, and not the
129
+ // init arguments of the ledger's service class.
130
+ expect(did).not.toContain("icrc1_name")
131
+ expect(did).not.toContain("InitArgs")
132
+ expect(did).not.toContain("ledger_arg")
133
+ expect(first.lines).toContainEqual(
134
+ expect.stringContaining(
135
+ `wrote did/ledger.did from the certified candid:service of ${LEDGER_ID} on ${ic.host}, projected to icrc1_balance_of, icrc1_transfer (`
136
+ )
137
+ )
138
+ const module = read(app, "src/canisters/ledger.ts")
139
+ expect(module).toContain("icrc1_transfer")
140
+ expect(module).not.toContain("icrc1_name")
141
+ expect(ic.requests).toEqual([
142
+ "GET /api/v2/status",
143
+ `POST /api/v3/canister/${LEDGER_ID}/read_state`,
144
+ ])
145
+
146
+ // The projection on disk matches its methods: read, compared and
147
+ // generated with no request.
148
+ const second = runBuild(app.root, options)
149
+ await second.result
150
+ expect(ic.requests).toHaveLength(2)
151
+ expect(read(app, "did/ledger.did")).toBe(did)
152
+ expect(second.lines.filter((line) => line.startsWith("warn:"))).toEqual([])
153
+ })
154
+
155
+ it("with a name the live interface lacks fails, naming it and the canister's methods, and writes nothing", async () => {
156
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: LEDGER } })
157
+ const app = newApp({ "did/ping.did": PING_DID })
158
+ const { message } = await buildError(app, {
159
+ canisters: {
160
+ ping: { didFile: "did/ping.did" },
161
+ ledger: {
162
+ didFile: "did/ledger.did",
163
+ canisterId: LEDGER_ID,
164
+ network: { host: ic.host },
165
+ methods: ["icrc1_transfer", "icrc1_transfr"],
166
+ },
167
+ },
168
+ })
169
+ expect(message).toContain("could not generate 1 of 2 canisters")
170
+ const line = lineOf(message, "ledger")
171
+ expect(line).toContain(
172
+ `ledger (did/ledger.did): methods names icrc1_transfr, which the live interface of ${LEDGER_ID} on ${ic.host} does not have, so did/ledger.did is not written.`
173
+ )
174
+ expect(line).toContain(
175
+ "It has 10 methods: icrc1_balance_of, icrc1_decimals, icrc1_fee,"
176
+ )
177
+ expect(exists(app, "did/ledger.did")).toBe(false)
178
+ expect(exists(app, "src/canisters/ledger.ts")).toBe(false)
179
+ expect(exists(app, "src/canisters/ping.ts")).toBe(true)
180
+ })
181
+
182
+ it("of canisters that share a missing didFile must be one list, or nothing is fetched", async () => {
183
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: LEDGER } })
184
+ const app = newApp({})
185
+ const network = { host: ic.host }
186
+ const { message } = await buildError(app, {
187
+ canisters: {
188
+ a: {
189
+ didFile: "did/ledger.did",
190
+ canisterId: LEDGER_ID,
191
+ network,
192
+ methods: ["icrc1_transfer", "icrc1_fee"],
193
+ },
194
+ // The same list in another order is the same projection.
195
+ b: {
196
+ didFile: "did/ledger.did",
197
+ canisterId: LEDGER_ID,
198
+ network,
199
+ methods: ["icrc1_fee", "icrc1_transfer"],
200
+ outDir: "src/b",
201
+ },
202
+ c: {
203
+ didFile: "did/ledger.did",
204
+ canisterId: LEDGER_ID,
205
+ network,
206
+ methods: ["icrc1_fee"],
207
+ outDir: "src/c",
208
+ },
209
+ },
210
+ })
211
+ expect(message).toContain(
212
+ `"a", "b", "c" name did/ledger.did but not one list of methods to project it to (` +
213
+ `a: icrc1_fee, icrc1_transfer; b: icrc1_fee, icrc1_transfer; c: icrc1_fee): ` +
214
+ `the file holds one projection, which every one of them generates from, so it is not fetched.`
215
+ )
216
+ expect(ic.requests).toEqual([])
217
+ expect(exists(app, "did/ledger.did")).toBe(false)
218
+
219
+ // Without c, a and b share one fetch.
220
+ const { result } = runBuild(app.root, {
221
+ canisters: {
222
+ a: {
223
+ didFile: "did/ledger.did",
224
+ canisterId: LEDGER_ID,
225
+ network,
226
+ methods: ["icrc1_transfer", "icrc1_fee"],
227
+ },
228
+ b: {
229
+ didFile: "did/ledger.did",
230
+ canisterId: LEDGER_ID,
231
+ network,
232
+ methods: ["icrc1_fee", "icrc1_transfer"],
233
+ outDir: "src/b",
234
+ },
235
+ },
236
+ })
237
+ await result
238
+ expect(ic.requests).toHaveLength(2)
239
+ expect(read(app, "did/ledger.did")).not.toContain("icrc1_name")
240
+ })
241
+
242
+ it("of a canister with no canisterId count when it shares a missing didFile: no methods is the whole interface, and nothing is fetched", async () => {
243
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: LEDGER } })
244
+ const app = newApp({})
245
+ const options: IcReactorPluginOptions = {
246
+ canisters: {
247
+ // Only this entry can fetch the file, for one method.
248
+ transfer: {
249
+ didFile: "did/ledger.did",
250
+ canisterId: LEDGER_ID,
251
+ network: { host: ic.host },
252
+ methods: ["icrc1_transfer"],
253
+ },
254
+ // It reads the same file, and wants the whole interface.
255
+ ledger: { didFile: "did/ledger.did", outDir: "src/whole" },
256
+ },
257
+ }
258
+ const { message } = await buildError(app, options)
259
+ const refusal =
260
+ `"transfer", "ledger" name did/ledger.did but not one list of methods to project it to (` +
261
+ `transfer: icrc1_transfer; ledger: no methods, the whole interface): ` +
262
+ `the file holds one projection, which every one of them generates from, so it is not fetched. ` +
263
+ `Give them one list, or give each list a didFile of its own.`
264
+ // One message for both, on one line.
265
+ expect(message).toContain(
266
+ `- transfer (did/ledger.did), ledger (did/ledger.did): ${refusal}`
267
+ )
268
+ expect(ic.requests).toEqual([])
269
+ expect(exists(app, "did/ledger.did")).toBe(false)
270
+ expect(exists(app, "src/canisters/transfer.ts")).toBe(false)
271
+ expect(exists(app, "src/whole/ledger.ts")).toBe(false)
272
+
273
+ // A file on disk is read by both, with no request, and still compared
274
+ // with each list: the whole interface holds more than icrc1_transfer.
275
+ fs.mkdirSync(path.join(app.root, "did"), { recursive: true })
276
+ fs.writeFileSync(path.join(app.root, "did/ledger.did"), LEDGER)
277
+ const onDisk = runBuild(app.root, options)
278
+ await onDisk.result
279
+ expect(ic.requests).toEqual([])
280
+ expect(onDisk.lines).toContainEqual(
281
+ expect.stringContaining(
282
+ "ic-reactor: transfer: did/ledger.did also holds icrc1_balance_of,"
283
+ )
284
+ )
285
+ expect(read(app, "src/whole/ledger.ts")).toContain("icrc1_name")
286
+
287
+ // Named by IC_REACTOR_FETCH while on disk, the file is to be fetched
288
+ // again: the lists are compared before the request, as for a missing one.
289
+ vi.stubEnv("IC_REACTOR_FETCH", "transfer")
290
+ const again = await buildError(app, options)
291
+ expect(again.message).toContain(
292
+ `- transfer (did/ledger.did), ledger (did/ledger.did): ${refusal}`
293
+ )
294
+ expect(ic.requests).toEqual([])
295
+ expect(read(app, "did/ledger.did")).toBe(LEDGER)
296
+ })
297
+
298
+ it("of a canister with no canisterId that names another list for a shared missing didFile refuse the fetch, naming each list", async () => {
299
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: LEDGER } })
300
+ const app = newApp({})
301
+ const { message } = await buildError(app, {
302
+ canisters: {
303
+ transfer: {
304
+ didFile: "did/ledger.did",
305
+ canisterId: LEDGER_ID,
306
+ network: { host: ic.host },
307
+ methods: ["icrc1_transfer", "icrc1_fee"],
308
+ },
309
+ balance: {
310
+ didFile: "did/ledger.did",
311
+ outDir: "src/balance",
312
+ methods: ["icrc1_balance_of"],
313
+ },
314
+ },
315
+ })
316
+ const refusal =
317
+ `"transfer", "balance" name did/ledger.did but not one list of methods to project it to (` +
318
+ `transfer: icrc1_fee, icrc1_transfer; balance: icrc1_balance_of)`
319
+ // One message for both, on one line.
320
+ expect(message).toContain(
321
+ `- transfer (did/ledger.did), balance (did/ledger.did): ${refusal}`
322
+ )
323
+ expect(ic.requests).toEqual([])
324
+ expect(exists(app, "did/ledger.did")).toBe(false)
325
+ })
326
+
327
+ it("of a canister with no canisterId that cannot be projected to refuse a shared missing didFile's fetch", async () => {
328
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: LEDGER } })
329
+ const app = newApp({})
330
+ const { message } = await buildError(app, {
331
+ canisters: {
332
+ transfer: {
333
+ didFile: "did/ledger.did",
334
+ canisterId: LEDGER_ID,
335
+ network: { host: ic.host },
336
+ methods: ["icrc1_transfer"],
337
+ },
338
+ broken: {
339
+ didFile: "did/ledger.did",
340
+ outDir: "src/broken",
341
+ methods: ["icrc1_transfer,icrc1_fee"],
342
+ },
343
+ },
344
+ })
345
+ const refusal =
346
+ `"transfer", "broken" name did/ledger.did, which is not fetched, since "broken" cannot be generated ` +
347
+ `(broken: methods names "icrc1_transfer,icrc1_fee", which has a comma`
348
+ // One message for both, on one line.
349
+ expect(message).toContain(
350
+ `- transfer (did/ledger.did), broken (did/ledger.did): ${refusal}`
351
+ )
352
+ expect(ic.requests).toEqual([])
353
+ expect(exists(app, "did/ledger.did")).toBe(false)
354
+ })
355
+
356
+ it("refuses an empty list and a name with a comma, with no request", async () => {
357
+ const asked = refuseNetwork()
358
+ const app = newApp({})
359
+ const { message } = await buildError(app, {
360
+ canisters: {
361
+ none: {
362
+ didFile: "did/none.did",
363
+ canisterId: LEDGER_ID,
364
+ methods: [],
365
+ },
366
+ comma: {
367
+ didFile: "did/comma.did",
368
+ canisterId: LEDGER_ID,
369
+ methods: ["a,b"],
370
+ },
371
+ },
372
+ })
373
+ expect(lineOf(message, "none")).toContain(
374
+ "none (did/none.did): methods is empty"
375
+ )
376
+ expect(lineOf(message, "comma")).toContain(
377
+ `comma (did/comma.did): methods names "a,b", which has a comma`
378
+ )
379
+ expect(asked).toEqual([])
380
+ })
381
+
382
+ // A JavaScript config is not type-checked: only an omitted list is the
383
+ // whole interface, and every value given is checked as a list.
384
+ it("given as null, false, 0 or an empty string fail the canister of a didFile on disk, with no request", async () => {
385
+ const asked = refuseNetwork()
386
+ const app = newApp({ "did/ping.did": PING_DID })
387
+ for (const methods of [null, false, 0, ""]) {
388
+ const { message } = await buildError(app, {
389
+ canisters: {
390
+ ping: { didFile: "did/ping.did", methods: methods as never },
391
+ },
392
+ })
393
+ expect(lineOf(message, "ping")).toContain(
394
+ `ping (did/ping.did): methods is ${JSON.stringify(methods)}, not a list of method names`
395
+ )
396
+ }
397
+ expect(exists(app, "src/canisters/ping.ts")).toBe(false)
398
+ expect(asked).toEqual([])
399
+ })
400
+
401
+ it("of a didFile on disk are compared with it, with no request: a name it lacks fails, and a method it has too is a warning", async () => {
402
+ const asked = refuseNetwork()
403
+ const app = newApp({ "did/ledger.did": LEDGER })
404
+ const base = {
405
+ didFile: "did/ledger.did",
406
+ canisterId: LEDGER_ID,
407
+ network: "ic" as const,
408
+ }
409
+
410
+ // The file has every method of the list, and more.
411
+ const { result, lines } = runBuild(app.root, {
412
+ canisters: { ledger: { ...base, methods: METHODS } },
413
+ })
414
+ await result
415
+ expect(lines).toContainEqual(
416
+ expect.stringContaining(
417
+ "warn: ic-reactor: ledger: did/ledger.did also holds icrc1_decimals, icrc1_fee, icrc1_metadata,"
418
+ )
419
+ )
420
+ expect(lines).toContainEqual(
421
+ expect.stringContaining(
422
+ "which methods does not name, so its module has them too. Fetch it again with IC_REACTOR_FETCH=ledger to write the projection to methods."
423
+ )
424
+ )
425
+ expect(exists(app, "src/canisters/ledger.ts")).toBe(true)
426
+
427
+ // The list names a method the file does not hold.
428
+ const { message } = await buildError(app, {
429
+ canisters: {
430
+ ledger: { ...base, methods: [...METHODS, "icrc2_approve"] },
431
+ },
432
+ })
433
+ expect(lineOf(message, "ledger")).toContain(
434
+ "ledger (did/ledger.did): methods names icrc2_approve, which did/ledger.did does not hold (it holds icrc1_balance_of,"
435
+ )
436
+ expect(lineOf(message, "ledger")).toContain(
437
+ "Fetch it again with IC_REACTOR_FETCH=ledger to write the projection to methods, or take the name out of methods."
438
+ )
439
+ expect(asked).toEqual([])
440
+ expect(read(app, "did/ledger.did")).toBe(LEDGER)
441
+ })
442
+
443
+ it("are compared with a didFile saved under vite dev, with no request", async () => {
444
+ const asked = refuseNetwork()
445
+ const app = newApp({ "did/item.did": WRITTEN })
446
+ const running = await startDev(app.root, {
447
+ canisters: {
448
+ item: {
449
+ didFile: "did/item.did",
450
+ canisterId: LEDGER_ID,
451
+ methods: ["get", "maybe", "put"],
452
+ },
453
+ },
454
+ })
455
+ closers.push(running.close)
456
+ expect(running.lines.filter((line) => line.startsWith("error:"))).toEqual(
457
+ []
458
+ )
459
+
460
+ // A save that drops a method the list names.
461
+ const file = path.join(app.root, "did/item.did")
462
+ fs.writeFileSync(file, WRITTEN.replace(" put : (nat) -> ();\n", ""))
463
+ running.server.watcher.emit("change", file)
464
+ await vi.waitFor(() =>
465
+ expect(running.lines).toContainEqual(
466
+ expect.stringContaining(
467
+ "item (did/item.did): methods names put, which did/item.did does not hold (it holds get, maybe)"
468
+ )
469
+ )
470
+ )
471
+ expect(asked).toEqual([])
472
+ })
473
+
474
+ // The names are listed with a projection to a name no service can have.
475
+ // One that a service can have, as the name the plugin once asked for, lists
476
+ // nothing when the service has it.
477
+ it("of a didFile are compared with it whatever its methods are named", async () => {
478
+ const asked = refuseNetwork()
479
+ const app = newApp({
480
+ "did/item.did": WRITTEN.replace(
481
+ "service : {\n",
482
+ 'service : {\n "ic-reactor list methods" : () -> ();\n'
483
+ ),
484
+ })
485
+ const { message } = await buildError(app, {
486
+ canisters: {
487
+ item: {
488
+ didFile: "did/item.did",
489
+ canisterId: LEDGER_ID,
490
+ methods: ["get", "absent"],
491
+ },
492
+ },
493
+ })
494
+ expect(lineOf(message, "item")).toContain(
495
+ "item (did/item.did): methods names absent, which did/item.did does not hold (it holds get, ic-reactor list methods, maybe, put)"
496
+ )
497
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
498
+ expect(asked).toEqual([])
499
+ })
500
+
501
+ // The CLI that lists them crashes, and its `gen` would succeed: the
502
+ // comparison that did not run must not pass for one that did.
503
+ it("of a didFile whose methods cannot be listed fail the canister, which is not generated", async () => {
504
+ const asked = refuseNetwork()
505
+ const app = createApp({ "did/item.did": WRITTEN }, "fake")
506
+ apps.push(app)
507
+ const { message } = await buildError(app, {
508
+ canisters: {
509
+ item: {
510
+ didFile: "did/item.did",
511
+ canisterId: LEDGER_ID,
512
+ methods: ["get"],
513
+ },
514
+ },
515
+ })
516
+ expect(message).toContain("could not generate 1 of 1 canisters")
517
+ expect(lineOf(message, "item")).toContain(
518
+ "item (did/item.did): methods cannot be compared with did/item.did, whose methods could not be listed:"
519
+ )
520
+ expect(message).toContain("fake trap in project")
521
+ expect(app.runs().map(([command]) => command)).toEqual(["project"])
522
+ expect(asked).toEqual([])
523
+ })
524
+
525
+ it("is projected again by IC_REACTOR_FETCH, which rewrites the file", async () => {
526
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: LEDGER } })
527
+ const app = newApp({ "did/ledger.did": LEDGER })
528
+ vi.stubEnv("IC_REACTOR_FETCH", "ledger")
529
+ const { result, lines } = runBuild(app.root, {
530
+ canisters: {
531
+ ledger: {
532
+ didFile: "did/ledger.did",
533
+ canisterId: LEDGER_ID,
534
+ network: { host: ic.host },
535
+ methods: METHODS,
536
+ },
537
+ },
538
+ })
539
+ await result
540
+ expect(lines).toContainEqual(
541
+ expect.stringContaining(
542
+ "rewrote did/ledger.did from the certified candid:service"
543
+ )
544
+ )
545
+ expect(read(app, "did/ledger.did")).not.toContain("icrc1_name")
546
+ expect(lines.filter((line) => line.startsWith("warn:"))).toEqual([])
547
+ })
548
+ })
549
+
550
+ // A service small enough to change by hand, for the live check.
551
+ const WRITTEN = `type Item = record { a : nat };
552
+ service : {
553
+ get : () -> (Item) query;
554
+ put : (nat) -> ();
555
+ maybe : () -> (opt nat) query;
556
+ }
557
+ `
558
+
559
+ /** `WRITTEN` with `edits` made, as the live canister now publishes it. */
560
+ const liveWith = (...edits: Array<[string, string]>) =>
561
+ edits.reduce((text, [from, to]) => text.replace(from, to), WRITTEN)
562
+
563
+ /** `WRITTEN` without `put`. */
564
+ const WITHOUT_PUT = liveWith([" put : (nat) -> ();\n", ""])
565
+
566
+ /**
567
+ * The bin of a meant that runs the real one, whose bin script is
568
+ * `real`, and writes a file around one process (see `hookCli`). Every
569
+ * process appends its arguments to runs.log beside it, as the fake CLI does.
570
+ */
571
+ function hookedBin(real: string): string {
572
+ return `const { spawnSync } = require("node:child_process")
573
+ const fs = require("node:fs")
574
+ const path = require("node:path")
575
+
576
+ const args = process.argv.slice(2)
577
+ const runs = path.join(__dirname, "runs.log")
578
+ fs.appendFileSync(runs, JSON.stringify(args) + "\\n")
579
+ // Held, from the process the test names on, while block.json is there.
580
+ const block = path.join(__dirname, "block.json")
581
+ try {
582
+ const { from } = JSON.parse(fs.readFileSync(block, "utf-8"))
583
+ const count = fs.readFileSync(runs, "utf-8").split("\\n").filter(Boolean).length
584
+ const pause = new Int32Array(new SharedArrayBuffer(4))
585
+ while (count >= from && fs.existsSync(block)) Atomics.wait(pause, 0, 0, 20)
586
+ } catch {}
587
+ // A projection to the empty name lists a file's methods.
588
+ const step = args[0] === "project" && args[3] === "," ? "list" : args[0]
589
+ const hook = path.join(__dirname, "hook.json")
590
+ let save
591
+ try {
592
+ const armed = JSON.parse(fs.readFileSync(hook, "utf-8"))
593
+ if (armed.step === step) {
594
+ // Taken by one process alone.
595
+ fs.renameSync(hook, hook + "." + process.pid)
596
+ save = armed
597
+ }
598
+ } catch {}
599
+ if (save && save.when === "before") fs.writeFileSync(save.file, save.text)
600
+ const run = spawnSync(process.execPath, [${JSON.stringify(real)}, ...args], {
601
+ stdio: "inherit",
602
+ })
603
+ if (save && save.when === "after") fs.writeFileSync(save.file, save.text)
604
+ if (save && save.back !== undefined) fs.writeFileSync(save.file, save.back)
605
+ process.exit(run.status === null ? 1 : run.status)
606
+ `
607
+ }
608
+
609
+ /**
610
+ * Install in `app` a meant that runs the real one, in place of the
611
+ * link to it, so that a test can write a `.did` at a moment it cannot time
612
+ * otherwise: `save` writes `text` to `file` once, around the first process
613
+ * that runs `step` (`"list"`, the projection that lists a file's methods,
614
+ * `"check"` or `"gen"`), either `"before"` the real CLI starts or `"after"`
615
+ * it ended, before the process exits. With `back`, the process then writes
616
+ * `back` too, once the real CLI ended: a file saved and saved back holds its
617
+ * text again, with another stamp. The plugin sees those writes as soon as the
618
+ * process is done. `commands` names the command each process ran. `hold`
619
+ * keeps each process from the `from`th one on (counted from the first)
620
+ * waiting before the real CLI starts, until `release`.
621
+ */
622
+ function hookCli(app: App) {
623
+ const installed = path.join(app.root, "node_modules", "@meantjs", "cli")
624
+ const manifest = createRequire(import.meta.url).resolve(
625
+ "@meantjs/cli/package.json"
626
+ )
627
+ const { version, bin } = JSON.parse(fs.readFileSync(manifest, "utf-8")) as {
628
+ version: string
629
+ bin: Record<string, string>
630
+ }
631
+ fs.unlinkSync(installed)
632
+ fs.mkdirSync(installed)
633
+ fs.writeFileSync(
634
+ path.join(installed, "package.json"),
635
+ JSON.stringify({
636
+ name: "@meantjs/cli",
637
+ version,
638
+ bin: { meant: "./bin.js" },
639
+ })
640
+ )
641
+ fs.writeFileSync(
642
+ path.join(installed, "bin.js"),
643
+ hookedBin(path.join(path.dirname(manifest), bin["meant"]))
644
+ )
645
+ return {
646
+ save: (
647
+ step: "list" | "check" | "gen",
648
+ when: "before" | "after",
649
+ file: string,
650
+ text: string,
651
+ { back }: { back?: string } = {}
652
+ ) =>
653
+ fs.writeFileSync(
654
+ path.join(installed, "hook.json"),
655
+ JSON.stringify({ step, when, file, text, back })
656
+ ),
657
+ commands: () => app.runs().map(([command]) => command),
658
+ hold: (from: number) =>
659
+ fs.writeFileSync(
660
+ path.join(installed, "block.json"),
661
+ JSON.stringify({ from })
662
+ ),
663
+ release: () =>
664
+ fs.rmSync(path.join(installed, "block.json"), { force: true }),
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Record what the plugin sends the browsers, through each server it is
670
+ * handed: the first, and the one each restart creates, which Vite hands the
671
+ * plugin before the restart's buildStart.
672
+ */
673
+ function recordSends() {
674
+ const spies: Array<{ mock: { calls: unknown[][] } }> = []
675
+ const plugin: Plugin = {
676
+ name: "record-sends",
677
+ enforce: "pre",
678
+ configureServer(server) {
679
+ spies.push(vi.spyOn(server.ws, "send"))
680
+ },
681
+ }
682
+ return {
683
+ plugin,
684
+ /** Every payload sent, in order. */
685
+ sent: () =>
686
+ spies.flatMap((spy) => spy.mock.calls.map(([payload]) => payload)),
687
+ }
688
+ }
689
+
690
+ /** The error overlays among `payloads`. */
691
+ const overlays = (payloads: unknown[]) =>
692
+ payloads.filter(
693
+ (payload) => (payload as { type?: string } | undefined)?.type === "error"
694
+ )
695
+
696
+ /**
697
+ * Capture what the plugin registers on the WebSocket server. A listener is
698
+ * handed the connecting client as Vite hands it: a socket with `send`.
699
+ */
700
+ function spyOnConnections() {
701
+ const connections: Array<(client?: unknown) => void> = []
702
+ const plugin: Plugin = {
703
+ name: "spy-on-connections",
704
+ enforce: "pre",
705
+ configureServer(server) {
706
+ vi.spyOn(server.ws, "on").mockImplementation(((
707
+ event: string,
708
+ callback: (client?: unknown) => void
709
+ ) => {
710
+ if (event === "connection") connections.push(callback)
711
+ }) as never)
712
+ },
713
+ }
714
+ return { connections, plugin }
715
+ }
716
+
717
+ /**
718
+ * A `fetch` that answers as the network does, but holds the answer to the
719
+ * first `read_state` until `release` is called: a live check under way.
720
+ * `reached` resolves once that answer is held, read from the canister as it
721
+ * was then.
722
+ */
723
+ function holdFirstReadState() {
724
+ const real = globalThis.fetch
725
+ let release!: () => void
726
+ const released = new Promise<void>((resolve) => (release = resolve))
727
+ let reach!: () => void
728
+ const reached = new Promise<void>((resolve) => (reach = resolve))
729
+ let holding = true
730
+ vi.stubGlobal(
731
+ "fetch",
732
+ async (input: string | URL | Request, init?: RequestInit) => {
733
+ const response = await real(input, init)
734
+ const url = input instanceof Request ? input.url : String(input)
735
+ if (holding && url.includes("/read_state")) {
736
+ holding = false
737
+ reach()
738
+ await released
739
+ }
740
+ return response
741
+ }
742
+ )
743
+ return { reached, release }
744
+ }
745
+
746
+ /**
747
+ * A plugin of `options` under `vite build`, whose buildStart the test calls,
748
+ * as each rebuild of `vite build --watch` does. `newRun` resolves the config
749
+ * again, as the next build or a restart does.
750
+ */
751
+ function drivenPlugin(app: App, options: IcReactorPluginOptions) {
752
+ const plugin = icReactor(options) as Plugin & {
753
+ configResolved: (config: unknown) => void
754
+ buildStart: (this: unknown) => Promise<void>
755
+ }
756
+ const { logger, lines } = recordingLogger()
757
+ const newRun = () =>
758
+ plugin.configResolved({ root: app.root, command: "build", logger })
759
+ newRun()
760
+ const context = { addWatchFile: vi.fn(), error: vi.fn() }
761
+ return {
762
+ buildStart: () => plugin.buildStart.call(context),
763
+ newRun,
764
+ context,
765
+ lines,
766
+ }
767
+ }
768
+
769
+ /** The options of one canister, `item`, checked live against `host`. */
770
+ const checkedLive = (host: string): IcReactorPluginOptions => ({
771
+ canisters: {
772
+ item: {
773
+ didFile: "did/item.did",
774
+ canisterId: LEDGER_ID,
775
+ network: { host },
776
+ verify: "live",
777
+ },
778
+ },
779
+ })
780
+
781
+ /** A plugin for one canister checked live, as `drivenPlugin`. */
782
+ const verifyingPlugin = (app: App, host: string) =>
783
+ drivenPlugin(app, checkedLive(host))
784
+
785
+ describe("the live check", () => {
786
+ const checked = (host: string): IcReactorPluginOptions => ({
787
+ canisters: {
788
+ item: {
789
+ didFile: "did/item.did",
790
+ canisterId: LEDGER_ID,
791
+ network: { host },
792
+ verify: "live",
793
+ },
794
+ },
795
+ })
796
+
797
+ it("passes when the live interface is unchanged, and is made once for one text", async () => {
798
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: WRITTEN } })
799
+ const app = newApp({ "did/item.did": WRITTEN })
800
+ const { buildStart, context, lines } = verifyingPlugin(app, ic.host)
801
+
802
+ await buildStart()
803
+ expect(context.error).not.toHaveBeenCalled()
804
+ expect(lines).toContainEqual(
805
+ `info: ic-reactor: item: did/item.did matches the live interface of ${LEDGER_ID} on ${ic.host}`
806
+ )
807
+ expect(exists(app, "src/canisters/item.ts")).toBe(true)
808
+ expect(ic.requests).toEqual([
809
+ "GET /api/v2/status",
810
+ `POST /api/v3/canister/${LEDGER_ID}/read_state`,
811
+ ])
812
+
813
+ // Vite 6+'s second buildStart, or a rebuild of `vite build --watch`.
814
+ await buildStart()
815
+ expect(ic.requests).toHaveLength(2)
816
+ })
817
+
818
+ // A rebuild of `vite build --watch` is the same run, so it may reuse a
819
+ // pass. Not after a check of the canister failed: restoring the text that
820
+ // passed must not stand in for a check, whether the network is down or the
821
+ // canister changed. Each refused check retries with the agent's backoff,
822
+ // which takes about 5 s, past vitest's default timeout on a loaded machine.
823
+ it("checks a text that passed again once a check of the canister failed, so restoring it is no pass", async () => {
824
+ const canisters: Record<string, Canister> = {
825
+ [LEDGER_ID]: { candid: WRITTEN },
826
+ }
827
+ const ic = await fakeIc(canisters)
828
+ const app = newApp({ "did/item.did": WRITTEN })
829
+ const { buildStart, context } = verifyingPlugin(app, ic.host)
830
+ const file = path.join(app.root, "did/item.did")
831
+ const edited = `// Edited.\n${WRITTEN}`
832
+ const lastError = () => String(context.error.mock.lastCall?.[0])
833
+
834
+ await buildStart()
835
+ expect(context.error).not.toHaveBeenCalled()
836
+ expect(ic.requests).toHaveLength(2)
837
+
838
+ // The network goes down, and an edit's check cannot be made.
839
+ const asked = refuseNetwork()
840
+ fs.writeFileSync(file, edited)
841
+ await buildStart()
842
+ expect(context.error).toHaveBeenCalledTimes(1)
843
+ expect(lastError()).toContain(`could not reach ${ic.host}`)
844
+ const refused = asked.length
845
+ expect(refused).toBeGreaterThan(0)
846
+
847
+ // The text that passed is restored: checked again, and still not made.
848
+ fs.writeFileSync(file, WRITTEN)
849
+ await buildStart()
850
+ expect(asked.length).toBeGreaterThan(refused)
851
+ expect(context.error).toHaveBeenCalledTimes(2)
852
+ expect(lastError()).toContain(`could not reach ${ic.host}`)
853
+
854
+ // The network is back: it passes.
855
+ vi.unstubAllGlobals()
856
+ await buildStart()
857
+ expect(context.error).toHaveBeenCalledTimes(2)
858
+ expect(ic.requests).toHaveLength(4)
859
+
860
+ // The canister loses put, and an edit's check fails.
861
+ canisters[LEDGER_ID] = { candid: liveWith([" put : (nat) -> ();\n", ""]) }
862
+ fs.writeFileSync(file, edited)
863
+ await buildStart()
864
+ expect(context.error).toHaveBeenCalledTimes(3)
865
+ expect(ic.requests).toHaveLength(6)
866
+ const incompatible = `incompatible with did/item.did (1 error)`
867
+ expect(lastError()).toContain(incompatible)
868
+
869
+ // The text that passed is restored: checked again, and it fails.
870
+ fs.writeFileSync(file, WRITTEN)
871
+ await buildStart()
872
+ expect(ic.requests).toHaveLength(8)
873
+ expect(context.error).toHaveBeenCalledTimes(4)
874
+ expect(lastError()).toContain(incompatible)
875
+ }, 30_000)
876
+
877
+ // A fetch reads the live interface, as a check does: a text that passed
878
+ // before must not stand for a canister the plugin has just read without
879
+ // it, nor for one it could not read.
880
+ it("checks a text that passed again once a fetch of its didFile read another interface", async () => {
881
+ const canisters: Record<string, Canister> = {
882
+ [LEDGER_ID]: { candid: WRITTEN },
883
+ }
884
+ const ic = await fakeIc(canisters)
885
+ const app = newApp({ "did/item.did": WRITTEN })
886
+ const { buildStart, context } = verifyingPlugin(app, ic.host)
887
+ const file = path.join(app.root, "did/item.did")
888
+ const withoutPut = liveWith([" put : (nat) -> ();\n", ""])
889
+
890
+ await buildStart()
891
+ expect(context.error).not.toHaveBeenCalled()
892
+ expect(ic.requests).toHaveLength(2)
893
+
894
+ // The canister loses put, and the file is deleted: fetched, without put.
895
+ canisters[LEDGER_ID] = { candid: withoutPut }
896
+ fs.rmSync(file)
897
+ await buildStart()
898
+ expect(context.error).not.toHaveBeenCalled()
899
+ expect(read(app, "did/item.did")).toBe(withoutPut)
900
+ expect(ic.requests).toHaveLength(4)
901
+
902
+ // The text that passed, with put, is restored: checked again, it fails.
903
+ fs.writeFileSync(file, WRITTEN)
904
+ await buildStart()
905
+ expect(ic.requests).toHaveLength(6)
906
+ expect(context.error).toHaveBeenCalledTimes(1)
907
+ expect(String(context.error.mock.lastCall?.[0])).toContain(
908
+ `item (did/item.did): the live check found ${LEDGER_ID} on ${ic.host} incompatible with did/item.did (1 error)`
909
+ )
910
+
911
+ // Fetched again, it passes as fetched: the next rebuild checks nothing.
912
+ fs.rmSync(file)
913
+ await buildStart()
914
+ expect(ic.requests).toHaveLength(8)
915
+ await buildStart()
916
+ expect(ic.requests).toHaveLength(8)
917
+ expect(context.error).toHaveBeenCalledTimes(1)
918
+ })
919
+
920
+ // Each refused fetch or check retries with the agent's backoff, about 5 s.
921
+ it("checks a text that passed again once a fetch of its didFile failed", async () => {
922
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: WRITTEN } })
923
+ const app = newApp({ "did/item.did": WRITTEN })
924
+ const { buildStart, context } = verifyingPlugin(app, ic.host)
925
+ const file = path.join(app.root, "did/item.did")
926
+ const lastError = () => String(context.error.mock.lastCall?.[0])
927
+
928
+ await buildStart()
929
+ expect(context.error).not.toHaveBeenCalled()
930
+ expect(ic.requests).toHaveLength(2)
931
+
932
+ // The network goes down, and the file is deleted: the fetch fails.
933
+ const asked = refuseNetwork()
934
+ fs.rmSync(file)
935
+ await buildStart()
936
+ expect(context.error).toHaveBeenCalledTimes(1)
937
+ expect(lastError()).toContain(`could not reach ${ic.host}`)
938
+ const refused = asked.length
939
+ expect(refused).toBeGreaterThan(0)
940
+
941
+ // The text that passed is restored with the network still down: checked
942
+ // again, and still not made.
943
+ fs.writeFileSync(file, WRITTEN)
944
+ await buildStart()
945
+ expect(asked.length).toBeGreaterThan(refused)
946
+ expect(context.error).toHaveBeenCalledTimes(2)
947
+ expect(lastError()).toContain(`could not reach ${ic.host}`)
948
+ }, 30_000)
949
+
950
+ // Two builds of one plugin object (a programmatic build() called twice, or
951
+ // a framework's server build then client build) are two runs.
952
+ it("that passed in one build runs again in the next build of the same plugin object", async () => {
953
+ const canisters: Record<string, Canister> = {
954
+ [LEDGER_ID]: { candid: WRITTEN },
955
+ }
956
+ const ic = await fakeIc(canisters)
957
+ const app = newApp({ "did/item.did": WRITTEN })
958
+ const { buildStart, newRun, context } = verifyingPlugin(app, ic.host)
959
+
960
+ await buildStart()
961
+ expect(context.error).not.toHaveBeenCalled()
962
+ expect(ic.requests).toHaveLength(2)
963
+
964
+ // The canister loses put, the .did is unchanged, and the next build
965
+ // resolves its config.
966
+ canisters[LEDGER_ID] = { candid: liveWith([" put : (nat) -> ();\n", ""]) }
967
+ newRun()
968
+ await buildStart()
969
+ expect(ic.requests).toHaveLength(4)
970
+ expect(context.error).toHaveBeenCalledTimes(1)
971
+ expect(String(context.error.mock.lastCall?.[0])).toContain(
972
+ `incompatible with did/item.did (1 error)`
973
+ )
974
+ })
975
+
976
+ // Vite's restart resolves the new config, and runs the new server's
977
+ // buildStart, before it closes the old server: a check of the old run that
978
+ // ends after that must not pass for the new run.
979
+ it("of a run that ends after a restart started another passes nothing for the new run", async () => {
980
+ const canisters: Record<string, Canister> = {
981
+ [LEDGER_ID]: { candid: WRITTEN },
982
+ }
983
+ const ic = await fakeIc(canisters)
984
+ const app = newApp({ "did/item.did": WRITTEN })
985
+ const { buildStart, newRun, context } = verifyingPlugin(app, ic.host)
986
+ const { reached, release } = holdFirstReadState()
987
+
988
+ const before = buildStart()
989
+ // The old run read the canister as it was, and its check is under way.
990
+ await reached
991
+ canisters[LEDGER_ID] = { candid: liveWith([" put : (nat) -> ();\n", ""]) }
992
+ newRun()
993
+ const after = buildStart()
994
+ release()
995
+ await before
996
+ await after
997
+
998
+ // The old run's check passed; the new run's checked again, and failed.
999
+ expect(ic.requests).toHaveLength(4)
1000
+ expect(context.error).toHaveBeenCalledTimes(1)
1001
+ expect(String(context.error.mock.lastCall?.[0])).toContain(
1002
+ `incompatible with did/item.did (1 error)`
1003
+ )
1004
+ })
1005
+
1006
+ // meant reads the file itself, after the fetch: a save while the
1007
+ // fetch is under way is what it checks, not the text the run read. The text
1008
+ // read first, which no check saw, must not pass when it is restored.
1009
+ it("records no pass for a didFile saved while its check ran, so restoring the text read first is checked", async () => {
1010
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: WITHOUT_PUT } })
1011
+ // Incompatible: the file has put, which the canister lacks.
1012
+ const app = newApp({ "did/item.did": WRITTEN })
1013
+ const { buildStart, context } = verifyingPlugin(app, ic.host)
1014
+ const file = path.join(app.root, "did/item.did")
1015
+ const lastError = () => String(context.error.mock.lastCall?.[0])
1016
+ const { reached, release } = holdFirstReadState()
1017
+
1018
+ const run = buildStart()
1019
+ await reached
1020
+ // Saved to the canister's interface while the fetch is held.
1021
+ fs.writeFileSync(file, WITHOUT_PUT)
1022
+ release()
1023
+ await run
1024
+ // The saved text was checked, and passed; but the build had read the
1025
+ // file before, so it takes nothing of it.
1026
+ expect(context.error).toHaveBeenCalledTimes(1)
1027
+ expect(lastError()).toContain("did/item.did changed during the build")
1028
+ expect(ic.requests).toHaveLength(2)
1029
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1030
+
1031
+ // The text read first is restored: checked, and it fails.
1032
+ fs.writeFileSync(file, WRITTEN)
1033
+ await buildStart()
1034
+ expect(ic.requests).toHaveLength(4)
1035
+ expect(context.error).toHaveBeenCalledTimes(2)
1036
+ expect(lastError()).toContain(`incompatible with did/item.did (1 error)`)
1037
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1038
+ })
1039
+
1040
+ it("warns on a compatible change, and generates", async () => {
1041
+ const ic = await fakeIc({
1042
+ // A field added to a result and a method added: compatible.
1043
+ [LEDGER_ID]: {
1044
+ candid: liveWith(
1045
+ ["record { a : nat }", "record { a : nat; b : opt text }"],
1046
+ ["put : (nat) -> ();", "put : (nat) -> ();\n added : () -> ();"]
1047
+ ),
1048
+ },
1049
+ // An opt whose content changed: compatible, but read as null.
1050
+ [OTHER_ID]: { candid: liveWith(["(opt nat)", "(opt text)"]) },
1051
+ })
1052
+ const app = newApp({ "did/item.did": WRITTEN, "did/opt.did": WRITTEN })
1053
+ const { result, lines } = runBuild(app.root, {
1054
+ canisters: {
1055
+ item: checked(ic.host).canisters!.item,
1056
+ opt: {
1057
+ didFile: "did/opt.did",
1058
+ canisterId: OTHER_ID,
1059
+ network: { host: ic.host },
1060
+ verify: "live",
1061
+ },
1062
+ },
1063
+ })
1064
+ await result
1065
+ expect(lines).toContainEqual(
1066
+ `warn: ic-reactor: item: ${LEDGER_ID} on ${ic.host} changed compatibly for the methods of did/item.did: ` +
1067
+ `its module still works. To take the change, fetch did/item.did again with IC_REACTOR_FETCH=item.`
1068
+ )
1069
+ const opt = lines.find((line) => line.startsWith("warn: ic-reactor: opt:"))
1070
+ expect(opt).toContain(
1071
+ `${OTHER_ID} on ${ic.host} is compatible with did/opt.did, with 1 warning`
1072
+ )
1073
+ expect(opt).toContain("maybe: special_opt_rule at $results[0]: ")
1074
+ expect(exists(app, "src/canisters/item.ts")).toBe(true)
1075
+ expect(exists(app, "src/canisters/opt.ts")).toBe(true)
1076
+ })
1077
+
1078
+ it("fails the build on a method that is missing, changed mode or became incompatible, naming the canister and each method", async () => {
1079
+ const ic = await fakeIc({
1080
+ [LEDGER_ID]: {
1081
+ candid: liveWith(
1082
+ ["record { a : nat }", "record { a : text }"],
1083
+ [" put : (nat) -> ();\n", ""],
1084
+ ["(opt nat) query", "(opt nat)"]
1085
+ ),
1086
+ },
1087
+ })
1088
+ const app = newApp({ "did/item.did": WRITTEN })
1089
+ const { message } = await buildError(app, checked(ic.host))
1090
+ expect(message).toContain("could not generate 1 of 1 canisters")
1091
+ expect(message).toContain(
1092
+ `- item (did/item.did): the live check found ${LEDGER_ID} on ${ic.host} incompatible with did/item.did (3 errors):`
1093
+ )
1094
+ expect(message).toContain("get: method_incompatible at $results[0].a: ")
1095
+ expect(message).toContain("maybe: mode_changed: ")
1096
+ expect(message).toContain(
1097
+ 'put: method_missing: the live service has no method "put"'
1098
+ )
1099
+ expect(message).toContain(
1100
+ "fetch did/item.did again with IC_REACTOR_FETCH=item, regenerate, and fix the calls the type checker then reports."
1101
+ )
1102
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1103
+ })
1104
+
1105
+ it("follows failOnError: logged, and the other canisters generated", async () => {
1106
+ const ic = await fakeIc({
1107
+ [LEDGER_ID]: { candid: liveWith([" put : (nat) -> ();\n", ""]) },
1108
+ })
1109
+ const app = newApp({ "did/item.did": WRITTEN, "did/ping.did": PING_DID })
1110
+ const { result, lines } = runBuild(app.root, {
1111
+ failOnError: false,
1112
+ canisters: {
1113
+ ...checked(ic.host).canisters,
1114
+ ping: { didFile: "did/ping.did" },
1115
+ },
1116
+ })
1117
+ await result
1118
+ const errors = lines.filter((line) => line.startsWith("error:"))
1119
+ expect(errors).toHaveLength(1)
1120
+ expect(errors[0]).toContain("could not generate 1 of 2 canisters")
1121
+ expect(errors[0]).toContain(
1122
+ `item (did/item.did): the live check found ${LEDGER_ID} on ${ic.host} incompatible with did/item.did (1 error)`
1123
+ )
1124
+ expect(exists(app, "src/canisters/ping.ts")).toBe(true)
1125
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1126
+ })
1127
+
1128
+ it("runs for IC_REACTOR_VERIFY, and without it or verify nothing is fetched", async () => {
1129
+ const ic = await fakeIc({
1130
+ [LEDGER_ID]: { candid: liveWith([" put : (nat) -> ();\n", ""]) },
1131
+ })
1132
+ const app = newApp({ "did/item.did": WRITTEN, "did/ping.did": PING_DID })
1133
+ const options: IcReactorPluginOptions = {
1134
+ canisters: {
1135
+ item: {
1136
+ didFile: "did/item.did",
1137
+ canisterId: LEDGER_ID,
1138
+ network: { host: ic.host },
1139
+ },
1140
+ // No canisterId: nothing to check against, and `1` passes it over.
1141
+ ping: { didFile: "did/ping.did" },
1142
+ },
1143
+ }
1144
+
1145
+ await runBuild(app.root, options).result
1146
+ expect(ic.requests).toEqual([])
1147
+
1148
+ vi.stubEnv("IC_REACTOR_VERIFY", "1")
1149
+ const { message } = await buildError(app, options)
1150
+ expect(message).toContain("could not generate 1 of 2 canisters")
1151
+ expect(lineOf(message, "item")).toContain(
1152
+ `item (did/item.did): the live check found ${LEDGER_ID} on ${ic.host} incompatible with did/item.did (1 error)`
1153
+ )
1154
+ expect(ic.requests).toHaveLength(2)
1155
+
1156
+ // By name, as IC_REACTOR_FETCH: a canister with no canisterId fails.
1157
+ vi.stubEnv("IC_REACTOR_VERIFY", "ping")
1158
+ const named = await buildError(app, options)
1159
+ expect(lineOf(named.message, "ping")).toContain(
1160
+ "ping (did/ping.did): IC_REACTOR_VERIFY asks for its live check, but it names no canisterId"
1161
+ )
1162
+ })
1163
+
1164
+ it("is off for IC_REACTOR_VERIFY=0, over verify: live, with no request", async () => {
1165
+ // Incompatible, so a check that ran would fail the build.
1166
+ const ic = await fakeIc({
1167
+ [LEDGER_ID]: { candid: liveWith([" put : (nat) -> ();\n", ""]) },
1168
+ })
1169
+ const app = newApp({ "did/item.did": WRITTEN })
1170
+
1171
+ for (const value of ["0", "false"]) {
1172
+ vi.stubEnv("IC_REACTOR_VERIFY", value)
1173
+ const { result, lines } = runBuild(app.root, checked(ic.host))
1174
+ await result
1175
+ expect(lines.filter((line) => !line.startsWith("info:"))).toEqual([])
1176
+ expect(exists(app, "src/canisters/item.ts")).toBe(true)
1177
+ }
1178
+ expect(ic.requests).toEqual([])
1179
+
1180
+ // A name next to 0 is not checked either, and the run says so.
1181
+ vi.stubEnv("IC_REACTOR_VERIFY", "0,item")
1182
+ const { result, lines } = runBuild(app.root, checked(ic.host))
1183
+ await result
1184
+ expect(lines).toContain(
1185
+ 'warn: ic-reactor: IC_REACTOR_VERIFY is "0,item", and 0 turns the live check off: no canister is checked'
1186
+ )
1187
+ expect(ic.requests).toEqual([])
1188
+ })
1189
+
1190
+ // A save makes no request, so it cannot tell that the file matches the
1191
+ // canister again: whatever it changed, the failure stands, in the log and
1192
+ // the overlay, until a check passes when the server starts again.
1193
+ it("that failed under vite dev stands when the .did is saved, changed or not, until a check passes at the next start", async () => {
1194
+ const canisters: Record<string, Canister> = {
1195
+ [LEDGER_ID]: { candid: liveWith([" put : (nat) -> ();\n", ""]) },
1196
+ }
1197
+ const ic = await fakeIc(canisters)
1198
+ const app = newApp({ "did/item.did": WRITTEN })
1199
+ const { connections, plugin } = spyOnConnections()
1200
+ const running = await startDev(app.root, checked(ic.host), {
1201
+ before: [plugin],
1202
+ })
1203
+ let closed = false
1204
+ const close = async () => {
1205
+ if (closed) return
1206
+ closed = true
1207
+ await running.close()
1208
+ }
1209
+ closers.push(close)
1210
+ const failure =
1211
+ `item (did/item.did): the live check found ${LEDGER_ID} on ${ic.host} ` +
1212
+ `incompatible with did/item.did (1 error)`
1213
+ const errors = () =>
1214
+ running.lines.filter((line) => line.startsWith("error:"))
1215
+ expect(errors()).toHaveLength(1)
1216
+ expect(errors()[0]).toContain(failure)
1217
+ expect(ic.requests).toHaveLength(2)
1218
+ const send = vi.spyOn(running.server.ws, "send")
1219
+
1220
+ // Saved as it is (a touch), then with an edit that changes no method.
1221
+ const file = path.join(app.root, "did/item.did")
1222
+ for (const text of [WRITTEN, `// Edited.\n${WRITTEN}`]) {
1223
+ const before = errors().length
1224
+ fs.writeFileSync(file, text)
1225
+ running.server.watcher.emit("change", file)
1226
+ await vi.waitFor(() => expect(errors()).toHaveLength(before + 1))
1227
+ expect(errors()[before]).toContain(failure)
1228
+ expect(errors()[before]).toContain(
1229
+ "did/item.did was saved, and a save makes no request: this stands until a live check passes, which runs when the dev server restarts."
1230
+ )
1231
+ expect(send).not.toHaveBeenCalledWith({ type: "full-reload" })
1232
+ expect(
1233
+ running.lines.filter((line) => line.includes("changed, regenerating"))
1234
+ ).toEqual([])
1235
+ // A browser that connects now is handed it.
1236
+ const client = { send: vi.fn() }
1237
+ connections[0](client)
1238
+ expect(client.send).toHaveBeenCalledTimes(1)
1239
+ expect(
1240
+ JSON.parse(client.send.mock.calls[0][0] as string).err.message
1241
+ ).toContain(failure)
1242
+ }
1243
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1244
+ expect(ic.requests).toHaveLength(2)
1245
+
1246
+ // The canister is put back: the next start checks again, and it passes.
1247
+ canisters[LEDGER_ID] = { candid: WRITTEN }
1248
+ await close()
1249
+ const next = await startDev(app.root, checked(ic.host))
1250
+ closers.push(next.close)
1251
+ expect(next.lines.filter((line) => line.startsWith("error:"))).toEqual([])
1252
+ expect(next.lines).toContainEqual(
1253
+ `info: ic-reactor: item: did/item.did matches the live interface of ${LEDGER_ID} on ${ic.host}`
1254
+ )
1255
+ expect(exists(app, "src/canisters/item.ts")).toBe(true)
1256
+ expect(ic.requests).toHaveLength(4)
1257
+ })
1258
+
1259
+ // `r` and Enter restarts the server with the same plugin object, so the
1260
+ // failure it holds must be cleared by the check that passes, or the next
1261
+ // save would still stand on it.
1262
+ it("that failed under vite dev is cleared by a check that passes when the same server restarts, and a save then regenerates", async () => {
1263
+ const canisters: Record<string, Canister> = {
1264
+ [LEDGER_ID]: { candid: liveWith([" put : (nat) -> ();\n", ""]) },
1265
+ }
1266
+ const ic = await fakeIc(canisters)
1267
+ const app = newApp({ "did/item.did": WRITTEN })
1268
+ const running = await startDev(app.root, checked(ic.host))
1269
+ closers.push(running.close)
1270
+ const errors = () =>
1271
+ running.lines.filter((line) => line.startsWith("error:"))
1272
+ expect(errors()).toHaveLength(1)
1273
+ expect(errors()[0]).toContain(
1274
+ `item (did/item.did): the live check found ${LEDGER_ID} on ${ic.host} incompatible with did/item.did (1 error)`
1275
+ )
1276
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1277
+
1278
+ // The canister is put back, and the same server restarts.
1279
+ canisters[LEDGER_ID] = { candid: WRITTEN }
1280
+ await running.server.restart()
1281
+ expect(running.lines).toContainEqual(
1282
+ `info: ic-reactor: item: did/item.did matches the live interface of ${LEDGER_ID} on ${ic.host}`
1283
+ )
1284
+ expect(exists(app, "src/canisters/item.ts")).toBe(true)
1285
+ expect(ic.requests).toHaveLength(4)
1286
+
1287
+ // A save now regenerates, with no request and no new error.
1288
+ fs.rmSync(path.join(app.root, "src/canisters/item.ts"))
1289
+ const generatedLine =
1290
+ "info: ic-reactor: generated item into src/canisters/item.ts"
1291
+ const before = running.lines.filter((line) => line === generatedLine).length
1292
+ const file = path.join(app.root, "did/item.did")
1293
+ fs.writeFileSync(file, `// Edited.\n${WRITTEN}`)
1294
+ running.server.watcher.emit("change", file)
1295
+ await vi.waitFor(() =>
1296
+ expect(
1297
+ running.lines.filter((line) => line === generatedLine)
1298
+ ).toHaveLength(before + 1)
1299
+ )
1300
+ expect(running.lines).toContain(
1301
+ "info: ic-reactor: did/item.did changed, regenerating item"
1302
+ )
1303
+ expect(exists(app, "src/canisters/item.ts")).toBe(true)
1304
+ expect(errors()).toHaveLength(1)
1305
+ expect(ic.requests).toHaveLength(4)
1306
+ })
1307
+
1308
+ // The restart keeps the plugin object, and with it what passed: a pass of
1309
+ // the server before must not stand for the canister now.
1310
+ it("that passed under vite dev runs again when the same server restarts, and finds a canister that changed since", async () => {
1311
+ const canisters: Record<string, Canister> = {
1312
+ [LEDGER_ID]: { candid: WRITTEN },
1313
+ }
1314
+ const ic = await fakeIc(canisters)
1315
+ const app = newApp({ "did/item.did": WRITTEN })
1316
+ const running = await startDev(app.root, checked(ic.host))
1317
+ closers.push(running.close)
1318
+ const errors = () =>
1319
+ running.lines.filter((line) => line.startsWith("error:"))
1320
+ expect(errors()).toEqual([])
1321
+ expect(running.lines).toContainEqual(
1322
+ `info: ic-reactor: item: did/item.did matches the live interface of ${LEDGER_ID} on ${ic.host}`
1323
+ )
1324
+ // One check for the server, whatever the number of its environments.
1325
+ expect(ic.requests).toHaveLength(2)
1326
+
1327
+ // The canister loses put, the .did is unchanged, and the server restarts.
1328
+ canisters[LEDGER_ID] = { candid: liveWith([" put : (nat) -> ();\n", ""]) }
1329
+ await running.server.restart()
1330
+ expect(ic.requests).toHaveLength(4)
1331
+ expect(errors()).toHaveLength(1)
1332
+ expect(errors()[0]).toContain(
1333
+ `item (did/item.did): the live check found ${LEDGER_ID} on ${ic.host} incompatible with did/item.did (1 error)`
1334
+ )
1335
+ })
1336
+
1337
+ // Only a check that ran and passed clears a failure: a restart that fails
1338
+ // the canister before its check (here, its file lacks a name of its
1339
+ // `methods`) leaves it standing, so restoring that file does not serve
1340
+ // bindings known not to match the canister.
1341
+ it("that failed under vite dev stands through a restart that fails the canister before its check", async () => {
1342
+ const ic = await fakeIc({
1343
+ [LEDGER_ID]: { candid: liveWith([" put : (nat) -> ();\n", ""]) },
1344
+ })
1345
+ const app = newApp({ "did/item.did": WRITTEN })
1346
+ const options = checked(ic.host)
1347
+ options.canisters!.item = {
1348
+ ...options.canisters!.item,
1349
+ methods: ["get", "put", "maybe"],
1350
+ }
1351
+ const running = await startDev(app.root, options)
1352
+ closers.push(running.close)
1353
+ const failure =
1354
+ `item (did/item.did): the live check found ${LEDGER_ID} on ${ic.host} ` +
1355
+ `incompatible with did/item.did (1 error)`
1356
+ const errors = () =>
1357
+ running.lines.filter((line) => line.startsWith("error:"))
1358
+ expect(errors()).toHaveLength(1)
1359
+ expect(errors()[0]).toContain(failure)
1360
+ expect(ic.requests).toHaveLength(2)
1361
+
1362
+ // The file loses put, and the same server restarts: the methods
1363
+ // comparison fails the canister, so its live check does not run.
1364
+ const file = path.join(app.root, "did/item.did")
1365
+ fs.writeFileSync(file, liveWith([" put : (nat) -> ();\n", ""]))
1366
+ await running.server.restart()
1367
+ expect(errors()).toHaveLength(2)
1368
+ expect(errors()[1]).not.toContain(failure)
1369
+ expect(errors()[1]).toContain(
1370
+ "item (did/item.did): methods names put, which did/item.did does not hold (it holds get, maybe)"
1371
+ )
1372
+ expect(ic.requests).toHaveLength(2)
1373
+
1374
+ // put is restored and saved: the live failure still stands.
1375
+ fs.writeFileSync(file, WRITTEN)
1376
+ running.server.watcher.emit("change", file)
1377
+ await vi.waitFor(() => expect(errors()).toHaveLength(3))
1378
+ expect(errors()[2]).toContain(failure)
1379
+ expect(
1380
+ running.lines.filter((line) => line.includes("changed, regenerating"))
1381
+ ).toEqual([])
1382
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1383
+ expect(ic.requests).toHaveLength(2)
1384
+ })
1385
+
1386
+ it("that cannot read the canister fails it, and says how to turn the check off", async () => {
1387
+ const ic = await fakeIc({ [LEDGER_ID]: { private: true } })
1388
+ const app = newApp({ "did/item.did": WRITTEN })
1389
+ const { message } = await buildError(app, checked(ic.host))
1390
+ expect(lineOf(message, "item")).toContain(
1391
+ `item (did/item.did): ${ic.host} refused to read the candid:service of ${LEDGER_ID} (HTTP 403`
1392
+ )
1393
+ expect(lineOf(message, "item")).toContain(
1394
+ 'the live check cannot read it, so build with IC_REACTOR_VERIFY=0, and stop asking for the check of did/item.did (remove its verify: "live", or its name from IC_REACTOR_VERIFY).'
1395
+ )
1396
+ expect(read(app, "did/item.did")).toBe(WRITTEN)
1397
+ })
1398
+ })
1399
+
1400
+ // A run reads each didFile once, when its steps start, and meant
1401
+ // reads it again for each step (the methods listing, the live check,
1402
+ // generation). A file written in between must not pass one step for a text
1403
+ // another step read: the run takes nothing of it. A build fails it, and a dev
1404
+ // server queues it again, as a save does. Each save here is made by the
1405
+ // CLI's own process (`hookCli`), at the moment the test names.
1406
+ describe("a didFile written while a run reads it", () => {
1407
+ // A file on disk that is only compared with its methods: no request.
1408
+ const listed = (): IcReactorPluginOptions => ({
1409
+ canisters: {
1410
+ item: {
1411
+ didFile: "did/item.did",
1412
+ canisterId: LEDGER_ID,
1413
+ methods: ["get", "maybe", "put"],
1414
+ },
1415
+ },
1416
+ })
1417
+ const changed =
1418
+ "item (did/item.did): did/item.did changed during the build, after the build read it"
1419
+ const lacksPut =
1420
+ "item (did/item.did): methods names put, which did/item.did does not hold (it holds get, maybe)"
1421
+
1422
+ // The check passes the text it read; the text saved after it must not be
1423
+ // generated as if it had been checked.
1424
+ it("fails the build when saved after the live check read it, and generates nothing of the saved text", async () => {
1425
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: WITHOUT_PUT } })
1426
+ // Compatible: the canister's own interface.
1427
+ const app = newApp({ "did/item.did": WITHOUT_PUT })
1428
+ const cli = hookCli(app)
1429
+ const { buildStart, context } = verifyingPlugin(app, ic.host)
1430
+ const file = path.join(app.root, "did/item.did")
1431
+ const lastError = () => String(context.error.mock.lastCall?.[0])
1432
+
1433
+ // put, which the canister lacks, is saved once check has read the file.
1434
+ cli.save("check", "after", file, WRITTEN)
1435
+ await buildStart()
1436
+ expect(context.error).toHaveBeenCalledTimes(1)
1437
+ expect(lastError()).toContain(changed)
1438
+ expect(cli.commands()).not.toContain("gen")
1439
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1440
+ expect(ic.requests).toHaveLength(2)
1441
+
1442
+ // The next build checks the text saved, and it fails.
1443
+ await buildStart()
1444
+ expect(ic.requests).toHaveLength(4)
1445
+ expect(context.error).toHaveBeenCalledTimes(2)
1446
+ expect(lastError()).toContain("incompatible with did/item.did (1 error)")
1447
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1448
+ })
1449
+
1450
+ // The stamp, and not the text alone, tells a file saved and saved back:
1451
+ // check reads a text the build never read, and once check is done the
1452
+ // file holds the build's text again.
1453
+ it("fails the build when saved and saved back while the live check read it, and records no pass", async () => {
1454
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: WITHOUT_PUT } })
1455
+ // Incompatible: the file has put, which the canister lacks.
1456
+ const app = newApp({ "did/item.did": WRITTEN })
1457
+ const cli = hookCli(app)
1458
+ const { buildStart, context, lines } = verifyingPlugin(app, ic.host)
1459
+ const file = path.join(app.root, "did/item.did")
1460
+ const lastError = () => String(context.error.mock.lastCall?.[0])
1461
+
1462
+ // put is taken out just before check reads the file, and saved back once
1463
+ // check is done: check passes the text without it.
1464
+ cli.save("check", "before", file, WITHOUT_PUT, { back: WRITTEN })
1465
+ await buildStart()
1466
+ expect(read(app, "did/item.did")).toBe(WRITTEN)
1467
+ expect(context.error).toHaveBeenCalledTimes(1)
1468
+ expect(lastError()).toContain(changed)
1469
+ expect(cli.commands()).not.toContain("gen")
1470
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1471
+ // What that check found is not logged either.
1472
+ expect(lines.filter((line) => line.includes("live interface"))).toEqual([])
1473
+ expect(ic.requests).toHaveLength(2)
1474
+
1475
+ // The next build checks the file, and it fails.
1476
+ await buildStart()
1477
+ expect(ic.requests).toHaveLength(4)
1478
+ expect(context.error).toHaveBeenCalledTimes(2)
1479
+ expect(lastError()).toContain("incompatible with did/item.did (1 error)")
1480
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1481
+ })
1482
+
1483
+ // The listing approves the text it read; the text saved after it, which
1484
+ // lacks a method the list names, must not be generated as if listed.
1485
+ it("fails the build when saved after its methods were listed, and generates nothing of the saved text", async () => {
1486
+ const asked = refuseNetwork()
1487
+ const app = newApp({ "did/item.did": WRITTEN })
1488
+ const cli = hookCli(app)
1489
+
1490
+ // Saved without put, which methods names, once the file was listed.
1491
+ cli.save("list", "after", path.join(app.root, "did/item.did"), WITHOUT_PUT)
1492
+ const { message } = await buildError(app, listed())
1493
+ expect(lineOf(message, "item")).toContain(changed)
1494
+ expect(cli.commands()).toEqual(["project"])
1495
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1496
+
1497
+ // The next build lists the text saved, which lacks put.
1498
+ const again = await buildError(app, listed())
1499
+ expect(lineOf(again.message, "item")).toContain(lacksPut)
1500
+ expect(asked).toEqual([])
1501
+ })
1502
+
1503
+ // Saved and saved back around the listing, the file lacked put only for
1504
+ // the CLI: the build fails it as changed, for that reason alone, and does
1505
+ // not report what the CLI listed of the other text.
1506
+ it("fails the build when saved and saved back while its methods were listed, for that reason alone, and lists the file again", async () => {
1507
+ const asked = refuseNetwork()
1508
+ // The file holds every method of methods.
1509
+ const app = newApp({ "did/item.did": WRITTEN })
1510
+ const cli = hookCli(app)
1511
+ const { buildStart, context } = drivenPlugin(app, listed())
1512
+ const file = path.join(app.root, "did/item.did")
1513
+ const lastError = () => String(context.error.mock.lastCall?.[0])
1514
+
1515
+ // put is taken out just before the CLI lists the file, and saved back
1516
+ // once it is done.
1517
+ cli.save("list", "before", file, WITHOUT_PUT, { back: WRITTEN })
1518
+ await buildStart()
1519
+ expect(read(app, "did/item.did")).toBe(WRITTEN)
1520
+ expect(context.error).toHaveBeenCalledTimes(1)
1521
+ expect(lastError()).toContain("could not generate 1 of 1 canisters")
1522
+ expect(lineOf(lastError(), "item")).toContain(changed)
1523
+ expect(lastError()).not.toContain("methods names put")
1524
+ expect(cli.commands()).toEqual(["project"])
1525
+
1526
+ // The next build lists the file again: it holds put, and generates.
1527
+ await buildStart()
1528
+ expect(context.error).toHaveBeenCalledTimes(1)
1529
+ expect(cli.commands()).toEqual(["project", "project", "gen"])
1530
+ expect(read(app, "src/canisters/item.ts")).toMatch(/\bput\b/)
1531
+ expect(asked).toEqual([])
1532
+ })
1533
+
1534
+ // A file found written before generation is not handed to the generator,
1535
+ // which would write its module (a hot update, under vite dev) only for it
1536
+ // to be put back.
1537
+ it("fails the build when saved while another canister's methods were listed, and runs no generator on it", async () => {
1538
+ const asked = refuseNetwork()
1539
+ const app = newApp({ "did/item.did": WRITTEN, "did/ping.did": PING_DID })
1540
+ const cli = hookCli(app)
1541
+ const { buildStart, context } = drivenPlugin(app, {
1542
+ canisters: {
1543
+ ...listed().canisters,
1544
+ ping: { didFile: "did/ping.did" },
1545
+ },
1546
+ })
1547
+
1548
+ // ping's file is saved once item's methods were listed.
1549
+ const ping = path.join(app.root, "did/ping.did")
1550
+ cli.save("list", "after", ping, `// Saved.\n${PING_DID}`)
1551
+ await buildStart()
1552
+ expect(context.error).toHaveBeenCalledTimes(1)
1553
+ const message = String(context.error.mock.lastCall?.[0])
1554
+ expect(message).toContain("could not generate 1 of 2 canisters")
1555
+ expect(lineOf(message, "ping")).toContain(
1556
+ "ping (did/ping.did): did/ping.did changed during the build, after the build read it"
1557
+ )
1558
+ // One generator, for item alone.
1559
+ const generators = app.runs().filter(([command]) => command === "gen")
1560
+ expect(generators).toHaveLength(1)
1561
+ expect(generators[0]).toContain(path.join(app.root, "did/item.did"))
1562
+ expect(generators[0]).not.toContain(ping)
1563
+ expect(exists(app, "src/canisters/item.ts")).toBe(true)
1564
+ expect(exists(app, "src/canisters/ping.ts")).toBe(false)
1565
+ expect(asked).toEqual([])
1566
+ })
1567
+
1568
+ // The listing read the file after the run did: what it found is not the
1569
+ // run's text, and must not be recorded for it.
1570
+ it("records no listing for the text read first when saved before its methods were listed, so restoring that text is listed again", async () => {
1571
+ const asked = refuseNetwork()
1572
+ // put, which methods names, is not in the file: the canister fails.
1573
+ const app = newApp({ "did/item.did": WITHOUT_PUT })
1574
+ const cli = hookCli(app)
1575
+ const { buildStart, context } = drivenPlugin(app, listed())
1576
+ const file = path.join(app.root, "did/item.did")
1577
+ const lastError = () => String(context.error.mock.lastCall?.[0])
1578
+
1579
+ // Saved with put once the run read the file, before the CLI lists it.
1580
+ cli.save("list", "before", file, WRITTEN)
1581
+ await buildStart()
1582
+ expect(context.error).toHaveBeenCalledTimes(1)
1583
+ expect(lastError()).toContain(changed)
1584
+
1585
+ // The text read first is restored: listed again, it lacks put.
1586
+ fs.writeFileSync(file, WITHOUT_PUT)
1587
+ await buildStart()
1588
+ expect(context.error).toHaveBeenCalledTimes(2)
1589
+ expect(lastError()).toContain(lacksPut)
1590
+ expect(cli.commands()).toEqual(["project", "project"])
1591
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1592
+ expect(asked).toEqual([])
1593
+ })
1594
+
1595
+ // The generator read the file after the run did: the module it wrote may
1596
+ // be of a text the run never read.
1597
+ it("fails the build when saved as the generator starts, and puts the module back as it was", async () => {
1598
+ const app = newApp({ "did/item.did": WRITTEN })
1599
+ const cli = hookCli(app)
1600
+ const { buildStart, context } = drivenPlugin(app, {
1601
+ canisters: { item: { didFile: "did/item.did" } },
1602
+ })
1603
+ const file = path.join(app.root, "did/item.did")
1604
+ const module = () => read(app, "src/canisters/item.ts")
1605
+
1606
+ await buildStart()
1607
+ expect(context.error).not.toHaveBeenCalled()
1608
+ const first = module()
1609
+ expect(first).toMatch(/\bmaybe\b/)
1610
+
1611
+ // An edit without maybe is read by the run; once it is, a text without
1612
+ // put is saved, just before the generator reads the file.
1613
+ const withoutMaybe = liveWith([" maybe : () -> (opt nat) query;\n", ""])
1614
+ fs.writeFileSync(file, withoutMaybe)
1615
+ cli.save("gen", "before", file, WITHOUT_PUT)
1616
+ await buildStart()
1617
+ expect(context.error).toHaveBeenCalledTimes(1)
1618
+ expect(String(context.error.mock.lastCall?.[0])).toContain(changed)
1619
+ expect(cli.commands()).toEqual(["gen", "gen"])
1620
+ expect(module()).toBe(first)
1621
+
1622
+ // The edit is saved again: generated now, since the run before took
1623
+ // nothing of it.
1624
+ fs.writeFileSync(file, withoutMaybe)
1625
+ await buildStart()
1626
+ expect(context.error).toHaveBeenCalledTimes(1)
1627
+ expect(module()).not.toMatch(/\bmaybe\b/)
1628
+ expect(module()).toMatch(/\bput\b/)
1629
+ })
1630
+
1631
+ // The run put back files a rule of its own named, where the generator
1632
+ // names its output after the file's name with a final `.did` cut off, and
1633
+ // nothing else (`outputsOf`). For a file named `.did` the generator writes
1634
+ // `.ts`, and the rule named `.did.ts`: the module of the text saved stayed.
1635
+ // The generator reads no file whose name does not end in `.did`
1636
+ // (`api.candid`, `api.DID`), and writes nothing for it: what was there,
1637
+ // under its name or under a name with the whole extension cut, stays.
1638
+ // `meant` 0.5.1 writes nothing for those two, so no fault of the
1639
+ // put-back fails them: they guard against a CLI that reads such a file and
1640
+ // writes under another name than the one the plugin puts back. The test
1641
+ // bites through `.did`.
1642
+ it("fails the build when saved as the generator starts, and puts back the files the generator names, whatever the file's name", async () => {
1643
+ const withoutMaybe = liveWith([" maybe : () -> (opt nat) query;\n", ""])
1644
+ const names = [
1645
+ ["did/.did", ""],
1646
+ ["did/api.candid", "api.candid"],
1647
+ ["did/api.DID", "api.DID"],
1648
+ ] as const
1649
+ for (const [didFile, stem] of names) {
1650
+ const app = newApp({ [didFile]: WRITTEN })
1651
+ const cli = hookCli(app)
1652
+ const { buildStart, context } = drivenPlugin(app, {
1653
+ canisters: { item: { didFile } },
1654
+ })
1655
+ const file = path.join(app.root, didFile)
1656
+ const outputs = [
1657
+ `src/canisters/${stem}.ts`,
1658
+ `src/canisters/${stem}.envelope.json`,
1659
+ ]
1660
+ const lastError = () => String(context.error.mock.lastCall?.[0])
1661
+
1662
+ await buildStart()
1663
+ if (stem === "") {
1664
+ expect(context.error).not.toHaveBeenCalled()
1665
+ expect(read(app, outputs[0])).toMatch(/\bmaybe\b/)
1666
+ } else {
1667
+ // The generator's own refusal, which writes nothing: files of an
1668
+ // earlier run are made here, under its names and under the names
1669
+ // that cut the whole extension.
1670
+ expect(lastError()).toContain(
1671
+ `did_source_not_found: cannot read ${file}: no such .did file`
1672
+ )
1673
+ for (const output of [
1674
+ ...outputs,
1675
+ "src/canisters/api.ts",
1676
+ "src/canisters/api.envelope.json",
1677
+ ]) {
1678
+ fs.mkdirSync(path.join(app.root, "src/canisters"), {
1679
+ recursive: true,
1680
+ })
1681
+ fs.writeFileSync(path.join(app.root, output), `// ${output}\n`)
1682
+ }
1683
+ }
1684
+ const outDir = path.join(app.root, "src/canisters")
1685
+ const before = new Map(
1686
+ fs
1687
+ .readdirSync(outDir)
1688
+ .map((name) => [name, read(app, `src/canisters/${name}`)])
1689
+ )
1690
+
1691
+ // An edit without maybe is read by the run; once it is, a text
1692
+ // without put is saved, just before the generator reads the file.
1693
+ fs.writeFileSync(file, withoutMaybe)
1694
+ cli.save("gen", "before", file, WITHOUT_PUT)
1695
+ await buildStart()
1696
+ expect(lastError()).toContain(
1697
+ `item (${didFile}): ${didFile} changed during the build, after the build read it`
1698
+ )
1699
+ expect(cli.commands()).toEqual(["gen", "gen"])
1700
+ expect(
1701
+ new Map(
1702
+ fs
1703
+ .readdirSync(outDir)
1704
+ .map((name) => [name, read(app, `src/canisters/${name}`)])
1705
+ )
1706
+ ).toEqual(before)
1707
+ }
1708
+ })
1709
+
1710
+ // A save's run lists the file and generates it with no request: saved
1711
+ // again in between, it is listed again, as the save it is, and the save's
1712
+ // own run says nothing of it. Each dev test here has room past its wait,
1713
+ // so that what it waits for fails by the wait's own assertion.
1714
+ it("under vite dev, is queued again as a save is when saved after a save's run listed it, and that run says nothing of it", async () => {
1715
+ const asked = refuseNetwork()
1716
+ // put, which methods names, is not in the file: the start fails it.
1717
+ const app = newApp({ "did/item.did": WITHOUT_PUT })
1718
+ const cli = hookCli(app)
1719
+ const sends = recordSends()
1720
+ const running = await startDev(app.root, listed(), {
1721
+ before: [sends.plugin],
1722
+ })
1723
+ closers.push(running.close)
1724
+ const errors = () =>
1725
+ running.lines.filter((line) => line.startsWith("error:"))
1726
+ expect(errors()).toHaveLength(1)
1727
+ expect(errors()[0]).toContain(lacksPut)
1728
+ const sentAtStart = sends.sent().length
1729
+
1730
+ // An edit that adds put is saved; once the save's run listed it, the
1731
+ // text without put is saved again.
1732
+ const file = path.join(app.root, "did/item.did")
1733
+ cli.save("list", "after", file, WITHOUT_PUT)
1734
+ fs.writeFileSync(file, `// Edited.\n${WRITTEN}`)
1735
+ running.server.watcher.emit("change", file)
1736
+ await vi.waitFor(() => expect(errors()).toHaveLength(2), {
1737
+ timeout: 10_000,
1738
+ })
1739
+ expect(errors()[1]).toContain(lacksPut)
1740
+ // The save's run, which took nothing, and the run it queued.
1741
+ expect(
1742
+ running.lines.filter(
1743
+ (line) =>
1744
+ line === "info: ic-reactor: did/item.did changed, regenerating item"
1745
+ )
1746
+ ).toHaveLength(2)
1747
+ // The save's run neither showed a failure nor reloaded the page as for a
1748
+ // canister fixed: only the queued run's overlay was sent.
1749
+ const sent = sends.sent().slice(sentAtStart)
1750
+ expect(sent).not.toContainEqual({ type: "full-reload" })
1751
+ expect(overlays(sent)).toHaveLength(1)
1752
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1753
+ expect(asked).toEqual([])
1754
+ }, 30_000)
1755
+
1756
+ // The generator of a save's run read the file after the run did, so the
1757
+ // module it wrote may be of a text the run never read: the run puts the
1758
+ // module back, says nothing of the file, and queues it again. It can know
1759
+ // only once the generator is done.
1760
+ it("under vite dev, is queued again as a save is when saved as a save's generator starts, and that run says nothing of it", async () => {
1761
+ const asked = refuseNetwork()
1762
+ // put, which methods names, is not in the file: the start fails it.
1763
+ const app = newApp({ "did/item.did": WITHOUT_PUT })
1764
+ const cli = hookCli(app)
1765
+ const sends = recordSends()
1766
+ const running = await startDev(app.root, listed(), {
1767
+ before: [sends.plugin],
1768
+ })
1769
+ closers.push(running.close)
1770
+ const errors = () =>
1771
+ running.lines.filter((line) => line.startsWith("error:"))
1772
+ expect(errors()).toHaveLength(1)
1773
+ expect(errors()[0]).toContain(lacksPut)
1774
+ const sentAtStart = sends.sent().length
1775
+
1776
+ // An edit that adds put is saved, and the save's run lists it; just
1777
+ // before the run's generator reads the file, the text without put is
1778
+ // saved again.
1779
+ const file = path.join(app.root, "did/item.did")
1780
+ cli.save("gen", "before", file, WITHOUT_PUT)
1781
+ fs.writeFileSync(file, `// Edited.\n${WRITTEN}`)
1782
+ running.server.watcher.emit("change", file)
1783
+ await vi.waitFor(() => expect(errors()).toHaveLength(2), {
1784
+ timeout: 10_000,
1785
+ })
1786
+ expect(errors()[1]).toContain(lacksPut)
1787
+ // The start's listing, the save's listing and generator, and the queued
1788
+ // run's listing of the text saved.
1789
+ expect(cli.commands()).toEqual(["project", "project", "gen", "project"])
1790
+ expect(
1791
+ running.lines.filter(
1792
+ (line) =>
1793
+ line === "info: ic-reactor: did/item.did changed, regenerating item"
1794
+ )
1795
+ ).toHaveLength(2)
1796
+ // The save's run neither showed a failure nor reloaded the page as for a
1797
+ // canister fixed, nor logged a module: only the queued run's overlay was
1798
+ // sent, and the module of the text saved was put back.
1799
+ const sent = sends.sent().slice(sentAtStart)
1800
+ expect(sent).not.toContainEqual({ type: "full-reload" })
1801
+ expect(overlays(sent)).toHaveLength(1)
1802
+ expect(
1803
+ running.lines.filter((line) => line.includes("ic-reactor: generated"))
1804
+ ).toEqual([])
1805
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1806
+ expect(asked).toEqual([])
1807
+ }, 30_000)
1808
+
1809
+ // A restart keeps the plugin object, and the failures it shows: a
1810
+ // restart's run that took nothing of a file says nothing of it, not even
1811
+ // that the canister is fixed, and the run it queues reports it.
1812
+ it("under vite dev, says nothing of a file saved during a restart's listing, and the run it queues reports it", async () => {
1813
+ const asked = refuseNetwork()
1814
+ // put, which methods names, is not in the file: the start fails it.
1815
+ const app = newApp({ "did/item.did": WITHOUT_PUT })
1816
+ const cli = hookCli(app)
1817
+ const sends = recordSends()
1818
+ const running = await startDev(app.root, listed(), {
1819
+ before: [sends.plugin],
1820
+ })
1821
+ closers.push(running.close)
1822
+ const errors = () =>
1823
+ running.lines.filter((line) => line.startsWith("error:"))
1824
+ expect(errors()).toHaveLength(1)
1825
+ expect(errors()[0]).toContain(lacksPut)
1826
+ const sentAtStart = sends.sent().length
1827
+
1828
+ // An edit that adds put is written, and the same server restarts; once
1829
+ // the restart's run listed the edit, the text without put is saved
1830
+ // again. The start listed that text, so the run queued for it needs no
1831
+ // process: the plugin object outlives the restart of an inline config,
1832
+ // and the old server's close would stop one.
1833
+ const file = path.join(app.root, "did/item.did")
1834
+ fs.writeFileSync(file, `// Edited.\n${WRITTEN}`)
1835
+ cli.save("list", "after", file, WITHOUT_PUT)
1836
+ await running.server.restart()
1837
+ await vi.waitFor(() => expect(errors()).toHaveLength(2), {
1838
+ timeout: 10_000,
1839
+ })
1840
+ expect(errors()[1]).toContain(lacksPut)
1841
+ expect(
1842
+ running.lines.filter(
1843
+ (line) =>
1844
+ line === "info: ic-reactor: did/item.did changed, regenerating item"
1845
+ )
1846
+ ).toHaveLength(1)
1847
+ // The restart's run neither showed a failure nor reloaded the page as
1848
+ // for a canister fixed: only the queued run's overlay was sent.
1849
+ const sent = sends.sent().slice(sentAtStart)
1850
+ expect(sent).not.toContainEqual({ type: "full-reload" })
1851
+ expect(overlays(sent)).toHaveLength(1)
1852
+ expect(cli.commands()).toEqual(["project", "project"])
1853
+ expect(asked).toEqual([])
1854
+ }, 30_000)
1855
+
1856
+ // A check that failed what it read, while the file was saved and saved
1857
+ // back, is no failure of the file: it sets no failure that stands, and the
1858
+ // save the start queued generates the file.
1859
+ it("under vite dev, sets no failed check when saved and saved back while the start's check read it, and the queued save generates", async () => {
1860
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: WITHOUT_PUT } })
1861
+ // Compatible: the canister's own interface.
1862
+ const app = newApp({ "did/item.did": WITHOUT_PUT })
1863
+ const cli = hookCli(app)
1864
+ // put, which the canister lacks, is saved just before the start's check
1865
+ // reads the file, and the file is saved back once check is done.
1866
+ const file = path.join(app.root, "did/item.did")
1867
+ cli.save("check", "before", file, WRITTEN, { back: WITHOUT_PUT })
1868
+ const running = await startDev(app.root, checkedLive(ic.host))
1869
+ closers.push(running.close)
1870
+ const generated =
1871
+ "info: ic-reactor: generated item into src/canisters/item.ts"
1872
+ await vi.waitFor(
1873
+ () =>
1874
+ expect(
1875
+ running.lines.some(
1876
+ (line) => line === generated || line.startsWith("error:")
1877
+ )
1878
+ ).toBe(true),
1879
+ { timeout: 10_000 }
1880
+ )
1881
+ expect(running.lines.filter((line) => line.startsWith("error:"))).toEqual(
1882
+ []
1883
+ )
1884
+ expect(running.lines).toContain(
1885
+ "info: ic-reactor: did/item.did changed, regenerating item"
1886
+ )
1887
+ expect(read(app, "src/canisters/item.ts")).not.toMatch(/\bput\b/)
1888
+ // The start's check; a save makes no request.
1889
+ expect(ic.requests).toHaveLength(2)
1890
+ }, 30_000)
1891
+
1892
+ // A check that passed what it read, while the file was saved, is no pass
1893
+ // of the file: a failure that stands is not cleared by it.
1894
+ it("under vite dev, leaves a failed check standing when saved after a restart's check passed the text it read", async () => {
1895
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: WITHOUT_PUT } })
1896
+ // put, which the canister lacks: the check fails.
1897
+ const app = newApp({ "did/item.did": WRITTEN })
1898
+ const cli = hookCli(app)
1899
+ const running = await startDev(app.root, checkedLive(ic.host))
1900
+ closers.push(running.close)
1901
+ const failure =
1902
+ `item (did/item.did): the live check found ${LEDGER_ID} on ${ic.host} ` +
1903
+ `incompatible with did/item.did (1 error)`
1904
+ const errors = () =>
1905
+ running.lines.filter((line) => line.startsWith("error:"))
1906
+ expect(errors()).toHaveLength(1)
1907
+ expect(errors()[0]).toContain(failure)
1908
+
1909
+ // The file loses put, and the same server restarts; once the restart's
1910
+ // check has read the file, put is saved back.
1911
+ const file = path.join(app.root, "did/item.did")
1912
+ fs.writeFileSync(file, WITHOUT_PUT)
1913
+ cli.save("check", "after", file, WRITTEN)
1914
+ await running.server.restart()
1915
+ await vi.waitFor(() => expect(errors()).toHaveLength(2), {
1916
+ timeout: 10_000,
1917
+ })
1918
+ expect(errors()[1]).toContain(failure)
1919
+ expect(errors()[1]).toContain(
1920
+ "did/item.did was saved, and a save makes no request: this stands until a live check passes"
1921
+ )
1922
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
1923
+ expect(ic.requests).toHaveLength(4)
1924
+ }, 30_000)
1925
+ })
1926
+
1927
+ describe("a canister's name", () => {
1928
+ // A JSON config can name a canister after a member of Object.prototype.
1929
+ // Each is read, generated, compared and refused as any other name is.
1930
+ it("is kept as given: canisters named __proto__, constructor, toString or hasOwnProperty are read, generated and reported as any other", async () => {
1931
+ const asked = refuseNetwork()
1932
+ const app = newApp({
1933
+ "did/a.did": PING_DID,
1934
+ "did/b.did": PING_DID,
1935
+ "did/c.did": PING_DID,
1936
+ })
1937
+ const { message, lines } = await buildError(app, {
1938
+ canisters: JSON.parse(
1939
+ `{
1940
+ "__proto__": { "didFile": "did/a.did" },
1941
+ "constructor": { "didFile": "did/b.did", "methods": ["nope"] },
1942
+ "toString": { "didFile": "did/c.did", "verify": "maybe" },
1943
+ "hasOwnProperty": {}
1944
+ }`
1945
+ ),
1946
+ })
1947
+ expect(message).toContain("could not generate 2 of 3 canisters")
1948
+ expect(lineOf(message, "constructor")).toContain(
1949
+ "constructor (did/b.did): methods names nope, which did/b.did does not hold (it holds ping)"
1950
+ )
1951
+ expect(lineOf(message, "toString")).toContain(
1952
+ 'toString (did/c.did): verify is "maybe", not "live"'
1953
+ )
1954
+ expect(lines).toContain(
1955
+ "info: ic-reactor: generated __proto__ into src/canisters/a.ts"
1956
+ )
1957
+ expect(exists(app, "src/canisters/a.ts")).toBe(true)
1958
+ expect(exists(app, "src/canisters/b.ts")).toBe(false)
1959
+ expect(asked).toEqual([])
1960
+ })
1961
+ })
1962
+
1963
+ // A JavaScript config is not type-checked: only an omitted option is one
1964
+ // not given, and every value given is checked.
1965
+ describe("options given a value of the wrong kind", () => {
1966
+ // Only an omitted option takes its default. null, which a JSON config or a
1967
+ // `?? null` gives, is a value given, and none of these options takes it:
1968
+ // read as omitted, it would generate with the default instead (outDir,
1969
+ // network), or take the canister for one only named in the cookie
1970
+ // (didFile).
1971
+ it("null given for any option of a canister fails it, naming the option, and only an omitted option takes its default", async () => {
1972
+ const asked = refuseNetwork()
1973
+ const app = newApp({ "did/a.did": PING_DID, "did/ping.did": PING_DID })
1974
+ const cli = hookCli(app)
1975
+ const { message } = await buildError(app, {
1976
+ canisters: {
1977
+ file: { didFile: null as never },
1978
+ out: { didFile: "did/a.did", outDir: null as never },
1979
+ id: { didFile: "did/a.did", canisterId: null as never },
1980
+ network: { didFile: "did/a.did", network: null as never },
1981
+ methods: { didFile: "did/a.did", methods: null as never },
1982
+ verify: { didFile: "did/a.did", verify: null as never },
1983
+ entry: null as never,
1984
+ omitted: {
1985
+ didFile: "did/ping.did",
1986
+ outDir: undefined,
1987
+ canisterId: undefined,
1988
+ network: undefined,
1989
+ methods: undefined,
1990
+ verify: undefined,
1991
+ },
1992
+ },
1993
+ })
1994
+ expect(message).toContain("could not generate 7 of 8 canisters")
1995
+ expect(message).toContain("- file: didFile is null, not a path")
1996
+ expect(lineOf(message, "out")).toContain(
1997
+ "out (did/a.did): outDir is null, not a path"
1998
+ )
1999
+ expect(lineOf(message, "id")).toContain(
2000
+ "id (did/a.did): canisterId is null, not a canister ID"
2001
+ )
2002
+ expect(lineOf(message, "network")).toContain(
2003
+ 'network (did/a.did): network null is not "ic", "local" or { host: "<http or https URL>" }'
2004
+ )
2005
+ expect(lineOf(message, "methods")).toContain(
2006
+ "methods (did/a.did): methods is null, not a list of method names"
2007
+ )
2008
+ expect(lineOf(message, "verify")).toContain(
2009
+ 'verify (did/a.did): verify is null, not "live"'
2010
+ )
2011
+ expect(message).toContain(
2012
+ "- entry: its options are null, not a plain object of options"
2013
+ )
2014
+ expect(message).not.toContain("- omitted (")
2015
+ // The omitted options' defaults: generated into src/canisters.
2016
+ expect(cli.commands()).toEqual(["gen"])
2017
+ expect(app.runs()[0]).toEqual([
2018
+ "gen",
2019
+ path.join(app.root, "did/ping.did"),
2020
+ "-o",
2021
+ path.join(app.root, "src/canisters"),
2022
+ "--json",
2023
+ ])
2024
+ expect(exists(app, "src/canisters/ping.ts")).toBe(true)
2025
+ expect(exists(app, "src/canisters/a.ts")).toBe(false)
2026
+ expect(asked).toEqual([])
2027
+ })
2028
+
2029
+ it("verify other than live (true, false, null, a typo) fails the canister with no request, under IC_REACTOR_VERIFY=0 too", async () => {
2030
+ const asked = refuseNetwork()
2031
+ const app = newApp({ "did/item.did": WRITTEN })
2032
+ const options = (verify: unknown): IcReactorPluginOptions => ({
2033
+ canisters: {
2034
+ item: {
2035
+ didFile: "did/item.did",
2036
+ canisterId: LEDGER_ID,
2037
+ verify: verify as never,
2038
+ },
2039
+ },
2040
+ })
2041
+ for (const verify of [true, false, null, "Live"]) {
2042
+ const { message } = await buildError(app, options(verify))
2043
+ expect(lineOf(message, "item")).toContain(
2044
+ `item (did/item.did): verify is ${JSON.stringify(verify)}, not "live": set it to "live" for the live check, or remove it for none`
2045
+ )
2046
+ }
2047
+ vi.stubEnv("IC_REACTOR_VERIFY", "0")
2048
+ const { message } = await buildError(app, options(true))
2049
+ expect(lineOf(message, "item")).toContain('verify is true, not "live"')
2050
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
2051
+ expect(asked).toEqual([])
2052
+ })
2053
+
2054
+ it("canisterId given as an empty string or null is no canister ID: it fails the canister with no request, whether or not a fetch or a check would use it", async () => {
2055
+ const asked = refuseNetwork()
2056
+ const app = newApp({ "did/item.did": WRITTEN, "did/ping.did": PING_DID })
2057
+ const { message } = await buildError(app, {
2058
+ canisters: {
2059
+ // Missing, so it would be fetched.
2060
+ fetched: { didFile: "did/fetched.did", canisterId: "" },
2061
+ // On disk, and checked live.
2062
+ checked: {
2063
+ didFile: "did/item.did",
2064
+ canisterId: null as never,
2065
+ verify: "live",
2066
+ },
2067
+ // On disk, neither fetched nor checked: only generated.
2068
+ unused: { didFile: "did/ping.did", canisterId: "" },
2069
+ },
2070
+ })
2071
+ expect(message).toContain("could not generate 3 of 3 canisters")
2072
+ // One reason, so one line.
2073
+ expect(message).toContain(
2074
+ '- fetched (did/fetched.did), unused (did/ping.did): canisterId is "", not a canister ID'
2075
+ )
2076
+ expect(lineOf(message, "checked")).toContain(
2077
+ "checked (did/item.did): canisterId is null, not a canister ID"
2078
+ )
2079
+ expect(exists(app, "did/fetched.did")).toBe(false)
2080
+ expect(exists(app, "src/canisters/ping.ts")).toBe(false)
2081
+ expect(asked).toEqual([])
2082
+ })
2083
+
2084
+ // JSON.stringify throws on a bigint: a message built with it threw out of
2085
+ // the run, past failOnError, so a dev server that keeps serving failed its
2086
+ // start instead, with no log and no overlay.
2087
+ it("methods given as 1n or [1n] fails the canister cleanly under failOnError: false, in the log and the overlay, and nothing runs for it", async () => {
2088
+ const asked = refuseNetwork()
2089
+ const app = newApp({
2090
+ "did/a.did": PING_DID,
2091
+ "did/b.did": PING_DID,
2092
+ "did/ping.did": PING_DID,
2093
+ })
2094
+ const cli = hookCli(app)
2095
+ const { connections, plugin } = spyOnConnections()
2096
+ const running = await startDev(
2097
+ app.root,
2098
+ {
2099
+ failOnError: false,
2100
+ canisters: {
2101
+ bigint: { didFile: "did/a.did", methods: 1n as never },
2102
+ listed: { didFile: "did/b.did", methods: [1n] as never },
2103
+ ping: { didFile: "did/ping.did" },
2104
+ },
2105
+ },
2106
+ { before: [plugin] }
2107
+ )
2108
+ closers.push(running.close)
2109
+ const errors = running.lines.filter((line) => line.startsWith("error:"))
2110
+ expect(errors).toHaveLength(1)
2111
+ expect(errors[0]).toContain("could not generate 2 of 3 canisters")
2112
+ expect(lineOf(errors[0], "bigint")).toContain(
2113
+ "bigint (did/a.did): methods is 1n, not a list of method names"
2114
+ )
2115
+ expect(lineOf(errors[0], "listed")).toContain(
2116
+ "listed (did/b.did): methods holds 1n, which is not a method name"
2117
+ )
2118
+ // A browser that connects is handed both.
2119
+ const client = { send: vi.fn() }
2120
+ connections[0](client)
2121
+ const shown = JSON.parse(client.send.mock.calls[0][0] as string) as {
2122
+ err: { message: string }
2123
+ }
2124
+ expect(shown.err.message).toContain("methods is 1n")
2125
+ expect(shown.err.message).toContain("methods holds 1n")
2126
+ // Nothing ran for them: no listing, and one generator, for ping alone.
2127
+ expect(cli.commands()).toEqual(["gen"])
2128
+ expect(app.runs()[0]).toContain(path.join(app.root, "did/ping.did"))
2129
+ expect(app.runs()[0]).not.toContain(path.join(app.root, "did/a.did"))
2130
+ expect(app.runs()[0]).not.toContain(path.join(app.root, "did/b.did"))
2131
+ expect(exists(app, "src/canisters/ping.ts")).toBe(true)
2132
+ expect(asked).toEqual([])
2133
+ })
2134
+
2135
+ // Each kind of value a JavaScript config can give, quoted as it was given:
2136
+ // none throws its message out of the run, no getter of it runs, and none
2137
+ // costs a process.
2138
+ it("of any kind is quoted as given, fails only its canister, and runs no getter of it", async () => {
2139
+ const asked = refuseNetwork()
2140
+ const app = newApp({
2141
+ "did/a.did": PING_DID,
2142
+ "did/b.did": PING_DID,
2143
+ "did/c.did": PING_DID,
2144
+ "did/d.did": PING_DID,
2145
+ "did/e.did": PING_DID,
2146
+ "did/ping.did": PING_DID,
2147
+ })
2148
+ const cli = hookCli(app)
2149
+ const cyclic: Record<string, unknown> = { name: "a" }
2150
+ cyclic.self = cyclic
2151
+ let read = 0
2152
+ const { result, lines } = runBuild(app.root, {
2153
+ failOnError: false,
2154
+ canisters: {
2155
+ symbol: { didFile: "did/a.did", verify: Symbol("live") as never },
2156
+ cycle: { didFile: "did/b.did", methods: [cyclic] as never },
2157
+ fn: {
2158
+ didFile: "did/c.did",
2159
+ canisterId: (() => LEDGER_ID) as never,
2160
+ },
2161
+ throwing: {
2162
+ didFile: "did/d.did",
2163
+ get outDir(): string {
2164
+ throw new Error("no outDir today")
2165
+ },
2166
+ },
2167
+ accessor: {
2168
+ didFile: "did/e.did",
2169
+ methods: [
2170
+ {
2171
+ get name() {
2172
+ read += 1
2173
+ return "get"
2174
+ },
2175
+ },
2176
+ ] as never,
2177
+ },
2178
+ nul: { didFile: "did/f.did\0" },
2179
+ nil: null as never,
2180
+ ping: { didFile: "did/ping.did" },
2181
+ },
2182
+ })
2183
+ await result
2184
+ const errors = lines.filter((line) => line.startsWith("error:"))
2185
+ expect(errors).toHaveLength(1)
2186
+ const [message] = errors
2187
+ expect(message).toContain("could not generate 7 of 8 canisters")
2188
+ expect(lineOf(message, "symbol")).toContain(
2189
+ 'symbol (did/a.did): verify is Symbol(live), not "live"'
2190
+ )
2191
+ expect(lineOf(message, "cycle")).toContain(
2192
+ 'cycle (did/b.did): methods holds {"name":"a","self":[circular]}, which is not a method name'
2193
+ )
2194
+ expect(lineOf(message, "fn")).toContain(
2195
+ "fn (did/c.did): canisterId is a function, not a canister ID"
2196
+ )
2197
+ expect(lineOf(message, "throwing")).toContain(
2198
+ "throwing (did/d.did): outDir could not be read: no outDir today"
2199
+ )
2200
+ expect(lineOf(message, "accessor")).toContain(
2201
+ 'accessor (did/e.did): methods holds {"name":[getter]}, which is not a method name'
2202
+ )
2203
+ expect(message).toContain(
2204
+ '- nul: didFile is "did/f.did\\u0000", not a path'
2205
+ )
2206
+ expect(message).toContain(
2207
+ "- nil: its options are null, not a plain object of options"
2208
+ )
2209
+ expect(read).toBe(0)
2210
+ expect(cli.commands()).toEqual(["gen"])
2211
+ expect(app.runs()[0]).toContain(path.join(app.root, "did/ping.did"))
2212
+ expect(exists(app, "src/canisters/ping.ts")).toBe(true)
2213
+ expect(asked).toEqual([])
2214
+ })
2215
+
2216
+ // An object of options is a plain one. Read for its keys, a Map's entries
2217
+ // or a Promise's value were none, so the canister was taken for one only
2218
+ // named in the cookie, and nothing was generated for it without a word,
2219
+ // and an instance of a class was read through its class: a network of one
2220
+ // with a host took that host.
2221
+ it("an entry, or its network, that is not a plain object fails the canister, quoted as given, and one with no prototype is generated", async () => {
2222
+ const asked = refuseNetwork()
2223
+ const app = newApp({
2224
+ "did/a.did": PING_DID,
2225
+ "did/b.did": PING_DID,
2226
+ "did/c.did": PING_DID,
2227
+ "did/d.did": PING_DID,
2228
+ "did/e.did": PING_DID,
2229
+ "did/f.did": PING_DID,
2230
+ "did/ping.did": PING_DID,
2231
+ })
2232
+ const cli = hookCli(app)
2233
+ class Options {
2234
+ didFile = "did/c.did"
2235
+ }
2236
+ class Network {
2237
+ host = "http://127.0.0.1:4943"
2238
+ }
2239
+ const { message } = await buildError(app, {
2240
+ canisters: {
2241
+ map: new Map([["didFile", "did/a.did"]]) as never,
2242
+ promise: Promise.resolve({ didFile: "did/b.did" }) as never,
2243
+ instance: new Options(),
2244
+ inherits: Object.create({ didFile: "did/d.did" }),
2245
+ url: {
2246
+ didFile: "did/e.did",
2247
+ network: new URL("http://127.0.0.1:4943") as never,
2248
+ },
2249
+ host: { didFile: "did/f.did", network: new Network() },
2250
+ bare: Object.assign(Object.create(null), {
2251
+ didFile: "did/ping.did",
2252
+ }),
2253
+ },
2254
+ })
2255
+ expect(message).toContain("could not generate 6 of 7 canisters")
2256
+ const notOptions = "not a plain object of options (didFile, canisterId, …)"
2257
+ expect(message).toContain(`- map: its options are Map {}, ${notOptions}`)
2258
+ expect(message).toContain(
2259
+ `- promise: its options are Promise {}, ${notOptions}`
2260
+ )
2261
+ expect(message).toContain(
2262
+ `- instance: its options are Options {"didFile":"did/c.did"}, ${notOptions}`
2263
+ )
2264
+ expect(message).toContain(
2265
+ `- inherits: its options are {} (an object that inherits from another), ${notOptions}`
2266
+ )
2267
+ const notNetwork =
2268
+ 'is not "ic", "local" or a plain object { host: "<http or https URL>" }'
2269
+ expect(lineOf(message, "url")).toContain(
2270
+ `url (did/e.did): network URL {} ${notNetwork}`
2271
+ )
2272
+ expect(lineOf(message, "host")).toContain(
2273
+ `host (did/f.did): network Network {"host":"http://127.0.0.1:4943"} ${notNetwork}`
2274
+ )
2275
+ expect(message).not.toContain("- bare")
2276
+ expect(cli.commands()).toEqual(["gen"])
2277
+ expect(app.runs()[0]).toContain(path.join(app.root, "did/ping.did"))
2278
+ expect(exists(app, "src/canisters/ping.ts")).toBe(true)
2279
+ for (const stem of ["a", "b", "c", "d", "e", "f"]) {
2280
+ expect(exists(app, `src/canisters/${stem}.ts`)).toBe(false)
2281
+ }
2282
+ expect(asked).toEqual([])
2283
+ })
2284
+
2285
+ // A key that is no option was never read: a misspelled didFile took the
2286
+ // canister for one only named in the cookie, so nothing was generated and
2287
+ // the module of the last run stayed, a misspelled canisterId generated it
2288
+ // without its ID, and a network's rootKey was read as nothing. None said a
2289
+ // word.
2290
+ it("a key that is not an option fails the canister, whatever its value, naming the option it may misspell, with no request and nothing run", async () => {
2291
+ const asked = refuseNetwork()
2292
+ const app = newApp({
2293
+ "did/b.did": PING_DID,
2294
+ "did/c.did": PING_DID,
2295
+ "did/d.did": PING_DID,
2296
+ "did/e.did": PING_DID,
2297
+ "did/f.did": PING_DID,
2298
+ "did/g.did": PING_DID,
2299
+ "did/ping.did": PING_DID,
2300
+ // The module of the last run, which a misspelled didFile left as it was.
2301
+ "src/canisters/a.ts": "// The last run's module.\n",
2302
+ })
2303
+ const cli = hookCli(app)
2304
+ const { result, lines } = runBuild(app.root, {
2305
+ failOnError: false,
2306
+ canisters: {
2307
+ typo: { didfile: "did/a.did" } as never,
2308
+ id: { didFile: "did/b.did", canisterID: LEDGER_ID } as never,
2309
+ unset: { didFile: "did/c.did", canisterID: undefined } as never,
2310
+ extra: { didFile: "did/d.did", decimals: 8, Methods: ["x"] } as never,
2311
+ net: {
2312
+ didFile: "did/e.did",
2313
+ network: { host: "http://127.0.0.1:4943", rootKey: "00" } as never,
2314
+ },
2315
+ unlisted: new Proxy(
2316
+ { didFile: "did/f.did" },
2317
+ {
2318
+ ownKeys() {
2319
+ throw new Error("no keys today")
2320
+ },
2321
+ }
2322
+ ),
2323
+ hidden: {
2324
+ didFile: "did/g.did",
2325
+ network: new Proxy(
2326
+ { host: "http://127.0.0.1:4943" },
2327
+ {
2328
+ ownKeys() {
2329
+ throw new Error("no keys today")
2330
+ },
2331
+ }
2332
+ ),
2333
+ },
2334
+ ping: { didFile: "did/ping.did" },
2335
+ },
2336
+ })
2337
+ await result
2338
+ const errors = lines.filter((line) => line.startsWith("error:"))
2339
+ expect(errors).toHaveLength(1)
2340
+ const [message] = errors
2341
+ expect(message).toContain("could not generate 7 of 8 canisters")
2342
+ const options =
2343
+ "its options are didFile, outDir, canisterId, network, methods and verify"
2344
+ expect(message).toContain(
2345
+ `- typo: "didfile" (did you mean didFile?) is not an option of a canister: ${options}`
2346
+ )
2347
+ // One reason, so one line.
2348
+ expect(message).toContain(
2349
+ '- id (did/b.did), unset (did/c.did): "canisterID" (did you mean canisterId?) ' +
2350
+ `is not an option of a canister: ${options}`
2351
+ )
2352
+ expect(lineOf(message, "extra")).toContain(
2353
+ `extra (did/d.did): "decimals" and "Methods" (did you mean methods?) are not options of a canister: ${options}`
2354
+ )
2355
+ expect(lineOf(message, "net")).toContain(
2356
+ 'net (did/e.did): "rootKey" is not an option of network { host }: ' +
2357
+ 'give the network as { host: "<http or https URL>" } alone'
2358
+ )
2359
+ expect(lineOf(message, "unlisted")).toContain(
2360
+ "unlisted (did/f.did): its options could not be listed: no keys today"
2361
+ )
2362
+ expect(lineOf(message, "hidden")).toContain(
2363
+ "hidden (did/g.did): network's keys could not be listed: no keys today"
2364
+ )
2365
+ // Nothing ran for them: one generator, for ping alone.
2366
+ expect(cli.commands()).toEqual(["gen"])
2367
+ expect(app.runs()[0]).toContain(path.join(app.root, "did/ping.did"))
2368
+ expect(exists(app, "src/canisters/ping.ts")).toBe(true)
2369
+ for (const stem of ["b", "c", "d", "e", "f", "g"]) {
2370
+ expect(exists(app, `src/canisters/${stem}.ts`)).toBe(false)
2371
+ }
2372
+ expect(read(app, "src/canisters/a.ts")).toBe("// The last run's module.\n")
2373
+ expect(asked).toEqual([])
2374
+ })
2375
+
2376
+ // Each option is checked when the plugin is made, whether or not a step of
2377
+ // the run would use it: a file on disk that is neither fetched nor checked
2378
+ // reads no canisterId and no network, and still fails for one that is not.
2379
+ it("didFile, outDir or network that is not one fails the canister of a file on disk that nothing fetches or checks", async () => {
2380
+ const asked = refuseNetwork()
2381
+ const app = newApp({ "did/a.did": PING_DID, "did/ping.did": PING_DID })
2382
+ const cli = hookCli(app)
2383
+ const { message } = await buildError(app, {
2384
+ canisters: {
2385
+ path: { didFile: 1 as never },
2386
+ empty: { didFile: "" },
2387
+ out: { didFile: "did/a.did", outDir: "" },
2388
+ network: { didFile: "did/a.did", network: "mainnet" as never },
2389
+ ping: { didFile: "did/ping.did" },
2390
+ },
2391
+ })
2392
+ expect(message).toContain("could not generate 4 of 5 canisters")
2393
+ expect(message).toContain("- path: didFile is 1, not a path")
2394
+ expect(message).toContain('- empty: didFile is "", not a path')
2395
+ expect(lineOf(message, "out")).toContain(
2396
+ 'out (did/a.did): outDir is "", not a path'
2397
+ )
2398
+ expect(lineOf(message, "network")).toContain(
2399
+ 'network (did/a.did): network "mainnet" is not "ic", "local" or { host: "<http or https URL>" }'
2400
+ )
2401
+ expect(cli.commands()).toEqual(["gen"])
2402
+ expect(app.runs()[0]).not.toContain(path.join(app.root, "did/a.did"))
2403
+ expect(exists(app, "src/canisters/a.ts")).toBe(false)
2404
+ expect(asked).toEqual([])
2405
+ })
2406
+
2407
+ // The live check read verify, after the fetch: a verify that is not one
2408
+ // made the request and wrote the file, and only then failed the canister.
2409
+ it("verify that is not live fails the canister before its didFile is fetched: no request, and nothing written", async () => {
2410
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: WRITTEN } })
2411
+ const app = newApp({ "did/committed.did": WITHOUT_PUT })
2412
+ const network = { host: ic.host }
2413
+
2414
+ // Missing, so it would be fetched.
2415
+ const missing = await buildError(app, {
2416
+ canisters: {
2417
+ missing: {
2418
+ didFile: "did/missing.did",
2419
+ canisterId: LEDGER_ID,
2420
+ network,
2421
+ verify: true as never,
2422
+ },
2423
+ },
2424
+ })
2425
+ expect(lineOf(missing.message, "missing")).toContain(
2426
+ 'missing (did/missing.did): verify is true, not "live"'
2427
+ )
2428
+ expect(exists(app, "did/missing.did")).toBe(false)
2429
+
2430
+ // Asked for again by IC_REACTOR_FETCH: the committed file is not
2431
+ // rewritten.
2432
+ vi.stubEnv("IC_REACTOR_FETCH", "committed")
2433
+ const again = await buildError(app, {
2434
+ canisters: {
2435
+ committed: {
2436
+ didFile: "did/committed.did",
2437
+ canisterId: LEDGER_ID,
2438
+ network,
2439
+ verify: "Live" as never,
2440
+ },
2441
+ },
2442
+ })
2443
+ expect(lineOf(again.message, "committed")).toContain(
2444
+ 'committed (did/committed.did): verify is "Live", not "live"'
2445
+ )
2446
+ expect(read(app, "did/committed.did")).toBe(WITHOUT_PUT)
2447
+ vi.unstubAllEnvs()
2448
+
2449
+ // Shared with a canister that could fetch it: not fetched for either.
2450
+ const shared = await buildError(app, {
2451
+ canisters: {
2452
+ fetcher: { didFile: "did/shared.did", canisterId: LEDGER_ID, network },
2453
+ typo: {
2454
+ didFile: "did/shared.did",
2455
+ outDir: "src/typo",
2456
+ verify: "yes" as never,
2457
+ },
2458
+ },
2459
+ })
2460
+ expect(shared.message).toContain(
2461
+ `- fetcher (did/shared.did), typo (did/shared.did): "fetcher", "typo" name did/shared.did, which is not fetched, ` +
2462
+ `since "typo" cannot be generated (typo: verify is "yes", not "live"`
2463
+ )
2464
+ expect(exists(app, "did/shared.did")).toBe(false)
2465
+ expect(ic.requests).toEqual([])
2466
+ })
2467
+
2468
+ // IC_REACTOR_VERIFY was read by the live check, after the methods of the
2469
+ // canister were listed: a canister it names that cannot be checked costs
2470
+ // nothing either.
2471
+ it("IC_REACTOR_VERIFY or IC_REACTOR_FETCH naming a canister with no canisterId fails it before the run does anything for it", async () => {
2472
+ const asked = refuseNetwork()
2473
+ const app = newApp({ "did/item.did": WRITTEN })
2474
+ const cli = hookCli(app)
2475
+ const options: IcReactorPluginOptions = {
2476
+ canisters: {
2477
+ item: { didFile: "did/item.did", methods: ["get", "maybe", "put"] },
2478
+ },
2479
+ }
2480
+ for (const [variable, message] of [
2481
+ [
2482
+ "IC_REACTOR_VERIFY",
2483
+ "item (did/item.did): IC_REACTOR_VERIFY asks for its live check, but it names no canisterId",
2484
+ ],
2485
+ [
2486
+ "IC_REACTOR_FETCH",
2487
+ "item (did/item.did): IC_REACTOR_FETCH asks to fetch its didFile again, but it names no canisterId",
2488
+ ],
2489
+ ]) {
2490
+ vi.unstubAllEnvs()
2491
+ vi.stubEnv(variable, "item")
2492
+ const failed = await buildError(app, options)
2493
+ expect(lineOf(failed.message, "item")).toContain(message)
2494
+ expect(cli.commands()).toEqual([])
2495
+ }
2496
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
2497
+ expect(asked).toEqual([])
2498
+ })
2499
+
2500
+ // A save's run sees only the canisters that name the saved file, and it
2501
+ // asked only whether another canister writes their module and whether
2502
+ // their live check failed: a canister that each start refuses for a
2503
+ // variable was listed and generated by a save of its file, and taken for
2504
+ // fixed, out of the log and the overlay.
2505
+ it("IC_REACTOR_VERIFY or IC_REACTOR_FETCH naming a canister with no canisterId leaves it failed through a save of its file under vite dev, which runs nothing for it, and through its deletion", async () => {
2506
+ const asked = refuseNetwork()
2507
+ const app = newApp({
2508
+ "did/item.did": WRITTEN,
2509
+ "did/other.did": WRITTEN,
2510
+ "did/ping.did": PING_DID,
2511
+ })
2512
+ const cli = hookCli(app)
2513
+ // "nope" names no canister: the start warns of it, and a save does not
2514
+ // again.
2515
+ vi.stubEnv("IC_REACTOR_VERIFY", "item,nope")
2516
+ vi.stubEnv("IC_REACTOR_FETCH", "other")
2517
+ const { connections, plugin } = spyOnConnections()
2518
+ const methods = ["get", "maybe", "put"]
2519
+ const running = await startDev(
2520
+ app.root,
2521
+ {
2522
+ canisters: {
2523
+ item: { didFile: "did/item.did", methods },
2524
+ other: { didFile: "did/other.did", outDir: "src/other", methods },
2525
+ ping: { didFile: "did/ping.did" },
2526
+ },
2527
+ },
2528
+ { before: [plugin] }
2529
+ )
2530
+ closers.push(running.close)
2531
+ const refusals = {
2532
+ item: "item (did/item.did): IC_REACTOR_VERIFY asks for its live check, but it names no canisterId",
2533
+ other:
2534
+ "other (did/other.did): IC_REACTOR_FETCH asks to fetch its didFile again, but it names no canisterId",
2535
+ }
2536
+ const errors = () =>
2537
+ running.lines.filter((line) => line.startsWith("error: ic-reactor:"))
2538
+ /** What a browser that connects now is shown. */
2539
+ const shown = () => {
2540
+ const client = { send: vi.fn() }
2541
+ connections[0](client)
2542
+ expect(client.send).toHaveBeenCalledTimes(1)
2543
+ return (
2544
+ JSON.parse(client.send.mock.calls[0][0] as string) as {
2545
+ err: { message: string }
2546
+ }
2547
+ ).err.message
2548
+ }
2549
+ expect(errors()).toHaveLength(1)
2550
+ expect(lineOf(errors()[0], "item")).toContain(refusals.item)
2551
+ expect(lineOf(errors()[0], "other")).toContain(refusals.other)
2552
+ // One generator, for ping alone.
2553
+ expect(cli.commands()).toEqual(["gen"])
2554
+ const send = vi.spyOn(running.server.ws, "send")
2555
+
2556
+ // A save of each file: refused again, in the log and the overlay, and
2557
+ // nothing runs for it.
2558
+ for (const name of ["item", "other"] as const) {
2559
+ const before = errors().length
2560
+ const file = path.join(app.root, `did/${name}.did`)
2561
+ fs.writeFileSync(file, `// Saved.\n${WRITTEN}`)
2562
+ running.server.watcher.emit("change", file)
2563
+ await vi.waitFor(() => expect(errors()).toHaveLength(before + 1), {
2564
+ timeout: 10_000,
2565
+ })
2566
+ expect(lineOf(errors()[before], name)).toContain(refusals[name])
2567
+ expect(shown()).toContain(refusals.item)
2568
+ expect(shown()).toContain(refusals.other)
2569
+ }
2570
+ expect(send).not.toHaveBeenCalledWith({ type: "full-reload" })
2571
+ expect(
2572
+ running.lines.filter((line) => line.includes("changed, regenerating"))
2573
+ ).toEqual([])
2574
+ expect(cli.commands()).toEqual(["gen"])
2575
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
2576
+ expect(exists(app, "src/other")).toBe(false)
2577
+
2578
+ // Deleted: still refused for the variable, since the file coming back
2579
+ // would not be generated either.
2580
+ const file = path.join(app.root, "did/item.did")
2581
+ fs.rmSync(file)
2582
+ running.server.watcher.emit("unlink", file)
2583
+ await vi.waitFor(() => expect(errors()).toHaveLength(4), {
2584
+ timeout: 10_000,
2585
+ })
2586
+ expect(lineOf(errors()[3], "item")).toContain(refusals.item)
2587
+ expect(errors()[3]).not.toContain("was deleted")
2588
+ expect(shown()).toContain(refusals.item)
2589
+ expect(
2590
+ running.lines.filter((line) => line.includes('names "nope"'))
2591
+ ).toEqual([
2592
+ `warn: ic-reactor: IC_REACTOR_VERIFY names "nope", which is not a canister with a didFile in this plugin's options`,
2593
+ ])
2594
+ expect(asked).toEqual([])
2595
+ }, 30_000)
2596
+
2597
+ // A canister with no didFile generates nothing, and its options were read
2598
+ // no further than its canisterId: methods: 1n, verify: "nope", network: 5
2599
+ // or outDir: 3 on it were taken without a word. One whose values are all
2600
+ // values is only named in the cookie, whatever they are.
2601
+ it("of a canister with no didFile are checked as any canister's: one that is not one fails it, with no request and nothing run", async () => {
2602
+ const asked = refuseNetwork()
2603
+ const app = newApp({ "did/ping.did": PING_DID })
2604
+ const cli = hookCli(app)
2605
+ const { result, lines } = runBuild(app.root, {
2606
+ failOnError: false,
2607
+ canisters: {
2608
+ typo: {
2609
+ canisterId: LEDGER_ID,
2610
+ outDir: 3 as never,
2611
+ network: 5 as never,
2612
+ methods: 1n as never,
2613
+ verify: "nope" as never,
2614
+ },
2615
+ empty: { canisterId: "" },
2616
+ // Values all, so none fails it, though nothing reads them.
2617
+ named: {
2618
+ canisterId: LEDGER_ID,
2619
+ outDir: "src/named",
2620
+ network: "ic",
2621
+ methods: ["icrc1_fee"],
2622
+ verify: "live",
2623
+ },
2624
+ ping: { didFile: "did/ping.did" },
2625
+ },
2626
+ })
2627
+ await result
2628
+ const errors = lines.filter((line) => line.startsWith("error:"))
2629
+ expect(errors).toHaveLength(1)
2630
+ const [message] = errors
2631
+ expect(message).toContain("could not generate 2 of 3 canisters")
2632
+ expect(message).toContain(
2633
+ '- typo: outDir is 3, not a path: give the directory relative to the Vite root, or remove outDir for "src/canisters"; ' +
2634
+ 'network 5 is not "ic", "local" or { host: "<http or https URL>" }; ' +
2635
+ "methods is 1n, not a list of method names; " +
2636
+ 'verify is "nope", not "live": set it to "live" for the live check, or remove it for none'
2637
+ )
2638
+ expect(message).toContain(
2639
+ '- empty: canisterId is "", not a canister ID: give the canister\'s principal as text, or remove canisterId'
2640
+ )
2641
+ expect(message).not.toContain("named")
2642
+ expect(cli.commands()).toEqual(["gen"])
2643
+ expect(exists(app, "src/canisters/ping.ts")).toBe(true)
2644
+ expect(exists(app, "src/named")).toBe(false)
2645
+ expect(asked).toEqual([])
2646
+ })
2647
+
2648
+ // Whether text is a principal was told only by the fetch or the check that
2649
+ // read it, so a canister with methods and verify: "live" had its methods
2650
+ // listed by a `meant` process before its check failed.
2651
+ it("canisterId text that is no principal fails the canister when the plugin is made, before any process or request", async () => {
2652
+ const asked = refuseNetwork()
2653
+ const app = newApp({ "did/item.did": WRITTEN, "did/ping.did": PING_DID })
2654
+ const cli = hookCli(app)
2655
+ const wrong = (text: string) =>
2656
+ `canisterId is ${JSON.stringify(text)}, not a canister ID: it is not the text of a principal, ` +
2657
+ `which is lowercase, in groups of five joined by dashes, with a checksum, and at most 63 characters. ` +
2658
+ `Give the canister's ID as it is printed, or remove canisterId`
2659
+ const { result, lines } = runBuild(app.root, {
2660
+ failOnError: false,
2661
+ canisters: {
2662
+ // The ledger's ID with its last letter changed: no checksum matches.
2663
+ checksum: {
2664
+ didFile: "did/item.did",
2665
+ canisterId: "ryjl3-tyaaa-aaaaa-aaaba-caj",
2666
+ methods: ["get", "maybe", "put"],
2667
+ verify: "live",
2668
+ },
2669
+ upper: {
2670
+ didFile: "did/item.did",
2671
+ outDir: "src/upper",
2672
+ canisterId: "RYJL3-TYAAA-AAAAA-AAABA-CAI",
2673
+ verify: "live",
2674
+ },
2675
+ words: { canisterId: "the ledger" },
2676
+ ping: { didFile: "did/ping.did" },
2677
+ },
2678
+ })
2679
+ await result
2680
+ const errors = lines.filter((line) => line.startsWith("error:"))
2681
+ expect(errors).toHaveLength(1)
2682
+ const [message] = errors
2683
+ expect(message).toContain("could not generate 3 of 4 canisters")
2684
+ expect(lineOf(message, "checksum")).toContain(
2685
+ `checksum (did/item.did): ${wrong("ryjl3-tyaaa-aaaaa-aaaba-caj")}`
2686
+ )
2687
+ expect(lineOf(message, "upper")).toContain(
2688
+ `upper (did/item.did): ${wrong("RYJL3-TYAAA-AAAAA-AAABA-CAI")}`
2689
+ )
2690
+ expect(message).toContain(`- words: ${wrong("the ledger")}`)
2691
+ // No listing and no check: one generator, for ping alone.
2692
+ expect(cli.commands()).toEqual(["gen"])
2693
+ expect(exists(app, "src/canisters/item.ts")).toBe(false)
2694
+ expect(asked).toEqual([])
2695
+ })
2696
+ })
2697
+
2698
+ // The generator names a module after its .did, so two files of one name in
2699
+ // one outDir write one module, which the first canister in the options
2700
+ // keeps. The second was refused only by the generator's run, after the run
2701
+ // had fetched, projected, listed and checked its file; and a save of its file
2702
+ // under vite dev, whose run sees only the canisters that name the file,
2703
+ // generated it over the first one's module.
2704
+ describe("a canister whose module a canister before it writes", () => {
2705
+ const collision =
2706
+ 'a (a/ping.did): would write src/canisters/ping.ts, which "b" already writes. ' +
2707
+ "The generator names a module after its .did file: rename one file or give one canister its own outDir"
2708
+
2709
+ it("fails before the run does anything for it, its file missing or on disk: no request, no process, nothing written", async () => {
2710
+ const ic = await fakeIc({ [LEDGER_ID]: { candid: LEDGER } })
2711
+ const app = newApp({ "b/ping.did": PING_DID })
2712
+ const cli = hookCli(app)
2713
+ const a = {
2714
+ didFile: "a/ping.did",
2715
+ canisterId: LEDGER_ID,
2716
+ network: { host: ic.host },
2717
+ methods: ["icrc1_fee"],
2718
+ }
2719
+
2720
+ // Missing, so it would be fetched and projected.
2721
+ const missing = runBuild(app.root, {
2722
+ failOnError: false,
2723
+ canisters: { b: { didFile: "b/ping.did" }, a },
2724
+ })
2725
+ await missing.result
2726
+ const errors = missing.lines.filter((line) => line.startsWith("error:"))
2727
+ expect(errors).toHaveLength(1)
2728
+ expect(lineOf(errors[0], "a")).toContain(collision)
2729
+ expect(exists(app, "a/ping.did")).toBe(false)
2730
+ expect(read(app, "src/canisters/ping.ts")).not.toContain("icrc1_fee")
2731
+
2732
+ // On disk, with methods to compare it with and the live check asked for.
2733
+ fs.mkdirSync(path.join(app.root, "a"))
2734
+ fs.writeFileSync(path.join(app.root, "a/ping.did"), LEDGER)
2735
+ const checked = await buildError(app, {
2736
+ canisters: { b: { didFile: "b/ping.did" }, a: { ...a, verify: "live" } },
2737
+ })
2738
+ expect(lineOf(checked.message, "a")).toContain(collision)
2739
+
2740
+ // One generator for each build, for b alone.
2741
+ expect(cli.commands()).toEqual(["gen", "gen"])
2742
+ const aFile = path.join(app.root, "a/ping.did")
2743
+ expect(app.runs().filter((args) => args.includes(aFile))).toEqual([])
2744
+ expect(ic.requests).toEqual([])
2745
+ })
2746
+
2747
+ it("is not generated by a save of its file under vite dev, which leaves the module as it was, nor taken for deleted with its file", async () => {
2748
+ const asked = refuseNetwork()
2749
+ const app = newApp({ "a/ping.did": WRITTEN, "b/ping.did": PING_DID })
2750
+ const cli = hookCli(app)
2751
+ const running = await startDev(app.root, {
2752
+ canisters: {
2753
+ b: { didFile: "b/ping.did" },
2754
+ a: { didFile: "a/ping.did" },
2755
+ },
2756
+ })
2757
+ closers.push(running.close)
2758
+ const errors = () =>
2759
+ running.lines.filter((line) => line.startsWith("error: ic-reactor:"))
2760
+ expect(errors()).toHaveLength(1)
2761
+ expect(lineOf(errors()[0], "a")).toContain(collision)
2762
+ const module = read(app, "src/canisters/ping.ts")
2763
+ expect(module).not.toContain("maybe")
2764
+
2765
+ // A save of a's file alone: a is refused again, and nothing runs for it.
2766
+ const file = path.join(app.root, "a/ping.did")
2767
+ fs.writeFileSync(file, `// Saved.\n${WRITTEN}`)
2768
+ running.server.watcher.emit("change", file)
2769
+ await vi.waitFor(() => expect(errors()).toHaveLength(2), {
2770
+ timeout: 10_000,
2771
+ })
2772
+ expect(lineOf(errors()[1], "a")).toContain(collision)
2773
+ expect(read(app, "src/canisters/ping.ts")).toBe(module)
2774
+ expect(cli.commands()).toEqual(["gen"])
2775
+
2776
+ // Deleted: still refused for the module, which the file coming back
2777
+ // would not generate either.
2778
+ fs.rmSync(file)
2779
+ running.server.watcher.emit("unlink", file)
2780
+ await vi.waitFor(() => expect(errors()).toHaveLength(3), {
2781
+ timeout: 10_000,
2782
+ })
2783
+ expect(lineOf(errors()[2], "a")).toContain(collision)
2784
+ expect(errors()[2]).not.toContain("was deleted")
2785
+ expect(asked).toEqual([])
2786
+ }, 30_000)
2787
+
2788
+ // The plan named a module by a rule of its own, which cut the whole
2789
+ // extension (`api.candid` meant `api.ts`), where the generator cuts only a
2790
+ // final `.did` (`outputsOf`): a canister whose file the generator cannot
2791
+ // even read claimed the module of the canisters after it, which were
2792
+ // refused for it.
2793
+ it("is told by the module the generator names: a file whose name does not end in .did claims no module of a .did's, and fails with the generator's own message", async () => {
2794
+ const asked = refuseNetwork()
2795
+ const app = newApp({
2796
+ "a/api.candid": PING_DID,
2797
+ "b/api.DID": PING_DID,
2798
+ "c/api.did": PING_DID,
2799
+ })
2800
+ const cli = hookCli(app)
2801
+ const { message } = await buildError(app, {
2802
+ canisters: {
2803
+ candid: { didFile: "a/api.candid" },
2804
+ upper: { didFile: "b/api.DID" },
2805
+ plain: { didFile: "c/api.did" },
2806
+ },
2807
+ })
2808
+ expect(message).toContain("could not generate 2 of 3 canisters")
2809
+ for (const [name, file] of [
2810
+ ["candid", "a/api.candid"],
2811
+ ["upper", "b/api.DID"],
2812
+ ]) {
2813
+ expect(lineOf(message, name)).toContain(
2814
+ `${name} (${file}): did_source_not_found: cannot read ${path.join(app.root, file)}: no such .did file`
2815
+ )
2816
+ }
2817
+ expect(message).not.toContain("already writes")
2818
+ expect(message).not.toContain("- plain (")
2819
+ expect(read(app, "src/canisters/api.ts")).toMatch(/\bping\b/)
2820
+ // One generator, given the three files.
2821
+ expect(cli.commands()).toEqual(["gen"])
2822
+ expect(app.runs()[0]).toEqual([
2823
+ "gen",
2824
+ path.join(app.root, "a/api.candid"),
2825
+ path.join(app.root, "b/api.DID"),
2826
+ path.join(app.root, "c/api.did"),
2827
+ "-o",
2828
+ path.join(app.root, "src/canisters"),
2829
+ "--json",
2830
+ ])
2831
+ expect(asked).toEqual([])
2832
+ })
2833
+ })
2834
+
2835
+ /**
2836
+ * A plugin, ahead of the plugin under test, whose `closeBundle` holds each
2837
+ * server close, once armed, until `until` passes: the plugin's own
2838
+ * `closeBundle` runs after it (`sequential`), as for a server whose close
2839
+ * comes late.
2840
+ */
2841
+ function holdCloses(until: () => boolean) {
2842
+ let armed = false
2843
+ const plugin: Plugin = {
2844
+ name: "hold-closes",
2845
+ enforce: "pre",
2846
+ closeBundle: {
2847
+ sequential: true,
2848
+ async handler() {
2849
+ if (!armed) return
2850
+ await vi.waitFor(() => expect(until()).toBe(true), {
2851
+ timeout: 10_000,
2852
+ interval: 10,
2853
+ })
2854
+ },
2855
+ },
2856
+ }
2857
+ return {
2858
+ plugin,
2859
+ arm: () => (armed = true),
2860
+ disarm: () => (armed = false),
2861
+ }
2862
+ }
2863
+
2864
+ // `server.restart()` of an inline config keeps the plugin object, and Vite
2865
+ // resolves the new config, and starts the new server, before it closes the
2866
+ // old one. Each server's runs are its own: the old server's close stops only
2867
+ // its own, and the plugin keeps what it knows of the files.
2868
+ describe("a dev server restarted with the same plugin object", () => {
2869
+ const listed = (): IcReactorPluginOptions => ({
2870
+ canisters: {
2871
+ item: {
2872
+ didFile: "did/item.did",
2873
+ canisterId: LEDGER_ID,
2874
+ methods: ["get", "maybe", "put"],
2875
+ },
2876
+ },
2877
+ })
2878
+ const lacksPut =
2879
+ "item (did/item.did): methods names put, which did/item.did does not hold (it holds get, maybe)"
2880
+ // The errors below are the plugin's own: Vite 8.0.0 also logs one when a
2881
+ // restart cuts short the old server's dependency scan ("The server is being
2882
+ // restarted or closed").
2883
+
2884
+ // The run the restart queues for a file saved during its listing has to
2885
+ // list the text saved, a process of its own. The old server's close comes
2886
+ // once that process runs, and is held until then; the process is held in
2887
+ // turn while the test looks at the restarted server.
2888
+ it("keeps the run its start queues for a file saved during its listing when the old server closes after that start, and the overlay as it was until that run reports", async () => {
2889
+ const asked = refuseNetwork()
2890
+ // put, which methods names, is not in the file: the start fails it.
2891
+ const app = newApp({ "did/item.did": WITHOUT_PUT })
2892
+ const cli = hookCli(app)
2893
+ const hold = holdCloses(() => cli.commands().length >= 3)
2894
+ const { connections, plugin } = spyOnConnections()
2895
+ const running = await startDev(app.root, listed(), {
2896
+ before: [hold.plugin, plugin],
2897
+ })
2898
+ closers.push(async () => {
2899
+ cli.release()
2900
+ await running.close()
2901
+ })
2902
+ const errors = () =>
2903
+ running.lines.filter((line) => line.startsWith("error: ic-reactor:"))
2904
+ expect(errors()).toHaveLength(1)
2905
+ expect(errors()[0]).toContain(lacksPut)
2906
+
2907
+ // An edit that adds put is written, and the same server restarts. Once
2908
+ // the restart's run listed the edit, a text without put that no run
2909
+ // listed is saved: the run queued for it lists it again, in the third
2910
+ // process, which waits.
2911
+ const file = path.join(app.root, "did/item.did")
2912
+ fs.writeFileSync(file, `// Edited.\n${WRITTEN}`)
2913
+ cli.save("list", "after", file, `// Saved again.\n${WITHOUT_PUT}`)
2914
+ cli.hold(3)
2915
+ hold.arm()
2916
+ await running.server.restart()
2917
+ hold.disarm()
2918
+ expect(cli.commands()).toEqual(["project", "project", "project"])
2919
+ expect(errors()).toHaveLength(1)
2920
+ // The restart's run took nothing of the file: a browser that connects to
2921
+ // the restarted server is handed the failure the overlay showed.
2922
+ const client = { send: vi.fn() }
2923
+ connections[connections.length - 1](client)
2924
+ const shown = JSON.parse(client.send.mock.calls[0][0] as string) as {
2925
+ err: { message: string }
2926
+ }
2927
+ expect(shown.err.message).toContain(lacksPut)
2928
+
2929
+ // The queued run, which the old server's close did not stop, reports the
2930
+ // text saved.
2931
+ cli.release()
2932
+ await vi.waitFor(() => expect(errors()).toHaveLength(2), {
2933
+ timeout: 10_000,
2934
+ })
2935
+ expect(errors()[1]).toContain(lacksPut)
2936
+ expect(cli.commands()).toEqual(["project", "project", "project"])
2937
+ expect(asked).toEqual([])
2938
+ }, 30_000)
2939
+
2940
+ it("lists and generates nothing again that it did, shows what still fails, and regenerates on a save", async () => {
2941
+ const asked = refuseNetwork()
2942
+ // put, which methods names, is not in item's file: the start fails it.
2943
+ const app = newApp({
2944
+ "did/item.did": WITHOUT_PUT,
2945
+ "did/ping.did": PING_DID,
2946
+ })
2947
+ const cli = hookCli(app)
2948
+ const { connections, plugin } = spyOnConnections()
2949
+ const running = await startDev(
2950
+ app.root,
2951
+ {
2952
+ canisters: {
2953
+ ...listed().canisters,
2954
+ ping: { didFile: "did/ping.did" },
2955
+ },
2956
+ },
2957
+ { before: [plugin] }
2958
+ )
2959
+ closers.push(running.close)
2960
+ const errors = () =>
2961
+ running.lines.filter((line) => line.startsWith("error: ic-reactor:"))
2962
+ expect(errors()).toHaveLength(1)
2963
+ expect(errors()[0]).toContain(lacksPut)
2964
+ expect(cli.commands()).toEqual(["project", "gen"])
2965
+
2966
+ await running.server.restart()
2967
+ // Nothing changed: no process runs again, and the failure is told again.
2968
+ expect(cli.commands()).toEqual(["project", "gen"])
2969
+ expect(errors()).toHaveLength(2)
2970
+ expect(errors()[1]).toContain(lacksPut)
2971
+ // A browser that connects to the restarted server is handed it.
2972
+ const client = { send: vi.fn() }
2973
+ connections[connections.length - 1](client)
2974
+ const shown = JSON.parse(client.send.mock.calls[0][0] as string) as {
2975
+ err: { message: string }
2976
+ }
2977
+ expect(shown.err.message).toContain(lacksPut)
2978
+
2979
+ // put is saved back: the restarted server lists and generates item.
2980
+ const file = path.join(app.root, "did/item.did")
2981
+ fs.writeFileSync(file, WRITTEN)
2982
+ running.server.watcher.emit("change", file)
2983
+ await vi.waitFor(
2984
+ () => expect(exists(app, "src/canisters/item.ts")).toBe(true),
2985
+ {
2986
+ timeout: 10_000,
2987
+ }
2988
+ )
2989
+ expect(cli.commands()).toEqual(["project", "gen", "project", "gen"])
2990
+ expect(errors()).toHaveLength(2)
2991
+ expect(asked).toEqual([])
2992
+ }, 30_000)
2993
+ })