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
@@ -0,0 +1,602 @@
1
+ import type { ChildProcessWithoutNullStreams } 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 { tmpdir } from 'node:os';
16
+ import { dirname, join } from 'node:path';
17
+ import { PassThrough } from 'node:stream';
18
+ import { test } from 'node:test';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ import type { WindowsNativeJob } from '../vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts';
22
+
23
+ import {
24
+ createTestClaim,
25
+ loadTestClaim,
26
+ sendTestClaimEvent,
27
+ startTestClaimBroker,
28
+ } from '../vona/packages-vona/vona-core/src/lib/core/testClaim.ts';
29
+ import {
30
+ createWindowsNativeJob,
31
+ inspectWindowsProcess,
32
+ inspectWindowsProcesses,
33
+ resolveWindowsProcessAssets,
34
+ } from '../vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts';
35
+ import { isTestClaimInactive } from './testClaimProcesses.ts';
36
+ import { startWindowsOwnedProcess } from './testProcessGuardianWindows.ts';
37
+
38
+ const nativeOptions = {
39
+ skip:
40
+ process.platform !== 'win32'
41
+ ? 'Requires native Windows + PowerShell 7; not executed on this platform'
42
+ : false,
43
+ timeout: 120_000,
44
+ };
45
+ const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
46
+ const nativeModule = new URL(
47
+ '../vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts',
48
+ import.meta.url,
49
+ );
50
+
51
+ function fixture() {
52
+ const root = mkdtempSync(join(realpathSync(tmpdir()), 'cabloy windows tree 空格 '));
53
+ return { root, dispose: () => rmSync(root, { recursive: true, force: true }) };
54
+ }
55
+
56
+ async function until<T>(
57
+ check: () => T | undefined | Promise<T | undefined>,
58
+ timeout = 10_000,
59
+ ): Promise<T> {
60
+ const deadline = Date.now() + timeout;
61
+ for (;;) {
62
+ const value = await check();
63
+ if (value !== undefined) return value;
64
+ if (Date.now() >= deadline)
65
+ throw new Error('Windows process fixture did not reach expected state');
66
+ await new Promise(resolve => setTimeout(resolve, 50));
67
+ }
68
+ }
69
+
70
+ async function readPids(path: string) {
71
+ return await until(() => {
72
+ try {
73
+ return JSON.parse(readFileSync(path, 'utf8')) as number[];
74
+ } catch (error) {
75
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT' || error instanceof SyntaxError)
76
+ return undefined;
77
+ throw error;
78
+ }
79
+ });
80
+ }
81
+
82
+ async function absent(pids: number[]) {
83
+ await until(
84
+ async () => ((await inspectWindowsProcesses(pids)).length === 0 ? true : undefined),
85
+ 20_000,
86
+ );
87
+ }
88
+
89
+ function output() {
90
+ const stdout = new PassThrough();
91
+ const stderr = new PassThrough();
92
+ let out = '',
93
+ err = '';
94
+ stdout.on('data', value => {
95
+ out += value.toString('utf8');
96
+ });
97
+ stderr.on('data', value => {
98
+ err += value.toString('utf8');
99
+ });
100
+ return { stdout, stderr, text: () => ({ out, err }) };
101
+ }
102
+
103
+ async function launch(
104
+ code: string,
105
+ args: string[] = [],
106
+ cwd = rootDir,
107
+ options: {
108
+ rejectJobAssignment?: boolean;
109
+ onHelperSpawn?: (helper: ChildProcessWithoutNullStreams) => void;
110
+ } = {},
111
+ ) {
112
+ const streams = output();
113
+ const job = await createWindowsNativeJob(
114
+ {
115
+ jobId: randomUUID(),
116
+ command: process.execPath,
117
+ args: ['-e', code, ...args],
118
+ cwd,
119
+ env: { ...process.env },
120
+ rejectJobAssignment: options.rejectJobAssignment,
121
+ },
122
+ { ...streams, onHelperSpawn: options.onHelperSpawn },
123
+ );
124
+ await job.ready;
125
+ job.authorize();
126
+ return { job, streams };
127
+ }
128
+
129
+ const treeCode = `
130
+ const {spawn} = require('node:child_process');
131
+ const fs = require('node:fs');
132
+ const path = require('node:path');
133
+ const directory = process.argv[1];
134
+ const grandchild = "require('node:fs').writeFileSync(process.argv[1], JSON.stringify([process.pid])); setInterval(()=>{},1000)";
135
+ const childCode = "const c=require('node:child_process').spawn(process.execPath,['-e'," + JSON.stringify(grandchild) + ",process.argv[1]],{detached:true,stdio:'ignore'});c.unref();require('node:fs').writeFileSync(process.argv[2],JSON.stringify([process.pid,c.pid]));setInterval(()=>{},1000)";
136
+ const child = spawn(process.execPath, ['-e',childCode,path.join(directory,'grandchild.json'),path.join(directory,'child.json')], {detached:true,stdio:'ignore'});
137
+ child.unref(); fs.writeFileSync(path.join(directory,'root.json'), JSON.stringify([process.pid,child.pid]));
138
+ setInterval(()=>{},1000);
139
+ `;
140
+
141
+ async function treePids(root: string) {
142
+ const rootPids = await readPids(join(root, 'root.json'));
143
+ const childPids = await readPids(join(root, 'child.json'));
144
+ const grandPids = await readPids(join(root, 'grandchild.json'));
145
+ return [...new Set([...rootPids, ...childPids, ...grandPids])];
146
+ }
147
+
148
+ test('Windows native assets resolve from both source and packaged compiled module locations', () => {
149
+ const source = resolveWindowsProcessAssets(nativeModule.href);
150
+ const compiledUrl = new URL(
151
+ '../vona/packages-vona/vona-core/dist/lib/core/testProcessWindowsNative.js',
152
+ import.meta.url,
153
+ );
154
+ assert.deepEqual(resolveWindowsProcessAssets(compiledUrl.href), source);
155
+ assert.equal(existsSync(source.source), true);
156
+ assert.match(readFileSync(source.script, 'utf8'), /PSVersion\.Major -lt 7/);
157
+ });
158
+
159
+ test('native implementation has no post-creation Job assignment or historical PID termination fallback', () => {
160
+ const { source } = resolveWindowsProcessAssets(nativeModule.href);
161
+ const code = readFileSync(source, 'utf8');
162
+ assert.match(code, /PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x2000D/);
163
+ assert.match(
164
+ code,
165
+ /CREATE_SUSPENDED \| CREATE_UNICODE_ENVIRONMENT \| EXTENDED_STARTUPINFO_PRESENT/,
166
+ );
167
+ assert.match(code, /rootExited && active == 0/);
168
+ assert.match(code, /CreateJobObjectW\(IntPtr.Zero, null\)/);
169
+ assert.match(code, /SetHandleInformation\(job, HANDLE_FLAG_INHERIT, 0\)/);
170
+ assert.match(code, /CreationTime\(process\) == creation/);
171
+ assert.match(code, /IsProcessInJob\(process, job, out inJob\)/);
172
+ assert.doesNotMatch(
173
+ code,
174
+ /extern.*AssignProcessToJobObject|taskkill|BREAKAWAY_OK|DuplicateHandle/,
175
+ );
176
+ });
177
+
178
+ test(
179
+ 'Windows process adapter fails explicitly off native Windows',
180
+ { skip: process.platform === 'win32' },
181
+ async () => {
182
+ await assert.rejects(inspectWindowsProcess(process.pid), /requires native Windows/);
183
+ await assert.rejects(
184
+ startWindowsOwnedProcess(process.execPath, [], rootDir, process.env),
185
+ /require native Windows/,
186
+ );
187
+ },
188
+ );
189
+
190
+ test(
191
+ 'native inspection uses stable exact FILETIME strings, with absent and denied distinguished',
192
+ nativeOptions,
193
+ async () => {
194
+ const first = await inspectWindowsProcess(process.pid);
195
+ const second = await inspectWindowsProcess(process.pid);
196
+ assert.ok(first);
197
+ assert.deepEqual(first, second);
198
+ assert.match(first.startTime, /^\d{17,20}$/);
199
+ assert.equal((await inspectWindowsProcesses([process.pid, process.pid])).length, 1);
200
+ assert.equal(await inspectWindowsProcess(0xffffffff), undefined);
201
+ // Hosted administrators may inspect System. Only access denial is an alternative to a live identity.
202
+ let system;
203
+ try {
204
+ system = await inspectWindowsProcess(4);
205
+ } catch (error) {
206
+ assert.ok(error instanceof Error);
207
+ assert.match(error.message, /Win32Exception \(5\): (?:OpenProcess\(4\)|GetProcessTimes)/);
208
+ return;
209
+ }
210
+ assert.ok(system, 'Windows System must not be reported absent');
211
+ assert.equal(system.pid, 4);
212
+ assert.equal(system.host, first.host);
213
+ assert.equal(system.state, 'running');
214
+ assert.match(system.startTime, /^\d{17,20}$/);
215
+ },
216
+ );
217
+
218
+ test(
219
+ 'owned Windows processes preserve Unicode, empty args, quoting, cwd and environment with separate raw streams',
220
+ nativeOptions,
221
+ async () => {
222
+ const f = fixture();
223
+ const values = ['', '空格 value', 'literal "quotes"', 'end\\', 'a\\"b', '& | % ^ $ `'];
224
+ const streams = output();
225
+ let job: WindowsNativeJob | undefined;
226
+ try {
227
+ job = await createWindowsNativeJob(
228
+ {
229
+ jobId: randomUUID(),
230
+ command: process.execPath,
231
+ args: [
232
+ '-e',
233
+ 'process.stdout.write(JSON.stringify({args:process.argv.slice(1),cwd:process.cwd(),env:process.env.CABLOY_TEST_UNICODE}));process.stderr.write("raw stderr 空格");',
234
+ ...values,
235
+ ],
236
+ cwd: f.root,
237
+ env: { ...process.env, CABLOY_TEST_UNICODE: '环境 value' },
238
+ },
239
+ streams,
240
+ );
241
+ await job.ready;
242
+ job.authorize();
243
+ await job.started;
244
+ const result = await job.wait();
245
+ assert.equal(result.failed, false);
246
+ assert.equal(result.result.code, 0);
247
+ assert.deepEqual(JSON.parse(streams.text().out), {
248
+ args: values,
249
+ cwd: f.root,
250
+ env: '环境 value',
251
+ });
252
+ assert.equal(streams.text().err, 'raw stderr 空格');
253
+ } finally {
254
+ job?.stop();
255
+ if (job) await job.wait().catch(() => {});
256
+ f.dispose();
257
+ }
258
+ },
259
+ );
260
+
261
+ test(
262
+ 'private Job verifier requires exact live PID/FILETIME and rejects outsiders and another owned Job',
263
+ nativeOptions,
264
+ async () => {
265
+ const f = fixture();
266
+ const secondRoot = join(f.root, 'second');
267
+ mkdirSync(secondRoot);
268
+ const sentinel = spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'], {
269
+ stdio: 'ignore',
270
+ });
271
+ const sentinelClosed = new Promise<void>(resolve => sentinel.once('close', () => resolve()));
272
+ let job: WindowsNativeJob | undefined;
273
+ let other: WindowsNativeJob | undefined;
274
+ try {
275
+ job = (await launch(treeCode, [f.root])).job;
276
+ other = (await launch(treeCode, [secondRoot])).job;
277
+ await Promise.all([job.started, other.started]);
278
+ const pids = await treePids(f.root);
279
+ const otherPids = await treePids(secondRoot);
280
+ const identities = await inspectWindowsProcesses([...pids, ...otherPids, sentinel.pid!]);
281
+ const find = (pid: number) => {
282
+ const identity = identities.find(item => item.pid === pid);
283
+ assert.ok(identity);
284
+ return identity;
285
+ };
286
+ assert.deepEqual(await Promise.all(pids.map(pid => job!.contains(find(pid)))), [
287
+ true,
288
+ true,
289
+ true,
290
+ ]);
291
+ assert.equal(
292
+ await job.contains({ ...find(pids[0]), startTime: '133000000000000000' }),
293
+ false,
294
+ );
295
+ assert.equal(await job.contains({ pid: 0xffffffff, startTime: '133000000000000000' }), false);
296
+ assert.equal(await job.contains(find(sentinel.pid!)), false);
297
+ assert.equal(await job.contains(find(otherPids[0])), false);
298
+ assert.equal(await other.contains(find(otherPids[0])), true);
299
+ assert.equal(await other.contains(find(pids[0])), false);
300
+ await assert.rejects(
301
+ job.contains({ pid: 0, startTime: '133000000000000000' }),
302
+ /unavailable or invalid/,
303
+ );
304
+ await assert.rejects(
305
+ job.contains({ pid: pids[0], startTime: 'not-FILETIME' }),
306
+ /unavailable or invalid/,
307
+ );
308
+ job.stop();
309
+ await assert.rejects(job.contains(find(pids[0])), /unavailable or invalid/);
310
+ assert.equal((await job.wait()).failed, true);
311
+ await absent(pids);
312
+ assert.deepEqual(await inspectWindowsProcess(sentinel.pid!), find(sentinel.pid!));
313
+ } finally {
314
+ job?.stop();
315
+ other?.stop();
316
+ await Promise.all([job?.wait().catch(() => {}), other?.wait().catch(() => {})]);
317
+ sentinel.kill();
318
+ await sentinelClosed;
319
+ f.dispose();
320
+ }
321
+ },
322
+ );
323
+
324
+ test('target cannot spoof control protocol with JSON-looking stdout', nativeOptions, async () => {
325
+ const { job, streams } = await launch(
326
+ 'console.log(JSON.stringify({type:"drained",code:0,failed:false}));process.exit(7)',
327
+ );
328
+ await job.started;
329
+ const result = await job.wait();
330
+ assert.equal(result.result.code, 7);
331
+ assert.equal(result.failed, false);
332
+ assert.match(streams.text().out, /"type":"drained"/);
333
+ });
334
+
335
+ test('normal completion waits for a detached child and grandchild', nativeOptions, async () => {
336
+ const f = fixture();
337
+ try {
338
+ const flag = join(f.root, 'done');
339
+ const script =
340
+ "const {spawn}=require('node:child_process');const grand='setTimeout(()=>require(\"node:fs\").writeFileSync(process.argv[1],\"done\"),350)';const child='const c=require(\"node:child_process\").spawn(process.execPath,[\"-e\",'+JSON.stringify(grand)+',process.argv[1]],{detached:true,stdio:\"ignore\"});c.unref()';const c=spawn(process.execPath,['-e',child,process.argv[1]],{detached:true,stdio:'ignore'});c.unref();";
341
+ const { job } = await launch(script, [flag]);
342
+ await job.started;
343
+ const result = await job.wait();
344
+ assert.equal(result.failed, false);
345
+ assert.equal(result.result.code, 0);
346
+ assert.equal(readFileSync(flag, 'utf8'), 'done');
347
+ } finally {
348
+ f.dispose();
349
+ }
350
+ });
351
+
352
+ test(
353
+ 'stop drains live root/child/detached grandchild, is failure, and preserves unrelated sentinel',
354
+ nativeOptions,
355
+ async () => {
356
+ const f = fixture();
357
+ const sentinel = spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'], {
358
+ stdio: 'ignore',
359
+ });
360
+ const sentinelClosed = new Promise<void>(resolve => sentinel.once('close', () => resolve()));
361
+ let job: WindowsNativeJob | undefined;
362
+ try {
363
+ const launched = await launch(treeCode, [f.root]);
364
+ job = launched.job;
365
+ await job.started;
366
+ const pids = await treePids(f.root);
367
+ assert.equal(pids.length, 3);
368
+ const sentinelIdentity = await inspectWindowsProcess(sentinel.pid!);
369
+ job.stop();
370
+ const result = await job.wait();
371
+ assert.equal(result.failed, true);
372
+ await absent(pids);
373
+ assert.deepEqual(await inspectWindowsProcess(sentinel.pid!), sentinelIdentity);
374
+ } finally {
375
+ job?.stop();
376
+ if (job) await job.wait().catch(() => {});
377
+ sentinel.kill();
378
+ await sentinelClosed;
379
+ f.dispose();
380
+ }
381
+ },
382
+ );
383
+
384
+ test(
385
+ 'control-input disconnect kills a live detached tree and never reports success',
386
+ nativeOptions,
387
+ async () => {
388
+ const f = fixture();
389
+ let job: WindowsNativeJob | undefined;
390
+ try {
391
+ job = (await launch(treeCode, [f.root])).job;
392
+ await job.started;
393
+ const pids = await treePids(f.root);
394
+ job.disconnect();
395
+ assert.equal((await job.wait()).failed, true);
396
+ await absent(pids);
397
+ } finally {
398
+ job?.stop();
399
+ if (job) await job.wait().catch(() => {});
400
+ f.dispose();
401
+ }
402
+ },
403
+ );
404
+
405
+ test(
406
+ 'helper hard death closes sole Job handle, kills detached tree, and produces no drainage proof',
407
+ nativeOptions,
408
+ async () => {
409
+ const f = fixture();
410
+ let helper: ChildProcessWithoutNullStreams | undefined;
411
+ let job: WindowsNativeJob | undefined;
412
+ try {
413
+ job = (
414
+ await launch(treeCode, [f.root], rootDir, {
415
+ onHelperSpawn: value => {
416
+ helper = value;
417
+ },
418
+ })
419
+ ).job;
420
+ await job.started;
421
+ const pids = await treePids(f.root);
422
+ assert.ok(helper);
423
+ helper.kill('SIGKILL'); // live test-owned ChildProcess, not historical PID
424
+ await assert.rejects(job.wait(), /without proving Job shutdown/);
425
+ await absent(pids);
426
+ } finally {
427
+ job?.stop();
428
+ if (job) await job.wait().catch(() => {});
429
+ f.dispose();
430
+ }
431
+ },
432
+ );
433
+
434
+ test(
435
+ 'a hard-dead controller closes its control pipe and kills the owned detached tree',
436
+ nativeOptions,
437
+ async () => {
438
+ const f = fixture();
439
+ const script = join(f.root, 'controller.mjs');
440
+ const nativeUrl = nativeModule.href;
441
+ writeFileSync(
442
+ script,
443
+ `import {createWindowsNativeJob} from ${JSON.stringify(nativeUrl)};const job=await createWindowsNativeJob({jobId:${JSON.stringify(randomUUID())},command:process.execPath,args:['-e',${JSON.stringify(treeCode)},${JSON.stringify(f.root)}],cwd:${JSON.stringify(rootDir)},env:process.env});await job.ready;job.authorize();await job.started;setInterval(()=>{},1000);`,
444
+ );
445
+ const controller = spawn(process.execPath, [script], { stdio: 'ignore' });
446
+ const closed = new Promise<void>(resolve => controller.once('close', () => resolve()));
447
+ try {
448
+ const pids = await treePids(f.root);
449
+ controller.kill('SIGKILL');
450
+ await closed;
451
+ await absent(pids);
452
+ } finally {
453
+ if (controller.exitCode === null) controller.kill();
454
+ await closed;
455
+ f.dispose();
456
+ }
457
+ },
458
+ );
459
+
460
+ test(
461
+ 'creation-time Job assignment failure prevents every line of user code',
462
+ nativeOptions,
463
+ async () => {
464
+ const f = fixture();
465
+ let job: WindowsNativeJob | undefined;
466
+ try {
467
+ const marker = join(f.root, 'must-not-run');
468
+ job = (
469
+ await launch(
470
+ 'require("node:fs").writeFileSync(process.argv[1],"unsafe")',
471
+ [marker],
472
+ rootDir,
473
+ { rejectJobAssignment: true },
474
+ )
475
+ ).job;
476
+ await assert.rejects(job.started, /without proving Job shutdown/);
477
+ await assert.rejects(job.wait(), /without proving Job shutdown/);
478
+ assert.equal(existsSync(marker), false);
479
+ } finally {
480
+ job?.stop();
481
+ if (job) await job.wait().catch(() => {});
482
+ f.dispose();
483
+ }
484
+ },
485
+ );
486
+
487
+ test(
488
+ 'helper crash retains a durable registered claim even after every owned process is absent',
489
+ nativeOptions,
490
+ async () => {
491
+ const f = fixture();
492
+ const project = join(f.root, 'vona');
493
+ mkdirSync(project);
494
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
495
+ let helper: ChildProcessWithoutNullStreams | undefined;
496
+ let job: WindowsNativeJob | undefined;
497
+ try {
498
+ const owner = await inspectWindowsProcess(process.pid);
499
+ assert.ok(owner);
500
+ const runId = `20261018010203-${randomUUID().replaceAll('-', '').slice(0, 8)}`;
501
+ const { claimPath } = await createTestClaim(project, runId, 'unit', {
502
+ pid: owner.pid,
503
+ startTime: owner.startTime,
504
+ host: owner.host,
505
+ });
506
+ broker = await startTestClaimBroker(claimPath);
507
+ const env = { ...process.env, ...broker.env, META_MODE: 'test', VONA_TEST_RUN_ID: runId };
508
+ const registration = {
509
+ id: randomUUID(),
510
+ role: 'guardian',
511
+ containment: 'windows-job' as const,
512
+ jobId: randomUUID(),
513
+ };
514
+ await sendTestClaimEvent(env, {
515
+ type: 'process',
516
+ process: { ...registration, stage: 'pending' },
517
+ });
518
+ const streams = output();
519
+ job = await createWindowsNativeJob(
520
+ {
521
+ jobId: registration.jobId,
522
+ command: process.execPath,
523
+ args: ['-e', treeCode, f.root],
524
+ cwd: rootDir,
525
+ env,
526
+ },
527
+ {
528
+ ...streams,
529
+ onHelperSpawn: value => {
530
+ helper = value;
531
+ },
532
+ },
533
+ );
534
+ const identity = await job.ready;
535
+ await sendTestClaimEvent(env, {
536
+ type: 'process',
537
+ process: {
538
+ ...registration,
539
+ stage: 'registered',
540
+ pid: identity.pid,
541
+ startTime: identity.startTime,
542
+ host: identity.host,
543
+ },
544
+ });
545
+ job.authorize();
546
+ await job.started;
547
+ const pids = await treePids(f.root);
548
+ assert.ok(helper);
549
+ helper.kill('SIGKILL');
550
+ await assert.rejects(job.wait(), /without proving Job shutdown/);
551
+ await absent([...pids, identity.pid]);
552
+ const retained = await loadTestClaim(claimPath);
553
+ assert.equal(retained.processes[0].stage, 'registered');
554
+ assert.equal((await isTestClaimInactive(retained, true)).inactive, false);
555
+ } finally {
556
+ job?.stop();
557
+ if (job) await job.wait().catch(() => {});
558
+ await broker?.close();
559
+ f.dispose();
560
+ }
561
+ },
562
+ );
563
+
564
+ test(
565
+ 'managed guardian durably records helper identity and only then drainage, including nonzero exit',
566
+ nativeOptions,
567
+ async () => {
568
+ const f = fixture();
569
+ const project = join(f.root, 'vona');
570
+ mkdirSync(project);
571
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
572
+ try {
573
+ const owner = await inspectWindowsProcess(process.pid);
574
+ assert.ok(owner);
575
+ const runId = `20261018010203-${randomUUID().replaceAll('-', '').slice(0, 8)}`;
576
+ const { claimPath } = await createTestClaim(project, runId, 'unit', {
577
+ pid: owner.pid,
578
+ startTime: owner.startTime,
579
+ host: owner.host,
580
+ });
581
+ broker = await startTestClaimBroker(claimPath);
582
+ const env = { ...process.env, ...broker.env, META_MODE: 'test', VONA_TEST_RUN_ID: runId };
583
+ const child = await startWindowsOwnedProcess(
584
+ process.execPath,
585
+ ['-e', 'process.exit(7)'],
586
+ rootDir,
587
+ env,
588
+ );
589
+ assert.equal((await child.wait()).result?.code, 7);
590
+ const processes = (await loadTestClaim(claimPath)).processes;
591
+ assert.equal(processes.length, 1);
592
+ assert.equal(processes[0].stage, 'drained');
593
+ assert.equal(processes[0].containment, 'windows-job');
594
+ assert.equal(processes[0].pgid, undefined);
595
+ assert.match(processes[0].startTime!, /^\d{17,20}$/);
596
+ assert.ok(processes[0].jobId);
597
+ } finally {
598
+ await broker?.close();
599
+ f.dispose();
600
+ }
601
+ },
602
+ );