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.
- package/.cabloy-version +1 -1
- package/.github/workflows/create-cabloy.yml +137 -0
- package/.github/workflows/playwright-e2e.yml +163 -13
- package/.github/workflows/vona-test-sqlite3.yml +77 -7
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +4 -2
- package/package.json +1 -1
- package/repo-docs/frontend/scripts.md +2 -0
- package/repo-docs/reference/repo-scripts.md +27 -1
- package/repo-e2e/scripts/e2e.test.ts +1 -1
- package/repo-e2e/scripts/managedTest.test.ts +115 -3
- package/repo-e2e/scripts/managedTest.ts +69 -20
- package/repo-e2e/scripts/managedTestImports.test.ts +55 -5
- package/repo-e2e/scripts/runE2e.test.ts +109 -0
- package/repo-e2e/scripts/runE2e.ts +58 -39
- package/repo-e2e/scripts/runE2eArgs.test.ts +16 -1
- package/repo-e2e/scripts/runE2eArgs.ts +5 -0
- package/repo-e2e/specs/a-commerce.spec.ts +10 -2
- package/repo-e2e/specs/home-user-account.spec.ts +4 -0
- package/repo-e2e/specs/markdown.spec.ts +14 -3
- package/scripts/generateOpenapiIsolated.test.ts +5 -1
- package/scripts/runIsolatedTest.test.ts +403 -36
- package/scripts/runIsolatedTest.ts +139 -12
- package/scripts/testBrowserGuardianWindows.test.ts +327 -0
- package/scripts/testClaimCleanup.test.ts +25 -9
- package/scripts/testClaimCleanup.ts +14 -45
- package/scripts/testClaimProcesses.test.ts +81 -2
- package/scripts/testClaimProcesses.ts +46 -10
- package/scripts/testProcessGuardianWindows.test.ts +602 -0
- package/scripts/testProcessGuardianWindows.ts +155 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +4 -1
- package/vona/packages-cli/cli-set-api/test/binBuildEntries.test.ts +159 -0
- 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/core/testClaim.test.ts +298 -17
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +205 -97
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts +247 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.ts +224 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.cs +229 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ps1 +20 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaimFilesystemWindows.ts +200 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.cs +336 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ps1 +20 -0
- package/vona/packages-vona/vona-core/src/lib/core/testProcessWindowsNative.ts +531 -0
- package/vona/packages-vona/vona-core/src/lib/framework/bootstrap.test.ts +61 -0
- package/vona/packages-vona/vona-core/src/lib/framework/index.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/framework/processWorker.test.ts +59 -0
- package/vona/packages-vona/vona-core/src/lib/framework/processWorker.ts +8 -5
- package/vona/packages-vona/vona-core/src/lib/framework/useApp.test.ts +84 -0
- package/vona/packages-vona/vona-core/src/lib/framework/useApp.ts +2 -2
- package/vona/packages-vona/vona-core/src/lib/utils/util.ts +1 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +42 -181
- 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-executor/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +2 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts +16 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.ts +6 -3
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +80 -0
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/src/bean/middlewareSystem.health.ts +73 -2
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts +309 -0
- 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/service/transactionAsyncLocalStorage_.ts +3 -3
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-static/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-static/src/bean/middlewareSystem.static.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-static/test/middlewareSystem.static.test.ts +101 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +4 -4
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-utils/zova-vite/package.json +2 -2
- package/zova/packages-utils/zova-vite/src/configUtils.ts +1 -1
- package/zova/packages-utils/zova-vite/test/configUtils.test.ts +55 -0
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +2 -2
- package/zova/pnpm-lock.yaml +324 -63
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +8 -4
- package/zova/src/suite/a-commerce/modules/commerce-trade/test/lib/cartModel.test.ts +71 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
|
@@ -801,7 +801,7 @@ test(
|
|
|
801
801
|
|
|
802
802
|
const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
|
|
803
803
|
await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
|
|
804
|
-
await addResponse;
|
|
804
|
+
expect((await addResponse).ok()).toBeTruthy();
|
|
805
805
|
|
|
806
806
|
await page.getByRole('link', { name: /^Cart/ }).click();
|
|
807
807
|
await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/);
|
|
@@ -1343,7 +1343,7 @@ test(
|
|
|
1343
1343
|
await expect(page.getByText('$45.99')).toBeVisible();
|
|
1344
1344
|
const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
|
|
1345
1345
|
await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
|
|
1346
|
-
await addResponse;
|
|
1346
|
+
expect((await addResponse).ok()).toBeTruthy();
|
|
1347
1347
|
await page.getByRole('link', { name: /^Cart/ }).click();
|
|
1348
1348
|
await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/);
|
|
1349
1349
|
await page.getByRole('link', { name: 'Checkout', exact: true }).click();
|
|
@@ -1662,6 +1662,10 @@ test(
|
|
|
1662
1662
|
/\/commerce-admin\/rest\/resource\/commerce-promotion(?:%3A|:|%253A)couponTemplate\/create(?:[/?#]|$)/,
|
|
1663
1663
|
);
|
|
1664
1664
|
|
|
1665
|
+
await expect(adminPage.locator('html')).toHaveAttribute(
|
|
1666
|
+
'data-zova-hydrated',
|
|
1667
|
+
'commerceAdmin',
|
|
1668
|
+
);
|
|
1665
1669
|
for (const groupName of [
|
|
1666
1670
|
'Basic Information',
|
|
1667
1671
|
'Discount Policy',
|
|
@@ -1765,6 +1769,10 @@ test(
|
|
|
1765
1769
|
expectTableIdentity(parentId);
|
|
1766
1770
|
|
|
1767
1771
|
await adminPage.goto(categoryCreatePath, { waitUntil: 'load' });
|
|
1772
|
+
await expect(adminPage.locator('html')).toHaveAttribute(
|
|
1773
|
+
'data-zova-hydrated',
|
|
1774
|
+
'commerceAdmin',
|
|
1775
|
+
);
|
|
1768
1776
|
const parentPublication = adminPage
|
|
1769
1777
|
.getByRole('group', { name: 'Published' })
|
|
1770
1778
|
.getByRole('combobox');
|
|
@@ -536,6 +536,10 @@ test(
|
|
|
536
536
|
expect(profile.ok()).toBeTruthy();
|
|
537
537
|
const profileBody = (await profile.json()).data;
|
|
538
538
|
expect(profileBody.avatar).toEqual(expect.any(String));
|
|
539
|
+
const avatarResponse = await request.get(profileBody.avatar);
|
|
540
|
+
expect(avatarResponse.ok()).toBeTruthy();
|
|
541
|
+
expect(avatarResponse.headers()['content-type']).toMatch(/^image\//);
|
|
542
|
+
expect((await avatarResponse.body()).length).toBeGreaterThan(0);
|
|
539
543
|
expect(pageErrors).toEqual([]);
|
|
540
544
|
expect(consoleErrors).toEqual([]);
|
|
541
545
|
} finally {
|
|
@@ -590,15 +590,26 @@ test(
|
|
|
590
590
|
await linkInput.fill('');
|
|
591
591
|
await linkInput.press('Enter');
|
|
592
592
|
await expect(editor.locator('h1 a')).toHaveCount(0);
|
|
593
|
+
const persistedText = `Link removal saved ${suffix}`;
|
|
594
|
+
await editor.press('ControlOrMeta+End');
|
|
595
|
+
await editor.press('Enter');
|
|
596
|
+
await editor.pressSequentially(persistedText);
|
|
597
|
+
const productUpdate = waitForApiResponse(
|
|
598
|
+
adminPage,
|
|
599
|
+
'PATCH',
|
|
600
|
+
`${productActionPath}/${productId}`,
|
|
601
|
+
);
|
|
593
602
|
await adminPage.getByRole('button', { name: 'Submit', exact: true }).click();
|
|
603
|
+
expect((await productUpdate).ok()).toBeTruthy();
|
|
594
604
|
const productResponseAfterLinkRemoval = await adminPage.request.get(
|
|
595
605
|
`${productActionPath}/${productId}`,
|
|
596
606
|
{ headers },
|
|
597
607
|
);
|
|
598
608
|
expect(productResponseAfterLinkRemoval.ok()).toBeTruthy();
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
).
|
|
609
|
+
const savedMarkdown = (await productResponseAfterLinkRemoval.json()).productContentForm
|
|
610
|
+
.descriptionMarkdown;
|
|
611
|
+
expect(savedMarkdown).toContain(persistedText);
|
|
612
|
+
expect(savedMarkdown).not.toContain('https://example.com/updated');
|
|
602
613
|
expect(adminPageErrors).toEqual([]);
|
|
603
614
|
expect(adminConsoleErrors).toEqual([]);
|
|
604
615
|
} finally {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { once } from 'node:events';
|
|
3
3
|
import { createServer } from 'node:http';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
4
5
|
import { test } from 'node:test';
|
|
5
6
|
|
|
6
7
|
import type { GuardianResult, OwnedProcess } from './runIsolatedTest.ts';
|
|
@@ -46,7 +47,7 @@ test('launches only a loopback single-worker test-mode Vona service', () => {
|
|
|
46
47
|
'--flavor=normal',
|
|
47
48
|
'--mode=test',
|
|
48
49
|
]);
|
|
49
|
-
assert.equal(command.cwd,
|
|
50
|
+
assert.equal(command.cwd, resolve(ROOT_DIR, 'vona'));
|
|
50
51
|
assert.deepEqual(command.env, {
|
|
51
52
|
CUSTOM: 'keep',
|
|
52
53
|
VONA_TEST_READY_KEY: 'test-key',
|
|
@@ -81,6 +82,9 @@ function lifecycleFixture(options: { readyFailure?: boolean; stopFailure?: boole
|
|
|
81
82
|
finish,
|
|
82
83
|
result,
|
|
83
84
|
dependencies: {
|
|
85
|
+
// These legacy fixtures assert POSIX signal/stop semantics; Windows cooperative
|
|
86
|
+
// shutdown is exercised separately with explicit platform injection.
|
|
87
|
+
platform: 'darwin' as const,
|
|
84
88
|
start: async () => {
|
|
85
89
|
events.push('start');
|
|
86
90
|
return server;
|
|
@@ -15,6 +15,7 @@ import { createServer } from 'node:http';
|
|
|
15
15
|
import { tmpdir } from 'node:os';
|
|
16
16
|
import { join, resolve } from 'node:path';
|
|
17
17
|
import { test } from 'node:test';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
18
19
|
|
|
19
20
|
import type { VonaApplication } from '../vona/packages-vona/vona-core/src/lib/core/application.ts';
|
|
20
21
|
|
|
@@ -32,10 +33,14 @@ import {
|
|
|
32
33
|
createTestRunId,
|
|
33
34
|
getUnitTestCommands,
|
|
34
35
|
reserveTestRun,
|
|
36
|
+
resolveNpmCli,
|
|
35
37
|
resolveCabloyEdition,
|
|
36
38
|
ROOT_DIR,
|
|
37
39
|
runIsolatedProcess,
|
|
38
40
|
runIsolatedTest,
|
|
41
|
+
startOwnedProcess,
|
|
42
|
+
requestIsolatedVonaStop,
|
|
43
|
+
withIsolatedVona,
|
|
39
44
|
verifyReadyProof,
|
|
40
45
|
waitForReady,
|
|
41
46
|
withIsolatedRun,
|
|
@@ -65,7 +70,11 @@ test('public test scripts delegate to the isolated runner without recursion', ()
|
|
|
65
70
|
|
|
66
71
|
const commands = getUnitTestCommands(['--coverage', '--ci', 'src/test/example.test.ts']);
|
|
67
72
|
assert.deepEqual(commands, [
|
|
68
|
-
{
|
|
73
|
+
{
|
|
74
|
+
command: process.execPath,
|
|
75
|
+
args: [resolveNpmCli(), 'run', 'prerun'],
|
|
76
|
+
cwd: resolve(ROOT_DIR, 'vona'),
|
|
77
|
+
},
|
|
69
78
|
{
|
|
70
79
|
command: process.execPath,
|
|
71
80
|
args: [
|
|
@@ -85,6 +94,72 @@ test('public test scripts delegate to the isolated runner without recursion', ()
|
|
|
85
94
|
]);
|
|
86
95
|
});
|
|
87
96
|
|
|
97
|
+
test('resolves the npm JavaScript entrypoint without a command shell', () => {
|
|
98
|
+
const root = mkdtempSync(join(realpathSync(tmpdir()), 'cabloy npm argv '));
|
|
99
|
+
try {
|
|
100
|
+
const cli = join(root, 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
101
|
+
mkdirSync(join(root, 'node_modules', 'npm', 'bin'), { recursive: true });
|
|
102
|
+
writeFileSync(cli, '');
|
|
103
|
+
const executable = join(root, process.platform === 'win32' ? 'node.exe' : 'node');
|
|
104
|
+
assert.equal(resolveNpmCli({}, executable), realpathSync(cli));
|
|
105
|
+
assert.equal(resolveNpmCli({ npm_execpath: cli }, '/missing/node'), realpathSync(cli));
|
|
106
|
+
assert.throws(
|
|
107
|
+
() => resolveNpmCli({ npm_execpath: join(root, 'pnpm.cjs') }, join(root, 'missing', 'node')),
|
|
108
|
+
/Cannot resolve npm CLI/,
|
|
109
|
+
);
|
|
110
|
+
} finally {
|
|
111
|
+
rmSync(root, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test(
|
|
116
|
+
'public npm test entrypoint can be invoked from CMD and PowerShell',
|
|
117
|
+
{
|
|
118
|
+
skip: process.platform !== 'win32' ? 'Native Windows shells are required' : false,
|
|
119
|
+
},
|
|
120
|
+
() => {
|
|
121
|
+
const root = mkdtempSync(join(realpathSync(tmpdir()), 'cabloy shell entry '));
|
|
122
|
+
try {
|
|
123
|
+
writeFileSync(join(root, '__CABLOY_BASIC__'), '');
|
|
124
|
+
mkdirSync(join(root, 'vona'));
|
|
125
|
+
const entry = resolve(ROOT_DIR, 'scripts/runIsolatedTest.ts');
|
|
126
|
+
writeFileSync(
|
|
127
|
+
join(root, 'package.json'),
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
private: true,
|
|
130
|
+
scripts: { test: `node "${entry}" unit` },
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
for (const [command, args] of [
|
|
134
|
+
['cmd.exe', ['/d', '/s', '/c', 'npm run test -- --ci']],
|
|
135
|
+
[
|
|
136
|
+
'pwsh.exe',
|
|
137
|
+
[
|
|
138
|
+
'-NoLogo',
|
|
139
|
+
'-NoProfile',
|
|
140
|
+
'-NonInteractive',
|
|
141
|
+
'-Command',
|
|
142
|
+
'npm run test -- --ci; exit $LASTEXITCODE',
|
|
143
|
+
],
|
|
144
|
+
],
|
|
145
|
+
] as const) {
|
|
146
|
+
const result = spawnSync(command, [...args], {
|
|
147
|
+
cwd: root,
|
|
148
|
+
env: { ...process.env, E2E_BASE_URL: 'https://example.invalid' },
|
|
149
|
+
encoding: 'utf8',
|
|
150
|
+
timeout: 30_000,
|
|
151
|
+
});
|
|
152
|
+
assert.equal(result.error, undefined);
|
|
153
|
+
assert.notEqual(result.status, 0);
|
|
154
|
+
assert.match(result.stderr, /E2E_BASE_URL is unsupported/);
|
|
155
|
+
assert.equal(existsSync(join(root, 'vona', '.vona-test-claim')), false);
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
rmSync(root, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
88
163
|
test('accepts either Cabloy edition and rejects ambiguous or missing markers', () => {
|
|
89
164
|
const root = mkdtempSync(join(tmpdir(), 'cabloy-run-edition-'));
|
|
90
165
|
const basicMarker = join(root, '__CABLOY_BASIC__');
|
|
@@ -132,12 +207,12 @@ test('creates exclusive manifest claims and refuses stale or symlinked roots', a
|
|
|
132
207
|
assert.equal(readFileSync(join(staleRoot, 'business-data'), 'utf8'), 'keep');
|
|
133
208
|
assert.equal(existsSync(join(staleRoot, '.vona-test-owner')), false);
|
|
134
209
|
const linkedRoot = join(vonaDir, `.vona-test-${id}`);
|
|
135
|
-
symlinkSync(staleRoot, linkedRoot);
|
|
210
|
+
symlinkSync(staleRoot, linkedRoot, process.platform === 'win32' ? 'junction' : 'dir');
|
|
136
211
|
assert.throws(() => createOwned(linkedRoot, id), /already exists/);
|
|
137
212
|
assert.equal(readFileSync(join(staleRoot, 'business-data'), 'utf8'), 'keep');
|
|
138
213
|
|
|
139
214
|
const linked = join(root, 'linked');
|
|
140
|
-
symlinkSync(vonaDir, linked);
|
|
215
|
+
symlinkSync(vonaDir, linked, process.platform === 'win32' ? 'junction' : 'dir');
|
|
141
216
|
await assert.rejects(reserveTestRun(linked, '20261014010204-d4e5f6a7'), /Unsafe/);
|
|
142
217
|
assert.equal(
|
|
143
218
|
existsSync(join(vonaDir, '.vona-test-claim', '20261014010204-d4e5f6a7.json')),
|
|
@@ -294,7 +369,7 @@ test('rejects external E2E targets before creating run directories', async () =>
|
|
|
294
369
|
|
|
295
370
|
test('resolves distinct run paths and rejects invalid identity without touching dev paths', () => {
|
|
296
371
|
const script = `
|
|
297
|
-
import { resolveVonaLocalPaths, getTestAppName } from ${JSON.stringify(isolationSource)};
|
|
372
|
+
import { resolveVonaLocalPaths, getTestAppName } from ${JSON.stringify(pathToFileURL(isolationSource).href)};
|
|
298
373
|
const project = '/project/vona';
|
|
299
374
|
const a = resolveVonaLocalPaths(project, 'test', '20261014010203-a1b2c3d4');
|
|
300
375
|
const b = resolveVonaLocalPaths(project, 'test', '20261014010203-d4e5f6a7');
|
|
@@ -316,7 +391,7 @@ test('owner checks reject unowned paths and symlinked leaves', () => {
|
|
|
316
391
|
const root = mkdtempSync(join(tmpdir(), 'cabloy-test-isolation-'));
|
|
317
392
|
const id = '20261014010203-a1b2c3d4';
|
|
318
393
|
const script = `
|
|
319
|
-
import { assertTestRunOwner } from ${JSON.stringify(isolationSource)};
|
|
394
|
+
import { assertTestRunOwner } from ${JSON.stringify(pathToFileURL(isolationSource).href)};
|
|
320
395
|
const [unowned, linked] = process.argv.slice(1);
|
|
321
396
|
try { assertTestRunOwner(unowned, ${JSON.stringify(id)}); process.exit(1); } catch {}
|
|
322
397
|
try { assertTestRunOwner(linked, ${JSON.stringify(id)}); process.exit(2); } catch {}
|
|
@@ -328,7 +403,7 @@ test('owner checks reject unowned paths and symlinked leaves', () => {
|
|
|
328
403
|
const unowned = join(root, 'unowned');
|
|
329
404
|
mkdirSync(unowned);
|
|
330
405
|
const linked = join(root, 'linked');
|
|
331
|
-
symlinkSync(owned, linked);
|
|
406
|
+
symlinkSync(owned, linked, process.platform === 'win32' ? 'junction' : 'dir');
|
|
332
407
|
const result = spawnSync(
|
|
333
408
|
process.execPath,
|
|
334
409
|
['--input-type=module', '-e', script, unowned, linked],
|
|
@@ -345,13 +420,13 @@ test('owner checks reject unowned paths and symlinked leaves', () => {
|
|
|
345
420
|
});
|
|
346
421
|
|
|
347
422
|
test('waits for descendants before reporting a successful child exit', async () => {
|
|
348
|
-
if (process.platform === 'win32') return;
|
|
349
423
|
const root = mkdtempSync(join(tmpdir(), 'cabloy-process-group-'));
|
|
350
424
|
const flag = join(root, 'descendant-finished');
|
|
351
425
|
try {
|
|
352
426
|
const script = `
|
|
353
427
|
import { spawn } from 'node:child_process';
|
|
354
|
-
|
|
428
|
+
// Avoid libuv's parent-owned kill-on-close Job on Windows; the outer Job still owns this child.
|
|
429
|
+
const child = spawn(process.execPath, ['-e', 'setTimeout(() => require("node:fs").writeFileSync(process.argv[1], "done"), 400)', process.argv[1]], { stdio: 'ignore', detached: process.platform === 'win32' });
|
|
355
430
|
child.unref();
|
|
356
431
|
`;
|
|
357
432
|
await runIsolatedProcess(
|
|
@@ -367,13 +442,13 @@ test('waits for descendants before reporting a successful child exit', async ()
|
|
|
367
442
|
});
|
|
368
443
|
|
|
369
444
|
test('a guardian waits for an inherited-group descendant before success', async () => {
|
|
370
|
-
if (process.platform === 'win32') return;
|
|
371
445
|
const root = mkdtempSync(join(resolve(ROOT_DIR, 'vona'), '.e2e-group-test-'));
|
|
372
446
|
const flag = join(root, 'server-finished');
|
|
373
447
|
try {
|
|
374
448
|
const script = `
|
|
375
449
|
import { spawn } from 'node:child_process';
|
|
376
|
-
|
|
450
|
+
// Preserve the inherited POSIX group, but avoid libuv's kill-on-close Job on Windows.
|
|
451
|
+
const child = spawn(process.execPath, ['-e', 'setTimeout(() => require("node:fs").writeFileSync(process.argv[1], "done"), 400)', process.argv[1]], { stdio: 'ignore', detached: process.platform === 'win32' });
|
|
377
452
|
child.unref();
|
|
378
453
|
`;
|
|
379
454
|
await runIsolatedProcess(
|
|
@@ -408,7 +483,13 @@ test('managed guardians journal registration and drainage for successful and fai
|
|
|
408
483
|
for (const item of claim.processes) {
|
|
409
484
|
assert.equal(item.stage, 'drained');
|
|
410
485
|
assert.equal(item.role, 'guardian');
|
|
411
|
-
|
|
486
|
+
if (process.platform === 'win32') {
|
|
487
|
+
assert.equal(item.containment, 'windows-job');
|
|
488
|
+
assert.equal(item.pgid, undefined);
|
|
489
|
+
assert.ok(item.jobId);
|
|
490
|
+
} else {
|
|
491
|
+
assert.equal(item.pid, item.pgid);
|
|
492
|
+
}
|
|
412
493
|
assert.ok(item.host);
|
|
413
494
|
assert.ok(item.startTime);
|
|
414
495
|
}
|
|
@@ -419,20 +500,26 @@ test('managed guardians journal registration and drainage for successful and fai
|
|
|
419
500
|
});
|
|
420
501
|
|
|
421
502
|
test('does not report success when a child exits nonzero', async () => {
|
|
422
|
-
if (process.platform === 'win32') return;
|
|
423
503
|
await assert.rejects(
|
|
424
504
|
runIsolatedProcess(process.execPath, ['-e', 'process.exit(7)'], ROOT_DIR, { ...process.env }),
|
|
425
505
|
/Isolated test exited: 7/,
|
|
426
506
|
);
|
|
427
507
|
});
|
|
428
508
|
|
|
429
|
-
test(
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
509
|
+
test(
|
|
510
|
+
'interrupts an owned group and waits for its graceful child shutdown',
|
|
511
|
+
{
|
|
512
|
+
skip:
|
|
513
|
+
process.platform === 'win32'
|
|
514
|
+
? 'Windows cancellation terminates the Job; POSIX graceful signals are not supported'
|
|
515
|
+
: false,
|
|
516
|
+
},
|
|
517
|
+
async () => {
|
|
518
|
+
const root = mkdtempSync(join(resolve(ROOT_DIR, 'vona'), '.guardian-interrupt-test-'));
|
|
519
|
+
const readyFile = join(root, 'ready');
|
|
520
|
+
const stoppedFile = join(root, 'stopped');
|
|
521
|
+
try {
|
|
522
|
+
const script = `
|
|
436
523
|
import { writeFileSync } from 'node:fs';
|
|
437
524
|
writeFileSync(process.argv[1], 'ready');
|
|
438
525
|
process.on('SIGINT', () => {
|
|
@@ -443,24 +530,25 @@ test('interrupts an owned group and waits for its graceful child shutdown', asyn
|
|
|
443
530
|
});
|
|
444
531
|
setInterval(() => {}, 1000);
|
|
445
532
|
`;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
533
|
+
const pending = runIsolatedProcess(
|
|
534
|
+
process.execPath,
|
|
535
|
+
['--input-type=module', '-e', script, readyFile, stoppedFile],
|
|
536
|
+
ROOT_DIR,
|
|
537
|
+
{ ...process.env },
|
|
538
|
+
);
|
|
539
|
+
const deadline = Date.now() + 5000;
|
|
540
|
+
while (!existsSync(readyFile) && Date.now() < deadline) {
|
|
541
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 50));
|
|
542
|
+
}
|
|
543
|
+
assert.ok(existsSync(readyFile));
|
|
544
|
+
process.emit('SIGINT');
|
|
545
|
+
await assert.rejects(pending, /Isolated test exited/);
|
|
546
|
+
assert.ok(existsSync(stoppedFile));
|
|
547
|
+
} finally {
|
|
548
|
+
rmSync(root, { recursive: true, force: true });
|
|
455
549
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
await assert.rejects(pending, /Isolated test exited/);
|
|
459
|
-
assert.ok(existsSync(stoppedFile));
|
|
460
|
-
} finally {
|
|
461
|
-
rmSync(root, { recursive: true, force: true });
|
|
462
|
-
}
|
|
463
|
-
});
|
|
550
|
+
},
|
|
551
|
+
);
|
|
464
552
|
|
|
465
553
|
test('rejects merged paths, Redis prefixes, and database overrides before cleanup', () => {
|
|
466
554
|
const root = mkdtempSync(join(resolve(ROOT_DIR, 'vona'), '.isolation-guard-test-'));
|
|
@@ -535,3 +623,282 @@ test('rejects merged paths, Redis prefixes, and database overrides before cleanu
|
|
|
535
623
|
rmSync(root, { recursive: true, force: true });
|
|
536
624
|
}
|
|
537
625
|
});
|
|
626
|
+
|
|
627
|
+
function shutdownFixture(
|
|
628
|
+
options: {
|
|
629
|
+
readyFailure?: boolean;
|
|
630
|
+
requestFailure?: boolean;
|
|
631
|
+
fallbackFailure?: boolean;
|
|
632
|
+
drainageFailure?: boolean;
|
|
633
|
+
earlyExit?: boolean;
|
|
634
|
+
hang?: boolean;
|
|
635
|
+
code?: number;
|
|
636
|
+
} = {},
|
|
637
|
+
) {
|
|
638
|
+
const events: string[] = [];
|
|
639
|
+
let finish!: (result: import('./runIsolatedTest.ts').GuardianResult) => void;
|
|
640
|
+
const result = {
|
|
641
|
+
type: 'drained' as const,
|
|
642
|
+
failed: !!options.drainageFailure,
|
|
643
|
+
result: { code: options.code ?? 0, signal: null },
|
|
644
|
+
};
|
|
645
|
+
const waited = new Promise<import('./runIsolatedTest.ts').GuardianResult>(resolve => {
|
|
646
|
+
finish = resolve;
|
|
647
|
+
});
|
|
648
|
+
const server = {
|
|
649
|
+
wait: () => waited,
|
|
650
|
+
stop: async () => {
|
|
651
|
+
events.push('force-stop');
|
|
652
|
+
if (options.fallbackFailure) throw new Error('fallback failed');
|
|
653
|
+
finish(result);
|
|
654
|
+
return result;
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
const dependencies = {
|
|
658
|
+
platform: 'win32' as const,
|
|
659
|
+
shutdownTimeoutMs: 30,
|
|
660
|
+
start: async () => {
|
|
661
|
+
events.push('start');
|
|
662
|
+
return server;
|
|
663
|
+
},
|
|
664
|
+
ready: async () => {
|
|
665
|
+
events.push('ready');
|
|
666
|
+
if (options.earlyExit) {
|
|
667
|
+
finish(result);
|
|
668
|
+
await Promise.resolve();
|
|
669
|
+
}
|
|
670
|
+
if (options.readyFailure) throw new Error('readiness failed');
|
|
671
|
+
},
|
|
672
|
+
requestStop: async () => {
|
|
673
|
+
events.push('cooperative-stop');
|
|
674
|
+
if (options.requestFailure) throw new Error('authorization failed');
|
|
675
|
+
if (!options.hang) finish(result);
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
return { dependencies, events, server };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
test('Windows verified-readiness teardown requests cooperative shutdown then waits without force-stop', async () => {
|
|
682
|
+
const f = shutdownFixture();
|
|
683
|
+
const before = process.listenerCount('SIGINT');
|
|
684
|
+
assert.equal(
|
|
685
|
+
await withIsolatedVona(
|
|
686
|
+
1234,
|
|
687
|
+
{ VONA_TEST_RUN_ID: '20261014010203-a1b2c3d4' },
|
|
688
|
+
async () => {
|
|
689
|
+
f.events.push('callback');
|
|
690
|
+
return 'done';
|
|
691
|
+
},
|
|
692
|
+
f.dependencies,
|
|
693
|
+
),
|
|
694
|
+
'done',
|
|
695
|
+
);
|
|
696
|
+
assert.deepEqual(f.events, ['start', 'ready', 'callback', 'cooperative-stop']);
|
|
697
|
+
assert.equal(process.listenerCount('SIGINT'), before);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
for (const options of [
|
|
701
|
+
{ requestFailure: true },
|
|
702
|
+
{ hang: true },
|
|
703
|
+
{ drainageFailure: true },
|
|
704
|
+
{ code: 1 },
|
|
705
|
+
]) {
|
|
706
|
+
test(`Windows cooperative failure always force-stops and fails (${JSON.stringify(options)})`, async () => {
|
|
707
|
+
const f = shutdownFixture(options);
|
|
708
|
+
await assert.rejects(withIsolatedVona(1234, {}, async () => {}, f.dependencies));
|
|
709
|
+
assert.deepEqual(f.events, ['start', 'ready', 'cooperative-stop', 'force-stop']);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
test('Windows readiness failure or early exit never requests cooperative shutdown', async () => {
|
|
714
|
+
for (const options of [{ readyFailure: true }, { earlyExit: true }]) {
|
|
715
|
+
const f = shutdownFixture(options);
|
|
716
|
+
await assert.rejects(withIsolatedVona(1234, {}, async () => {}, f.dependencies));
|
|
717
|
+
assert.deepEqual(f.events, ['start', 'ready', 'force-stop']);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('Windows interruption is failure and uses force-stop, not authenticated normal shutdown', async () => {
|
|
722
|
+
const f = shutdownFixture();
|
|
723
|
+
await assert.rejects(
|
|
724
|
+
withIsolatedVona(
|
|
725
|
+
1234,
|
|
726
|
+
{},
|
|
727
|
+
async () => {
|
|
728
|
+
process.emit('SIGINT');
|
|
729
|
+
},
|
|
730
|
+
f.dependencies,
|
|
731
|
+
),
|
|
732
|
+
/interrupted/,
|
|
733
|
+
);
|
|
734
|
+
assert.equal(f.events.includes('cooperative-stop'), false);
|
|
735
|
+
assert.ok(f.events.includes('force-stop'));
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test('callback, cooperative shutdown and fallback errors all survive', async () => {
|
|
739
|
+
const f = shutdownFixture({ requestFailure: true, fallbackFailure: true });
|
|
740
|
+
await assert.rejects(
|
|
741
|
+
withIsolatedVona(
|
|
742
|
+
1234,
|
|
743
|
+
{},
|
|
744
|
+
async () => {
|
|
745
|
+
throw new Error('callback failed');
|
|
746
|
+
},
|
|
747
|
+
f.dependencies,
|
|
748
|
+
),
|
|
749
|
+
(error: unknown) => {
|
|
750
|
+
assert.ok(error instanceof AggregateError);
|
|
751
|
+
assert.equal(error.errors[0].message, 'callback failed');
|
|
752
|
+
assert.ok(error.errors[1] instanceof AggregateError);
|
|
753
|
+
assert.deepEqual(
|
|
754
|
+
error.errors[1].errors.map((e: Error) => e.message),
|
|
755
|
+
['authorization failed', 'fallback failed'],
|
|
756
|
+
);
|
|
757
|
+
return true;
|
|
758
|
+
},
|
|
759
|
+
);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test('callback error survives a clean Windows cooperative shutdown', async () => {
|
|
763
|
+
const f = shutdownFixture();
|
|
764
|
+
await assert.rejects(
|
|
765
|
+
withIsolatedVona(
|
|
766
|
+
1234,
|
|
767
|
+
{},
|
|
768
|
+
async () => {
|
|
769
|
+
throw new Error('callback failed');
|
|
770
|
+
},
|
|
771
|
+
f.dependencies,
|
|
772
|
+
),
|
|
773
|
+
/callback failed/,
|
|
774
|
+
);
|
|
775
|
+
assert.deepEqual(f.events, ['start', 'ready', 'cooperative-stop']);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test('POSIX teardown preserves the existing stop path', async () => {
|
|
779
|
+
const f = shutdownFixture();
|
|
780
|
+
await withIsolatedVona(1234, {}, async () => {}, { ...f.dependencies, platform: 'darwin' });
|
|
781
|
+
assert.deepEqual(f.events, ['start', 'ready', 'force-stop']);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test('shutdown HTTP request uses distinct nonce-bound domains and rejects invalid acknowledgments or redirects', async () => {
|
|
785
|
+
const key = 'a'.repeat(64);
|
|
786
|
+
const runId = '20261014010203-a1b2c3d4';
|
|
787
|
+
let mode = 'ok';
|
|
788
|
+
let requests = 0;
|
|
789
|
+
const listener = createServer((req, res) => {
|
|
790
|
+
requests++;
|
|
791
|
+
assert.equal(req.method, 'POST');
|
|
792
|
+
assert.equal(req.url, '/health/test-stop');
|
|
793
|
+
const nonce = req.headers['x-vona-test-stop-nonce'];
|
|
794
|
+
assert.match(String(nonce), /^[a-f0-9]{32}$/);
|
|
795
|
+
const payload = `${runId}\nPOST\n/health/test-stop\n${nonce}`;
|
|
796
|
+
const sign = (value: string) => createHmac('sha256', key).update(value).digest('hex');
|
|
797
|
+
assert.equal(
|
|
798
|
+
req.headers['x-vona-test-stop-authorization'],
|
|
799
|
+
sign(`vona-test-stop-request-v1\n${payload}`),
|
|
800
|
+
);
|
|
801
|
+
assert.equal(JSON.stringify(req.headers).includes(key), false);
|
|
802
|
+
if (mode === 'timeout') return;
|
|
803
|
+
if (mode === 'redirect') {
|
|
804
|
+
res.writeHead(307, { location: '/health/test-stop' });
|
|
805
|
+
res.end();
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const proof =
|
|
809
|
+
mode === 'ready-proof' ? sign(String(nonce)) : sign(`vona-test-stop-ack-v1\n${payload}`);
|
|
810
|
+
res.writeHead(mode === 'refuse' ? 403 : 200, {
|
|
811
|
+
'x-vona-test-stop-proof': mode === 'bad-proof' ? 'f'.repeat(64) : proof,
|
|
812
|
+
});
|
|
813
|
+
res.end(JSON.stringify({ status: mode === 'bad-body' ? 'ok' : 'stopping' }));
|
|
814
|
+
});
|
|
815
|
+
await new Promise<void>(resolve => listener.listen(0, '127.0.0.1', resolve));
|
|
816
|
+
const address = listener.address();
|
|
817
|
+
assert.ok(address && typeof address !== 'string');
|
|
818
|
+
try {
|
|
819
|
+
await requestIsolatedVonaStop(address.port, runId, key);
|
|
820
|
+
for (mode of ['bad-proof', 'ready-proof', 'refuse', 'bad-body', 'redirect', 'timeout']) {
|
|
821
|
+
const before = requests;
|
|
822
|
+
await assert.rejects(requestIsolatedVonaStop(address.port, runId, key, 40));
|
|
823
|
+
assert.equal(requests, before + 1);
|
|
824
|
+
}
|
|
825
|
+
} finally {
|
|
826
|
+
listener.closeAllConnections();
|
|
827
|
+
await new Promise<void>((resolve, reject) =>
|
|
828
|
+
listener.close(error => (error ? reject(error) : resolve())),
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test(
|
|
834
|
+
'cooperative worker exit retains native guardian full drainage proof (Windows Job on Windows)',
|
|
835
|
+
{ timeout: 30_000 },
|
|
836
|
+
async () => {
|
|
837
|
+
const root = mkdtempSync(join(realpathSync(tmpdir()), 'cabloy cooperative worker '));
|
|
838
|
+
const marker = join(root, 'closed');
|
|
839
|
+
const descendant = join(root, 'descendant');
|
|
840
|
+
const port = await allocateTestPort();
|
|
841
|
+
const workerUrl = new URL(
|
|
842
|
+
'../vona/packages-vona/vona-core/src/lib/framework/processWorker.ts',
|
|
843
|
+
import.meta.url,
|
|
844
|
+
).href;
|
|
845
|
+
const source = `
|
|
846
|
+
import { createServer } from 'node:http';
|
|
847
|
+
import { createHmac } from 'node:crypto';
|
|
848
|
+
import { spawn } from 'node:child_process';
|
|
849
|
+
import { writeFileSync } from 'node:fs';
|
|
850
|
+
import { registerHooks } from 'node:module';
|
|
851
|
+
const workerUrl = ${JSON.stringify(workerUrl)};
|
|
852
|
+
globalThis.closeMock = async () => {
|
|
853
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
854
|
+
writeFileSync(${JSON.stringify(marker)}, 'closed');
|
|
855
|
+
};
|
|
856
|
+
registerHooks({ resolve(specifier, context, nextResolve) {
|
|
857
|
+
if (context.parentURL === workerUrl) {
|
|
858
|
+
const source = {
|
|
859
|
+
'@cabloy/utils': 'export async function catchError(fn) { try { return [await fn()]; } catch (error) { return [undefined, error]; } } export function catchErrorSync(fn) { try { return [fn()]; } catch (error) { return [undefined, error]; } }',
|
|
860
|
+
'./useApp.ts': 'export const closeApp = globalThis.closeMock; export function useApp() {}',
|
|
861
|
+
}[specifier];
|
|
862
|
+
if (source) return { url: 'data:text/javascript,' + encodeURIComponent(source), shortCircuit: true };
|
|
863
|
+
}
|
|
864
|
+
return nextResolve(specifier, context);
|
|
865
|
+
} });
|
|
866
|
+
const { closeProcessWorker } = await import(workerUrl);
|
|
867
|
+
const sign = payload => createHmac('sha256', process.env.VONA_TEST_READY_KEY).update(payload).digest('hex');
|
|
868
|
+
const listener = createServer((req, res) => {
|
|
869
|
+
if (req.url === '/health/ready') {
|
|
870
|
+
res.writeHead(200, { 'x-vona-test-proof': sign(String(req.headers['x-vona-test-challenge'])) });
|
|
871
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const payload = process.env.VONA_TEST_RUN_ID + '\\nPOST\\n/health/test-stop\\n' + req.headers['x-vona-test-stop-nonce'];
|
|
875
|
+
if (req.method !== 'POST' || req.url !== '/health/test-stop' || req.headers['x-vona-test-stop-authorization'] !== sign('vona-test-stop-request-v1\\n' + payload)) {
|
|
876
|
+
res.writeHead(403); res.end(); return;
|
|
877
|
+
}
|
|
878
|
+
const child = spawn(process.execPath, ['-e', 'setTimeout(() => require("node:fs").writeFileSync(process.argv[1], "done"), 300)', ${JSON.stringify(descendant)}], { stdio: 'ignore', detached: process.platform === 'win32' });
|
|
879
|
+
child.unref();
|
|
880
|
+
res.writeHead(200, { 'x-vona-test-stop-proof': sign('vona-test-stop-ack-v1\\n' + payload) });
|
|
881
|
+
res.once('finish', () => void closeProcessWorker());
|
|
882
|
+
res.end(JSON.stringify({ status: 'stopping' }));
|
|
883
|
+
});
|
|
884
|
+
listener.listen(${port}, '127.0.0.1');
|
|
885
|
+
`;
|
|
886
|
+
try {
|
|
887
|
+
await withIsolatedVona(
|
|
888
|
+
port,
|
|
889
|
+
{ ...process.env, META_MODE: 'test', VONA_TEST_RUN_ID: '20261014010203-a1b2c3d4' },
|
|
890
|
+
async () => {},
|
|
891
|
+
{
|
|
892
|
+
platform: 'win32',
|
|
893
|
+
start: (_command, _args, cwd, env) =>
|
|
894
|
+
startOwnedProcess(process.execPath, ['--input-type=module', '-e', source], cwd, env),
|
|
895
|
+
ready: waitForReady,
|
|
896
|
+
},
|
|
897
|
+
);
|
|
898
|
+
assert.equal(readFileSync(marker, 'utf8'), 'closed');
|
|
899
|
+
assert.equal(readFileSync(descendant, 'utf8'), 'done');
|
|
900
|
+
} finally {
|
|
901
|
+
rmSync(root, { recursive: true, force: true });
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
);
|