cabloy 5.1.203 → 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 (127) 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 +6 -5
  8. package/repo-agent-governance/managed-assets.json +9 -9
  9. package/repo-agent-governance/policies/repository.md +5 -2
  10. package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +2 -2
  11. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -0
  12. package/repo-docs/backend/migration-and-changes.md +13 -12
  13. package/repo-docs/backend/orm-select-guide.md +6 -0
  14. package/repo-docs/backend/runtime-and-flavors.md +2 -1
  15. package/repo-docs/backend/transaction-guide.md +12 -0
  16. package/repo-docs/backend/unit-testing.md +12 -8
  17. package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/cover-en-v1.png +0 -0
  18. package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/images/completion-summary.jpg +0 -0
  19. package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/images/multitasking-draft.jpg +0 -0
  20. package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/images/ssr-refresh.jpg +0 -0
  21. package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/index.md +216 -0
  22. package/repo-docs/blogs/index.md +15 -0
  23. package/repo-docs/frontend/scripts.md +1 -1
  24. package/repo-docs/fullstack/parallel-worktree-environment.md +6 -6
  25. package/repo-docs/fullstack/ssr-entry-modes.md +19 -0
  26. package/repo-docs/index.md +2 -0
  27. package/repo-docs/reference/repo-scripts.md +22 -1
  28. package/repo-e2e/config/playwright.config.ts +32 -14
  29. package/repo-e2e/docs/specs/blogs-index.spec.ts +23 -6
  30. package/repo-e2e/scripts/e2e.test.ts +39 -0
  31. package/repo-e2e/scripts/e2e.ts +19 -1
  32. package/repo-e2e/scripts/managedTest.test.ts +389 -0
  33. package/repo-e2e/scripts/managedTest.ts +210 -0
  34. package/repo-e2e/scripts/managedTestImports.test.ts +74 -0
  35. package/repo-e2e/scripts/managedTestImports.ts +110 -0
  36. package/repo-e2e/scripts/runE2e.ts +12 -1
  37. package/repo-e2e/scripts/startE2eVona.ts +37 -5
  38. package/repo-e2e/specs/a-commerce.spec.ts +1 -1
  39. package/repo-e2e/specs/cabloy-basic.spec.ts +1 -1
  40. package/repo-e2e/specs/controller-boundary.spec.ts +1 -1
  41. package/repo-e2e/specs/home-user-account.spec.ts +2 -1
  42. package/repo-e2e/specs/markdown.spec.ts +1 -1
  43. package/repo-e2e/specs/routed-dialog.spec.ts +1 -1
  44. package/scripts/runIsolatedTest.test.ts +426 -0
  45. package/scripts/runIsolatedTest.ts +454 -0
  46. package/scripts/testClaimCleanup.test.ts +346 -0
  47. package/scripts/testClaimCleanup.ts +375 -0
  48. package/scripts/testClaimProcesses.test.ts +106 -0
  49. package/scripts/testClaimProcesses.ts +129 -0
  50. package/scripts/testProcessGuardian.ts +163 -0
  51. package/scripts/upgrade.ts +1 -16
  52. package/vona/oxfmt.config.ts +2 -0
  53. package/vona/oxlint.config.ts +2 -0
  54. package/vona/package.original.json +2 -2
  55. package/vona/packages-cli/cli/package.json +1 -1
  56. package/vona/packages-cli/cli/src/play.test.ts +37 -0
  57. package/vona/packages-cli/cli/src/play.ts +23 -1
  58. package/vona/packages-cli/cli-set-api/package.json +1 -1
  59. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dbReset.ts +16 -7
  60. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +13 -7
  61. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.play.ts +5 -1
  62. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.test.ts +12 -7
  63. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/configUtils.ts +21 -2
  64. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateVonaMeta.ts +10 -0
  65. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/resolveTestCleanupConfig.test.ts +153 -0
  66. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/resolveTestCleanupConfig.ts +141 -0
  67. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupAdapters.test.ts +148 -0
  68. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupAdapters.ts +127 -0
  69. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupProcess.test.ts +47 -0
  70. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupProcess.ts +139 -0
  71. package/vona/packages-cli/cli-set-api/src/lib/utils.ts +2 -3
  72. package/vona/packages-cli/cli-set-api/templates/config/_tsconfig_project.json +1 -0
  73. package/vona/packages-cli/cli-set-api/templates/config/_tsconfig_source.json +1 -0
  74. package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +18 -0
  75. package/vona/packages-cli/cli-set-api/toolsIsolate/dbReset.ts +15 -2
  76. package/vona/packages-cli/cli-set-api/tsconfig.build.json +2 -1
  77. package/vona/packages-utils/babel-plugin-vona-bean-module/package.json +1 -1
  78. package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.test.ts +16 -0
  79. package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.ts +3 -2
  80. package/vona/packages-vona/vona/package.json +1 -1
  81. package/vona/packages-vona/vona-core/package.json +1 -1
  82. package/vona/packages-vona/vona-core/src/lib/addon/sqlite3.ts +1 -1
  83. package/vona/packages-vona/vona-core/src/lib/core/config.ts +44 -6
  84. package/vona/packages-vona/vona-core/src/lib/core/configCleanup.test.ts +40 -0
  85. package/vona/packages-vona/vona-core/src/lib/core/index.ts +3 -0
  86. package/vona/packages-vona/vona-core/src/lib/core/isolationGuard.test.ts +301 -0
  87. package/vona/packages-vona/vona-core/src/lib/core/isolationGuard.ts +312 -0
  88. package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +577 -0
  89. package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +985 -0
  90. package/vona/packages-vona/vona-core/src/lib/core/testIsolation.ts +129 -0
  91. package/vona/packages-vona/vona-core/src/lib/framework/createApp.ts +2 -0
  92. package/vona/packages-vona/vona-core/src/lib/framework/start.ts +4 -0
  93. package/vona/packages-vona/vona-core/src/lib/framework/useApp.ts +5 -1
  94. package/vona/packages-vona/vona-core/src/types/application/app.ts +2 -0
  95. package/vona/packages-vona/vona-core/src/types/utils/env.ts +1 -0
  96. package/vona/packages-vona/vona-mock/package.json +1 -1
  97. package/vona/pnpm-lock.yaml +40 -40
  98. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentIndexes.test.ts +73 -10
  99. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  100. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  101. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  102. package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
  103. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +15 -1
  104. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts +24 -0
  105. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.ts +11 -0
  106. package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
  107. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.health.ts +15 -0
  108. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  109. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/extend/schemaBuilder.ts +11 -4
  110. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/databaseClient_.ts +6 -1
  111. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/package.json +1 -1
  112. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/bean/databaseDialect.betterSqlite3.ts +41 -1
  113. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/bean/databaseDialect.mysql.ts +4 -1
  114. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/bean/databaseDialect.pg.ts +2 -1
  115. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  116. package/vona/src/suite-vendor/a-vona/modules/a-redis/package.json +1 -1
  117. package/vona/src/suite-vendor/a-vona/modules/a-redis/src/lib/redis.ts +8 -0
  118. package/vona/src/suite-vendor/a-vona/modules/a-redis/src/service/redis.ts +23 -6
  119. package/vona/src/suite-vendor/a-vona/modules/a-redis/src/service/redisClient_.ts +18 -1
  120. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  121. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +43 -15
  122. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  123. package/vona/src/suite-vendor/a-vona/modules/a-version/src/lib/databaseTestName.ts +3 -37
  124. package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/database.ts +93 -15
  125. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  126. package/vona/tsconfig.json +1 -0
  127. package/scripts/initTestData.ts +0 -25
@@ -0,0 +1,577 @@
1
+ import '@cabloy/json5';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ chmodSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ rmSync,
11
+ symlinkSync,
12
+ } from 'node:fs';
13
+ import { hostname, tmpdir } from 'node:os';
14
+ import path from 'node:path';
15
+ import { test } from 'node:test';
16
+
17
+ import {
18
+ assertTestClaimDatabaseClient,
19
+ assertTestClaimRedisClient,
20
+ buildTestClaimResourceInventory,
21
+ createTestClaim,
22
+ getTestClaimCreatedAt,
23
+ getTestClaimPaths,
24
+ hasTestClaimTransport,
25
+ loadTestClaim,
26
+ mergeTestClaimRedisOptions,
27
+ normalizeTestClaimDatabaseEndpoint,
28
+ normalizeTestClaimRedisEndpoint,
29
+ registerTestClaimDatabaseCreation,
30
+ registerTestClaimResourceInventory,
31
+ sendTestClaimEvent,
32
+ startTestClaimBroker,
33
+ validateTestClaim,
34
+ writeTestClaim,
35
+ } from './testClaim.ts';
36
+ import { getTestAppName } from './testIsolation.ts';
37
+
38
+ const runId = '20261013010203-a1b2c3d4';
39
+ const owner = { pid: process.pid, startTime: 'test-process-start', host: hostname() };
40
+ function fixture() {
41
+ const root = mkdtempSync(path.join(realpathSync(tmpdir()), 'vona-test-claim-'));
42
+ const projectPath = path.join(root, 'vona');
43
+ mkdirSync(projectPath);
44
+ return { root, projectPath, dispose: () => rmSync(root, { recursive: true, force: true }) };
45
+ }
46
+ function appFixture(projectPath: string) {
47
+ return {
48
+ projectPath,
49
+ name: getTestAppName(runId),
50
+ meta: { env: { META_MODE: 'test', VONA_TEST_RUN_ID: runId } },
51
+ config: {
52
+ database: {
53
+ defaultClient: 'pg',
54
+ base: {},
55
+ clients: {
56
+ pg: {
57
+ client: 'pg',
58
+ connection: {
59
+ host: '127.0.0.1',
60
+ port: 5432,
61
+ database: 'postgres',
62
+ password: 'SECRET_DB',
63
+ user: 'owner',
64
+ },
65
+ },
66
+ },
67
+ },
68
+ instance: { instances: { isolated: { isolate: true, isolateClient: 'pg' } } },
69
+ redis: {
70
+ base: { host: '127.0.0.1', port: 6379, password: 'SECRET_REDIS' },
71
+ clients: {
72
+ cache: { keyPrefix: true, db: 2 },
73
+ queue: {},
74
+ redlock: {},
75
+ redlockOther: { port: 6380 },
76
+ broadcast: {},
77
+ },
78
+ },
79
+ },
80
+ };
81
+ }
82
+ function transportEnv(env: Record<string, string | undefined>) {
83
+ return { ...env, META_MODE: 'test', VONA_TEST_RUN_ID: runId };
84
+ }
85
+
86
+ test('exclusive central claim has canonical immutable UTC creation time and private modes', async () => {
87
+ const f = fixture();
88
+ try {
89
+ const { claimPath, manifest } = await createTestClaim(f.projectPath, runId, 'unit', owner);
90
+ assert.equal(claimPath, path.join(f.projectPath, '.vona-test-claim', `${runId}.json`));
91
+ assert.equal(manifest.createdAt, '2026-10-13T01:02:03.000Z');
92
+ assert.ok(JSON.parse('{"createdAt":"2026-10-13T01:02:03.000Z"}').createdAt instanceof Date);
93
+ assert.equal(lstatSync(claimPath).mode & 0o777, 0o600);
94
+ assert.equal(lstatSync(path.dirname(claimPath)).mode & 0o777, 0o700);
95
+ await assert.rejects(createTestClaim(f.projectPath, runId, 'unit', owner), { code: 'EEXIST' });
96
+ await createTestClaim(f.projectPath, '20261013010203-deadbeef', 'e2e', owner);
97
+ assert.deepEqual(
98
+ manifest.locals.map(item => item.kind),
99
+ ['generated', 'data'],
100
+ );
101
+ assert.deepEqual(await loadTestClaim(claimPath), manifest);
102
+ for (const id of [
103
+ '20260230010203-a1b2c3d4',
104
+ '20261013240203-a1b2c3d4',
105
+ '20261013010203-A1B2C3D4',
106
+ ])
107
+ assert.throws(() => getTestClaimCreatedAt(id));
108
+ } finally {
109
+ f.dispose();
110
+ }
111
+ });
112
+
113
+ test('claim creation rejects symlink paths and permissive shared directories', async () => {
114
+ const f = fixture();
115
+ try {
116
+ const directory = getTestClaimPaths(f.projectPath, runId).claimDirectory;
117
+ mkdirSync(path.join(f.root, 'outside'));
118
+ symlinkSync(path.join(f.root, 'outside'), directory);
119
+ await assert.rejects(createTestClaim(f.projectPath, runId, 'unit', owner), /Unsafe/);
120
+ rmSync(directory, { recursive: true, force: true });
121
+ mkdirSync(directory, { mode: 0o755 });
122
+ await assert.rejects(createTestClaim(f.projectPath, runId, 'unit', owner), /Unsafe/);
123
+ rmSync(directory, { recursive: true, force: true });
124
+ const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
125
+ chmodSync(claimPath, 0o644);
126
+ await assert.rejects(loadTestClaim(claimPath), /Unsafe/);
127
+ } finally {
128
+ f.dispose();
129
+ }
130
+ });
131
+
132
+ test('manifest validation rejects filename drift, secret snapshots and arbitrary resources', async () => {
133
+ const f = fixture();
134
+ try {
135
+ const { manifest, claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
136
+ assert.throws(() => validateTestClaim(manifest, `${claimPath}.other`));
137
+ assert.throws(() => validateTestClaim({ ...manifest, password: 'secret' }));
138
+ assert.throws(() => validateTestClaim({ ...manifest, createdAt: '2026-10-14T01:02:03.000Z' }));
139
+ assert.throws(() =>
140
+ validateTestClaim({
141
+ ...manifest,
142
+ locals: [{ ...manifest.locals[0], path: f.root }, manifest.locals[1]],
143
+ }),
144
+ );
145
+ assert.throws(() =>
146
+ validateTestClaim({
147
+ ...manifest,
148
+ processes: [{ id: 'x', role: 'browser', stage: 'registered', pid: 1, pgid: 1 }],
149
+ }),
150
+ );
151
+ assert.throws(() =>
152
+ validateTestClaim({
153
+ ...manifest,
154
+ processes: [{ id: 'x', role: 'browser', stage: 'pending', profilePath: f.root }],
155
+ }),
156
+ );
157
+ const inventory = buildTestClaimResourceInventory(appFixture(f.projectPath));
158
+ manifest.databases = inventory.databases.map(item => ({ ...item, creations: [] }));
159
+ manifest.redis = inventory.redis;
160
+ assert.throws(() =>
161
+ validateTestClaim({ ...manifest, redis: [{ ...manifest.redis[0], prefix: 'cache_' }] }),
162
+ );
163
+ assert.throws(() =>
164
+ validateTestClaim({
165
+ ...manifest,
166
+ databases: [
167
+ { ...manifest.databases[0], creations: [{ name: 'development', stage: 'intent' }] },
168
+ ],
169
+ }),
170
+ );
171
+ } finally {
172
+ f.dispose();
173
+ }
174
+ });
175
+
176
+ test('normalization whitelists endpoint identity and exact physical Redis prefixes', () => {
177
+ const app = appFixture('/project/vona');
178
+ const inventory = buildTestClaimResourceInventory(app);
179
+ assert.equal(inventory.databases.length, 2);
180
+ assert.equal(inventory.databases[1].scope, 'isolate-isolated');
181
+ assert.equal(inventory.redis.length, 4);
182
+ assert.equal(
183
+ inventory.redis.find(item => item.clientName === 'queue')!.prefix,
184
+ `bull_${app.name}_local:queue:`,
185
+ );
186
+ assert.equal(
187
+ inventory.redis.find(item => item.clientName === 'redlockOther')!.prefix,
188
+ `redlock_${app.name}_local:`,
189
+ );
190
+ assert.equal(
191
+ inventory.redis.find(item => item.clientName === 'redlockOther')!.endpoint.port,
192
+ 6380,
193
+ );
194
+ assert.equal(JSON.stringify(inventory).includes('SECRET'), false);
195
+ assert.equal(JSON.stringify(inventory).includes('password'), false);
196
+ const options = mergeTestClaimRedisOptions(
197
+ { host: 'base', port: 6379 },
198
+ { host: undefined, port: 6380 },
199
+ );
200
+ assert.deepEqual(normalizeTestClaimRedisEndpoint(options), {
201
+ transport: 'tcp',
202
+ host: 'base',
203
+ port: 6380,
204
+ tls: false,
205
+ db: 0,
206
+ });
207
+ assert.deepEqual(normalizeTestClaimRedisEndpoint({ path: '/run/redis.sock', db: 2 }), {
208
+ transport: 'socket',
209
+ socketPath: '/run/redis.sock',
210
+ db: 2,
211
+ tls: false,
212
+ });
213
+ });
214
+
215
+ test('normalization rejects unresolved or custom targets and captures pg socket port', () => {
216
+ assert.throws(
217
+ () =>
218
+ normalizeTestClaimDatabaseEndpoint(
219
+ { client: 'pg', connection: { database: 'postgres' } },
220
+ '/vona',
221
+ runId,
222
+ ),
223
+ /explicit/,
224
+ );
225
+ assert.deepEqual(
226
+ normalizeTestClaimDatabaseEndpoint(
227
+ { client: 'pg', connection: { host: '/run/postgresql', port: 5433 } },
228
+ '/vona',
229
+ runId,
230
+ ),
231
+ {
232
+ dialect: 'pg',
233
+ transport: 'socket',
234
+ socketPath: '/run/postgresql',
235
+ port: 5433,
236
+ tls: false,
237
+ adminDatabase: 'postgres',
238
+ },
239
+ );
240
+ assert.throws(() =>
241
+ normalizeTestClaimDatabaseEndpoint(
242
+ { client: 'pg', connection: { host: '/run/postgresql' } },
243
+ '/vona',
244
+ runId,
245
+ ),
246
+ );
247
+ for (const extra of [{ connectionString: 'postgres://secret' }, { stream: () => {} }]) {
248
+ assert.throws(() =>
249
+ normalizeTestClaimDatabaseEndpoint(
250
+ { client: 'pg', connection: { host: 'localhost', port: 5432, ...extra } },
251
+ '/vona',
252
+ runId,
253
+ ),
254
+ );
255
+ }
256
+ for (const options of [
257
+ { Connector: class {} },
258
+ { nodes: [{}] },
259
+ { host: 'redis://password@host' },
260
+ { db: -1 },
261
+ { port: 0 },
262
+ ])
263
+ assert.throws(() => normalizeTestClaimRedisEndpoint(options));
264
+ });
265
+
266
+ test('broker serializes simultaneous updates and acknowledges durable intent before allocation', async () => {
267
+ const f = fixture();
268
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
269
+ try {
270
+ const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
271
+ broker = await startTestClaimBroker(claimPath);
272
+ const env = transportEnv(broker.env);
273
+ const inventory = buildTestClaimResourceInventory(appFixture(f.projectPath));
274
+ await sendTestClaimEvent(env, { type: 'inventory', ...inventory });
275
+ const db = inventory.databases[0];
276
+ await Promise.all(
277
+ Array.from({ length: 15 }, (_, i) =>
278
+ sendTestClaimEvent(env, {
279
+ type: 'database',
280
+ database: db,
281
+ name: `${db.prefix}20261013-0102${String(i).padStart(2, '0')}`,
282
+ stage: 'intent',
283
+ }),
284
+ ),
285
+ );
286
+ const manifest = await loadTestClaim(claimPath);
287
+ assert.equal(manifest.databases[0].creations.length, 15);
288
+ assert.equal(readFileSync(claimPath, 'utf8').includes(broker.env.VONA_TEST_CLAIM_TOKEN), false);
289
+ assert.equal(readFileSync(claimPath, 'utf8').includes('SECRET'), false);
290
+ await assert.rejects(
291
+ sendTestClaimEvent(
292
+ { ...env, VONA_TEST_CLAIM_TOKEN: '0'.repeat(64) },
293
+ { type: 'status', status: 'failed' },
294
+ ),
295
+ /registration failed/,
296
+ );
297
+ await assert.rejects(
298
+ sendTestClaimEvent(env, {
299
+ type: 'database',
300
+ database: db,
301
+ name: 'development',
302
+ stage: 'intent',
303
+ }),
304
+ );
305
+ assert.equal((await loadTestClaim(claimPath)).databases[0].creations.length, 15);
306
+ } finally {
307
+ await broker?.close();
308
+ f.dispose();
309
+ }
310
+ });
311
+
312
+ test('database confirmations and process registration cannot precede allocation intent', async () => {
313
+ const f = fixture();
314
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
315
+ try {
316
+ const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
317
+ broker = await startTestClaimBroker(claimPath);
318
+ const inventory = buildTestClaimResourceInventory(appFixture(f.projectPath));
319
+ await broker.update({ type: 'inventory', ...inventory });
320
+ const db = inventory.databases[0];
321
+ const name = `${db.prefix}20261013-010203`;
322
+ await assert.rejects(
323
+ broker.update({ type: 'database', database: db, name, stage: 'created' }),
324
+ /write-ahead/,
325
+ );
326
+ await broker.update({ type: 'database', database: db, name, stage: 'intent' });
327
+ await broker.update({ type: 'database', database: db, name, stage: 'created' });
328
+ await assert.rejects(
329
+ broker.update({ type: 'database', database: db, name, stage: 'intent' }),
330
+ /regression/,
331
+ );
332
+ const proc = {
333
+ id: 'browser-uuid',
334
+ role: 'browser',
335
+ pid: 42,
336
+ pgid: 42,
337
+ startTime: 'start',
338
+ host: hostname(),
339
+ };
340
+ await assert.rejects(
341
+ broker.update({ type: 'process', process: { ...proc, stage: 'registered' } }),
342
+ /write-ahead/,
343
+ );
344
+ const tmp = path.join(getTestClaimPaths(f.projectPath, runId).dataRoot, 'tmp');
345
+ await broker.update({
346
+ type: 'process',
347
+ process: { id: proc.id, role: proc.role, stage: 'pending', profilePath: tmp },
348
+ });
349
+ await assert.rejects(
350
+ broker.update({ type: 'process', process: { ...proc, stage: 'drained', profilePath: tmp } }),
351
+ /Pending/,
352
+ );
353
+ await broker.update({
354
+ type: 'process',
355
+ process: { ...proc, stage: 'registered', profilePath: path.join(tmp, 'browser-profile') },
356
+ });
357
+ await assert.rejects(
358
+ broker.update({ type: 'process', process: { ...proc, pid: 43, stage: 'drained' } }),
359
+ /identity/,
360
+ );
361
+ await broker.update({
362
+ type: 'process',
363
+ process: { id: proc.id, role: proc.role, stage: 'drained' },
364
+ });
365
+ assert.equal((await loadTestClaim(claimPath)).processes[0].stage, 'drained');
366
+ } finally {
367
+ await broker?.close();
368
+ f.dispose();
369
+ }
370
+ });
371
+
372
+ test('cleanup editor persists partial progress but may not replace claim identity', async () => {
373
+ const f = fixture();
374
+ try {
375
+ const { claimPath, manifest } = await createTestClaim(f.projectPath, runId, 'unit', owner);
376
+ manifest.status = 'cleaning';
377
+ manifest.cleanup.processes = true;
378
+ manifest.locals[0].stage = 'released';
379
+ await writeTestClaim(claimPath, manifest);
380
+ assert.deepEqual(await loadTestClaim(claimPath), manifest);
381
+ await assert.rejects(
382
+ writeTestClaim(claimPath, { ...manifest, owner: { ...owner, pid: 1 } }),
383
+ /immutable/,
384
+ );
385
+ } finally {
386
+ f.dispose();
387
+ }
388
+ });
389
+
390
+ test('manual isolation remains transport-free, partial and non-test transport fail closed', async () => {
391
+ assert.equal(hasTestClaimTransport({}), false);
392
+ await sendTestClaimEvent({}, { type: 'status', status: 'running' });
393
+ assert.throws(
394
+ () => hasTestClaimTransport({ VONA_TEST_CLAIM_TOKEN: '0'.repeat(64) }),
395
+ /Incomplete/,
396
+ );
397
+ assert.throws(
398
+ () =>
399
+ hasTestClaimTransport({
400
+ VONA_TEST_CLAIM_URL: 'http://127.0.0.1:1/event',
401
+ VONA_TEST_CLAIM_TOKEN: '0'.repeat(64),
402
+ }),
403
+ /identity/,
404
+ );
405
+ assert.throws(
406
+ () =>
407
+ hasTestClaimTransport({
408
+ ...transportEnv({
409
+ VONA_TEST_CLAIM_URL: 'http://remote:1/event',
410
+ VONA_TEST_CLAIM_TOKEN: '0'.repeat(64),
411
+ }),
412
+ }),
413
+ /Invalid/,
414
+ );
415
+ await assert.rejects(
416
+ sendTestClaimEvent(
417
+ transportEnv({
418
+ VONA_TEST_CLAIM_URL: 'http://127.0.0.1:1/event',
419
+ VONA_TEST_CLAIM_TOKEN: '0'.repeat(64),
420
+ }),
421
+ { type: 'status', status: 'running' },
422
+ ),
423
+ /registration failed/,
424
+ );
425
+ });
426
+
427
+ test(
428
+ 'managed resource gates reject drift and allocation before broker acknowledgement',
429
+ { concurrency: false },
430
+ async () => {
431
+ const f = fixture();
432
+ const old = {
433
+ META_MODE: process.env.META_MODE,
434
+ VONA_TEST_RUN_ID: process.env.VONA_TEST_RUN_ID,
435
+ VONA_TEST_CLAIM_URL: process.env.VONA_TEST_CLAIM_URL,
436
+ VONA_TEST_CLAIM_TOKEN: process.env.VONA_TEST_CLAIM_TOKEN,
437
+ };
438
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
439
+ try {
440
+ const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
441
+ broker = await startTestClaimBroker(claimPath);
442
+ Object.assign(process.env, transportEnv(broker.env));
443
+ const app = appFixture(f.projectPath);
444
+ const client = app.config.database.clients.pg;
445
+ assert.throws(() => assertTestClaimDatabaseClient(app, 'pg', client), /Unregistered/);
446
+ await registerTestClaimResourceInventory(app);
447
+ assertTestClaimDatabaseClient(app, 'pg', client);
448
+ assert.throws(() => assertTestClaimDatabaseClient(app, 'dynamic_admin', client), /endpoint/);
449
+ assert.throws(
450
+ () =>
451
+ assertTestClaimDatabaseClient(app, 'pg', {
452
+ ...client,
453
+ connection: { ...client.connection, port: 5433 },
454
+ }),
455
+ /Unregistered/,
456
+ );
457
+ const plan = buildTestClaimResourceInventory(app).databases[0];
458
+ const name = `${plan.prefix}20261013-010203`;
459
+ const allocatedClient = { ...client, connection: { ...client.connection, database: name } };
460
+ const aliases = ['dynamic_123', 'index-recovery-test'];
461
+ assert.throws(() => assertTestClaimDatabaseClient(app, 'pg', allocatedClient), /allocation/);
462
+ for (const alias of aliases) {
463
+ assert.throws(
464
+ () => assertTestClaimDatabaseClient(app, alias, allocatedClient),
465
+ /allocation/,
466
+ );
467
+ }
468
+ await registerTestClaimDatabaseCreation(app, client, name, 'intent');
469
+ assertTestClaimDatabaseClient(app, 'pg', allocatedClient);
470
+ for (const alias of aliases) assertTestClaimDatabaseClient(app, alias, allocatedClient);
471
+ assert.equal((await loadTestClaim(claimPath)).databases[0].creations[0].stage, 'intent');
472
+ await registerTestClaimDatabaseCreation(app, client, name, 'created');
473
+ for (const alias of aliases) {
474
+ assertTestClaimDatabaseClient(app, alias, allocatedClient);
475
+ assert.throws(
476
+ () =>
477
+ assertTestClaimDatabaseClient(app, alias, {
478
+ ...allocatedClient,
479
+ connection: { ...allocatedClient.connection, port: 5433 },
480
+ }),
481
+ /endpoint/,
482
+ );
483
+ assert.throws(
484
+ () =>
485
+ assertTestClaimDatabaseClient(app, alias, {
486
+ ...allocatedClient,
487
+ connection: {
488
+ ...allocatedClient.connection,
489
+ database: `${plan.prefix}20261013-010205`,
490
+ },
491
+ }),
492
+ /allocation/,
493
+ );
494
+ }
495
+ const isolatePlan = buildTestClaimResourceInventory(app).databases[1];
496
+ const isolateName = `${isolatePlan.prefix}20261013-010203`;
497
+ await registerTestClaimDatabaseCreation(app, client, isolateName, 'intent');
498
+ assertTestClaimDatabaseClient(app, 'dynamic_isolated', {
499
+ ...client,
500
+ connection: { ...client.connection, database: isolateName },
501
+ });
502
+ assert.ok((await loadTestClaim(claimPath)).databases.every(item => item.clientName === 'pg'));
503
+ const sqliteApp = appFixture(f.projectPath);
504
+ const sqliteClient = { client: 'better-sqlite3', connection: { filename: '' } };
505
+ sqliteApp.config.database = {
506
+ ...sqliteApp.config.database,
507
+ defaultClient: 'sqlite3',
508
+ clients: { sqlite3: sqliteClient } as any,
509
+ };
510
+ sqliteApp.config.instance.instances = {} as any;
511
+ await registerTestClaimResourceInventory(sqliteApp);
512
+ const sqlitePlan = buildTestClaimResourceInventory(sqliteApp).databases[0];
513
+ const copyName = `${sqlitePlan.prefix}20261013-010206`;
514
+ const copyClient = {
515
+ ...sqliteClient,
516
+ connection: {
517
+ filename: path.join(getTestClaimPaths(f.projectPath, runId).sqliteRoot, `${copyName}.db`),
518
+ },
519
+ };
520
+ assert.throws(
521
+ () => assertTestClaimDatabaseClient(sqliteApp, 'index-recovery-copy', copyClient),
522
+ /allocation/,
523
+ );
524
+ await registerTestClaimDatabaseCreation(sqliteApp, sqliteClient, copyName, 'intent');
525
+ assertTestClaimDatabaseClient(sqliteApp, 'index-recovery-copy', copyClient);
526
+ assert.throws(
527
+ () =>
528
+ assertTestClaimDatabaseClient(sqliteApp, 'index-recovery-copy', {
529
+ ...copyClient,
530
+ connection: { filename: `${copyName}.db` },
531
+ }),
532
+ /allocation/,
533
+ );
534
+ assert.throws(
535
+ () => assertTestClaimDatabaseClient(sqliteApp, 'dynamic_admin', sqliteClient),
536
+ /endpoint/,
537
+ );
538
+ await registerTestClaimDatabaseCreation(sqliteApp, sqliteClient, copyName, 'created');
539
+ const manifest = await loadTestClaim(claimPath);
540
+ assert.equal(
541
+ manifest.databases.find(item => item.clientName === 'sqlite3')!.creations[0].stage,
542
+ 'created',
543
+ );
544
+ const redis = { host: '127.0.0.1', port: 6379, db: 2, keyPrefix: `cache_${app.name}_local:` };
545
+ assertTestClaimRedisClient(app, 'cache', redis);
546
+ assert.throws(
547
+ () => assertTestClaimRedisClient(app, 'cache', { ...redis, db: 0 }),
548
+ /Unregistered/,
549
+ );
550
+ assert.throws(
551
+ () => assertTestClaimRedisClient(app, 'cache', { ...redis, keyPrefix: 'cache_' }),
552
+ /prefix/,
553
+ );
554
+ await broker.close();
555
+ broker = undefined;
556
+ await assert.rejects(
557
+ registerTestClaimDatabaseCreation(app, client, `${plan.prefix}20261013-010204`, 'intent'),
558
+ /registration failed/,
559
+ );
560
+ assert.throws(
561
+ () =>
562
+ assertTestClaimDatabaseClient(app, 'dynamic_rejected', {
563
+ ...client,
564
+ connection: { ...client.connection, database: `${plan.prefix}20261013-010204` },
565
+ }),
566
+ /allocation/,
567
+ );
568
+ } finally {
569
+ for (const [key, value] of Object.entries(old)) {
570
+ if (value === undefined) delete process.env[key];
571
+ else process.env[key] = value;
572
+ }
573
+ await broker?.close();
574
+ f.dispose();
575
+ }
576
+ },
577
+ );