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
@@ -3,8 +3,9 @@ import type { ChildProcess } from 'node:child_process';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
5
5
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
6
+ import { realpathSync } from 'node:fs';
6
7
  import { createServer } from 'node:net';
7
- import { dirname, resolve } from 'node:path';
8
+ import { basename, dirname, resolve } from 'node:path';
8
9
  import { fileURLToPath } from 'node:url';
9
10
 
10
11
  import {
@@ -19,6 +20,7 @@ import {
19
20
  reapExpiredTestClaims,
20
21
  } from './testClaimCleanup.ts';
21
22
  import { inspectTestProcess, isTestClaimInactive } from './testClaimProcesses.ts';
23
+ import { startWindowsOwnedProcess } from './testProcessGuardianWindows.ts';
22
24
 
23
25
  export const ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..');
24
26
  const OWNER_FILE = '.vona-test-owner';
@@ -92,9 +94,7 @@ export async function startOwnedProcess(
92
94
  cwd: string,
93
95
  env: NodeJS.ProcessEnv,
94
96
  ): Promise<OwnedProcess> {
95
- if (process.platform === 'win32') {
96
- throw new Error('Isolated test process-tree cleanup requires POSIX process groups');
97
- }
97
+ if (process.platform === 'win32') return startWindowsOwnedProcess(command, args, cwd, env);
98
98
  const processId = randomUUID();
99
99
  const managed = hasTestClaimTransport(env);
100
100
  const registration = { id: processId, role: 'guardian', stage: 'pending' as const };
@@ -255,11 +255,85 @@ export function getIsolatedVonaCommand(port: number, env: NodeJS.ProcessEnv, rea
255
255
  };
256
256
  }
257
257
 
258
+ interface IsolatedVonaDependencies {
259
+ start: typeof startOwnedProcess;
260
+ ready: typeof waitForReady;
261
+ platform?: NodeJS.Platform;
262
+ requestStop?: typeof requestIsolatedVonaStop;
263
+ shutdownTimeoutMs?: number;
264
+ }
265
+
266
+ export async function requestIsolatedVonaStop(
267
+ port: number,
268
+ runId: string | undefined,
269
+ key: string,
270
+ timeoutMs = 1500,
271
+ ) {
272
+ if (!runId || !/^\d{14}-[a-f0-9]{8}$/.test(runId) || !/^[a-f0-9]{64}$/.test(key)) {
273
+ throw new Error('Invalid isolated Vona shutdown identity');
274
+ }
275
+ const nonce = randomBytes(16).toString('hex');
276
+ const payload = `${runId}\nPOST\n/health/test-stop\n${nonce}`;
277
+ const response = await fetch(`http://127.0.0.1:${port}/health/test-stop`, {
278
+ method: 'POST',
279
+ redirect: 'error',
280
+ signal: AbortSignal.timeout(timeoutMs),
281
+ headers: {
282
+ 'x-vona-test-stop-nonce': nonce,
283
+ 'x-vona-test-stop-authorization': createHmac('sha256', key)
284
+ .update(`vona-test-stop-request-v1\n${payload}`)
285
+ .digest('hex'),
286
+ },
287
+ });
288
+ const proof = response.headers.get('x-vona-test-stop-proof');
289
+ const expected = createHmac('sha256', key).update(`vona-test-stop-ack-v1\n${payload}`).digest();
290
+ if (
291
+ response.status !== 200 ||
292
+ !proof ||
293
+ !/^[a-f0-9]{64}$/.test(proof) ||
294
+ !timingSafeEqual(expected, Buffer.from(proof, 'hex')) ||
295
+ ((await response.json()) as { status?: string }).status !== 'stopping'
296
+ ) {
297
+ throw new Error('Isolated Vona shutdown refused or acknowledgment proof was invalid');
298
+ }
299
+ }
300
+
301
+ async function stopVonaCooperatively(
302
+ port: number,
303
+ env: NodeJS.ProcessEnv,
304
+ readyKey: string,
305
+ server: OwnedProcess,
306
+ dependencies: IsolatedVonaDependencies,
307
+ ) {
308
+ let timeout: ReturnType<typeof setTimeout> | undefined;
309
+ try {
310
+ return await Promise.race([
311
+ (async () => {
312
+ await (dependencies.requestStop ?? requestIsolatedVonaStop)(
313
+ port,
314
+ env.VONA_TEST_RUN_ID,
315
+ readyKey,
316
+ );
317
+ return await server.wait();
318
+ })(),
319
+ new Promise<never>((_, reject) => {
320
+ timeout = setTimeout(
321
+ () =>
322
+ reject(new Error('Isolated Vona cooperative shutdown did not prove drainage in time')),
323
+ dependencies.shutdownTimeoutMs ?? 20_000,
324
+ );
325
+ }),
326
+ ]);
327
+ } finally {
328
+ clearTimeout(timeout);
329
+ }
330
+ }
331
+
258
332
  export async function withIsolatedVona<T>(
259
333
  port: number,
260
334
  env: NodeJS.ProcessEnv,
261
335
  callback: () => Promise<T>,
262
- dependencies = { start: startOwnedProcess, ready: waitForReady },
336
+ dependencies: IsolatedVonaDependencies = { start: startOwnedProcess, ready: waitForReady },
263
337
  ): Promise<T> {
264
338
  const readyKey = randomBytes(32).toString('hex');
265
339
  const command = getIsolatedVonaCommand(port, env, readyKey);
@@ -284,8 +358,43 @@ export async function withIsolatedVona<T>(
284
358
  let operationError: Error | undefined;
285
359
  let shutdownError: Error | undefined;
286
360
  let result: T | undefined;
361
+ let ready = false;
362
+ const windows = (dependencies.platform ?? process.platform) === 'win32';
363
+ const shutdown = async () => {
364
+ const cooperative = windows && ready && !interrupted && !exitedEarly;
365
+ try {
366
+ const stopped = cooperative
367
+ ? await stopVonaCooperatively(port, env, readyKey, server, dependencies)
368
+ : await server.stop();
369
+ if (
370
+ stopped.failed ||
371
+ interrupted ||
372
+ exitedEarly ||
373
+ stopped.result?.code !== 0 ||
374
+ (windows && !!stopped.result?.signal)
375
+ ) {
376
+ throw new Error(
377
+ 'Isolated Vona server did not stop cleanly, exited early, or run was interrupted',
378
+ );
379
+ }
380
+ } catch (error) {
381
+ // Force-stop remains a failed-run fallback, never successful Windows teardown.
382
+ if (cooperative) {
383
+ try {
384
+ await server.stop();
385
+ } catch (fallbackError) {
386
+ throw new AggregateError(
387
+ [error, fallbackError],
388
+ 'Isolated Vona cooperative shutdown and failure-stop failed',
389
+ );
390
+ }
391
+ }
392
+ throw error;
393
+ }
394
+ };
287
395
  try {
288
396
  await dependencies.ready(port, server, readyKey);
397
+ ready = true;
289
398
  if (interrupted) throw new Error('Isolated Vona run interrupted');
290
399
  result = await callback();
291
400
  } catch (error) {
@@ -293,12 +402,7 @@ export async function withIsolatedVona<T>(
293
402
  } finally {
294
403
  stopping = true;
295
404
  try {
296
- const stopped = await server.stop();
297
- if (stopped.failed || interrupted || exitedEarly || stopped.result?.code !== 0) {
298
- shutdownError = new Error(
299
- 'Isolated Vona server did not stop cleanly, exited early, or run was interrupted',
300
- );
301
- }
405
+ await shutdown();
302
406
  } catch (error) {
303
407
  shutdownError = toError(error);
304
408
  } finally {
@@ -384,10 +488,27 @@ export async function waitForReady(
384
488
  throw new Error('Isolated Vona server did not become ready with a valid ownership proof');
385
489
  }
386
490
 
491
+ export function resolveNpmCli(
492
+ env: NodeJS.ProcessEnv = process.env,
493
+ execPath = process.execPath,
494
+ ): string {
495
+ const candidates = [
496
+ env.npm_execpath,
497
+ resolve(dirname(execPath), 'node_modules/npm/bin/npm-cli.js'),
498
+ resolve(dirname(execPath), '../lib/node_modules/npm/bin/npm-cli.js'),
499
+ ];
500
+ for (const candidate of candidates) {
501
+ if (!candidate || !existsSync(candidate)) continue;
502
+ const file = realpathSync(candidate);
503
+ if (basename(file) === 'npm-cli.js' && basename(dirname(dirname(file))) === 'npm') return file;
504
+ }
505
+ throw new Error('Cannot resolve npm CLI; run through npm or install npm beside Node.js');
506
+ }
507
+
387
508
  export function getUnitTestCommands(args: string[]) {
388
509
  const vonaDir = resolve(ROOT_DIR, 'vona');
389
510
  return [
390
- { command: 'npm', args: ['run', 'prerun'], cwd: vonaDir },
511
+ { command: process.execPath, args: [resolveNpmCli(), 'run', 'prerun'], cwd: vonaDir },
391
512
  {
392
513
  command: process.execPath,
393
514
  args: [
@@ -468,6 +589,12 @@ export async function withIsolatedRun<T>(
468
589
  SERVER_SERVE_HOST: undefined,
469
590
  TMPDIR: resolve(dataRoot, 'tmp'),
470
591
  };
592
+ if (process.platform === 'win32') {
593
+ for (const key of Object.keys(env)) {
594
+ if (['TEMP', 'TMP', 'TMPDIR'].includes(key.toUpperCase())) delete env[key];
595
+ }
596
+ env.TEMP = env.TMP = env.TMPDIR = resolve(dataRoot, 'tmp');
597
+ }
471
598
  await broker.update({ type: 'status', status: 'running' });
472
599
  result = await callback({
473
600
  runId,
@@ -0,0 +1,327 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { spawn } from 'node:child_process';
5
+ import { randomUUID } from 'node:crypto';
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ mkdtempSync,
10
+ readFileSync,
11
+ realpathSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from 'node:fs';
15
+ import { createRequire } from 'node:module';
16
+ import { tmpdir } from 'node:os';
17
+ import { dirname, join } from 'node:path';
18
+ import { test } from 'node:test';
19
+ import { fileURLToPath, pathToFileURL } from 'node:url';
20
+
21
+ import { loadTestClaim } from '../vona/packages-vona/vona-core/src/lib/core/testClaim.ts';
22
+ import {
23
+ inspectWindowsProcess,
24
+ inspectWindowsProcesses,
25
+ } from '../vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts';
26
+ import { isTestClaimInactive } from './testClaimProcesses.ts';
27
+
28
+ const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
29
+ const require = createRequire(import.meta.url);
30
+ const nativeOptions = {
31
+ skip:
32
+ process.platform !== 'win32'
33
+ ? 'Requires native Windows, PowerShell 7 and installed Playwright Chromium'
34
+ : false,
35
+ timeout: 180_000,
36
+ };
37
+ const moduleUrl = (name: string) => new URL(name, import.meta.url).href;
38
+ const claimUrl = moduleUrl('../vona/packages-vona/vona-core/src/lib/core/testClaim.ts');
39
+ const nativeUrl = moduleUrl(
40
+ '../vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts',
41
+ );
42
+ const guardianUrl = moduleUrl('./testProcessGuardianWindows.ts');
43
+ const managedUrl = moduleUrl('../repo-e2e/scripts/managedTest.ts');
44
+
45
+ async function until<T>(
46
+ check: () => T | undefined | Promise<T | undefined>,
47
+ timeout = 60_000,
48
+ ): Promise<T> {
49
+ const deadline = Date.now() + timeout;
50
+ for (;;) {
51
+ const value = await check();
52
+ if (value !== undefined) return value;
53
+ if (Date.now() >= deadline) throw new Error('Windows Chromium containment fixture timed out');
54
+ await new Promise(resolve => setTimeout(resolve, 100));
55
+ }
56
+ }
57
+ function json<T>(file: string): T | undefined {
58
+ try {
59
+ return JSON.parse(readFileSync(file, 'utf8')) as T;
60
+ } catch (error) {
61
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' || error instanceof SyntaxError)
62
+ return undefined;
63
+ throw error;
64
+ }
65
+ }
66
+ async function exited(
67
+ identities: { pid: number; startTime: string }[],
68
+ inspect = inspectWindowsProcesses,
69
+ ) {
70
+ return await until(async () => {
71
+ const snapshots = await inspect(identities.map(item => item.pid));
72
+ return identities.every(
73
+ identity =>
74
+ !snapshots.some(item => item.pid === identity.pid && item.startTime === identity.startTime),
75
+ )
76
+ ? snapshots
77
+ : undefined;
78
+ }, 30_000);
79
+ }
80
+ test('exit observation accepts PID reuse but returns the occupied snapshot for cleanup checks', async () => {
81
+ const identities = [{ pid: 100, startTime: '134000000000000000' }];
82
+ const snapshots = [
83
+ {
84
+ pid: 100,
85
+ startTime: '134000000000000001',
86
+ host: 'test-host',
87
+ state: 'running' as const,
88
+ args: '',
89
+ },
90
+ ];
91
+ let inspections = 0;
92
+ const observed = await exited(identities, async pids => {
93
+ assert.deepEqual(pids, [100]);
94
+ inspections++;
95
+ return inspections === 1 ? [{ ...snapshots[0], ...identities[0] }] : snapshots;
96
+ });
97
+ assert.equal(observed, snapshots);
98
+ assert.equal(inspections, 2);
99
+ });
100
+
101
+ function closePromise(child: ChildProcess) {
102
+ return new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
103
+ child.once('error', reject);
104
+ child.once('close', (code, signal) => resolve({ code, signal }));
105
+ });
106
+ }
107
+
108
+ for (const mode of ['normal', 'cancel', 'controller-death', 'helper-death'] as const) {
109
+ test(
110
+ `real Playwright worker/Chromium inherited private Job: ${mode}`,
111
+ nativeOptions,
112
+ async () => {
113
+ // Deliberately resolve the public package/CLI. Missing browser binaries are a Windows gate failure, not a skip.
114
+ const playwrightUrl = pathToFileURL(require.resolve('@playwright/test')).href;
115
+ const playwrightCli = require.resolve('@playwright/test/cli');
116
+ const root = mkdtempSync(join(realpathSync(tmpdir()), 'cabloy chromium job 空格 '));
117
+ const project = join(root, 'vona');
118
+ mkdirSync(project);
119
+ const runId = `20261018010203-${randomUUID().replaceAll('-', '').slice(0, 8)}`;
120
+ const dataRoot = join(project, `.app-test-${runId}`);
121
+ const temporaryRoot = join(dataRoot, 'tmp');
122
+ mkdirSync(temporaryRoot, { recursive: true });
123
+ writeFileSync(join(dataRoot, '.vona-test-owner'), `${runId}\n`);
124
+ const configPath = join(root, 'playwright.config.mjs');
125
+ const specPath = join(root, 'contained.spec.mjs');
126
+ const controllerPath = join(root, 'controller.mjs');
127
+ const browserPath = join(root, 'browser.json');
128
+ const releasePath = join(root, 'release');
129
+ const pageReadyPath = join(root, 'page-ready');
130
+ const resultPath = join(root, 'result.json');
131
+ const claimPath = join(project, '.vona-test-claim', `${runId}.json`);
132
+ writeFileSync(
133
+ configPath,
134
+ `export default {testDir:${JSON.stringify(root)},testMatch:'contained.spec.mjs',workers:1,retries:0,timeout:120000,reporter:'line',outputDir:${JSON.stringify(join(dataRoot, 'test-results'))}};`,
135
+ );
136
+ writeFileSync(
137
+ specPath,
138
+ `
139
+ import playwrightTest from ${JSON.stringify(playwrightUrl)};
140
+ import {existsSync, writeFileSync} from 'node:fs';
141
+ import {useManagedBrowser} from ${JSON.stringify(managedUrl)};
142
+ import {assertTestClaimWindowsJob, sendTestClaimEvent} from ${JSON.stringify(claimUrl)};
143
+ import {inspectTestProcess, waitForTestProcessGroupExit} from ${JSON.stringify(moduleUrl('./testClaimProcesses.ts'))};
144
+ const {test:base,expect}=playwrightTest;
145
+ const test=base.extend({browser:[async({playwright},use)=>{
146
+ let child;
147
+ const chromium={launchServer:async options=>{const server=await playwright.chromium.launchServer(options);child=server.process();return server;},
148
+ connect:(...args)=>playwright.chromium.connect(...args)};
149
+ await useManagedBrowser({browserType:chromium,launchOptions:{},headless:true,channel:undefined},async browser=>{
150
+ const worker=await inspectTestProcess(process.pid), browserIdentity=await inspectTestProcess(child.pid);
151
+ await assertTestClaimWindowsJob(process.env,worker);
152
+ await assertTestClaimWindowsJob(process.env,browserIdentity);
153
+ const session=await browser.newBrowserCDPSession();
154
+ const rows=await session.send('SystemInfo.getProcessInfo');
155
+ const identities=[];
156
+ for(const row of rows.processInfo){
157
+ const identity=await inspectTestProcess(row.id);
158
+ await assertTestClaimWindowsJob(process.env,identity);
159
+ identities.push(identity);
160
+ }
161
+ await session.detach();
162
+ writeFileSync(${JSON.stringify(browserPath)},JSON.stringify({worker,browser:browserIdentity,processes:identities,locator:process.env.VONA_TEST_WINDOWS_GUARDIAN_ID}));
163
+ await use(browser);
164
+ },{env:process.env,projectPath:${JSON.stringify(project)},sendEvent:sendTestClaimEvent,inspectProcess:inspectTestProcess,waitForGroupExit:waitForTestProcessGroupExit});
165
+ },{scope:'worker',timeout:0}]});
166
+ test('actual Chromium worker membership',async({page})=>{
167
+ await page.setContent('<title>contained Chromium</title><h1>private Job</h1>');
168
+ await expect(page).toHaveTitle('contained Chromium');
169
+ writeFileSync(${JSON.stringify(pageReadyPath)},'ready');
170
+ while(!existsSync(${JSON.stringify(releasePath)})) await new Promise(resolve=>setTimeout(resolve,50));
171
+ });
172
+ `,
173
+ );
174
+ writeFileSync(
175
+ controllerPath,
176
+ `
177
+ import {writeFileSync} from 'node:fs';
178
+ import {PassThrough} from 'node:stream';
179
+ import {createTestClaim,startTestClaimBroker} from ${JSON.stringify(claimUrl)};
180
+ import {inspectWindowsProcess} from ${JSON.stringify(nativeUrl)};
181
+ import {startWindowsOwnedProcess} from ${JSON.stringify(guardianUrl)};
182
+ let guardian,helper,broker;let stopping=false;let diagnostics='';
183
+ process.on('message',message=>{
184
+ if(message==='stop'){stopping=true;if(guardian)void guardian.stop().catch(()=>{});}
185
+ if(message==='kill-helper'){if(!helper)throw Error('No live helper');helper.kill('SIGKILL');}
186
+ });
187
+ const stdout=new PassThrough(),stderr=new PassThrough();
188
+ stdout.on('data',value=>{diagnostics=(diagnostics+value).slice(-32768)});
189
+ stderr.on('data',value=>{diagnostics=(diagnostics+value).slice(-32768)});
190
+ try {
191
+ const owner=await inspectWindowsProcess(process.pid);
192
+ await createTestClaim(${JSON.stringify(project)},${JSON.stringify(runId)},'e2e',{pid:owner.pid,startTime:owner.startTime,host:owner.host});
193
+ broker=await startTestClaimBroker(${JSON.stringify(claimPath)});
194
+ const env={...process.env,...broker.env,META_MODE:'test',VONA_TEST_RUN_ID:${JSON.stringify(runId)}};
195
+ for(const key of Object.keys(env))if(['TEMP','TMP','TMPDIR','VONA_TEST_WINDOWS_GUARDIAN_ID'].includes(key.toUpperCase()))delete env[key];
196
+ Object.assign(env,{TEMP:${JSON.stringify(temporaryRoot)},TMP:${JSON.stringify(temporaryRoot)},TMPDIR:${JSON.stringify(temporaryRoot)},vona_test_windows_guardian_id:'inherited-spoof'});
197
+ guardian=await startWindowsOwnedProcess(process.execPath,[${JSON.stringify(playwrightCli)},'test','--config',${JSON.stringify(configPath)}],${JSON.stringify(rootDir)},env,{stdout,stderr,onHelperSpawn:value=>{helper=value}});
198
+ if(stopping)void guardian.stop().catch(()=>{});
199
+ const result=await guardian.wait();
200
+ writeFileSync(${JSON.stringify(resultPath)},JSON.stringify({result,diagnostics}));
201
+ } catch(error) {
202
+ writeFileSync(${JSON.stringify(resultPath)},JSON.stringify({error:String(error),diagnostics}));
203
+ } finally {await broker?.close();process.disconnect();}
204
+ `,
205
+ );
206
+ const sentinel = spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'], {
207
+ stdio: 'ignore',
208
+ });
209
+ const sentinelClosed = closePromise(sentinel);
210
+ const controller = spawn(process.execPath, [controllerPath], {
211
+ cwd: rootDir,
212
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
213
+ });
214
+ const controllerClosed = closePromise(controller);
215
+ let diagnostics = '';
216
+ controller.stdout!.on('data', value => {
217
+ diagnostics += value.toString();
218
+ });
219
+ controller.stderr!.on('data', value => {
220
+ diagnostics += value.toString();
221
+ });
222
+ try {
223
+ const sentinelIdentity = await inspectWindowsProcess(sentinel.pid!);
224
+ assert.ok(sentinelIdentity);
225
+ const record = await until(() => {
226
+ const value = json<{
227
+ worker: { pid: number; startTime: string };
228
+ browser: { pid: number; startTime: string };
229
+ processes: { pid: number; startTime: string }[];
230
+ locator: string;
231
+ }>(browserPath);
232
+ if (value && existsSync(pageReadyPath)) return value;
233
+ const result = json<{ error?: string; diagnostics: string }>(resultPath);
234
+ if (result || controller.exitCode !== null) {
235
+ throw new Error(
236
+ `Chromium did not reach membership gate: ${JSON.stringify(result)} ${diagnostics}`,
237
+ );
238
+ }
239
+ return undefined;
240
+ }, 90_000);
241
+ const manifest = await loadTestClaim(claimPath);
242
+ assert.equal(manifest.kind, 'e2e');
243
+ assert.equal(manifest.version, 1);
244
+ assert.equal(manifest.processes.length, 1);
245
+ const registered = manifest.processes[0];
246
+ assert.equal(registered.stage, 'registered');
247
+ assert.equal(registered.containment, 'windows-job');
248
+ assert.equal(record.locator, registered.id);
249
+ assert.notEqual(record.locator, 'inherited-spoof');
250
+ assert.notEqual(record.worker.pid, record.browser.pid);
251
+ assert.ok(record.processes.some(item => item.pid === record.browser.pid));
252
+ const ownedIdentities = [record.worker, record.browser, ...record.processes];
253
+ if (mode === 'normal') writeFileSync(releasePath, 'close');
254
+ else if (mode === 'cancel') controller.send('stop');
255
+ else if (mode === 'controller-death') controller.kill('SIGKILL');
256
+ else controller.send('kill-helper');
257
+ await Promise.race([
258
+ controllerClosed,
259
+ new Promise<never>((_, reject) => {
260
+ const timer = setTimeout(
261
+ () => reject(new Error(`Controller shutdown timed out: ${diagnostics}`)),
262
+ 40_000,
263
+ );
264
+ timer.unref();
265
+ void controllerClosed.finally(() => clearTimeout(timer)).catch(() => {});
266
+ }),
267
+ ]);
268
+ // Compare exact identities: a later native inspector can reuse an exited PID itself.
269
+ const snapshots = await exited([
270
+ ...ownedIdentities,
271
+ { pid: registered.pid!, startTime: registered.startTime! },
272
+ manifest.owner,
273
+ ]);
274
+ assert.deepEqual(await inspectWindowsProcess(sentinel.pid!), sentinelIdentity);
275
+ const retained = await loadTestClaim(claimPath);
276
+ assert.equal(retained.processes.length, 1); // No per-browser intents/registration/drainage on Windows.
277
+ if (mode === 'normal' || mode === 'cancel') {
278
+ const completed = json<{
279
+ result?: { failed: boolean; result: { code: number } };
280
+ error?: string;
281
+ diagnostics: string;
282
+ }>(resultPath);
283
+ assert.ok(completed?.result, JSON.stringify(completed));
284
+ assert.equal(completed.result.failed, mode !== 'normal', completed.diagnostics);
285
+ if (mode === 'normal')
286
+ assert.equal(completed.result.result.code, 0, completed.diagnostics);
287
+ assert.equal(retained.processes[0].stage, 'drained');
288
+ // Cleanup deliberately blocks reused PIDs too. Evaluate the same native snapshot
289
+ // used above, not a second inspector that can change PID occupancy between checks.
290
+ const inactivity = await isTestClaimInactive(retained, false, async () => snapshots);
291
+ const occupied = snapshots.some(item =>
292
+ [retained.owner.pid, retained.processes[0].pid].includes(item.pid),
293
+ );
294
+ assert.deepEqual(
295
+ inactivity,
296
+ occupied
297
+ ? {
298
+ inactive: false,
299
+ reason: snapshots.some(item => item.pid === retained.owner.pid)
300
+ ? 'claim owner PID is still present'
301
+ : 'registered Windows job owner is still present',
302
+ }
303
+ : { inactive: true },
304
+ JSON.stringify({ snapshots, owner: retained.owner, guardian: retained.processes[0] }),
305
+ );
306
+ } else {
307
+ assert.equal(retained.processes[0].stage, 'registered');
308
+ assert.equal((await isTestClaimInactive(retained)).inactive, false);
309
+ if (mode === 'helper-death') {
310
+ assert.match(
311
+ json<{ error: string }>(resultPath)!.error,
312
+ /without proving Job shutdown/,
313
+ );
314
+ }
315
+ }
316
+ } finally {
317
+ if (controller.exitCode === null && controller.signalCode === null)
318
+ controller.kill('SIGKILL');
319
+ await controllerClosed.catch(() => {});
320
+ sentinel.kill();
321
+ await sentinelClosed;
322
+ // Retention assertions precede removal of this test's own temporary fixture.
323
+ if (existsSync(root)) rmSync(root, { recursive: true, force: true });
324
+ }
325
+ },
326
+ );
327
+ }
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
2
3
  import {
3
4
  chmod,
4
5
  lstat,
@@ -22,6 +23,7 @@ import {
22
23
  loadTestClaim,
23
24
  writeTestClaim,
24
25
  } from '../vona/packages-vona/vona-core/src/lib/core/testClaim.ts';
26
+ import { createTestClaimFile } from '../vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.ts';
25
27
  import {
26
28
  getTestAppName,
27
29
  getDatabaseTestPrefix,
@@ -204,7 +206,11 @@ test('live, missing inspector, drifted config, malformed manifest and unmarked r
204
206
  if (reason === 'unmarked') await rm(path.join(f.manifest.locals[0].path, '.vona-test-owner'));
205
207
  if (reason === 'symlink') {
206
208
  await rm(f.manifest.locals[0].path, { recursive: true });
207
- await symlink(path.join(f.project, '.app'), f.manifest.locals[0].path);
209
+ await symlink(
210
+ path.join(f.project, '.app'),
211
+ f.manifest.locals[0].path,
212
+ process.platform === 'win32' ? 'junction' : 'dir',
213
+ );
208
214
  }
209
215
  const result = await cleanTestClaim(f.claimPath, {
210
216
  isInactive: reason === 'inspector' ? undefined : async () => reason !== 'live',
@@ -250,10 +256,9 @@ test('exclusive cleaners skip contenders; orphaned old lock is never taken over'
250
256
  }
251
257
  const second = await fixture();
252
258
  try {
253
- await writeFile(
259
+ await createTestClaimFile(
254
260
  `${second.claimPath}.cleanup.lock`,
255
261
  JSON.stringify({ pid: 999999, startTime: 'ancient', token: 'orphan' }),
256
- { mode: 0o600 },
257
262
  );
258
263
  const result = await reapExpiredTestClaims(second.project, {
259
264
  now: Date.parse(second.manifest.createdAt) + 10000,
@@ -271,12 +276,23 @@ test('exclusive cleaners skip contenders; orphaned old lock is never taken over'
271
276
  test('unsafe shared directories fail invocation; legacy directories warn and remain', async () => {
272
277
  const f = await fixture();
273
278
  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);
279
+ if (process.platform === 'win32') {
280
+ execFileSync('icacls.exe', [path.dirname(f.claimPath), '/grant', '*S-1-1-0:(R)'], {
281
+ windowsHide: true,
282
+ stdio: 'pipe',
283
+ });
284
+ } else {
285
+ await chmod(path.dirname(f.claimPath), 0o777);
286
+ }
287
+ await assert.rejects(reapExpiredTestClaims(f.project, { isInactive: inactive }), /Unsafe/);
288
+ if (process.platform === 'win32') {
289
+ execFileSync('icacls.exe', [path.dirname(f.claimPath), '/remove:g', '*S-1-1-0'], {
290
+ windowsHide: true,
291
+ stdio: 'pipe',
292
+ });
293
+ } else {
294
+ await chmod(path.dirname(f.claimPath), 0o700);
295
+ }
280
296
  const legacy = path.join(f.project, `.vona-test-claim-${runId}`);
281
297
  await mkdir(legacy);
282
298
  const warnings: string[] = [];