@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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +108 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +77 -0
- package/.github/actions/setup-rust-wasm/action.yml +25 -0
- package/.github/dependabot.yml +107 -0
- package/.github/workflows/auth-latest.yml +67 -0
- package/.github/workflows/b3pay-site.yml +49 -0
- package/.github/workflows/ci.yml +430 -0
- package/.github/workflows/codeql.yml +41 -0
- package/.github/workflows/dependency-review.yml +68 -0
- package/.github/workflows/docs-check.yml +98 -0
- package/.github/workflows/docs.yml +265 -0
- package/.github/workflows/e2e.yml +90 -0
- package/.github/workflows/examples-published.yml +97 -0
- package/.github/workflows/release.yml +326 -0
- package/.husky/pre-commit +12 -0
- package/.prettierignore +109 -0
- package/.prettierrc +6 -0
- package/AGENTS.md +103 -0
- package/CHANGELOG.md +3268 -0
- package/CLAUDE.md +93 -0
- package/CNAME +1 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/CONTRIBUTING.md +433 -0
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/SECURITY.md +74 -0
- package/b3pay-module.json +194 -0
- package/docs/.remarkrc.json +10 -0
- package/docs/README.md +64 -0
- package/docs/astro.config.mjs +205 -0
- package/docs/favicon.svg +15 -0
- package/docs/package.json +39 -0
- package/docs/public/favicon.svg +15 -0
- package/docs/public/ic-reactor-social-card.png +0 -0
- package/docs/scripts/b3pay-static.mjs +22 -0
- package/docs/scripts/check-example-docs.mjs +214 -0
- package/docs/scripts/check-links.mjs +212 -0
- package/docs/scripts/fix-dist-links.js +125 -0
- package/docs/scripts/fix-frontmatter.js +71 -0
- package/docs/scripts/remove-redundant-h1.cjs +48 -0
- package/docs/scripts/rewrite-typedoc-links.js +110 -0
- package/docs/scripts/split-index.js +69 -0
- package/docs/src/assets/ic-reactor-hero-dark.svg +23 -0
- package/docs/src/assets/ic-reactor-hero-light.svg +23 -0
- package/docs/src/assets/ic-reactor-lockup-dark.svg +15 -0
- package/docs/src/assets/ic-reactor-lockup-light.svg +15 -0
- package/docs/src/assets/ic-reactor-mark-dark.svg +14 -0
- package/docs/src/assets/ic-reactor-mark-light.svg +14 -0
- package/docs/src/assets/layers/candid-core-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-core-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-light.svg +4 -0
- package/docs/src/components/LibraryCards.astro +172 -0
- package/docs/src/components/LibraryIcon.astro +28 -0
- package/docs/src/components/SiteTitle.astro +241 -0
- package/docs/src/content/docs/404.mdx +19 -0
- package/docs/src/content/docs/build/agent-tools.mdx +593 -0
- package/docs/src/content/docs/build/icrc1-wallet.mdx +619 -0
- package/docs/src/content/docs/build/ssr-per-user.mdx +552 -0
- package/docs/src/content/docs/examples/canister-studio.mdx +95 -0
- package/docs/src/content/docs/examples/icrc-ledger.mdx +45 -0
- package/docs/src/content/docs/examples/index.mdx +38 -0
- package/docs/src/content/docs/examples/next-ssr.mdx +73 -0
- package/docs/src/content/docs/examples/node-agent-tool.mdx +80 -0
- package/docs/src/content/docs/examples/vite-wallet.mdx +71 -0
- package/docs/src/content/docs/guides/auth.mdx +1181 -0
- package/docs/src/content/docs/guides/client.mdx +551 -0
- package/docs/src/content/docs/guides/deploy.mdx +180 -0
- package/docs/src/content/docs/guides/errors.mdx +475 -0
- package/docs/src/content/docs/guides/getting-started.mdx +620 -0
- package/docs/src/content/docs/guides/reads.mdx +586 -0
- package/docs/src/content/docs/guides/ssr.mdx +529 -0
- package/docs/src/content/docs/guides/testing.mdx +401 -0
- package/docs/src/content/docs/guides/values.mdx +261 -0
- package/docs/src/content/docs/guides/vite-plugin.mdx +690 -0
- package/docs/src/content/docs/guides/writes.mdx +572 -0
- package/docs/src/content/docs/ic-reactor/index.mdx +167 -0
- package/docs/src/content/docs/index.mdx +65 -0
- package/docs/src/content/docs/migrating-from-3.mdx +535 -0
- package/docs/src/content/docs/packages/core.mdx +186 -0
- package/docs/src/content/docs/packages/mcp.mdx +163 -0
- package/docs/src/content/docs/packages/react.mdx +207 -0
- package/docs/src/content/docs/packages/vite-plugin.mdx +73 -0
- package/docs/src/content/docs/versioning.mdx +104 -0
- package/docs/src/content.config.ts +7 -0
- package/docs/src/styles/custom.css +189 -0
- package/docs/tsconfig.typedoc.json +26 -0
- package/docs/typedoc-entry.ts +17 -0
- package/docs/typedoc.json +37 -0
- package/e2e/Cargo.lock +646 -0
- package/e2e/Cargo.toml +3 -0
- package/e2e/README.md +101 -0
- package/e2e/frontends/build.mjs +94 -0
- package/e2e/frontends/index.html +10 -0
- package/e2e/global-setup.ts +65 -0
- package/e2e/icp.yaml +38 -0
- package/e2e/package.json +40 -0
- package/e2e/setup.ts +6 -0
- package/e2e/src/actor/Cargo.toml +14 -0
- package/e2e/src/actor/hello_actor.did +10 -0
- package/e2e/src/actor/src/lib.rs +73 -0
- package/e2e/src/calls.test.ts +132 -0
- package/e2e/src/declarations/hello_actor.envelope.json +175 -0
- package/e2e/src/declarations/hello_actor.ts +15 -0
- package/e2e/src/errors.test.ts +107 -0
- package/e2e/src/frontend-headers.test.ts +253 -0
- package/e2e/src/identity.test.ts +61 -0
- package/e2e/src/react.test.tsx +59 -0
- package/e2e/src/replica.ts +70 -0
- package/e2e/src/vite-env.test.ts +108 -0
- package/e2e/test.sh +47 -0
- package/e2e/tsconfig.json +20 -0
- package/e2e/tsconfig.typecheck.json +9 -0
- package/e2e/vitest.config.ts +22 -0
- package/eslint.config.mjs +100 -0
- package/evals/PREREGISTRATION.md +1346 -0
- package/evals/README.md +756 -0
- package/evals/conditions/thin/condition.json +8 -0
- package/evals/conditions/thin/docs/candid-core-cli.README.md +90 -0
- package/evals/conditions/thin/docs/candid-core-schema.README.md +339 -0
- package/evals/conditions/thin/package.json +25 -0
- package/evals/conditions/thin/starter/src/generated/icrc1.ts +57 -0
- package/evals/conditions/thin-guide/condition.json +10 -0
- package/evals/conditions/thin-guide/docs/llms.txt +292 -0
- package/evals/conditions/v3/condition.json +9 -0
- package/evals/conditions/v3/docs/ic-reactor-core.llms.txt +232 -0
- package/evals/conditions/v3/docs/ic-reactor-react.llms.txt +280 -0
- package/evals/conditions/v3/docs/llms-full.txt +1053 -0
- package/evals/conditions/v3/package.json +26 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.d.ts +95 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.js +140 -0
- package/evals/conditions/v4/PORTING.md +384 -0
- package/evals/conditions/v4/README.md +106 -0
- package/evals/conditions/v4/condition.json +5 -0
- package/evals/conditions/v4/docs/llms.txt +323 -0
- package/evals/conditions/v4/package.json +27 -0
- package/evals/conditions/v4/starter/src/generated/icrc1.ts +78 -0
- package/evals/conditions/v4-proto/condition.json +6 -0
- package/evals/conditions/v4-proto/docs/llms.txt +291 -0
- package/evals/conditions/v4-proto/lib/SHIM-NOTES.md +20 -0
- package/evals/conditions/v4-proto/lib/package.json +27 -0
- package/evals/conditions/v4-proto/lib/src/client.ts +471 -0
- package/evals/conditions/v4-proto/lib/src/errors.ts +214 -0
- package/evals/conditions/v4-proto/lib/src/index.ts +36 -0
- package/evals/conditions/v4-proto/lib/src/principal.ts +175 -0
- package/evals/conditions/v4-proto/lib/src/service.ts +69 -0
- package/evals/conditions/v4-proto/lib/src/types.ts +174 -0
- package/evals/conditions/v4-proto/lib/test/runtime.test.ts +170 -0
- package/evals/conditions/v4-proto/lib/test/tsconfig.json +10 -0
- package/evals/conditions/v4-proto/lib/test/types.test-d.ts +145 -0
- package/evals/conditions/v4-proto/lib/test/vitest.config.mjs +8 -0
- package/evals/conditions/v4-proto/lib/tsconfig.json +17 -0
- package/evals/conditions/v4-proto/package.json +26 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.service.ts +19 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.ts +57 -0
- package/evals/drive.mjs +1221 -0
- package/evals/gate.mjs +161 -0
- package/evals/harness/aggregate.mjs +534 -0
- package/evals/harness/aggregate.test.mjs +344 -0
- package/evals/harness/assemble.mjs +216 -0
- package/evals/harness/assemble.test.mjs +143 -0
- package/evals/harness/auth.mjs +91 -0
- package/evals/harness/check-docs.mjs +157 -0
- package/evals/harness/declarations/icrc1.did.d.ts +95 -0
- package/evals/harness/declarations/icrc1.did.js +140 -0
- package/evals/harness/drive-auth.test.mjs +584 -0
- package/evals/harness/drive.test.mjs +509 -0
- package/evals/harness/fake-auth.ts +67 -0
- package/evals/harness/fake-ledger.ts +178 -0
- package/evals/harness/gate-plan.mjs +104 -0
- package/evals/harness/gate-plan.test.mjs +243 -0
- package/evals/harness/icrc1.did +88 -0
- package/evals/harness/judge.mjs +63 -0
- package/evals/harness/judge.test.mjs +81 -0
- package/evals/harness/leak-scan.mjs +1373 -0
- package/evals/harness/leak-scan.test.mjs +1300 -0
- package/evals/harness/probes/amounts.ts +14 -0
- package/evals/harness/probes/node-tool.probe.ts +48 -0
- package/evals/harness/probes/probe.mjs +149 -0
- package/evals/harness/probes/react-wallet.probe.ts +96 -0
- package/evals/harness/public/support/auth.ts +38 -0
- package/evals/harness/public/support/fake-replica.d.ts +28 -0
- package/evals/harness/public/support/fake-replica.js +434 -0
- package/evals/harness/public/support/ledger.idl.d.ts +95 -0
- package/evals/harness/public/support/ledger.idl.js +140 -0
- package/evals/harness/public/support/world.ts +179 -0
- package/evals/harness/runs.mjs +139 -0
- package/evals/harness/sandbox.mjs +88 -0
- package/evals/harness/sandbox.test.mjs +134 -0
- package/evals/harness/ship.mjs +264 -0
- package/evals/harness/ship.test.mjs +552 -0
- package/evals/harness/world.ts +291 -0
- package/evals/package.json +26 -0
- package/evals/pnpm-lock.yaml +1537 -0
- package/evals/pnpm-workspace.yaml +12 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.json +384 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.rescored.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/plan.json +240 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.json +1800 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.json +1843 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.rescored.json +2100 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.rescored.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/plan.json +242 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.json +1843 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.rescored.json +2195 -0
- package/evals/results/2026-10-03-beta1-gate/pilot.json +162 -0
- package/evals/results/2026-10-03-beta1-gate/plan.json +141 -0
- package/evals/results/2026-10-03-beta1-gate/summary.json +983 -0
- package/evals/results/2026-10-05-ga-gate/contaminated-run.json +55 -0
- package/evals/results/2026-10-05-ga-gate/pilot.json +162 -0
- package/evals/results/2026-10-05-ga-gate/plan.json +457 -0
- package/evals/results/2026-10-05-ga-gate/summary.json +1037 -0
- package/evals/results/2026-10-05-ga-gate/summary.rescanned.json +1037 -0
- package/evals/results/README.md +31 -0
- package/evals/runs-pilot.log +150 -0
- package/evals/runs-pilot2.log +150 -0
- package/evals/score.mjs +256 -0
- package/evals/setup.mjs +547 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/src/index.ts +121 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/src/index.ts +116 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/src/index.ts +119 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/meta.json +10 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/src/index.ts +120 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/meta.json +11 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/src/index.ts +149 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/src/index.ts +71 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/src/index.ts +84 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/src/index.ts +84 -0
- package/evals/tasks/node-tool/hidden/node-tool.test.ts +312 -0
- package/evals/tasks/node-tool/prompt.md +55 -0
- package/evals/tasks/node-tool/prompt.minimal.md +30 -0
- package/evals/tasks/node-tool/solutions/thin/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/v3/reference/src/index.ts +140 -0
- package/evals/tasks/node-tool/solutions/v3/reference-module-scope/src/index.ts +153 -0
- package/evals/tasks/node-tool/solutions/v4/reference/src/index.ts +84 -0
- package/evals/tasks/node-tool/solutions/v4/reference-module-scope/src/index.ts +105 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference/src/index.ts +72 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference-module-scope/src/index.ts +87 -0
- package/evals/tasks/node-tool/starter/src/contract.ts +31 -0
- package/evals/tasks/node-tool/starter/src/index.ts +7 -0
- package/evals/tasks/node-tool/starter/test/smoke.test.ts +22 -0
- package/evals/tasks/node-tool/starter/tsconfig.json +17 -0
- package/evals/tasks/node-tool/task.json +63 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/src/Wallet.tsx +216 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/src/Wallet.tsx +212 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/src/Wallet.tsx +210 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/meta.json +8 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/src/Wallet.tsx +254 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/src/Wallet.tsx +215 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/src/Wallet.tsx +214 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/src/Wallet.tsx +225 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/meta.json +11 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/src/Wallet.tsx +182 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/src/Wallet.tsx +166 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/hidden/react-wallet.test.ts +536 -0
- package/evals/tasks/react-wallet/prompt.md +88 -0
- package/evals/tasks/react-wallet/prompt.minimal.md +51 -0
- package/evals/tasks/react-wallet/solutions/thin/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/v3/reference/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/solutions/v3/reference-module-scope/src/Wallet.tsx +230 -0
- package/evals/tasks/react-wallet/solutions/v4/reference/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/solutions/v4/reference-module-scope/src/Wallet.tsx +178 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference-module-scope/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/starter/src/Wallet.tsx +12 -0
- package/evals/tasks/react-wallet/starter/src/auth.ts +16 -0
- package/evals/tasks/react-wallet/starter/src/config.ts +14 -0
- package/evals/tasks/react-wallet/starter/src/ii-auth.ts +43 -0
- package/evals/tasks/react-wallet/starter/src/main.tsx +15 -0
- package/evals/tasks/react-wallet/starter/test/smoke.test.ts +34 -0
- package/evals/tasks/react-wallet/starter/tsconfig.json +18 -0
- package/evals/tasks/react-wallet/task.json +84 -0
- package/examples/canister-studio/.stackblitzrc +4 -0
- package/examples/canister-studio/README.md +183 -0
- package/examples/canister-studio/did/ckbtc_minter.did +806 -0
- package/examples/canister-studio/did/icp_ledger.did +586 -0
- package/examples/canister-studio/did/icrc1.did +88 -0
- package/examples/canister-studio/package.json +45 -0
- package/examples/canister-studio/scripts/smoke.ts +188 -0
- package/examples/canister-studio/src/calls/CacheKeyPanel.tsx +104 -0
- package/examples/canister-studio/src/calls/ConfirmWrite.tsx +265 -0
- package/examples/canister-studio/src/calls/ErrorPanel.test.tsx +113 -0
- package/examples/canister-studio/src/calls/ErrorPanel.tsx +214 -0
- package/examples/canister-studio/src/calls/MethodRunner.test.tsx +354 -0
- package/examples/canister-studio/src/calls/MethodRunner.tsx +57 -0
- package/examples/canister-studio/src/calls/ReadRunner.tsx +244 -0
- package/examples/canister-studio/src/calls/ResendPanel.tsx +165 -0
- package/examples/canister-studio/src/calls/ShareLink.tsx +59 -0
- package/examples/canister-studio/src/calls/WriteRunner.test.tsx +678 -0
- package/examples/canister-studio/src/calls/WriteRunner.tsx +670 -0
- package/examples/canister-studio/src/calls/dedupe.ts +189 -0
- package/examples/canister-studio/src/calls/error-info.ts +168 -0
- package/examples/canister-studio/src/calls/history.test.ts +284 -0
- package/examples/canister-studio/src/calls/history.ts +287 -0
- package/examples/canister-studio/src/calls/kinds.ts +68 -0
- package/examples/canister-studio/src/calls/prepare.ts +32 -0
- package/examples/canister-studio/src/calls/query-options.ts +30 -0
- package/examples/canister-studio/src/calls/read.ts +47 -0
- package/examples/canister-studio/src/calls/readback.ts +100 -0
- package/examples/canister-studio/src/calls/record.ts +123 -0
- package/examples/canister-studio/src/calls/safe-calls.test.ts +299 -0
- package/examples/canister-studio/src/calls/share.ts +46 -0
- package/examples/canister-studio/src/calls/testing/doubles.tsx +108 -0
- package/examples/canister-studio/src/calls/testing/fixtures.ts +185 -0
- package/examples/canister-studio/src/calls/useCanisterHandle.ts +35 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.envelope.json +4228 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.ts +868 -0
- package/examples/canister-studio/src/canisters/icp_ledger.envelope.json +3454 -0
- package/examples/canister-studio/src/canisters/icp_ledger.ts +584 -0
- package/examples/canister-studio/src/canisters/icrc1.envelope.json +840 -0
- package/examples/canister-studio/src/canisters/icrc1.ts +78 -0
- package/examples/canister-studio/src/components/ActionButton.tsx +32 -0
- package/examples/canister-studio/src/components/AppHeader.tsx +83 -0
- package/examples/canister-studio/src/components/AppShell.tsx +29 -0
- package/examples/canister-studio/src/components/AuthButton.tsx +79 -0
- package/examples/canister-studio/src/components/Badge.tsx +79 -0
- package/examples/canister-studio/src/components/CanisterHeader.tsx +200 -0
- package/examples/canister-studio/src/components/CanisterJump.tsx +130 -0
- package/examples/canister-studio/src/components/Card.tsx +64 -0
- package/examples/canister-studio/src/components/Code.tsx +101 -0
- package/examples/canister-studio/src/components/CopyButton.tsx +126 -0
- package/examples/canister-studio/src/components/Disclosure.tsx +34 -0
- package/examples/canister-studio/src/components/EmptyState.tsx +34 -0
- package/examples/canister-studio/src/components/MethodList.test.tsx +137 -0
- package/examples/canister-studio/src/components/MethodList.tsx +244 -0
- package/examples/canister-studio/src/components/Notice.tsx +36 -0
- package/examples/canister-studio/src/components/ProvenanceBadge.tsx +44 -0
- package/examples/canister-studio/src/components/RouteError.tsx +42 -0
- package/examples/canister-studio/src/components/SectionBoundary.tsx +76 -0
- package/examples/canister-studio/src/components/Spinner.tsx +33 -0
- package/examples/canister-studio/src/components/Tabs.tsx +96 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.test.tsx +56 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.tsx +165 -0
- package/examples/canister-studio/src/components/canister/CanisterNotFound.tsx +59 -0
- package/examples/canister-studio/src/components/canister/CanisterPending.tsx +33 -0
- package/examples/canister-studio/src/components/canister/MethodOverview.tsx +93 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.test.tsx +201 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.tsx +187 -0
- package/examples/canister-studio/src/components/canister/shared-call.test.ts +186 -0
- package/examples/canister-studio/src/components/canister/shared-call.ts +133 -0
- package/examples/canister-studio/src/components/canister/shared-link-loader.test.ts +116 -0
- package/examples/canister-studio/src/components/canister/use-canister-page.ts +44 -0
- package/examples/canister-studio/src/components/canister/workspace-key.test.ts +27 -0
- package/examples/canister-studio/src/components/canister/workspace-key.ts +36 -0
- package/examples/canister-studio/src/components/canister-id-input.ts +69 -0
- package/examples/canister-studio/src/components/canister-names.ts +11 -0
- package/examples/canister-studio/src/components/download.ts +34 -0
- package/examples/canister-studio/src/components/fallbacks.tsx +33 -0
- package/examples/canister-studio/src/components/helpers.test.ts +107 -0
- package/examples/canister-studio/src/components/history/HistoryList.tsx +284 -0
- package/examples/canister-studio/src/components/home/FeaturedLedger.tsx +149 -0
- package/examples/canister-studio/src/components/home/OpenCanister.tsx +99 -0
- package/examples/canister-studio/src/components/home/featured.test.tsx +92 -0
- package/examples/canister-studio/src/components/home/featured.ts +64 -0
- package/examples/canister-studio/src/components/interface/CandidTab.tsx +38 -0
- package/examples/canister-studio/src/components/interface/CompatReport.tsx +92 -0
- package/examples/canister-studio/src/components/interface/CompatibilityTab.tsx +190 -0
- package/examples/canister-studio/src/components/interface/ProjectTab.tsx +196 -0
- package/examples/canister-studio/src/components/interface/TypeScriptTab.tsx +114 -0
- package/examples/canister-studio/src/components/interface/tools.test.tsx +169 -0
- package/examples/canister-studio/src/components/modes.ts +31 -0
- package/examples/canister-studio/src/components/playground/MethodPicker.tsx +67 -0
- package/examples/canister-studio/src/components/playground/PlaygroundForms.tsx +112 -0
- package/examples/canister-studio/src/components/playground/PlaygroundTools.tsx +219 -0
- package/examples/canister-studio/src/components/playground/sample.ts +43 -0
- package/examples/canister-studio/src/components/playground/target.test.tsx +86 -0
- package/examples/canister-studio/src/components/primitives.test.tsx +121 -0
- package/examples/canister-studio/src/components/provenance.ts +38 -0
- package/examples/canister-studio/src/components/route-render.test.tsx +71 -0
- package/examples/canister-studio/src/components/use-task.ts +56 -0
- package/examples/canister-studio/src/components/wallet/BalancePanel.tsx +110 -0
- package/examples/canister-studio/src/components/wallet/LedgerSummary.tsx +63 -0
- package/examples/canister-studio/src/components/wallet/MinterPanel.tsx +255 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.test.tsx +297 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.tsx +446 -0
- package/examples/canister-studio/src/components/wallet/ledgers.test.ts +36 -0
- package/examples/canister-studio/src/components/wallet/ledgers.ts +111 -0
- package/examples/canister-studio/src/forms/JsonPanel.tsx +157 -0
- package/examples/canister-studio/src/forms/MethodForm.test.tsx +369 -0
- package/examples/canister-studio/src/forms/MethodForm.tsx +368 -0
- package/examples/canister-studio/src/forms/TypedForm.tsx +55 -0
- package/examples/canister-studio/src/forms/context.ts +62 -0
- package/examples/canister-studio/src/forms/convert.ts +131 -0
- package/examples/canister-studio/src/forms/edge.test.ts +252 -0
- package/examples/canister-studio/src/forms/engine.test.ts +358 -0
- package/examples/canister-studio/src/forms/engine.ts +1492 -0
- package/examples/canister-studio/src/forms/fields/BytesField.tsx +210 -0
- package/examples/canister-studio/src/forms/fields/Field.tsx +106 -0
- package/examples/canister-studio/src/forms/fields/FieldShell.tsx +101 -0
- package/examples/canister-studio/src/forms/fields/Group.tsx +74 -0
- package/examples/canister-studio/src/forms/fields/ListField.tsx +256 -0
- package/examples/canister-studio/src/forms/fields/Segmented.tsx +78 -0
- package/examples/canister-studio/src/forms/fields/TimestampField.tsx +108 -0
- package/examples/canister-studio/src/forms/fields/UnitsField.tsx +119 -0
- package/examples/canister-studio/src/forms/fields/composites.tsx +389 -0
- package/examples/canister-studio/src/forms/fields/leaves.tsx +350 -0
- package/examples/canister-studio/src/forms/fields.test.tsx +396 -0
- package/examples/canister-studio/src/forms/helpers.test.ts +225 -0
- package/examples/canister-studio/src/forms/hints.ts +128 -0
- package/examples/canister-studio/src/forms/json.ts +119 -0
- package/examples/canister-studio/src/forms/parsing.test.ts +217 -0
- package/examples/canister-studio/src/forms/useMethodForm.ts +251 -0
- package/examples/canister-studio/src/interface/browser.test.ts +23 -0
- package/examples/canister-studio/src/interface/browser.ts +153 -0
- package/examples/canister-studio/src/interface/cache.ts +68 -0
- package/examples/canister-studio/src/interface/cli-cache.test.ts +155 -0
- package/examples/canister-studio/src/interface/cli-cache.ts +125 -0
- package/examples/canister-studio/src/interface/compile.server.test.ts +198 -0
- package/examples/canister-studio/src/interface/compile.server.ts +262 -0
- package/examples/canister-studio/src/interface/compile.test.ts +108 -0
- package/examples/canister-studio/src/interface/compile.ts +305 -0
- package/examples/canister-studio/src/interface/docs.test.ts +75 -0
- package/examples/canister-studio/src/interface/docs.ts +100 -0
- package/examples/canister-studio/src/interface/functions.test.ts +42 -0
- package/examples/canister-studio/src/interface/functions.ts +200 -0
- package/examples/canister-studio/src/interface/live-candid.server.ts +91 -0
- package/examples/canister-studio/src/interface/live-loader.test.ts +515 -0
- package/examples/canister-studio/src/interface/live-loader.ts +364 -0
- package/examples/canister-studio/src/interface/load.test.ts +328 -0
- package/examples/canister-studio/src/interface/load.ts +249 -0
- package/examples/canister-studio/src/interface/read-candid.test.ts +185 -0
- package/examples/canister-studio/src/interface/read-candid.ts +233 -0
- package/examples/canister-studio/src/interface/server-result.ts +42 -0
- package/examples/canister-studio/src/interface/typed.ts +68 -0
- package/examples/canister-studio/src/interface/types.ts +121 -0
- package/examples/canister-studio/src/lib/format.test.ts +107 -0
- package/examples/canister-studio/src/lib/format.ts +139 -0
- package/examples/canister-studio/src/lib/hex.test.ts +43 -0
- package/examples/canister-studio/src/lib/hex.ts +33 -0
- package/examples/canister-studio/src/lib/ids.test.ts +70 -0
- package/examples/canister-studio/src/lib/ids.ts +93 -0
- package/examples/canister-studio/src/lib/reactor.test.ts +55 -0
- package/examples/canister-studio/src/lib/reactor.ts +55 -0
- package/examples/canister-studio/src/lib/registry.test.ts +74 -0
- package/examples/canister-studio/src/lib/registry.ts +238 -0
- package/examples/canister-studio/src/lib/schema.ts +148 -0
- package/examples/canister-studio/src/routeTree.gen.ts +227 -0
- package/examples/canister-studio/src/router.test.ts +290 -0
- package/examples/canister-studio/src/router.tsx +127 -0
- package/examples/canister-studio/src/routes/__root.tsx +58 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.index.tsx +172 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.interface.tsx +98 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.tsx +91 -0
- package/examples/canister-studio/src/routes/history.tsx +55 -0
- package/examples/canister-studio/src/routes/index.tsx +169 -0
- package/examples/canister-studio/src/routes/playground.tsx +286 -0
- package/examples/canister-studio/src/routes/safety.tsx +57 -0
- package/examples/canister-studio/src/routes/wallet.tsx +213 -0
- package/examples/canister-studio/src/safety/LabTransferForm.tsx +84 -0
- package/examples/canister-studio/src/safety/RequestLog.tsx +79 -0
- package/examples/canister-studio/src/safety/SafetyLab.test.tsx +89 -0
- package/examples/canister-studio/src/safety/SafetyLab.tsx +140 -0
- package/examples/canister-studio/src/safety/ScenarioCard.tsx +262 -0
- package/examples/canister-studio/src/safety/lab-run.ts +283 -0
- package/examples/canister-studio/src/safety/mock-ledger.test.ts +99 -0
- package/examples/canister-studio/src/safety/mock-ledger.ts +263 -0
- package/examples/canister-studio/src/safety/scenarios.test.ts +157 -0
- package/examples/canister-studio/src/safety/scenarios.ts +924 -0
- package/examples/canister-studio/src/styles.css +3453 -0
- package/examples/canister-studio/src/styles.test.ts +46 -0
- package/examples/canister-studio/src/values/CallPreview.test.tsx +86 -0
- package/examples/canister-studio/src/values/CallPreview.tsx +148 -0
- package/examples/canister-studio/src/values/ErrValueView.tsx +66 -0
- package/examples/canister-studio/src/values/Inspector.tsx +133 -0
- package/examples/canister-studio/src/values/ReplyView.test.tsx +341 -0
- package/examples/canister-studio/src/values/ReplyView.tsx +164 -0
- package/examples/canister-studio/src/values/candid-text.test.ts +516 -0
- package/examples/canister-studio/src/values/candid-text.ts +479 -0
- package/examples/canister-studio/src/values/hints.ts +58 -0
- package/examples/canister-studio/src/values/json.test.ts +66 -0
- package/examples/canister-studio/src/values/json.ts +31 -0
- package/examples/canister-studio/src/values/leaves.tsx +309 -0
- package/examples/canister-studio/src/values/schema.test.ts +123 -0
- package/examples/canister-studio/src/values/schema.ts +78 -0
- package/examples/canister-studio/src/values/test-router.tsx +21 -0
- package/examples/canister-studio/src/values/tree.tsx +670 -0
- package/examples/canister-studio/src/values/use-sha256.ts +29 -0
- package/examples/canister-studio/tsconfig.json +20 -0
- package/examples/canister-studio/vite.config.ts +50 -0
- package/examples/canister-studio/vitest.config.ts +20 -0
- package/examples/icrc-ledger/.stackblitzrc +4 -0
- package/examples/icrc-ledger/README.md +100 -0
- package/examples/icrc-ledger/ckbtc_minter.did +12 -0
- package/examples/icrc-ledger/icp_ledger.did +147 -0
- package/examples/icrc-ledger/icrc1.did +88 -0
- package/examples/icrc-ledger/index.html +13 -0
- package/examples/icrc-ledger/package.json +40 -0
- package/examples/icrc-ledger/scripts/smoke.ts +151 -0
- package/examples/icrc-ledger/src/App.tsx +71 -0
- package/examples/icrc-ledger/src/ErrorPanel.tsx +105 -0
- package/examples/icrc-ledger/src/MainnetTab.tsx +450 -0
- package/examples/icrc-ledger/src/SandboxTab.test.tsx +379 -0
- package/examples/icrc-ledger/src/SandboxTab.tsx +761 -0
- package/examples/icrc-ledger/src/blocks.test.ts +247 -0
- package/examples/icrc-ledger/src/blocks.ts +140 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.envelope.json +96 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.ts +16 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.envelope.json +690 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.ts +223 -0
- package/examples/icrc-ledger/src/canisters/icrc1.envelope.json +840 -0
- package/examples/icrc-ledger/src/canisters/icrc1.ts +78 -0
- package/examples/icrc-ledger/src/format.test.ts +41 -0
- package/examples/icrc-ledger/src/format.ts +68 -0
- package/examples/icrc-ledger/src/ledger.ts +15 -0
- package/examples/icrc-ledger/src/main.tsx +10 -0
- package/examples/icrc-ledger/src/minter.ts +16 -0
- package/examples/icrc-ledger/src/sandbox.test.ts +464 -0
- package/examples/icrc-ledger/src/sandbox.ts +281 -0
- package/examples/icrc-ledger/src/styles.css +391 -0
- package/examples/icrc-ledger/src/transfer-form.test.ts +62 -0
- package/examples/icrc-ledger/src/transfer-form.ts +97 -0
- package/examples/icrc-ledger/tsconfig.json +19 -0
- package/examples/icrc-ledger/vite.config.ts +44 -0
- package/examples/icrc-ledger/vitest.config.ts +12 -0
- package/examples/next-ssr/.stackblitzrc +4 -0
- package/examples/next-ssr/README.md +112 -0
- package/examples/next-ssr/icrc1.did +88 -0
- package/examples/next-ssr/next.config.ts +10 -0
- package/examples/next-ssr/package.json +41 -0
- package/examples/next-ssr/scripts/smoke.ts +169 -0
- package/examples/next-ssr/src/app/account/CertifiedBalances.tsx +58 -0
- package/examples/next-ssr/src/app/account/account.test.tsx +165 -0
- package/examples/next-ssr/src/app/account/page.tsx +82 -0
- package/examples/next-ssr/src/app/api/balance/[ledger]/[principal]/route.ts +10 -0
- package/examples/next-ssr/src/app/globals.css +278 -0
- package/examples/next-ssr/src/app/layout.tsx +34 -0
- package/examples/next-ssr/src/app/page.tsx +46 -0
- package/examples/next-ssr/src/app/providers.browser.test.tsx +127 -0
- package/examples/next-ssr/src/app/providers.test.tsx +73 -0
- package/examples/next-ssr/src/app/providers.tsx +60 -0
- package/examples/next-ssr/src/canisters/icrc1.envelope.json +840 -0
- package/examples/next-ssr/src/canisters/icrc1.ts +78 -0
- package/examples/next-ssr/src/components/BalanceTable.tsx +43 -0
- package/examples/next-ssr/src/components/LedgerCard.test.tsx +96 -0
- package/examples/next-ssr/src/components/LedgerCard.tsx +230 -0
- package/examples/next-ssr/src/components/LedgerError.tsx +32 -0
- package/examples/next-ssr/src/components/LedgerSections.tsx +22 -0
- package/examples/next-ssr/src/components/MyBalances.test.tsx +253 -0
- package/examples/next-ssr/src/components/MyBalances.tsx +129 -0
- package/examples/next-ssr/src/components/SessionBadge.tsx +18 -0
- package/examples/next-ssr/src/components/hydration.test.tsx +259 -0
- package/examples/next-ssr/src/components/server-data.ts +10 -0
- package/examples/next-ssr/src/format.ts +46 -0
- package/examples/next-ssr/src/ledgers.ts +38 -0
- package/examples/next-ssr/src/server/balance-route.test.ts +151 -0
- package/examples/next-ssr/src/server/balance-route.ts +157 -0
- package/examples/next-ssr/src/server/error-summary.test.ts +34 -0
- package/examples/next-ssr/src/server/error-summary.ts +37 -0
- package/examples/next-ssr/src/server/parse-owner.test.ts +39 -0
- package/examples/next-ssr/src/server/parse-owner.ts +34 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.test.ts +137 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.ts +70 -0
- package/examples/next-ssr/src/server/read-balances.test.ts +103 -0
- package/examples/next-ssr/src/server/read-balances.ts +78 -0
- package/examples/next-ssr/src/server/request-client.rsc.test.tsx +110 -0
- package/examples/next-ssr/src/server/request-client.ts +24 -0
- package/examples/next-ssr/src/testing/mock-ledgers.ts +115 -0
- package/examples/next-ssr/src/testing/react-server-dom-turbopack.d.ts +15 -0
- package/examples/next-ssr/src/testing/setup.ts +10 -0
- package/examples/next-ssr/tsconfig.json +31 -0
- package/examples/next-ssr/vitest.config.ts +59 -0
- package/examples/node-agent-tool/.stackblitzrc +4 -0
- package/examples/node-agent-tool/README.md +165 -0
- package/examples/node-agent-tool/icp.yaml +11 -0
- package/examples/node-agent-tool/icrc1.did +88 -0
- package/examples/node-agent-tool/package.json +32 -0
- package/examples/node-agent-tool/src/canisters/icrc1.envelope.json +840 -0
- package/examples/node-agent-tool/src/canisters/icrc1.ts +78 -0
- package/examples/node-agent-tool/src/cli.test.ts +64 -0
- package/examples/node-agent-tool/src/cli.ts +23 -0
- package/examples/node-agent-tool/src/command-line.ts +159 -0
- package/examples/node-agent-tool/src/commands/balance.ts +73 -0
- package/examples/node-agent-tool/src/commands/demo.test.ts +51 -0
- package/examples/node-agent-tool/src/commands/demo.ts +359 -0
- package/examples/node-agent-tool/src/commands/info.ts +69 -0
- package/examples/node-agent-tool/src/commands/reads.test.ts +133 -0
- package/examples/node-agent-tool/src/commands/resend.test.ts +363 -0
- package/examples/node-agent-tool/src/commands/transfer.test.ts +313 -0
- package/examples/node-agent-tool/src/commands/transfer.ts +415 -0
- package/examples/node-agent-tool/src/commands/watch.test.ts +159 -0
- package/examples/node-agent-tool/src/commands/watch.ts +135 -0
- package/examples/node-agent-tool/src/commands/whoami.ts +36 -0
- package/examples/node-agent-tool/src/context.ts +20 -0
- package/examples/node-agent-tool/src/dispatch.ts +70 -0
- package/examples/node-agent-tool/src/failure.test.ts +59 -0
- package/examples/node-agent-tool/src/failure.ts +176 -0
- package/examples/node-agent-tool/src/identity.test.ts +242 -0
- package/examples/node-agent-tool/src/identity.ts +228 -0
- package/examples/node-agent-tool/src/input.ts +110 -0
- package/examples/node-agent-tool/src/ledgers.test.ts +74 -0
- package/examples/node-agent-tool/src/ledgers.ts +62 -0
- package/examples/node-agent-tool/src/mock-ledger.ts +211 -0
- package/examples/node-agent-tool/src/network.test.ts +128 -0
- package/examples/node-agent-tool/src/network.ts +101 -0
- package/examples/node-agent-tool/src/output.test.ts +208 -0
- package/examples/node-agent-tool/src/output.ts +163 -0
- package/examples/node-agent-tool/src/run.ts +73 -0
- package/examples/node-agent-tool/src/test-kit.ts +167 -0
- package/examples/node-agent-tool/tsconfig.json +18 -0
- package/examples/node-agent-tool/vitest.config.ts +8 -0
- package/examples/vite-wallet/.stackblitzrc +4 -0
- package/examples/vite-wallet/Cargo.lock +646 -0
- package/examples/vite-wallet/Cargo.toml +5 -0
- package/examples/vite-wallet/README.md +107 -0
- package/examples/vite-wallet/backend/Cargo.toml +12 -0
- package/examples/vite-wallet/backend/backend.did +37 -0
- package/examples/vite-wallet/backend/src/lib.rs +133 -0
- package/examples/vite-wallet/icp.yaml +44 -0
- package/examples/vite-wallet/index.html +13 -0
- package/examples/vite-wallet/ledger.did +54 -0
- package/examples/vite-wallet/package.json +46 -0
- package/examples/vite-wallet/public/.ic-assets.json5 +26 -0
- package/examples/vite-wallet/scripts/faucet-args.test.ts +30 -0
- package/examples/vite-wallet/scripts/faucet-args.ts +41 -0
- package/examples/vite-wallet/scripts/faucet.ts +41 -0
- package/examples/vite-wallet/src/AddressBook.test.tsx +191 -0
- package/examples/vite-wallet/src/AddressBook.tsx +147 -0
- package/examples/vite-wallet/src/App.tsx +53 -0
- package/examples/vite-wallet/src/Balance.test.tsx +57 -0
- package/examples/vite-wallet/src/Balance.tsx +58 -0
- package/examples/vite-wallet/src/EnvironmentPanel.test.tsx +83 -0
- package/examples/vite-wallet/src/EnvironmentPanel.tsx +103 -0
- package/examples/vite-wallet/src/ErrorNote.tsx +70 -0
- package/examples/vite-wallet/src/Profile.test.tsx +145 -0
- package/examples/vite-wallet/src/Profile.tsx +92 -0
- package/examples/vite-wallet/src/SendIcp.test.tsx +231 -0
- package/examples/vite-wallet/src/SendIcp.tsx +244 -0
- package/examples/vite-wallet/src/SendToContact.test.tsx +113 -0
- package/examples/vite-wallet/src/SendToContact.tsx +102 -0
- package/examples/vite-wallet/src/SignIn.test.tsx +106 -0
- package/examples/vite-wallet/src/SignIn.tsx +120 -0
- package/examples/vite-wallet/src/Wallet.test.tsx +89 -0
- package/examples/vite-wallet/src/account-switch.test.tsx +108 -0
- package/examples/vite-wallet/src/auth/dev-accounts.test.ts +113 -0
- package/examples/vite-wallet/src/auth/dev-accounts.ts +216 -0
- package/examples/vite-wallet/src/auth/local-page.test.ts +28 -0
- package/examples/vite-wallet/src/auth/local-page.ts +20 -0
- package/examples/vite-wallet/src/auth/wallet-auth.test.ts +159 -0
- package/examples/vite-wallet/src/auth/wallet-auth.ts +131 -0
- package/examples/vite-wallet/src/canisters/backend.envelope.json +275 -0
- package/examples/vite-wallet/src/canisters/backend.ts +58 -0
- package/examples/vite-wallet/src/canisters/ledger.envelope.json +451 -0
- package/examples/vite-wallet/src/canisters/ledger.ts +56 -0
- package/examples/vite-wallet/src/client.ts +45 -0
- package/examples/vite-wallet/src/main.tsx +18 -0
- package/examples/vite-wallet/src/optimistic-contacts.ts +113 -0
- package/examples/vite-wallet/src/refusal.ts +17 -0
- package/examples/vite-wallet/src/styles.css +282 -0
- package/examples/vite-wallet/src/test/test-wallet.tsx +322 -0
- package/examples/vite-wallet/src/token.ts +50 -0
- package/examples/vite-wallet/src/transfer.test.ts +178 -0
- package/examples/vite-wallet/src/transfer.ts +110 -0
- package/examples/vite-wallet/src/use-canisters.ts +49 -0
- package/examples/vite-wallet/tsconfig.json +19 -0
- package/examples/vite-wallet/vite.config.ts +47 -0
- package/examples/vite-wallet/vitest.config.ts +17 -0
- package/package.json +9 -4
- package/packages/core/.size-limit.js +47 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +84 -0
- package/packages/core/llms.txt +610 -0
- package/packages/core/package.json +89 -0
- package/packages/core/src/call.ts +961 -0
- package/packages/core/src/canister.ts +259 -0
- package/packages/core/src/client.ts +2061 -0
- package/packages/core/src/errors.ts +1079 -0
- package/packages/core/src/hydration.ts +239 -0
- package/packages/core/src/index.ts +23 -0
- package/packages/core/src/keys.ts +108 -0
- package/packages/core/src/management.ts +138 -0
- package/packages/core/src/network.ts +1020 -0
- package/packages/core/src/options.ts +1000 -0
- package/packages/core/src/runtime.ts +50 -0
- package/packages/core/src/testing/fake-replica.ts +1166 -0
- package/packages/core/src/testing/index.ts +16 -0
- package/packages/core/src/testing/test-auth.ts +340 -0
- package/packages/core/src/testing/test-client.ts +559 -0
- package/packages/core/src/types.ts +541 -0
- package/packages/core/src/units.ts +299 -0
- package/packages/core/src/version.ts +4 -0
- package/packages/core/tests/canister-helpers.ts +187 -0
- package/packages/core/tests/canister-id.test.ts +158 -0
- package/packages/core/tests/canister-unwrap.test.ts +94 -0
- package/packages/core/tests/canister.test-d.ts +406 -0
- package/packages/core/tests/canister.test.ts +1921 -0
- package/packages/core/tests/client-account-change.test.ts +131 -0
- package/packages/core/tests/client-agents.test.ts +402 -0
- package/packages/core/tests/client-auth-network.test.ts +719 -0
- package/packages/core/tests/client-disposed.test.ts +403 -0
- package/packages/core/tests/client-helpers.ts +136 -0
- package/packages/core/tests/client-hydration.test.ts +166 -0
- package/packages/core/tests/client-pinned.test.ts +626 -0
- package/packages/core/tests/client-session.test.ts +348 -0
- package/packages/core/tests/client.test-d.ts +181 -0
- package/packages/core/tests/client.test.ts +1162 -0
- package/packages/core/tests/encode.test.ts +171 -0
- package/packages/core/tests/env-canister-id.test.ts +666 -0
- package/packages/core/tests/env-cookie-copies.test.ts +182 -0
- package/packages/core/tests/error-guard.test.ts +222 -0
- package/packages/core/tests/errors.test-d.ts +280 -0
- package/packages/core/tests/errors.test.ts +1359 -0
- package/packages/core/tests/fixtures/README.md +28 -0
- package/packages/core/tests/fixtures/accounts.did +21 -0
- package/packages/core/tests/fixtures/accounts.envelope.json +367 -0
- package/packages/core/tests/fixtures/accounts.ts +26 -0
- package/packages/core/tests/fixtures/archive.did +31 -0
- package/packages/core/tests/fixtures/archive.envelope.json +259 -0
- package/packages/core/tests/fixtures/archive.ts +55 -0
- package/packages/core/tests/fixtures/icrc1.did +88 -0
- package/packages/core/tests/fixtures/icrc1.envelope.json +840 -0
- package/packages/core/tests/fixtures/icrc1.ts +78 -0
- package/packages/core/tests/fixtures/management.did +724 -0
- package/packages/core/tests/fixtures/management.envelope.json +4455 -0
- package/packages/core/tests/fixtures/management.ts +486 -0
- package/packages/core/tests/fixtures/shapes.did +33 -0
- package/packages/core/tests/fixtures/shapes.envelope.json +440 -0
- package/packages/core/tests/fixtures/shapes.ts +56 -0
- package/packages/core/tests/fixtures/skippable.did +15 -0
- package/packages/core/tests/fixtures/skippable.envelope.json +182 -0
- package/packages/core/tests/fixtures/skippable.ts +33 -0
- package/packages/core/tests/func.test.ts +154 -0
- package/packages/core/tests/identity-attributes.test.ts +214 -0
- package/packages/core/tests/keys.test.ts +289 -0
- package/packages/core/tests/management.test.ts +206 -0
- package/packages/core/tests/network-helpers.ts +121 -0
- package/packages/core/tests/network.test-d.ts +78 -0
- package/packages/core/tests/network.test.ts +1051 -0
- package/packages/core/tests/options.test.ts +1594 -0
- package/packages/core/tests/redirect-sign-in.test.ts +393 -0
- package/packages/core/tests/resend.test-d.ts +81 -0
- package/packages/core/tests/resend.test.ts +723 -0
- package/packages/core/tests/skip.test-d.ts +125 -0
- package/packages/core/tests/testing-entry.test.ts +73 -0
- package/packages/core/tests/testing-fake-replica.test.ts +1242 -0
- package/packages/core/tests/testing-test-auth.test-d.ts +84 -0
- package/packages/core/tests/testing-test-auth.test.ts +483 -0
- package/packages/core/tests/testing-test-client.test-d.ts +184 -0
- package/packages/core/tests/testing-test-client.test.ts +1272 -0
- package/packages/core/tests/time-realm.test.ts +63 -0
- package/packages/core/tests/time.test.ts +238 -0
- package/packages/core/tests/traps/README.md +39 -0
- package/packages/core/tests/traps/anonymous-write.test.ts +145 -0
- package/packages/core/tests/traps/classification-table.test.ts +256 -0
- package/packages/core/tests/traps/err-arm.test.ts +144 -0
- package/packages/core/tests/traps/fetch-spy.ts +39 -0
- package/packages/core/tests/traps/invalidation-after-write.test.ts +178 -0
- package/packages/core/tests/traps/ledger.ts +248 -0
- package/packages/core/tests/traps/root-key-off-local.test.ts +130 -0
- package/packages/core/tests/traps/stale-data-across-users.test.ts +173 -0
- package/packages/core/tests/traps/unresolved-name.test.ts +155 -0
- package/packages/core/tests/traps/update-as-query.test.ts +244 -0
- package/packages/core/tests/traps/update-not-resent.test.ts +126 -0
- package/packages/core/tests/traps/update-resent-when-never-delivered.test.ts +159 -0
- package/packages/core/tests/traps.test-d.ts +476 -0
- package/packages/core/tests/units.test-d.ts +118 -0
- package/packages/core/tests/units.test.ts +634 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/core/tsconfig.typecheck.json +16 -0
- package/packages/core/vitest.config.ts +13 -0
- package/packages/mcp/LICENSE +21 -0
- package/packages/mcp/README.md +189 -0
- package/packages/mcp/package.json +62 -0
- package/packages/mcp/src/candid/candid-text.test.ts +455 -0
- package/packages/mcp/src/candid/candid-text.ts +501 -0
- package/packages/mcp/src/candid/contract.test.ts +398 -0
- package/packages/mcp/src/candid/formats.ts +155 -0
- package/packages/mcp/src/candid/hex.ts +29 -0
- package/packages/mcp/src/candid/json-schema.test.ts +758 -0
- package/packages/mcp/src/candid/json-schema.ts +454 -0
- package/packages/mcp/src/candid/json.test.ts +1207 -0
- package/packages/mcp/src/candid/json.ts +1053 -0
- package/packages/mcp/src/candid/schema-walk.ts +189 -0
- package/packages/mcp/src/candid/type-text.test.ts +179 -0
- package/packages/mcp/src/candid/type-text.ts +238 -0
- package/packages/mcp/src/cli.ts +134 -0
- package/packages/mcp/src/identity.ts +188 -0
- package/packages/mcp/src/interface/cache.test.ts +48 -0
- package/packages/mcp/src/interface/cache.ts +68 -0
- package/packages/mcp/src/interface/cli-cache.test.ts +150 -0
- package/packages/mcp/src/interface/cli-cache.ts +122 -0
- package/packages/mcp/src/interface/compile-worker.test.ts +246 -0
- package/packages/mcp/src/interface/compile-worker.ts +290 -0
- package/packages/mcp/src/interface/compile.test.ts +155 -0
- package/packages/mcp/src/interface/compile.ts +281 -0
- package/packages/mcp/src/interface/docs.test.ts +105 -0
- package/packages/mcp/src/interface/docs.ts +86 -0
- package/packages/mcp/src/interface/fixtures/icrc1.did +88 -0
- package/packages/mcp/src/interface/hex.ts +8 -0
- package/packages/mcp/src/interface/ids.test.ts +71 -0
- package/packages/mcp/src/interface/ids.ts +87 -0
- package/packages/mcp/src/interface/loader.test.ts +804 -0
- package/packages/mcp/src/interface/loader.ts +519 -0
- package/packages/mcp/src/interface/read-candid.test.ts +300 -0
- package/packages/mcp/src/interface/read-candid.ts +259 -0
- package/packages/mcp/src/interface/types.ts +113 -0
- package/packages/mcp/src/network.ts +122 -0
- package/packages/mcp/src/options.ts +149 -0
- package/packages/mcp/src/server/approval.ts +159 -0
- package/packages/mcp/src/server/naming.ts +43 -0
- package/packages/mcp/src/server/results.ts +102 -0
- package/packages/mcp/src/server/server.ts +797 -0
- package/packages/mcp/src/server/tools.ts +116 -0
- package/packages/mcp/src/usage.ts +40 -0
- package/packages/mcp/tests/approval.test.ts +60 -0
- package/packages/mcp/tests/identity.test.ts +234 -0
- package/packages/mcp/tests/naming.test.ts +62 -0
- package/packages/mcp/tests/options.test.ts +140 -0
- package/packages/mcp/tests/server.test.ts +997 -0
- package/packages/mcp/tsconfig.json +19 -0
- package/packages/mcp/tsconfig.typecheck.json +10 -0
- package/packages/mcp/vitest.config.ts +10 -0
- package/packages/react/LICENSE +21 -0
- package/packages/react/README.md +328 -0
- package/packages/react/llms.txt +95 -0
- package/packages/react/package.json +95 -0
- package/packages/react/src/index.tsx +678 -0
- package/packages/react/tests/abandoned-render.test.tsx +816 -0
- package/packages/react/tests/auth.test.tsx +315 -0
- package/packages/react/tests/caller-keys.test.tsx +2334 -0
- package/packages/react/tests/client-time.test.ts +40 -0
- package/packages/react/tests/entry.test.ts +52 -0
- package/packages/react/tests/helpers.tsx +291 -0
- package/packages/react/tests/hydration.test.tsx +262 -0
- package/packages/react/tests/ownership.test.tsx +260 -0
- package/packages/react/tests/provider.server.test.tsx +169 -0
- package/packages/react/tests/query-client.test.tsx +107 -0
- package/packages/react/tests/strict-mode.test.tsx +288 -0
- package/packages/react/tests/suspense.test-d.tsx +357 -0
- package/packages/react/tests/two-query-cores.test.tsx +135 -0
- package/packages/react/tests/types.test-d.tsx +63 -0
- package/packages/react/tsconfig.json +23 -0
- package/packages/react/tsconfig.test.json +8 -0
- package/packages/react/tsconfig.typecheck.json +16 -0
- package/packages/react/vitest.config.ts +39 -0
- package/packages/vite-plugin/LICENSE +21 -0
- package/packages/vite-plugin/README.md +532 -0
- package/packages/vite-plugin/package.json +76 -0
- package/packages/vite-plugin/src/describe-value.test.ts +43 -0
- package/packages/vite-plugin/src/describe-value.ts +167 -0
- package/packages/vite-plugin/src/dev-environment.test.ts +747 -0
- package/packages/vite-plugin/src/dev-environment.ts +510 -0
- package/packages/vite-plugin/src/env.ts +431 -0
- package/packages/vite-plugin/src/fetch.ts +348 -0
- package/packages/vite-plugin/src/file-identity.ts +106 -0
- package/packages/vite-plugin/src/generate.ts +691 -0
- package/packages/vite-plugin/src/index.test.ts +1243 -0
- package/packages/vite-plugin/src/index.ts +2565 -0
- package/packages/vite-plugin/src/interface.ts +339 -0
- package/packages/vite-plugin/src/local-host.ts +34 -0
- package/packages/vite-plugin/src/options.ts +641 -0
- package/packages/vite-plugin/src/principal-text.test.ts +87 -0
- package/packages/vite-plugin/src/principal-text.ts +98 -0
- package/packages/vite-plugin/tests/close.test.ts +141 -0
- package/packages/vite-plugin/tests/environment-mode.test.ts +136 -0
- package/packages/vite-plugin/tests/fake-ic.ts +214 -0
- package/packages/vite-plugin/tests/fetch-sdk.test.ts +27 -0
- package/packages/vite-plugin/tests/fetch.test.ts +1196 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/bin.js +90 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/package.json +8 -0
- package/packages/vite-plugin/tests/fixtures/icrc1.did +88 -0
- package/packages/vite-plugin/tests/generation.test.ts +1036 -0
- package/packages/vite-plugin/tests/icp-command.test.ts +194 -0
- package/packages/vite-plugin/tests/icp-timeout.test.ts +83 -0
- package/packages/vite-plugin/tests/no-process-exit.test.ts +83 -0
- package/packages/vite-plugin/tests/on-generated.test.ts +323 -0
- package/packages/vite-plugin/tests/output-collision.test.ts +329 -0
- package/packages/vite-plugin/tests/package.test.ts +62 -0
- package/packages/vite-plugin/tests/project.test.ts +2993 -0
- package/packages/vite-plugin/tests/support.ts +339 -0
- package/packages/vite-plugin/tests/timeout.test.ts +107 -0
- package/packages/vite-plugin/tsconfig.json +23 -0
- package/packages/vite-plugin/tsconfig.typecheck.json +16 -0
- package/packages/vite-plugin/vitest.config.ts +9 -0
- package/pnpm-workspace.yaml +23 -0
- package/scripts/ai-context-files.js +72 -0
- package/scripts/assemble-docs-site.mjs +485 -0
- package/scripts/assemble-docs-site.test.mjs +568 -0
- package/scripts/build-examples.js +58 -0
- package/scripts/check-ai-context.js +963 -0
- package/scripts/check-ai-context.test.mjs +811 -0
- package/scripts/check-exports.mjs +767 -0
- package/scripts/check-exports.test.mjs +1136 -0
- package/scripts/check-snippets/ambient.d.ts +428 -0
- package/scripts/check-snippets/app/generated/backend.did +7 -0
- package/scripts/check-snippets/app/generated/backend.ts +15 -0
- package/scripts/check-snippets/app/generated/icrc1.did +88 -0
- package/scripts/check-snippets/app/generated/icrc1.ts +78 -0
- package/scripts/check-snippets/app/globals.ts +26 -0
- package/scripts/check-snippets/docs/guides/App.tsx +21 -0
- package/scripts/check-snippets/docs/guides/app/providers.tsx +23 -0
- package/scripts/check-snippets/docs/guides/auth-client.ts +47 -0
- package/scripts/check-snippets/docs/guides/canisters/backend.ts +3 -0
- package/scripts/check-snippets/docs/guides/canisters/icrc1.ts +3 -0
- package/scripts/check-snippets/docs/guides/components/LedgerCard.tsx +26 -0
- package/scripts/check-snippets/docs/guides/components/shell.tsx +11 -0
- package/scripts/check-snippets/docs/guides/components/sign-in-page.tsx +29 -0
- package/scripts/check-snippets/docs/guides/components/slow-section.tsx +5 -0
- package/scripts/check-snippets/docs/guides/generated/accounts.ts +95 -0
- package/scripts/check-snippets/docs/guides/generated/ckbtc_minter.ts +40 -0
- package/scripts/check-snippets/docs/guides/generated/icp_ledger.ts +267 -0
- package/scripts/check-snippets/docs/guides/ledger.ts +10 -0
- package/scripts/check-snippets/docs/guides/server/request-client.ts +8 -0
- package/scripts/check-snippets.mjs +993 -0
- package/scripts/check-snippets.test.mjs +338 -0
- package/scripts/check-stack.mjs +501 -0
- package/scripts/check-stack.test.mjs +800 -0
- package/scripts/clean-node-modules.js +67 -0
- package/scripts/example-projects.js +58 -0
- package/scripts/export-budget.mjs +145 -0
- package/scripts/faults.json +6749 -0
- package/scripts/lib/fault-workspace.mjs +210 -0
- package/scripts/lib/io.mjs +29 -0
- package/scripts/lib/io.test.mjs +38 -0
- package/scripts/lib/markdown-fences.mjs +56 -0
- package/scripts/lib/markdown-fences.test.mjs +84 -0
- package/scripts/lib/package-exports.mjs +54 -0
- package/scripts/lib/package-exports.test.mjs +66 -0
- package/scripts/lib/release-pr-steps.mjs +47 -0
- package/scripts/release-tag.mjs +95 -0
- package/scripts/release-tag.test.mjs +278 -0
- package/scripts/release-workflow.test.mjs +98 -0
- package/scripts/release.js +167 -0
- package/scripts/release.test.mjs +347 -0
- package/scripts/removed-v3-names.js +125 -0
- package/scripts/size-app/.size-limit.js +90 -0
- package/scripts/size-app/core.js +3 -0
- package/scripts/size-app/package.json +24 -0
- package/scripts/size-app/react.js +4 -0
- package/scripts/size-app.test.mjs +73 -0
- package/scripts/sync-ai-context-versions.js +102 -0
- package/scripts/sync-example-versions.js +151 -0
- package/scripts/test-examples-published.mjs +1585 -0
- package/scripts/test-examples-published.test.mjs +1586 -0
- package/scripts/traps/any-method-vars-not-a-list.json +5 -0
- package/scripts/traps/canister-method-unknown.json +5 -0
- package/scripts/traps/client-says-who-calls-once.json +5 -0
- package/scripts/traps/client-says-who-calls.json +5 -0
- package/scripts/traps/err-only-on-canister-err.json +14 -0
- package/scripts/traps/format-units-refuses-missing-value.json +5 -0
- package/scripts/traps/format-units-refuses-number.json +5 -0
- package/scripts/traps/format-units-unknown-option.json +5 -0
- package/scripts/traps/guard-err-is-the-methods.json +5 -0
- package/scripts/traps/guard-false-keeps-every-kind.json +5 -0
- package/scripts/traps/internals-not-exported.json +9 -0
- package/scripts/traps/network-refuses-unknown-name.json +5 -0
- package/scripts/traps/never-err-has-no-type.json +5 -0
- package/scripts/traps/options-method-unknown.json +5 -0
- package/scripts/traps/options-vars-wrong-type.json +5 -0
- package/scripts/traps/parse-units-refuses-number.json +5 -0
- package/scripts/traps/parse-units-unknown-option.json +5 -0
- package/scripts/traps/principal-is-not-plain-text.json +14 -0
- package/scripts/traps/query-options-refuses-oneway.json +5 -0
- package/scripts/traps/query-options-refuses-update.json +5 -0
- package/scripts/traps/refuse-next-unknown-option.json +5 -0
- package/scripts/traps/resend-key-reads-the-methods-arg.json +5 -0
- package/scripts/traps/resend-states-its-dedupe-key.json +5 -0
- package/scripts/traps/suspense-refuses-argument-skip.json +5 -0
- package/scripts/traps/suspense-refuses-skippable-read.json +5 -0
- package/scripts/traps/suspense-refuses-vars-skip-fits.json +5 -0
- package/scripts/traps/test-handler-method-unknown.json +5 -0
- package/scripts/traps/test-handler-nat-is-bigint.json +5 -0
- package/scripts/traps/test-handler-takes-the-domain-value.json +5 -0
- package/scripts/traps/vars-of-two-arguments-are-the-tuple.json +5 -0
- package/scripts/typecheck-examples.js +122 -0
- package/scripts/verify-faults.mjs +375 -0
- package/scripts/verify-faults.test.mjs +402 -0
- package/scripts/verify-package-artifacts.mjs +590 -0
- package/scripts/verify-peer-floors.js +399 -0
- package/scripts/verify-peer-floors.test.mjs +422 -0
- package/scripts/verify-test-fails-rules.mjs +104 -0
- package/scripts/verify-test-fails-rules.test.mjs +215 -0
- package/scripts/verify-test-fails.js +371 -0
- package/scripts/verify-traps.mjs +505 -0
- package/scripts/verify-traps.test.mjs +521 -0
- package/scripts/verify-typescript-floor.js +239 -0
- package/skill-packages/ic-reactor/SKILL.md +103 -0
- package/stack.json +8 -0
- package/tsconfig.base.json +23 -0
- 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
|
+
})
|