@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
|
@@ -12,12 +12,13 @@ import { resolveDockerHostFromContext, runCommandAndCapture } from "./runtime-ut
|
|
|
12
12
|
function shouldUseIsolatedPortForFreshEnvironment(options) {
|
|
13
13
|
return options.reuseExisting === false || options.existingStateBeforeReuseAttempt !== null;
|
|
14
14
|
}
|
|
15
|
-
const
|
|
15
|
+
const DEFAULT_APP_READY_TIMEOUT_MS = 9e4;
|
|
16
16
|
const APP_READY_INTERVAL_MS = 1e3;
|
|
17
17
|
const DEFAULT_EPHEMERAL_APP_PORT = 5001;
|
|
18
18
|
const EPHEMERAL_ENV_LOCK_TIMEOUT_MS = 6e4;
|
|
19
19
|
const EPHEMERAL_ENV_LOCK_POLL_MS = 500;
|
|
20
20
|
const DEFAULT_BUILD_CACHE_TTL_SECONDS = 600;
|
|
21
|
+
const APP_READY_TIMEOUT_ENV_VAR = "OM_INTEGRATION_APP_READY_TIMEOUT_SECONDS";
|
|
21
22
|
const BUILD_CACHE_TTL_ENV_VAR = "OM_INTEGRATION_BUILD_CACHE_TTL_SECONDS";
|
|
22
23
|
const PLAYWRIGHT_ENV_UNAVAILABLE_PATTERNS = [
|
|
23
24
|
/net::ERR_CONNECTION_REFUSED/i,
|
|
@@ -32,7 +33,6 @@ const PLAYWRIGHT_QUICK_FAILURE_THRESHOLD = 6;
|
|
|
32
33
|
const PLAYWRIGHT_QUICK_FAILURE_MAX_DURATION_MS = 1500;
|
|
33
34
|
const PLAYWRIGHT_HEALTH_PROBE_INTERVAL_MS = 3e3;
|
|
34
35
|
const ANSI_ESCAPE_REGEX = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
35
|
-
const NEXT_STATIC_ASSET_PATTERN = /\/_next\/static\/[^"'`\s)]+?\.(?:js|css)/g;
|
|
36
36
|
const resolver = createResolver();
|
|
37
37
|
const projectRootDirectory = resolver.getRootDir();
|
|
38
38
|
const EPHEMERAL_ENV_FILE_PATH = path.join(projectRootDirectory, ".ai", "qa", "ephemeral-env.json");
|
|
@@ -410,6 +410,20 @@ function resolveBuildCacheTtlSeconds(logPrefix) {
|
|
|
410
410
|
}
|
|
411
411
|
return parsed;
|
|
412
412
|
}
|
|
413
|
+
function resolveAppReadyTimeoutMs(logPrefix) {
|
|
414
|
+
const rawValue = process.env[APP_READY_TIMEOUT_ENV_VAR];
|
|
415
|
+
if (!rawValue) {
|
|
416
|
+
return DEFAULT_APP_READY_TIMEOUT_MS;
|
|
417
|
+
}
|
|
418
|
+
const parsedSeconds = Number.parseInt(rawValue, 10);
|
|
419
|
+
if (!Number.isFinite(parsedSeconds) || parsedSeconds < 1) {
|
|
420
|
+
console.warn(
|
|
421
|
+
`[${logPrefix}] Invalid ${APP_READY_TIMEOUT_ENV_VAR} value "${rawValue}". Using default ${DEFAULT_APP_READY_TIMEOUT_MS / 1e3}s.`
|
|
422
|
+
);
|
|
423
|
+
return DEFAULT_APP_READY_TIMEOUT_MS;
|
|
424
|
+
}
|
|
425
|
+
return parsedSeconds * 1e3;
|
|
426
|
+
}
|
|
413
427
|
function buildCacheDefaults(overrides = {}) {
|
|
414
428
|
return {
|
|
415
429
|
artifactPaths: overrides.artifactPaths ?? APP_BUILD_ARTIFACTS,
|
|
@@ -755,53 +769,55 @@ async function readEphemeralEnvironmentState() {
|
|
|
755
769
|
startedAt: record.startedAt
|
|
756
770
|
};
|
|
757
771
|
}
|
|
758
|
-
|
|
772
|
+
function isLoginHtmlHealthy(html) {
|
|
773
|
+
return !/Application error: a client-side exception has occurred/i.test(html);
|
|
774
|
+
}
|
|
775
|
+
function isSuccessfulBrowserNavigationStatus(status) {
|
|
776
|
+
return status === 200 || status >= 300 && status < 400;
|
|
777
|
+
}
|
|
778
|
+
async function probeLoginPage(baseUrl) {
|
|
759
779
|
try {
|
|
760
780
|
const response = await fetch(`${baseUrl}/login`, {
|
|
761
781
|
method: "GET",
|
|
762
782
|
redirect: "manual"
|
|
763
783
|
});
|
|
764
|
-
if (response.status
|
|
765
|
-
return
|
|
784
|
+
if (!isSuccessfulBrowserNavigationStatus(response.status)) {
|
|
785
|
+
return {
|
|
786
|
+
status: response.status,
|
|
787
|
+
healthy: false,
|
|
788
|
+
detail: `GET /login returned ${response.status}`
|
|
789
|
+
};
|
|
766
790
|
}
|
|
767
791
|
if (response.status !== 200) {
|
|
768
|
-
return
|
|
792
|
+
return {
|
|
793
|
+
status: response.status,
|
|
794
|
+
healthy: true,
|
|
795
|
+
detail: `GET /login returned redirect ${response.status}`
|
|
796
|
+
};
|
|
769
797
|
}
|
|
770
|
-
const html = await response.text();
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
return !/Application error: a client-side exception has occurred/i.test(html);
|
|
778
|
-
}
|
|
779
|
-
function extractReferencedNextAssets(html, maxAssets = 8) {
|
|
780
|
-
const matches = html.match(NEXT_STATIC_ASSET_PATTERN) ?? [];
|
|
781
|
-
const unique = Array.from(new Set(matches));
|
|
782
|
-
return unique.slice(0, maxAssets);
|
|
783
|
-
}
|
|
784
|
-
async function areReferencedNextAssetsReachable(baseUrl, html) {
|
|
785
|
-
const assets = extractReferencedNextAssets(html);
|
|
786
|
-
if (assets.length === 0) {
|
|
787
|
-
return false;
|
|
788
|
-
}
|
|
789
|
-
for (const assetPath of assets) {
|
|
790
|
-
try {
|
|
791
|
-
const response = await fetch(`${baseUrl}${assetPath}`, {
|
|
792
|
-
method: "GET",
|
|
793
|
-
redirect: "manual"
|
|
794
|
-
});
|
|
795
|
-
if (response.status !== 200 && response.status !== 304) {
|
|
796
|
-
return false;
|
|
797
|
-
}
|
|
798
|
-
} catch {
|
|
799
|
-
return false;
|
|
798
|
+
const html = await response.text().catch(() => "");
|
|
799
|
+
if (!isLoginHtmlHealthy(html)) {
|
|
800
|
+
return {
|
|
801
|
+
status: response.status,
|
|
802
|
+
healthy: false,
|
|
803
|
+
detail: "GET /login returned client-side exception HTML"
|
|
804
|
+
};
|
|
800
805
|
}
|
|
806
|
+
return {
|
|
807
|
+
status: response.status,
|
|
808
|
+
healthy: true,
|
|
809
|
+
detail: "GET /login returned healthy HTML"
|
|
810
|
+
};
|
|
811
|
+
} catch (error) {
|
|
812
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
813
|
+
return {
|
|
814
|
+
status: null,
|
|
815
|
+
healthy: false,
|
|
816
|
+
detail: `GET /login failed: ${message}`
|
|
817
|
+
};
|
|
801
818
|
}
|
|
802
|
-
return true;
|
|
803
819
|
}
|
|
804
|
-
async function
|
|
820
|
+
async function probeBackendLoginEndpoint(baseUrl) {
|
|
805
821
|
try {
|
|
806
822
|
const form = new URLSearchParams();
|
|
807
823
|
form.set("email", "integration-healthcheck@example.invalid");
|
|
@@ -814,11 +830,32 @@ async function isBackendLoginEndpointHealthy(baseUrl) {
|
|
|
814
830
|
},
|
|
815
831
|
body: form.toString()
|
|
816
832
|
});
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
833
|
+
const healthy = response.status === 200 || response.status === 400 || response.status === 401 || response.status === 403;
|
|
834
|
+
return {
|
|
835
|
+
status: response.status,
|
|
836
|
+
healthy,
|
|
837
|
+
detail: healthy ? `POST /api/auth/login returned ${response.status}` : `POST /api/auth/login returned unexpected ${response.status}`
|
|
838
|
+
};
|
|
839
|
+
} catch (error) {
|
|
840
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
841
|
+
return {
|
|
842
|
+
status: null,
|
|
843
|
+
healthy: false,
|
|
844
|
+
detail: `POST /api/auth/login failed: ${message}`
|
|
845
|
+
};
|
|
820
846
|
}
|
|
821
847
|
}
|
|
848
|
+
async function probeApplicationReadiness(baseUrl) {
|
|
849
|
+
const [frontend, backend] = await Promise.all([
|
|
850
|
+
probeLoginPage(baseUrl),
|
|
851
|
+
probeBackendLoginEndpoint(baseUrl)
|
|
852
|
+
]);
|
|
853
|
+
return {
|
|
854
|
+
ready: frontend.healthy && backend.healthy,
|
|
855
|
+
frontend,
|
|
856
|
+
backend
|
|
857
|
+
};
|
|
858
|
+
}
|
|
822
859
|
async function acquireEphemeralEnvironmentLock(logPrefix) {
|
|
823
860
|
await mkdir(path.dirname(EPHEMERAL_ENV_LOCK_PATH), { recursive: true });
|
|
824
861
|
const deadlineTimestamp = Date.now() + EPHEMERAL_ENV_LOCK_TIMEOUT_MS;
|
|
@@ -914,8 +951,11 @@ function buildReusableEnvironment(baseUrl, captureScreenshots) {
|
|
|
914
951
|
return buildEnvironment({
|
|
915
952
|
BASE_URL: baseUrl,
|
|
916
953
|
NODE_ENV: "production",
|
|
954
|
+
JWT_SECRET: process.env.JWT_SECRET ?? "om-ephemeral-integration-jwt-secret",
|
|
955
|
+
OM_SECURITY_MFA_SETUP_SECRET: process.env.OM_SECURITY_MFA_SETUP_SECRET ?? "om-ephemeral-integration-mfa-setup-secret",
|
|
917
956
|
OM_ENABLE_ENTERPRISE_MODULES: process.env.OM_ENABLE_ENTERPRISE_MODULES ?? "false",
|
|
918
957
|
OM_ENABLE_ENTERPRISE_MODULES_SSO: process.env.OM_ENABLE_ENTERPRISE_MODULES_SSO ?? "false",
|
|
958
|
+
OM_ENABLE_ENTERPRISE_MODULES_SECURITY: process.env.OM_ENABLE_ENTERPRISE_MODULES_SECURITY ?? "false",
|
|
919
959
|
OM_TEST_MODE: "1",
|
|
920
960
|
ENABLE_CRUD_API_CACHE: "true",
|
|
921
961
|
NEXT_PUBLIC_OM_EXAMPLE_INJECTION_WIDGETS_ENABLED: "true",
|
|
@@ -980,42 +1020,21 @@ async function tryReuseExistingEnvironment(options) {
|
|
|
980
1020
|
}
|
|
981
1021
|
};
|
|
982
1022
|
}
|
|
983
|
-
async function waitForApplicationReadiness(baseUrl, appProcess) {
|
|
1023
|
+
async function waitForApplicationReadiness(baseUrl, appProcess, options) {
|
|
984
1024
|
const startTimestamp = Date.now();
|
|
985
1025
|
const exitPromise = getProcessExitPromise(appProcess);
|
|
986
1026
|
const readinessStabilizationMs = 600;
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
method: "GET",
|
|
990
|
-
redirect: "manual"
|
|
991
|
-
}).then(async (response) => ({
|
|
992
|
-
response,
|
|
993
|
-
body: response.status === 200 ? await response.text().catch(() => "") : ""
|
|
994
|
-
})).catch(() => null);
|
|
1027
|
+
let lastProbe = null;
|
|
1028
|
+
while (Date.now() - startTimestamp < options.timeoutMs) {
|
|
995
1029
|
const result = await Promise.race([
|
|
996
|
-
|
|
997
|
-
if (!payload) {
|
|
998
|
-
return { kind: "network_error" };
|
|
999
|
-
}
|
|
1000
|
-
return {
|
|
1001
|
-
kind: "response",
|
|
1002
|
-
status: payload.response.status,
|
|
1003
|
-
body: payload.body
|
|
1004
|
-
};
|
|
1005
|
-
}),
|
|
1030
|
+
probeApplicationReadiness(baseUrl).then((probe) => ({ kind: "probe", probe })),
|
|
1006
1031
|
exitPromise.then((code) => ({ kind: "exit", code })),
|
|
1007
1032
|
delay(APP_READY_INTERVAL_MS).then(() => ({ kind: "timeout" }))
|
|
1008
1033
|
]);
|
|
1009
|
-
if (result.kind === "
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
continue;
|
|
1014
|
-
}
|
|
1015
|
-
const assetsReachable = await areReferencedNextAssetsReachable(baseUrl, loginHtml);
|
|
1016
|
-
if (!assetsReachable) {
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1034
|
+
if (result.kind === "probe") {
|
|
1035
|
+
lastProbe = result.probe;
|
|
1036
|
+
if (!result.probe.ready) {
|
|
1037
|
+
continue;
|
|
1019
1038
|
}
|
|
1020
1039
|
const processExited = await Promise.race([
|
|
1021
1040
|
exitPromise.then(() => true),
|
|
@@ -1030,7 +1049,11 @@ async function waitForApplicationReadiness(baseUrl, appProcess) {
|
|
|
1030
1049
|
throw new Error(`Application process exited before readiness check (exit ${result.code ?? "unknown"})`);
|
|
1031
1050
|
}
|
|
1032
1051
|
}
|
|
1033
|
-
|
|
1052
|
+
const lastFrontendDetail = lastProbe?.frontend.detail ?? "GET /login was never observed";
|
|
1053
|
+
const lastBackendDetail = lastProbe?.backend.detail ?? "POST /api/auth/login was never observed";
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
`Application did not become ready within ${options.timeoutMs / 1e3} seconds. Last probe: ${lastFrontendDetail}; ${lastBackendDetail}`
|
|
1056
|
+
);
|
|
1034
1057
|
}
|
|
1035
1058
|
function parseOptions(rawArgs) {
|
|
1036
1059
|
let keep = false;
|
|
@@ -1847,11 +1870,8 @@ async function runIntegrationTestSuiteOnce(environment, options) {
|
|
|
1847
1870
|
});
|
|
1848
1871
|
}
|
|
1849
1872
|
async function isEnvironmentUnavailable(baseUrl) {
|
|
1850
|
-
const
|
|
1851
|
-
|
|
1852
|
-
isBackendLoginEndpointHealthy(baseUrl)
|
|
1853
|
-
]);
|
|
1854
|
-
return !applicationReachable || !backendHealthy;
|
|
1873
|
+
const readiness = await probeApplicationReadiness(baseUrl);
|
|
1874
|
+
return !readiness.ready;
|
|
1855
1875
|
}
|
|
1856
1876
|
function isEnvironmentUnavailableError(error) {
|
|
1857
1877
|
if (!(error instanceof Error)) {
|
|
@@ -1991,7 +2011,8 @@ async function startEphemeralEnvironment(options) {
|
|
|
1991
2011
|
const commandEnvironment = buildEnvironment({
|
|
1992
2012
|
DATABASE_URL: databaseUrl,
|
|
1993
2013
|
BASE_URL: applicationBaseUrl,
|
|
1994
|
-
JWT_SECRET: "om-ephemeral-integration-jwt-secret",
|
|
2014
|
+
JWT_SECRET: process.env.JWT_SECRET ?? "om-ephemeral-integration-jwt-secret",
|
|
2015
|
+
OM_SECURITY_MFA_SETUP_SECRET: process.env.OM_SECURITY_MFA_SETUP_SECRET ?? "om-ephemeral-integration-mfa-setup-secret",
|
|
1995
2016
|
NODE_ENV: "production",
|
|
1996
2017
|
DB_POOL_MIN: "0",
|
|
1997
2018
|
DB_POOL_MAX: "5",
|
|
@@ -2001,6 +2022,7 @@ async function startEphemeralEnvironment(options) {
|
|
|
2001
2022
|
DB_IDLE_IN_TRANSACTION_TIMEOUT_MS: "30000",
|
|
2002
2023
|
OM_ENABLE_ENTERPRISE_MODULES: process.env.OM_ENABLE_ENTERPRISE_MODULES ?? "false",
|
|
2003
2024
|
OM_ENABLE_ENTERPRISE_MODULES_SSO: process.env.OM_ENABLE_ENTERPRISE_MODULES_SSO ?? "false",
|
|
2025
|
+
OM_ENABLE_ENTERPRISE_MODULES_SECURITY: process.env.OM_ENABLE_ENTERPRISE_MODULES_SECURITY ?? "false",
|
|
2004
2026
|
OM_TEST_MODE: "1",
|
|
2005
2027
|
OM_TEST_AUTH_RATE_LIMIT_MODE: "opt-in",
|
|
2006
2028
|
OM_DISABLE_EMAIL_DELIVERY: "1",
|
|
@@ -2030,6 +2052,7 @@ async function startEphemeralEnvironment(options) {
|
|
|
2030
2052
|
await clearEphemeralEnvironmentState();
|
|
2031
2053
|
};
|
|
2032
2054
|
try {
|
|
2055
|
+
const appReadyTimeoutMs = resolveAppReadyTimeoutMs(options.logPrefix);
|
|
2033
2056
|
const buildCacheTtlSeconds = resolveBuildCacheTtlSeconds(options.logPrefix);
|
|
2034
2057
|
let sourceFingerprintValue = null;
|
|
2035
2058
|
let needsBuild = true;
|
|
@@ -2091,7 +2114,14 @@ async function startEphemeralEnvironment(options) {
|
|
|
2091
2114
|
silent: !options.verbose
|
|
2092
2115
|
});
|
|
2093
2116
|
applicationProcess = startedAppProcess;
|
|
2094
|
-
await runTimedStep(
|
|
2117
|
+
await runTimedStep(
|
|
2118
|
+
options.logPrefix,
|
|
2119
|
+
"Waiting for application readiness",
|
|
2120
|
+
{ expectedSeconds: Math.max(12, Math.ceil(appReadyTimeoutMs / 1e3)) },
|
|
2121
|
+
async () => waitForApplicationReadiness(applicationBaseUrl, startedAppProcess, {
|
|
2122
|
+
timeoutMs: appReadyTimeoutMs
|
|
2123
|
+
})
|
|
2124
|
+
);
|
|
2095
2125
|
console.log(`[${options.logPrefix}] Application is ready at ${applicationBaseUrl}`);
|
|
2096
2126
|
await writeEphemeralEnvironmentState({
|
|
2097
2127
|
baseUrl: applicationBaseUrl,
|
|
@@ -2352,6 +2382,7 @@ export {
|
|
|
2352
2382
|
parseInteractiveIntegrationOptions,
|
|
2353
2383
|
parseOptions,
|
|
2354
2384
|
readEphemeralEnvironmentState,
|
|
2385
|
+
resolveAppReadyTimeoutMs,
|
|
2355
2386
|
resolveBuildCacheTtlSeconds,
|
|
2356
2387
|
runEphemeralAppForQa,
|
|
2357
2388
|
runIntegrationCoverageReport,
|