@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.
@@ -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 APP_READY_TIMEOUT_MS = 9e4;
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
- async function isApplicationReachable(baseUrl) {
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 === 302) {
765
- return true;
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 false;
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
- return isLoginHtmlHealthy(html) && await areReferencedNextAssetsReachable(baseUrl, html);
772
- } catch {
773
- return false;
774
- }
775
- }
776
- function isLoginHtmlHealthy(html) {
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 isBackendLoginEndpointHealthy(baseUrl) {
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
- return response.status === 200 || response.status === 400 || response.status === 401 || response.status === 403;
818
- } catch {
819
- return false;
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
- while (Date.now() - startTimestamp < APP_READY_TIMEOUT_MS) {
988
- const responsePromise = fetch(`${baseUrl}/login`, {
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
- responsePromise.then((payload) => {
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 === "response" && (result.status === 200 || result.status === 302)) {
1010
- if (result.status === 200) {
1011
- const loginHtml = result.body ?? "";
1012
- if (!isLoginHtmlHealthy(loginHtml)) {
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
- throw new Error(`Application did not become ready within ${APP_READY_TIMEOUT_MS / 1e3} seconds`);
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 [applicationReachable, backendHealthy] = await Promise.all([
1851
- isApplicationReachable(baseUrl),
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(options.logPrefix, "Waiting for application readiness", { expectedSeconds: 12 }, async () => waitForApplicationReadiness(applicationBaseUrl, startedAppProcess));
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,