cabloy 5.1.203 → 5.1.205
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/.gitignore +2 -0
- package/CHANGELOG.md +21 -0
- package/lint-staged.config.mjs +2 -0
- package/oxfmt.config.ts +2 -0
- package/oxlint.config.ts +2 -0
- package/package.json +6 -5
- package/repo-agent-governance/managed-assets.json +9 -9
- package/repo-agent-governance/policies/repository.md +5 -2
- package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +2 -2
- package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -0
- package/repo-docs/backend/migration-and-changes.md +13 -12
- package/repo-docs/backend/orm-select-guide.md +6 -0
- package/repo-docs/backend/runtime-and-flavors.md +2 -1
- package/repo-docs/backend/transaction-guide.md +12 -0
- package/repo-docs/backend/unit-testing.md +12 -8
- package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/cover-en-v1.png +0 -0
- package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/images/completion-summary.jpg +0 -0
- package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/images/multitasking-draft.jpg +0 -0
- package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/images/ssr-refresh.jpg +0 -0
- package/repo-docs/blogs/ai-coding-one-prompt-crud-ssr-multitasking/index.md +216 -0
- package/repo-docs/blogs/index.md +15 -0
- package/repo-docs/frontend/scripts.md +1 -1
- package/repo-docs/fullstack/parallel-worktree-environment.md +6 -6
- package/repo-docs/fullstack/ssr-entry-modes.md +19 -0
- package/repo-docs/index.md +2 -0
- package/repo-docs/reference/repo-scripts.md +22 -1
- package/repo-e2e/config/playwright.config.ts +32 -14
- package/repo-e2e/docs/specs/blogs-index.spec.ts +23 -6
- package/repo-e2e/scripts/e2e.test.ts +39 -0
- package/repo-e2e/scripts/e2e.ts +19 -1
- package/repo-e2e/scripts/managedTest.test.ts +389 -0
- package/repo-e2e/scripts/managedTest.ts +210 -0
- package/repo-e2e/scripts/managedTestImports.test.ts +74 -0
- package/repo-e2e/scripts/managedTestImports.ts +110 -0
- package/repo-e2e/scripts/runE2e.ts +12 -1
- package/repo-e2e/scripts/startE2eVona.ts +37 -5
- package/repo-e2e/specs/a-commerce.spec.ts +1 -1
- package/repo-e2e/specs/cabloy-basic.spec.ts +1 -1
- package/repo-e2e/specs/controller-boundary.spec.ts +1 -1
- package/repo-e2e/specs/home-user-account.spec.ts +2 -1
- package/repo-e2e/specs/markdown.spec.ts +1 -1
- package/repo-e2e/specs/routed-dialog.spec.ts +1 -1
- package/scripts/runIsolatedTest.test.ts +426 -0
- package/scripts/runIsolatedTest.ts +454 -0
- package/scripts/testClaimCleanup.test.ts +346 -0
- package/scripts/testClaimCleanup.ts +375 -0
- package/scripts/testClaimProcesses.test.ts +106 -0
- package/scripts/testClaimProcesses.ts +129 -0
- package/scripts/testProcessGuardian.ts +163 -0
- package/scripts/upgrade.ts +1 -16
- package/vona/oxfmt.config.ts +2 -0
- package/vona/oxlint.config.ts +2 -0
- package/vona/package.original.json +2 -2
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli/src/play.test.ts +37 -0
- package/vona/packages-cli/cli/src/play.ts +23 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dbReset.ts +16 -7
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +13 -7
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.play.ts +5 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.test.ts +12 -7
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/configUtils.ts +21 -2
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateVonaMeta.ts +10 -0
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/resolveTestCleanupConfig.test.ts +153 -0
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/resolveTestCleanupConfig.ts +141 -0
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupAdapters.test.ts +148 -0
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupAdapters.ts +127 -0
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupProcess.test.ts +47 -0
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/testCleanupProcess.ts +139 -0
- package/vona/packages-cli/cli-set-api/src/lib/utils.ts +2 -3
- package/vona/packages-cli/cli-set-api/templates/config/_tsconfig_project.json +1 -0
- package/vona/packages-cli/cli-set-api/templates/config/_tsconfig_source.json +1 -0
- package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +18 -0
- package/vona/packages-cli/cli-set-api/toolsIsolate/dbReset.ts +15 -2
- package/vona/packages-cli/cli-set-api/tsconfig.build.json +2 -1
- package/vona/packages-utils/babel-plugin-vona-bean-module/package.json +1 -1
- package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.test.ts +16 -0
- package/vona/packages-utils/babel-plugin-vona-bean-module/src/index.ts +3 -2
- 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/addon/sqlite3.ts +1 -1
- package/vona/packages-vona/vona-core/src/lib/core/config.ts +44 -6
- package/vona/packages-vona/vona-core/src/lib/core/configCleanup.test.ts +40 -0
- package/vona/packages-vona/vona-core/src/lib/core/index.ts +3 -0
- package/vona/packages-vona/vona-core/src/lib/core/isolationGuard.test.ts +301 -0
- package/vona/packages-vona/vona-core/src/lib/core/isolationGuard.ts +312 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +577 -0
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +985 -0
- package/vona/packages-vona/vona-core/src/lib/core/testIsolation.ts +129 -0
- package/vona/packages-vona/vona-core/src/lib/framework/createApp.ts +2 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.ts +4 -0
- package/vona/packages-vona/vona-core/src/lib/framework/useApp.ts +5 -1
- package/vona/packages-vona/vona-core/src/types/application/app.ts +2 -0
- package/vona/packages-vona/vona-core/src/types/utils/env.ts +1 -0
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +40 -40
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentIndexes.test.ts +73 -10
- 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-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +15 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.test.ts +24 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactIsolatedReadyHeader.ts +11 -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 +15 -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/extend/schemaBuilder.ts +11 -4
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/service/databaseClient_.ts +6 -1
- package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/bean/databaseDialect.betterSqlite3.ts +41 -1
- package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/bean/databaseDialect.mysql.ts +4 -1
- package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/bean/databaseDialect.pg.ts +2 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-redis/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-redis/src/lib/redis.ts +8 -0
- package/vona/src/suite-vendor/a-vona/modules/a-redis/src/service/redis.ts +23 -6
- package/vona/src/suite-vendor/a-vona/modules/a-redis/src/service/redisClient_.ts +18 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +43 -15
- package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/lib/databaseTestName.ts +3 -37
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/database.ts +93 -15
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/vona/tsconfig.json +1 -0
- package/scripts/initTestData.ts +0 -25
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import '@cabloy/json5';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
symlinkSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { hostname, tmpdir } from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
assertTestClaimDatabaseClient,
|
|
19
|
+
assertTestClaimRedisClient,
|
|
20
|
+
buildTestClaimResourceInventory,
|
|
21
|
+
createTestClaim,
|
|
22
|
+
getTestClaimCreatedAt,
|
|
23
|
+
getTestClaimPaths,
|
|
24
|
+
hasTestClaimTransport,
|
|
25
|
+
loadTestClaim,
|
|
26
|
+
mergeTestClaimRedisOptions,
|
|
27
|
+
normalizeTestClaimDatabaseEndpoint,
|
|
28
|
+
normalizeTestClaimRedisEndpoint,
|
|
29
|
+
registerTestClaimDatabaseCreation,
|
|
30
|
+
registerTestClaimResourceInventory,
|
|
31
|
+
sendTestClaimEvent,
|
|
32
|
+
startTestClaimBroker,
|
|
33
|
+
validateTestClaim,
|
|
34
|
+
writeTestClaim,
|
|
35
|
+
} from './testClaim.ts';
|
|
36
|
+
import { getTestAppName } from './testIsolation.ts';
|
|
37
|
+
|
|
38
|
+
const runId = '20261013010203-a1b2c3d4';
|
|
39
|
+
const owner = { pid: process.pid, startTime: 'test-process-start', host: hostname() };
|
|
40
|
+
function fixture() {
|
|
41
|
+
const root = mkdtempSync(path.join(realpathSync(tmpdir()), 'vona-test-claim-'));
|
|
42
|
+
const projectPath = path.join(root, 'vona');
|
|
43
|
+
mkdirSync(projectPath);
|
|
44
|
+
return { root, projectPath, dispose: () => rmSync(root, { recursive: true, force: true }) };
|
|
45
|
+
}
|
|
46
|
+
function appFixture(projectPath: string) {
|
|
47
|
+
return {
|
|
48
|
+
projectPath,
|
|
49
|
+
name: getTestAppName(runId),
|
|
50
|
+
meta: { env: { META_MODE: 'test', VONA_TEST_RUN_ID: runId } },
|
|
51
|
+
config: {
|
|
52
|
+
database: {
|
|
53
|
+
defaultClient: 'pg',
|
|
54
|
+
base: {},
|
|
55
|
+
clients: {
|
|
56
|
+
pg: {
|
|
57
|
+
client: 'pg',
|
|
58
|
+
connection: {
|
|
59
|
+
host: '127.0.0.1',
|
|
60
|
+
port: 5432,
|
|
61
|
+
database: 'postgres',
|
|
62
|
+
password: 'SECRET_DB',
|
|
63
|
+
user: 'owner',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
instance: { instances: { isolated: { isolate: true, isolateClient: 'pg' } } },
|
|
69
|
+
redis: {
|
|
70
|
+
base: { host: '127.0.0.1', port: 6379, password: 'SECRET_REDIS' },
|
|
71
|
+
clients: {
|
|
72
|
+
cache: { keyPrefix: true, db: 2 },
|
|
73
|
+
queue: {},
|
|
74
|
+
redlock: {},
|
|
75
|
+
redlockOther: { port: 6380 },
|
|
76
|
+
broadcast: {},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function transportEnv(env: Record<string, string | undefined>) {
|
|
83
|
+
return { ...env, META_MODE: 'test', VONA_TEST_RUN_ID: runId };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test('exclusive central claim has canonical immutable UTC creation time and private modes', async () => {
|
|
87
|
+
const f = fixture();
|
|
88
|
+
try {
|
|
89
|
+
const { claimPath, manifest } = await createTestClaim(f.projectPath, runId, 'unit', owner);
|
|
90
|
+
assert.equal(claimPath, path.join(f.projectPath, '.vona-test-claim', `${runId}.json`));
|
|
91
|
+
assert.equal(manifest.createdAt, '2026-10-13T01:02:03.000Z');
|
|
92
|
+
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);
|
|
95
|
+
await assert.rejects(createTestClaim(f.projectPath, runId, 'unit', owner), { code: 'EEXIST' });
|
|
96
|
+
await createTestClaim(f.projectPath, '20261013010203-deadbeef', 'e2e', owner);
|
|
97
|
+
assert.deepEqual(
|
|
98
|
+
manifest.locals.map(item => item.kind),
|
|
99
|
+
['generated', 'data'],
|
|
100
|
+
);
|
|
101
|
+
assert.deepEqual(await loadTestClaim(claimPath), manifest);
|
|
102
|
+
for (const id of [
|
|
103
|
+
'20260230010203-a1b2c3d4',
|
|
104
|
+
'20261013240203-a1b2c3d4',
|
|
105
|
+
'20261013010203-A1B2C3D4',
|
|
106
|
+
])
|
|
107
|
+
assert.throws(() => getTestClaimCreatedAt(id));
|
|
108
|
+
} finally {
|
|
109
|
+
f.dispose();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('claim creation rejects symlink paths and permissive shared directories', async () => {
|
|
114
|
+
const f = fixture();
|
|
115
|
+
try {
|
|
116
|
+
const directory = getTestClaimPaths(f.projectPath, runId).claimDirectory;
|
|
117
|
+
mkdirSync(path.join(f.root, 'outside'));
|
|
118
|
+
symlinkSync(path.join(f.root, 'outside'), directory);
|
|
119
|
+
await assert.rejects(createTestClaim(f.projectPath, runId, 'unit', owner), /Unsafe/);
|
|
120
|
+
rmSync(directory, { recursive: true, force: true });
|
|
121
|
+
mkdirSync(directory, { mode: 0o755 });
|
|
122
|
+
await assert.rejects(createTestClaim(f.projectPath, runId, 'unit', owner), /Unsafe/);
|
|
123
|
+
rmSync(directory, { recursive: true, force: true });
|
|
124
|
+
const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
|
|
125
|
+
chmodSync(claimPath, 0o644);
|
|
126
|
+
await assert.rejects(loadTestClaim(claimPath), /Unsafe/);
|
|
127
|
+
} finally {
|
|
128
|
+
f.dispose();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('manifest validation rejects filename drift, secret snapshots and arbitrary resources', async () => {
|
|
133
|
+
const f = fixture();
|
|
134
|
+
try {
|
|
135
|
+
const { manifest, claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
|
|
136
|
+
assert.throws(() => validateTestClaim(manifest, `${claimPath}.other`));
|
|
137
|
+
assert.throws(() => validateTestClaim({ ...manifest, password: 'secret' }));
|
|
138
|
+
assert.throws(() => validateTestClaim({ ...manifest, createdAt: '2026-10-14T01:02:03.000Z' }));
|
|
139
|
+
assert.throws(() =>
|
|
140
|
+
validateTestClaim({
|
|
141
|
+
...manifest,
|
|
142
|
+
locals: [{ ...manifest.locals[0], path: f.root }, manifest.locals[1]],
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
assert.throws(() =>
|
|
146
|
+
validateTestClaim({
|
|
147
|
+
...manifest,
|
|
148
|
+
processes: [{ id: 'x', role: 'browser', stage: 'registered', pid: 1, pgid: 1 }],
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
assert.throws(() =>
|
|
152
|
+
validateTestClaim({
|
|
153
|
+
...manifest,
|
|
154
|
+
processes: [{ id: 'x', role: 'browser', stage: 'pending', profilePath: f.root }],
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
const inventory = buildTestClaimResourceInventory(appFixture(f.projectPath));
|
|
158
|
+
manifest.databases = inventory.databases.map(item => ({ ...item, creations: [] }));
|
|
159
|
+
manifest.redis = inventory.redis;
|
|
160
|
+
assert.throws(() =>
|
|
161
|
+
validateTestClaim({ ...manifest, redis: [{ ...manifest.redis[0], prefix: 'cache_' }] }),
|
|
162
|
+
);
|
|
163
|
+
assert.throws(() =>
|
|
164
|
+
validateTestClaim({
|
|
165
|
+
...manifest,
|
|
166
|
+
databases: [
|
|
167
|
+
{ ...manifest.databases[0], creations: [{ name: 'development', stage: 'intent' }] },
|
|
168
|
+
],
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
} finally {
|
|
172
|
+
f.dispose();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('normalization whitelists endpoint identity and exact physical Redis prefixes', () => {
|
|
177
|
+
const app = appFixture('/project/vona');
|
|
178
|
+
const inventory = buildTestClaimResourceInventory(app);
|
|
179
|
+
assert.equal(inventory.databases.length, 2);
|
|
180
|
+
assert.equal(inventory.databases[1].scope, 'isolate-isolated');
|
|
181
|
+
assert.equal(inventory.redis.length, 4);
|
|
182
|
+
assert.equal(
|
|
183
|
+
inventory.redis.find(item => item.clientName === 'queue')!.prefix,
|
|
184
|
+
`bull_${app.name}_local:queue:`,
|
|
185
|
+
);
|
|
186
|
+
assert.equal(
|
|
187
|
+
inventory.redis.find(item => item.clientName === 'redlockOther')!.prefix,
|
|
188
|
+
`redlock_${app.name}_local:`,
|
|
189
|
+
);
|
|
190
|
+
assert.equal(
|
|
191
|
+
inventory.redis.find(item => item.clientName === 'redlockOther')!.endpoint.port,
|
|
192
|
+
6380,
|
|
193
|
+
);
|
|
194
|
+
assert.equal(JSON.stringify(inventory).includes('SECRET'), false);
|
|
195
|
+
assert.equal(JSON.stringify(inventory).includes('password'), false);
|
|
196
|
+
const options = mergeTestClaimRedisOptions(
|
|
197
|
+
{ host: 'base', port: 6379 },
|
|
198
|
+
{ host: undefined, port: 6380 },
|
|
199
|
+
);
|
|
200
|
+
assert.deepEqual(normalizeTestClaimRedisEndpoint(options), {
|
|
201
|
+
transport: 'tcp',
|
|
202
|
+
host: 'base',
|
|
203
|
+
port: 6380,
|
|
204
|
+
tls: false,
|
|
205
|
+
db: 0,
|
|
206
|
+
});
|
|
207
|
+
assert.deepEqual(normalizeTestClaimRedisEndpoint({ path: '/run/redis.sock', db: 2 }), {
|
|
208
|
+
transport: 'socket',
|
|
209
|
+
socketPath: '/run/redis.sock',
|
|
210
|
+
db: 2,
|
|
211
|
+
tls: false,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('normalization rejects unresolved or custom targets and captures pg socket port', () => {
|
|
216
|
+
assert.throws(
|
|
217
|
+
() =>
|
|
218
|
+
normalizeTestClaimDatabaseEndpoint(
|
|
219
|
+
{ client: 'pg', connection: { database: 'postgres' } },
|
|
220
|
+
'/vona',
|
|
221
|
+
runId,
|
|
222
|
+
),
|
|
223
|
+
/explicit/,
|
|
224
|
+
);
|
|
225
|
+
assert.deepEqual(
|
|
226
|
+
normalizeTestClaimDatabaseEndpoint(
|
|
227
|
+
{ client: 'pg', connection: { host: '/run/postgresql', port: 5433 } },
|
|
228
|
+
'/vona',
|
|
229
|
+
runId,
|
|
230
|
+
),
|
|
231
|
+
{
|
|
232
|
+
dialect: 'pg',
|
|
233
|
+
transport: 'socket',
|
|
234
|
+
socketPath: '/run/postgresql',
|
|
235
|
+
port: 5433,
|
|
236
|
+
tls: false,
|
|
237
|
+
adminDatabase: 'postgres',
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
assert.throws(() =>
|
|
241
|
+
normalizeTestClaimDatabaseEndpoint(
|
|
242
|
+
{ client: 'pg', connection: { host: '/run/postgresql' } },
|
|
243
|
+
'/vona',
|
|
244
|
+
runId,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
for (const extra of [{ connectionString: 'postgres://secret' }, { stream: () => {} }]) {
|
|
248
|
+
assert.throws(() =>
|
|
249
|
+
normalizeTestClaimDatabaseEndpoint(
|
|
250
|
+
{ client: 'pg', connection: { host: 'localhost', port: 5432, ...extra } },
|
|
251
|
+
'/vona',
|
|
252
|
+
runId,
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
for (const options of [
|
|
257
|
+
{ Connector: class {} },
|
|
258
|
+
{ nodes: [{}] },
|
|
259
|
+
{ host: 'redis://password@host' },
|
|
260
|
+
{ db: -1 },
|
|
261
|
+
{ port: 0 },
|
|
262
|
+
])
|
|
263
|
+
assert.throws(() => normalizeTestClaimRedisEndpoint(options));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('broker serializes simultaneous updates and acknowledges durable intent before allocation', async () => {
|
|
267
|
+
const f = fixture();
|
|
268
|
+
let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
|
|
269
|
+
try {
|
|
270
|
+
const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
|
|
271
|
+
broker = await startTestClaimBroker(claimPath);
|
|
272
|
+
const env = transportEnv(broker.env);
|
|
273
|
+
const inventory = buildTestClaimResourceInventory(appFixture(f.projectPath));
|
|
274
|
+
await sendTestClaimEvent(env, { type: 'inventory', ...inventory });
|
|
275
|
+
const db = inventory.databases[0];
|
|
276
|
+
await Promise.all(
|
|
277
|
+
Array.from({ length: 15 }, (_, i) =>
|
|
278
|
+
sendTestClaimEvent(env, {
|
|
279
|
+
type: 'database',
|
|
280
|
+
database: db,
|
|
281
|
+
name: `${db.prefix}20261013-0102${String(i).padStart(2, '0')}`,
|
|
282
|
+
stage: 'intent',
|
|
283
|
+
}),
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
const manifest = await loadTestClaim(claimPath);
|
|
287
|
+
assert.equal(manifest.databases[0].creations.length, 15);
|
|
288
|
+
assert.equal(readFileSync(claimPath, 'utf8').includes(broker.env.VONA_TEST_CLAIM_TOKEN), false);
|
|
289
|
+
assert.equal(readFileSync(claimPath, 'utf8').includes('SECRET'), false);
|
|
290
|
+
await assert.rejects(
|
|
291
|
+
sendTestClaimEvent(
|
|
292
|
+
{ ...env, VONA_TEST_CLAIM_TOKEN: '0'.repeat(64) },
|
|
293
|
+
{ type: 'status', status: 'failed' },
|
|
294
|
+
),
|
|
295
|
+
/registration failed/,
|
|
296
|
+
);
|
|
297
|
+
await assert.rejects(
|
|
298
|
+
sendTestClaimEvent(env, {
|
|
299
|
+
type: 'database',
|
|
300
|
+
database: db,
|
|
301
|
+
name: 'development',
|
|
302
|
+
stage: 'intent',
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
assert.equal((await loadTestClaim(claimPath)).databases[0].creations.length, 15);
|
|
306
|
+
} finally {
|
|
307
|
+
await broker?.close();
|
|
308
|
+
f.dispose();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('database confirmations and process registration cannot precede allocation intent', async () => {
|
|
313
|
+
const f = fixture();
|
|
314
|
+
let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
|
|
315
|
+
try {
|
|
316
|
+
const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
|
|
317
|
+
broker = await startTestClaimBroker(claimPath);
|
|
318
|
+
const inventory = buildTestClaimResourceInventory(appFixture(f.projectPath));
|
|
319
|
+
await broker.update({ type: 'inventory', ...inventory });
|
|
320
|
+
const db = inventory.databases[0];
|
|
321
|
+
const name = `${db.prefix}20261013-010203`;
|
|
322
|
+
await assert.rejects(
|
|
323
|
+
broker.update({ type: 'database', database: db, name, stage: 'created' }),
|
|
324
|
+
/write-ahead/,
|
|
325
|
+
);
|
|
326
|
+
await broker.update({ type: 'database', database: db, name, stage: 'intent' });
|
|
327
|
+
await broker.update({ type: 'database', database: db, name, stage: 'created' });
|
|
328
|
+
await assert.rejects(
|
|
329
|
+
broker.update({ type: 'database', database: db, name, stage: 'intent' }),
|
|
330
|
+
/regression/,
|
|
331
|
+
);
|
|
332
|
+
const proc = {
|
|
333
|
+
id: 'browser-uuid',
|
|
334
|
+
role: 'browser',
|
|
335
|
+
pid: 42,
|
|
336
|
+
pgid: 42,
|
|
337
|
+
startTime: 'start',
|
|
338
|
+
host: hostname(),
|
|
339
|
+
};
|
|
340
|
+
await assert.rejects(
|
|
341
|
+
broker.update({ type: 'process', process: { ...proc, stage: 'registered' } }),
|
|
342
|
+
/write-ahead/,
|
|
343
|
+
);
|
|
344
|
+
const tmp = path.join(getTestClaimPaths(f.projectPath, runId).dataRoot, 'tmp');
|
|
345
|
+
await broker.update({
|
|
346
|
+
type: 'process',
|
|
347
|
+
process: { id: proc.id, role: proc.role, stage: 'pending', profilePath: tmp },
|
|
348
|
+
});
|
|
349
|
+
await assert.rejects(
|
|
350
|
+
broker.update({ type: 'process', process: { ...proc, stage: 'drained', profilePath: tmp } }),
|
|
351
|
+
/Pending/,
|
|
352
|
+
);
|
|
353
|
+
await broker.update({
|
|
354
|
+
type: 'process',
|
|
355
|
+
process: { ...proc, stage: 'registered', profilePath: path.join(tmp, 'browser-profile') },
|
|
356
|
+
});
|
|
357
|
+
await assert.rejects(
|
|
358
|
+
broker.update({ type: 'process', process: { ...proc, pid: 43, stage: 'drained' } }),
|
|
359
|
+
/identity/,
|
|
360
|
+
);
|
|
361
|
+
await broker.update({
|
|
362
|
+
type: 'process',
|
|
363
|
+
process: { id: proc.id, role: proc.role, stage: 'drained' },
|
|
364
|
+
});
|
|
365
|
+
assert.equal((await loadTestClaim(claimPath)).processes[0].stage, 'drained');
|
|
366
|
+
} finally {
|
|
367
|
+
await broker?.close();
|
|
368
|
+
f.dispose();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('cleanup editor persists partial progress but may not replace claim identity', async () => {
|
|
373
|
+
const f = fixture();
|
|
374
|
+
try {
|
|
375
|
+
const { claimPath, manifest } = await createTestClaim(f.projectPath, runId, 'unit', owner);
|
|
376
|
+
manifest.status = 'cleaning';
|
|
377
|
+
manifest.cleanup.processes = true;
|
|
378
|
+
manifest.locals[0].stage = 'released';
|
|
379
|
+
await writeTestClaim(claimPath, manifest);
|
|
380
|
+
assert.deepEqual(await loadTestClaim(claimPath), manifest);
|
|
381
|
+
await assert.rejects(
|
|
382
|
+
writeTestClaim(claimPath, { ...manifest, owner: { ...owner, pid: 1 } }),
|
|
383
|
+
/immutable/,
|
|
384
|
+
);
|
|
385
|
+
} finally {
|
|
386
|
+
f.dispose();
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('manual isolation remains transport-free, partial and non-test transport fail closed', async () => {
|
|
391
|
+
assert.equal(hasTestClaimTransport({}), false);
|
|
392
|
+
await sendTestClaimEvent({}, { type: 'status', status: 'running' });
|
|
393
|
+
assert.throws(
|
|
394
|
+
() => hasTestClaimTransport({ VONA_TEST_CLAIM_TOKEN: '0'.repeat(64) }),
|
|
395
|
+
/Incomplete/,
|
|
396
|
+
);
|
|
397
|
+
assert.throws(
|
|
398
|
+
() =>
|
|
399
|
+
hasTestClaimTransport({
|
|
400
|
+
VONA_TEST_CLAIM_URL: 'http://127.0.0.1:1/event',
|
|
401
|
+
VONA_TEST_CLAIM_TOKEN: '0'.repeat(64),
|
|
402
|
+
}),
|
|
403
|
+
/identity/,
|
|
404
|
+
);
|
|
405
|
+
assert.throws(
|
|
406
|
+
() =>
|
|
407
|
+
hasTestClaimTransport({
|
|
408
|
+
...transportEnv({
|
|
409
|
+
VONA_TEST_CLAIM_URL: 'http://remote:1/event',
|
|
410
|
+
VONA_TEST_CLAIM_TOKEN: '0'.repeat(64),
|
|
411
|
+
}),
|
|
412
|
+
}),
|
|
413
|
+
/Invalid/,
|
|
414
|
+
);
|
|
415
|
+
await assert.rejects(
|
|
416
|
+
sendTestClaimEvent(
|
|
417
|
+
transportEnv({
|
|
418
|
+
VONA_TEST_CLAIM_URL: 'http://127.0.0.1:1/event',
|
|
419
|
+
VONA_TEST_CLAIM_TOKEN: '0'.repeat(64),
|
|
420
|
+
}),
|
|
421
|
+
{ type: 'status', status: 'running' },
|
|
422
|
+
),
|
|
423
|
+
/registration failed/,
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test(
|
|
428
|
+
'managed resource gates reject drift and allocation before broker acknowledgement',
|
|
429
|
+
{ concurrency: false },
|
|
430
|
+
async () => {
|
|
431
|
+
const f = fixture();
|
|
432
|
+
const old = {
|
|
433
|
+
META_MODE: process.env.META_MODE,
|
|
434
|
+
VONA_TEST_RUN_ID: process.env.VONA_TEST_RUN_ID,
|
|
435
|
+
VONA_TEST_CLAIM_URL: process.env.VONA_TEST_CLAIM_URL,
|
|
436
|
+
VONA_TEST_CLAIM_TOKEN: process.env.VONA_TEST_CLAIM_TOKEN,
|
|
437
|
+
};
|
|
438
|
+
let broker: Awaited<ReturnType<typeof startTestClaimBroker>> | undefined;
|
|
439
|
+
try {
|
|
440
|
+
const { claimPath } = await createTestClaim(f.projectPath, runId, 'unit', owner);
|
|
441
|
+
broker = await startTestClaimBroker(claimPath);
|
|
442
|
+
Object.assign(process.env, transportEnv(broker.env));
|
|
443
|
+
const app = appFixture(f.projectPath);
|
|
444
|
+
const client = app.config.database.clients.pg;
|
|
445
|
+
assert.throws(() => assertTestClaimDatabaseClient(app, 'pg', client), /Unregistered/);
|
|
446
|
+
await registerTestClaimResourceInventory(app);
|
|
447
|
+
assertTestClaimDatabaseClient(app, 'pg', client);
|
|
448
|
+
assert.throws(() => assertTestClaimDatabaseClient(app, 'dynamic_admin', client), /endpoint/);
|
|
449
|
+
assert.throws(
|
|
450
|
+
() =>
|
|
451
|
+
assertTestClaimDatabaseClient(app, 'pg', {
|
|
452
|
+
...client,
|
|
453
|
+
connection: { ...client.connection, port: 5433 },
|
|
454
|
+
}),
|
|
455
|
+
/Unregistered/,
|
|
456
|
+
);
|
|
457
|
+
const plan = buildTestClaimResourceInventory(app).databases[0];
|
|
458
|
+
const name = `${plan.prefix}20261013-010203`;
|
|
459
|
+
const allocatedClient = { ...client, connection: { ...client.connection, database: name } };
|
|
460
|
+
const aliases = ['dynamic_123', 'index-recovery-test'];
|
|
461
|
+
assert.throws(() => assertTestClaimDatabaseClient(app, 'pg', allocatedClient), /allocation/);
|
|
462
|
+
for (const alias of aliases) {
|
|
463
|
+
assert.throws(
|
|
464
|
+
() => assertTestClaimDatabaseClient(app, alias, allocatedClient),
|
|
465
|
+
/allocation/,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
await registerTestClaimDatabaseCreation(app, client, name, 'intent');
|
|
469
|
+
assertTestClaimDatabaseClient(app, 'pg', allocatedClient);
|
|
470
|
+
for (const alias of aliases) assertTestClaimDatabaseClient(app, alias, allocatedClient);
|
|
471
|
+
assert.equal((await loadTestClaim(claimPath)).databases[0].creations[0].stage, 'intent');
|
|
472
|
+
await registerTestClaimDatabaseCreation(app, client, name, 'created');
|
|
473
|
+
for (const alias of aliases) {
|
|
474
|
+
assertTestClaimDatabaseClient(app, alias, allocatedClient);
|
|
475
|
+
assert.throws(
|
|
476
|
+
() =>
|
|
477
|
+
assertTestClaimDatabaseClient(app, alias, {
|
|
478
|
+
...allocatedClient,
|
|
479
|
+
connection: { ...allocatedClient.connection, port: 5433 },
|
|
480
|
+
}),
|
|
481
|
+
/endpoint/,
|
|
482
|
+
);
|
|
483
|
+
assert.throws(
|
|
484
|
+
() =>
|
|
485
|
+
assertTestClaimDatabaseClient(app, alias, {
|
|
486
|
+
...allocatedClient,
|
|
487
|
+
connection: {
|
|
488
|
+
...allocatedClient.connection,
|
|
489
|
+
database: `${plan.prefix}20261013-010205`,
|
|
490
|
+
},
|
|
491
|
+
}),
|
|
492
|
+
/allocation/,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
const isolatePlan = buildTestClaimResourceInventory(app).databases[1];
|
|
496
|
+
const isolateName = `${isolatePlan.prefix}20261013-010203`;
|
|
497
|
+
await registerTestClaimDatabaseCreation(app, client, isolateName, 'intent');
|
|
498
|
+
assertTestClaimDatabaseClient(app, 'dynamic_isolated', {
|
|
499
|
+
...client,
|
|
500
|
+
connection: { ...client.connection, database: isolateName },
|
|
501
|
+
});
|
|
502
|
+
assert.ok((await loadTestClaim(claimPath)).databases.every(item => item.clientName === 'pg'));
|
|
503
|
+
const sqliteApp = appFixture(f.projectPath);
|
|
504
|
+
const sqliteClient = { client: 'better-sqlite3', connection: { filename: '' } };
|
|
505
|
+
sqliteApp.config.database = {
|
|
506
|
+
...sqliteApp.config.database,
|
|
507
|
+
defaultClient: 'sqlite3',
|
|
508
|
+
clients: { sqlite3: sqliteClient } as any,
|
|
509
|
+
};
|
|
510
|
+
sqliteApp.config.instance.instances = {} as any;
|
|
511
|
+
await registerTestClaimResourceInventory(sqliteApp);
|
|
512
|
+
const sqlitePlan = buildTestClaimResourceInventory(sqliteApp).databases[0];
|
|
513
|
+
const copyName = `${sqlitePlan.prefix}20261013-010206`;
|
|
514
|
+
const copyClient = {
|
|
515
|
+
...sqliteClient,
|
|
516
|
+
connection: {
|
|
517
|
+
filename: path.join(getTestClaimPaths(f.projectPath, runId).sqliteRoot, `${copyName}.db`),
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
assert.throws(
|
|
521
|
+
() => assertTestClaimDatabaseClient(sqliteApp, 'index-recovery-copy', copyClient),
|
|
522
|
+
/allocation/,
|
|
523
|
+
);
|
|
524
|
+
await registerTestClaimDatabaseCreation(sqliteApp, sqliteClient, copyName, 'intent');
|
|
525
|
+
assertTestClaimDatabaseClient(sqliteApp, 'index-recovery-copy', copyClient);
|
|
526
|
+
assert.throws(
|
|
527
|
+
() =>
|
|
528
|
+
assertTestClaimDatabaseClient(sqliteApp, 'index-recovery-copy', {
|
|
529
|
+
...copyClient,
|
|
530
|
+
connection: { filename: `${copyName}.db` },
|
|
531
|
+
}),
|
|
532
|
+
/allocation/,
|
|
533
|
+
);
|
|
534
|
+
assert.throws(
|
|
535
|
+
() => assertTestClaimDatabaseClient(sqliteApp, 'dynamic_admin', sqliteClient),
|
|
536
|
+
/endpoint/,
|
|
537
|
+
);
|
|
538
|
+
await registerTestClaimDatabaseCreation(sqliteApp, sqliteClient, copyName, 'created');
|
|
539
|
+
const manifest = await loadTestClaim(claimPath);
|
|
540
|
+
assert.equal(
|
|
541
|
+
manifest.databases.find(item => item.clientName === 'sqlite3')!.creations[0].stage,
|
|
542
|
+
'created',
|
|
543
|
+
);
|
|
544
|
+
const redis = { host: '127.0.0.1', port: 6379, db: 2, keyPrefix: `cache_${app.name}_local:` };
|
|
545
|
+
assertTestClaimRedisClient(app, 'cache', redis);
|
|
546
|
+
assert.throws(
|
|
547
|
+
() => assertTestClaimRedisClient(app, 'cache', { ...redis, db: 0 }),
|
|
548
|
+
/Unregistered/,
|
|
549
|
+
);
|
|
550
|
+
assert.throws(
|
|
551
|
+
() => assertTestClaimRedisClient(app, 'cache', { ...redis, keyPrefix: 'cache_' }),
|
|
552
|
+
/prefix/,
|
|
553
|
+
);
|
|
554
|
+
await broker.close();
|
|
555
|
+
broker = undefined;
|
|
556
|
+
await assert.rejects(
|
|
557
|
+
registerTestClaimDatabaseCreation(app, client, `${plan.prefix}20261013-010204`, 'intent'),
|
|
558
|
+
/registration failed/,
|
|
559
|
+
);
|
|
560
|
+
assert.throws(
|
|
561
|
+
() =>
|
|
562
|
+
assertTestClaimDatabaseClient(app, 'dynamic_rejected', {
|
|
563
|
+
...client,
|
|
564
|
+
connection: { ...client.connection, database: `${plan.prefix}20261013-010204` },
|
|
565
|
+
}),
|
|
566
|
+
/allocation/,
|
|
567
|
+
);
|
|
568
|
+
} finally {
|
|
569
|
+
for (const [key, value] of Object.entries(old)) {
|
|
570
|
+
if (value === undefined) delete process.env[key];
|
|
571
|
+
else process.env[key] = value;
|
|
572
|
+
}
|
|
573
|
+
await broker?.close();
|
|
574
|
+
f.dispose();
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
);
|