cabloy 5.1.204 → 5.1.206

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 (117) hide show
  1. package/.cabloy-version +1 -1
  2. package/.gitignore +2 -0
  3. package/CHANGELOG.md +21 -0
  4. package/lint-staged.config.mjs +2 -0
  5. package/oxfmt.config.ts +2 -0
  6. package/oxlint.config.ts +2 -0
  7. package/package.json +2 -3
  8. package/repo-agent-governance/managed-assets.json +6 -6
  9. package/repo-agent-governance/policies/repository.md +3 -2
  10. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -0
  11. package/repo-docs/backend/migration-and-changes.md +13 -12
  12. package/repo-docs/backend/runtime-and-flavors.md +2 -1
  13. package/repo-docs/backend/unit-testing.md +12 -8
  14. package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/index.md +9 -9
  15. package/repo-docs/frontend/scripts.md +1 -1
  16. package/repo-docs/fullstack/parallel-worktree-environment.md +6 -6
  17. package/repo-docs/fullstack/ssr-entry-modes.md +19 -0
  18. package/repo-docs/reference/repo-scripts.md +22 -1
  19. package/repo-e2e/config/playwright.config.ts +32 -14
  20. package/repo-e2e/scripts/e2e.test.ts +39 -0
  21. package/repo-e2e/scripts/e2e.ts +19 -1
  22. package/repo-e2e/scripts/managedTest.test.ts +389 -0
  23. package/repo-e2e/scripts/managedTest.ts +210 -0
  24. package/repo-e2e/scripts/managedTestImports.test.ts +74 -0
  25. package/repo-e2e/scripts/managedTestImports.ts +110 -0
  26. package/repo-e2e/scripts/runE2e.ts +12 -1
  27. package/repo-e2e/scripts/startE2eVona.ts +37 -5
  28. package/repo-e2e/specs/a-commerce.spec.ts +1 -1
  29. package/repo-e2e/specs/cabloy-basic.spec.ts +1 -1
  30. package/repo-e2e/specs/controller-boundary.spec.ts +1 -1
  31. package/repo-e2e/specs/home-user-account.spec.ts +2 -1
  32. package/repo-e2e/specs/markdown.spec.ts +1 -1
  33. package/repo-e2e/specs/routed-dialog.spec.ts +1 -1
  34. package/scripts/runIsolatedTest.test.ts +447 -0
  35. package/scripts/runIsolatedTest.ts +462 -0
  36. package/scripts/testClaimCleanup.test.ts +346 -0
  37. package/scripts/testClaimCleanup.ts +375 -0
  38. package/scripts/testClaimProcesses.test.ts +106 -0
  39. package/scripts/testClaimProcesses.ts +129 -0
  40. package/scripts/testProcessGuardian.ts +163 -0
  41. package/scripts/upgrade.ts +1 -16
  42. package/vona/oxfmt.config.ts +2 -0
  43. package/vona/oxlint.config.ts +2 -0
  44. package/vona/package.original.json +2 -2
  45. package/vona/packages-cli/cli/package.json +1 -1
  46. package/vona/packages-cli/cli/src/play.test.ts +37 -0
  47. package/vona/packages-cli/cli/src/play.ts +23 -1
  48. package/vona/packages-cli/cli-set-api/package.json +1 -1
  49. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dbReset.ts +16 -7
  50. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +13 -7
  51. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.play.ts +5 -1
  52. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.test.ts +12 -7
  53. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/configUtils.ts +21 -2
  54. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateVonaMeta.ts +10 -0
  55. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/resolveTestCleanupConfig.test.ts +153 -0
  56. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/resolveTestCleanupConfig.ts +141 -0
  57. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupAdapters.test.ts +148 -0
  58. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupAdapters.ts +127 -0
  59. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupProcess.test.ts +47 -0
  60. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupProcess.ts +139 -0
  61. package/vona/packages-cli/cli-set-api/src/lib/utils.ts +2 -3
  62. package/vona/packages-cli/cli-set-api/templates/config/_tsconfig_project.json +1 -0
  63. package/vona/packages-cli/cli-set-api/templates/config/_tsconfig_source.json +1 -0
  64. package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +18 -0
  65. package/vona/packages-cli/cli-set-api/toolsIsolate/dbReset.ts +15 -2
  66. package/vona/packages-cli/cli-set-api/tsconfig.build.json +2 -1
  67. package/vona/packages-utils/babel-plugin-vona-bean-module/package.json +1 -1
  68. package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.test.ts +16 -0
  69. package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.ts +3 -2
  70. package/vona/packages-vona/vona/package.json +1 -1
  71. package/vona/packages-vona/vona-core/package.json +1 -1
  72. package/vona/packages-vona/vona-core/src/lib/addon/sqlite3.ts +1 -1
  73. package/vona/packages-vona/vona-core/src/lib/core/config.ts +44 -6
  74. package/vona/packages-vona/vona-core/src/lib/core/configCleanup.test.ts +40 -0
  75. package/vona/packages-vona/vona-core/src/lib/core/index.ts +3 -0
  76. package/vona/packages-vona/vona-core/src/lib/core/isolationGuard.test.ts +301 -0
  77. package/vona/packages-vona/vona-core/src/lib/core/isolationGuard.ts +312 -0
  78. package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +577 -0
  79. package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +985 -0
  80. package/vona/packages-vona/vona-core/src/lib/core/testIsolation.ts +129 -0
  81. package/vona/packages-vona/vona-core/src/lib/framework/createApp.ts +2 -0
  82. package/vona/packages-vona/vona-core/src/lib/framework/start.ts +4 -0
  83. package/vona/packages-vona/vona-core/src/lib/framework/useApp.ts +5 -1
  84. package/vona/packages-vona/vona-core/src/types/application/app.ts +2 -0
  85. package/vona/packages-vona/vona-core/src/types/utils/env.ts +1 -0
  86. package/vona/packages-vona/vona-mock/package.json +1 -1
  87. package/vona/pnpm-lock.yaml +1 -1
  88. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentIndexes.test.ts +73 -10
  89. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  90. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  91. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  92. package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
  93. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +15 -1
  94. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts +24 -0
  95. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.ts +11 -0
  96. package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
  97. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.health.ts +15 -0
  98. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  99. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/extend/schemaBuilder.ts +11 -4
  100. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/databaseClient_.ts +6 -1
  101. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/package.json +1 -1
  102. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/bean/databaseDialect.betterSqlite3.ts +41 -1
  103. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/bean/databaseDialect.mysql.ts +4 -1
  104. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/bean/databaseDialect.pg.ts +2 -1
  105. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  106. package/vona/src/suite-vendor/a-vona/modules/a-redis/package.json +1 -1
  107. package/vona/src/suite-vendor/a-vona/modules/a-redis/src/lib/redis.ts +8 -0
  108. package/vona/src/suite-vendor/a-vona/modules/a-redis/src/service/redis.ts +23 -6
  109. package/vona/src/suite-vendor/a-vona/modules/a-redis/src/service/redisClient_.ts +18 -1
  110. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  111. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +43 -15
  112. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  113. package/vona/src/suite-vendor/a-vona/modules/a-version/src/lib/databaseTestName.ts +3 -37
  114. package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/database.ts +93 -15
  115. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  116. package/vona/tsconfig.json +1 -0
  117. package/scripts/initTestData.ts +0 -25
@@ -0,0 +1,163 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+
3
+ import { spawn } from 'node:child_process';
4
+
5
+ import type { TestProcessIdentity } from './testClaimProcesses.ts';
6
+
7
+ import { sendTestClaimEvent } from '../vona/packages-vona/vona-core/src/lib/core/testClaim.ts';
8
+
9
+ interface StartMessage {
10
+ type: 'start';
11
+ command: string;
12
+ args: string[];
13
+ cwd: string;
14
+ env: NodeJS.ProcessEnv;
15
+ processId?: string;
16
+ identity?: TestProcessIdentity;
17
+ }
18
+
19
+ if (process.platform === 'win32' || !process.send) {
20
+ throw new Error('Isolated test guardian requires a POSIX process group and parent IPC');
21
+ }
22
+
23
+ let child: ChildProcess | undefined;
24
+ let stopping = false;
25
+ let finishing = false;
26
+ let finished = false;
27
+ let failed = false;
28
+ let childResult: { code: number | null; signal: NodeJS.Signals | null } | undefined;
29
+ let forceTimer: NodeJS.Timeout | undefined;
30
+ let launch: StartMessage | undefined;
31
+
32
+ async function groupMembers(): Promise<number[]> {
33
+ const output = await new Promise<string>((resolvePromise, reject) => {
34
+ const inspector = spawn('ps', ['-A', '-o', 'pid=', '-o', 'pgid=', '-o', 'state='], {
35
+ detached: true,
36
+ stdio: ['ignore', 'pipe', 'pipe'],
37
+ });
38
+ let stdout = '';
39
+ let stderr = '';
40
+ inspector.stdout?.on('data', data => {
41
+ stdout += data;
42
+ });
43
+ inspector.stderr?.on('data', data => {
44
+ stderr += data;
45
+ });
46
+ inspector.once('error', reject);
47
+ inspector.once('close', code => {
48
+ if (code === 0) resolvePromise(stdout);
49
+ else reject(new Error(`Cannot inspect isolated process group: ${stderr}`));
50
+ });
51
+ });
52
+ return output
53
+ .split('\n')
54
+ .map(line => line.trim().split(/\s+/))
55
+ .filter(
56
+ ([pid, pgid, state]) =>
57
+ pid &&
58
+ pgid &&
59
+ state &&
60
+ Number(pgid) === process.pid &&
61
+ Number(pid) !== process.pid &&
62
+ !state.startsWith('Z'),
63
+ )
64
+ .map(([pid]) => Number(pid));
65
+ }
66
+
67
+ function requestStop() {
68
+ if (stopping || finished) return;
69
+ stopping = true;
70
+ forceTimer = setTimeout(() => {
71
+ // The guardian remains its own group leader while issuing the last resort signal.
72
+ process.kill(-process.pid, 'SIGKILL');
73
+ }, 12_000);
74
+ try {
75
+ process.kill(-process.pid, 'SIGINT');
76
+ } catch (error) {
77
+ if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error;
78
+ }
79
+ if (!child) void drain();
80
+ }
81
+
82
+ async function drain() {
83
+ if (finishing || finished) return;
84
+ finishing = true;
85
+ try {
86
+ let deadline = Date.now() + (stopping ? 10_000 : 1_000);
87
+ while ((await groupMembers()).length > 0) {
88
+ if (Date.now() >= deadline) {
89
+ if (stopping) {
90
+ throw new Error('Isolated process group did not drain after shutdown');
91
+ }
92
+ requestStop();
93
+ deadline = Date.now() + 10_000;
94
+ }
95
+ await new Promise(resolvePromise => setTimeout(resolvePromise, 100));
96
+ }
97
+ if (forceTimer) clearTimeout(forceTimer);
98
+ if (launch?.processId && launch.identity) {
99
+ try {
100
+ await sendTestClaimEvent(launch.env, {
101
+ type: 'process',
102
+ process: {
103
+ id: launch.processId,
104
+ role: 'guardian',
105
+ stage: 'drained',
106
+ pid: launch.identity.pid,
107
+ pgid: launch.identity.pgid,
108
+ startTime: launch.identity.startTime,
109
+ host: launch.identity.host,
110
+ },
111
+ });
112
+ } catch {
113
+ failed = true;
114
+ }
115
+ }
116
+ finished = true;
117
+ process.send?.({ type: 'drained', result: childResult, failed }, error => {
118
+ if (error) process.exit(1);
119
+ else process.exit(0);
120
+ });
121
+ } catch (error) {
122
+ console.error(error);
123
+ requestStop();
124
+ }
125
+ }
126
+
127
+ function start(message: StartMessage) {
128
+ if (child || stopping) return;
129
+ launch = message;
130
+ try {
131
+ child = spawn(message.command, message.args, {
132
+ cwd: message.cwd,
133
+ env: message.env,
134
+ stdio: 'inherit',
135
+ detached: false,
136
+ });
137
+ child.once('error', error => {
138
+ failed = true;
139
+ console.error(error);
140
+ });
141
+ child.once('close', (code, signal) => {
142
+ childResult = { code, signal };
143
+ void drain();
144
+ });
145
+ if (stopping) child.kill('SIGINT');
146
+ process.send?.({ type: 'started' });
147
+ } catch (error) {
148
+ console.error(error);
149
+ failed = true;
150
+ childResult = { code: 1, signal: null };
151
+ void drain();
152
+ }
153
+ }
154
+
155
+ process.on('SIGINT', requestStop);
156
+ process.on('SIGTERM', requestStop);
157
+ process.on('disconnect', requestStop);
158
+ process.on('message', message => {
159
+ if (!message || typeof message !== 'object') return;
160
+ if ('type' in message && message.type === 'stop') requestStop();
161
+ if ('type' in message && message.type === 'start') start(message as StartMessage);
162
+ });
163
+ process.send?.({ type: 'ready' });
@@ -549,17 +549,7 @@ function runInit(dryRun: boolean, version: string): void {
549
549
  exec('npm run init', { CABLOY_VERSION: version } as any);
550
550
  }
551
551
 
552
- // --- Step 5: Run init:test-data ---
553
-
554
- function runInitTestData(dryRun: boolean): void {
555
- if (dryRun) {
556
- log(' [dry-run] Run: npm run init:test-data');
557
- return;
558
- }
559
- exec('npm run init:test-data');
560
- }
561
-
562
- // --- Step 6: Cleanup ---
552
+ // --- Step 5: Cleanup ---
563
553
 
564
554
  function cleanup(dryRun?: boolean): void {
565
555
  if (existsSync(TEMP_DIR)) {
@@ -637,11 +627,6 @@ async function main(): Promise<void> {
637
627
  runInit(dryRun, latestPackageInfo.version);
638
628
  log('');
639
629
 
640
- // 6. Run init:test-data
641
- log('Running npm run init:test-data...');
642
- runInitTestData(dryRun);
643
- log('');
644
-
645
630
  if (dryRun) {
646
631
  log(`[dry-run] Current Cabloy version would become: ${latestPackageInfo.version}`);
647
632
  } else {
@@ -11,6 +11,8 @@ export default defineConfig(
11
11
  '**/.rollup.cache/**',
12
12
  '**/.metadata/index.ts',
13
13
  '.vona',
14
+ '.vona-test-*',
15
+ '.app-test-*',
14
16
  '.assets',
15
17
  'coverage',
16
18
  'docker-compose',
@@ -43,6 +43,8 @@ export default defineConfig(
43
43
  '**/.rollup.cache/**',
44
44
  '**/.metadata/index.ts',
45
45
  '.vona',
46
+ '.vona-test-*',
47
+ '.app-test-*',
46
48
  '.claude',
47
49
  '.assets',
48
50
  'coverage',
@@ -23,8 +23,8 @@
23
23
  "db:reset": "npm run prerun && npm run vona :bin:dbReset -- --flavor=normal",
24
24
  "dev": "npm run prerun && npm run vona :bin:dev -- --workers=2 --flavor=normal",
25
25
  "dev:one": "npm run prerun && npm run vona :bin:dev -- --workers=1 --flavor=normal",
26
- "test": "npm run prerun && npm run vona :bin:test -- --flavor=normal",
27
- "cov": "npm run prerun && npm run vona :bin:test -- --coverage --flavor=normal",
26
+ "test": "node ../scripts/runIsolatedTest.ts unit",
27
+ "cov": "node ../scripts/runIsolatedTest.ts unit --coverage",
28
28
  "tsc": "npm run prerun && npm run vona :bin:tsc",
29
29
  "build": "npm run prerun && npm run vona :bin:build -- --flavor=normal",
30
30
  "build:docker": "npm run prerun && npm run vona :bin:build -- --flavor=docker",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.158",
3
+ "version": "1.1.160",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -0,0 +1,37 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { test } from 'node:test';
4
+
5
+ const script = `
6
+ import { playAttach } from './play.ts';
7
+ await playAttach('/nonexistent-vona-project', []);
8
+ `;
9
+
10
+ test('play attach accepts the timestamped test identity before locating its runtime', () => {
11
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
12
+ cwd: import.meta.dirname,
13
+ env: {
14
+ ...process.env,
15
+ META_MODE: 'test',
16
+ VONA_TEST_RUN_ID: '20261014010203-a1b2c3d4',
17
+ },
18
+ encoding: 'utf8',
19
+ });
20
+ assert.notEqual(result.status, 0);
21
+ assert.match(result.stderr, /dev server not running/);
22
+ assert.doesNotMatch(result.stderr, /VONA_TEST_RUN_ID requires/);
23
+ });
24
+
25
+ test('play attach rejects impossible UTC seconds', () => {
26
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
27
+ cwd: import.meta.dirname,
28
+ env: {
29
+ ...process.env,
30
+ META_MODE: 'test',
31
+ VONA_TEST_RUN_ID: '20260230010203-a1b2c3d4',
32
+ },
33
+ encoding: 'utf8',
34
+ });
35
+ assert.notEqual(result.status, 0);
36
+ assert.match(result.stderr, /VONA_TEST_RUN_ID requires/);
37
+ });
@@ -4,7 +4,29 @@ import path from 'node:path';
4
4
  import { loadJSONFile } from './utils.ts';
5
5
 
6
6
  export async function playAttach(projectPath: string, args: string[]) {
7
- const runtimeFile = path.join(projectPath, '.app/runtime/-.json');
7
+ const runId = process.env.VONA_TEST_RUN_ID;
8
+ if (runId) {
9
+ const match = /^(\d{14})-[a-f0-9]{8}$/.exec(runId);
10
+ const timestamp = match?.[1];
11
+ const date = timestamp
12
+ ? new Date(
13
+ `${timestamp.slice(0, 4)}-${timestamp.slice(4, 6)}-${timestamp.slice(6, 8)}T${timestamp.slice(8, 10)}:${timestamp.slice(10, 12)}:${timestamp.slice(12, 14)}Z`,
14
+ )
15
+ : undefined;
16
+ if (
17
+ process.env.META_MODE !== 'test' ||
18
+ !timestamp ||
19
+ !date ||
20
+ Number.isNaN(date.getTime()) ||
21
+ date.toISOString().slice(0, 19).replace(/[-:T]/g, '') !== timestamp
22
+ ) {
23
+ throw new Error(
24
+ 'VONA_TEST_RUN_ID requires test mode and a UTC timestamp plus eight hex digits',
25
+ );
26
+ }
27
+ }
28
+ const dataDir = runId ? `.app-test-${runId}` : '.app';
29
+ const runtimeFile = path.join(projectPath, dataDir, 'runtime/-.json');
8
30
  if (!fse.existsSync(runtimeFile)) throw new Error('dev server not running');
9
31
  // body
10
32
  const body = { args, projectPath };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.156",
3
+ "version": "1.1.158",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -5,6 +5,7 @@ import { BeanCliBase } from '@cabloy/cli';
5
5
  import fse from 'fs-extra';
6
6
  import path from 'node:path';
7
7
  import { rimraf } from 'rimraf';
8
+ import { resolveVonaLocalPaths } from 'vona-core';
8
9
 
9
10
  import type { VonaBinConfigOptions } from './toolsBin/types.ts';
10
11
 
@@ -23,23 +24,27 @@ export class CliBinDbReset extends BeanCliBase {
23
24
  // super
24
25
  await super.execute();
25
26
  const projectPath = argv.projectPath;
26
- // test
27
+ // recreate the development database
27
28
  await this._dbReset(projectPath);
28
29
  }
29
30
 
30
31
  async _dbReset(projectPath: string) {
31
32
  const { argv } = this.context;
32
- const mode: VonaMetaMode = 'test';
33
+ const mode: VonaMetaMode = 'dev';
33
34
  const flavor: VonaMetaFlavor = argv.flavor || 'normal';
34
35
  const configMeta: VonaConfigMeta = { flavor, mode };
36
+ const paths = resolveVonaLocalPaths(projectPath, mode);
35
37
  const configOptions: VonaBinConfigOptions = {
36
38
  appDir: projectPath,
37
- runtimeDir: '.vona',
39
+ runtimeDir: paths.generatedDir,
38
40
  workers: 1,
39
41
  };
40
- const { modulesMeta } = await generateVonaMeta(configMeta, configOptions);
41
- await this._run(projectPath, modulesMeta);
42
- await rimraf(path.join(projectPath, '.vona'));
42
+ try {
43
+ const { modulesMeta } = await generateVonaMeta(configMeta, configOptions);
44
+ await this._run(projectPath, modulesMeta);
45
+ } finally {
46
+ await rimraf(paths.generatedRoot);
47
+ }
43
48
  }
44
49
 
45
50
  async _run(projectPath: string, _modulesMeta: Awaited<ReturnType<typeof glob>>) {
@@ -50,7 +55,11 @@ export class CliBinDbReset extends BeanCliBase {
50
55
  }
51
56
  // run
52
57
  let args: string[] = [];
53
- args = args.concat([getImportEsm(), testFile, projectPath]);
58
+ args = args.concat([
59
+ getImportEsm(resolveVonaLocalPaths(projectPath, 'dev').generatedDir),
60
+ testFile,
61
+ projectPath,
62
+ ]);
54
63
  // args = args.concat(['--experimental-transform-types', getImportEsm(), testFile, projectPath]);
55
64
  await this.helper.spawnExe({
56
65
  cmd: 'node',
@@ -3,9 +3,9 @@ import type { VonaConfigMeta, VonaMetaFlavor, VonaMetaMode } from '@cabloy/modul
3
3
 
4
4
  import { BeanCliBase } from '@cabloy/cli';
5
5
  import { spawn } from 'node:child_process';
6
- import path from 'node:path';
7
6
  import nodemon from 'nodemon';
8
7
  import { rimraf } from 'rimraf';
8
+ import { resolveVonaLocalPaths } from 'vona-core';
9
9
 
10
10
  import type { VonaBinConfigOptions } from './toolsBin/types.ts';
11
11
 
@@ -48,9 +48,10 @@ export class CliBinDev extends BeanCliBase {
48
48
  const workers = resolveTestWorkers(mode, argv.workers);
49
49
  const flavor: VonaMetaFlavor = argv.flavor || 'normal';
50
50
  const configMeta: VonaConfigMeta = { flavor, mode };
51
+ const paths = resolveVonaLocalPaths(projectPath, mode);
51
52
  const configOptions: VonaBinConfigOptions = {
52
53
  appDir: projectPath,
53
- runtimeDir: '.vona',
54
+ runtimeDir: paths.generatedDir,
54
55
  workers,
55
56
  };
56
57
  try {
@@ -61,15 +62,20 @@ export class CliBinDev extends BeanCliBase {
61
62
  await this._run(projectPath, modulesMeta);
62
63
  }
63
64
  } finally {
64
- await rimraf(path.join(projectPath, '.vona'));
65
+ if (!paths.runId) await rimraf(paths.generatedRoot);
65
66
  }
66
67
  }
67
68
 
68
69
  async _runTest(projectPath: string) {
69
- const child = spawn(process.execPath, [getImportEsm(), '.vona/bootstrap.ts'], {
70
- cwd: projectPath,
71
- stdio: 'inherit',
72
- });
70
+ const generatedDir = resolveVonaLocalPaths(projectPath, 'test').generatedDir;
71
+ const child = spawn(
72
+ process.execPath,
73
+ [getImportEsm(generatedDir), `${generatedDir}/bootstrap.ts`],
74
+ {
75
+ cwd: projectPath,
76
+ stdio: 'inherit',
77
+ },
78
+ );
73
79
  const forwardSignal = (signal: NodeJS.Signals) => child.kill(signal);
74
80
  const onInterrupt = () => forwardSignal('SIGINT');
75
81
  const onTerminate = () => forwardSignal('SIGTERM');
@@ -3,6 +3,7 @@ import type { VonaConfigMeta, VonaMetaFlavor, VonaMetaMode } from '@cabloy/modul
3
3
  import { BeanCliBase } from '@cabloy/cli';
4
4
  import fse from 'fs-extra';
5
5
  import path from 'node:path';
6
+ import { resolveVonaLocalPaths } from 'vona-core';
6
7
 
7
8
  import type { VonaBinConfigOptions } from './toolsBin/types.ts';
8
9
 
@@ -49,7 +50,10 @@ export class CliBinPlay extends BeanCliBase {
49
50
  }
50
51
 
51
52
  async _runAttach(projectPath: string) {
52
- const runtimeFile = path.join(projectPath, '.app/runtime/-.json');
53
+ const runtimeFile = path.join(
54
+ resolveVonaLocalPaths(projectPath, process.env.META_MODE ?? 'dev').runtimeRoot,
55
+ '-.json',
56
+ );
53
57
  if (!fse.existsSync(runtimeFile)) throw new Error('dev server not running');
54
58
  // args
55
59
  let args: string[] = [];
@@ -6,6 +6,7 @@ import { catchError } from '@cabloy/utils';
6
6
  import fse from 'fs-extra';
7
7
  import path from 'node:path';
8
8
  import { rimraf } from 'rimraf';
9
+ import { resolveVonaLocalPaths } from 'vona-core';
9
10
 
10
11
  import type { VonaBinConfigOptions } from './toolsBin/types.ts';
11
12
 
@@ -34,17 +35,21 @@ export class CliBinTest extends BeanCliBase {
34
35
  const mode: VonaMetaMode = 'test';
35
36
  const flavor: VonaMetaFlavor = argv.flavor || 'normal';
36
37
  const configMeta: VonaConfigMeta = { flavor, mode };
38
+ const paths = resolveVonaLocalPaths(projectPath, mode);
37
39
  const configOptions: VonaBinConfigOptions = {
38
40
  appDir: projectPath,
39
- runtimeDir: '.vona',
41
+ runtimeDir: paths.generatedDir,
40
42
  workers: 1,
41
43
  };
42
- const { modulesMeta } = await generateVonaMeta(configMeta, configOptions);
43
- await this._run(projectPath, modulesMeta);
44
- if (argv.coverage && !argv.ci) {
45
- await this._outputCoverageReportViewer(projectPath);
44
+ try {
45
+ const { modulesMeta } = await generateVonaMeta(configMeta, configOptions);
46
+ await this._run(projectPath, modulesMeta);
47
+ if (argv.coverage && !argv.ci) {
48
+ await this._outputCoverageReportViewer(projectPath);
49
+ }
50
+ } finally {
51
+ if (!paths.runId) await rimraf(paths.generatedRoot);
46
52
  }
47
- await rimraf(path.join(projectPath, '.vona'));
48
53
  }
49
54
 
50
55
  async _run(projectPath: string, modulesMeta: Awaited<ReturnType<typeof glob>>) {
@@ -70,7 +75,7 @@ export class CliBinTest extends BeanCliBase {
70
75
  args.push('--import=why-is-node-running/include');
71
76
  }
72
77
  args = args.concat([
73
- getImportEsm(),
78
+ getImportEsm(resolveVonaLocalPaths(projectPath, 'test').generatedDir),
74
79
  testFile,
75
80
  projectPath,
76
81
  (!!argv.coverage).toString(),
@@ -4,6 +4,7 @@ import * as dotenv from '@cabloy/dotenv';
4
4
  import { glob } from '@cabloy/module-glob';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
+ import { getTestAppName, resolveTestRunId, TEST_RUN_ID_ENV } from 'vona-core';
7
8
 
8
9
  import type { VonaBinConfigOptions } from './types.ts';
9
10
 
@@ -27,6 +28,10 @@ export function createConfigUtils(
27
28
  function __loadEnvs(): NodeJS.ProcessEnv {
28
29
  const meta = getEnvMeta(configMeta);
29
30
  const envDir = path.join(configOptions.appDir, 'env');
31
+ const runId = resolveTestRunId(meta.mode, process.env[TEST_RUN_ID_ENV]);
32
+ if (runId && process.env.VONA_DB_RESET !== undefined) {
33
+ throw new Error('Isolated test cannot run a development database reset');
34
+ }
30
35
  const envs = dotenv.loadEnvs(meta, envDir, '.env');
31
36
  const res = Object.assign(
32
37
  {
@@ -38,6 +43,20 @@ export function createConfigUtils(
38
43
  META_MODE: meta.mode,
39
44
  },
40
45
  );
46
+ if (runId) {
47
+ res[TEST_RUN_ID_ENV] = runId;
48
+ res.APP_NAME = getTestAppName(runId);
49
+ const port = Number(process.env.SERVER_LISTEN_PORT);
50
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
51
+ throw new Error('Isolated test requires an assigned SERVER_LISTEN_PORT');
52
+ }
53
+ res.SERVER_LISTEN_PORT = String(port);
54
+ res.SERVER_LISTEN_HOSTNAME = '127.0.0.1';
55
+ if (process.env.VONA_TEST_SERVER_ORIGIN === 'true') {
56
+ res.SERVER_SERVE_PROTOCOL = 'http';
57
+ res.SERVER_SERVE_HOST = `127.0.0.1:${port}`;
58
+ }
59
+ }
41
60
  if (configOptions.workers !== undefined) {
42
61
  res.SERVER_WORKERS = configOptions.workers.toString();
43
62
  }
@@ -49,8 +68,8 @@ export function createConfigUtils(
49
68
  res.SERVER_WORKERS = '1';
50
69
  }
51
70
  }
52
- for (const key of ['NODE_ENV', 'SERVER_WORKERS', 'META_FLAVOR', 'META_MODE']) {
53
- if ((res[key] as any) !== false) {
71
+ for (const key of ['NODE_ENV', 'SERVER_WORKERS', 'META_FLAVOR', 'META_MODE', TEST_RUN_ID_ENV]) {
72
+ if (res[key] !== undefined && (res[key] as any) !== false) {
54
73
  process.env[key] = res[key];
55
74
  }
56
75
  }
@@ -1,5 +1,7 @@
1
1
  import type { VonaConfigMeta } from '@cabloy/module-info';
2
2
 
3
+ import { assertTestRunOwner, resolveVonaLocalPaths } from 'vona-core';
4
+
3
5
  import type { VonaBinConfigOptions } from './types.ts';
4
6
 
5
7
  import { createConfigUtils } from './configUtils.ts';
@@ -9,6 +11,14 @@ export async function generateVonaMeta(
9
11
  configMeta: VonaConfigMeta,
10
12
  configOptions: VonaBinConfigOptions,
11
13
  ) {
14
+ const paths = resolveVonaLocalPaths(configOptions.appDir, configMeta.mode);
15
+ if (paths.runId) {
16
+ if (configOptions.runtimeDir !== paths.generatedDir) {
17
+ throw new Error('Isolated test generated directory does not match its run ID');
18
+ }
19
+ assertTestRunOwner(paths.dataRoot, paths.runId);
20
+ assertTestRunOwner(paths.generatedRoot, paths.runId);
21
+ }
12
22
  // config utils
13
23
  const configUtils = createConfigUtils(configMeta, configOptions);
14
24
  // env