cabloy 5.1.220 → 5.1.222
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/agent-governance.yml +2 -2
- package/.github/workflows/create-cabloy.yml +2 -2
- package/.github/workflows/docs-pages.yml +2 -2
- package/.github/workflows/playwright-e2e.yml +4 -4
- package/.github/workflows/vona-cov-pg.yml +2 -2
- package/.github/workflows/vona-test-crud.yml +2 -2
- package/.github/workflows/vona-test-mysql.yml +2 -2
- package/.github/workflows/vona-test-pg.yml +2 -2
- package/.github/workflows/vona-test-sqlite3.yml +2 -2
- package/.github/workflows/vona-tsc.yml +2 -2
- package/.github/workflows/zova-ui.yml +2 -2
- package/CHANGELOG.md +22 -0
- package/package.json +4 -3
- package/repo-agent-governance/managed-assets.json +15 -15
- package/repo-agent-governance/policies/repository.md +1 -0
- package/repo-agent-governance/skills/cabloy-backend-scaffold/SKILL.md +5 -1
- package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +7 -5
- package/repo-agent-governance/skills/cabloy-frontend-scaffold/SKILL.md +5 -1
- package/repo-agent-governance/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +6 -4
- package/repo-docs/backend/unit-testing.md +5 -1
- package/repo-docs/frontend/openapi-sdk-guide.md +2 -0
- package/repo-docs/frontend/scripts.md +8 -0
- package/repo-docs/reference/repo-scripts.md +5 -1
- package/scripts/generateOpenapiIsolated.ts +18 -1
- package/scripts/runIsolatedTest.test.ts +9 -1
- package/scripts/runIsolatedTest.ts +81 -18
- package/scripts/strictGenerationDiagnostics.test.ts +1009 -0
- package/scripts/strictGenerationDiagnostics.ts +61 -0
- package/vona/package.original.json +3 -2
- package/vona/packages-cli/cabloy-cli/package.json +1 -1
- 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.dev.ts +14 -4
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateEntryFiles.ts +6 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateVonaMeta.ts +2 -10
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/validateGenerationPaths.ts +27 -0
- package/vona/packages-cli/cli-set-api/test/binDevLifecycle.test.ts +1 -1
- package/vona/packages-cli/cli-set-api/test/strictGenerationPrivacy.test.ts +439 -0
- package/vona/packages-utils/process-helper/package.json +1 -1
- package/vona/packages-utils/process-helper/src/index.test.ts +62 -0
- package/vona/packages-utils/process-helper/src/index.ts +9 -4
- 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/completedReadiness.test.ts +558 -0
- package/vona/packages-vona/vona-core/src/lib/core/completedReadiness.ts +389 -0
- package/vona/packages-vona/vona-core/src/lib/core/context.ts +3 -0
- package/vona/packages-vona/vona-core/src/lib/core/index.ts +11 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/appLogger.ts +10 -2
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.test.ts +268 -68
- package/vona/packages-vona/vona-core/src/lib/core/meta.ts +5 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.ts +3 -9
- package/vona/packages-vona/vona-core/src/lib/core/testStrictMode.ts +9 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +10 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.ts +8 -0
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +542 -236
- 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 +124 -4
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +7 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExisting.test.ts +423 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExistingCommit.test.ts +174 -0
- package/vona/src/suite-vendor/a-vona/modules/a-instance/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-instance/src/bean/bean.instance.ts +3 -1
- package/vona/src/suite-vendor/a-vona/modules/a-instance/src/lib/existingReadyAdmission.ts +125 -0
- package/vona/src/suite-vendor/a-vona/modules/a-instance/src/service/instance.ts +72 -25
- package/vona/src/suite-vendor/a-vona/modules/a-instance/test/existingReadyAdmission.test.ts +949 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +61 -59
- package/vona/src/suite-vendor/a-vona/modules/a-mail/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-mail/src/service/mail.ts +5 -0
- 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/index.ts +11 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/lib/protectedSqliteRegistry.ts +356 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +61 -2
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/package.original.json +3 -3
- package/zova/packages-cli/cli/package.json +4 -4
- package/zova/packages-cli/cli-set-front/package.json +2 -2
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencyVersions.ts +22 -1
- package/zova/packages-cli/cli-set-front/test/restDependencies.test.ts +137 -0
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +448 -641
- 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
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHmac } from 'node:crypto';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { registerHooks } from 'node:module';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { finished } from 'node:stream/promises';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import { setImmediate as nextTurn } from 'node:timers/promises';
|
|
9
|
+
import { inspect } from 'node:util';
|
|
10
|
+
|
|
11
|
+
import type { GuardianResult, OwnedProcess } from './runIsolatedTest.ts';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
requiresFreshTestResources,
|
|
15
|
+
TEST_REQUIRE_FRESH_RESOURCES_ENV,
|
|
16
|
+
} from '../vona/packages-vona/vona-core/src/lib/core/testStrictMode.ts';
|
|
17
|
+
import {
|
|
18
|
+
discardNativeOutput,
|
|
19
|
+
guardianStdio,
|
|
20
|
+
reportIsolatedDiagnostic,
|
|
21
|
+
StrictGenerationError,
|
|
22
|
+
strictGenerationFailure,
|
|
23
|
+
usesStrictGeneration,
|
|
24
|
+
} from './strictGenerationDiagnostics.ts';
|
|
25
|
+
|
|
26
|
+
const runnerUrl = new URL('./runIsolatedTest.ts', import.meta.url).href;
|
|
27
|
+
const generatorUrl = new URL('./generateOpenapiIsolated.ts', import.meta.url).href;
|
|
28
|
+
const freshnessUrl = new URL(
|
|
29
|
+
'../vona/packages-vona/vona-core/src/lib/core/testFreshness.ts',
|
|
30
|
+
import.meta.url,
|
|
31
|
+
).href;
|
|
32
|
+
const SECRET = 'synthetic-private-diagnostic-9a72';
|
|
33
|
+
const SOURCE = 'http://127.0.0.1:47123/swagger/json?version=V31';
|
|
34
|
+
const ordinaryEnv: NodeJS.ProcessEnv = {
|
|
35
|
+
NODE_ENV: 'test',
|
|
36
|
+
META_FLAVOR: 'normal',
|
|
37
|
+
META_MODE: 'test',
|
|
38
|
+
};
|
|
39
|
+
const strictEnv: NodeJS.ProcessEnv = { ...ordinaryEnv, VONA_TEST_REQUIRE_FRESH_RESOURCES: 'true' };
|
|
40
|
+
const drained: GuardianResult = {
|
|
41
|
+
type: 'drained',
|
|
42
|
+
failed: false,
|
|
43
|
+
result: { code: 0, signal: null },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function privateError() {
|
|
47
|
+
return new AggregateError([new Error(SECRET)], SECRET, { cause: new Error(SECRET) });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deferred<T>() {
|
|
51
|
+
let resolvePromise!: (value: T) => void;
|
|
52
|
+
const promise = new Promise<T>(resolveValue => {
|
|
53
|
+
resolvePromise = resolveValue;
|
|
54
|
+
});
|
|
55
|
+
return { promise, resolve: resolvePromise };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertSafe(error: unknown, stage: string) {
|
|
59
|
+
assert.ok(error instanceof StrictGenerationError);
|
|
60
|
+
assert.equal(error.stage, stage);
|
|
61
|
+
assert.equal(error.message, `Strict contract generation failed: ${stage}`);
|
|
62
|
+
assert.equal(Object.hasOwn(error, 'cause'), false);
|
|
63
|
+
assert.equal(Object.hasOwn(error, 'errors'), false);
|
|
64
|
+
assert.deepEqual(Object.getOwnPropertyNames(error).sort(), ['message', 'name', 'stack', 'stage']);
|
|
65
|
+
const visible = inspect(error, { showHidden: true, depth: 20 });
|
|
66
|
+
assert.ok(!visible.includes(SECRET), 'raw synthetic diagnostic escaped');
|
|
67
|
+
assert.ok(!visible.includes(SOURCE), 'source URL escaped');
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let state: ReturnType<typeof fixture>;
|
|
72
|
+
|
|
73
|
+
class FakeGuardian extends EventEmitter {
|
|
74
|
+
connected = true;
|
|
75
|
+
readonly pid: number;
|
|
76
|
+
readonly messages: any[] = [];
|
|
77
|
+
startMessage?: any;
|
|
78
|
+
|
|
79
|
+
constructor(pid: number) {
|
|
80
|
+
super();
|
|
81
|
+
this.pid = pid;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
send(message: any, callback?: (error: Error | null) => void) {
|
|
85
|
+
this.messages.push(message);
|
|
86
|
+
if (message.type === 'start') {
|
|
87
|
+
this.startMessage = message;
|
|
88
|
+
callback?.(null);
|
|
89
|
+
queueMicrotask(() => {
|
|
90
|
+
this.emit('message', { type: 'started' });
|
|
91
|
+
if (message.args.includes(':openapi:generate') && state.generatorCompletes) {
|
|
92
|
+
queueMicrotask(() => this.complete(state.generatorResult));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
} else if (message.type === 'stop') {
|
|
96
|
+
this.complete(drained);
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
complete(result?: GuardianResult) {
|
|
102
|
+
if (!this.connected) return;
|
|
103
|
+
if (result) this.emit('message', result);
|
|
104
|
+
this.connected = false;
|
|
105
|
+
this.emit('close', 0, null);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function fixture() {
|
|
110
|
+
const directories = new Set<string>();
|
|
111
|
+
const writes: { path: string; value: string; options: unknown }[] = [];
|
|
112
|
+
const updates: any[] = [];
|
|
113
|
+
const claimEvents: any[] = [];
|
|
114
|
+
const guardians: FakeGuardian[] = [];
|
|
115
|
+
const launches: { command: string; args: string[]; options: any }[] = [];
|
|
116
|
+
const calls: string[] = [];
|
|
117
|
+
const inventory = {
|
|
118
|
+
databases: [{ clientName: 'main', prefix: 'synthetic-db-', endpoint: { dialect: 'pg' } }],
|
|
119
|
+
redis: [{ clientName: 'main', prefix: 'synthetic-redis:' }],
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
directories,
|
|
123
|
+
writes,
|
|
124
|
+
updates,
|
|
125
|
+
claimEvents,
|
|
126
|
+
guardians,
|
|
127
|
+
launches,
|
|
128
|
+
calls,
|
|
129
|
+
inventory,
|
|
130
|
+
cleanupStatus: 'cleaned',
|
|
131
|
+
cleanupWarn: false,
|
|
132
|
+
cleanupThrows: false,
|
|
133
|
+
statusThrows: false,
|
|
134
|
+
brokerCloseThrows: false,
|
|
135
|
+
generatorCompletes: true,
|
|
136
|
+
generatorResult: drained,
|
|
137
|
+
pendingAck: undefined as Promise<void> | undefined,
|
|
138
|
+
registeredAck: undefined as Promise<void> | undefined,
|
|
139
|
+
nativeOutput: undefined as ReturnType<typeof discardNativeOutput> | undefined,
|
|
140
|
+
nativeResult: undefined as OwnedProcess | undefined,
|
|
141
|
+
localEntries: [] as string[],
|
|
142
|
+
fetchMode: 'ok',
|
|
143
|
+
fetches: [] as { source: string; options: RequestInit | undefined }[],
|
|
144
|
+
fs: {
|
|
145
|
+
existsSync: (path: string) => path.endsWith('/__CABLOY_BASIC__'),
|
|
146
|
+
realpathSync: () => {
|
|
147
|
+
throw new Error('Unexpected npm path resolution');
|
|
148
|
+
},
|
|
149
|
+
mkdirSync: (path: string) => {
|
|
150
|
+
calls.push('mkdir');
|
|
151
|
+
assert.equal(directories.has(path), false);
|
|
152
|
+
directories.add(path);
|
|
153
|
+
},
|
|
154
|
+
writeFileSync: (path: string, value: string, options: unknown) => {
|
|
155
|
+
writes.push({ path, value, options });
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
createServer: () => {
|
|
159
|
+
calls.push('allocate');
|
|
160
|
+
const server = new EventEmitter() as EventEmitter & {
|
|
161
|
+
listen: (port: number, host: string, callback: () => void) => void;
|
|
162
|
+
address: () => { port: number };
|
|
163
|
+
close: (callback: (error?: Error) => void) => void;
|
|
164
|
+
};
|
|
165
|
+
server.listen = (port, host, callback) => {
|
|
166
|
+
assert.equal(port, 0);
|
|
167
|
+
assert.equal(host, '127.0.0.1');
|
|
168
|
+
callback();
|
|
169
|
+
};
|
|
170
|
+
server.address = () => ({ port: 47123 });
|
|
171
|
+
server.close = callback => callback();
|
|
172
|
+
return server;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
state = fixture();
|
|
178
|
+
const globalKey = '__strictDiagnosticsHarness';
|
|
179
|
+
const globals = globalThis as typeof globalThis & { [globalKey]?: Record<string, any> };
|
|
180
|
+
const previousGlobal = Object.getOwnPropertyDescriptor(globalThis, globalKey);
|
|
181
|
+
const bridge = `globalThis.${globalKey}`;
|
|
182
|
+
const virtual = (source: string) => ({
|
|
183
|
+
url: `data:text/javascript,${encodeURIComponent(source)}`,
|
|
184
|
+
shortCircuit: true as const,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Only these production parents can see virtual collaborators. No app, env-file,
|
|
188
|
+
// disk resource, listener, broker, native helper or real child is constructed.
|
|
189
|
+
Object.defineProperty(globalThis, globalKey, {
|
|
190
|
+
configurable: true,
|
|
191
|
+
value: {
|
|
192
|
+
fs: (name: keyof ReturnType<typeof fixture>['fs'], ...args: any[]) =>
|
|
193
|
+
Reflect.apply(state.fs[name], undefined, args),
|
|
194
|
+
createServer: () => state.createServer(),
|
|
195
|
+
spawn: (command: string, args: string[], options: any) => {
|
|
196
|
+
state.calls.push('spawn');
|
|
197
|
+
state.launches.push({ command, args, options });
|
|
198
|
+
const guardian = new FakeGuardian(41000 + state.guardians.length);
|
|
199
|
+
state.guardians.push(guardian);
|
|
200
|
+
queueMicrotask(() => guardian.emit('message', { type: 'ready' }));
|
|
201
|
+
return guardian;
|
|
202
|
+
},
|
|
203
|
+
createClaim: async (project: string, runId: string, kind: string, owner: unknown) => {
|
|
204
|
+
state.calls.push('claim');
|
|
205
|
+
assert.equal(kind, 'contract');
|
|
206
|
+
assert.deepEqual(owner, {
|
|
207
|
+
pid: process.pid,
|
|
208
|
+
startTime: 'synthetic-start',
|
|
209
|
+
host: 'synthetic',
|
|
210
|
+
});
|
|
211
|
+
return { claimPath: resolve(project, '.vona-test-claim', `${runId}-${SECRET}.json`) };
|
|
212
|
+
},
|
|
213
|
+
broker: async () => {
|
|
214
|
+
state.calls.push('broker');
|
|
215
|
+
return {
|
|
216
|
+
env: { VONA_TEST_CLAIM_URL: 'http://synthetic.invalid', VONA_TEST_CLAIM_TOKEN: SECRET },
|
|
217
|
+
update: async (event: any) => {
|
|
218
|
+
state.updates.push(event);
|
|
219
|
+
if (event.status === 'failed' && state.statusThrows) throw privateError();
|
|
220
|
+
},
|
|
221
|
+
close: async () => {
|
|
222
|
+
state.calls.push('broker-close');
|
|
223
|
+
if (state.brokerCloseThrows) throw privateError();
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
claimEvent: async (_env: unknown, event: any) => {
|
|
228
|
+
state.claimEvents.push(event);
|
|
229
|
+
if (event.process.stage === 'pending') await state.pendingAck;
|
|
230
|
+
if (event.process.stage === 'registered') await state.registeredAck;
|
|
231
|
+
},
|
|
232
|
+
inspectProcess: async (pid: number) => ({
|
|
233
|
+
pid,
|
|
234
|
+
pgid: pid,
|
|
235
|
+
startTime: 'synthetic-start',
|
|
236
|
+
host: 'synthetic',
|
|
237
|
+
}),
|
|
238
|
+
inactive: async () => true,
|
|
239
|
+
clean: async (_path: string, options: any) => {
|
|
240
|
+
state.calls.push('cleanup');
|
|
241
|
+
assert.equal(await options.isInactive({}), true);
|
|
242
|
+
if (state.cleanupWarn) options.warn(`${SECRET} ${SOURCE}`);
|
|
243
|
+
if (state.cleanupThrows) throw privateError();
|
|
244
|
+
return { status: state.cleanupStatus };
|
|
245
|
+
},
|
|
246
|
+
reap: async () => {
|
|
247
|
+
state.calls.push('reap');
|
|
248
|
+
},
|
|
249
|
+
windows: async (
|
|
250
|
+
_command: string,
|
|
251
|
+
_args: string[],
|
|
252
|
+
_cwd: string,
|
|
253
|
+
_env: unknown,
|
|
254
|
+
output: any,
|
|
255
|
+
) => {
|
|
256
|
+
state.nativeOutput = output;
|
|
257
|
+
assert.ok(state.nativeResult, 'Windows fixture must supply lifecycle proof');
|
|
258
|
+
return state.nativeResult;
|
|
259
|
+
},
|
|
260
|
+
lstat: async () => ({ isDirectory: () => true }),
|
|
261
|
+
readdir: async () => state.localEntries,
|
|
262
|
+
inventory: () => state.inventory,
|
|
263
|
+
assertConfig: () => state.calls.push('config-guard'),
|
|
264
|
+
assertPath: () => state.calls.push('path-guard'),
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const runnerMocks: Record<string, string> = {
|
|
269
|
+
'node:fs': ['existsSync', 'realpathSync', 'mkdirSync', 'writeFileSync']
|
|
270
|
+
.map(name => `export function ${name}(...args) { return ${bridge}.fs('${name}', ...args); }`)
|
|
271
|
+
.join('\n'),
|
|
272
|
+
'node:net': `export function createServer() { return ${bridge}.createServer(); }`,
|
|
273
|
+
'node:child_process': `export function spawn(...args) { return ${bridge}.spawn(...args); }`,
|
|
274
|
+
'../vona/packages-vona/vona-core/src/lib/core/testClaim.ts': `
|
|
275
|
+
export function createTestClaim(...args) { return ${bridge}.createClaim(...args); }
|
|
276
|
+
export function startTestClaimBroker(...args) { return ${bridge}.broker(...args); }
|
|
277
|
+
export function hasTestClaimTransport(env) { return !!env.VONA_TEST_CLAIM_TOKEN; }
|
|
278
|
+
export function sendTestClaimEvent(...args) { return ${bridge}.claimEvent(...args); }
|
|
279
|
+
`,
|
|
280
|
+
'./testClaimCleanup.ts': `
|
|
281
|
+
export function cleanTestClaim(...args) { return ${bridge}.clean(...args); }
|
|
282
|
+
export function reapExpiredTestClaims(...args) { return ${bridge}.reap(...args); }
|
|
283
|
+
export function parseTestClaimTtlSeconds() { return 1; }
|
|
284
|
+
`,
|
|
285
|
+
'./testClaimProcesses.ts': `
|
|
286
|
+
export function inspectTestProcess(...args) { return ${bridge}.inspectProcess(...args); }
|
|
287
|
+
export function isTestClaimInactive(...args) { return ${bridge}.inactive(...args); }
|
|
288
|
+
`,
|
|
289
|
+
'./testProcessGuardianWindows.ts': `
|
|
290
|
+
export function startWindowsOwnedProcess(...args) { return ${bridge}.windows(...args); }
|
|
291
|
+
`,
|
|
292
|
+
};
|
|
293
|
+
const freshnessMocks: Record<string, string> = {
|
|
294
|
+
'node:fs/promises': `
|
|
295
|
+
export function lstat(...args) { return ${bridge}.lstat(...args); }
|
|
296
|
+
export function readdir(...args) { return ${bridge}.readdir(...args); }
|
|
297
|
+
`,
|
|
298
|
+
'./isolationGuard.ts': `
|
|
299
|
+
export function assertIsolatedTestConfig() { return ${bridge}.assertConfig(); }
|
|
300
|
+
export function assertIsolatedTestPath() { return ${bridge}.assertPath(); }
|
|
301
|
+
`,
|
|
302
|
+
'./testIsolation.ts': `
|
|
303
|
+
export function resolveVonaLocalPaths() {
|
|
304
|
+
return { publicRoot: '/synthetic/public', runtimeRoot: '/synthetic/runtime', sqliteRoot: '/synthetic/sqlite' };
|
|
305
|
+
}
|
|
306
|
+
`,
|
|
307
|
+
'./testClaim.ts': `
|
|
308
|
+
export function buildTestClaimResourceInventory() { return ${bridge}.inventory(); }
|
|
309
|
+
export function mergeTestClaimDatabaseOptions(...args) { return Object.assign({}, ...args); }
|
|
310
|
+
export function mergeTestClaimRedisOptions(...args) { return Object.assign({}, ...args); }
|
|
311
|
+
`,
|
|
312
|
+
};
|
|
313
|
+
const hooks = registerHooks({
|
|
314
|
+
resolve(specifier, context, nextResolve) {
|
|
315
|
+
const mocks =
|
|
316
|
+
context.parentURL === runnerUrl
|
|
317
|
+
? runnerMocks
|
|
318
|
+
: context.parentURL === freshnessUrl
|
|
319
|
+
? freshnessMocks
|
|
320
|
+
: undefined;
|
|
321
|
+
if (mocks && Object.hasOwn(mocks, specifier)) return virtual(mocks[specifier]);
|
|
322
|
+
return nextResolve(specifier, context);
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
let runner: typeof import('./runIsolatedTest.ts');
|
|
326
|
+
let generator: typeof import('./generateOpenapiIsolated.ts');
|
|
327
|
+
let freshness: typeof import('../vona/packages-vona/vona-core/src/lib/core/testFreshness.ts');
|
|
328
|
+
try {
|
|
329
|
+
runner = await import(runnerUrl);
|
|
330
|
+
generator = await import(generatorUrl);
|
|
331
|
+
freshness = await import(freshnessUrl);
|
|
332
|
+
} finally {
|
|
333
|
+
hooks.deregister();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function lifecycle(options: { start?: boolean; ready?: boolean; stop?: boolean } = {}) {
|
|
337
|
+
const calls: string[] = [];
|
|
338
|
+
const closed = deferred<GuardianResult>();
|
|
339
|
+
const server: OwnedProcess = {
|
|
340
|
+
wait: () => closed.promise,
|
|
341
|
+
stop: async () => {
|
|
342
|
+
calls.push('stop');
|
|
343
|
+
if (options.stop) throw privateError();
|
|
344
|
+
closed.resolve(drained);
|
|
345
|
+
return drained;
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
calls,
|
|
350
|
+
dependencies: {
|
|
351
|
+
platform: 'darwin' as const,
|
|
352
|
+
start: async () => {
|
|
353
|
+
calls.push('start');
|
|
354
|
+
if (options.start) throw privateError();
|
|
355
|
+
return server;
|
|
356
|
+
},
|
|
357
|
+
ready: async () => {
|
|
358
|
+
calls.push('ready');
|
|
359
|
+
if (options.ready) throw privateError();
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function installFetch() {
|
|
366
|
+
globalThis.fetch = async (input, options) => {
|
|
367
|
+
const source = String(input);
|
|
368
|
+
state.fetches.push({ source, options });
|
|
369
|
+
if (source.endsWith('/health/ready')) {
|
|
370
|
+
const server = state.guardians.find(child => child.startMessage?.args.includes(':bin:dev'));
|
|
371
|
+
assert.ok(server?.startMessage);
|
|
372
|
+
const challenge = new Headers(options?.headers).get('x-vona-test-challenge');
|
|
373
|
+
assert.ok(challenge);
|
|
374
|
+
const key = server.startMessage.env.VONA_TEST_READY_KEY;
|
|
375
|
+
return new Response(JSON.stringify({ status: 'ok' }), {
|
|
376
|
+
headers: { 'x-vona-test-proof': createHmac('sha256', key).update(challenge).digest('hex') },
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
assert.equal(source, SOURCE, 'no alternate source or fallback request is permitted');
|
|
380
|
+
if (state.fetchMode === 'request-error') throw privateError();
|
|
381
|
+
if (state.fetchMode === 'json-error') {
|
|
382
|
+
return {
|
|
383
|
+
ok: true,
|
|
384
|
+
json: async () => {
|
|
385
|
+
throw privateError();
|
|
386
|
+
},
|
|
387
|
+
} as unknown as Response;
|
|
388
|
+
}
|
|
389
|
+
if (state.fetchMode === 'http-error') return new Response(SECRET, { status: 503 });
|
|
390
|
+
if (state.fetchMode === 'shape-error') return new Response(JSON.stringify({ private: SECRET }));
|
|
391
|
+
return new Response(JSON.stringify({ openapi: '3.1.0', paths: {} }));
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
test(
|
|
396
|
+
'strict contract diagnostics use only fake resources and preserve lifecycle proof',
|
|
397
|
+
{ concurrency: false },
|
|
398
|
+
async t => {
|
|
399
|
+
const originalConsole = console.error;
|
|
400
|
+
const originalFetch = globalThis.fetch;
|
|
401
|
+
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')!;
|
|
402
|
+
const savedEnv = new Map(
|
|
403
|
+
[TEST_REQUIRE_FRESH_RESOURCES_ENV, 'E2E_BASE_URL'].map(key => [key, process.env[key]]),
|
|
404
|
+
);
|
|
405
|
+
const diagnostics: unknown[][] = [];
|
|
406
|
+
console.error = (...values) => {
|
|
407
|
+
diagnostics.push(values);
|
|
408
|
+
};
|
|
409
|
+
delete process.env.E2E_BASE_URL;
|
|
410
|
+
Object.defineProperty(process, 'platform', { ...originalPlatform, value: 'darwin' });
|
|
411
|
+
const reset = () => {
|
|
412
|
+
state = fixture();
|
|
413
|
+
diagnostics.length = 0;
|
|
414
|
+
};
|
|
415
|
+
const assertPrivate = () => {
|
|
416
|
+
const output = inspect(diagnostics, { depth: 20, showHidden: true });
|
|
417
|
+
assert.ok(!output.includes(SECRET), 'raw synthetic diagnostic was logged');
|
|
418
|
+
assert.ok(!output.includes(SOURCE), 'source URL was logged');
|
|
419
|
+
};
|
|
420
|
+
try {
|
|
421
|
+
await t.test('strict flag parsing, safe errors and ordinary diagnostic behavior', () => {
|
|
422
|
+
assert.equal(requiresFreshTestResources(ordinaryEnv), false);
|
|
423
|
+
assert.equal(requiresFreshTestResources(strictEnv), true);
|
|
424
|
+
assert.equal(freshness.requiresFreshTestResources, requiresFreshTestResources);
|
|
425
|
+
assert.equal(freshness.TEST_REQUIRE_FRESH_RESOURCES_ENV, TEST_REQUIRE_FRESH_RESOURCES_ENV);
|
|
426
|
+
assert.equal(usesStrictGeneration(ordinaryEnv), false);
|
|
427
|
+
assert.equal(usesStrictGeneration(strictEnv), true);
|
|
428
|
+
for (const value of ['', 'false', 'TRUE', SECRET]) {
|
|
429
|
+
assert.throws(
|
|
430
|
+
() =>
|
|
431
|
+
requiresFreshTestResources({
|
|
432
|
+
...ordinaryEnv,
|
|
433
|
+
[TEST_REQUIRE_FRESH_RESOURCES_ENV]: value,
|
|
434
|
+
}),
|
|
435
|
+
/Invalid strict/,
|
|
436
|
+
);
|
|
437
|
+
assert.throws(
|
|
438
|
+
() =>
|
|
439
|
+
usesStrictGeneration({ ...ordinaryEnv, [TEST_REQUIRE_FRESH_RESOURCES_ENV]: value }),
|
|
440
|
+
error => assertSafe(error, 'arguments'),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const poisoned = strictGenerationFailure('operation', privateError());
|
|
444
|
+
Object.assign(poisoned, {
|
|
445
|
+
message: SECRET,
|
|
446
|
+
stack: SECRET,
|
|
447
|
+
stage: SECRET,
|
|
448
|
+
cause: privateError(),
|
|
449
|
+
private: SECRET,
|
|
450
|
+
});
|
|
451
|
+
const safe = strictGenerationFailure('run', poisoned);
|
|
452
|
+
assert.notEqual(safe, poisoned);
|
|
453
|
+
assertSafe(safe, 'operation');
|
|
454
|
+
assertSafe(strictGenerationFailure('cleanup', privateError()), 'cleanup');
|
|
455
|
+
reportIsolatedDiagnostic(true, 'cleanup-warning', privateError());
|
|
456
|
+
assert.deepEqual(diagnostics, [['Strict contract generation: cleanup-warning']]);
|
|
457
|
+
assertPrivate();
|
|
458
|
+
const ordinary = privateError();
|
|
459
|
+
reportIsolatedDiagnostic(false, 'ignored', ordinary);
|
|
460
|
+
assert.equal(diagnostics[1][0], ordinary);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await t.test(
|
|
464
|
+
'freshness APIs reject existing namespaces and sanitize fake driver failures',
|
|
465
|
+
async () => {
|
|
466
|
+
process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV] = 'true';
|
|
467
|
+
const app = () =>
|
|
468
|
+
({
|
|
469
|
+
meta: { env: { VONA_TEST_RUN_ID: 'synthetic' }, appReady: false },
|
|
470
|
+
projectPath: '/synthetic',
|
|
471
|
+
config: {
|
|
472
|
+
database: { base: {}, clients: { main: {} } },
|
|
473
|
+
redis: { base: {}, clients: { main: {} } },
|
|
474
|
+
},
|
|
475
|
+
}) as any;
|
|
476
|
+
for (const failure of ['local', 'database', 'redis', 'driver']) {
|
|
477
|
+
reset();
|
|
478
|
+
if (failure === 'local') state.localEntries.push('retained');
|
|
479
|
+
const target = app();
|
|
480
|
+
const readers = {
|
|
481
|
+
databaseExists: async () => {
|
|
482
|
+
state.calls.push('database-read');
|
|
483
|
+
if (failure === 'driver') throw privateError();
|
|
484
|
+
return failure === 'database';
|
|
485
|
+
},
|
|
486
|
+
redisExists: async () => {
|
|
487
|
+
state.calls.push('redis-read');
|
|
488
|
+
return failure === 'redis';
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
assert.throws(
|
|
492
|
+
() => freshness.assertFreshTestResourcesReady(target),
|
|
493
|
+
/was not established/,
|
|
494
|
+
);
|
|
495
|
+
await assert.rejects(freshness.assertFreshTestResources(target, readers), error => {
|
|
496
|
+
assert.ok(error instanceof Error);
|
|
497
|
+
assert.ok(!inspect(error, { showHidden: true }).includes(SECRET));
|
|
498
|
+
assert.equal(Object.hasOwn(error, 'cause'), false);
|
|
499
|
+
return true;
|
|
500
|
+
});
|
|
501
|
+
assert.throws(
|
|
502
|
+
() => freshness.assertFreshTestResourcesReady(target),
|
|
503
|
+
/was not established/,
|
|
504
|
+
);
|
|
505
|
+
assert.ok(!state.calls.includes('cleanup'));
|
|
506
|
+
if (failure === 'local') assert.ok(!state.calls.includes('database-read'));
|
|
507
|
+
if (failure === 'database') assert.ok(!state.calls.includes('redis-read'));
|
|
508
|
+
}
|
|
509
|
+
reset();
|
|
510
|
+
const target = app();
|
|
511
|
+
const readers = { databaseExists: async () => false, redisExists: async () => false };
|
|
512
|
+
await freshness.assertFreshTestResources(target, readers);
|
|
513
|
+
freshness.assertFreshTestResourcesReady(target);
|
|
514
|
+
await assert.rejects(
|
|
515
|
+
freshness.assertFreshTestResources(target, readers),
|
|
516
|
+
/new isolated startup/,
|
|
517
|
+
);
|
|
518
|
+
const readyApp = app();
|
|
519
|
+
readyApp.meta.appReady = true;
|
|
520
|
+
await assert.rejects(
|
|
521
|
+
freshness.assertFreshTestResources(readyApp, readers),
|
|
522
|
+
/new isolated startup/,
|
|
523
|
+
);
|
|
524
|
+
delete process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
|
|
525
|
+
reset();
|
|
526
|
+
await freshness.assertFreshTestResources(app(), {
|
|
527
|
+
databaseExists: async () => {
|
|
528
|
+
throw new Error('ordinary lane must not discover resources');
|
|
529
|
+
},
|
|
530
|
+
redisExists: async () => {
|
|
531
|
+
throw new Error('ordinary lane must not discover resources');
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
freshness.assertFreshTestResourcesReady(app());
|
|
535
|
+
assert.deepEqual(state.calls, []);
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
await t.test('explicit POSIX stdio and non-buffering native discard streams', async () => {
|
|
540
|
+
assert.deepEqual(guardianStdio(true), ['ignore', 'ignore', 'ignore', 'ipc']);
|
|
541
|
+
assert.deepEqual(guardianStdio(false), ['inherit', 'inherit', 'inherit', 'ipc']);
|
|
542
|
+
const output = discardNativeOutput();
|
|
543
|
+
assert.notEqual(output.stdout, output.stderr);
|
|
544
|
+
let callbacks = 0;
|
|
545
|
+
for (const sink of [output.stdout, output.stderr]) {
|
|
546
|
+
sink.write(Buffer.from(SECRET), () => {
|
|
547
|
+
callbacks++;
|
|
548
|
+
});
|
|
549
|
+
sink.write(Buffer.alloc(1024 * 1024, 120), () => {
|
|
550
|
+
callbacks++;
|
|
551
|
+
});
|
|
552
|
+
assert.equal(sink.writableLength, 0);
|
|
553
|
+
assert.deepEqual(sink.writableBuffer, []);
|
|
554
|
+
sink.end();
|
|
555
|
+
await finished(sink);
|
|
556
|
+
}
|
|
557
|
+
assert.equal(callbacks, 4);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
await t.test(
|
|
561
|
+
'real Vona lifecycle categorizes startup, operation, shutdown and combined failures',
|
|
562
|
+
async () => {
|
|
563
|
+
for (const scenario of [
|
|
564
|
+
'success',
|
|
565
|
+
'start',
|
|
566
|
+
'ready',
|
|
567
|
+
'operation',
|
|
568
|
+
'stop',
|
|
569
|
+
'combined',
|
|
570
|
+
'interrupt',
|
|
571
|
+
]) {
|
|
572
|
+
reset();
|
|
573
|
+
const f = lifecycle({
|
|
574
|
+
start: scenario === 'start',
|
|
575
|
+
ready: scenario === 'ready',
|
|
576
|
+
stop: ['stop', 'combined', 'interrupt'].includes(scenario),
|
|
577
|
+
});
|
|
578
|
+
const before = ['SIGINT', 'SIGTERM'].map(signal => process.listenerCount(signal));
|
|
579
|
+
const run = runner.withIsolatedVona(
|
|
580
|
+
47123,
|
|
581
|
+
strictEnv,
|
|
582
|
+
async () => {
|
|
583
|
+
f.calls.push('operation');
|
|
584
|
+
if (['operation', 'combined'].includes(scenario)) throw privateError();
|
|
585
|
+
if (scenario === 'interrupt') {
|
|
586
|
+
process.emit('SIGINT');
|
|
587
|
+
await nextTurn();
|
|
588
|
+
}
|
|
589
|
+
return 'generated';
|
|
590
|
+
},
|
|
591
|
+
f.dependencies,
|
|
592
|
+
);
|
|
593
|
+
if (scenario === 'success') {
|
|
594
|
+
assert.equal(await run, 'generated');
|
|
595
|
+
} else {
|
|
596
|
+
const stage = ['start', 'ready'].includes(scenario)
|
|
597
|
+
? 'startup'
|
|
598
|
+
: scenario === 'combined'
|
|
599
|
+
? 'operation-and-shutdown'
|
|
600
|
+
: scenario === 'operation'
|
|
601
|
+
? 'operation'
|
|
602
|
+
: 'shutdown';
|
|
603
|
+
await assert.rejects(run, error => assertSafe(error, stage));
|
|
604
|
+
}
|
|
605
|
+
assert.deepEqual(
|
|
606
|
+
['SIGINT', 'SIGTERM'].map(signal => process.listenerCount(signal)),
|
|
607
|
+
before,
|
|
608
|
+
);
|
|
609
|
+
if (scenario === 'start') {
|
|
610
|
+
assert.deepEqual(f.calls, ['start']);
|
|
611
|
+
} else if (scenario === 'ready') {
|
|
612
|
+
assert.deepEqual(f.calls, ['start', 'ready', 'stop']);
|
|
613
|
+
} else {
|
|
614
|
+
assert.equal(
|
|
615
|
+
f.calls.filter(call => call === 'stop').length,
|
|
616
|
+
scenario === 'interrupt' ? 2 : 1,
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
if (scenario === 'interrupt') {
|
|
620
|
+
assert.deepEqual(diagnostics, [
|
|
621
|
+
['Strict contract generation: interrupt-stop-failed'],
|
|
622
|
+
]);
|
|
623
|
+
}
|
|
624
|
+
assertPrivate();
|
|
625
|
+
}
|
|
626
|
+
const f = lifecycle();
|
|
627
|
+
const ordinary = privateError();
|
|
628
|
+
await assert.rejects(
|
|
629
|
+
runner.withIsolatedVona(
|
|
630
|
+
47123,
|
|
631
|
+
ordinaryEnv,
|
|
632
|
+
async () => {
|
|
633
|
+
throw ordinary;
|
|
634
|
+
},
|
|
635
|
+
f.dependencies,
|
|
636
|
+
),
|
|
637
|
+
error => error === ordinary,
|
|
638
|
+
);
|
|
639
|
+
},
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
await t.test(
|
|
643
|
+
'real isolated run owns synthetic roots, retains failed status and never cleans failed operations',
|
|
644
|
+
async () => {
|
|
645
|
+
for (const scenario of ['operation', 'status', 'close']) {
|
|
646
|
+
reset();
|
|
647
|
+
state.statusThrows = scenario === 'status';
|
|
648
|
+
state.brokerCloseThrows = scenario === 'close';
|
|
649
|
+
await assert.rejects(
|
|
650
|
+
runner.withIsolatedRun(
|
|
651
|
+
'contract',
|
|
652
|
+
async context => {
|
|
653
|
+
assert.equal(context.env.META_MODE, 'test');
|
|
654
|
+
assert.equal(context.env.VONA_TEST_REQUIRE_FRESH_RESOURCES, 'true');
|
|
655
|
+
assert.equal(context.env.SERVER_LISTEN_PORT, '47123');
|
|
656
|
+
assert.equal(context.env.VONA_TEST_CLAIM_TOKEN, SECRET);
|
|
657
|
+
assert.equal(state.writes.length, 2);
|
|
658
|
+
for (const write of state.writes) {
|
|
659
|
+
assert.equal(write.value, `${context.runId}\n`);
|
|
660
|
+
assert.deepEqual(write.options, { flag: 'wx' });
|
|
661
|
+
}
|
|
662
|
+
throw privateError();
|
|
663
|
+
},
|
|
664
|
+
'/synthetic',
|
|
665
|
+
{ skipExpiredClaimReaping: true },
|
|
666
|
+
),
|
|
667
|
+
error => assertSafe(error, scenario === 'close' ? 'preparation' : 'run'),
|
|
668
|
+
);
|
|
669
|
+
assert.equal(state.directories.size, 3);
|
|
670
|
+
assert.deepEqual(state.updates, [
|
|
671
|
+
{ type: 'local', kind: 'data', stage: 'owned' },
|
|
672
|
+
{ type: 'local', kind: 'generated', stage: 'owned' },
|
|
673
|
+
{ type: 'status', status: 'running' },
|
|
674
|
+
{ type: 'status', status: 'failed' },
|
|
675
|
+
]);
|
|
676
|
+
assert.ok(!state.calls.includes('reap'));
|
|
677
|
+
assert.ok(!state.calls.includes('cleanup'));
|
|
678
|
+
assert.ok(state.calls.includes('broker-close'));
|
|
679
|
+
assert.ok(
|
|
680
|
+
diagnostics.some(
|
|
681
|
+
values => values[0] === 'Strict contract generation: failed-resources-retained',
|
|
682
|
+
),
|
|
683
|
+
);
|
|
684
|
+
assertPrivate();
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
await t.test(
|
|
690
|
+
'cleanup failures remain failures, discard warnings, and success alone reaches cleanup',
|
|
691
|
+
async () => {
|
|
692
|
+
for (const scenario of ['success', 'incomplete', 'throws']) {
|
|
693
|
+
reset();
|
|
694
|
+
state.cleanupWarn = scenario !== 'success';
|
|
695
|
+
state.cleanupStatus = scenario === 'incomplete' ? 'retained' : 'cleaned';
|
|
696
|
+
state.cleanupThrows = scenario === 'throws';
|
|
697
|
+
const run = runner.withIsolatedRun('contract', async () => 'done', '/synthetic', {
|
|
698
|
+
skipExpiredClaimReaping: true,
|
|
699
|
+
});
|
|
700
|
+
if (scenario === 'success') assert.equal(await run, 'done');
|
|
701
|
+
else await assert.rejects(run, error => assertSafe(error, 'cleanup'));
|
|
702
|
+
assert.equal(state.calls.filter(call => call === 'cleanup').length, 1);
|
|
703
|
+
assert.ok(state.calls.indexOf('broker-close') < state.calls.indexOf('cleanup'));
|
|
704
|
+
assert.ok(!state.updates.some(event => event.status === 'failed'));
|
|
705
|
+
if (scenario !== 'success') {
|
|
706
|
+
assert.ok(
|
|
707
|
+
diagnostics.some(
|
|
708
|
+
values => values[0] === 'Strict contract generation: cleanup-warning',
|
|
709
|
+
),
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
assertPrivate();
|
|
713
|
+
}
|
|
714
|
+
reset();
|
|
715
|
+
process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV] = 'true';
|
|
716
|
+
await runner.withIsolatedRun(
|
|
717
|
+
'contract',
|
|
718
|
+
async ({ env }) => {
|
|
719
|
+
assert.equal(
|
|
720
|
+
env.VONA_TEST_REQUIRE_FRESH_RESOURCES,
|
|
721
|
+
undefined,
|
|
722
|
+
'ordinary runner clears inherited activation',
|
|
723
|
+
);
|
|
724
|
+
},
|
|
725
|
+
'/synthetic',
|
|
726
|
+
);
|
|
727
|
+
assert.ok(state.calls.includes('reap'));
|
|
728
|
+
assert.ok(state.calls.includes('cleanup'));
|
|
729
|
+
delete process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
|
|
730
|
+
reset();
|
|
731
|
+
await assert.rejects(
|
|
732
|
+
runner.withIsolatedRun('unit', async () => assert.fail('must not run'), '/synthetic', {
|
|
733
|
+
skipExpiredClaimReaping: true,
|
|
734
|
+
}),
|
|
735
|
+
error => assertSafe(error, 'preparation'),
|
|
736
|
+
);
|
|
737
|
+
assert.deepEqual(state.calls, []);
|
|
738
|
+
},
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
await t.test(
|
|
742
|
+
'real POSIX owner waits for both durable ACKs and preserves IPC, identity and drainage',
|
|
743
|
+
async () => {
|
|
744
|
+
reset();
|
|
745
|
+
const pending = deferred<void>();
|
|
746
|
+
const registered = deferred<void>();
|
|
747
|
+
state.pendingAck = pending.promise;
|
|
748
|
+
state.registeredAck = registered.promise;
|
|
749
|
+
const env = { ...strictEnv, VONA_TEST_CLAIM_TOKEN: SECRET };
|
|
750
|
+
const starting = runner.startOwnedProcess(
|
|
751
|
+
'synthetic-command',
|
|
752
|
+
['synthetic-argument'],
|
|
753
|
+
'/synthetic',
|
|
754
|
+
env,
|
|
755
|
+
);
|
|
756
|
+
await nextTurn();
|
|
757
|
+
assert.equal(state.launches.length, 0, 'pending ACK must precede spawn');
|
|
758
|
+
pending.resolve();
|
|
759
|
+
await nextTurn();
|
|
760
|
+
assert.equal(state.launches.length, 1);
|
|
761
|
+
const child = state.guardians[0];
|
|
762
|
+
assert.equal(child.messages.length, 0, 'registered ACK must precede start authorization');
|
|
763
|
+
registered.resolve();
|
|
764
|
+
const owner = await starting;
|
|
765
|
+
assert.deepEqual(state.launches[0].options.stdio, ['ignore', 'ignore', 'ignore', 'ipc']);
|
|
766
|
+
assert.equal(state.launches[0].options.detached, true);
|
|
767
|
+
assert.equal(state.launches[0].options.env, env);
|
|
768
|
+
assert.equal(child.startMessage.command, 'synthetic-command');
|
|
769
|
+
assert.deepEqual(child.startMessage.args, ['synthetic-argument']);
|
|
770
|
+
assert.equal(child.startMessage.env, env);
|
|
771
|
+
assert.equal(child.startMessage.identity.pgid, child.pid);
|
|
772
|
+
assert.deepEqual(
|
|
773
|
+
state.claimEvents.map(event => event.process.stage),
|
|
774
|
+
['pending', 'registered'],
|
|
775
|
+
);
|
|
776
|
+
assert.ok(
|
|
777
|
+
state.claimEvents.every(event => !inspect(event).includes(SECRET)),
|
|
778
|
+
'claim records must not serialize environment',
|
|
779
|
+
);
|
|
780
|
+
let settled = false;
|
|
781
|
+
const completion = owner.wait().then(result => {
|
|
782
|
+
settled = true;
|
|
783
|
+
return result;
|
|
784
|
+
});
|
|
785
|
+
child.emit('message', drained);
|
|
786
|
+
await nextTurn();
|
|
787
|
+
assert.equal(settled, false, 'drained IPC alone does not prove successful guardian exit');
|
|
788
|
+
child.complete(drained);
|
|
789
|
+
assert.deepEqual(await completion, drained);
|
|
790
|
+
assert.deepEqual(await owner.stop(), drained);
|
|
791
|
+
reset();
|
|
792
|
+
const unproved = await runner.startOwnedProcess('synthetic', [], '/synthetic', strictEnv);
|
|
793
|
+
const rejection = assert.rejects(
|
|
794
|
+
unproved.wait(),
|
|
795
|
+
/without proving process-tree shutdown/,
|
|
796
|
+
);
|
|
797
|
+
state.guardians[0].complete();
|
|
798
|
+
await rejection;
|
|
799
|
+
reset();
|
|
800
|
+
const ordinary = await runner.startOwnedProcess(
|
|
801
|
+
'synthetic',
|
|
802
|
+
[],
|
|
803
|
+
'/synthetic',
|
|
804
|
+
ordinaryEnv,
|
|
805
|
+
);
|
|
806
|
+
assert.deepEqual(state.launches[0].options.stdio, [
|
|
807
|
+
'inherit',
|
|
808
|
+
'inherit',
|
|
809
|
+
'inherit',
|
|
810
|
+
'ipc',
|
|
811
|
+
]);
|
|
812
|
+
await ordinary.stop();
|
|
813
|
+
reset();
|
|
814
|
+
await assert.rejects(
|
|
815
|
+
runner.startOwnedProcess('synthetic', [], '/synthetic', {
|
|
816
|
+
...ordinaryEnv,
|
|
817
|
+
VONA_TEST_REQUIRE_FRESH_RESOURCES: SECRET,
|
|
818
|
+
}),
|
|
819
|
+
error => assertSafe(error, 'arguments'),
|
|
820
|
+
);
|
|
821
|
+
assert.deepEqual(state.calls, []);
|
|
822
|
+
assert.deepEqual(state.claimEvents, []);
|
|
823
|
+
},
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
await t.test(
|
|
827
|
+
'mocked Windows owner receives discard sinks without changing wait or stop proof',
|
|
828
|
+
async () => {
|
|
829
|
+
Object.defineProperty(process, 'platform', { ...originalPlatform, value: 'win32' });
|
|
830
|
+
try {
|
|
831
|
+
for (const strict of [true, false]) {
|
|
832
|
+
reset();
|
|
833
|
+
const completion = deferred<GuardianResult>();
|
|
834
|
+
const native: OwnedProcess = {
|
|
835
|
+
wait: () => completion.promise,
|
|
836
|
+
stop: () => completion.promise,
|
|
837
|
+
};
|
|
838
|
+
state.nativeResult = native;
|
|
839
|
+
const owner = await runner.startOwnedProcess(
|
|
840
|
+
'synthetic',
|
|
841
|
+
[],
|
|
842
|
+
'/synthetic',
|
|
843
|
+
strict ? strictEnv : ordinaryEnv,
|
|
844
|
+
);
|
|
845
|
+
assert.equal(owner, native);
|
|
846
|
+
assert.equal(owner.wait(), completion.promise);
|
|
847
|
+
assert.equal(owner.stop(), completion.promise);
|
|
848
|
+
if (strict) {
|
|
849
|
+
assert.ok(state.nativeOutput);
|
|
850
|
+
for (const sink of [state.nativeOutput.stdout, state.nativeOutput.stderr]) {
|
|
851
|
+
await new Promise<void>((resolvePromise, reject) =>
|
|
852
|
+
sink.write(SECRET, error => (error ? reject(error) : resolvePromise())),
|
|
853
|
+
);
|
|
854
|
+
assert.equal(sink.writableLength, 0);
|
|
855
|
+
assert.deepEqual(sink.writableBuffer, []);
|
|
856
|
+
sink.end();
|
|
857
|
+
await finished(sink);
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
assert.equal(state.nativeOutput, undefined);
|
|
861
|
+
}
|
|
862
|
+
completion.resolve(drained);
|
|
863
|
+
assert.equal(await owner.wait(), drained);
|
|
864
|
+
assert.deepEqual(state.launches, []);
|
|
865
|
+
}
|
|
866
|
+
} finally {
|
|
867
|
+
Object.defineProperty(process, 'platform', { ...originalPlatform, value: 'darwin' });
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
await t.test(
|
|
873
|
+
'real generator process fails safely on nonzero exit and asynchronous interrupt-stop rejection',
|
|
874
|
+
async () => {
|
|
875
|
+
for (const interrupted of [false, true]) {
|
|
876
|
+
reset();
|
|
877
|
+
state.generatorCompletes = !interrupted;
|
|
878
|
+
state.generatorResult = {
|
|
879
|
+
type: 'drained',
|
|
880
|
+
failed: true,
|
|
881
|
+
result: { code: 7, signal: null },
|
|
882
|
+
};
|
|
883
|
+
const before = process.listenerCount('SIGTERM');
|
|
884
|
+
const run = runner.runIsolatedProcess(
|
|
885
|
+
'synthetic',
|
|
886
|
+
[':openapi:generate'],
|
|
887
|
+
'/synthetic',
|
|
888
|
+
strictEnv,
|
|
889
|
+
);
|
|
890
|
+
const rejection = assert.rejects(run, error => assertSafe(error, 'generator'));
|
|
891
|
+
if (interrupted) {
|
|
892
|
+
await nextTurn();
|
|
893
|
+
const child = state.guardians[0];
|
|
894
|
+
child.send = message => {
|
|
895
|
+
child.messages.push(message);
|
|
896
|
+
if (message.type === 'stop') child.complete();
|
|
897
|
+
return true;
|
|
898
|
+
};
|
|
899
|
+
process.emit('SIGTERM');
|
|
900
|
+
}
|
|
901
|
+
await rejection;
|
|
902
|
+
await nextTurn();
|
|
903
|
+
assert.equal(process.listenerCount('SIGTERM'), before);
|
|
904
|
+
if (interrupted) {
|
|
905
|
+
assert.deepEqual(diagnostics, [
|
|
906
|
+
['Strict contract generation: interrupt-stop-failed'],
|
|
907
|
+
]);
|
|
908
|
+
}
|
|
909
|
+
assertPrivate();
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
await t.test(
|
|
915
|
+
'public strict contract command drives both real orchestration children with no source fallback or leakage',
|
|
916
|
+
async () => {
|
|
917
|
+
installFetch();
|
|
918
|
+
for (const mode of [
|
|
919
|
+
'ok',
|
|
920
|
+
'request-error',
|
|
921
|
+
'json-error',
|
|
922
|
+
'http-error',
|
|
923
|
+
'shape-error',
|
|
924
|
+
'generator-error',
|
|
925
|
+
]) {
|
|
926
|
+
reset();
|
|
927
|
+
state.fetchMode = mode;
|
|
928
|
+
if (mode === 'generator-error') {
|
|
929
|
+
state.generatorResult = {
|
|
930
|
+
type: 'drained',
|
|
931
|
+
failed: true,
|
|
932
|
+
result: { code: 19, signal: null },
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
const run = generator.generateOpenapiIsolated([
|
|
936
|
+
'synthetic-module',
|
|
937
|
+
'--skip-expired-claim-reaping',
|
|
938
|
+
'--nometadata',
|
|
939
|
+
]);
|
|
940
|
+
if (mode === 'ok') {
|
|
941
|
+
await run;
|
|
942
|
+
} else {
|
|
943
|
+
await assert.rejects(run, error =>
|
|
944
|
+
assertSafe(error, mode === 'generator-error' ? 'generator' : 'operation'),
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
assert.deepEqual(
|
|
948
|
+
state.fetches.map(request => request.source),
|
|
949
|
+
['http://127.0.0.1:47123/health/ready', SOURCE],
|
|
950
|
+
);
|
|
951
|
+
assert.equal(state.fetches[1].options?.redirect, 'error');
|
|
952
|
+
const children = state.guardians.map(child => child.startMessage);
|
|
953
|
+
const expectedChildren = ['ok', 'generator-error'].includes(mode) ? 2 : 1;
|
|
954
|
+
assert.equal(children.length, expectedChildren);
|
|
955
|
+
for (const launch of state.launches)
|
|
956
|
+
assert.deepEqual(launch.options.stdio, ['ignore', 'ignore', 'ignore', 'ipc']);
|
|
957
|
+
for (const child of children) {
|
|
958
|
+
assert.equal(child.env.VONA_TEST_REQUIRE_FRESH_RESOURCES, 'true');
|
|
959
|
+
assert.equal(child.env.META_MODE, 'test');
|
|
960
|
+
assert.equal(child.env.VONA_TEST_CLAIM_TOKEN, SECRET);
|
|
961
|
+
}
|
|
962
|
+
assert.ok(children[0].args.includes(':bin:dev'));
|
|
963
|
+
assert.ok(children[0].env.VONA_TEST_READY_KEY);
|
|
964
|
+
if (expectedChildren === 2) {
|
|
965
|
+
assert.ok(children[1].args.includes(':openapi:generate'));
|
|
966
|
+
assert.equal(children[1].env.OPENAPI_SOURCE_URL, SOURCE);
|
|
967
|
+
assert.equal(children[1].env.VONA_TEST_READY_KEY, undefined);
|
|
968
|
+
assert.ok(!children[1].args.includes('--skip-expired-claim-reaping'));
|
|
969
|
+
assert.ok(children[1].args.includes('--nometadata'));
|
|
970
|
+
}
|
|
971
|
+
assert.ok(state.guardians[0].messages.some(message => message.type === 'stop'));
|
|
972
|
+
assert.equal(state.calls.includes('cleanup'), mode === 'ok');
|
|
973
|
+
assert.ok(!state.calls.includes('reap'));
|
|
974
|
+
assertPrivate();
|
|
975
|
+
}
|
|
976
|
+
for (const args of [
|
|
977
|
+
['--skip-expired-claim-reaping=true'],
|
|
978
|
+
['--skip-expired-claim-reaping', `--projectPath=${SECRET}`],
|
|
979
|
+
]) {
|
|
980
|
+
reset();
|
|
981
|
+
await assert.rejects(generator.generateOpenapiIsolated(args), error =>
|
|
982
|
+
assertSafe(error, 'run'),
|
|
983
|
+
);
|
|
984
|
+
assert.deepEqual(state.calls, []);
|
|
985
|
+
assert.deepEqual(state.fetches, []);
|
|
986
|
+
}
|
|
987
|
+
reset();
|
|
988
|
+
process.env.E2E_BASE_URL = SECRET;
|
|
989
|
+
await assert.rejects(
|
|
990
|
+
generator.generateOpenapiIsolated(['--skip-expired-claim-reaping']),
|
|
991
|
+
error => assertSafe(error, 'run'),
|
|
992
|
+
);
|
|
993
|
+
assert.deepEqual(state.calls, []);
|
|
994
|
+
assertPrivate();
|
|
995
|
+
},
|
|
996
|
+
);
|
|
997
|
+
} finally {
|
|
998
|
+
console.error = originalConsole;
|
|
999
|
+
globalThis.fetch = originalFetch;
|
|
1000
|
+
Object.defineProperty(process, 'platform', originalPlatform);
|
|
1001
|
+
for (const [key, value] of savedEnv) {
|
|
1002
|
+
if (value === undefined) delete process.env[key];
|
|
1003
|
+
else process.env[key] = value;
|
|
1004
|
+
}
|
|
1005
|
+
if (previousGlobal) Object.defineProperty(globalThis, globalKey, previousGlobal);
|
|
1006
|
+
else Reflect.deleteProperty(globals, globalKey);
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
);
|