cabloy 5.1.204 → 5.1.205

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 +11 -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 +426 -0
  35. package/scripts/runIsolatedTest.ts +454 -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,426 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { createHmac } from 'node:crypto';
4
+ import {
5
+ existsSync,
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ rmSync,
11
+ symlinkSync,
12
+ writeFileSync,
13
+ } from 'node:fs';
14
+ import { createServer } from 'node:http';
15
+ import { tmpdir } from 'node:os';
16
+ import { join, resolve } from 'node:path';
17
+ import { test } from 'node:test';
18
+
19
+ import type { VonaApplication } from '../vona/packages-vona/vona-core/src/lib/core/application.ts';
20
+
21
+ import {
22
+ assertIsolatedTestConfig,
23
+ assertIsolatedTestPath,
24
+ } from '../vona/packages-vona/vona-core/src/lib/core/isolationGuard.ts';
25
+ import {
26
+ loadTestClaim,
27
+ startTestClaimBroker,
28
+ } from '../vona/packages-vona/vona-core/src/lib/core/testClaim.ts';
29
+ import {
30
+ allocateTestPort,
31
+ createOwned,
32
+ createTestRunId,
33
+ getUnitTestCommands,
34
+ reserveTestRun,
35
+ ROOT_DIR,
36
+ runIsolatedProcess,
37
+ runIsolatedTest,
38
+ verifyReadyProof,
39
+ waitForReady,
40
+ } from './runIsolatedTest.ts';
41
+
42
+ const isolationSource = resolve(
43
+ ROOT_DIR,
44
+ 'vona/packages-vona/vona-core/src/lib/core/testIsolation.ts',
45
+ );
46
+
47
+ test('public test scripts delegate to the isolated runner without recursion', () => {
48
+ const root = JSON.parse(readFileSync(resolve(ROOT_DIR, 'package.json'), 'utf8'));
49
+ const original = JSON.parse(
50
+ readFileSync(resolve(ROOT_DIR, 'vona/package.original.json'), 'utf8'),
51
+ );
52
+ const working = JSON.parse(readFileSync(resolve(ROOT_DIR, 'vona/package.json'), 'utf8'));
53
+ assert.equal(root.scripts.test, 'cd vona && npm run test');
54
+ assert.equal(root.scripts['test:e2e'], 'node scripts/runIsolatedTest.ts e2e');
55
+ assert.equal(original.scripts.test, 'node ../scripts/runIsolatedTest.ts unit');
56
+ assert.equal(original.scripts.cov, 'node ../scripts/runIsolatedTest.ts unit --coverage');
57
+ assert.equal(working.scripts.test, original.scripts.test);
58
+ assert.equal(working.scripts.cov, original.scripts.cov);
59
+
60
+ const commands = getUnitTestCommands(['--coverage', '--ci', 'src/test/example.test.ts']);
61
+ assert.deepEqual(commands, [
62
+ { command: 'npm', args: ['run', 'prerun'], cwd: resolve(ROOT_DIR, 'vona') },
63
+ {
64
+ command: process.execPath,
65
+ args: [
66
+ './packages-cli/cli/src/bin/vona.ts',
67
+ ':bin:test',
68
+ '--flavor=normal',
69
+ '--coverage',
70
+ '--ci=true',
71
+ 'src/test/example.test.ts',
72
+ ],
73
+ cwd: resolve(ROOT_DIR, 'vona'),
74
+ },
75
+ ]);
76
+ assert.deepEqual(getUnitTestCommands(['src/test/example.test.ts', '--ci'])[1].args.slice(-2), [
77
+ 'src/test/example.test.ts',
78
+ '--ci=true',
79
+ ]);
80
+ });
81
+
82
+ test('creates a UTC timestamp and eight random hex digits for one run identity', () => {
83
+ const date = new Date('2026-10-14T01:02:03.456Z');
84
+ assert.match(createTestRunId(date), /^20261014010203-[a-f0-9]{8}$/);
85
+ });
86
+
87
+ test('creates exclusive manifest claims and refuses stale or symlinked roots', async () => {
88
+ const root = mkdtempSync(join(realpathSync(tmpdir()), 'cabloy-run-claim-'));
89
+ const vonaDir = join(root, 'vona');
90
+ const id = '20261014010203-a1b2c3d4';
91
+ try {
92
+ mkdirSync(vonaDir);
93
+ const claim = await reserveTestRun(vonaDir, id);
94
+ assert.equal(claim, join(vonaDir, '.vona-test-claim', `${id}.json`));
95
+ await assert.rejects(reserveTestRun(vonaDir, id), { code: 'EEXIST' });
96
+ const manifest = JSON.parse(readFileSync(claim, 'utf8'));
97
+ assert.equal(manifest.runId, id);
98
+ assert.equal(manifest.createdAt, '2026-10-14T01:02:03.000Z');
99
+ assert.equal(manifest.owner.pid, process.pid);
100
+ assert.ok(existsSync(claim));
101
+
102
+ const staleRoot = join(vonaDir, `.app-test-${id}`);
103
+ mkdirSync(staleRoot);
104
+ writeFileSync(join(staleRoot, 'business-data'), 'keep');
105
+ assert.throws(() => createOwned(staleRoot, id), /already exists/);
106
+ assert.equal(readFileSync(join(staleRoot, 'business-data'), 'utf8'), 'keep');
107
+ assert.equal(existsSync(join(staleRoot, '.vona-test-owner')), false);
108
+ const linkedRoot = join(vonaDir, `.vona-test-${id}`);
109
+ symlinkSync(staleRoot, linkedRoot);
110
+ assert.throws(() => createOwned(linkedRoot, id), /already exists/);
111
+ assert.equal(readFileSync(join(staleRoot, 'business-data'), 'utf8'), 'keep');
112
+
113
+ const linked = join(root, 'linked');
114
+ symlinkSync(vonaDir, linked);
115
+ await assert.rejects(reserveTestRun(linked, '20261014010204-d4e5f6a7'), /Unsafe/);
116
+ assert.equal(
117
+ existsSync(join(vonaDir, '.vona-test-claim', '20261014010204-d4e5f6a7.json')),
118
+ false,
119
+ );
120
+ } finally {
121
+ rmSync(root, { recursive: true, force: true });
122
+ }
123
+ });
124
+
125
+ test('allocates a loopback listener without a persistent reservation', async () => {
126
+ const first = await allocateTestPort();
127
+ assert.ok(first > 0 && first <= 65535);
128
+ });
129
+
130
+ test('accepts only a signed, challenge-matching readiness response', () => {
131
+ const key = 'a'.repeat(64);
132
+ const challenge = 'b'.repeat(32);
133
+ const proof = createHmac('sha256', key).update(challenge).digest('hex');
134
+ assert.equal(verifyReadyProof(challenge, key, proof), true);
135
+ assert.equal(verifyReadyProof('c'.repeat(32), key, proof), false);
136
+ assert.equal(verifyReadyProof(challenge, 'c'.repeat(64), proof), false);
137
+ assert.equal(verifyReadyProof(challenge, key, 'f'.repeat(64)), false);
138
+ assert.equal(verifyReadyProof(challenge, key, null), false);
139
+ });
140
+
141
+ test('refuses an impostor occupying the allocated E2E port', async () => {
142
+ const port = await allocateTestPort();
143
+ const impostorKey = 'b'.repeat(64);
144
+ const impostor = createServer((request, response) => {
145
+ const challenge = request.headers['x-vona-test-challenge'];
146
+ const proof = createHmac('sha256', impostorKey).update(String(challenge)).digest('hex');
147
+ response.writeHead(200, {
148
+ 'content-type': 'application/json',
149
+ 'x-vona-test-proof': proof,
150
+ });
151
+ response.end('{"status":"ok"}');
152
+ });
153
+ await new Promise<void>((resolvePromise, reject) => {
154
+ impostor.once('error', reject);
155
+ impostor.listen(port, '127.0.0.1', resolvePromise);
156
+ });
157
+ try {
158
+ await assert.rejects(
159
+ waitForReady(
160
+ port,
161
+ { wait: () => new Promise(() => {}), stop: async () => undefined } as never,
162
+ 'a'.repeat(64),
163
+ ),
164
+ /does not belong to this test run/,
165
+ );
166
+ } finally {
167
+ await new Promise<void>((resolvePromise, reject) => {
168
+ impostor.close(error => (error ? reject(error) : resolvePromise()));
169
+ });
170
+ }
171
+ });
172
+
173
+ test('rejects external E2E targets before creating run directories', async () => {
174
+ const previous = process.env.E2E_BASE_URL;
175
+ process.env.E2E_BASE_URL = 'https://example.invalid';
176
+ try {
177
+ await assert.rejects(runIsolatedTest('unit', []), /E2E_BASE_URL is unsupported/);
178
+ } finally {
179
+ if (previous === undefined) delete process.env.E2E_BASE_URL;
180
+ else process.env.E2E_BASE_URL = previous;
181
+ }
182
+ });
183
+
184
+ test('resolves distinct run paths and rejects invalid identity without touching dev paths', () => {
185
+ const script = `
186
+ import { resolveVonaLocalPaths, getTestAppName } from ${JSON.stringify(isolationSource)};
187
+ const project = '/project/vona';
188
+ const a = resolveVonaLocalPaths(project, 'test', '20261014010203-a1b2c3d4');
189
+ const b = resolveVonaLocalPaths(project, 'test', '20261014010203-d4e5f6a7');
190
+ if (a.generatedRoot === b.generatedRoot || a.dataRoot === b.dataRoot) process.exit(1);
191
+ if (getTestAppName(a.runId) === getTestAppName(b.runId)) process.exit(2);
192
+ if (resolveVonaLocalPaths(project, 'dev', undefined).dataDir !== '.app') process.exit(3);
193
+ if (resolveVonaLocalPaths(project, 'prod', undefined).generatedDir !== '.vona') process.exit(4);
194
+ try { resolveVonaLocalPaths(project, 'dev', a.runId); process.exit(5); } catch {}
195
+ try { resolveVonaLocalPaths(project, 'test', 'not-a-run-id'); process.exit(6); } catch {}
196
+ `;
197
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
198
+ cwd: ROOT_DIR,
199
+ encoding: 'utf8',
200
+ });
201
+ assert.equal(result.status, 0, result.stderr);
202
+ });
203
+
204
+ test('owner checks reject unowned paths and symlinked leaves', () => {
205
+ const root = mkdtempSync(join(tmpdir(), 'cabloy-test-isolation-'));
206
+ const id = '20261014010203-a1b2c3d4';
207
+ const script = `
208
+ import { assertTestRunOwner } from ${JSON.stringify(isolationSource)};
209
+ const [unowned, linked] = process.argv.slice(1);
210
+ try { assertTestRunOwner(unowned, ${JSON.stringify(id)}); process.exit(1); } catch {}
211
+ try { assertTestRunOwner(linked, ${JSON.stringify(id)}); process.exit(2); } catch {}
212
+ `;
213
+ try {
214
+ const owned = join(root, `.app-test-${id}`);
215
+ mkdirSync(owned);
216
+ writeFileSync(join(owned, '.vona-test-owner'), `${id}\n`);
217
+ const unowned = join(root, 'unowned');
218
+ mkdirSync(unowned);
219
+ const linked = join(root, 'linked');
220
+ symlinkSync(owned, linked);
221
+ const result = spawnSync(
222
+ process.execPath,
223
+ ['--input-type=module', '-e', script, unowned, linked],
224
+ {
225
+ cwd: ROOT_DIR,
226
+ encoding: 'utf8',
227
+ },
228
+ );
229
+ assert.equal(result.status, 0, result.stderr);
230
+ assert.ok(existsSync(owned));
231
+ } finally {
232
+ rmSync(root, { recursive: true, force: true });
233
+ }
234
+ });
235
+
236
+ test('waits for descendants before reporting a successful child exit', async () => {
237
+ if (process.platform === 'win32') return;
238
+ const root = mkdtempSync(join(tmpdir(), 'cabloy-process-group-'));
239
+ const flag = join(root, 'descendant-finished');
240
+ try {
241
+ const script = `
242
+ import { spawn } from 'node:child_process';
243
+ const child = spawn(process.execPath, ['-e', 'setTimeout(() => require("node:fs").writeFileSync(process.argv[1], "done"), 400) ', process.argv[1]], { stdio: 'ignore' });
244
+ child.unref();
245
+ `;
246
+ await runIsolatedProcess(
247
+ process.execPath,
248
+ ['--input-type=module', '-e', script, flag],
249
+ ROOT_DIR,
250
+ { ...process.env },
251
+ );
252
+ assert.equal(existsSync(flag), true);
253
+ } finally {
254
+ rmSync(root, { recursive: true, force: true });
255
+ }
256
+ });
257
+
258
+ test('a guardian waits for an inherited-group descendant before success', async () => {
259
+ if (process.platform === 'win32') return;
260
+ const root = mkdtempSync(join(resolve(ROOT_DIR, 'vona'), '.e2e-group-test-'));
261
+ const flag = join(root, 'server-finished');
262
+ try {
263
+ const script = `
264
+ import { spawn } from 'node:child_process';
265
+ const child = spawn(process.execPath, ['-e', 'setTimeout(() => require("node:fs").writeFileSync(process.argv[1], "done"), 400)', process.argv[1]], { stdio: 'ignore' });
266
+ child.unref();
267
+ `;
268
+ await runIsolatedProcess(
269
+ process.execPath,
270
+ ['--input-type=module', '-e', script, flag],
271
+ ROOT_DIR,
272
+ { ...process.env },
273
+ );
274
+ assert.equal(existsSync(flag), true);
275
+ } finally {
276
+ rmSync(root, { recursive: true, force: true });
277
+ }
278
+ });
279
+
280
+ test('managed guardians journal registration and drainage for successful and failed children', async () => {
281
+ const root = mkdtempSync(join(realpathSync(tmpdir()), 'cabloy-guardian-claim-'));
282
+ const projectPath = join(root, 'vona');
283
+ mkdirSync(projectPath);
284
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
285
+ try {
286
+ const runId = createTestRunId();
287
+ const claimPath = await reserveTestRun(projectPath, runId);
288
+ broker = await startTestClaimBroker(claimPath);
289
+ const env = { ...process.env, ...broker.env, META_MODE: 'test', VONA_TEST_RUN_ID: runId };
290
+ await runIsolatedProcess(process.execPath, ['-e', 'process.exit(0)'], ROOT_DIR, env);
291
+ await assert.rejects(
292
+ runIsolatedProcess(process.execPath, ['-e', 'process.exit(7)'], ROOT_DIR, env),
293
+ /Isolated test exited: 7/,
294
+ );
295
+ const claim = await loadTestClaim(claimPath);
296
+ assert.equal(claim.processes.length, 2);
297
+ for (const item of claim.processes) {
298
+ assert.equal(item.stage, 'drained');
299
+ assert.equal(item.role, 'guardian');
300
+ assert.equal(item.pid, item.pgid);
301
+ assert.ok(item.host);
302
+ assert.ok(item.startTime);
303
+ }
304
+ } finally {
305
+ await broker?.close();
306
+ rmSync(root, { recursive: true, force: true });
307
+ }
308
+ });
309
+
310
+ test('does not report success when a child exits nonzero', async () => {
311
+ if (process.platform === 'win32') return;
312
+ await assert.rejects(
313
+ runIsolatedProcess(process.execPath, ['-e', 'process.exit(7)'], ROOT_DIR, { ...process.env }),
314
+ /Isolated test exited: 7/,
315
+ );
316
+ });
317
+
318
+ test('interrupts an owned group and waits for its graceful child shutdown', async () => {
319
+ if (process.platform === 'win32') return;
320
+ const root = mkdtempSync(join(resolve(ROOT_DIR, 'vona'), '.guardian-interrupt-test-'));
321
+ const readyFile = join(root, 'ready');
322
+ const stoppedFile = join(root, 'stopped');
323
+ try {
324
+ const script = `
325
+ import { writeFileSync } from 'node:fs';
326
+ writeFileSync(process.argv[1], 'ready');
327
+ process.on('SIGINT', () => {
328
+ setTimeout(() => {
329
+ writeFileSync(process.argv[2], 'stopped');
330
+ process.exit(0);
331
+ }, 150);
332
+ });
333
+ setInterval(() => {}, 1000);
334
+ `;
335
+ const pending = runIsolatedProcess(
336
+ process.execPath,
337
+ ['--input-type=module', '-e', script, readyFile, stoppedFile],
338
+ ROOT_DIR,
339
+ { ...process.env },
340
+ );
341
+ const deadline = Date.now() + 5000;
342
+ while (!existsSync(readyFile) && Date.now() < deadline) {
343
+ await new Promise(resolvePromise => setTimeout(resolvePromise, 50));
344
+ }
345
+ assert.ok(existsSync(readyFile));
346
+ process.emit('SIGINT');
347
+ await assert.rejects(pending, /Isolated test exited/);
348
+ assert.ok(existsSync(stoppedFile));
349
+ } finally {
350
+ rmSync(root, { recursive: true, force: true });
351
+ }
352
+ });
353
+
354
+ test('rejects merged paths, Redis prefixes, and database overrides before cleanup', () => {
355
+ const root = mkdtempSync(join(resolve(ROOT_DIR, 'vona'), '.isolation-guard-test-'));
356
+ const id = '20261014010203-a1b2c3d4';
357
+ const name = `cabtest${id}`;
358
+ const projectPath = join(root, 'vona');
359
+ const dataRoot = join(projectPath, `.app-test-${id}`);
360
+ const generatedRoot = join(projectPath, `.vona-test-${id}`);
361
+ const port = 42321;
362
+ try {
363
+ mkdirSync(dataRoot, { recursive: true });
364
+ mkdirSync(generatedRoot);
365
+ writeFileSync(join(dataRoot, '.vona-test-owner'), `${id}\n`);
366
+ writeFileSync(join(generatedRoot, '.vona-test-owner'), `${id}\n`);
367
+ const sentinel = join(root, 'dev-business-data');
368
+ writeFileSync(sentinel, 'keep');
369
+ const config = {
370
+ server: {
371
+ publicDir: join(dataRoot, 'public'),
372
+ workers: 1,
373
+ listen: { hostname: '127.0.0.1', port },
374
+ serve: { protocol: 'http', host: `127.0.0.1:${port}` },
375
+ },
376
+ logger: { baseDir: join(dataRoot, 'logs') },
377
+ database: {
378
+ testDatabase: true,
379
+ base: {},
380
+ defaultClient: 'sqlite3',
381
+ clients: { sqlite3: { client: 'better-sqlite3', connection: { filename: '' } } },
382
+ },
383
+ instance: { instances: {} },
384
+ redis: { base: {}, clients: { default: { keyPrefix: true }, queue: {} } },
385
+ };
386
+ const app = {
387
+ name,
388
+ projectPath,
389
+ meta: { env: { META_MODE: 'test', VONA_TEST_RUN_ID: id, SERVER_LISTEN_PORT: String(port) } },
390
+ config,
391
+ } as unknown as VonaApplication;
392
+ assertIsolatedTestConfig(app);
393
+ config.server.publicDir = sentinel;
394
+ assert.throws(() => assertIsolatedTestConfig(app), /path escapes/);
395
+ config.server.publicDir = join(dataRoot, 'public');
396
+ symlinkSync(sentinel, config.server.publicDir);
397
+ assert.throws(() => assertIsolatedTestConfig(app), /symlink/);
398
+ rmSync(config.server.publicDir);
399
+ config.redis.clients.default.keyPrefix = false as unknown as true;
400
+ assert.throws(() => assertIsolatedTestConfig(app), /framework-managed Redis prefix/);
401
+ config.redis.clients.default.keyPrefix = true;
402
+ config.database.clients.sqlite3.connection.filename = sentinel;
403
+ assert.throws(() => assertIsolatedTestConfig(app), /discovery connection/);
404
+ config.database.clients.sqlite3.connection.filename = '';
405
+ (config.database.base as { connection?: { connectionString?: string } }).connection = {
406
+ connectionString: 'postgres://localhost/development',
407
+ };
408
+ assert.throws(() => assertIsolatedTestConfig(app), /base connection target or pool/);
409
+ delete (config.database.base as { connection?: unknown }).connection;
410
+ (config.database.clients.sqlite3 as { connectionPool?: unknown }).connectionPool = {};
411
+ assert.throws(() => assertIsolatedTestConfig(app), /external target or pool/);
412
+ delete (config.database.clients.sqlite3 as { connectionPool?: unknown }).connectionPool;
413
+ (config.database.clients as Record<string, unknown>).secondary = {
414
+ client: 'pg',
415
+ connection: { database: 'postgres', connectionString: 'postgres://localhost/development' },
416
+ };
417
+ assert.throws(() => assertIsolatedTestConfig(app), /external target or pool: secondary/);
418
+ assert.equal(existsSync(sentinel), true);
419
+ assert.equal(
420
+ assertIsolatedTestPath(join(dataRoot, 'public'), join(dataRoot, 'public')),
421
+ undefined,
422
+ );
423
+ } finally {
424
+ rmSync(root, { recursive: true, force: true });
425
+ }
426
+ });