cabloy 5.1.220 → 5.1.221

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 (49) 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 +11 -0
  14. package/package.json +4 -3
  15. package/repo-docs/frontend/openapi-sdk-guide.md +2 -0
  16. package/repo-docs/reference/repo-scripts.md +5 -1
  17. package/scripts/generateOpenapiIsolated.ts +18 -1
  18. package/scripts/runIsolatedTest.test.ts +9 -1
  19. package/scripts/runIsolatedTest.ts +81 -18
  20. package/scripts/strictGenerationDiagnostics.test.ts +1009 -0
  21. package/scripts/strictGenerationDiagnostics.ts +61 -0
  22. package/vona/package.original.json +3 -2
  23. package/vona/packages-cli/cli/package.json +1 -1
  24. package/vona/packages-cli/cli-set-api/package.json +1 -1
  25. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +14 -4
  26. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateEntryFiles.ts +6 -1
  27. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateVonaMeta.ts +2 -10
  28. package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/validateGenerationPaths.ts +27 -0
  29. package/vona/packages-cli/cli-set-api/test/binDevLifecycle.test.ts +1 -1
  30. package/vona/packages-cli/cli-set-api/test/strictGenerationPrivacy.test.ts +439 -0
  31. package/vona/packages-vona/vona/package.json +1 -1
  32. package/vona/packages-vona/vona-core/package.json +1 -1
  33. package/vona/packages-vona/vona-core/src/lib/core/logger/appLogger.ts +10 -2
  34. package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.test.ts +268 -68
  35. package/vona/packages-vona/vona-core/src/lib/core/testFreshness.ts +3 -9
  36. package/vona/packages-vona/vona-core/src/lib/core/testStrictMode.ts +9 -0
  37. package/vona/packages-vona/vona-mock/package.json +1 -1
  38. package/vona/pnpm-lock.yaml +542 -236
  39. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  40. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/package.json +1 -1
  41. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/index.ts +11 -0
  42. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/lib/protectedSqliteRegistry.ts +356 -0
  43. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  44. package/zova/package.original.json +3 -2
  45. package/zova/packages-cli/cli/package.json +2 -2
  46. package/zova/packages-cli/cli-set-front/package.json +1 -1
  47. package/zova/packages-cli/cli-set-front/src/lib/common/restDependencyVersions.ts +22 -1
  48. package/zova/packages-cli/cli-set-front/test/restDependencies.test.ts +137 -0
  49. package/zova/pnpm-lock.yaml +442 -564
@@ -0,0 +1,61 @@
1
+ import type { StdioOptions } from 'node:child_process';
2
+
3
+ import { Writable } from 'node:stream';
4
+
5
+ import { requiresFreshTestResources } from '../vona/packages-vona/vona-core/src/lib/core/testStrictMode.ts';
6
+
7
+ export type StrictGenerationStage =
8
+ | 'arguments'
9
+ | 'preparation'
10
+ | 'startup'
11
+ | 'operation'
12
+ | 'shutdown'
13
+ | 'operation-and-shutdown'
14
+ | 'generator'
15
+ | 'cleanup'
16
+ | 'run';
17
+
18
+ const failureStages = new WeakMap<object, StrictGenerationStage>();
19
+
20
+ export class StrictGenerationError extends Error {
21
+ readonly stage: StrictGenerationStage;
22
+
23
+ constructor(stage: StrictGenerationStage) {
24
+ super(`Strict contract generation failed: ${stage}`);
25
+ this.name = 'StrictGenerationError';
26
+ this.stage = stage;
27
+ failureStages.set(this, stage);
28
+ }
29
+ }
30
+
31
+ export function strictGenerationFailure(stage: StrictGenerationStage, error?: unknown) {
32
+ const previous = error && typeof error === 'object' ? failureStages.get(error) : undefined;
33
+ return new StrictGenerationError(previous ?? stage);
34
+ }
35
+
36
+ export function usesStrictGeneration(env: NodeJS.ProcessEnv): boolean {
37
+ try {
38
+ return requiresFreshTestResources(env);
39
+ } catch {
40
+ throw new StrictGenerationError('arguments');
41
+ }
42
+ }
43
+
44
+ export function guardianStdio(strict: boolean): StdioOptions {
45
+ return strict ? ['ignore', 'ignore', 'ignore', 'ipc'] : ['inherit', 'inherit', 'inherit', 'ipc'];
46
+ }
47
+
48
+ export function discardNativeOutput() {
49
+ const discard = () =>
50
+ new Writable({
51
+ write(_chunk, _encoding, callback) {
52
+ callback();
53
+ },
54
+ });
55
+ return { stdout: discard(), stderr: discard() };
56
+ }
57
+
58
+ export function reportIsolatedDiagnostic(strict: boolean, category: string, value?: unknown) {
59
+ if (strict) console.error(`Strict contract generation: ${category}`);
60
+ else console.error(value);
61
+ }
@@ -2,7 +2,7 @@
2
2
  "name": "vona",
3
3
  "private": true,
4
4
  "type": "module",
5
- "packageManager": "pnpm@11.5.2",
5
+ "packageManager": "pnpm@12.14.0",
6
6
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
7
7
  "repository": {
8
8
  "type": "git",
@@ -15,7 +15,8 @@
15
15
  },
16
16
  "homepage": "https://github.com/vonajs/vona#readme",
17
17
  "engines": {
18
- "node": ">=24.4.0"
18
+ "node": ">=24.4.0",
19
+ "pnpm": ">=11.5.2"
19
20
  },
20
21
  "scripts": {
21
22
  "vona": "node ./packages-cli/cli/src/bin/vona.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.166",
3
+ "version": "1.1.167",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.164",
3
+ "version": "1.1.165",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -5,7 +5,7 @@ import { BeanCliBase } from '@cabloy/cli';
5
5
  import { spawn } from 'node:child_process';
6
6
  import nodemon from 'nodemon';
7
7
  import { rimraf } from 'rimraf';
8
- import { resolveVonaLocalPaths } from 'vona-core';
8
+ import { requiresFreshTestResources, resolveVonaLocalPaths } from 'vona-core';
9
9
 
10
10
  import type { VonaBinConfigOptions } from './toolsBin/types.ts';
11
11
 
@@ -48,16 +48,21 @@ export class CliBinDev extends BeanCliBase {
48
48
  const workers = resolveTestWorkers(mode, argv.workers);
49
49
  const flavor: VonaMetaFlavor = argv.flavor || 'normal';
50
50
  const configMeta: VonaConfigMeta = { flavor, mode };
51
+ const strict = requiresFreshTestResources();
51
52
  const paths = resolveVonaLocalPaths(projectPath, mode);
53
+ if (strict && (mode !== 'test' || !paths.runId)) {
54
+ throw new Error('Strict generation requires an isolated test run in test mode');
55
+ }
52
56
  const configOptions: VonaBinConfigOptions = {
53
57
  appDir: projectPath,
54
58
  runtimeDir: paths.generatedDir,
55
59
  workers,
56
60
  };
57
61
  try {
58
- const { modulesMeta } = await generateVonaMeta(configMeta, configOptions);
62
+ const { env, modulesMeta } = await generateVonaMeta(configMeta, configOptions);
59
63
  if (mode === 'test') {
60
- await this._runTest(projectPath);
64
+ if (strict) await this._runTest(projectPath, env);
65
+ else await this._runTest(projectPath);
61
66
  } else {
62
67
  await this._run(projectPath, modulesMeta);
63
68
  }
@@ -66,7 +71,11 @@ export class CliBinDev extends BeanCliBase {
66
71
  }
67
72
  }
68
73
 
69
- async _runTest(projectPath: string) {
74
+ async _runTest(projectPath: string, resolvedEnv?: NodeJS.ProcessEnv) {
75
+ const strict = requiresFreshTestResources();
76
+ if (strict && !resolvedEnv) {
77
+ throw new Error('Strict generation requires resolved environment handoff');
78
+ }
70
79
  const generatedDir = resolveVonaLocalPaths(projectPath, 'test').generatedDir;
71
80
  const child = spawn(
72
81
  process.execPath,
@@ -74,6 +83,7 @@ export class CliBinDev extends BeanCliBase {
74
83
  {
75
84
  cwd: projectPath,
76
85
  stdio: 'inherit',
86
+ ...(strict ? { env: { ...process.env, ...resolvedEnv } } : {}),
77
87
  },
78
88
  );
79
89
  const forwardSignal = (signal: NodeJS.Signals) => child.kill(signal);
@@ -10,6 +10,7 @@ import type { VonaBinConfigOptions } from './types.ts';
10
10
 
11
11
  import { resolveTemplatePath } from '../../../utils.ts';
12
12
  import { copyTemplateFile, getEnvMeta } from '../../utils.ts';
13
+ import { validateGenerationPaths } from './validateGenerationPaths.ts';
13
14
  // import { generateZod } from './generateZod.ts';
14
15
 
15
16
  export async function generateEntryFiles(
@@ -18,6 +19,7 @@ export async function generateEntryFiles(
18
19
  modulesMeta: Awaited<ReturnType<typeof glob>>,
19
20
  env: NodeJS.ProcessEnv,
20
21
  ) {
22
+ const strict = validateGenerationPaths(configMeta, configOptions);
21
23
  // config
22
24
  await __generateConfig();
23
25
  // modules meta
@@ -103,7 +105,10 @@ export async function generateEntryFiles(
103
105
  }
104
106
 
105
107
  async function __generateEnvJson() {
106
- const contentDest = `export default ${JSON.stringify(env, null, 2)} as unknown as NodeJS.ProcessEnv;\n`;
108
+ // Strict generation retains the selected app.meta.env surface, never its resolved values.
109
+ const contentDest = strict
110
+ ? `const keys = ${JSON.stringify(Object.keys(env).filter(key => env[key] !== undefined))};\nexport default Object.fromEntries(keys.filter(key => process.env[key] !== undefined).map(key => [key, process.env[key]])) as NodeJS.ProcessEnv;\n`
111
+ : `export default ${JSON.stringify(env, null, 2)} as unknown as NodeJS.ProcessEnv;\n`;
107
112
  // output
108
113
  const fileDest = path.join(configOptions.appDir, configOptions.runtimeDir, 'env.ts');
109
114
  fse.ensureFileSync(fileDest);
@@ -1,24 +1,16 @@
1
1
  import type { VonaConfigMeta } from '@cabloy/module-info';
2
2
 
3
- import { assertTestRunOwner, resolveVonaLocalPaths } from 'vona-core';
4
-
5
3
  import type { VonaBinConfigOptions } from './types.ts';
6
4
 
7
5
  import { createConfigUtils } from './configUtils.ts';
8
6
  import { generateEntryFiles } from './generateEntryFiles.ts';
7
+ import { validateGenerationPaths } from './validateGenerationPaths.ts';
9
8
 
10
9
  export async function generateVonaMeta(
11
10
  configMeta: VonaConfigMeta,
12
11
  configOptions: VonaBinConfigOptions,
13
12
  ) {
14
- const paths = resolveVonaLocalPaths(configOptions.appDir, configMeta.mode);
15
- if (paths.runId) {
16
- if (configOptions.runtimeDir !== paths.generatedDir) {
17
- throw new Error('Isolated test generated directory does not match its run ID');
18
- }
19
- assertTestRunOwner(paths.dataRoot, paths.runId);
20
- assertTestRunOwner(paths.generatedRoot, paths.runId);
21
- }
13
+ validateGenerationPaths(configMeta, configOptions);
22
14
  // config utils
23
15
  const configUtils = createConfigUtils(configMeta, configOptions);
24
16
  // env
@@ -0,0 +1,27 @@
1
+ import type { VonaConfigMeta } from '@cabloy/module-info';
2
+
3
+ import { assertTestRunOwner, requiresFreshTestResources, resolveVonaLocalPaths } from 'vona-core';
4
+
5
+ import type { VonaBinConfigOptions } from './types.ts';
6
+
7
+ export function validateGenerationPaths(
8
+ configMeta: VonaConfigMeta,
9
+ configOptions: VonaBinConfigOptions,
10
+ ) {
11
+ const strict = requiresFreshTestResources();
12
+ if (strict && configMeta.mode !== 'test') {
13
+ throw new Error('Strict generation requires test mode');
14
+ }
15
+ const paths = resolveVonaLocalPaths(configOptions.appDir, configMeta.mode);
16
+ if (strict && !paths.runId) {
17
+ throw new Error('Strict generation requires an isolated test run');
18
+ }
19
+ if (paths.runId) {
20
+ if (configOptions.runtimeDir !== paths.generatedDir) {
21
+ throw new Error('Isolated test generated directory does not match its run ID');
22
+ }
23
+ assertTestRunOwner(paths.dataRoot, paths.runId);
24
+ assertTestRunOwner(paths.generatedRoot, paths.runId);
25
+ }
26
+ return strict;
27
+ }
@@ -26,7 +26,7 @@ const source = `
26
26
  '@cabloy/cli': 'export class BeanCliBase {}',
27
27
  'nodemon': 'export default globalThis.nodemonMock;',
28
28
  'rimraf': 'export function rimraf() { throw new Error("Unexpected cleanup"); }',
29
- 'vona-core': 'export function resolveVonaLocalPaths() { throw new Error("Unexpected app access"); }',
29
+ 'vona-core': 'export function resolveVonaLocalPaths() { throw new Error("Unexpected app access"); } export function requiresFreshTestResources() { return false; }',
30
30
  '../utils.ts': 'export function getImportEsm() { return "--import=./.vona/register.js"; }',
31
31
  './toolsBin/generateVonaMeta.ts': 'export function generateVonaMeta() { throw new Error("Unexpected generation"); }',
32
32
  };
@@ -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
+ });