cabloy 5.1.230 → 5.1.232
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.
- package/.cabloy-version +1 -1
- package/.github/workflows/playwright-e2e.yml +4 -2
- package/.github/workflows/vona-test-sqlite3.yml +2 -1
- package/CHANGELOG.md +24 -0
- package/package.json +1 -1
- package/repo-docs/backend/model-guide.md +1 -0
- package/repo-docs/backend/websocket-usage-guide.md +3 -0
- package/repo-docs/frontend/controller-boundary-guide.md +1 -1
- package/repo-docs/frontend/ioc-and-beans.md +2 -1
- package/repo-docs/frontend/modules-and-suites.md +1 -0
- package/repo-docs/reference/repo-scripts.md +2 -0
- package/vona/env/.env +1 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-utils/zod-query/package.json +1 -1
- package/vona/packages-utils/zod-query/src/lib/query.ts +4 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/bean/type.ts +3 -3
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +294 -1
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +47 -9
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts +163 -39
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.cs +16 -6
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ts +9 -4
- package/vona/packages-vona/vona-core/src/lib/utils/index.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/utils/zod-enhance.ts +8 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +171 -30
- package/vona/pnpm-workspace.yaml +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/cartItem.tsx +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +144 -18
- package/vona/src/suite-vendor/a-auth/modules/auth-oauth/package.json +1 -1
- package/vona/src/suite-vendor/a-auth/modules/auth-oauth/src/bean/authProvider.oauth.ts +1 -0
- package/vona/src/suite-vendor/a-auth/modules/auth-oauth/src/types/index.ts +1 -0
- package/vona/src/suite-vendor/a-auth/package.json +2 -2
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/relationsColumns.ts +7 -5
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +2 -2
- package/zova/packages-zova/zova-core/src/bean/resource/component/type.ts +3 -3
- package/zova/packages-zova/zova-core/src/bean/type.ts +6 -6
- package/zova/pnpm-lock.yaml +16 -16
- package/zova/src/suite/cabloy-basic/modules/basic-button/src/bean/behavior.perform.tsx +5 -5
- package/zova/src/suite/cabloy-basic/modules/basic-date/src/.metadata/component/date.ts +3 -1
- package/zova/src/suite/cabloy-basic/modules/basic-date/src/.metadata/component/dateRange.ts +12 -4
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +8 -2
- package/zova/src/suite/cabloy-basic/modules/basic-select/src/.metadata/component/select.ts +9 -3
- package/zova/src/suite-vendor/a-zova/modules/a-behavior/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-behavior/src/types/behavior.ts +3 -3
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/detail/detailsActionBulk.ts +5 -3
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/detail/detailsActionRow.ts +5 -3
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/formActionRow.ts +5 -3
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +5 -3
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionRow.ts +5 -3
- package/zova/src/suite-vendor/a-zova/modules/a-zod/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +5 -5
- /package/vona/patches/{zova-core@5.1.93.patch → zova-core@5.1.94.patch} +0 -0
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.232
|
|
@@ -98,7 +98,8 @@ jobs:
|
|
|
98
98
|
if: runner.os != 'Linux'
|
|
99
99
|
run: npx playwright install chromium
|
|
100
100
|
- name: run focused managed E2E regression gate
|
|
101
|
-
|
|
101
|
+
# Bound concurrent PowerShell/Add-Type cold starts on Windows runners.
|
|
102
|
+
run: node --test ${{ runner.os == 'Windows' && '--test-concurrency=2' || '' }} zova/packages-utils/zova-vite/test/configUtils.test.ts scripts/testClaimProcesses.test.ts scripts/runIsolatedTest.test.ts scripts/generateOpenapiIsolated.test.ts scripts/testClaimCleanup.test.ts scripts/testProcessGuardianWindows.test.ts scripts/testBrowserGuardianWindows.test.ts vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts vona/packages-vona/vona-core/src/lib/framework/processWorker.test.ts vona/src/suite-vendor/a-vona/modules/a-static/test/middlewareSystem.static.test.ts vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts repo-e2e/scripts/managedTest.test.ts repo-e2e/scripts/managedTestImports.test.ts repo-e2e/scripts/e2e.test.ts repo-e2e/scripts/runE2eArgs.test.ts repo-e2e/scripts/runE2e.test.ts
|
|
102
103
|
- name: run Basic E2E baseline
|
|
103
104
|
run: npm run test:e2e -- cabloy-basic home-user-account
|
|
104
105
|
- name: upload Playwright artifacts
|
|
@@ -206,7 +207,8 @@ jobs:
|
|
|
206
207
|
if: runner.os != 'Linux'
|
|
207
208
|
run: npx playwright install chromium
|
|
208
209
|
- name: run focused managed E2E regression gate
|
|
209
|
-
|
|
210
|
+
# Bound concurrent PowerShell/Add-Type cold starts on Windows runners.
|
|
211
|
+
run: node --test ${{ runner.os == 'Windows' && '--test-concurrency=2' || '' }} zova/packages-utils/zova-vite/test/configUtils.test.ts scripts/testClaimProcesses.test.ts scripts/runIsolatedTest.test.ts scripts/generateOpenapiIsolated.test.ts scripts/testClaimCleanup.test.ts scripts/testProcessGuardianWindows.test.ts scripts/testBrowserGuardianWindows.test.ts vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts vona/packages-vona/vona-core/src/lib/framework/processWorker.test.ts vona/src/suite-vendor/a-vona/modules/a-static/test/middlewareSystem.static.test.ts vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts repo-e2e/scripts/managedTest.test.ts repo-e2e/scripts/managedTestImports.test.ts repo-e2e/scripts/e2e.test.ts repo-e2e/scripts/runE2eArgs.test.ts repo-e2e/scripts/runE2e.test.ts
|
|
210
212
|
- name: run Commerce cart model regression
|
|
211
213
|
run: node --test zova/src/suite/a-commerce/modules/commerce-trade/test/lib/cartModel.test.ts
|
|
212
214
|
- name: build Commerce Zova artifacts
|
|
@@ -93,7 +93,8 @@ jobs:
|
|
|
93
93
|
- name: init
|
|
94
94
|
run: npm run init
|
|
95
95
|
- name: run isolated test infrastructure gate
|
|
96
|
-
|
|
96
|
+
# Bound concurrent PowerShell/Add-Type cold starts on Windows runners.
|
|
97
|
+
run: node --test ${{ runner.os == 'Windows' && '--test-concurrency=2' || '' }} scripts/testClaimProcesses.test.ts scripts/runIsolatedTest.test.ts scripts/testClaimCleanup.test.ts scripts/testProcessGuardianWindows.test.ts vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts vona/packages-vona/vona-core/src/lib/framework/bootstrap.test.ts vona/packages-vona/vona-core/src/lib/framework/useApp.test.ts vona/packages-cli/cli-set-api/test/binBuildEntries.test.ts vona/packages-cli/cli-set-api/test/binTestPatterns.test.ts
|
|
97
98
|
- name: run A-Pay focused gate
|
|
98
99
|
run: npm run test -- test-pay/test/paymentSession.test.ts test-pay/test/payMock.test.ts test-pay/test/paypalProvider.test.ts test-pay/test/paypalLifecycle.test.ts test-pay/test/stripeProvider.test.ts test-pay/test/stripeLifecycle.test.ts test-pay/test/outbox.test.ts test-pay/test/webhook.test.ts
|
|
99
100
|
working-directory: vona
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.232
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- Refresh the Zova lockfile after preparing the compensation rerun.
|
|
8
|
+
- Update the Vona zova-core patch for v5.1.94.
|
|
9
|
+
|
|
10
|
+
## 5.1.231
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- Add commerce functionality.
|
|
15
|
+
- Add formatting support.
|
|
16
|
+
- Apply feature updates.
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
- Fix Windows tests.
|
|
21
|
+
- Patch Zod integration.
|
|
22
|
+
|
|
23
|
+
### Improvements
|
|
24
|
+
|
|
25
|
+
- Update the controller implementation.
|
|
26
|
+
|
|
3
27
|
## 5.1.230
|
|
4
28
|
|
|
5
29
|
### Features
|
package/package.json
CHANGED
|
@@ -63,6 +63,7 @@ Representative structure:
|
|
|
63
63
|
|
|
64
64
|
```typescript
|
|
65
65
|
import type { IDecoratorSocketNamespaceOptions } from 'vona-module-a-socket';
|
|
66
|
+
|
|
66
67
|
import { BeanSocketNamespaceBase, SocketNamespace } from 'vona-module-a-socket';
|
|
67
68
|
|
|
68
69
|
declare module 'vona-module-a-socket' {
|
|
@@ -242,6 +243,7 @@ import type {
|
|
|
242
243
|
ISocketConnectionExecute,
|
|
243
244
|
} from 'vona-module-a-socket';
|
|
244
245
|
import type { WebSocket } from 'ws';
|
|
246
|
+
|
|
245
247
|
import { BeanBase } from 'vona';
|
|
246
248
|
import { SocketConnection } from 'vona-module-a-socket';
|
|
247
249
|
|
|
@@ -284,6 +286,7 @@ Representative generated shape:
|
|
|
284
286
|
import type { Next } from 'vona';
|
|
285
287
|
import type { IDecoratorSocketPacketOptions, ISocketPacketExecute } from 'vona-module-a-socket';
|
|
286
288
|
import type { WebSocket } from 'ws';
|
|
289
|
+
|
|
287
290
|
import { BeanBase } from 'vona';
|
|
288
291
|
import { SocketPacket } from 'vona-module-a-socket';
|
|
289
292
|
|
|
@@ -88,8 +88,8 @@ static $componentOptions: IComponentOptions = {
|
|
|
88
88
|
Application configuration supplies the shared renderer contract:
|
|
89
89
|
|
|
90
90
|
```ts
|
|
91
|
-
import type { ZovaContext } from 'zova';
|
|
92
91
|
import type { VNodeChild } from 'vue';
|
|
92
|
+
import type { ZovaContext } from 'zova';
|
|
93
93
|
|
|
94
94
|
config.boundary = {
|
|
95
95
|
loading: {
|
|
@@ -138,9 +138,10 @@ In practice, Zova can still preserve the ergonomic class-based development exper
|
|
|
138
138
|
Use a getter when injection arguments must be prepared dynamically. The getter must explicitly identify the target bean with `beanFullName`; `usePrepareArg(...)` supplies initialization arguments only and does not resolve the target bean.
|
|
139
139
|
|
|
140
140
|
```typescript
|
|
141
|
-
import { Use, usePrepareArg } from 'zova';
|
|
142
141
|
import type { ServiceSsrLayout } from 'zova-module-home-base';
|
|
143
142
|
|
|
143
|
+
import { Use, usePrepareArg } from 'zova';
|
|
144
|
+
|
|
144
145
|
class ControllerLayout {
|
|
145
146
|
@Use({ beanFullName: 'home-base.service.ssrLayout' })
|
|
146
147
|
get $$serviceSsrLayout(): ServiceSsrLayout {
|
|
@@ -70,6 +70,8 @@ Use the root commands to select the workspace scope:
|
|
|
70
70
|
|
|
71
71
|
Explicit native and legacy checks use `--noEmit --incremental false`: Zova selects `tsconfig.vue-tsc.json`, and Vona selects `tsconfig.json`. Vona's daily config has no `include` or project `references`; default source discovery is filtered by `exclude`, following Zova's approach. Backend/application/module source and module tests are checked together in one program. CLI tooling, package-directory roots, dependencies, distribution output, and generated runtime/REST directories are excluded from initial discovery. Exclusions are not import barriers: required framework/package source and hidden module metadata still participate through imports. Unreferenced hidden helpers and generated `.vona` bootstraps are not independent daily-check roots.
|
|
72
72
|
|
|
73
|
+
Choose the narrowest **supported, semantically appropriate checking project**, not simply the nearest module `tsconfig.json`. Prefer the active repository's root/workspace source-check entrypoints; a module publication/declaration configuration is not automatically a standalone application check. Before using a custom local invocation or interpreting configuration-related diagnostics, inspect the selected configuration's `extends` chain and effective compiler options, and confirm whether dependencies resolve to source or declarations. Dependency source imported into the consuming program is checked under that program's compiler options; the dependency package's own `tsconfig.json` is not automatically applied. Distinguish an unsuitable invocation/configuration from a business-source type error, and do not change import suffixes or relax compiler options merely to make that invocation pass. Keep daily application checks separate from publication/declaration validation, and verify the actual entrypoints in Cabloy Start rather than assuming all Basic aliases apply.
|
|
74
|
+
|
|
73
75
|
Root `npm run tsc` runs `tsc:zova`, then `tsc:vona`; these are the only root `tsc` entrypoints. Compiler selection remains in the nested workspaces: use `pnpm --dir zova run tsc:legacy` or `pnpm --dir vona run tsc:legacy` for comparison or fallback, and the corresponding workspace `tsc:native` scripts for explicit native checks. The former `tsc:force`, `tsc:vona:native:force`, and nested Vona `tsc:native:force` aliases have been removed: these source checks neither run a project build nor emit declarations/build-info files. Each module's existing publication scripts remain responsible for its declaration output; there is no separate root declaration-validation workflow.
|
|
74
76
|
|
|
75
77
|
In both nested workspaces, `npm run tsc` delegates to `npm run tsc:native`. Native and legacy checks first run `npm run prerun`, then invoke the selected compiler with incremental caching disabled. Root workspace aliases delegate to these scripts too, so dependency and generated metadata preparation is not bypassed. Each workspace's `prerun` runs only its `:tools:deps` command; it does not call `tsc`, avoiding recursive script execution. Preparation may refresh generated metadata or dependency manifests and install changed dependencies, while the compiler step itself emits no declarations or build-info files.
|
package/vona/env/.env
CHANGED
|
@@ -10,13 +10,16 @@ interface IParsePayload {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
let ZodMetadata: any;
|
|
13
|
-
|
|
13
|
+
let shouldAdapt: (() => boolean) | undefined;
|
|
14
|
+
export function setParseAdapter(zodMetadata: any, policy?: () => boolean) {
|
|
14
15
|
ZodMetadata = zodMetadata;
|
|
16
|
+
shouldAdapt = policy;
|
|
15
17
|
(z.core as any).setParseAdapter(__parseAdapter);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function __parseAdapter(inst: z.ZodType, parse) {
|
|
19
21
|
return (payload: IParsePayload, _) => {
|
|
22
|
+
if (shouldAdapt?.() === false) return parse(payload, _);
|
|
20
23
|
const type = inst.type;
|
|
21
24
|
switch (type) {
|
|
22
25
|
case 'string':
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.117",
|
|
4
4
|
"gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
|
|
5
5
|
"description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
|
|
6
6
|
"keywords": [
|
|
@@ -7,9 +7,9 @@ import type { TypeErrorsInternal } from './resource/error/errorInternal.ts';
|
|
|
7
7
|
export interface IBeanRecordGlobal {}
|
|
8
8
|
export interface IBeanRecordGeneral {}
|
|
9
9
|
export type TypeBeanRecordGeneralSelector<SCENE extends keyof IBeanSceneRecord> = {
|
|
10
|
-
[
|
|
11
|
-
? K
|
|
12
|
-
|
|
10
|
+
[
|
|
11
|
+
K in keyof IBeanRecordGeneral as K extends `${string}.${SCENE}.${string}` ? K : never
|
|
12
|
+
]: IBeanRecordGeneral[K];
|
|
13
13
|
};
|
|
14
14
|
export type TypeBeanRecordGeneralSelectorKeys<SCENE extends keyof IBeanSceneRecord> =
|
|
15
15
|
keyof TypeBeanRecordGeneralSelector<SCENE>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import '@cabloy/json5';
|
|
2
|
+
import type { Socket } from 'node:net';
|
|
3
|
+
|
|
2
4
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
4
6
|
import {
|
|
5
7
|
chmodSync,
|
|
6
8
|
lstatSync,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
rmSync,
|
|
12
14
|
symlinkSync,
|
|
13
15
|
} from 'node:fs';
|
|
16
|
+
import { createServer } from 'node:http';
|
|
14
17
|
import { hostname, tmpdir } from 'node:os';
|
|
15
18
|
import path from 'node:path';
|
|
16
19
|
import { test } from 'node:test';
|
|
@@ -363,6 +366,296 @@ test('broker serializes simultaneous updates and acknowledges durable intent bef
|
|
|
363
366
|
}
|
|
364
367
|
});
|
|
365
368
|
|
|
369
|
+
test('broker reports safe failure phases without acknowledging invalid or unreadable claims', async () => {
|
|
370
|
+
const f = fixture();
|
|
371
|
+
let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
|
|
372
|
+
try {
|
|
373
|
+
const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
|
|
374
|
+
broker = await startTestClaimBroker(claimPath);
|
|
375
|
+
const env = transportEnv(broker.env);
|
|
376
|
+
const before = await readTestClaimFile(claimPath);
|
|
377
|
+
await assert.rejects(
|
|
378
|
+
sendTestClaimEvent(env, { type: 'status', status: 'SECRET_INVALID_STATUS' as any }),
|
|
379
|
+
{ message: 'Managed test allocation registration failed (HTTP 409, validate)' },
|
|
380
|
+
);
|
|
381
|
+
assert.equal(await readTestClaimFile(claimPath), before);
|
|
382
|
+
await assert.rejects(
|
|
383
|
+
sendTestClaimEvent(
|
|
384
|
+
{ ...env, VONA_TEST_CLAIM_TOKEN: '0'.repeat(64) },
|
|
385
|
+
{ type: 'status', status: 'running' },
|
|
386
|
+
),
|
|
387
|
+
{ message: 'Managed test allocation registration failed (HTTP 403)' },
|
|
388
|
+
);
|
|
389
|
+
rmSync(claimPath);
|
|
390
|
+
await assert.rejects(sendTestClaimEvent(env, { type: 'status', status: 'running' }), {
|
|
391
|
+
message:
|
|
392
|
+
/^Managed test allocation registration failed \(HTTP 409, read, ENOENT(?:, native [23])?\)$/,
|
|
393
|
+
});
|
|
394
|
+
} finally {
|
|
395
|
+
await broker?.close();
|
|
396
|
+
f.dispose();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test(
|
|
401
|
+
'registration uses fresh connections after a blocked startup without replaying events',
|
|
402
|
+
{ timeout: 45000 },
|
|
403
|
+
async () => {
|
|
404
|
+
const requests: {
|
|
405
|
+
connection: string | undefined;
|
|
406
|
+
authorization: string | undefined;
|
|
407
|
+
event: unknown;
|
|
408
|
+
socket: Socket;
|
|
409
|
+
}[] = [];
|
|
410
|
+
const server = createServer(async (req, res) => {
|
|
411
|
+
let body = '';
|
|
412
|
+
for await (const chunk of req) body += chunk.toString();
|
|
413
|
+
requests.push({
|
|
414
|
+
connection: req.headers.connection,
|
|
415
|
+
authorization: req.headers.authorization,
|
|
416
|
+
event: JSON.parse(body),
|
|
417
|
+
socket: req.socket,
|
|
418
|
+
});
|
|
419
|
+
res.writeHead(204).end();
|
|
420
|
+
});
|
|
421
|
+
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
422
|
+
let child: ReturnType<typeof spawn> | undefined;
|
|
423
|
+
let exited: Promise<number | null> | undefined;
|
|
424
|
+
let spawnError: Error | undefined;
|
|
425
|
+
try {
|
|
426
|
+
const address = server.address();
|
|
427
|
+
assert.ok(address && typeof address !== 'string');
|
|
428
|
+
const env = transportEnv({
|
|
429
|
+
VONA_TEST_CLAIM_URL: `http://127.0.0.1:${address.port}/event`,
|
|
430
|
+
VONA_TEST_CLAIM_TOKEN: 'a'.repeat(64),
|
|
431
|
+
});
|
|
432
|
+
const events = [
|
|
433
|
+
{ type: 'status', status: 'running' },
|
|
434
|
+
{ type: 'status', status: 'failed' },
|
|
435
|
+
];
|
|
436
|
+
const script = `
|
|
437
|
+
import { setImmediate } from 'node:timers/promises';
|
|
438
|
+
import { sendTestClaimEvent } from ${JSON.stringify(new URL('./testClaim.ts', import.meta.url).href)};
|
|
439
|
+
const [env, events] = JSON.parse(process.argv[1]);
|
|
440
|
+
await sendTestClaimEvent(env, events[0]);
|
|
441
|
+
await setImmediate();
|
|
442
|
+
// Let the parent broker expire an idle connection while startup blocks this event loop.
|
|
443
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 6500);
|
|
444
|
+
await sendTestClaimEvent(env, events[1]);
|
|
445
|
+
`;
|
|
446
|
+
child = spawn(
|
|
447
|
+
process.execPath,
|
|
448
|
+
['--input-type=module', '-e', script, JSON.stringify([env, events])],
|
|
449
|
+
{
|
|
450
|
+
stdio: 'ignore',
|
|
451
|
+
windowsHide: true,
|
|
452
|
+
timeout: 30000,
|
|
453
|
+
killSignal: 'SIGKILL',
|
|
454
|
+
},
|
|
455
|
+
);
|
|
456
|
+
child.once('error', error => {
|
|
457
|
+
spawnError = error;
|
|
458
|
+
});
|
|
459
|
+
exited = new Promise(resolve => child!.once('close', code => resolve(code)));
|
|
460
|
+
assert.equal(await exited, 0, 'claim client child failed');
|
|
461
|
+
assert.equal(spawnError, undefined);
|
|
462
|
+
assert.deepEqual(
|
|
463
|
+
requests.map(request => request.event),
|
|
464
|
+
events,
|
|
465
|
+
);
|
|
466
|
+
assert.deepEqual(
|
|
467
|
+
requests.map(request => request.connection),
|
|
468
|
+
['close', 'close'],
|
|
469
|
+
);
|
|
470
|
+
assert.ok(
|
|
471
|
+
requests.every(request => request.authorization === `Bearer ${env.VONA_TEST_CLAIM_TOKEN}`),
|
|
472
|
+
);
|
|
473
|
+
assert.notEqual(requests[0].socket, requests[1].socket);
|
|
474
|
+
} finally {
|
|
475
|
+
if (child && child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
476
|
+
await exited;
|
|
477
|
+
server.closeAllConnections();
|
|
478
|
+
await new Promise<void>((resolve, reject) =>
|
|
479
|
+
server.close(error => (error ? reject(error) : resolve())),
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
test('containment requests do not reuse broker connections', async () => {
|
|
486
|
+
const requests: { connection: string | undefined; url: string | undefined; socket: Socket }[] =
|
|
487
|
+
[];
|
|
488
|
+
const server = createServer((req, res) => {
|
|
489
|
+
req.resume();
|
|
490
|
+
req.on('end', () => {
|
|
491
|
+
requests.push({ connection: req.headers.connection, url: req.url, socket: req.socket });
|
|
492
|
+
res.writeHead(204).end();
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
496
|
+
try {
|
|
497
|
+
const address = server.address();
|
|
498
|
+
assert.ok(address && typeof address !== 'string');
|
|
499
|
+
const env = {
|
|
500
|
+
...transportEnv({
|
|
501
|
+
VONA_TEST_CLAIM_URL: `http://127.0.0.1:${address.port}/event`,
|
|
502
|
+
VONA_TEST_CLAIM_TOKEN: 'a'.repeat(64),
|
|
503
|
+
}),
|
|
504
|
+
[TEST_CLAIM_WINDOWS_GUARDIAN_ENV]: 'windows-e2e-guardian',
|
|
505
|
+
};
|
|
506
|
+
const identity = { pid: 123, startTime: '133000000000000001', host: hostname() };
|
|
507
|
+
await assertTestClaimWindowsJob(env, identity);
|
|
508
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
509
|
+
await assertTestClaimWindowsJob(env, identity);
|
|
510
|
+
assert.deepEqual(
|
|
511
|
+
requests.map(request => request.url),
|
|
512
|
+
['/containment', '/containment'],
|
|
513
|
+
);
|
|
514
|
+
assert.deepEqual(
|
|
515
|
+
requests.map(request => request.connection),
|
|
516
|
+
['close', 'close'],
|
|
517
|
+
);
|
|
518
|
+
assert.notEqual(requests[0].socket, requests[1].socket);
|
|
519
|
+
} finally {
|
|
520
|
+
server.closeAllConnections();
|
|
521
|
+
await new Promise<void>((resolve, reject) =>
|
|
522
|
+
server.close(error => (error ? reject(error) : resolve())),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test('registration waits beyond ten seconds for acknowledgement without replaying the event', async () => {
|
|
528
|
+
const received = Promise.withResolvers<() => void>();
|
|
529
|
+
let requests = 0;
|
|
530
|
+
const server = createServer((req, res) => {
|
|
531
|
+
requests++;
|
|
532
|
+
req.resume();
|
|
533
|
+
req.on('end', () => received.resolve(() => res.writeHead(204).end()));
|
|
534
|
+
});
|
|
535
|
+
let registration: Promise<void> | undefined;
|
|
536
|
+
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
537
|
+
try {
|
|
538
|
+
const address = server.address();
|
|
539
|
+
assert.ok(address && typeof address !== 'string');
|
|
540
|
+
const env = transportEnv({
|
|
541
|
+
VONA_TEST_CLAIM_URL: `http://127.0.0.1:${address.port}/event`,
|
|
542
|
+
VONA_TEST_CLAIM_TOKEN: 'a'.repeat(64),
|
|
543
|
+
});
|
|
544
|
+
let settled = false;
|
|
545
|
+
registration = sendTestClaimEvent(env, { type: 'status', status: 'running' });
|
|
546
|
+
void registration.then(
|
|
547
|
+
() => {
|
|
548
|
+
settled = true;
|
|
549
|
+
},
|
|
550
|
+
() => {
|
|
551
|
+
settled = true;
|
|
552
|
+
},
|
|
553
|
+
);
|
|
554
|
+
const acknowledge = await received.promise;
|
|
555
|
+
await new Promise(resolve => setTimeout(resolve, 11000));
|
|
556
|
+
assert.equal(settled, false);
|
|
557
|
+
assert.equal(requests, 1);
|
|
558
|
+
acknowledge();
|
|
559
|
+
await registration;
|
|
560
|
+
assert.equal(requests, 1);
|
|
561
|
+
} finally {
|
|
562
|
+
server.closeAllConnections();
|
|
563
|
+
await registration?.catch(() => {});
|
|
564
|
+
await new Promise<void>((resolve, reject) =>
|
|
565
|
+
server.close(error => (error ? reject(error) : resolve())),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test(
|
|
571
|
+
'registration has a finite acknowledgement deadline and fails closed without replay',
|
|
572
|
+
{ concurrency: false },
|
|
573
|
+
async t => {
|
|
574
|
+
const controller = new AbortController();
|
|
575
|
+
const timeout = AbortSignal.timeout.bind(AbortSignal);
|
|
576
|
+
let requests = 0;
|
|
577
|
+
const server = createServer(req => {
|
|
578
|
+
requests++;
|
|
579
|
+
req.resume();
|
|
580
|
+
req.on('end', () => {
|
|
581
|
+
timeout(20).addEventListener('abort', () => controller.abort(), { once: true });
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
const deadlines: number[] = [];
|
|
585
|
+
let registration: Promise<void> | undefined;
|
|
586
|
+
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
587
|
+
const mock = t.mock.method(AbortSignal, 'timeout', (milliseconds: number) => {
|
|
588
|
+
deadlines.push(milliseconds);
|
|
589
|
+
return controller.signal;
|
|
590
|
+
});
|
|
591
|
+
try {
|
|
592
|
+
const address = server.address();
|
|
593
|
+
assert.ok(address && typeof address !== 'string');
|
|
594
|
+
const env = transportEnv({
|
|
595
|
+
VONA_TEST_CLAIM_URL: `http://127.0.0.1:${address.port}/event`,
|
|
596
|
+
VONA_TEST_CLAIM_TOKEN: 'a'.repeat(64),
|
|
597
|
+
});
|
|
598
|
+
registration = sendTestClaimEvent(env, { type: 'status', status: 'running' });
|
|
599
|
+
await assert.rejects(registration, {
|
|
600
|
+
message: 'Managed test allocation registration failed (transport)',
|
|
601
|
+
});
|
|
602
|
+
assert.deepEqual(deadlines, [120000]);
|
|
603
|
+
assert.equal(requests, 1);
|
|
604
|
+
} finally {
|
|
605
|
+
mock.mock.restore();
|
|
606
|
+
server.closeAllConnections();
|
|
607
|
+
await registration?.catch(() => {});
|
|
608
|
+
await new Promise<void>((resolve, reject) =>
|
|
609
|
+
server.close(error => (error ? reject(error) : resolve())),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
test('registration diagnostics accept only categorical headers and never response bodies', async () => {
|
|
616
|
+
let headers: Record<string, string> = {
|
|
617
|
+
'x-vona-claim-phase': 'write',
|
|
618
|
+
'x-vona-claim-code': 'EBUSY',
|
|
619
|
+
'x-vona-claim-native-code': '32',
|
|
620
|
+
};
|
|
621
|
+
const server = createServer((req, res) => {
|
|
622
|
+
req.resume();
|
|
623
|
+
res.writeHead(409, headers).end('SECRET_RESPONSE_BODY');
|
|
624
|
+
});
|
|
625
|
+
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
626
|
+
try {
|
|
627
|
+
const address = server.address();
|
|
628
|
+
assert.ok(address && typeof address !== 'string');
|
|
629
|
+
const env = transportEnv({
|
|
630
|
+
VONA_TEST_CLAIM_URL: `http://127.0.0.1:${address.port}/event`,
|
|
631
|
+
VONA_TEST_CLAIM_TOKEN: 'a'.repeat(64),
|
|
632
|
+
});
|
|
633
|
+
await assert.rejects(sendTestClaimEvent(env, { type: 'status', status: 'running' }), {
|
|
634
|
+
message: 'Managed test allocation registration failed (HTTP 409, write, EBUSY, native 32)',
|
|
635
|
+
});
|
|
636
|
+
headers = {
|
|
637
|
+
'x-vona-claim-phase': 'SECRET_PATH',
|
|
638
|
+
'x-vona-claim-code': 'SECRET_TOKEN',
|
|
639
|
+
'x-vona-claim-native-code': '999999',
|
|
640
|
+
};
|
|
641
|
+
await assert.rejects(sendTestClaimEvent(env, { type: 'status', status: 'running' }), {
|
|
642
|
+
message: 'Managed test allocation registration failed (HTTP 409)',
|
|
643
|
+
});
|
|
644
|
+
await new Promise<void>((resolve, reject) =>
|
|
645
|
+
server.close(error => (error ? reject(error) : resolve())),
|
|
646
|
+
);
|
|
647
|
+
await assert.rejects(sendTestClaimEvent(env, { type: 'status', status: 'running' }), {
|
|
648
|
+
message: 'Managed test allocation registration failed (transport)',
|
|
649
|
+
});
|
|
650
|
+
} finally {
|
|
651
|
+
if (server.listening) {
|
|
652
|
+
await new Promise<void>((resolve, reject) =>
|
|
653
|
+
server.close(error => (error ? reject(error) : resolve())),
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
366
659
|
test('database confirmations and process registration cannot precede allocation intent', async () => {
|
|
367
660
|
const f = fixture();
|
|
368
661
|
let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
|
|
@@ -662,9 +662,17 @@ export async function startTestClaimBroker(claimPath: string) {
|
|
|
662
662
|
const update = (event: TestClaimEvent) => {
|
|
663
663
|
if (closing) return Promise.reject(new Error('Test claim broker is closed'));
|
|
664
664
|
const result = queue.then(async () => {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
665
|
+
let phase = 'read';
|
|
666
|
+
try {
|
|
667
|
+
const manifest = await loadTestClaim(claimPath);
|
|
668
|
+
phase = 'validate';
|
|
669
|
+
applyEvent(manifest, event);
|
|
670
|
+
phase = 'write';
|
|
671
|
+
await writeTestClaim(claimPath, manifest);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
if (error instanceof Error) Object.assign(error, { testClaimPhase: phase });
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
668
676
|
});
|
|
669
677
|
queue = result.catch(() => {});
|
|
670
678
|
return result;
|
|
@@ -692,8 +700,21 @@ export async function startTestClaimBroker(claimPath: string) {
|
|
|
692
700
|
if (req.url === '/containment') await containment(parseTestClaimJson(body));
|
|
693
701
|
else await update(parseTestClaimJson(body));
|
|
694
702
|
res.writeHead(204).end();
|
|
695
|
-
} catch {
|
|
696
|
-
|
|
703
|
+
} catch (error) {
|
|
704
|
+
const failure = error as { testClaimPhase?: string; code?: string; nativeCode?: number };
|
|
705
|
+
const headers: Record<string, string> = {};
|
|
706
|
+
if (['read', 'validate', 'write'].includes(failure?.testClaimPhase!))
|
|
707
|
+
headers['x-vona-claim-phase'] = failure.testClaimPhase!;
|
|
708
|
+
if (['EEXIST', 'ENOENT', 'EBUSY', 'EACCES', 'EUNSAFE'].includes(failure?.code!))
|
|
709
|
+
headers['x-vona-claim-code'] = failure.code!;
|
|
710
|
+
if (
|
|
711
|
+
Number.isInteger(failure?.nativeCode) &&
|
|
712
|
+
failure.nativeCode! >= 0 &&
|
|
713
|
+
failure.nativeCode! <= 65535
|
|
714
|
+
) {
|
|
715
|
+
headers['x-vona-claim-native-code'] = String(failure.nativeCode);
|
|
716
|
+
}
|
|
717
|
+
res.writeHead(409, headers).end('Test claim registration rejected');
|
|
697
718
|
}
|
|
698
719
|
});
|
|
699
720
|
server.requestTimeout = 10000;
|
|
@@ -753,20 +774,35 @@ export function hasTestClaimTransport(env: TestClaimEnv = process.env) {
|
|
|
753
774
|
}
|
|
754
775
|
export async function sendTestClaimEvent(env: TestClaimEnv, event: TestClaimEvent) {
|
|
755
776
|
if (!hasTestClaimTransport(env)) return;
|
|
777
|
+
let response: Response;
|
|
756
778
|
try {
|
|
757
|
-
|
|
779
|
+
response = await fetch(env[TEST_CLAIM_URL_ENV]!, {
|
|
758
780
|
method: 'POST',
|
|
759
781
|
headers: {
|
|
760
782
|
'authorization': `Bearer ${env[TEST_CLAIM_TOKEN_ENV]}`,
|
|
783
|
+
// Startup can block past the broker's idle timeout; never reuse a stale socket.
|
|
784
|
+
'connection': 'close',
|
|
761
785
|
'content-type': 'application/json',
|
|
762
786
|
},
|
|
763
787
|
body: JSON.stringify(event),
|
|
764
|
-
|
|
788
|
+
// Includes the serialized queue and durable Windows filesystem work (30s per operation).
|
|
789
|
+
signal: AbortSignal.timeout(120000),
|
|
765
790
|
redirect: 'error',
|
|
766
791
|
});
|
|
767
|
-
if (response.status !== 204) throw new Error('Registration rejected');
|
|
768
792
|
} catch {
|
|
769
|
-
throw new Error('Managed test allocation registration failed');
|
|
793
|
+
throw new Error('Managed test allocation registration failed (transport)');
|
|
794
|
+
}
|
|
795
|
+
if (response.status !== 204) {
|
|
796
|
+
const details = [`HTTP ${response.status}`];
|
|
797
|
+
const phase = response.headers.get('x-vona-claim-phase');
|
|
798
|
+
const code = response.headers.get('x-vona-claim-code');
|
|
799
|
+
const nativeCode = response.headers.get('x-vona-claim-native-code');
|
|
800
|
+
if (['read', 'validate', 'write'].includes(phase!)) details.push(phase!);
|
|
801
|
+
if (['EEXIST', 'ENOENT', 'EBUSY', 'EACCES', 'EUNSAFE'].includes(code!)) details.push(code!);
|
|
802
|
+
if (nativeCode && /^\d{1,5}$/.test(nativeCode) && Number(nativeCode) <= 65535)
|
|
803
|
+
details.push(`native ${nativeCode}`);
|
|
804
|
+
await response.body?.cancel().catch(() => {});
|
|
805
|
+
throw new Error(`Managed test allocation registration failed (${details.join(', ')})`);
|
|
770
806
|
}
|
|
771
807
|
}
|
|
772
808
|
|
|
@@ -784,6 +820,8 @@ export async function assertTestClaimWindowsJob(env: TestClaimEnv, identity: Tes
|
|
|
784
820
|
method: 'POST',
|
|
785
821
|
headers: {
|
|
786
822
|
'authorization': `Bearer ${env[TEST_CLAIM_TOKEN_ENV]}`,
|
|
823
|
+
// Startup can block past the broker's idle timeout; never reuse a stale socket.
|
|
824
|
+
'connection': 'close',
|
|
787
825
|
'content-type': 'application/json',
|
|
788
826
|
},
|
|
789
827
|
body: JSON.stringify(query),
|