@open-mercato/cli 0.4.9-develop-7afbe1e834 → 0.4.9-develop-94fb251ed3
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/AGENTS.md +34 -0
- package/dist/lib/generators/module-registry.js +144 -0
- package/dist/lib/generators/module-registry.js.map +3 -3
- package/dist/lib/testing/integration.js +109 -78
- package/dist/lib/testing/integration.js.map +2 -2
- package/package.json +4 -4
- package/src/lib/generators/__tests__/module-subset.test.ts +107 -1
- package/src/lib/generators/__tests__/scanner.test.ts +2 -1
- package/src/lib/generators/module-registry.ts +144 -0
- package/src/lib/testing/__tests__/integration.test.ts +106 -3
- package/src/lib/testing/integration.ts +137 -85
|
@@ -141,12 +141,13 @@ type EphemeralEnvironmentState = {
|
|
|
141
141
|
|
|
142
142
|
type PlaywrightRunOptions = Pick<InteractiveIntegrationOptions, 'verbose' | 'captureScreenshots' | 'workers' | 'retries'>
|
|
143
143
|
|
|
144
|
-
const
|
|
144
|
+
const DEFAULT_APP_READY_TIMEOUT_MS = 90_000
|
|
145
145
|
const APP_READY_INTERVAL_MS = 1_000
|
|
146
146
|
const DEFAULT_EPHEMERAL_APP_PORT = 5001
|
|
147
147
|
const EPHEMERAL_ENV_LOCK_TIMEOUT_MS = 60_000
|
|
148
148
|
const EPHEMERAL_ENV_LOCK_POLL_MS = 500
|
|
149
149
|
const DEFAULT_BUILD_CACHE_TTL_SECONDS = 600
|
|
150
|
+
const APP_READY_TIMEOUT_ENV_VAR = 'OM_INTEGRATION_APP_READY_TIMEOUT_SECONDS'
|
|
150
151
|
const BUILD_CACHE_TTL_ENV_VAR = 'OM_INTEGRATION_BUILD_CACHE_TTL_SECONDS'
|
|
151
152
|
const PLAYWRIGHT_ENV_UNAVAILABLE_PATTERNS: RegExp[] = [
|
|
152
153
|
/net::ERR_CONNECTION_REFUSED/i,
|
|
@@ -161,7 +162,6 @@ const PLAYWRIGHT_QUICK_FAILURE_THRESHOLD = 6
|
|
|
161
162
|
const PLAYWRIGHT_QUICK_FAILURE_MAX_DURATION_MS = 1_500
|
|
162
163
|
const PLAYWRIGHT_HEALTH_PROBE_INTERVAL_MS = 3_000
|
|
163
164
|
const ANSI_ESCAPE_REGEX = /\x1b\[[0-?]*[ -/]*[@-~]/g // NOSONAR — ANSI escape sequence pattern
|
|
164
|
-
const NEXT_STATIC_ASSET_PATTERN = /\/_next\/static\/[^"'`\s)]+?\.(?:js|css)/g
|
|
165
165
|
const resolver = createResolver()
|
|
166
166
|
const projectRootDirectory = resolver.getRootDir()
|
|
167
167
|
const EPHEMERAL_ENV_FILE_PATH = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.json')
|
|
@@ -250,6 +250,24 @@ type CommandMonitoringOptions = {
|
|
|
250
250
|
playwrightFailureHealthCheck?: PlaywrightFailureHealthCheckOptions
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
type LoginPageProbeResult = {
|
|
254
|
+
status: number | null
|
|
255
|
+
healthy: boolean
|
|
256
|
+
detail: string
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
type BackendLoginProbeResult = {
|
|
260
|
+
status: number | null
|
|
261
|
+
healthy: boolean
|
|
262
|
+
detail: string
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type ApplicationReadinessProbeResult = {
|
|
266
|
+
ready: boolean
|
|
267
|
+
frontend: LoginPageProbeResult
|
|
268
|
+
backend: BackendLoginProbeResult
|
|
269
|
+
}
|
|
270
|
+
|
|
253
271
|
type PlaywrightFailureHealthCheckOptions = {
|
|
254
272
|
baseUrl: string
|
|
255
273
|
consecutiveFailureThreshold?: number
|
|
@@ -678,6 +696,21 @@ export function resolveBuildCacheTtlSeconds(logPrefix: string): number {
|
|
|
678
696
|
return parsed
|
|
679
697
|
}
|
|
680
698
|
|
|
699
|
+
export function resolveAppReadyTimeoutMs(logPrefix: string): number {
|
|
700
|
+
const rawValue = process.env[APP_READY_TIMEOUT_ENV_VAR]
|
|
701
|
+
if (!rawValue) {
|
|
702
|
+
return DEFAULT_APP_READY_TIMEOUT_MS
|
|
703
|
+
}
|
|
704
|
+
const parsedSeconds = Number.parseInt(rawValue, 10)
|
|
705
|
+
if (!Number.isFinite(parsedSeconds) || parsedSeconds < 1) {
|
|
706
|
+
console.warn(
|
|
707
|
+
`[${logPrefix}] Invalid ${APP_READY_TIMEOUT_ENV_VAR} value "${rawValue}". Using default ${DEFAULT_APP_READY_TIMEOUT_MS / 1000}s.`,
|
|
708
|
+
)
|
|
709
|
+
return DEFAULT_APP_READY_TIMEOUT_MS
|
|
710
|
+
}
|
|
711
|
+
return parsedSeconds * 1000
|
|
712
|
+
}
|
|
713
|
+
|
|
681
714
|
function buildCacheDefaults(overrides: BuildCacheOptions = {}): {
|
|
682
715
|
artifactPaths: string[]
|
|
683
716
|
inputPaths: string[]
|
|
@@ -1089,59 +1122,58 @@ export async function readEphemeralEnvironmentState(): Promise<EphemeralEnvironm
|
|
|
1089
1122
|
}
|
|
1090
1123
|
}
|
|
1091
1124
|
|
|
1092
|
-
|
|
1125
|
+
function isLoginHtmlHealthy(html: string): boolean {
|
|
1126
|
+
return !/Application error: a client-side exception has occurred/i.test(html)
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function isSuccessfulBrowserNavigationStatus(status: number): boolean {
|
|
1130
|
+
return status === 200 || (status >= 300 && status < 400)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async function probeLoginPage(baseUrl: string): Promise<LoginPageProbeResult> {
|
|
1093
1134
|
try {
|
|
1094
1135
|
const response = await fetch(`${baseUrl}/login`, {
|
|
1095
1136
|
method: 'GET',
|
|
1096
1137
|
redirect: 'manual',
|
|
1097
1138
|
})
|
|
1098
|
-
if (response.status
|
|
1099
|
-
return
|
|
1139
|
+
if (!isSuccessfulBrowserNavigationStatus(response.status)) {
|
|
1140
|
+
return {
|
|
1141
|
+
status: response.status,
|
|
1142
|
+
healthy: false,
|
|
1143
|
+
detail: `GET /login returned ${response.status}`,
|
|
1144
|
+
}
|
|
1100
1145
|
}
|
|
1101
1146
|
if (response.status !== 200) {
|
|
1102
|
-
return
|
|
1147
|
+
return {
|
|
1148
|
+
status: response.status,
|
|
1149
|
+
healthy: true,
|
|
1150
|
+
detail: `GET /login returned redirect ${response.status}`,
|
|
1151
|
+
}
|
|
1103
1152
|
}
|
|
1104
|
-
const html = await response.text()
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
function isLoginHtmlHealthy(html: string): boolean {
|
|
1112
|
-
return !/Application error: a client-side exception has occurred/i.test(html)
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
function extractReferencedNextAssets(html: string, maxAssets = 8): string[] {
|
|
1116
|
-
const matches = html.match(NEXT_STATIC_ASSET_PATTERN) ?? []
|
|
1117
|
-
const unique = Array.from(new Set(matches))
|
|
1118
|
-
return unique.slice(0, maxAssets)
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
async function areReferencedNextAssetsReachable(baseUrl: string, html: string): Promise<boolean> {
|
|
1122
|
-
const assets = extractReferencedNextAssets(html)
|
|
1123
|
-
if (assets.length === 0) {
|
|
1124
|
-
return false
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
for (const assetPath of assets) {
|
|
1128
|
-
try {
|
|
1129
|
-
const response = await fetch(`${baseUrl}${assetPath}`, {
|
|
1130
|
-
method: 'GET',
|
|
1131
|
-
redirect: 'manual',
|
|
1132
|
-
})
|
|
1133
|
-
if (response.status !== 200 && response.status !== 304) {
|
|
1134
|
-
return false
|
|
1153
|
+
const html = await response.text().catch(() => '')
|
|
1154
|
+
if (!isLoginHtmlHealthy(html)) {
|
|
1155
|
+
return {
|
|
1156
|
+
status: response.status,
|
|
1157
|
+
healthy: false,
|
|
1158
|
+
detail: 'GET /login returned client-side exception HTML',
|
|
1135
1159
|
}
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
status: response.status,
|
|
1163
|
+
healthy: true,
|
|
1164
|
+
detail: 'GET /login returned healthy HTML',
|
|
1165
|
+
}
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1168
|
+
return {
|
|
1169
|
+
status: null,
|
|
1170
|
+
healthy: false,
|
|
1171
|
+
detail: `GET /login failed: ${message}`,
|
|
1138
1172
|
}
|
|
1139
1173
|
}
|
|
1140
|
-
|
|
1141
|
-
return true
|
|
1142
1174
|
}
|
|
1143
1175
|
|
|
1144
|
-
async function
|
|
1176
|
+
async function probeBackendLoginEndpoint(baseUrl: string): Promise<BackendLoginProbeResult> {
|
|
1145
1177
|
try {
|
|
1146
1178
|
const form = new URLSearchParams()
|
|
1147
1179
|
form.set('email', 'integration-healthcheck@example.invalid')
|
|
@@ -1155,9 +1187,34 @@ async function isBackendLoginEndpointHealthy(baseUrl: string): Promise<boolean>
|
|
|
1155
1187
|
body: form.toString(),
|
|
1156
1188
|
})
|
|
1157
1189
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1190
|
+
const healthy = response.status === 200 || response.status === 400 || response.status === 401 || response.status === 403
|
|
1191
|
+
return {
|
|
1192
|
+
status: response.status,
|
|
1193
|
+
healthy,
|
|
1194
|
+
detail: healthy
|
|
1195
|
+
? `POST /api/auth/login returned ${response.status}`
|
|
1196
|
+
: `POST /api/auth/login returned unexpected ${response.status}`,
|
|
1197
|
+
}
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1200
|
+
return {
|
|
1201
|
+
status: null,
|
|
1202
|
+
healthy: false,
|
|
1203
|
+
detail: `POST /api/auth/login failed: ${message}`,
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
async function probeApplicationReadiness(baseUrl: string): Promise<ApplicationReadinessProbeResult> {
|
|
1209
|
+
const [frontend, backend] = await Promise.all([
|
|
1210
|
+
probeLoginPage(baseUrl),
|
|
1211
|
+
probeBackendLoginEndpoint(baseUrl),
|
|
1212
|
+
])
|
|
1213
|
+
|
|
1214
|
+
return {
|
|
1215
|
+
ready: frontend.healthy && backend.healthy,
|
|
1216
|
+
frontend,
|
|
1217
|
+
backend,
|
|
1161
1218
|
}
|
|
1162
1219
|
}
|
|
1163
1220
|
|
|
@@ -1266,8 +1323,11 @@ function buildReusableEnvironment(baseUrl: string, captureScreenshots: boolean):
|
|
|
1266
1323
|
return buildEnvironment({
|
|
1267
1324
|
BASE_URL: baseUrl,
|
|
1268
1325
|
NODE_ENV: 'production',
|
|
1326
|
+
JWT_SECRET: process.env.JWT_SECRET ?? 'om-ephemeral-integration-jwt-secret',
|
|
1327
|
+
OM_SECURITY_MFA_SETUP_SECRET: process.env.OM_SECURITY_MFA_SETUP_SECRET ?? 'om-ephemeral-integration-mfa-setup-secret',
|
|
1269
1328
|
OM_ENABLE_ENTERPRISE_MODULES: process.env.OM_ENABLE_ENTERPRISE_MODULES ?? 'false',
|
|
1270
1329
|
OM_ENABLE_ENTERPRISE_MODULES_SSO: process.env.OM_ENABLE_ENTERPRISE_MODULES_SSO ?? 'false',
|
|
1330
|
+
OM_ENABLE_ENTERPRISE_MODULES_SECURITY: process.env.OM_ENABLE_ENTERPRISE_MODULES_SECURITY ?? 'false',
|
|
1271
1331
|
OM_TEST_MODE: '1',
|
|
1272
1332
|
ENABLE_CRUD_API_CACHE: 'true',
|
|
1273
1333
|
NEXT_PUBLIC_OM_EXAMPLE_INJECTION_WIDGETS_ENABLED: 'true',
|
|
@@ -1338,46 +1398,27 @@ export async function tryReuseExistingEnvironment(options: EphemeralRuntimeOptio
|
|
|
1338
1398
|
}
|
|
1339
1399
|
}
|
|
1340
1400
|
|
|
1341
|
-
async function waitForApplicationReadiness(
|
|
1401
|
+
async function waitForApplicationReadiness(
|
|
1402
|
+
baseUrl: string,
|
|
1403
|
+
appProcess: ChildProcess,
|
|
1404
|
+
options: { timeoutMs: number },
|
|
1405
|
+
): Promise<void> {
|
|
1342
1406
|
const startTimestamp = Date.now()
|
|
1343
1407
|
const exitPromise = getProcessExitPromise(appProcess)
|
|
1344
1408
|
const readinessStabilizationMs = 600
|
|
1409
|
+
let lastProbe: ApplicationReadinessProbeResult | null = null
|
|
1345
1410
|
|
|
1346
|
-
while (Date.now() - startTimestamp <
|
|
1347
|
-
const responsePromise = fetch(`${baseUrl}/login`, {
|
|
1348
|
-
method: 'GET',
|
|
1349
|
-
redirect: 'manual',
|
|
1350
|
-
})
|
|
1351
|
-
.then(async (response) => ({
|
|
1352
|
-
response,
|
|
1353
|
-
body: response.status === 200 ? await response.text().catch(() => '') : '',
|
|
1354
|
-
}))
|
|
1355
|
-
.catch(() => null)
|
|
1411
|
+
while (Date.now() - startTimestamp < options.timeoutMs) {
|
|
1356
1412
|
const result = await Promise.race([
|
|
1357
|
-
|
|
1358
|
-
if (!payload) {
|
|
1359
|
-
return { kind: 'network_error' as const }
|
|
1360
|
-
}
|
|
1361
|
-
return {
|
|
1362
|
-
kind: 'response' as const,
|
|
1363
|
-
status: payload.response.status,
|
|
1364
|
-
body: payload.body,
|
|
1365
|
-
}
|
|
1366
|
-
}),
|
|
1413
|
+
probeApplicationReadiness(baseUrl).then((probe) => ({ kind: 'probe' as const, probe })),
|
|
1367
1414
|
exitPromise.then((code) => ({ kind: 'exit' as const, code })),
|
|
1368
1415
|
delay(APP_READY_INTERVAL_MS).then(() => ({ kind: 'timeout' as const })),
|
|
1369
1416
|
])
|
|
1370
1417
|
|
|
1371
|
-
if (result.kind === '
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
continue
|
|
1376
|
-
}
|
|
1377
|
-
const assetsReachable = await areReferencedNextAssetsReachable(baseUrl, loginHtml)
|
|
1378
|
-
if (!assetsReachable) {
|
|
1379
|
-
continue
|
|
1380
|
-
}
|
|
1418
|
+
if (result.kind === 'probe') {
|
|
1419
|
+
lastProbe = result.probe
|
|
1420
|
+
if (!result.probe.ready) {
|
|
1421
|
+
continue
|
|
1381
1422
|
}
|
|
1382
1423
|
const processExited = await Promise.race([
|
|
1383
1424
|
exitPromise.then(() => true),
|
|
@@ -1393,7 +1434,11 @@ async function waitForApplicationReadiness(baseUrl: string, appProcess: ChildPro
|
|
|
1393
1434
|
}
|
|
1394
1435
|
}
|
|
1395
1436
|
|
|
1396
|
-
|
|
1437
|
+
const lastFrontendDetail = lastProbe?.frontend.detail ?? 'GET /login was never observed'
|
|
1438
|
+
const lastBackendDetail = lastProbe?.backend.detail ?? 'POST /api/auth/login was never observed'
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
`Application did not become ready within ${options.timeoutMs / 1000} seconds. Last probe: ${lastFrontendDetail}; ${lastBackendDetail}`,
|
|
1441
|
+
)
|
|
1397
1442
|
}
|
|
1398
1443
|
|
|
1399
1444
|
export function parseOptions(rawArgs: string[]): IntegrationOptions {
|
|
@@ -2304,11 +2349,8 @@ async function runIntegrationTestSuiteOnce(
|
|
|
2304
2349
|
}
|
|
2305
2350
|
|
|
2306
2351
|
async function isEnvironmentUnavailable(baseUrl: string): Promise<boolean> {
|
|
2307
|
-
const
|
|
2308
|
-
|
|
2309
|
-
isBackendLoginEndpointHealthy(baseUrl),
|
|
2310
|
-
])
|
|
2311
|
-
return !applicationReachable || !backendHealthy
|
|
2352
|
+
const readiness = await probeApplicationReadiness(baseUrl)
|
|
2353
|
+
return !readiness.ready
|
|
2312
2354
|
}
|
|
2313
2355
|
|
|
2314
2356
|
function isEnvironmentUnavailableError(error: unknown): boolean {
|
|
@@ -2496,7 +2538,8 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
|
|
|
2496
2538
|
const commandEnvironment = buildEnvironment({
|
|
2497
2539
|
DATABASE_URL: databaseUrl,
|
|
2498
2540
|
BASE_URL: applicationBaseUrl,
|
|
2499
|
-
JWT_SECRET: 'om-ephemeral-integration-jwt-secret',
|
|
2541
|
+
JWT_SECRET: process.env.JWT_SECRET ?? 'om-ephemeral-integration-jwt-secret',
|
|
2542
|
+
OM_SECURITY_MFA_SETUP_SECRET: process.env.OM_SECURITY_MFA_SETUP_SECRET ?? 'om-ephemeral-integration-mfa-setup-secret',
|
|
2500
2543
|
NODE_ENV: 'production',
|
|
2501
2544
|
DB_POOL_MIN: '0',
|
|
2502
2545
|
DB_POOL_MAX: '5',
|
|
@@ -2506,6 +2549,7 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
|
|
|
2506
2549
|
DB_IDLE_IN_TRANSACTION_TIMEOUT_MS: '30000',
|
|
2507
2550
|
OM_ENABLE_ENTERPRISE_MODULES: process.env.OM_ENABLE_ENTERPRISE_MODULES ?? 'false',
|
|
2508
2551
|
OM_ENABLE_ENTERPRISE_MODULES_SSO: process.env.OM_ENABLE_ENTERPRISE_MODULES_SSO ?? 'false',
|
|
2552
|
+
OM_ENABLE_ENTERPRISE_MODULES_SECURITY: process.env.OM_ENABLE_ENTERPRISE_MODULES_SECURITY ?? 'false',
|
|
2509
2553
|
OM_TEST_MODE: '1',
|
|
2510
2554
|
OM_TEST_AUTH_RATE_LIMIT_MODE: 'opt-in',
|
|
2511
2555
|
OM_DISABLE_EMAIL_DELIVERY: '1',
|
|
@@ -2537,6 +2581,7 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
|
|
|
2537
2581
|
}
|
|
2538
2582
|
|
|
2539
2583
|
try {
|
|
2584
|
+
const appReadyTimeoutMs = resolveAppReadyTimeoutMs(options.logPrefix)
|
|
2540
2585
|
const buildCacheTtlSeconds = resolveBuildCacheTtlSeconds(options.logPrefix)
|
|
2541
2586
|
let sourceFingerprintValue: string | null = null
|
|
2542
2587
|
let needsBuild = true
|
|
@@ -2614,8 +2659,15 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
|
|
|
2614
2659
|
})
|
|
2615
2660
|
applicationProcess = startedAppProcess
|
|
2616
2661
|
|
|
2617
|
-
await runTimedStep(
|
|
2618
|
-
|
|
2662
|
+
await runTimedStep(
|
|
2663
|
+
options.logPrefix,
|
|
2664
|
+
'Waiting for application readiness',
|
|
2665
|
+
{ expectedSeconds: Math.max(12, Math.ceil(appReadyTimeoutMs / 1000)) },
|
|
2666
|
+
async () =>
|
|
2667
|
+
waitForApplicationReadiness(applicationBaseUrl, startedAppProcess, {
|
|
2668
|
+
timeoutMs: appReadyTimeoutMs,
|
|
2669
|
+
}),
|
|
2670
|
+
)
|
|
2619
2671
|
console.log(`[${options.logPrefix}] Application is ready at ${applicationBaseUrl}`)
|
|
2620
2672
|
await writeEphemeralEnvironmentState({
|
|
2621
2673
|
baseUrl: applicationBaseUrl,
|