cabloy 5.1.216 → 5.1.218

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 (89) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/create-cabloy.yml +137 -0
  3. package/.github/workflows/playwright-e2e.yml +163 -13
  4. package/.github/workflows/vona-test-sqlite3.yml +77 -7
  5. package/CHANGELOG.md +22 -0
  6. package/CONTRIBUTING.md +4 -2
  7. package/package.json +1 -1
  8. package/repo-docs/frontend/scripts.md +2 -0
  9. package/repo-docs/reference/repo-scripts.md +27 -1
  10. package/repo-e2e/scripts/e2e.test.ts +1 -1
  11. package/repo-e2e/scripts/managedTest.test.ts +115 -3
  12. package/repo-e2e/scripts/managedTest.ts +69 -20
  13. package/repo-e2e/scripts/managedTestImports.test.ts +55 -5
  14. package/repo-e2e/scripts/runE2e.test.ts +109 -0
  15. package/repo-e2e/scripts/runE2e.ts +58 -39
  16. package/repo-e2e/scripts/runE2eArgs.test.ts +16 -1
  17. package/repo-e2e/scripts/runE2eArgs.ts +5 -0
  18. package/repo-e2e/specs/a-commerce.spec.ts +10 -2
  19. package/repo-e2e/specs/home-user-account.spec.ts +4 -0
  20. package/repo-e2e/specs/markdown.spec.ts +14 -3
  21. package/scripts/generateOpenapiIsolated.test.ts +5 -1
  22. package/scripts/runIsolatedTest.test.ts +403 -36
  23. package/scripts/runIsolatedTest.ts +139 -12
  24. package/scripts/testBrowserGuardianWindows.test.ts +327 -0
  25. package/scripts/testClaimCleanup.test.ts +25 -9
  26. package/scripts/testClaimCleanup.ts +14 -45
  27. package/scripts/testClaimProcesses.test.ts +81 -2
  28. package/scripts/testClaimProcesses.ts +46 -10
  29. package/scripts/testProcessGuardianWindows.test.ts +602 -0
  30. package/scripts/testProcessGuardianWindows.ts +155 -0
  31. package/vona/packages-cli/cli/package.json +1 -1
  32. package/vona/packages-cli/cli-set-api/package.json +1 -1
  33. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +4 -1
  34. package/vona/packages-cli/cli-set-api/test/binBuildEntries.test.ts +159 -0
  35. package/vona/packages-vona/vona/package.json +1 -1
  36. package/vona/packages-vona/vona-core/package.json +1 -1
  37. package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +298 -17
  38. package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +205 -97
  39. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts +247 -0
  40. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.ts +224 -0
  41. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.cs +229 -0
  42. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ps1 +20 -0
  43. package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ts +200 -0
  44. package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.cs +336 -0
  45. package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ps1 +20 -0
  46. package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts +531 -0
  47. package/vona/packages-vona/vona-core/src/lib/framework/bootstrap.test.ts +61 -0
  48. package/vona/packages-vona/vona-core/src/lib/framework/index.ts +1 -0
  49. package/vona/packages-vona/vona-core/src/lib/framework/processWorker.test.ts +59 -0
  50. package/vona/packages-vona/vona-core/src/lib/framework/processWorker.ts +8 -5
  51. package/vona/packages-vona/vona-core/src/lib/framework/useApp.test.ts +84 -0
  52. package/vona/packages-vona/vona-core/src/lib/framework/useApp.ts +2 -2
  53. package/vona/packages-vona/vona-core/src/lib/utils/util.ts +1 -1
  54. package/vona/packages-vona/vona-mock/package.json +1 -1
  55. package/vona/pnpm-lock.yaml +42 -181
  56. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  57. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  58. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  59. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  60. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +2 -1
  61. package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
  62. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +2 -2
  63. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts +16 -0
  64. package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.ts +6 -3
  65. package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +80 -0
  66. package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
  67. package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.health.ts +73 -2
  68. package/vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts +309 -0
  69. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  70. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/transactionAsyncLocalStorage_.ts +3 -3
  71. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  72. package/vona/src/suite-vendor/a-vona/modules/a-static/package.json +1 -1
  73. package/vona/src/suite-vendor/a-vona/modules/a-static/src/bean/middlewareSystem.static.ts +2 -2
  74. package/vona/src/suite-vendor/a-vona/modules/a-static/test/middlewareSystem.static.test.ts +101 -0
  75. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  76. package/zova/package.original.json +1 -1
  77. package/zova/packages-cli/cli/package.json +2 -2
  78. package/zova/packages-cli/cli-set-front/package.json +4 -4
  79. package/zova/packages-utils/zova-jsx/package.json +2 -2
  80. package/zova/packages-utils/zova-vite/package.json +2 -2
  81. package/zova/packages-utils/zova-vite/src/configUtils.ts +1 -1
  82. package/zova/packages-utils/zova-vite/test/configUtils.test.ts +55 -0
  83. package/zova/packages-zova/zova/package.json +3 -3
  84. package/zova/packages-zova/zova-core/package.json +2 -2
  85. package/zova/pnpm-lock.yaml +324 -63
  86. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +8 -4
  87. package/zova/src/suite/a-commerce/modules/commerce-trade/test/lib/cartModel.test.ts +71 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  89. package/zova/src/suite-vendor/a-zova/package.json +2 -2
@@ -1,8 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { randomBytes } from 'node:crypto';
3
- import { constants } from 'node:fs';
4
- import { lstat, open, readdir, rm, unlink } from 'node:fs/promises';
5
- import os from 'node:os';
2
+ import { lstat, readdir, rm } from 'node:fs/promises';
6
3
  import path from 'node:path';
7
4
  import { fileURLToPath } from 'node:url';
8
5
 
@@ -18,6 +15,11 @@ import {
18
15
  TEST_CLAIM_DIRECTORY,
19
16
  writeTestClaim,
20
17
  } from '../vona/packages-vona/vona-core/src/lib/core/testClaim.ts';
18
+ import {
19
+ acquireTestClaimCleanupLock,
20
+ assertTestClaimPrivateDirectory,
21
+ deleteTestClaimFile,
22
+ } from '../vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.ts';
21
23
  import { assertTestRunOwner } from '../vona/packages-vona/vona-core/src/lib/core/testIsolation.ts';
22
24
 
23
25
  export interface TestClaimCleanupSession {
@@ -52,24 +54,6 @@ export function parseTestClaimTtlSeconds(
52
54
  return Number(value);
53
55
  }
54
56
 
55
- async function assertSharedDirectory(directory: string, missingAllowed = true) {
56
- assertTestClaimSafePath(directory, missingAllowed);
57
- let stat;
58
- try {
59
- stat = await lstat(directory);
60
- } catch (error) {
61
- if (missingAllowed && (error as NodeJS.ErrnoException).code === 'ENOENT') return false;
62
- throw error;
63
- }
64
- if (
65
- !stat.isDirectory() ||
66
- (stat.mode & 0o077) !== 0 ||
67
- (process.getuid && stat.uid !== process.getuid())
68
- ) {
69
- throw new Error('Unsafe shared test claim directory');
70
- }
71
- return true;
72
- }
73
57
  async function exists(file: string) {
74
58
  try {
75
59
  await lstat(file);
@@ -84,7 +68,9 @@ async function validateLocalRoots(claim: TestClaimManifest) {
84
68
  assertTestClaimSafePath(local.path);
85
69
  if (!(await exists(local.path))) continue;
86
70
  if (local.stage === 'released') throw new Error('Released local test resource reappeared');
87
- const marker = await lstat(path.join(local.path, '.vona-test-owner'));
71
+ const markerPath = path.join(local.path, '.vona-test-owner');
72
+ assertTestClaimSafePath(markerPath, false);
73
+ const marker = await lstat(markerPath);
88
74
  if (marker.isSymbolicLink() || !marker.isFile() || marker.nlink !== 1)
89
75
  throw new Error('Unsafe test owner marker');
90
76
  assertTestRunOwner(local.path, claim.runId);
@@ -97,7 +83,7 @@ export async function cleanTestClaim(
97
83
  ): Promise<TestClaimCleanupResult> {
98
84
  claimPath = path.resolve(claimPath);
99
85
  // An unsafe shared namespace is fatal to the invocation, not a skippable claim.
100
- await assertSharedDirectory(path.dirname(claimPath), false);
86
+ await assertTestClaimPrivateDirectory(path.dirname(claimPath), false);
101
87
  const warn = options.warn ?? (() => {});
102
88
  const skip = (reason: string): TestClaimCleanupResult => {
103
89
  warn(`Retained test claim ${path.basename(claimPath)}: ${reason}`);
@@ -114,25 +100,15 @@ export async function cleanTestClaim(
114
100
  const lockPath = `${claimPath}.cleanup.lock`;
115
101
  let lock;
116
102
  try {
117
- assertTestClaimSafePath(lockPath);
118
- lock = await open(
119
- lockPath,
120
- constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
121
- 0o600,
122
- );
103
+ lock = await acquireTestClaimCleanupLock(lockPath);
123
104
  } catch (error) {
124
105
  if ((error as NodeJS.ErrnoException).code === 'EEXIST')
125
106
  return skip('cleanup lock present; inspect manually, age never permits takeover');
126
107
  return skip('cannot acquire safe cleanup lock');
127
108
  }
128
- const token = randomBytes(24).toString('hex');
129
109
  let session: TestClaimCleanupSession | undefined;
130
110
  let sequenceStarted = false;
131
111
  try {
132
- await lock.writeFile(
133
- `${JSON.stringify({ pid: process.pid, hostname: os.hostname(), startTime: String(Date.now() - process.uptime() * 1000), token })}\n`,
134
- );
135
- await lock.sync();
136
112
  // Re-read after locking; the runner must have closed its single inventory broker.
137
113
  claim = await loadTestClaim(claimPath);
138
114
  await validateLocalRoots(claim);
@@ -194,7 +170,7 @@ export async function cleanTestClaim(
194
170
  await writeTestClaim(claimPath, claim);
195
171
  // Validate once more before releasing the inventory, always last.
196
172
  await loadTestClaim(claimPath);
197
- await unlink(claimPath);
173
+ await deleteTestClaimFile(claimPath);
198
174
  return { status: 'cleaned' };
199
175
  } catch {
200
176
  const reason = sequenceStarted
@@ -204,16 +180,9 @@ export async function cleanTestClaim(
204
180
  return { status: sequenceStarted ? 'failed' : 'skipped', reason };
205
181
  } finally {
206
182
  if (session) await session.close().catch(() => {});
207
- await lock.close();
208
183
  // Do not remove a replacement lock. No automatic orphan-lock takeover.
209
184
  try {
210
- const handle = await open(lockPath, constants.O_RDONLY | constants.O_NOFOLLOW);
211
- try {
212
- const value = JSON.parse(await handle.readFile('utf8'));
213
- if (value.token === token) await unlink(lockPath);
214
- } finally {
215
- await handle.close();
216
- }
185
+ await lock.release();
217
186
  } catch {
218
187
  warn(`Retained cleanup lock ${path.basename(lockPath)}: manual inspection required`);
219
188
  }
@@ -240,7 +209,7 @@ export async function reapExpiredTestClaims(
240
209
  if (legacyRun && !(await exists(path.join(directory, `${legacyRun[1]}.json`))))
241
210
  warn(`Legacy inventory-less test root retained: ${item.name}`);
242
211
  }
243
- if (!(await assertSharedDirectory(directory))) return [];
212
+ if (!(await assertTestClaimPrivateDirectory(directory))) return [];
244
213
  const results: TestClaimCleanupResult[] = [];
245
214
  for (const name of await readdir(directory)) {
246
215
  if (name.endsWith('.cleanup.lock')) {
@@ -96,11 +96,90 @@ test('inactivity fails closed for active, reused, pending and uninspectable proc
96
96
  );
97
97
  });
98
98
 
99
+ test('Windows job inactivity requires durable drainage and absence of the exact owner PID', async () => {
100
+ const owner = { pid: 100, startTime: 'owner-start', host: hostname() };
101
+ const registered = {
102
+ id: 'guardian',
103
+ role: 'guardian',
104
+ stage: 'registered' as const,
105
+ containment: 'windows-job' as const,
106
+ jobId: '69a52c48-6bc4-46ce-9655-b0379117ad61',
107
+ pid: 200,
108
+ startTime: '134000000000000000',
109
+ host: hostname(),
110
+ };
111
+ assert.equal(
112
+ (await isTestClaimInactive({ owner, processes: [registered] }, false, async () => [])).inactive,
113
+ false,
114
+ );
115
+ const drained = { ...registered, stage: 'drained' as const };
116
+ assert.equal(
117
+ (await isTestClaimInactive({ owner, processes: [drained] }, false, async () => [])).inactive,
118
+ true,
119
+ );
120
+ for (const [pid, reason] of [
121
+ [owner.pid, 'claim owner PID is still present'],
122
+ [drained.pid, 'registered Windows job owner is still present'],
123
+ ] as const) {
124
+ let inspections = 0;
125
+ const inactivity = await isTestClaimInactive(
126
+ { owner, processes: [drained] },
127
+ false,
128
+ async () => {
129
+ inspections++;
130
+ return [
131
+ {
132
+ pid,
133
+ startTime: 'different-created-time',
134
+ host: hostname(),
135
+ state: 'running',
136
+ args: '',
137
+ },
138
+ ];
139
+ },
140
+ );
141
+ assert.deepEqual(inactivity, { inactive: false, reason });
142
+ assert.equal(inspections, 1);
143
+ }
144
+ assert.equal(
145
+ (
146
+ await isTestClaimInactive(
147
+ { owner, processes: [{ ...drained, jobId: undefined }] },
148
+ false,
149
+ async () => [],
150
+ )
151
+ ).inactive,
152
+ false,
153
+ );
154
+ assert.equal(
155
+ (
156
+ await isTestClaimInactive(
157
+ { owner, processes: [{ ...drained, pgid: 200 }] },
158
+ false,
159
+ async () => [],
160
+ )
161
+ ).inactive,
162
+ false,
163
+ );
164
+ await assert.rejects(
165
+ isTestClaimInactive({ owner, processes: [drained] }, false, async () => {
166
+ throw new Error('native inspection unavailable');
167
+ }),
168
+ /native inspection unavailable/,
169
+ );
170
+ });
171
+
99
172
  test('identifies the current process without publishing environment values', async () => {
100
173
  const identity = await inspectTestProcess(process.pid);
101
174
  assert.equal(identity.pid, process.pid);
102
- assert.ok(identity.pgid > 0);
103
175
  assert.ok(identity.startTime);
104
176
  assert.ok(identity.host);
105
- await assert.rejects(waitForTestProcessGroupExit(identity.pgid, 0), /did not drain/);
177
+ if (process.platform === 'win32') {
178
+ assert.equal(identity.pgid, undefined);
179
+ assert.match(identity.startTime, /^[1-9]\d+$/);
180
+ await assert.rejects(waitForTestProcessGroupExit(process.pid, 0), /unsupported/);
181
+ } else {
182
+ assert.ok(identity.pgid! > 0);
183
+ await assert.rejects(waitForTestProcessGroupExit(identity.pgid!, 0), /did not drain/);
184
+ }
106
185
  });
@@ -2,11 +2,16 @@ import { execFile } from 'node:child_process';
2
2
  import { hostname } from 'node:os';
3
3
  import { promisify } from 'node:util';
4
4
 
5
+ import {
6
+ inspectWindowsProcess,
7
+ inspectWindowsProcesses,
8
+ } from '../vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts';
9
+
5
10
  const execFileAsync = promisify(execFile);
6
11
 
7
12
  export interface TestProcessIdentity {
8
13
  pid: number;
9
- pgid: number;
14
+ pgid?: number;
10
15
  startTime: string;
11
16
  host: string;
12
17
  }
@@ -39,7 +44,7 @@ export function parseTestProcesses(output: string): TestProcessSnapshot[] {
39
44
 
40
45
  export async function inspectTestProcesses(): Promise<TestProcessSnapshot[]> {
41
46
  if (process.platform === 'win32') {
42
- throw new Error('Isolated test process inspection requires POSIX');
47
+ throw new Error('Windows process inspection requires explicit owned PIDs');
43
48
  }
44
49
  const { stdout } = await execFileAsync(
45
50
  'ps',
@@ -50,6 +55,11 @@ export async function inspectTestProcesses(): Promise<TestProcessSnapshot[]> {
50
55
  }
51
56
 
52
57
  export async function inspectTestProcess(pid: number): Promise<TestProcessSnapshot> {
58
+ if (process.platform === 'win32') {
59
+ const item = await inspectWindowsProcess(pid);
60
+ if (!item) throw new Error('Cannot identify isolated test process');
61
+ return item;
62
+ }
53
63
  const item = (await inspectTestProcesses()).find(
54
64
  item => item.pid === pid && !item.state.startsWith('Z'),
55
65
  );
@@ -63,6 +73,8 @@ export interface TestClaimProcessRecord {
63
73
  stage: 'pending' | 'registered' | 'drained';
64
74
  pid?: number;
65
75
  pgid?: number;
76
+ containment?: 'windows-job';
77
+ jobId?: string;
66
78
  startTime?: string;
67
79
  host?: string;
68
80
  profilePath?: string;
@@ -74,12 +86,27 @@ export async function isTestClaimInactive(
74
86
  processes: TestClaimProcessRecord[];
75
87
  },
76
88
  allowCurrentOwner = false,
77
- inspect: () => Promise<TestProcessSnapshot[]> = inspectTestProcesses,
89
+ inspect?: () => Promise<TestProcessSnapshot[]>,
78
90
  ): Promise<{ inactive: boolean; reason?: string }> {
79
91
  if (claim.owner.host !== hostname()) {
80
92
  return { inactive: false, reason: 'claim belongs to another or unidentified host' };
81
93
  }
82
- const processes = (await inspect()).filter(item => !item.state.startsWith('Z'));
94
+ if (
95
+ !inspect &&
96
+ process.platform === 'win32' &&
97
+ claim.processes.some(item => item.containment !== 'windows-job')
98
+ ) {
99
+ return { inactive: false, reason: 'POSIX process ownership cannot be inspected on Windows' };
100
+ }
101
+ const snapshots = inspect
102
+ ? await inspect()
103
+ : process.platform === 'win32'
104
+ ? await inspectWindowsProcesses([
105
+ claim.owner.pid,
106
+ ...claim.processes.flatMap(item => (item.pid ? [item.pid] : [])),
107
+ ])
108
+ : await inspectTestProcesses();
109
+ const processes = snapshots.filter(item => !item.state.startsWith('Z'));
83
110
  const owner = processes.find(item => item.pid === claim.owner.pid);
84
111
  if (
85
112
  owner &&
@@ -91,17 +118,24 @@ export async function isTestClaimInactive(
91
118
  if (registered.stage === 'pending') {
92
119
  return { inactive: false, reason: 'process launch registration is incomplete' };
93
120
  }
94
- if (
95
- !registered.pid ||
96
- !registered.pgid ||
97
- !registered.startTime ||
98
- registered.host !== hostname()
99
- ) {
121
+ if (!registered.pid || !registered.startTime || registered.host !== hostname()) {
100
122
  return {
101
123
  inactive: false,
102
124
  reason: 'process identity is incomplete or belongs to another host',
103
125
  };
104
126
  }
127
+ if (registered.containment === 'windows-job') {
128
+ if (!registered.jobId || registered.pgid !== undefined || registered.stage !== 'drained') {
129
+ return { inactive: false, reason: 'Windows job drainage is unproven' };
130
+ }
131
+ if (processes.some(item => item.pid === registered.pid)) {
132
+ return { inactive: false, reason: 'registered Windows job owner is still present' };
133
+ }
134
+ continue;
135
+ }
136
+ if (!registered.pgid || registered.jobId !== undefined) {
137
+ return { inactive: false, reason: 'POSIX process group identity is incomplete' };
138
+ }
105
139
  if (processes.some(item => item.pid === registered.pid || item.pgid === registered.pgid)) {
106
140
  return { inactive: false, reason: 'registered process or group is still present' };
107
141
  }
@@ -116,6 +150,8 @@ export async function isTestClaimInactive(
116
150
  }
117
151
 
118
152
  export async function waitForTestProcessGroupExit(pgid: number, timeoutMs = 10_000) {
153
+ if (process.platform === 'win32')
154
+ throw new Error('POSIX browser process groups are unsupported on Windows');
119
155
  if (!Number.isSafeInteger(pgid) || pgid <= 0) throw new Error('Invalid isolated process group');
120
156
  const deadline = Date.now() + timeoutMs;
121
157
  while (true) {