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.
Files changed (87) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/agent-governance.yml +2 -2
  3. package/.github/workflows/create-cabloy.yml +2 -2
  4. package/.github/workflows/docs-pages.yml +2 -2
  5. package/.github/workflows/playwright-e2e.yml +4 -4
  6. package/.github/workflows/vona-cov-pg.yml +2 -2
  7. package/.github/workflows/vona-test-crud.yml +2 -2
  8. package/.github/workflows/vona-test-mysql.yml +2 -2
  9. package/.github/workflows/vona-test-pg.yml +2 -2
  10. package/.github/workflows/vona-test-sqlite3.yml +2 -2
  11. package/.github/workflows/vona-tsc.yml +2 -2
  12. package/.github/workflows/zova-ui.yml +2 -2
  13. package/CHANGELOG.md +22 -0
  14. package/package.json +4 -3
  15. package/repo-agent-governance/managed-assets.json +15 -15
  16. package/repo-agent-governance/policies/repository.md +1 -0
  17. package/repo-agent-governance/skills/cabloy-backend-scaffold/SKILL.md +5 -1
  18. package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +7 -5
  19. package/repo-agent-governance/skills/cabloy-frontend-scaffold/SKILL.md +5 -1
  20. package/repo-agent-governance/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +6 -4
  21. package/repo-docs/backend/unit-testing.md +5 -1
  22. package/repo-docs/frontend/openapi-sdk-guide.md +2 -0
  23. package/repo-docs/frontend/scripts.md +8 -0
  24. package/repo-docs/reference/repo-scripts.md +5 -1
  25. package/scripts/generateOpenapiIsolated.ts +18 -1
  26. package/scripts/runIsolatedTest.test.ts +9 -1
  27. package/scripts/runIsolatedTest.ts +81 -18
  28. package/scripts/strictGenerationDiagnostics.test.ts +1009 -0
  29. package/scripts/strictGenerationDiagnostics.ts +61 -0
  30. package/vona/package.original.json +3 -2
  31. package/vona/packages-cli/cabloy-cli/package.json +1 -1
  32. package/vona/packages-cli/cli/package.json +1 -1
  33. package/vona/packages-cli/cli-set-api/package.json +1 -1
  34. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +14 -4
  35. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateEntryFiles.ts +6 -1
  36. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateVonaMeta.ts +2 -10
  37. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/validateGenerationPaths.ts +27 -0
  38. package/vona/packages-cli/cli-set-api/test/binDevLifecycle.test.ts +1 -1
  39. package/vona/packages-cli/cli-set-api/test/strictGenerationPrivacy.test.ts +439 -0
  40. package/vona/packages-utils/process-helper/package.json +1 -1
  41. package/vona/packages-utils/process-helper/src/index.test.ts +62 -0
  42. package/vona/packages-utils/process-helper/src/index.ts +9 -4
  43. package/vona/packages-vona/vona/package.json +1 -1
  44. package/vona/packages-vona/vona-core/package.json +1 -1
  45. package/vona/packages-vona/vona-core/src/lib/core/completedReadiness.test.ts +558 -0
  46. package/vona/packages-vona/vona-core/src/lib/core/completedReadiness.ts +389 -0
  47. package/vona/packages-vona/vona-core/src/lib/core/context.ts +3 -0
  48. package/vona/packages-vona/vona-core/src/lib/core/index.ts +11 -0
  49. package/vona/packages-vona/vona-core/src/lib/core/logger/appLogger.ts +10 -2
  50. package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.test.ts +268 -68
  51. package/vona/packages-vona/vona-core/src/lib/core/meta.ts +5 -0
  52. package/vona/packages-vona/vona-core/src/lib/core/testFreshness.ts +3 -9
  53. package/vona/packages-vona/vona-core/src/lib/core/testStrictMode.ts +9 -0
  54. package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +10 -0
  55. package/vona/packages-vona/vona-core/src/lib/framework/start.ts +8 -0
  56. package/vona/packages-vona/vona-mock/package.json +1 -1
  57. package/vona/pnpm-lock.yaml +542 -236
  58. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  59. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  60. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +124 -4
  61. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +7 -0
  62. package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExisting.test.ts +423 -0
  63. package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExistingCommit.test.ts +174 -0
  64. package/vona/src/suite-vendor/a-vona/modules/a-instance/package.json +1 -1
  65. package/vona/src/suite-vendor/a-vona/modules/a-instance/src/bean/bean.instance.ts +3 -1
  66. package/vona/src/suite-vendor/a-vona/modules/a-instance/src/lib/existingReadyAdmission.ts +125 -0
  67. package/vona/src/suite-vendor/a-vona/modules/a-instance/src/service/instance.ts +72 -25
  68. package/vona/src/suite-vendor/a-vona/modules/a-instance/test/existingReadyAdmission.test.ts +949 -0
  69. package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
  70. package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +61 -59
  71. package/vona/src/suite-vendor/a-vona/modules/a-mail/package.json +1 -1
  72. package/vona/src/suite-vendor/a-vona/modules/a-mail/src/service/mail.ts +5 -0
  73. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/package.json +1 -1
  74. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/index.ts +11 -0
  75. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/lib/protectedSqliteRegistry.ts +356 -0
  76. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  77. package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +61 -2
  78. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  79. package/zova/package.original.json +3 -3
  80. package/zova/packages-cli/cli/package.json +4 -4
  81. package/zova/packages-cli/cli-set-front/package.json +2 -2
  82. package/zova/packages-cli/cli-set-front/src/lib/common/restDependencyVersions.ts +22 -1
  83. package/zova/packages-cli/cli-set-front/test/restDependencies.test.ts +137 -0
  84. package/zova/packages-zova/zova/package.json +2 -2
  85. package/zova/pnpm-lock.yaml +448 -641
  86. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  87. package/zova/src/suite-vendor/a-zova/package.json +2 -2
@@ -0,0 +1,439 @@
1
+ import assert from 'node:assert/strict';
2
+ import { EventEmitter } from 'node:events';
3
+ import { registerHooks } from 'node:module';
4
+ import path from 'node:path';
5
+ import { after, test } from 'node:test';
6
+ import { setImmediate as nextTurn } from 'node:timers/promises';
7
+ import { runInNewContext } from 'node:vm';
8
+
9
+ const tools = new URL('../src/lib/bean/toolsBin/', import.meta.url);
10
+ const metaUrl = new URL('generateVonaMeta.ts', tools).href;
11
+ const entryUrl = new URL('generateEntryFiles.ts', tools).href;
12
+ const configUrl = new URL('configUtils.ts', tools).href;
13
+ const validationUrl = new URL('validateGenerationPaths.ts', tools).href;
14
+ const devUrl = new URL('../src/lib/bean/cli.bin.dev.ts', import.meta.url).href;
15
+ const core = new URL('../../../packages-vona/vona-core/src/lib/core/', import.meta.url);
16
+ const isolationUrl = new URL('testIsolation.ts', core).href;
17
+ const strictUrl = new URL('testStrictMode.ts', core).href;
18
+ const sourceUrls = [metaUrl, entryUrl, configUrl, validationUrl, devUrl, isolationUrl, strictUrl];
19
+ const runId = '20261020010203-a1b2c3d4';
20
+ const project = '/synthetic-project';
21
+ const generatedDir = `.vona-test-${runId}`;
22
+ const generatedRoot = path.join(project, generatedDir);
23
+ const dataRoot = path.join(project, `.app-test-${runId}`);
24
+ const secret = 'SYNTHETIC_ENV_VALUE_NEVER_PERSIST';
25
+ const escapedKey = 'selected"\\\nkey';
26
+ const ordinaryEnv: NodeJS.ProcessEnv = {
27
+ NODE_ENV: 'test',
28
+ META_FLAVOR: 'normal',
29
+ META_MODE: 'test',
30
+ };
31
+ const fakeProcess = Object.assign(new EventEmitter(), {
32
+ env: ordinaryEnv,
33
+ execPath: '/synthetic-node',
34
+ exit() {
35
+ throw new Error('Unexpected process exit');
36
+ },
37
+ });
38
+ let writes: Array<[string, string]> = [];
39
+ let actions: string[] = [];
40
+ let envLoads = 0;
41
+ let failTemplate = false;
42
+ let owners: Set<string>;
43
+ let spawned: { executable: string; args: string[]; options: any; child: any } | undefined;
44
+ let removed: string[] = [];
45
+ const nodemonEvents = new EventEmitter();
46
+ let nodemonOptions: any;
47
+ let resets = 0;
48
+ const nodemon = Object.assign(
49
+ (options: unknown) => {
50
+ nodemonOptions = options;
51
+ },
52
+ {
53
+ on(event: string, handler: (...args: any[]) => void) {
54
+ nodemonEvents.on(event, handler);
55
+ return nodemon;
56
+ },
57
+ emit(event: string, ...args: any[]) {
58
+ nodemonEvents.emit(event, ...args);
59
+ return nodemon;
60
+ },
61
+ reset(done: () => void) {
62
+ resets++;
63
+ nodemonEvents.removeAllListeners();
64
+ done();
65
+ },
66
+ },
67
+ );
68
+ const capture = (file: string, content = '') => {
69
+ actions.push('write');
70
+ writes.push([file, content]);
71
+ };
72
+ const fs = {
73
+ existsSync: () => true,
74
+ lstatSync: (file: string) => ({
75
+ isFile: () => owners.has(file),
76
+ isSymbolicLink: () => false,
77
+ }),
78
+ readFileSync: (file: string) => {
79
+ actions.push('owner');
80
+ assert.ok(owners.has(file));
81
+ return `${runId}\n`;
82
+ },
83
+ ensureFileSync: (file: string) => capture(file),
84
+ ensureDir: async (file: string) => capture(file),
85
+ writeFileSync: (file: string, content: string) => capture(file, content),
86
+ };
87
+ const fixture = {
88
+ process: fakeProcess,
89
+ fs,
90
+ nodemon,
91
+ loadEnvs() {
92
+ actions.push('env');
93
+ envLoads++;
94
+ return {
95
+ PRIVATE_SETTING: secret,
96
+ [escapedKey]: `${secret}-escaped`,
97
+ OMITTED: undefined,
98
+ EMPTY: '',
99
+ APP_NAME: `${secret}-dotenv-app`,
100
+ SERVER_LISTEN_PORT: '7102',
101
+ SERVER_LISTEN_HOSTNAME: `${secret}-dotenv-host`,
102
+ SERVER_WORKERS: '19',
103
+ META_MODE: 'prod',
104
+ };
105
+ },
106
+ async copyTemplateFile(_source: string, dest: string, vars: unknown) {
107
+ capture(dest, JSON.stringify(vars));
108
+ if (failTemplate && dest.endsWith('bootstrap.ts'))
109
+ throw new Error('Synthetic template failure');
110
+ },
111
+ spawn(executable: string, args: string[], options: any) {
112
+ const child = Object.assign(new EventEmitter(), {
113
+ signals: [] as string[],
114
+ kill(signal: string) {
115
+ this.signals.push(signal);
116
+ return true;
117
+ },
118
+ });
119
+ spawned = { executable, args, options, child };
120
+ return child;
121
+ },
122
+ async rimraf(root: string) {
123
+ removed.push(root);
124
+ },
125
+ };
126
+ const previous = Object.getOwnPropertyDescriptor(globalThis, '__strictCliPrivacy');
127
+ Object.defineProperty(globalThis, '__strictCliPrivacy', { configurable: true, value: fixture });
128
+ const access = 'globalThis.__strictCliPrivacy';
129
+ const mocks: Record<string, string> = {
130
+ 'vona-core': `export * from ${JSON.stringify(isolationUrl)}; export * from ${JSON.stringify(strictUrl)};`,
131
+ '@cabloy/cli': 'export class BeanCliBase {}',
132
+ 'fs-extra': `export default ${access}.fs;`,
133
+ 'node:fs': `export const lstatSync = ${access}.fs.lstatSync;`,
134
+ '@cabloy/dotenv': `export const loadEnvs = ${access}.loadEnvs; export function getEnvFiles() { return []; }`,
135
+ '@cabloy/module-glob':
136
+ 'export async function glob() { return { modules: {}, modulesArray: [] }; }',
137
+ 'node:os': 'export default { cpus() { throw new Error("Unexpected host CPU lookup"); } };',
138
+ 'chalk': 'export default { red: value => value };',
139
+ 'node:child_process': `export const spawn = ${access}.spawn;`,
140
+ 'nodemon': `export default ${access}.nodemon;`,
141
+ 'rimraf': `export const rimraf = ${access}.rimraf;`,
142
+ '../../../utils.ts': 'export function resolveTemplatePath(value) { return value; }',
143
+ '../../utils.ts': `
144
+ export const copyTemplateFile = ${access}.copyTemplateFile;
145
+ export function getEnvMeta(meta) { return { ...meta, local: 'local' }; }
146
+ export function getNodeEnv(mode) { return mode === 'test' ? 'test' : mode === 'dev' ? 'development' : 'production'; }
147
+ `,
148
+ '../utils.ts':
149
+ 'export function getImportEsm(dir = ".vona") { return "--import=./" + dir + "/register.js"; }',
150
+ };
151
+ // Only these real modules receive the synthetic process and virtual collaborators.
152
+ // There are no child processes, application/env files, listeners, drivers or cleanup on disk.
153
+ const hooks = registerHooks({
154
+ resolve(specifier, context, nextResolve) {
155
+ if (sourceUrls.includes(context.parentURL ?? '') && mocks[specifier]) {
156
+ return {
157
+ url: `data:text/javascript,${encodeURIComponent(mocks[specifier])}`,
158
+ shortCircuit: true,
159
+ };
160
+ }
161
+ return nextResolve(specifier, context);
162
+ },
163
+ load(url, context, nextLoad) {
164
+ const loaded = nextLoad(url, context);
165
+ if (!sourceUrls.includes(url)) return loaded;
166
+ return { ...loaded, source: `const process = ${access}.process;\n${loaded.source}` };
167
+ },
168
+ });
169
+ let generateVonaMeta: any;
170
+ let generateEntryFiles: any;
171
+ let CliBinDev: any;
172
+ try {
173
+ ({ generateVonaMeta } = await import(metaUrl));
174
+ ({ generateEntryFiles } = await import(entryUrl));
175
+ ({ CliBinDev } = await import(devUrl));
176
+ } finally {
177
+ hooks.deregister();
178
+ }
179
+ after(() => {
180
+ if (previous) Object.defineProperty(globalThis, '__strictCliPrivacy', previous);
181
+ else Reflect.deleteProperty(globalThis, '__strictCliPrivacy');
182
+ });
183
+
184
+ function reset(strict = true) {
185
+ fakeProcess.removeAllListeners();
186
+ fakeProcess.env = {
187
+ ...ordinaryEnv,
188
+ ...(strict ? { VONA_TEST_REQUIRE_FRESH_RESOURCES: 'true', VONA_TEST_RUN_ID: runId } : {}),
189
+ SERVER_LISTEN_PORT: '43123',
190
+ SERVER_LISTEN_HOSTNAME: 'inherited-host',
191
+ SERVER_WORKERS: '23',
192
+ APP_NAME: 'inherited-app',
193
+ PRIVATE_SETTING: 'inherited-value',
194
+ UNRELATED_OS_KEY: 'not-selected',
195
+ VONA_TEST_SERVER_ORIGIN: 'true',
196
+ VONA_TEST_READY_FILE: '/synthetic-ready',
197
+ VONA_TEST_CLAIM_TOKEN: 'synthetic-claim-control',
198
+ };
199
+ owners = new Set([dataRoot, generatedRoot].map(root => path.join(root, '.vona-test-owner')));
200
+ writes = [];
201
+ actions = [];
202
+ envLoads = 0;
203
+ failTemplate = false;
204
+ spawned = undefined;
205
+ removed = [];
206
+ nodemonEvents.removeAllListeners();
207
+ nodemonOptions = undefined;
208
+ resets = 0;
209
+ }
210
+ const meta = { flavor: 'normal', mode: 'test' };
211
+ const options = { appDir: project, runtimeDir: generatedDir, workers: 1 };
212
+ function cli(mode = 'test') {
213
+ const instance = Object.create(CliBinDev.prototype);
214
+ Object.defineProperty(instance, 'context', { value: { argv: { mode } } });
215
+ return instance;
216
+ }
217
+ function envSource() {
218
+ return writes.find(([file, content]) => file.endsWith('/env.ts') && content)?.[1] ?? '';
219
+ }
220
+ function assertPrivateWrites() {
221
+ assert.ok(writes.length > 0);
222
+ assert.ok(!JSON.stringify(writes).includes(secret), 'no resolved sentinel in any fake write');
223
+ }
224
+ function projectEnv(source: string, env: NodeJS.ProcessEnv) {
225
+ const js = source
226
+ .replace('export default ', 'globalThis.result = ')
227
+ .replace(' as NodeJS.ProcessEnv', '');
228
+ const context = { process: { env }, result: undefined as any };
229
+ runInNewContext(js, context);
230
+ return JSON.parse(JSON.stringify(context.result));
231
+ }
232
+
233
+ test('strict generation and direct writer reject invalid mode/run/directory/owners before env or writes', async () => {
234
+ const cases = [
235
+ 'flag',
236
+ 'mode',
237
+ 'missing',
238
+ 'malformed',
239
+ 'directory',
240
+ 'data-owner',
241
+ 'generated-owner',
242
+ ];
243
+ for (const name of cases) {
244
+ for (const direct of [false, true]) {
245
+ reset();
246
+ const configMeta = { ...meta };
247
+ const configOptions = { ...options };
248
+ if (name === 'flag') fakeProcess.env.VONA_TEST_REQUIRE_FRESH_RESOURCES = 'false';
249
+ if (name === 'mode') configMeta.mode = 'dev';
250
+ if (name === 'missing') delete fakeProcess.env.VONA_TEST_RUN_ID;
251
+ if (name === 'malformed') fakeProcess.env.VONA_TEST_RUN_ID = 'not-a-run';
252
+ if (name === 'directory') configOptions.runtimeDir = '.vona';
253
+ if (name === 'data-owner') owners.delete(path.join(dataRoot, '.vona-test-owner'));
254
+ if (name === 'generated-owner') owners.delete(path.join(generatedRoot, '.vona-test-owner'));
255
+ await assert.rejects(
256
+ direct
257
+ ? generateEntryFiles(
258
+ configMeta,
259
+ configOptions,
260
+ { modules: {}, modulesArray: [] },
261
+ { PRIVATE_SETTING: secret },
262
+ )
263
+ : generateVonaMeta(configMeta, configOptions),
264
+ );
265
+ assert.equal(envLoads, 0, name);
266
+ assert.deepEqual(writes, [], name);
267
+ }
268
+ }
269
+ });
270
+
271
+ test('strict generation writes only selected keys, including on later failure; projection reads child env', async () => {
272
+ reset();
273
+ const { env } = await generateVonaMeta(meta, options);
274
+ assert.equal(envLoads, 1);
275
+ assert.ok(actions.indexOf('owner') < actions.indexOf('env'));
276
+ assertPrivateWrites();
277
+ const source = envSource();
278
+ assert.ok(source);
279
+ const childEnv = { ...fakeProcess.env, ...env };
280
+ assert.deepEqual(projectEnv(source, childEnv), JSON.parse(JSON.stringify(env)));
281
+ assert.equal(projectEnv(source, childEnv).UNRELATED_OS_KEY, undefined);
282
+ assert.equal(projectEnv(source, childEnv).OMITTED, undefined);
283
+ assert.equal(
284
+ projectEnv(source, { ...childEnv, PRIVATE_SETTING: 'runtime-value' }).PRIVATE_SETTING,
285
+ 'runtime-value',
286
+ );
287
+ assert.equal(
288
+ projectEnv(source, { ...childEnv, PRIVATE_SETTING: undefined }).PRIVATE_SETTING,
289
+ undefined,
290
+ );
291
+ assert.equal(projectEnv(source, childEnv)[escapedKey], `${secret}-escaped`);
292
+ reset();
293
+ failTemplate = true;
294
+ await assert.rejects(generateVonaMeta(meta, options), /Synthetic template failure/);
295
+ assertPrivateWrites();
296
+ assert.ok(envSource(), 'failed generation retains a value-free env entry');
297
+ });
298
+
299
+ async function waitForSpawn() {
300
+ let launch = spawned;
301
+ while (!launch) {
302
+ await nextTurn();
303
+ launch = spawned;
304
+ }
305
+ return launch;
306
+ }
307
+
308
+ test('strict _dev hands resolved derived values to spawn and preserves inherited control keys', async () => {
309
+ reset();
310
+ const running = cli()._dev(project);
311
+ const { executable, args, options: launch, child } = await waitForSpawn();
312
+ assert.equal(executable, '/synthetic-node');
313
+ assert.deepEqual(args, [
314
+ `--import=./${generatedDir}/register.js`,
315
+ `${generatedDir}/bootstrap.ts`,
316
+ ]);
317
+ assert.equal(launch.cwd, project);
318
+ assert.equal(launch.stdio, 'inherit');
319
+ assert.equal(launch.env.PRIVATE_SETTING, secret);
320
+ assert.equal(launch.env.APP_NAME, `cabtest${runId}`);
321
+ assert.equal(launch.env.SERVER_LISTEN_PORT, '43123');
322
+ assert.equal(launch.env.SERVER_LISTEN_HOSTNAME, '127.0.0.1');
323
+ assert.equal(launch.env.SERVER_SERVE_HOST, '127.0.0.1:43123');
324
+ assert.equal(launch.env.SERVER_SERVE_PROTOCOL, 'http');
325
+ assert.equal(launch.env.SERVER_WORKERS, '1');
326
+ assert.equal(launch.env.META_MODE, 'test');
327
+ for (const key of [
328
+ 'VONA_TEST_REQUIRE_FRESH_RESOURCES',
329
+ 'VONA_TEST_RUN_ID',
330
+ 'VONA_TEST_READY_FILE',
331
+ 'VONA_TEST_CLAIM_TOKEN',
332
+ ]) {
333
+ assert.equal(launch.env[key], fakeProcess.env[key]);
334
+ }
335
+ assert.deepEqual(
336
+ projectEnv(envSource(), launch.env),
337
+ JSON.parse(
338
+ JSON.stringify({
339
+ ...fixture.loadEnvs(),
340
+ NODE_ENV: 'test',
341
+ META_FLAVOR: 'normal',
342
+ META_MODE: 'test',
343
+ VONA_TEST_RUN_ID: runId,
344
+ APP_NAME: `cabtest${runId}`,
345
+ SERVER_LISTEN_PORT: '43123',
346
+ SERVER_LISTEN_HOSTNAME: '127.0.0.1',
347
+ SERVER_SERVE_PROTOCOL: 'http',
348
+ SERVER_SERVE_HOST: '127.0.0.1:43123',
349
+ SERVER_WORKERS: '1',
350
+ }),
351
+ ),
352
+ );
353
+ fakeProcess.emit('SIGINT');
354
+ fakeProcess.emit('SIGTERM');
355
+ assert.deepEqual(child.signals, ['SIGINT', 'SIGTERM']);
356
+ child.emit('exit', 0, null);
357
+ await running;
358
+ assert.equal(fakeProcess.listenerCount('SIGINT'), 0);
359
+ assert.equal(fakeProcess.listenerCount('SIGTERM'), 0);
360
+ assert.deepEqual(removed, []);
361
+ assertPrivateWrites();
362
+ });
363
+
364
+ test('test-mode nonzero exits, signals and spawn errors reject and remove forwarding listeners', async () => {
365
+ for (const failure of ['code', 'signal', 'error']) {
366
+ reset();
367
+ const running = cli()._runTest(project, { PRIVATE_SETTING: secret });
368
+ const rejected = assert.rejects(
369
+ running,
370
+ failure === 'code'
371
+ ? /exited: 7/
372
+ : failure === 'signal'
373
+ ? /exited: SIGTERM/
374
+ : /Synthetic spawn failure/,
375
+ );
376
+ if (failure === 'error') {
377
+ spawned!.child.emit('error', new Error('Synthetic spawn failure'));
378
+ } else {
379
+ spawned!.child.emit(
380
+ 'exit',
381
+ failure === 'code' ? 7 : null,
382
+ failure === 'signal' ? 'SIGTERM' : null,
383
+ );
384
+ }
385
+ await rejected;
386
+ assert.equal(fakeProcess.listenerCount('SIGINT'), 0);
387
+ assert.equal(fakeProcess.listenerCount('SIGTERM'), 0);
388
+ assert.deepEqual(writes, []);
389
+ }
390
+ reset();
391
+ await assert.rejects(cli()._runTest(project), /resolved environment handoff/);
392
+ assert.equal(spawned, undefined);
393
+ });
394
+
395
+ test('ordinary test generation retains literal env and inherited spawn semantics', async () => {
396
+ reset(false);
397
+ const { env } = await generateVonaMeta(meta, { ...options, runtimeDir: '.vona' });
398
+ assert.equal(
399
+ envSource(),
400
+ `export default ${JSON.stringify(env, null, 2)} as unknown as NodeJS.ProcessEnv;\n`,
401
+ );
402
+ assert.ok(envSource().includes(secret));
403
+ assert.ok(!actions.includes('owner'));
404
+ const running = cli()._dev(project);
405
+ const launch = await waitForSpawn();
406
+ assert.equal(Object.hasOwn(launch.options, 'env'), false);
407
+ assert.equal(launch.args[1], '.vona/bootstrap.ts');
408
+ launch.child.emit('exit', 0, null);
409
+ await running;
410
+ assert.deepEqual(removed, [path.join(project, '.vona')]);
411
+ });
412
+
413
+ test('ordinary nodemon restart/quit and crash lifecycle remains supervised in memory', async () => {
414
+ for (const crash of [false, true]) {
415
+ reset(false);
416
+ const running = cli('dev')._run(project, {});
417
+ assert.equal(nodemonOptions.stdout, undefined);
418
+ assert.equal(nodemonOptions.stderr, undefined);
419
+ if (crash) {
420
+ const rejected = assert.rejects(running, /Development server crashed/);
421
+ nodemon.emit('crash');
422
+ nodemon.emit('crash');
423
+ await rejected;
424
+ assert.equal(resets, 1);
425
+ } else {
426
+ let settled = false;
427
+ running.then(() => {
428
+ settled = true;
429
+ });
430
+ nodemon.emit('start');
431
+ nodemon.emit('exit');
432
+ await nextTurn();
433
+ assert.equal(settled, false);
434
+ nodemon.emit('quit');
435
+ await running;
436
+ assert.equal(resets, 0);
437
+ }
438
+ }
439
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabloy/process-helper",
3
- "version": "3.1.8",
3
+ "version": "3.1.9",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "cabloy process-helper",
6
6
  "keywords": [
@@ -0,0 +1,62 @@
1
+ import assert from 'node:assert/strict';
2
+ import ChildProcess from 'node:child_process';
3
+ import { PassThrough } from 'node:stream';
4
+ import { describe, it } from 'node:test';
5
+
6
+ import { ProcessHelper } from './index.ts';
7
+
8
+ describe('ProcessHelper child termination', { concurrency: false }, () => {
9
+ for (const { code, signal } of [
10
+ { code: 0, signal: null },
11
+ { code: 7, signal: null },
12
+ { code: null, signal: 'SIGKILL' },
13
+ { code: null, signal: null },
14
+ ] as const) {
15
+ it(`handles exit code ${code} and signal ${signal}`, async t => {
16
+ const child = new ChildProcess.ChildProcess();
17
+ t.mock.method(ChildProcess, 'spawn', () => child);
18
+ const result = new ProcessHelper().spawn({ cmd: 'test-owned-command' });
19
+ child.emit('exit', code, signal);
20
+ if (code === 0) {
21
+ assert.equal(await result, '');
22
+ } else {
23
+ await assert.rejects(result, (error: Error & { code?: number; signal?: string }) => {
24
+ if (code !== null) {
25
+ assert.equal(error.code, 10000 + code);
26
+ assert.match(error.message, /exit code: 7/);
27
+ } else {
28
+ assert.equal(error.signal, signal);
29
+ assert.match(error.message, signal ? /SIGKILL/ : /exit code: null/);
30
+ }
31
+ return true;
32
+ });
33
+ }
34
+ });
35
+ }
36
+
37
+ it('retains the termination signal alongside stderr', async t => {
38
+ const child = new ChildProcess.ChildProcess();
39
+ child.stderr = new PassThrough();
40
+ t.mock.method(ChildProcess, 'spawn', () => child);
41
+ const helper = new ProcessHelper();
42
+ t.mock.method(helper.console, 'log', async () => {});
43
+ const result = helper.spawn({ cmd: 'test-owned-command' });
44
+ child.stderr.emit('data', Buffer.from('controlled_stderr'));
45
+ child.emit('exit', null, 'SIGTERM');
46
+ await assert.rejects(result, (error: Error & { signal?: string }) => {
47
+ assert.equal(error.signal, 'SIGTERM');
48
+ assert.match(error.message, /controlled_stderr/);
49
+ assert.match(error.message, /SIGTERM/);
50
+ return true;
51
+ });
52
+ });
53
+
54
+ it('preserves spawn failures', async t => {
55
+ const child = new ChildProcess.ChildProcess();
56
+ t.mock.method(ChildProcess, 'spawn', () => child);
57
+ const failure = new Error('controlled_spawn_failure');
58
+ const result = new ProcessHelper().spawn({ cmd: 'test-owned-command' });
59
+ child.emit('error', failure);
60
+ await assert.rejects(result, error => error === failure);
61
+ });
62
+ });
@@ -172,12 +172,17 @@ export class ProcessHelper {
172
172
  proc.once('error', err => {
173
173
  reject(err);
174
174
  });
175
- proc.once('exit', code => {
175
+ proc.once('exit', (code, signal) => {
176
176
  // if (options.gracefull) childs.delete(proc);
177
- if (code) {
178
- const message = stderr || `spawn ${cmd} ${args.join(' ')} fail, exit code: ${code}`;
177
+ if (code !== 0) {
178
+ const status = signal ? `signal: ${signal}` : `exit code: ${code}`;
179
+ const message =
180
+ signal || code === null
181
+ ? `${stderr ? `${stderr}\n` : ''}spawn ${cmd} ${args.join(' ')} fail, ${status}`
182
+ : stderr || `spawn ${cmd} ${args.join(' ')} fail, ${status}`;
179
183
  const err = new Error(message);
180
- (err as any).code = 10000 + Number(code);
184
+ if (code !== null) (err as any).code = 10000 + code;
185
+ (err as any).signal = signal;
181
186
  return reject(err);
182
187
  }
183
188
  resolve(stdout);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.111",
3
+ "version": "5.1.113",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-core",
3
- "version": "5.1.48",
3
+ "version": "5.1.50",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona",
6
6
  "keywords": [