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,346 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ chmod,
4
+ lstat,
5
+ mkdir,
6
+ mkdtemp,
7
+ readFile,
8
+ realpath,
9
+ rm,
10
+ symlink,
11
+ writeFile,
12
+ } from 'node:fs/promises';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { test } from 'node:test';
16
+
17
+ import type { TestClaimCleanupSession } from './testClaimCleanup.ts';
18
+
19
+ import {
20
+ createTestClaim,
21
+ getTestClaimRedisPrefix,
22
+ loadTestClaim,
23
+ writeTestClaim,
24
+ } from '../vona/packages-vona/vona-core/src/lib/core/testClaim.ts';
25
+ import {
26
+ getTestAppName,
27
+ getDatabaseTestPrefix,
28
+ } from '../vona/packages-vona/vona-core/src/lib/core/testIsolation.ts';
29
+ import {
30
+ cleanTestClaim,
31
+ parseTestClaimTtlSeconds,
32
+ reapExpiredTestClaims,
33
+ } from './testClaimCleanup.ts';
34
+
35
+ const runId = '20261011010203-deadbeef';
36
+ const inactive = async () => true;
37
+ async function fixture(remote = false) {
38
+ const parent = await mkdtemp(path.join(await realpath(os.tmpdir()), 'claim-cleanup-'));
39
+ const project = path.join(parent, 'vona');
40
+ await mkdir(project);
41
+ const { claimPath, manifest } = await createTestClaim(project, runId, 'unit', {
42
+ pid: process.pid,
43
+ startTime: 'test-start',
44
+ host: os.hostname(),
45
+ });
46
+ for (const local of manifest.locals) {
47
+ await mkdir(local.path);
48
+ await writeFile(path.join(local.path, '.vona-test-owner'), `${runId}\n`);
49
+ local.stage = 'owned';
50
+ await writeFile(path.join(local.path, 'test-owned'), 'resource');
51
+ }
52
+ await mkdir(path.join(project, 'coverage'));
53
+ await writeFile(path.join(project, 'coverage', 'sentinel'), 'keep');
54
+ await mkdir(path.join(project, '.app'));
55
+ await writeFile(path.join(project, '.app', 'sentinel'), 'development');
56
+ if (remote) {
57
+ const prefix = getDatabaseTestPrefix(getTestAppName(runId), 'share');
58
+ manifest.databases.push({
59
+ clientName: 'pg',
60
+ scope: 'share',
61
+ prefix,
62
+ endpoint: {
63
+ dialect: 'pg',
64
+ transport: 'tcp',
65
+ host: 'localhost',
66
+ port: 5432,
67
+ tls: false,
68
+ adminDatabase: 'postgres',
69
+ },
70
+ creations: [{ name: `${prefix}20261011-010203`, stage: 'intent' }],
71
+ });
72
+ manifest.redis.push({
73
+ clientName: 'cache',
74
+ ...getTestClaimRedisPrefix(runId, 'cache')!,
75
+ endpoint: { transport: 'tcp', host: 'localhost', port: 6379, tls: false, db: 0 },
76
+ released: false,
77
+ });
78
+ }
79
+ await writeTestClaim(claimPath, manifest);
80
+ return {
81
+ parent,
82
+ project,
83
+ claimPath,
84
+ manifest,
85
+ async close() {
86
+ await rm(parent, { recursive: true, force: true });
87
+ },
88
+ };
89
+ }
90
+ async function absent(file: string) {
91
+ await assert.rejects(lstat(file), { code: 'ENOENT' });
92
+ }
93
+
94
+ test('TTL defaults, positive safe integers and exact expiry boundary', async () => {
95
+ assert.equal(parseTestClaimTtlSeconds(undefined), 86400);
96
+ assert.equal(parseTestClaimTtlSeconds('1'), 1);
97
+ for (const value of ['0', '-1', '1.5', '1e3', '', ' 1', '9007199254740992', 'Infinity'])
98
+ assert.throws(() => parseTestClaimTtlSeconds(value));
99
+ const f = await fixture();
100
+ try {
101
+ const created = Date.parse(f.manifest.createdAt);
102
+ assert.deepEqual(
103
+ await reapExpiredTestClaims(f.project, {
104
+ now: created + 1000,
105
+ ttlSeconds: 1,
106
+ isInactive: inactive,
107
+ }),
108
+ [],
109
+ );
110
+ assert.deepEqual(
111
+ await reapExpiredTestClaims(f.project, {
112
+ now: created + 1001,
113
+ ttlSeconds: 1,
114
+ isInactive: inactive,
115
+ }),
116
+ [{ status: 'cleaned' }],
117
+ );
118
+ await absent(f.claimPath);
119
+ } finally {
120
+ await f.close();
121
+ }
122
+ });
123
+
124
+ test('one cleanup sequence journals database and Redis before roots; claim removed last', async () => {
125
+ const f = await fixture(true);
126
+ const actions: string[] = [];
127
+ try {
128
+ const result = await cleanTestClaim(f.claimPath, {
129
+ isInactive: async () => {
130
+ actions.push('inactive');
131
+ return true;
132
+ },
133
+ prepare: async () => {
134
+ actions.push('validate-config');
135
+ return {
136
+ async dropDatabase(db, name) {
137
+ actions.push('database');
138
+ assert.equal(name, db.creations[0].name);
139
+ await lstat(f.claimPath);
140
+ },
141
+ async clearRedis() {
142
+ actions.push('redis');
143
+ assert.equal(
144
+ (await loadTestClaim(f.claimPath)).databases[0].creations[0].stage,
145
+ 'released',
146
+ );
147
+ },
148
+ async close() {
149
+ actions.push('close');
150
+ await lstat(f.manifest.locals[0].path);
151
+ },
152
+ };
153
+ },
154
+ });
155
+ assert.deepEqual(result, { status: 'cleaned' });
156
+ assert.deepEqual(actions, ['validate-config', 'inactive', 'database', 'redis', 'close']);
157
+ await absent(f.claimPath);
158
+ for (const root of f.manifest.locals) await absent(root.path);
159
+ assert.equal(await readFile(path.join(f.project, '.app/sentinel'), 'utf8'), 'development');
160
+ assert.equal(await readFile(path.join(f.project, 'coverage/sentinel'), 'utf8'), 'keep');
161
+ } finally {
162
+ await f.close();
163
+ }
164
+ });
165
+
166
+ test('partial failure retains progress and retries only remaining resources', async () => {
167
+ const f = await fixture(true);
168
+ let drops = 0;
169
+ let fail = true;
170
+ const session: TestClaimCleanupSession = {
171
+ async dropDatabase() {
172
+ drops++;
173
+ },
174
+ async clearRedis() {
175
+ if (fail) throw new Error('secret-bearing error deliberately not surfaced');
176
+ },
177
+ async close() {},
178
+ };
179
+ try {
180
+ const first = await cleanTestClaim(f.claimPath, {
181
+ isInactive: inactive,
182
+ prepare: async () => session,
183
+ });
184
+ assert.equal(first.status, 'failed');
185
+ assert.equal((await loadTestClaim(f.claimPath)).databases[0].creations[0].stage, 'released');
186
+ for (const root of f.manifest.locals) await lstat(root.path);
187
+ fail = false;
188
+ assert.equal(
189
+ (await cleanTestClaim(f.claimPath, { isInactive: inactive, prepare: async () => session }))
190
+ .status,
191
+ 'cleaned',
192
+ );
193
+ assert.equal(drops, 1);
194
+ } finally {
195
+ await f.close();
196
+ }
197
+ });
198
+
199
+ test('live, missing inspector, drifted config, malformed manifest and unmarked roots retain resources', async () => {
200
+ for (const reason of ['live', 'inspector', 'drift', 'malformed', 'unmarked', 'symlink']) {
201
+ const f = await fixture(reason === 'drift');
202
+ try {
203
+ if (reason === 'malformed') await writeFile(f.claimPath, '{');
204
+ if (reason === 'unmarked') await rm(path.join(f.manifest.locals[0].path, '.vona-test-owner'));
205
+ if (reason === 'symlink') {
206
+ await rm(f.manifest.locals[0].path, { recursive: true });
207
+ await symlink(path.join(f.project, '.app'), f.manifest.locals[0].path);
208
+ }
209
+ const result = await cleanTestClaim(f.claimPath, {
210
+ isInactive: reason === 'inspector' ? undefined : async () => reason !== 'live',
211
+ prepare: async () => {
212
+ throw new Error('drift');
213
+ },
214
+ });
215
+ assert.equal(result.status, 'skipped', reason);
216
+ await lstat(f.claimPath);
217
+ assert.equal(await readFile(path.join(f.project, '.app/sentinel'), 'utf8'), 'development');
218
+ } finally {
219
+ await f.close();
220
+ }
221
+ }
222
+ });
223
+
224
+ test('exclusive cleaners skip contenders; orphaned old lock is never taken over', async () => {
225
+ const f = await fixture();
226
+ let release!: () => void;
227
+ let acquired!: () => void;
228
+ const ready = new Promise<void>(resolve => {
229
+ acquired = resolve;
230
+ });
231
+ const gate = new Promise<void>(resolve => {
232
+ release = resolve;
233
+ });
234
+ try {
235
+ const first = cleanTestClaim(f.claimPath, {
236
+ isInactive: async () => {
237
+ acquired();
238
+ await gate;
239
+ return true;
240
+ },
241
+ });
242
+ await ready;
243
+ assert.equal((await cleanTestClaim(f.claimPath, { isInactive: inactive })).status, 'skipped');
244
+ release();
245
+ assert.equal((await first).status, 'cleaned');
246
+ await absent(`${f.claimPath}.cleanup.lock`);
247
+ } finally {
248
+ release?.();
249
+ await f.close();
250
+ }
251
+ const second = await fixture();
252
+ try {
253
+ await writeFile(
254
+ `${second.claimPath}.cleanup.lock`,
255
+ JSON.stringify({ pid: 999999, startTime: 'ancient', token: 'orphan' }),
256
+ { mode: 0o600 },
257
+ );
258
+ const result = await reapExpiredTestClaims(second.project, {
259
+ now: Date.parse(second.manifest.createdAt) + 10000,
260
+ ttlSeconds: 1,
261
+ isInactive: inactive,
262
+ });
263
+ assert.equal(result[0].status, 'skipped');
264
+ await lstat(second.claimPath);
265
+ await lstat(`${second.claimPath}.cleanup.lock`);
266
+ } finally {
267
+ await second.close();
268
+ }
269
+ });
270
+
271
+ test('unsafe shared directories fail invocation; legacy directories warn and remain', async () => {
272
+ const f = await fixture();
273
+ try {
274
+ await chmod(path.dirname(f.claimPath), 0o777);
275
+ await assert.rejects(
276
+ reapExpiredTestClaims(f.project, { isInactive: inactive }),
277
+ /Unsafe shared/,
278
+ );
279
+ await chmod(path.dirname(f.claimPath), 0o700);
280
+ const legacy = path.join(f.project, `.vona-test-claim-${runId}`);
281
+ await mkdir(legacy);
282
+ const warnings: string[] = [];
283
+ await reapExpiredTestClaims(f.project, {
284
+ now: Date.parse(f.manifest.createdAt),
285
+ warn: warning => warnings.push(warning),
286
+ });
287
+ assert.ok(warnings.some(w => w.includes('Legacy')));
288
+ await lstat(legacy);
289
+ } finally {
290
+ await f.close();
291
+ }
292
+ });
293
+
294
+ test('missing already-deleted local roots are idempotent on retry', async () => {
295
+ const f = await fixture();
296
+ try {
297
+ await rm(f.manifest.locals[0].path, { recursive: true });
298
+ assert.equal((await cleanTestClaim(f.claimPath, { isInactive: inactive })).status, 'cleaned');
299
+ } finally {
300
+ await f.close();
301
+ }
302
+ });
303
+
304
+ test('SQLite files and sidecars remain intents until the marked data root is released', async () => {
305
+ const f = await fixture();
306
+ const prefix = getDatabaseTestPrefix(getTestAppName(runId), 'share');
307
+ const data = f.manifest.locals.find(local => local.kind === 'data')!.path;
308
+ const sqlite = path.join(data, 'sqlite3');
309
+ const name = path.join(sqlite, `${prefix}20261011-010203.db`);
310
+ let failClose = true;
311
+ try {
312
+ await mkdir(sqlite);
313
+ for (const suffix of ['', '-wal', '-shm']) await writeFile(name + suffix, 'test-owned');
314
+ f.manifest.databases.push({
315
+ clientName: 'sqlite3',
316
+ scope: 'share',
317
+ prefix,
318
+ endpoint: { dialect: 'better-sqlite3', transport: 'file', socketPath: sqlite, tls: false },
319
+ creations: [{ name, stage: 'intent' }],
320
+ });
321
+ await writeTestClaim(f.claimPath, f.manifest);
322
+ const options = {
323
+ isInactive: inactive,
324
+ prepare: async () => ({
325
+ async dropDatabase() {
326
+ throw new Error('SQLite must not use a remote driver');
327
+ },
328
+ async clearRedis() {
329
+ throw new Error('No Redis inventory');
330
+ },
331
+ async close() {
332
+ if (failClose) throw new Error('uncertain close');
333
+ },
334
+ }),
335
+ };
336
+ assert.equal((await cleanTestClaim(f.claimPath, options)).status, 'failed');
337
+ assert.equal((await loadTestClaim(f.claimPath)).databases[0].creations[0].stage, 'intent');
338
+ assert.equal((await loadTestClaim(f.claimPath)).cleanup.databases, false);
339
+ await lstat(`${name}-wal`);
340
+ failClose = false;
341
+ assert.equal((await cleanTestClaim(f.claimPath, options)).status, 'cleaned');
342
+ await absent(data);
343
+ } finally {
344
+ await f.close();
345
+ }
346
+ });