cabloy 5.1.218 → 5.1.220
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cabloy-version +1 -1
- package/.github/workflows/create-cabloy.yml +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/repo-docs/fullstack/quickstart.md +3 -3
- package/repo-docs/reference/repo-scripts.md +12 -0
- package/scripts/generateOpenapiIsolated.test.ts +25 -0
- package/scripts/generateOpenapiIsolated.ts +28 -11
- package/scripts/runIsolatedTest.test.ts +157 -45
- package/scripts/runIsolatedTest.ts +30 -6
- package/scripts/strictGenerationStartup.test.ts +212 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- 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/bean/beanBase.ts +4 -0
- package/vona/packages-vona/vona-core/src/lib/core/asyncLocalStorage.ts +84 -12
- package/vona/packages-vona/vona-core/src/lib/core/index.ts +14 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/appLogger.ts +11 -3
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.test.ts +146 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.ts +138 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/index.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/utils.ts +25 -5
- package/vona/packages-vona/vona-core/src/lib/core/requestBoundary.ts +89 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.test.ts +379 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.ts +238 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +4 -3
- package/vona/packages-vona/vona-core/src/lib/framework/start.ts +2 -0
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +218 -43
- package/vona/pnpm-workspace.yaml +1 -1
- 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 +111 -60
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/service/executor.ts +6 -6
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +13 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/lib/jwtClientLogging.test.ts +182 -0
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +2 -8
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/aopMethod.log.ts +4 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/bean.logger.ts +9 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +27 -14
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.test.ts +207 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.ts +30 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +33 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +6 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/bean.telemetry.ts +8 -1
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/middlewareSystem.trace.ts +9 -14
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/lib/spanDiagnostics.ts +70 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/service/telemetry.ts +37 -23
- package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/database.ts +31 -4
- package/vona/src/suite-vendor/a-vona/package.json +2 -2
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.bin.buildRest.ts +11 -101
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencies.ts +78 -0
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencyVersions.ts +231 -0
- package/zova/packages-cli/cli-set-front/test/restDependencies.test.ts +431 -0
- package/zova/pnpm-lock.yaml +8 -8
- /package/vona/patches/{zova-core@5.1.92.patch → zova-core@5.1.93.patch} +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type * as Winston from 'winston';
|
|
2
|
+
|
|
3
|
+
import { captureRequestBoundary, getRequestBoundary } from '../requestBoundary.ts';
|
|
4
|
+
|
|
5
|
+
const loggingMethods = new Set([
|
|
6
|
+
'log',
|
|
7
|
+
'error',
|
|
8
|
+
'warn',
|
|
9
|
+
'info',
|
|
10
|
+
'http',
|
|
11
|
+
'verbose',
|
|
12
|
+
'debug',
|
|
13
|
+
'silly',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function completeWrite(args: unknown[]) {
|
|
17
|
+
const callback = typeof args[1] === 'function' ? args[1] : args[2];
|
|
18
|
+
if (typeof callback === 'function') {
|
|
19
|
+
try {
|
|
20
|
+
callback();
|
|
21
|
+
} catch {
|
|
22
|
+
captureRequestBoundary()?.reportFailure('diagnostic_failed');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const suppressedLogger = createFacade();
|
|
29
|
+
|
|
30
|
+
export function getSuppressedDiagnosticLogger(): Winston.Logger {
|
|
31
|
+
return suppressedLogger;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createDiagnosticLogger(raw: Winston.Logger): Winston.Logger {
|
|
35
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
36
|
+
return createFacade(raw);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createFacade(raw?: Winston.Logger): Winston.Logger {
|
|
40
|
+
const methods = new Map<PropertyKey, (...args: any[]) => any>();
|
|
41
|
+
const handlers = new WeakMap<object, Winston.ExceptionHandler | Winston.RejectionHandler>();
|
|
42
|
+
const inertProfiler = Object.freeze({
|
|
43
|
+
get logger() {
|
|
44
|
+
return getSuppressedDiagnosticLogger();
|
|
45
|
+
},
|
|
46
|
+
done: () => getSuppressedDiagnosticLogger(),
|
|
47
|
+
});
|
|
48
|
+
const facade = new Proxy({} as Winston.Logger, {
|
|
49
|
+
get(_target, key) {
|
|
50
|
+
if (
|
|
51
|
+
loggingMethods.has(key as string) ||
|
|
52
|
+
['write', 'child', 'startTimer', 'profile'].includes(key as string)
|
|
53
|
+
) {
|
|
54
|
+
if (!methods.has(key)) {
|
|
55
|
+
methods.set(key, (...args: any[]) => {
|
|
56
|
+
if (!raw || getRequestBoundary()) {
|
|
57
|
+
if (key === 'write') return completeWrite(args);
|
|
58
|
+
if (key === 'child') return getSuppressedDiagnosticLogger();
|
|
59
|
+
if (key === 'startTimer') return inertProfiler;
|
|
60
|
+
return facade;
|
|
61
|
+
}
|
|
62
|
+
const result = Reflect.apply(Reflect.get(raw, key, raw), raw, args);
|
|
63
|
+
if (key === 'child') return createFacade(result as Winston.Logger);
|
|
64
|
+
if (key === 'startTimer') return wrapProfiler(result as Winston.Profiler, facade);
|
|
65
|
+
return result === raw ? facade : result;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return methods.get(key);
|
|
69
|
+
}
|
|
70
|
+
if (!raw) return undefined;
|
|
71
|
+
const value = Reflect.get(raw, key, raw);
|
|
72
|
+
if ((key === 'exceptions' || key === 'rejections') && value) {
|
|
73
|
+
if (!handlers.has(value)) handlers.set(value, wrapHandler(value, raw, facade));
|
|
74
|
+
return handlers.get(value);
|
|
75
|
+
}
|
|
76
|
+
if (typeof value !== 'function') return value;
|
|
77
|
+
if (!methods.has(key)) {
|
|
78
|
+
methods.set(key, (...args: any[]) => {
|
|
79
|
+
const result = Reflect.apply(value, raw, args);
|
|
80
|
+
return result === raw ? facade : result;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return methods.get(key);
|
|
84
|
+
},
|
|
85
|
+
set(_target, key, value) {
|
|
86
|
+
return raw ? Reflect.set(raw, key, value, raw) : true;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
return facade;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function wrapHandler<T extends Winston.ExceptionHandler | Winston.RejectionHandler>(
|
|
93
|
+
raw: T,
|
|
94
|
+
rawLogger: Winston.Logger,
|
|
95
|
+
logger: Winston.Logger,
|
|
96
|
+
): T {
|
|
97
|
+
const methods = new Map<PropertyKey, (...args: any[]) => any>();
|
|
98
|
+
const handler = new Proxy({} as T, {
|
|
99
|
+
get(_target, key) {
|
|
100
|
+
if (key === 'logger') return logger;
|
|
101
|
+
const value = Reflect.get(raw, key, raw);
|
|
102
|
+
if (typeof value !== 'function') return value;
|
|
103
|
+
if (!methods.has(key)) {
|
|
104
|
+
methods.set(key, (...args: any[]) => {
|
|
105
|
+
const result = Reflect.apply(value, raw, args);
|
|
106
|
+
if (result === raw) return handler;
|
|
107
|
+
return result === rawLogger ? logger : result;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return methods.get(key);
|
|
111
|
+
},
|
|
112
|
+
set(_target, key, value) {
|
|
113
|
+
if (key === 'logger') return false;
|
|
114
|
+
return Reflect.set(raw, key, value, raw);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
return handler;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function wrapProfiler(raw: Winston.Profiler, logger: Winston.Logger): Winston.Profiler {
|
|
121
|
+
return new Proxy({} as Winston.Profiler, {
|
|
122
|
+
get(_target, key) {
|
|
123
|
+
if (key === 'logger') return logger;
|
|
124
|
+
if (key === 'done') {
|
|
125
|
+
return (...args: any[]) => {
|
|
126
|
+
if (getRequestBoundary()) return logger;
|
|
127
|
+
const result = Reflect.apply(raw.done, raw, args);
|
|
128
|
+
return result === raw.logger ? logger : result;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return Reflect.get(raw, key, raw);
|
|
132
|
+
},
|
|
133
|
+
set(_target, key, value) {
|
|
134
|
+
if (key === 'logger') return false;
|
|
135
|
+
return Reflect.set(raw, key, value, raw);
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -11,10 +11,18 @@ import type {
|
|
|
11
11
|
|
|
12
12
|
import { cast } from '../../../types/utils/cast.ts';
|
|
13
13
|
import { useApp } from '../../framework/useApp.ts';
|
|
14
|
+
import { getRequestBoundary } from '../requestBoundary.ts';
|
|
14
15
|
|
|
15
16
|
const SymbolLoggerMessage = Symbol('SymbolLoggerMessage');
|
|
16
17
|
|
|
17
|
-
export const
|
|
18
|
+
export const formatLoggerBoundary = Winston.format(info => {
|
|
19
|
+
if (getRequestBoundary()) return false;
|
|
20
|
+
return info;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const formatLoggerAxiosError = Winston.format((einfo, opts: any) => {
|
|
24
|
+
if (getRequestBoundary()) return false;
|
|
25
|
+
const { stack, cause } = opts;
|
|
18
26
|
if (
|
|
19
27
|
(einfo instanceof Error && einfo.constructor.name.includes('AxiosError')) ||
|
|
20
28
|
einfo.name === 'AxiosError' ||
|
|
@@ -39,10 +47,16 @@ export const formatLoggerAxiosError = Winston.format((einfo, { stack, cause }: a
|
|
|
39
47
|
});
|
|
40
48
|
|
|
41
49
|
export const formatLoggerErrors = (opts: ILoggerFormatOpts) => {
|
|
42
|
-
|
|
50
|
+
if (getRequestBoundary()) return formatLoggerBoundary();
|
|
51
|
+
return Winston.format.combine(
|
|
52
|
+
formatLoggerBoundary(),
|
|
53
|
+
formatLoggerAxiosError(opts),
|
|
54
|
+
Winston.format.errors(opts),
|
|
55
|
+
);
|
|
43
56
|
};
|
|
44
57
|
|
|
45
58
|
export const formatLoggerCtx = Winston.format((info, _opts: any) => {
|
|
59
|
+
if (getRequestBoundary()) return false;
|
|
46
60
|
const app = useApp();
|
|
47
61
|
if (!app.ctx) return info;
|
|
48
62
|
if (app.ctx.method) info.method = app.ctx.method;
|
|
@@ -60,12 +74,14 @@ export const formatLoggerCtx = Winston.format((info, _opts: any) => {
|
|
|
60
74
|
});
|
|
61
75
|
|
|
62
76
|
export const formatLoggerDummy = Winston.format(info => {
|
|
77
|
+
if (getRequestBoundary()) return false;
|
|
63
78
|
const app = useApp();
|
|
64
79
|
if (app.meta.env.LOGGER_DUMMY === 'true') return false;
|
|
65
80
|
return info;
|
|
66
81
|
});
|
|
67
82
|
|
|
68
83
|
export const formatLoggerFilter = Winston.format(((info: any, opts: ILoggerFormatFilterOpts) => {
|
|
84
|
+
if (getRequestBoundary()) return false;
|
|
69
85
|
// check client
|
|
70
86
|
const child = typeof opts.child === 'function' ? opts.child() : opts.child;
|
|
71
87
|
if (child && !child.includes(info.name)) {
|
|
@@ -90,8 +106,11 @@ export const formatLoggerFilter = Winston.format(((info: any, opts: ILoggerForma
|
|
|
90
106
|
}) as any);
|
|
91
107
|
|
|
92
108
|
export const formatLoggerConsole = (clientInfo: ILoggerOptionsClientInfo) => {
|
|
93
|
-
return Winston.format.
|
|
94
|
-
(
|
|
109
|
+
return Winston.format.combine(
|
|
110
|
+
formatLoggerBoundary(),
|
|
111
|
+
Winston.format.printf(info => {
|
|
112
|
+
if (getRequestBoundary()) return '';
|
|
113
|
+
const { timestamp, level, stack, message, name, beanFullName, durationMs, ...meta } = info;
|
|
95
114
|
const textClientName =
|
|
96
115
|
clientInfo.clientName === 'default' ? '' : ` ${chalk.cyan(`[${clientInfo.clientName}]`)}`;
|
|
97
116
|
const textName = name ? ` ${chalk.cyan(`[${name}]`)}` : '';
|
|
@@ -101,11 +120,12 @@ export const formatLoggerConsole = (clientInfo: ILoggerOptionsClientInfo) => {
|
|
|
101
120
|
const textDurationMs = durationMs !== undefined ? ` ${chalk.cyan(`+${durationMs}ms`)}` : '';
|
|
102
121
|
const textStack = stack ? `\n${stack}` : '';
|
|
103
122
|
return `${timestamp} ${level}${textClientName}${textName}${textBeanFullName}${textMeta}${textMessage}${textDurationMs}${textStack}`;
|
|
104
|
-
},
|
|
123
|
+
}),
|
|
105
124
|
);
|
|
106
125
|
};
|
|
107
126
|
|
|
108
127
|
function __formatLoggerFilterCheckInfo(info) {
|
|
128
|
+
if (getRequestBoundary()) return false;
|
|
109
129
|
if (typeof info.message === 'function') {
|
|
110
130
|
if (info.message[SymbolLoggerMessage] === undefined) {
|
|
111
131
|
info.message[SymbolLoggerMessage] = info.message();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
export type RequestBoundaryFailure =
|
|
4
|
+
| 'observer_failed'
|
|
5
|
+
| 'diagnostic_failed'
|
|
6
|
+
| 'late_callback_failed';
|
|
7
|
+
export type VonaScopeOutcome = 'not_created' | 'disposed' | 'dispose_failed';
|
|
8
|
+
export type VonaScopeOutcomeObserver = (outcome: VonaScopeOutcome) => void;
|
|
9
|
+
|
|
10
|
+
declare const requestBoundaryBrand: unique symbol;
|
|
11
|
+
|
|
12
|
+
export interface RequestBoundary {
|
|
13
|
+
readonly [requestBoundaryBrand]: true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RequestBoundaryCapture {
|
|
17
|
+
readonly diagnostics: 'suppress';
|
|
18
|
+
reportFailure(failure: RequestBoundaryFailure): void;
|
|
19
|
+
snapshot(): Readonly<{ portFault: boolean }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const boundaries = new WeakMap<RequestBoundary, RequestBoundaryCapture>();
|
|
23
|
+
const boundaryStorage = new AsyncLocalStorage<RequestBoundary>();
|
|
24
|
+
|
|
25
|
+
export function createRequestBoundary(options?: {
|
|
26
|
+
readonly onFailure?: (failure: RequestBoundaryFailure) => void;
|
|
27
|
+
}): RequestBoundary {
|
|
28
|
+
const onFailure = options?.onFailure;
|
|
29
|
+
const boundary = Object.freeze({}) as RequestBoundary;
|
|
30
|
+
let portFault = false;
|
|
31
|
+
let reporting = false;
|
|
32
|
+
const reported = new Set<RequestBoundaryFailure>();
|
|
33
|
+
const capture: RequestBoundaryCapture = Object.freeze({
|
|
34
|
+
diagnostics: 'suppress',
|
|
35
|
+
reportFailure(failure: RequestBoundaryFailure) {
|
|
36
|
+
if (
|
|
37
|
+
failure !== 'observer_failed' &&
|
|
38
|
+
failure !== 'diagnostic_failed' &&
|
|
39
|
+
failure !== 'late_callback_failed'
|
|
40
|
+
) {
|
|
41
|
+
portFault = true;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
portFault = true;
|
|
45
|
+
if (reported.has(failure)) return;
|
|
46
|
+
reported.add(failure);
|
|
47
|
+
if (reporting || !onFailure) return;
|
|
48
|
+
reporting = true;
|
|
49
|
+
try {
|
|
50
|
+
const result: unknown = onFailure(failure);
|
|
51
|
+
if (result && (typeof result === 'object' || typeof result === 'function')) {
|
|
52
|
+
const then = (result as PromiseLike<unknown>).then;
|
|
53
|
+
if (typeof then === 'function') {
|
|
54
|
+
portFault = true;
|
|
55
|
+
void new Promise<unknown>((resolve, reject) => {
|
|
56
|
+
then.call(result, resolve, reject);
|
|
57
|
+
}).catch(() => {});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
portFault = true;
|
|
62
|
+
} finally {
|
|
63
|
+
reporting = false;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
snapshot() {
|
|
67
|
+
return Object.freeze({ portFault });
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
boundaries.set(boundary, capture);
|
|
71
|
+
return boundary;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function runWithRequestBoundary<T>(boundary: RequestBoundary, continuation: () => T): T {
|
|
75
|
+
if (!boundaries.has(boundary)) throw new Error('Invalid request boundary');
|
|
76
|
+
const current = boundaryStorage.getStore();
|
|
77
|
+
if (current && current !== boundary) throw new Error('Request boundary cannot be replaced');
|
|
78
|
+
if (current === boundary) return continuation();
|
|
79
|
+
return boundaryStorage.run(boundary, continuation);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getRequestBoundary(): RequestBoundary | undefined {
|
|
83
|
+
return boundaryStorage.getStore();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function captureRequestBoundary(): RequestBoundaryCapture | undefined {
|
|
87
|
+
const boundary = boundaryStorage.getStore();
|
|
88
|
+
return boundary ? boundaries.get(boundary) : undefined;
|
|
89
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
|
|
7
|
+
import type { VonaApplication } from './application.ts';
|
|
8
|
+
import type { TestFreshnessDependencies } from './testFreshness.ts';
|
|
9
|
+
|
|
10
|
+
import { buildTestClaimResourceInventory } from './testClaim.ts';
|
|
11
|
+
import {
|
|
12
|
+
assertFreshTestResources,
|
|
13
|
+
assertFreshTestResourcesReady,
|
|
14
|
+
createFreshnessDependencies,
|
|
15
|
+
hasFreshnessDatabase,
|
|
16
|
+
hasFreshnessRedis,
|
|
17
|
+
requiresFreshTestResources,
|
|
18
|
+
TEST_REQUIRE_FRESH_RESOURCES_ENV,
|
|
19
|
+
} from './testFreshness.ts';
|
|
20
|
+
import { resolveVonaLocalPaths } from './testIsolation.ts';
|
|
21
|
+
|
|
22
|
+
function fixture() {
|
|
23
|
+
const root = mkdtempSync(path.join(realpathSync(tmpdir()), 'vona-freshness-'));
|
|
24
|
+
const runId = '20261019010203-a1b2c3d4';
|
|
25
|
+
const projectPath = path.join(root, 'vona');
|
|
26
|
+
const paths = resolveVonaLocalPaths(projectPath, 'test', runId);
|
|
27
|
+
for (const directory of [paths.dataRoot, paths.generatedRoot]) {
|
|
28
|
+
mkdirSync(directory, { recursive: true });
|
|
29
|
+
writeFileSync(path.join(directory, '.vona-test-owner'), `${runId}\n`);
|
|
30
|
+
}
|
|
31
|
+
const fixtureApp = {
|
|
32
|
+
projectPath,
|
|
33
|
+
name: `cabtest${runId}`,
|
|
34
|
+
meta: { env: { META_MODE: 'test', VONA_TEST_RUN_ID: runId, SERVER_LISTEN_PORT: '41234' } },
|
|
35
|
+
config: {
|
|
36
|
+
server: {
|
|
37
|
+
publicDir: paths.publicRoot,
|
|
38
|
+
workers: 1,
|
|
39
|
+
listen: { hostname: '127.0.0.1', port: 41234 },
|
|
40
|
+
serve: { protocol: 'http', host: '127.0.0.1:41234' },
|
|
41
|
+
},
|
|
42
|
+
logger: { baseDir: paths.logsRoot },
|
|
43
|
+
database: {
|
|
44
|
+
testDatabase: true,
|
|
45
|
+
base: {},
|
|
46
|
+
defaultClient: 'main',
|
|
47
|
+
clients: {
|
|
48
|
+
main: {
|
|
49
|
+
client: 'pg',
|
|
50
|
+
connection: { host: '127.0.0.1', port: 5432, database: 'postgres', user: 'synthetic' },
|
|
51
|
+
},
|
|
52
|
+
isolated: {
|
|
53
|
+
client: 'pg',
|
|
54
|
+
connection: { host: '127.0.0.1', port: 5433, database: 'postgres', user: 'synthetic' },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
instance: { instances: { isolated: { id: 2, isolate: true, isolateClient: 'isolated' } } },
|
|
59
|
+
redis: {
|
|
60
|
+
base: {},
|
|
61
|
+
clients: {
|
|
62
|
+
worker: { keyPrefix: true },
|
|
63
|
+
queue: {},
|
|
64
|
+
broadcast: {},
|
|
65
|
+
redlock: {},
|
|
66
|
+
redlock2: { db: 2 },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const app = fixtureApp as unknown as VonaApplication & { config: typeof fixtureApp.config };
|
|
72
|
+
const events: string[] = [];
|
|
73
|
+
const readers: TestFreshnessDependencies = {
|
|
74
|
+
databaseExists: async plan => {
|
|
75
|
+
events.push(`db:${plan.scope}`);
|
|
76
|
+
return false;
|
|
77
|
+
},
|
|
78
|
+
redisExists: async resource => {
|
|
79
|
+
events.push(`redis:${resource.clientName}`);
|
|
80
|
+
return false;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return { root, paths, app, readers, events };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function strict(fn: () => Promise<void>) {
|
|
87
|
+
const previous = process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
|
|
88
|
+
process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV] = 'true';
|
|
89
|
+
try {
|
|
90
|
+
await fn();
|
|
91
|
+
} finally {
|
|
92
|
+
if (previous === undefined) delete process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
|
|
93
|
+
else process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV] = previous;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test(
|
|
98
|
+
'strict freshness covers all targets before marking proof, without writes or resource identifiers',
|
|
99
|
+
{ concurrency: false },
|
|
100
|
+
async () => {
|
|
101
|
+
await strict(async () => {
|
|
102
|
+
const f = fixture();
|
|
103
|
+
try {
|
|
104
|
+
assert.throws(() => assertFreshTestResourcesReady(f.app), /not established/);
|
|
105
|
+
mkdirSync(f.paths.publicRoot); // Config may already have created this empty directory.
|
|
106
|
+
await assertFreshTestResources(f.app, f.readers);
|
|
107
|
+
assert.deepEqual(f.events, [
|
|
108
|
+
'db:share',
|
|
109
|
+
'db:isolate-isolated',
|
|
110
|
+
'redis:worker',
|
|
111
|
+
'redis:queue',
|
|
112
|
+
'redis:redlock',
|
|
113
|
+
'redis:redlock2',
|
|
114
|
+
]);
|
|
115
|
+
assert.doesNotThrow(() => assertFreshTestResourcesReady(f.app));
|
|
116
|
+
await assert.rejects(assertFreshTestResources(f.app, f.readers), /new isolated startup/);
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(f.root, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
test(
|
|
125
|
+
'strict freshness rejects a later isolate conflict or Redis residue without admitting startup',
|
|
126
|
+
{ concurrency: false },
|
|
127
|
+
async () => {
|
|
128
|
+
await strict(async () => {
|
|
129
|
+
for (const conflict of ['db:isolate-isolated', 'redis:redlock2', 'read-error']) {
|
|
130
|
+
const f = fixture();
|
|
131
|
+
try {
|
|
132
|
+
f.readers.databaseExists = async plan => {
|
|
133
|
+
f.events.push(`db:${plan.scope}`);
|
|
134
|
+
if (conflict === 'read-error') throw new Error('synthetic-protected-driver-secret');
|
|
135
|
+
return conflict === `db:${plan.scope}`;
|
|
136
|
+
};
|
|
137
|
+
f.readers.redisExists = async resource => {
|
|
138
|
+
f.events.push(`redis:${resource.clientName}`);
|
|
139
|
+
return conflict === `redis:${resource.clientName}`;
|
|
140
|
+
};
|
|
141
|
+
await assert.rejects(assertFreshTestResources(f.app, f.readers), error => {
|
|
142
|
+
assert.equal((error as Error).message, 'Strict test resource freshness check failed');
|
|
143
|
+
assert.equal((error as Error).cause, undefined);
|
|
144
|
+
return true;
|
|
145
|
+
});
|
|
146
|
+
assert.throws(() => assertFreshTestResourcesReady(f.app), /not established/);
|
|
147
|
+
if (conflict.startsWith('db:')) {
|
|
148
|
+
assert.equal(
|
|
149
|
+
f.events.some(value => value.startsWith('redis:')),
|
|
150
|
+
false,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
} finally {
|
|
154
|
+
rmSync(f.root, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
test(
|
|
162
|
+
'strict freshness rejects local residue, symlinks and preselected database targets before driver reads',
|
|
163
|
+
{ concurrency: false },
|
|
164
|
+
async () => {
|
|
165
|
+
await strict(async () => {
|
|
166
|
+
for (const kind of ['public', 'runtime', 'sqlite', 'symlink', 'selected', 'unisolated']) {
|
|
167
|
+
const f = fixture();
|
|
168
|
+
try {
|
|
169
|
+
if (kind === 'selected') {
|
|
170
|
+
f.app.config.database.clients.main.connection.database = 'vona-test-preselected';
|
|
171
|
+
} else if (kind === 'unisolated') {
|
|
172
|
+
f.app.meta.env.VONA_TEST_RUN_ID = undefined;
|
|
173
|
+
} else if (kind === 'symlink') {
|
|
174
|
+
symlinkSync(f.paths.generatedRoot, f.paths.publicRoot);
|
|
175
|
+
} else {
|
|
176
|
+
const directory =
|
|
177
|
+
kind === 'public'
|
|
178
|
+
? f.paths.publicRoot
|
|
179
|
+
: kind === 'runtime'
|
|
180
|
+
? f.paths.runtimeRoot
|
|
181
|
+
: f.paths.sqliteRoot;
|
|
182
|
+
mkdirSync(directory);
|
|
183
|
+
writeFileSync(path.join(directory, 'synthetic-residue'), 'keep');
|
|
184
|
+
}
|
|
185
|
+
await assert.rejects(assertFreshTestResources(f.app, f.readers));
|
|
186
|
+
assert.deepEqual(f.events, []);
|
|
187
|
+
} finally {
|
|
188
|
+
rmSync(f.root, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
test(
|
|
196
|
+
'freshness is explicit and default lanes never invoke readers',
|
|
197
|
+
{ concurrency: false },
|
|
198
|
+
async () => {
|
|
199
|
+
const previous = process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
|
|
200
|
+
try {
|
|
201
|
+
delete process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
|
|
202
|
+
const app = {} as VonaApplication;
|
|
203
|
+
assert.equal(requiresFreshTestResources(), false);
|
|
204
|
+
await assertFreshTestResources(app, {
|
|
205
|
+
databaseExists: async () => {
|
|
206
|
+
throw new Error('must not read');
|
|
207
|
+
},
|
|
208
|
+
redisExists: async () => {
|
|
209
|
+
throw new Error('must not read');
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
assert.doesNotThrow(() => assertFreshTestResourcesReady(app));
|
|
213
|
+
process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV] = 'false';
|
|
214
|
+
assert.throws(() => requiresFreshTestResources(), /Invalid strict/);
|
|
215
|
+
} finally {
|
|
216
|
+
if (previous === undefined) delete process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
|
|
217
|
+
else process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV] = previous;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
test('read-only database inspection uses exact prefix bindings and always disposes', async () => {
|
|
223
|
+
const f = fixture();
|
|
224
|
+
try {
|
|
225
|
+
const plan = buildTestClaimResourceInventory(f.app).databases[0];
|
|
226
|
+
for (const dialect of ['pg', 'mysql', 'mysql2'] as const) {
|
|
227
|
+
for (const found of [false, true]) {
|
|
228
|
+
let closed = 0;
|
|
229
|
+
const result = await hasFreshnessDatabase(
|
|
230
|
+
{
|
|
231
|
+
raw: async (sql, bindings) => {
|
|
232
|
+
assert.match(sql, /^select .* limit 1$/);
|
|
233
|
+
assert.deepEqual(bindings, [plan.prefix.length, plan.prefix]);
|
|
234
|
+
const rows = found ? [{}] : [];
|
|
235
|
+
return dialect === 'pg' ? { rows } : [rows];
|
|
236
|
+
},
|
|
237
|
+
destroy: async () => {
|
|
238
|
+
closed++;
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{ ...plan, endpoint: { ...plan.endpoint, dialect } },
|
|
242
|
+
);
|
|
243
|
+
assert.equal(result, found);
|
|
244
|
+
assert.equal(closed, 1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
let closed = 0;
|
|
248
|
+
await assert.rejects(
|
|
249
|
+
hasFreshnessDatabase(
|
|
250
|
+
{
|
|
251
|
+
raw: async () => ({}),
|
|
252
|
+
destroy: async () => {
|
|
253
|
+
closed++;
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
plan,
|
|
257
|
+
),
|
|
258
|
+
/Invalid freshness database response/,
|
|
259
|
+
);
|
|
260
|
+
assert.equal(closed, 1);
|
|
261
|
+
} finally {
|
|
262
|
+
rmSync(f.root, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('Redis freshness requires a completed cursor scan and rejects foreign/malformed/limited responses', async () => {
|
|
267
|
+
const prefix = 'synthetic_[*]?:';
|
|
268
|
+
const cursors: string[] = [];
|
|
269
|
+
assert.equal(
|
|
270
|
+
await hasFreshnessRedis(
|
|
271
|
+
{
|
|
272
|
+
scan: async (cursor, _match, pattern) => {
|
|
273
|
+
cursors.push(cursor);
|
|
274
|
+
assert.equal(pattern, 'synthetic_\\[\\*\\]\\?:*');
|
|
275
|
+
return [cursor === '0' ? '7' : '0', []];
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
prefix,
|
|
279
|
+
),
|
|
280
|
+
false,
|
|
281
|
+
);
|
|
282
|
+
assert.deepEqual(cursors, ['0', '7']);
|
|
283
|
+
assert.equal(
|
|
284
|
+
await hasFreshnessRedis({ scan: async () => ['1', [`${prefix}key`]] }, prefix),
|
|
285
|
+
true,
|
|
286
|
+
);
|
|
287
|
+
for (const response of [undefined, ['bad', []], ['0', ['foreign']], ['0', [1]], ['0']]) {
|
|
288
|
+
await assert.rejects(
|
|
289
|
+
hasFreshnessRedis({ scan: async () => response }, prefix),
|
|
290
|
+
/Invalid Redis/,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
await assert.rejects(
|
|
294
|
+
hasFreshnessRedis({ scan: async () => ['1', []] }, prefix, 2),
|
|
295
|
+
/limit exceeded/,
|
|
296
|
+
);
|
|
297
|
+
const failure = new Error('synthetic-ACL-error');
|
|
298
|
+
await assert.rejects(
|
|
299
|
+
hasFreshnessRedis(
|
|
300
|
+
{
|
|
301
|
+
scan: async () => {
|
|
302
|
+
throw failure;
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
prefix,
|
|
306
|
+
),
|
|
307
|
+
value => value === failure,
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('direct readers project connection options, widen queue scan and dispose on failure', async () => {
|
|
312
|
+
const f = fixture();
|
|
313
|
+
try {
|
|
314
|
+
const inventory = buildTestClaimResourceInventory(f.app);
|
|
315
|
+
const plan = inventory.databases[0];
|
|
316
|
+
const resource = inventory.redis.find(item => item.clientName === 'queue')!;
|
|
317
|
+
let sqlClosed = 0;
|
|
318
|
+
let redisClosed = 0;
|
|
319
|
+
let fail = false;
|
|
320
|
+
const readers = createFreshnessDependencies(f.app.projectPath, name => {
|
|
321
|
+
if (name === 'knex') {
|
|
322
|
+
return (options: any) => {
|
|
323
|
+
assert.equal(options.connection.database, 'postgres');
|
|
324
|
+
assert.equal(options.connection.user, 'synthetic');
|
|
325
|
+
assert.equal(options.connection.password, 'synthetic-password');
|
|
326
|
+
assert.equal(options.connection.beforeConnect, undefined);
|
|
327
|
+
assert.equal(options.pool.afterCreate, undefined);
|
|
328
|
+
return {
|
|
329
|
+
raw: async () => ({ rows: [] }),
|
|
330
|
+
destroy: async () => {
|
|
331
|
+
sqlClosed++;
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
assert.equal(name, 'ioredis');
|
|
337
|
+
return {
|
|
338
|
+
Redis: class {
|
|
339
|
+
constructor(options: any) {
|
|
340
|
+
assert.equal(options.db, 0);
|
|
341
|
+
assert.equal(options.keyPrefix, undefined);
|
|
342
|
+
assert.equal(options.enableOfflineQueue, false);
|
|
343
|
+
assert.equal(options.retryStrategy(), null);
|
|
344
|
+
assert.equal(options.maxRetriesPerRequest, 0);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
on() {}
|
|
348
|
+
|
|
349
|
+
async connect() {
|
|
350
|
+
if (fail) throw new Error('synthetic-driver-error');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async scan(_cursor: string, _match: string, pattern: string) {
|
|
354
|
+
assert.equal(pattern, `${resource.prefix.slice(0, -'queue:'.length)}*`);
|
|
355
|
+
return ['0', []];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
disconnect() {
|
|
359
|
+
redisClosed++;
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
assert.equal(
|
|
365
|
+
await readers.databaseExists(plan, {
|
|
366
|
+
connection: { user: 'synthetic', password: 'synthetic-password', beforeConnect() {} },
|
|
367
|
+
pool: { afterCreate() {} },
|
|
368
|
+
}),
|
|
369
|
+
false,
|
|
370
|
+
);
|
|
371
|
+
assert.equal(sqlClosed, 1);
|
|
372
|
+
assert.equal(await readers.redisExists(resource, {}), false);
|
|
373
|
+
fail = true;
|
|
374
|
+
await assert.rejects(readers.redisExists(resource, {}), /synthetic-driver-error/);
|
|
375
|
+
assert.equal(redisClosed, 2);
|
|
376
|
+
} finally {
|
|
377
|
+
rmSync(f.root, { recursive: true, force: true });
|
|
378
|
+
}
|
|
379
|
+
});
|