cabloy 5.1.112 → 5.1.113

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 (41) 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 +9 -0
  4. package/.github/workflows/playwright-e2e.yml +34 -2
  5. package/.github/workflows/vona-test-pg.yml +8 -0
  6. package/CHANGELOG.md +13 -0
  7. package/CLAUDE.md +2 -0
  8. package/cabloy-docs/backend/migration-and-changes.md +13 -9
  9. package/cabloy-docs/backend/unit-testing.md +25 -0
  10. package/e2e/scripts/startE2eVona.ts +51 -13
  11. package/e2e/specs/a-commerce/commerce.spec.ts +1 -0
  12. package/e2e/specs/cabloy-basic/basic.spec.ts +12 -7
  13. package/package.json +1 -1
  14. package/vona/packages-cli/cli/package.json +1 -1
  15. package/vona/packages-cli/cli-set-api/package.json +1 -1
  16. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.test.ts +6 -9
  17. package/vona/packages-cli/cli-set-api/toolsIsolate/test.ts +50 -21
  18. package/vona/packages-vona/vona/package.json +1 -1
  19. package/vona/pnpm-lock.yaml +67 -67
  20. package/vona/src/backend/config/config/config.test.ts +1 -1
  21. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +234 -0
  22. package/vona/src/suite/a-commerce/modules/commerce-seed/src/bean/meta.version.ts +3 -3
  23. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +4 -0
  24. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +4 -0
  25. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +4 -0
  26. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockAudit.tsx +39 -7
  27. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +38 -1
  28. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +268 -65
  29. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +276 -81
  30. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  31. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +2 -2
  32. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  33. package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/version.ts +12 -12
  34. package/vona/src/suite-vendor/a-vona/modules/a-version/src/types/version.ts +4 -4
  35. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  36. package/zova/pnpm-lock.yaml +14 -14
  37. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +0 -1
  38. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +1 -0
  39. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +17 -7
  40. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.header.tsx +1 -1
  41. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +17 -7
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.112
1
+ 5.1.113
@@ -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,15 @@ 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
+
38
47
  ## Module composition and dependency intent
39
48
 
40
49
  - 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,14 @@ 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
+ TEST_CONCURRENCY=false 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
+ --flavor=normal
44
+ working-directory: vona
37
45
  - run: DATABASE_DEFAULT_CLIENT=pg npm run test
38
46
  working-directory: vona
39
47
  - run: DATABASE_DEFAULT_CLIENT=pg npm run build -- --flavor=ci
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.113
4
+
5
+ ### Features
6
+
7
+ - Update application capabilities and workflows.
8
+
9
+ ### Improvements
10
+
11
+ - Rename the version test lifecycle to the seed lifecycle.
12
+ - Run Commerce end-to-end tests independently in CI.
13
+ - Strengthen Commerce catalog, stock balance, and stock reservation test coverage.
14
+ - Record Commerce stock verification and Phase 20 CI evidence.
15
+
3
16
  ## 5.1.112
4
17
 
5
18
  ### Features
package/CLAUDE.md CHANGED
@@ -72,6 +72,8 @@ 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
+ - Unit tests must delete every test-owned persisted resource in `finally`, using precise owned identities and reverse dependency order.
76
+ - 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
77
 
76
78
  ## Verification expectations
77
79
 
@@ -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
 
@@ -86,6 +86,31 @@ await app.bean.executor.mockCtx(async () => {
86
86
 
87
87
  Locale-sensitive variants and additional request-context helpers are also available.
88
88
 
89
+ ## Persisted fixture lifecycle
90
+
91
+ Classify persisted test data before creating it:
92
+
93
+ - use a **test-local fixture** for a single test or scenario;
94
+ - 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.
95
+
96
+ 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.
97
+
98
+ ```typescript
99
+ let parentId: number | undefined;
100
+ let childId: number | undefined;
101
+ try {
102
+ parentId = await createParent();
103
+ childId = await createChild(parentId);
104
+
105
+ // exercise and assert the behavior under test
106
+ } finally {
107
+ if (childId) await deleteChild(childId);
108
+ if (parentId) await deleteParent(parentId);
109
+ }
110
+ ```
111
+
112
+ 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.
113
+
89
114
  ## Working with module scope in tests
90
115
 
91
116
  Representative pattern:
@@ -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.113",
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.131",
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.129",
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
 
@@ -63,9 +63,27 @@ async function testRun(projectPath: string, coverage: boolean, patterns: string[
63
63
  ];
64
64
  // app
65
65
  const app: VonaApplication = await createGeneralApp(projectPath);
66
- // concurrency
67
- const concurrency = await prepareConcurrency(app);
68
- return new Promise(resolve => {
66
+ let testError: unknown;
67
+ let closeError: unknown;
68
+ let closePromise: Promise<void> | undefined;
69
+ const closeApplication = async () => {
70
+ const [_, error] = await catchError(() => app.close());
71
+ closeError = error;
72
+ // handles
73
+ if (process.env.TEST_WHYISNODERUNNING === 'true') {
74
+ await sleep(2000);
75
+ const handles = (process as any)._getActiveHandles();
76
+ if (handles.length > 3) {
77
+ whyIsNodeRunning();
78
+ }
79
+ }
80
+ };
81
+ const closeApplicationOnce = () => {
82
+ return (closePromise ??= closeApplication());
83
+ };
84
+ try {
85
+ // concurrency
86
+ const concurrency = await prepareConcurrency(app);
69
87
  const testStream = run({
70
88
  isolation: 'none',
71
89
  concurrency,
@@ -80,27 +98,12 @@ async function testRun(projectPath: string, coverage: boolean, patterns: string[
80
98
  .on('test:coverage', data => {
81
99
  outputCoverageReport(data.summary.totals);
82
100
  })
83
- .on('test:summary', async () => {
84
- resolve(undefined);
85
- })
86
- .on('test:pass', async t => {
101
+ .on('test:pass', t => {
87
102
  if (t.name === '---done---') {
88
- const [_, err] = await catchError(() => {
89
- return app.close();
90
- });
91
- if (err) {
92
- console.error(err);
93
- }
94
- // handles
95
- if (process.env.TEST_WHYISNODERUNNING === 'true') {
96
- await sleep(2000);
97
- const handles = (process as any)._getActiveHandles();
98
- if (handles.length > 3) {
99
- whyIsNodeRunning();
100
- }
101
- }
103
+ void closeApplicationOnce();
102
104
  }
103
105
  });
106
+ const summaryPromise = waitForTestSummary(testStream);
104
107
  if (coverage) {
105
108
  const reporterDir = path.join(projectPath, 'coverage');
106
109
  fse.ensureDirSync(reporterDir);
@@ -109,6 +112,32 @@ async function testRun(projectPath: string, coverage: boolean, patterns: string[
109
112
  } else {
110
113
  testStream.compose(spec).pipe(process.stdout);
111
114
  }
115
+
116
+ const summarySuccess = await summaryPromise;
117
+ if (!summarySuccess) {
118
+ throw new Error('node:test reported failed tests');
119
+ }
120
+ } catch (error) {
121
+ testError = error;
122
+ } finally {
123
+ await closeApplicationOnce();
124
+ }
125
+ if (testError) {
126
+ if (closeError) console.error(closeError);
127
+ throw toError(testError);
128
+ }
129
+ if (closeError) {
130
+ throw toError(closeError);
131
+ }
132
+ }
133
+
134
+ function toError(error: unknown) {
135
+ return error instanceof Error ? error : new Error(String(error));
136
+ }
137
+
138
+ function waitForTestSummary(testStream: ReturnType<typeof run>) {
139
+ return new Promise<boolean>((resolve, reject) => {
140
+ testStream.once('test:summary', summary => resolve(summary.success)).once('error', reject);
112
141
  });
113
142
  }
114
143
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.77",
3
+ "version": "5.1.78",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [