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,5 +1,6 @@
1
1
  import '@cabloy/json5';
2
2
  import assert from 'node:assert/strict';
3
+ import { execFileSync } from 'node:child_process';
3
4
  import {
4
5
  chmodSync,
5
6
  lstatSync,
@@ -14,9 +15,12 @@ import { hostname, tmpdir } from 'node:os';
14
15
  import path from 'node:path';
15
16
  import { test } from 'node:test';
16
17
 
18
+ import type { TestClaimEnv } from './testClaim.ts';
19
+
17
20
  import {
18
21
  assertTestClaimDatabaseClient,
19
22
  assertTestClaimRedisClient,
23
+ assertTestClaimWindowsJob,
20
24
  buildTestClaimResourceInventory,
21
25
  createTestClaim,
22
26
  getTestClaimCreatedAt,
@@ -28,13 +32,28 @@ import {
28
32
  normalizeTestClaimRedisEndpoint,
29
33
  registerTestClaimDatabaseCreation,
30
34
  registerTestClaimResourceInventory,
35
+ registerTestClaimWindowsJobVerifier,
31
36
  sendTestClaimEvent,
37
+ TEST_CLAIM_WINDOWS_GUARDIAN_ENV,
32
38
  startTestClaimBroker,
33
39
  validateTestClaim,
34
40
  writeTestClaim,
35
41
  } from './testClaim.ts';
42
+ import { assertTestClaimPrivateDirectory, readTestClaimFile } from './testClaimFilesystem.ts';
36
43
  import { getTestAppName } from './testIsolation.ts';
37
44
 
45
+ function setUnsafePermissions(file: string, directory = false) {
46
+ if (process.platform === 'win32') {
47
+ // icacls receives exact argv: no CMD/PowerShell string interpolation.
48
+ execFileSync('icacls.exe', [file, '/grant', '*S-1-1-0:(R)'], {
49
+ windowsHide: true,
50
+ stdio: 'pipe',
51
+ });
52
+ } else {
53
+ chmodSync(file, directory ? 0o755 : 0o644);
54
+ }
55
+ }
56
+
38
57
  const runId = '20261013010203-a1b2c3d4';
39
58
  const owner = { pid: process.pid, startTime: 'test-process-start', host: hostname() };
40
59
  function fixture() {
@@ -79,7 +98,7 @@ function appFixture(projectPath: string) {
79
98
  },
80
99
  };
81
100
  }
82
- function transportEnv(env: Record<string, string | undefined>) {
101
+ function transportEnv(env: TestClaimEnv): TestClaimEnv {
83
102
  return { ...env, META_MODE: 'test', VONA_TEST_RUN_ID: runId };
84
103
  }
85
104
 
@@ -90,8 +109,13 @@ test('exclusive central claim has canonical immutable UTC creation time and priv
90
109
  assert.equal(claimPath, path.join(f.projectPath, '.vona-test-claim', `${runId}.json`));
91
110
  assert.equal(manifest.createdAt, '2026-10-13T01:02:03.000Z');
92
111
  assert.ok(JSON.parse('{"createdAt":"2026-10-13T01:02:03.000Z"}').createdAt instanceof Date);
93
- assert.equal(lstatSync(claimPath).mode & 0o777, 0o600);
94
- assert.equal(lstatSync(path.dirname(claimPath)).mode & 0o777, 0o700);
112
+ if (process.platform === 'win32') {
113
+ assert.equal(await assertTestClaimPrivateDirectory(path.dirname(claimPath), false), true);
114
+ assert.equal(typeof (await readTestClaimFile(claimPath)), 'string');
115
+ } else {
116
+ assert.equal(lstatSync(claimPath).mode & 0o777, 0o600);
117
+ assert.equal(lstatSync(path.dirname(claimPath)).mode & 0o777, 0o700);
118
+ }
95
119
  await assert.rejects(createTestClaim(f.projectPath, runId, 'unit', owner), { code: 'EEXIST' });
96
120
  const contractClaim = await createTestClaim(
97
121
  f.projectPath,
@@ -121,14 +145,19 @@ test('claim creation rejects symlink paths and permissive shared directories', a
121
145
  try {
122
146
  const directory = getTestClaimPaths(f.projectPath, runId).claimDirectory;
123
147
  mkdirSync(path.join(f.root, 'outside'));
124
- symlinkSync(path.join(f.root, 'outside'), directory);
148
+ symlinkSync(
149
+ path.join(f.root, 'outside'),
150
+ directory,
151
+ process.platform === 'win32' ? 'junction' : 'dir',
152
+ );
125
153
  await assert.rejects(createTestClaim(f.projectPath, runId, 'unit', owner), /Unsafe/);
126
154
  rmSync(directory, { recursive: true, force: true });
127
155
  mkdirSync(directory, { mode: 0o755 });
156
+ setUnsafePermissions(directory, true);
128
157
  await assert.rejects(createTestClaim(f.projectPath, runId, 'unit', owner), /Unsafe/);
129
158
  rmSync(directory, { recursive: true, force: true });
130
159
  const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
131
- chmodSync(claimPath, 0o644);
160
+ setUnsafePermissions(claimPath);
132
161
  await assert.rejects(loadTestClaim(claimPath), /Unsafe/);
133
162
  } finally {
134
163
  f.dispose();
@@ -211,12 +240,17 @@ test('normalization whitelists endpoint identity and exact physical Redis prefix
211
240
  tls: false,
212
241
  db: 0,
213
242
  });
214
- assert.deepEqual(normalizeTestClaimRedisEndpoint({ path: '/run/redis.sock', db: 2 }), {
243
+ const socketPath = path.resolve('/run/redis.sock');
244
+ assert.deepEqual(normalizeTestClaimRedisEndpoint({ path: socketPath, db: 2 }), {
215
245
  transport: 'socket',
216
- socketPath: '/run/redis.sock',
246
+ socketPath,
217
247
  db: 2,
218
248
  tls: false,
219
249
  });
250
+ assert.throws(() => normalizeTestClaimRedisEndpoint({ path: 'relative/redis.sock' }), /socket/);
251
+ if (process.platform === 'win32') {
252
+ assert.throws(() => normalizeTestClaimRedisEndpoint({ path: '/run/redis.sock' }), /socket/);
253
+ }
220
254
  });
221
255
 
222
256
  test('normalization rejects unresolved or custom targets and captures pg socket port', () => {
@@ -229,24 +263,37 @@ test('normalization rejects unresolved or custom targets and captures pg socket
229
263
  ),
230
264
  /explicit/,
231
265
  );
266
+ const socketPath = path.resolve('/run/postgresql');
267
+ const socketEndpoint = {
268
+ dialect: 'pg',
269
+ transport: 'socket',
270
+ socketPath,
271
+ port: 5433,
272
+ tls: false,
273
+ adminDatabase: 'postgres',
274
+ };
232
275
  assert.deepEqual(
233
276
  normalizeTestClaimDatabaseEndpoint(
234
- { client: 'pg', connection: { host: '/run/postgresql', port: 5433 } },
277
+ { client: 'pg', connection: { socketPath, port: 5433 } },
235
278
  '/vona',
236
279
  runId,
237
280
  ),
238
- {
239
- dialect: 'pg',
240
- transport: 'socket',
241
- socketPath: '/run/postgresql',
242
- port: 5433,
243
- tls: false,
244
- adminDatabase: 'postgres',
245
- },
281
+ socketEndpoint,
246
282
  );
283
+ const unixHost = { client: 'pg', connection: { host: '/run/postgresql', port: 5433 } };
284
+ if (process.platform === 'win32') {
285
+ assert.throws(() => normalizeTestClaimDatabaseEndpoint(unixHost, '/vona', runId), /socket/);
286
+ } else {
287
+ assert.deepEqual(normalizeTestClaimDatabaseEndpoint(unixHost, '/vona', runId), socketEndpoint);
288
+ }
289
+ for (const connection of [{ socketPath }, { host: '/run/postgresql' }]) {
290
+ assert.throws(() =>
291
+ normalizeTestClaimDatabaseEndpoint({ client: 'pg', connection }, '/vona', runId),
292
+ );
293
+ }
247
294
  assert.throws(() =>
248
295
  normalizeTestClaimDatabaseEndpoint(
249
- { client: 'pg', connection: { host: '/run/postgresql' } },
296
+ { client: 'pg', connection: { socketPath: 'relative/postgresql', port: 5433 } },
250
297
  '/vona',
251
298
  runId,
252
299
  ),
@@ -376,6 +423,240 @@ test('database confirmations and process registration cannot precede allocation
376
423
  }
377
424
  });
378
425
 
426
+ test('Windows process claim identity is explicit, write-ahead and immutable without fabricated pgid', async () => {
427
+ const f = fixture();
428
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
429
+ try {
430
+ const { claimPath, manifest } = await createTestClaim(f.projectPath, runId, 'unit', owner);
431
+ const pending = {
432
+ id: 'windows-guardian',
433
+ role: 'guardian',
434
+ stage: 'pending' as const,
435
+ containment: 'windows-job' as const,
436
+ jobId: '12345678-1234-1234-1234-123456789abc',
437
+ };
438
+ validateTestClaim({ ...manifest, processes: [pending] });
439
+ for (const invalid of [
440
+ { ...pending, pgid: 42 },
441
+ { ...pending, containment: undefined },
442
+ { ...pending, role: 'browser' },
443
+ { ...pending, jobId: 'not-a-job' },
444
+ { ...pending, stage: 'registered', pid: 42, startTime: 'posix-start', host: hostname() },
445
+ { ...pending, stage: 'registered', pid: 42, startTime: '133000000000000000' },
446
+ ])
447
+ assert.throws(() => validateTestClaim({ ...manifest, processes: [invalid] }));
448
+ broker = await startTestClaimBroker(claimPath);
449
+ await broker.update({ type: 'process', process: pending });
450
+ await assert.rejects(
451
+ broker.update({ type: 'process', process: { ...pending, stage: 'drained' } }),
452
+ /Pending/,
453
+ );
454
+ const registered = {
455
+ ...pending,
456
+ stage: 'registered' as const,
457
+ pid: 42,
458
+ startTime: '133000000000000000',
459
+ host: hostname(),
460
+ };
461
+ await broker.update({ type: 'process', process: registered });
462
+ await assert.rejects(
463
+ broker.update({
464
+ type: 'process',
465
+ process: { ...registered, jobId: '12345678-1234-1234-1234-123456789abd' },
466
+ }),
467
+ /identity/,
468
+ );
469
+ await assert.rejects(
470
+ broker.update({
471
+ type: 'process',
472
+ process: { ...registered, containment: undefined, jobId: undefined, pgid: 42 },
473
+ }),
474
+ /identity/,
475
+ );
476
+ await broker.update({ type: 'process', process: { ...registered, stage: 'drained' } });
477
+ const processClaim = (await loadTestClaim(claimPath)).processes[0];
478
+ assert.equal(processClaim.stage, 'drained');
479
+ assert.equal(processClaim.pgid, undefined);
480
+ } finally {
481
+ await broker?.close();
482
+ f.dispose();
483
+ }
484
+ });
485
+
486
+ test('Windows containment query is authenticated, transient, E2E-only and requires the exact live registered verifier', async () => {
487
+ for (const kind of ['e2e', 'unit', 'cov', 'contract'] as const) {
488
+ const f = fixture();
489
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
490
+ try {
491
+ const { claimPath } = await createTestClaim(f.projectPath, runId, kind, owner);
492
+ broker = await startTestClaimBroker(claimPath);
493
+ const pending = {
494
+ id: 'windows-e2e-guardian',
495
+ role: 'guardian',
496
+ stage: 'pending' as const,
497
+ containment: 'windows-job' as const,
498
+ jobId: '12345678-1234-1234-1234-123456789abc',
499
+ };
500
+ const registered = {
501
+ ...pending,
502
+ stage: 'registered' as const,
503
+ pid: 42,
504
+ startTime: '133000000000000000',
505
+ host: hostname(),
506
+ };
507
+ const identity = { pid: 123, startTime: '133000000000000001', host: hostname() };
508
+ const env: TestClaimEnv = {
509
+ ...transportEnv(broker.env),
510
+ [TEST_CLAIM_WINDOWS_GUARDIAN_ENV]: pending.id,
511
+ };
512
+ const denied = (environment = env, processIdentity = identity) =>
513
+ assert.rejects(
514
+ assertTestClaimWindowsJob(environment, processIdentity),
515
+ /containment verification failed/,
516
+ );
517
+ await denied();
518
+ await assert.rejects(
519
+ registerTestClaimWindowsJobVerifier(env, registered, async () => true),
520
+ /durable/,
521
+ );
522
+ await broker.update({ type: 'process', process: pending });
523
+ await denied();
524
+ await broker.update({ type: 'process', process: registered });
525
+ await denied(); // A durable record and copied locator are not a live capability.
526
+ let calls = 0;
527
+ const remove = await registerTestClaimWindowsJobVerifier(env, registered, async target => {
528
+ calls++;
529
+ return target.pid === identity.pid && target.startTime === identity.startTime;
530
+ });
531
+ const before = readFileSync(claimPath, 'utf8');
532
+ if (kind === 'e2e') {
533
+ await assertTestClaimWindowsJob(env, identity);
534
+ assert.equal(calls, 1);
535
+ await denied({ ...env, VONA_TEST_CLAIM_TOKEN: '0'.repeat(64) });
536
+ await denied({ ...env, VONA_TEST_RUN_ID: '20261013010203-deadbeef' });
537
+ await denied({ ...env, [TEST_CLAIM_WINDOWS_GUARDIAN_ENV]: 'copied-locator' });
538
+ await denied(env, { ...identity, host: 'other-host' });
539
+ assert.equal(calls, 1);
540
+ await denied(env, { ...identity, pid: 124 });
541
+ await denied(env, { ...identity, startTime: '133000000000000002' });
542
+ await denied(env, { ...identity, startTime: 'not-FILETIME' });
543
+ assert.equal(calls, 3);
544
+ } else {
545
+ await denied();
546
+ assert.equal(calls, 0);
547
+ }
548
+ assert.equal(readFileSync(claimPath, 'utf8'), before);
549
+ remove();
550
+ await denied();
551
+ const removeFailing = await registerTestClaimWindowsJobVerifier(env, registered, async () => {
552
+ throw new Error('helper lost');
553
+ });
554
+ await denied();
555
+ removeFailing();
556
+ const removeDrained = await registerTestClaimWindowsJobVerifier(
557
+ env,
558
+ registered,
559
+ async () => true,
560
+ );
561
+ await broker.update({ type: 'process', process: { ...registered, stage: 'drained' } });
562
+ await denied();
563
+ removeDrained();
564
+ const manifest = await loadTestClaim(claimPath);
565
+ assert.equal(manifest.version, 1);
566
+ assert.equal(manifest.processes.length, 1);
567
+ assert.equal(manifest.processes[0].role, 'guardian');
568
+ } finally {
569
+ await broker?.close();
570
+ f.dispose();
571
+ }
572
+ }
573
+ });
574
+
575
+ test('containment response cannot outlive verifier revocation or a durable drained transition', async () => {
576
+ const f = fixture();
577
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
578
+ try {
579
+ const { claimPath } = await createTestClaim(f.projectPath, runId, 'e2e', owner);
580
+ broker = await startTestClaimBroker(claimPath);
581
+ const pending = {
582
+ id: 'guardian',
583
+ role: 'guardian',
584
+ stage: 'pending' as const,
585
+ containment: 'windows-job' as const,
586
+ jobId: '12345678-1234-1234-1234-123456789abc',
587
+ };
588
+ const registered = {
589
+ ...pending,
590
+ stage: 'registered' as const,
591
+ pid: 42,
592
+ startTime: '133000000000000000',
593
+ host: hostname(),
594
+ };
595
+ const env = { ...transportEnv(broker.env), [TEST_CLAIM_WINDOWS_GUARDIAN_ENV]: pending.id };
596
+ await broker.update({ type: 'process', process: pending });
597
+ await broker.update({ type: 'process', process: registered });
598
+ for (const revoke of ['local', 'durable']) {
599
+ let entered!: () => void;
600
+ const entry = new Promise<void>(resolve => {
601
+ entered = resolve;
602
+ });
603
+ let release!: (contained: boolean) => void;
604
+ const remove = await registerTestClaimWindowsJobVerifier(env, registered, async () => {
605
+ entered();
606
+ return await new Promise<boolean>(resolve => {
607
+ release = resolve;
608
+ });
609
+ });
610
+ const queried = assertTestClaimWindowsJob(env, {
611
+ pid: 123,
612
+ startTime: '133000000000000001',
613
+ host: hostname(),
614
+ });
615
+ const rejected = assert.rejects(queried, /containment verification failed/);
616
+ await entry;
617
+ if (revoke === 'local') remove();
618
+ else await broker.update({ type: 'process', process: { ...registered, stage: 'drained' } });
619
+ release(true);
620
+ await rejected;
621
+ remove();
622
+ }
623
+ assert.equal((await loadTestClaim(claimPath)).processes.length, 1);
624
+ } finally {
625
+ await broker?.close();
626
+ f.dispose();
627
+ }
628
+ });
629
+
630
+ test('containment cannot be registered through persisted events or a copied remote broker transport', async () => {
631
+ await assert.rejects(assertTestClaimWindowsJob({}, owner), /containment verification failed/);
632
+ const f = fixture();
633
+ let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
634
+ try {
635
+ const { claimPath } = await createTestClaim(f.projectPath, runId, 'e2e', owner);
636
+ broker = await startTestClaimBroker(claimPath);
637
+ const env = transportEnv(broker.env);
638
+ const response = await fetch(env.VONA_TEST_CLAIM_URL!, {
639
+ method: 'POST',
640
+ headers: {
641
+ 'authorization': `Bearer ${env.VONA_TEST_CLAIM_TOKEN}`,
642
+ 'content-type': 'application/json',
643
+ },
644
+ body: JSON.stringify({ type: 'containment', guardianId: 'guardian', process: owner }),
645
+ });
646
+ assert.equal(response.status, 409);
647
+ const script = `import {registerTestClaimWindowsJobVerifier} from ${JSON.stringify(new URL('./testClaim.ts', import.meta.url).href)};
648
+ try { await registerTestClaimWindowsJobVerifier(JSON.parse(process.argv[1]),{},async()=>true); process.exit(1); }
649
+ catch(error) { if(!/local claim broker/.test(error.message)) throw error; }`;
650
+ execFileSync(process.execPath, ['--input-type=module', '-e', script, JSON.stringify(env)], {
651
+ stdio: 'pipe',
652
+ });
653
+ assert.equal((await loadTestClaim(claimPath)).processes.length, 0);
654
+ } finally {
655
+ await broker?.close();
656
+ f.dispose();
657
+ }
658
+ });
659
+
379
660
  test('cleanup editor persists partial progress but may not replace claim identity', async () => {
380
661
  const f = fixture();
381
662
  try {