cabloy 5.1.112 → 5.1.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +2 -0
  3. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +13 -0
  4. package/.github/workflows/playwright-e2e.yml +34 -2
  5. package/.github/workflows/vona-test-mysql.yml +12 -0
  6. package/.github/workflows/vona-test-pg.yml +12 -0
  7. package/CHANGELOG.md +27 -0
  8. package/CLAUDE.md +6 -0
  9. package/cabloy-docs/backend/internal-aop-guide.md +35 -0
  10. package/cabloy-docs/backend/migration-and-changes.md +13 -9
  11. package/cabloy-docs/backend/unit-testing.md +97 -1
  12. package/e2e/scripts/startE2eVona.ts +51 -13
  13. package/e2e/specs/a-commerce/commerce.spec.ts +1 -0
  14. package/e2e/specs/cabloy-basic/basic.spec.ts +12 -7
  15. package/package.json +1 -1
  16. package/vona/packages-cli/cli/package.json +1 -1
  17. package/vona/packages-cli/cli-set-api/package.json +1 -1
  18. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.test.ts +6 -9
  19. package/vona/packages-cli/cli-set-api/toolsIsolate/test.ts +74 -32
  20. package/vona/packages-utils/compose/package.json +1 -1
  21. package/vona/packages-utils/compose/src/index.ts +19 -11
  22. package/vona/packages-vona/vona/package.json +1 -1
  23. package/vona/packages-vona/vona-core/package.json +1 -1
  24. package/vona/packages-vona/vona-core/src/lib/bean/beanContainer.ts +15 -3
  25. package/vona/packages-vona/vona-mock/package.json +1 -1
  26. package/vona/pnpm-lock.yaml +82 -67
  27. package/vona/src/backend/config/config/config.test.ts +1 -1
  28. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +8 -0
  29. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +1 -0
  30. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
  31. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
  32. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuAttribute.tsx +17 -0
  33. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/sku.tsx +10 -1
  34. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +234 -0
  35. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +6 -1
  36. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
  37. package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +228 -0
  38. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +15 -0
  39. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +35 -0
  40. package/vona/src/suite/a-commerce/modules/commerce-payment/src/dto/paymentAttemptView.tsx +26 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAttempt.tsx +34 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/paymentAttempt.ts +10 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +41 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +33 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-promotion/package.json +4 -2
  46. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +533 -2
  47. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/locales.ts +18 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.index.ts +24 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.version.ts +137 -0
  50. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/ssrMenu.couponTemplate.ts +30 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +37 -0
  52. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +37 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/coupon.ts +37 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/couponTemplate.ts +65 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponIssue.tsx +22 -0
  56. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponMineItem.tsx +32 -0
  57. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +43 -0
  58. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectReq.tsx +28 -0
  59. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectRes.tsx +11 -0
  60. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectResItem.tsx +39 -0
  61. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +19 -0
  62. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateView.tsx +25 -0
  63. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponAudit.tsx +73 -0
  64. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponGrant.tsx +83 -0
  65. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +82 -0
  66. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/index.ts +1 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponAudit.ts +10 -0
  68. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponGrant.ts +10 -0
  69. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponTemplate.ts +10 -0
  70. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +397 -0
  71. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/couponTemplate.ts +52 -0
  72. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +162 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-seed/src/bean/meta.version.ts +11 -13
  74. package/vona/src/suite/a-commerce/modules/commerce-trade/package.json +4 -1
  75. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +277 -0
  76. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/bean.scheduleOrderExpiry.ts +26 -0
  77. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +9 -0
  78. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +79 -0
  79. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +14 -1
  80. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +14 -1
  81. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +19 -0
  82. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutCreate.tsx +19 -0
  83. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutResult.tsx +32 -0
  84. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAddressSnapshot.tsx +33 -0
  85. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderCouponSnapshot.tsx +35 -0
  86. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderLineSkuAttributeSnapshot.tsx +15 -0
  87. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/order.tsx +90 -0
  88. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderAudit.tsx +36 -0
  89. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderLine.tsx +52 -0
  90. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockAudit.tsx +39 -7
  91. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockReservation.tsx +4 -1
  92. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +15 -0
  93. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderAudit.ts +10 -0
  94. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderLine.ts +10 -0
  95. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +556 -0
  96. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +32 -0
  97. package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +8 -3
  98. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +158 -0
  99. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +203 -0
  100. package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +163 -0
  101. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +274 -66
  102. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +282 -82
  103. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  104. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +10 -7
  105. package/vona/src/suite-vendor/a-image/package.json +1 -1
  106. package/vona/src/suite-vendor/a-vona/modules/a-aspect/package.json +1 -1
  107. package/vona/src/suite-vendor/a-vona/modules/a-aspect/src/types/aopMethod.ts +16 -3
  108. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +4 -2
  109. package/vona/src/suite-vendor/a-vona/modules/a-core/src/bean/bean.core.ts +6 -0
  110. package/vona/src/suite-vendor/a-vona/modules/a-core/src/index.ts +1 -0
  111. package/vona/src/suite-vendor/a-vona/modules/a-core/src/lib/core.ts +6 -0
  112. package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/fetch.ts +7 -0
  113. package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/index.ts +1 -0
  114. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  115. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/lib/utils.ts +5 -3
  116. package/vona/src/suite-vendor/a-vona/modules/a-retryable/package.json +50 -0
  117. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/index.ts +55 -0
  118. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/this.ts +2 -0
  119. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/bean/aopMethod.retryable.ts +45 -0
  120. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/index.ts +1 -0
  121. package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.build.json +11 -0
  122. package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.json +7 -0
  123. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  124. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +2 -2
  125. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  126. package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/version.ts +12 -12
  127. package/vona/src/suite-vendor/a-vona/modules/a-version/src/types/version.ts +4 -4
  128. package/vona/src/suite-vendor/a-vona/modules/a-vona/package.json +2 -1
  129. package/vona/src/suite-vendor/a-vona/package.json +2 -1
  130. package/vona/src/suite-vendor/a-vona/tsconfig.json +3 -0
  131. package/zova/packages-utils/zova-jsx/package.json +3 -3
  132. package/zova/packages-zova/zova/package.json +3 -3
  133. package/zova/packages-zova/zova-core/package.json +2 -2
  134. package/zova/pnpm-lock.yaml +14 -14
  135. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +0 -1
  136. package/zova/src/suite/a-commerce/modules/commerce-promotion/cli/openapi.config.ts +9 -0
  137. package/zova/src/suite/a-commerce/modules/commerce-promotion/package.json +2 -1
  138. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +115 -1
  139. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/commercePromotionCoupon.ts +24 -0
  140. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/baseURL.ts +5 -0
  141. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/index.ts +3 -0
  142. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +478 -0
  143. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +8187 -0
  144. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/apiSchema/commercePromotionCoupon.ts +13 -0
  145. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/model/coupon.ts +19 -0
  146. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
  147. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +30 -0
  148. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeCheckout.ts +27 -0
  149. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +92 -52
  150. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +1389 -848
  151. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeCheckout.ts +13 -0
  152. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +14 -0
  153. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +17 -7
  154. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.header.tsx +1 -1
  155. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +17 -7
  156. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +3 -3
  157. package/zova/src/suite-vendor/a-zova/package.json +2 -2
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.112
1
+ 5.1.114
@@ -172,6 +172,8 @@ For the canonical distinction, read [Backend Foundation](../../../cabloy-docs/ba
172
172
 
173
173
  ### Verification
174
174
 
175
+ For persisted test data, classify each record as a durable module seed or a test-local fixture. Durable seed data belongs in the owning module's `meta.version.ts` `seed()` hook, runs from a newly recreated managed database, and is read-only to tests; test-local resources must be tracked and deleted in `finally` in reverse dependency order.
176
+
175
177
  Check whether the feature needs:
176
178
 
177
179
  - unit tests
@@ -35,6 +35,19 @@ After generating or extending a backend thread, check which follow-up layers app
35
35
  - add a warm-query → mutate-source → repeat-query regression test for each new dependency path; read [Vona Cross-Model Query-Cache Dependencies](../../../../.docs-internal/architecture/vona-cross-model-query-cache-dependencies.md) for the source-backed decision rules
36
36
  - transaction behavior
37
37
 
38
+ ## Test resource lifecycle follow-up
39
+
40
+ - classify persisted test data as either an owning-module durable seed or a test-local fixture
41
+ - create durable test or local-development baseline data in the owning module's `meta.version.ts` `seed()` hook; this managed path starts from a newly recreated database
42
+ - treat durable seed data as read-only in tests; create an independent fixture when a scenario needs mutation
43
+ - retain exact IDs or entities for every test-local persisted resource and delete them from `finally`
44
+ - delete joins, children, derived records, and other dependents before their owners, in the same active tenant/instance scope
45
+ - do not use unscoped, timestamp-, prefix-, or broad business-condition deletion when an exact test-owned identity is available
46
+ - treat the application lifecycle as runner-owned; do not create or close the shared `app` in an individual test
47
+ - give each scoped test or operation its own `app.bean.executor.mockCtx(...)` boundary
48
+ - for a business race, give every contender a separate `mockCtx(...)`, launch the competing operations explicitly, wait for all branches to settle, and assert the combined durable state
49
+ - gate lock- or isolation-sensitive contention tests on supporting database capabilities; runner scheduling is never race-condition evidence
50
+
38
51
  ## Module composition and dependency intent
39
52
 
40
53
  - target module is already composed into the application when code uses cross-module scope lookup
@@ -4,7 +4,7 @@ on:
4
4
  branches:
5
5
  - main
6
6
  jobs:
7
- test:
7
+ basic-e2e:
8
8
  runs-on: ubuntu-latest
9
9
  env:
10
10
  DATABASE_DEFAULT_CLIENT: sqlite3
@@ -28,6 +28,38 @@ jobs:
28
28
  run: npx playwright install --with-deps chromium
29
29
  - name: run Basic E2E baseline
30
30
  run: npm run test:e2e:basic:clean
31
+ - name: upload Playwright artifacts
32
+ if: failure()
33
+ uses: actions/upload-artifact@v4
34
+ with:
35
+ name: playwright-artifacts-basic
36
+ path: |
37
+ playwright-report/
38
+ test-results/
39
+ if-no-files-found: ignore
40
+
41
+ commerce-e2e:
42
+ runs-on: ubuntu-latest
43
+ env:
44
+ DATABASE_DEFAULT_CLIENT: sqlite3
45
+ PNPM_CONFIG_MINIMUM_RELEASE_AGE: 0
46
+ services:
47
+ redis:
48
+ image: redis:latest
49
+ ports:
50
+ - 6379:6379
51
+ steps:
52
+ - uses: actions/checkout@v6
53
+ - uses: actions/setup-node@v6
54
+ with:
55
+ node-version: 24
56
+ - uses: pnpm/action-setup@v5
57
+ with:
58
+ version: 11.5.2
59
+ - name: init
60
+ run: npm run init
61
+ - name: install Chromium
62
+ run: npx playwright install --with-deps chromium
31
63
  - name: build Commerce Zova artifacts
32
64
  run: npm run build:zova:commerce
33
65
  - name: synchronize Vona dependencies
@@ -38,7 +70,7 @@ jobs:
38
70
  if: failure()
39
71
  uses: actions/upload-artifact@v4
40
72
  with:
41
- name: playwright-artifacts
73
+ name: playwright-artifacts-commerce
42
74
  path: |
43
75
  playwright-report/
44
76
  test-results/
@@ -34,6 +34,18 @@ jobs:
34
34
  version: 11.5.2
35
35
  - name: init
36
36
  run: npm run init
37
+ - name: run Commerce lifecycle gate
38
+ run: >-
39
+ DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run vona :bin:test --
40
+ commerce-trade/test/stockBalance.test.ts
41
+ commerce-trade/test/stockReservation.test.ts
42
+ commerce-trade/test/stockAudit.test.ts
43
+ commerce-promotion/test/couponReservation.test.ts
44
+ commerce-payment/test/paymentAttempt.test.ts
45
+ commerce-trade/test/checkoutReservation.test.ts
46
+ commerce-trade/test/reservationExpiry.test.ts
47
+ --flavor=normal
48
+ working-directory: vona
37
49
  - run: DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run test
38
50
  working-directory: vona
39
51
  - run: DATABASE_DEFAULT_CLIENT=mysql DATABASE_CLIENT_MYSQL_HOST=127.0.0.1 DATABASE_CLIENT_MYSQL_PASSWORD=root npm run build -- --flavor=ci
@@ -34,6 +34,18 @@ jobs:
34
34
  version: 11.5.2
35
35
  - name: init
36
36
  run: npm run init
37
+ - name: run Commerce stock contention gate
38
+ run: >-
39
+ DATABASE_DEFAULT_CLIENT=pg npm run vona :bin:test --
40
+ commerce-trade/test/stockBalance.test.ts
41
+ commerce-trade/test/stockReservation.test.ts
42
+ commerce-trade/test/stockAudit.test.ts
43
+ commerce-promotion/test/couponReservation.test.ts
44
+ commerce-payment/test/paymentAttempt.test.ts
45
+ commerce-trade/test/checkoutReservation.test.ts
46
+ commerce-trade/test/reservationExpiry.test.ts
47
+ --flavor=normal
48
+ working-directory: vona
37
49
  - run: DATABASE_DEFAULT_CLIENT=pg npm run test
38
50
  working-directory: vona
39
51
  - run: DATABASE_DEFAULT_CLIENT=pg npm run build -- --flavor=ci
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.114
4
+
5
+ ### Features
6
+
7
+ - Add retryable AOP infrastructure for Commerce transactions.
8
+ - Add a foundational Commerce coupon lifecycle.
9
+ - Add a foundational Commerce order snapshot capability.
10
+ - Add `ctx.state.fetch` support.
11
+ - Add checkout functionality.
12
+
13
+ ### Improvements
14
+
15
+ - Decorate Commerce transaction retries.
16
+
17
+ ## 5.1.113
18
+
19
+ ### Features
20
+
21
+ - Update application capabilities and workflows.
22
+
23
+ ### Improvements
24
+
25
+ - Rename the version test lifecycle to the seed lifecycle.
26
+ - Run Commerce end-to-end tests independently in CI.
27
+ - Strengthen Commerce catalog, stock balance, and stock reservation test coverage.
28
+ - Record Commerce stock verification and Phase 20 CI evidence.
29
+
3
30
  ## 5.1.112
4
31
 
5
32
  ### Features
package/CLAUDE.md CHANGED
@@ -72,6 +72,12 @@ Before inventing a custom implementation path:
72
72
  - In Vona, a tenant corresponds to an instance. Ordinary resource-model CRUD is automatically scoped to the active instance; treat records absent from that scope as absent, and do not use raw cross-instance probes merely to choose between `403` and not-found behavior. Model future multi-merchant boundaries explicitly inside an instance.
73
73
  - Model cross-Model query-cache dependencies as one directed, acyclic `modelsClear` / `modelsClearedBy` graph, and verify source mutations refresh warmed dependent queries; read `.docs-internal/architecture/vona-cross-model-query-cache-dependencies.md` before designing a nontrivial graph.
74
74
  - For `@Api.field(...)` and related schemaLike composition, framework guards now preserve previously attached OpenAPI metadata across schema rebuilds, but structure-shaping schemaLike is still order-sensitive. Treat `v.object(...)`, `v.array(...)`, `v.optional()`, `v.nullable()`, `v.default(...)`, and preprocess/transform wrappers as structure-shaping; keep the final structure-defining schemaLike last and verify emitted schema/OpenAPI output after such edits.
75
+ - `@Core.transaction(...)` defaults to `REQUIRED`: it starts a transaction only when none exists and otherwise joins the current datasource transaction without upgrading its isolation. Prefer it over manual `inTransaction` wrappers for atomic service methods; use `REQUIRES_NEW` only when an independently committed boundary is explicitly required.
76
+ - For replay-safe transient failures, use `@Core.retryable(...)` with an explicit `errorCodes` allowlist. It retries the downstream AOP suffix, so place it closest to the method when it must wrap and retry a `@Core.transaction(...)` boundary; use `ownerOnly: true` for dual-role leaves that must not retry inside a caller-owned transaction; do not retry external side effects or infer retryability from isolation level.
77
+ - Backend tests that use scoped Vona state must own an `app.bean.executor.mockCtx(...)` boundary; every intentionally competing operation must run in a separate `mockCtx(...)`.
78
+ - Do not set `TEST_CONCURRENCY=false` for normal `npm run test` or CI runs: an unset value defaults to concurrent execution. For stateful `node:test` suites, declare `{ concurrency: false }` explicitly. Prove business contention by explicitly launching competing operations and asserting their combined durable outcome, never through runner parallelism or scheduling.
79
+ - Unit tests must delete every test-owned persisted resource in `finally`, using precise owned identities and reverse dependency order.
80
+ - Shared durable test or local-development fixtures must be created through the owning module's `meta.version.ts` `seed()` hook and treated as read-only by tests; the managed seed path starts from a newly recreated database rather than repeating against one database.
75
81
 
76
82
  ## Verification expectations
77
83
 
@@ -82,6 +82,7 @@ Representative examples include:
82
82
 
83
83
  - `a-logger:log`
84
84
  - `a-orm:transaction`
85
+ - `a-retryable:retryable`
85
86
  - `a-caching:cachingGet`
86
87
  - `a-caching:cachingSet`
87
88
  - `a-caching:cachingDel`
@@ -92,9 +93,43 @@ These built-ins also expose shorthand decorators such as:
92
93
  ```typescript
93
94
  @Core.log({ level: 'info' })
94
95
  @Core.transaction({ isolationLevel: 'READ_COMMITTED', propagation: 'REQUIRED' })
96
+ @Core.retryable({
97
+ retries: 1,
98
+ minTimeout: 0,
99
+ maxTimeout: 0,
100
+ errorCodes: ['40001'],
101
+ })
95
102
  @Caching.get({ cacheName: 'module-name:xxx' })
96
103
  ```
97
104
 
105
+ `@Core.retryable(...)` retries only errors whose string `code` is explicitly listed in `errorCodes`. It replays the downstream AOP suffix; to retry a complete transaction, place `@Core.retryable(...)` closest to the method so it runs outside the transaction decorator:
106
+
107
+ ```typescript
108
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
109
+ @Core.retryable({ retries: 1, minTimeout: 0, maxTimeout: 0, errorCodes: ['40001'] })
110
+ async reserve() {
111
+ // transactional writes
112
+ }
113
+ ```
114
+
115
+ Use `ownerOnly: true` for a dual-role leaf method that may either own a transaction when called directly or join an aggregate transaction owned by its caller. It retries only when no current-datasource transaction existed when the method began; when nested, it executes once and lets the aggregate owner decide whether to replay the complete unit of work:
116
+
117
+ ```typescript
118
+ @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
119
+ @Core.retryable({
120
+ retries: 1,
121
+ minTimeout: 0,
122
+ maxTimeout: 0,
123
+ errorCodes: ['40001'],
124
+ ownerOnly: true,
125
+ })
126
+ async reserveLeaf() {
127
+ // transactional writes
128
+ }
129
+ ```
130
+
131
+ Retried methods must be replay-safe: defer external side effects until commit, and do not use this helper to imply cross-datasource or independent `REQUIRES_NEW` retry semantics.
132
+
98
133
  For the broader logger-client, rotation, and level model behind `@Core.log(...)`, see [Logger Guide](/backend/logger-guide).
99
134
 
100
135
  ## Magic Method
@@ -73,19 +73,19 @@ Three migration scenarios are defined:
73
73
  | -------- | ------------------------------------------------ |
74
74
  | `update` | schema evolution such as tables and fields |
75
75
  | `init` | instance- or tenant-specific initialization data |
76
- | `test` | test-only data for the test environment |
76
+ | `seed` | shared baseline data for the test environment |
77
77
 
78
78
  This split is one of the most important Vona migration ideas because it separates:
79
79
 
80
80
  - structural change
81
81
  - initialization logic
82
- - test data setup
82
+ - shared baseline data setup
83
83
 
84
84
  A practical generated-thread interpretation is:
85
85
 
86
86
  - `update` follows schema and entity/model evolution
87
87
  - `init` follows instance-aware initialization needs introduced by the backend feature
88
- - `test` keeps the generated or refined contract easy to verify under the test lifecycle
88
+ - `seed` provides shared baseline data while the application starts in test mode
89
89
 
90
90
  ## Update: schema migration
91
91
 
@@ -128,23 +128,27 @@ export class MetaVersion extends BeanBase implements IMetaVersionInit {
128
128
 
129
129
  The important point is that initialization can run per instance or tenant, which keeps tenant data isolated.
130
130
 
131
- ## Test: test-only data
131
+ ## Seed: test-environment baseline data
132
132
 
133
133
  Representative pattern:
134
134
 
135
135
  ```typescript
136
136
  @Meta()
137
- export class MetaVersion extends BeanBase implements IMetaVersionTest {
138
- async test() {
137
+ export class MetaVersion extends BeanBase implements IMetaVersionSeed {
138
+ async seed() {
139
139
  await this.scope.model.student.insert({
140
140
  name: 'Jimmy',
141
- description: 'Only used in unit test',
141
+ description: 'Shared test-environment baseline data',
142
142
  });
143
143
  }
144
144
  }
145
145
  ```
146
146
 
147
- This is valuable because test data becomes part of the structured migration lifecycle instead of being scattered across unrelated setup code.
147
+ This is valuable because shared baseline data becomes part of the structured version lifecycle instead of being scattered across unrelated setup code.
148
+
149
+ Vona invokes `seed()` while the application starts in test mode. Use it for durable baseline fixtures shared by multiple tests, E2E tests, or the managed local-development test-data workflow. The seed path starts from a newly recreated managed database; it is not a repeatable, same-database data-import mechanism.
150
+
151
+ Treat shared `seed()` records as read-only in ordinary tests. A scenario that needs different state must create independent, test-owned data and clean it up in `finally`; it must not mutate or delete the shared baseline. `seed()` remains distinct from `init()`: use `init()` for instance-aware initialization data and reserve `seed()` for shared test-environment baseline data.
148
152
 
149
153
  ## Version changes across the generated backend thread
150
154
 
@@ -185,7 +189,7 @@ When changing backend schema or module initialization behavior, do not only edit
185
189
  Also ask:
186
190
 
187
191
  1. does this change require a `fileVersion` increment?
188
- 2. does `meta.version` need an `update`, `init`, or `test` branch?
192
+ 2. does `meta.version` need an `update`, `init`, or `seed` branch?
189
193
  3. should the local verification path include `test` or `db:reset`?
190
194
  4. does the change affect the CRUD-generated thread or frontend-facing API contract as well?
191
195
 
@@ -46,11 +46,18 @@ npm run test
46
46
 
47
47
  A typical Vona test flow includes:
48
48
 
49
- 1. create a global `app` object
49
+ 1. create one shared global `app` object for the test run
50
50
  2. clean Redis data
51
51
  3. recreate the database
52
52
  4. execute migration code
53
53
  5. run the test files
54
+ 6. close the shared application after the test run
55
+
56
+ The runner owns this application lifecycle. Test files import the shared app from `vona-mock` and must not create or close it themselves:
57
+
58
+ ```typescript
59
+ import { app } from 'vona-mock';
60
+ ```
54
61
 
55
62
  This is one of the most important distinctions from ordinary app flow: test execution rebuilds and verifies the framework lifecycle, not only the target function.
56
63
 
@@ -72,6 +79,33 @@ Representative command:
72
79
  cd vona && npm run cov
73
80
  ```
74
81
 
82
+ ## Runner concurrency
83
+
84
+ The Vona runner uses Node's built-in test runner with one shared application and process isolation disabled. `TEST_CONCURRENCY` controls runner scheduling:
85
+
86
+ - `true` uses the available CPU count;
87
+ - `false` forces serial execution;
88
+ - a positive integer sets an explicit concurrency limit.
89
+
90
+ ```bash
91
+ TEST_CONCURRENCY=false npm run test
92
+ TEST_CONCURRENCY=4 npm run test
93
+ ```
94
+
95
+ Cabloy Basic requests concurrency by default with `TEST_CONCURRENCY=true`. When the active database dialect does not advertise the required concurrency capability, the runner falls back to serial execution. The default SQLite path therefore runs serially.
96
+
97
+ Runner concurrency schedules ordinary tests; it does not prove a business-level race condition. Tests must not rely on scheduling, timing, or execution order for correctness.
98
+
99
+ A suite can also make sibling-test scheduling explicit:
100
+
101
+ ```typescript
102
+ describe('resource.test.ts', { concurrency: false }, () => {
103
+ // sibling tests in this suite are serialized
104
+ });
105
+ ```
106
+
107
+ This setting controls test scheduling only. It is not a substitute for explicitly creating competing business operations.
108
+
75
109
  ## Mock request context
76
110
 
77
111
  One of the most important Vona testing patterns is simulating a request context.
@@ -84,8 +118,68 @@ await app.bean.executor.mockCtx(async () => {
84
118
  });
85
119
  ```
86
120
 
121
+ Treat one `mockCtx(...)` callback as one simulated request boundary. Keep `app.ctx`, authentication, current database selection, and context-dependent service, model, or action work inside that callback, and always await it. Do not retain request-context state after the callback returns.
122
+
123
+ `mockCtx(...)` isolates request-local context. It does not isolate committed persisted records, app-global state, external caches, or shared durable fixtures. For independent concurrent requests, create one `mockCtx(...)` for each branch; parallel work started inside a single `mockCtx(...)` shares that request context.
124
+
87
125
  Locale-sensitive variants and additional request-context helpers are also available.
88
126
 
127
+ ## Persisted fixture lifecycle
128
+
129
+ Classify persisted test data before creating it:
130
+
131
+ - use a **test-local fixture** for a single test or scenario;
132
+ - use an owning module's `meta.version.ts` `seed()` hook only for stable baseline data shared across tests or intentionally used by the local-development test-data workflow.
133
+
134
+ A test owns every persisted resource it creates. Keep the returned entity or exact ID and delete owned records from `finally`, including when an assertion or action fails. Delete dependents before their owners and use the same active tenant/instance context that created them. Do not discover cleanup targets through broad table queries, time-based prefixes, or business conditions when the test already knows the exact identity.
135
+
136
+ ```typescript
137
+ let parentId: number | undefined;
138
+ let childId: number | undefined;
139
+ try {
140
+ parentId = await createParent();
141
+ childId = await createChild(parentId);
142
+
143
+ // exercise and assert the behavior under test
144
+ } finally {
145
+ if (childId) await deleteChild(childId);
146
+ if (parentId) await deleteParent(parentId);
147
+ }
148
+ ```
149
+
150
+ Treat shared `seed()` records as read-only. If a scenario needs to change a record, create a separate test-local fixture instead and clean it up. See [Migration and Changes](/backend/migration-and-changes#seed-test-environment-baseline-data) for the durable seed lifecycle.
151
+
152
+ Application shutdown is runner-owned; authentication and fixture cleanup are test-owned. Do not call `app.close()` from a test. Keep `signout()` and exact-identity deletion in `finally` inside an appropriate request context.
153
+
154
+ ## Testing concurrent behavior explicitly
155
+
156
+ A concurrency test creates competing business operations inside one test; it does not depend on the runner to happen to schedule tests at the same time.
157
+
158
+ ```typescript
159
+ const attempt = async () => {
160
+ return await app.bean.executor.mockCtx(async () => {
161
+ await app.bean.passport.signinMock();
162
+ try {
163
+ return await reserve(resourceId);
164
+ } finally {
165
+ await app.bean.passport.signout();
166
+ }
167
+ });
168
+ };
169
+
170
+ const results = await Promise.allSettled([attempt(), attempt()]);
171
+ ```
172
+
173
+ Use this sequence:
174
+
175
+ 1. create a dedicated test-local fixture in an appropriate `mockCtx(...)`;
176
+ 2. give every contender its own `mockCtx(...)` and authentication lifecycle;
177
+ 3. launch the contenders explicitly and wait for all of them to settle;
178
+ 4. use a fresh `mockCtx(...)` to assert both individual outcomes and the combined durable invariant;
179
+ 5. clean up exact owned fixtures only after every branch has settled, in the appropriate tenant/instance context.
180
+
181
+ For example, a one-winner reservation test should verify not only the fulfilled and rejected operations but also the final balance, surviving reservation, and audit records. If the invariant depends on row locks, transaction isolation, or another database-specific capability, skip the test on unsupported dialects rather than weakening its assertions. See [stockReservation.test.ts](../../vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts) for this pattern.
182
+
89
183
  ## Working with module scope in tests
90
184
 
91
185
  Representative pattern:
@@ -193,6 +287,8 @@ Read this guide together with:
193
287
  - [CRUD Workflow](/backend/crud-workflow)
194
288
  - [Migration and Changes](/backend/migration-and-changes)
195
289
  - [Controller Guide](/backend/controller-guide)
290
+ - [Transaction Guide](/backend/transaction-guide)
291
+ - [Redlock Guide](/backend/redlock-guide)
196
292
 
197
293
  A practical split is:
198
294
 
@@ -8,31 +8,69 @@ const child = spawn('npm', ['run', 'dev:one'], {
8
8
  stdio: 'inherit',
9
9
  });
10
10
 
11
+ const gracefulShutdownTimeout = 7000;
12
+
11
13
  let stopping = false;
14
+ let forced = false;
15
+ let shutdownTimer: NodeJS.Timeout | undefined;
12
16
 
13
- function stop() {
14
- if (stopping) return;
15
- stopping = true;
16
- if (process.platform === 'win32') {
17
- child.kill('SIGINT');
18
- } else if (child.pid) {
19
- try {
20
- process.kill(-child.pid, 'SIGINT');
21
- } catch (error: any) {
22
- if (error.code !== 'ESRCH') throw error;
17
+ function finish(code: number) {
18
+ if (shutdownTimer) {
19
+ clearTimeout(shutdownTimer);
20
+ shutdownTimer = undefined;
21
+ }
22
+ process.exit(code);
23
+ }
24
+
25
+ function signalChild(signal: NodeJS.Signals) {
26
+ if (!child.pid) return;
27
+ try {
28
+ if (process.platform === 'win32') {
29
+ child.kill(signal);
30
+ } else {
31
+ process.kill(-child.pid, signal);
32
+ }
33
+ } catch (error: any) {
34
+ if (error.code !== 'ESRCH') {
35
+ // eslint-disable-next-line
36
+ console.error(error);
23
37
  }
24
38
  }
25
39
  }
26
40
 
41
+ function forceStop() {
42
+ if (forced) return;
43
+ forced = true;
44
+ // eslint-disable-next-line
45
+ console.error(
46
+ `E2E Vona process group did not stop within ${gracefulShutdownTimeout}ms; forcing termination: ${child.pid}`,
47
+ );
48
+ signalChild('SIGKILL');
49
+ finish(1);
50
+ }
51
+
52
+ function stop() {
53
+ if (stopping) {
54
+ forceStop();
55
+ return;
56
+ }
57
+ stopping = true;
58
+ // Vona handles SIGINT, not SIGTERM. Keep this deadline below Playwright's 10-second timeout.
59
+ signalChild('SIGINT');
60
+ shutdownTimer = setTimeout(forceStop, gracefulShutdownTimeout);
61
+ shutdownTimer.unref();
62
+ }
63
+
27
64
  process.on('SIGINT', stop);
28
65
  process.on('SIGTERM', stop);
29
66
 
30
67
  child.on('error', error => {
31
68
  // eslint-disable-next-line
32
69
  console.error(error);
33
- process.exitCode = 1;
70
+ finish(1);
34
71
  });
35
72
 
36
- child.on('exit', code => {
37
- process.exitCode = code ?? 1;
73
+ child.on('exit', (code, signal) => {
74
+ // Playwright waits for this wrapper to exit, not only for Vona's workers to stop.
75
+ finish(code ?? (signal ? 1 : 0));
38
76
  });
@@ -111,6 +111,7 @@ test(
111
111
  const documentResponse = await page.goto(productPath, { waitUntil: 'load' });
112
112
  expect(documentResponse?.ok()).toBeTruthy();
113
113
  await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
114
+ await expect(page).toHaveURL(productPath);
114
115
  await expect(page.getByRole('heading', { name: 'Wireless Headphones' })).toBeVisible();
115
116
 
116
117
  const cartLink = page.locator('a[href="/commerce/cart"]');
@@ -98,6 +98,14 @@ function assertVisualOrder(geometry: IFieldGeometry[]) {
98
98
  }
99
99
  }
100
100
 
101
+ async function getDocumentHorizontalOverflow(page: Page) {
102
+ return page.evaluate(() => {
103
+ return (
104
+ Math.max(document.documentElement.scrollWidth, document.body.scrollWidth) - window.innerWidth
105
+ );
106
+ });
107
+ }
108
+
101
109
  test(
102
110
  'ATP-BASIC-SSR-01: anonymous Web HTML hydrates through the default site',
103
111
  { tag: ['@web', '@smoke'] },
@@ -217,6 +225,8 @@ test(
217
225
  }
218
226
 
219
227
  await page.setViewportSize({ width: 700, height: 900 });
228
+ const drawer = page.locator('.drawer').first();
229
+ await expect(drawer).not.toHaveClass(/\bdrawer-open\b/);
220
230
  await expect
221
231
  .poll(
222
232
  async () =>
@@ -238,15 +248,10 @@ test(
238
248
  const createdAtEndBox = await createdAtEnd.boundingBox();
239
249
  expect(createdAtEndBox).not.toBeNull();
240
250
  expect(createdAtEndBox!.x + createdAtEndBox!.width).toBeLessThanOrEqual(viewport.width + 1);
241
- await expect
242
- .poll(() =>
243
- page.evaluate(
244
- () => document.documentElement.scrollWidth - document.documentElement.clientWidth,
245
- ),
246
- )
247
- .toBeLessThanOrEqual(1);
251
+ await expect.poll(() => getDocumentHorizontalOverflow(page)).toBeLessThanOrEqual(1);
248
252
 
249
253
  await page.setViewportSize({ width: 1440, height: 900 });
254
+ await expect(drawer).toHaveClass(/\bdrawer-open\b/);
250
255
  await name.fill('Flow E2E');
251
256
  await level.selectOption('2');
252
257
  await createdAtStart.fill('2026-01-10');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.112",
3
+ "version": "5.1.114",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.130",
3
+ "version": "1.1.132",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.128",
3
+ "version": "1.1.130",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -77,15 +77,12 @@ export class CliBinTest extends BeanCliBase {
77
77
  patterns.join(','),
78
78
  ]);
79
79
  // args = args.concat(['--experimental-transform-types', getImportEsm(), testFile, projectPath, (!!argv.coverage).toString(), patterns.join(',')]);
80
- // ignore error special in windows
81
- await catchError(() => {
82
- return this.helper.spawnExe({
83
- cmd: 'node',
84
- args,
85
- options: {
86
- cwd: projectPath,
87
- },
88
- });
80
+ await this.helper.spawnExe({
81
+ cmd: 'node',
82
+ args,
83
+ options: {
84
+ cwd: projectPath,
85
+ },
89
86
  });
90
87
  }
91
88