From f5d4964a1b45adef2bc2d09c67af54b0acffb9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yuniel=20Acosta=20P=C3=A9rez?= <33158051+yacosta738@users.noreply.github.com> Date: Sun, 1 Mar 2026 20:46:58 +0100 Subject: [PATCH 1/4] chore(env): add repo-wide .env.example and adopt provider-aware site resolution for docs --- .env.example | 149 +++++++++++++++++++++++++ clients/web/apps/docs/astro.config.mjs | 34 +++++- 2 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..c5ddc6d63 --- /dev/null +++ b/.env.example @@ -0,0 +1,149 @@ +# corvus/.env.example +# +# Canonical list of environment variables used across this repository. +# - DO NOT store real secrets here. Use CI secrets or your local secret manager (direnv, .env.local, etc.). +# - This file is a documentation + example file. Copy it to .env for local development and fill in real values where required. +# - Frontend/public envs: the shared getEnv helper accepts keys with no prefix, PUBLIC_ or VITE_. + +######################################## +# Global / environment +######################################## +# NODE_ENV - common runtime mode. optional. values: development|production +NODE_ENV=development + +# Generic site fallback used by resolveSiteUrl() when provider-specific keys are missing +# Example: https://example.com or http://localhost:4321 +SITE_URL=http://localhost:9988 + +######################################## +# Local dev ports (sane defaults used by shared env helper) +######################################## +# These are the PORTS mapping used across the web packages. Change only if you have port conflicts. +MARKETING_PORT=9988 +PLUGINS_PORT=9990 +DOCS_PORT=4321 +CHAT_PORT=4323 + +######################################## +# Web apps / Static sites +######################################## +# Marketing site - provider-aware. Local default uses MARKETING_PORT above. +MARKETING_URL=http://localhost:${MARKETING_PORT} +# Docs site +DOCS_URL=http://localhost:${DOCS_PORT} + +# Hosting provider fallbacks (set by provider automatically in their build environments) +# These are read by resolveSiteUrl({ providerKeys: { cloudflare: 'CF_PAGES_URL', vercel: 'VERCEL_URL', netlify: 'URL' }}) +CF_PAGES_URL= +VERCEL_URL= +URL= + +######################################## +# Frontend public keys (safe for client; mark explicitly if client-side) +######################################## +# Public keys that are intentionally exposed to browser code should use PUBLIC_ or VITE_ prefix +# Example (marketing analytics; safe to be public): +PUBLIC_AHREFS_KEY=pk_ahrefs_example + +######################################## +# Agent runtime / backend (Corvus) +######################################## +# CORVUS_API_KEY - API key for internal agent auth (keep secret) +CORVUS_API_KEY=sk_corvus_xxx + +# CORVUS workspace and routing +CORVUS_WORKSPACE=default +CORVUS_GATEWAY_HOST=127.0.0.1 +CORVUS_GATEWAY_PORT=4000 + +# Feature flags (optional) +CORVUS_OPEN_SKILLS_ENABLED=false + +######################################## +# SurrealDB (primary app DB for agent runtime) +######################################## +# Use a local dev SurrealDB for development. Production should be set via secrets. +CORVUS_SURREALDB_URL=http://127.0.0.1:8000 +CORVUS_SURREALDB_NAMESPACE=corvus +CORVUS_SURREALDB_DATABASE=corvus_db +CORVUS_SURREALDB_USERNAME=root +CORVUS_SURREALDB_PASSWORD=local-dev-password +CORVUS_SURREALDB_TOKEN= + +# Test-specific SurrealDB (used by CI/tests when present) +CORVUS_TEST_SURREALDB_URL=http://127.0.0.1:8001 +CORVUS_TEST_SURREALDB_NAMESPACE=corvus_test +CORVUS_TEST_SURREALDB_DATABASE=corvus_test_db + +######################################## +# Third-party provider keys (examples/placeholders) +######################################## +# Google APIs (if used) +GOOGLE_API_KEY=AIza...your_key_here + +# Anthropic / OpenAI / LLM providers +ANTHROPIC_API_KEY=sk_antropic_xxx +OPENAI_API_KEY=sk_openai_xxx + +# GitHub / CI tokens +GITHUB_TOKEN=ghp_xxx +GH_TOKEN=ghp_xxx + +######################################## +# CI, release & publishing secrets (store in GitHub Secrets / CI provider) +######################################## +# GPG / signing used in release pipelines (example placeholders only) +SIGNING_IN_MEMORY_KEY=base64-private-key +SIGNING_IN_MEMORY_KEY_PASSWORD=changeit + +# Maven Central +MAVEN_CENTRAL_USERNAME=your_maven_username +MAVEN_CENTRAL_PASSWORD=your_maven_password + +# Cargo / crates.io +CARGO_REGISTRY_TOKEN=crates_io_token + +# NPM / packages +NPM_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# DockerHub +DOCKERHUB_USERNAME=your_dockerhub_user +DOCKERHUB_TOKEN=your_dockerhub_token + +# Code quality / telemetry +SONAR_TOKEN= +CODECOV_TOKEN= + +######################################## +# Gradle / build / repo config +######################################## +# Project coordinates and build cache credentials (optional) +GROUP=com.profiletailors +VERSION=0.1.0 +BUILD_CACHE_USER= +BUILD_CACHE_PWD= +PRIREPO_URL= +PRIREPO_USERNAME= +PRIREPO_PASSWORD= +ENABLE_LOCAL_CONFIG=true + +######################################## +# Optional / feature toggles & local helpers +######################################## +# If you need a shorter local override for SITE_URL per app, set the specific one above (MARKETING_URL, DOCS_URL, ...) + +# Example debug flags +DEBUG=true + +######################################## +# Notes +######################################## +# - Copy this file to .env and fill in the secret values before running local services. +# - For CI and releases: add all SECRET values (signing keys, repository tokens, package tokens) to your +# repository's GitHub Secrets or your CI provider's secret store. Never commit real credentials to the repo. +# - Frontend/public variables: prefix with PUBLIC_ or VITE_ if they are safe to expose to the browser. +# - The repo's shared env helper will prefer provider-specific envs (CF_PAGES_URL, VERCEL_URL, URL) when present +# and fall back to the primary key (MARKETING_URL / DOCS_URL / SITE_URL) for site resolution. + +# If you want me to run an exhaustive scan of the codebase to collect every referenced env var and +# produce a fully authoritative .env.example, say "Yes, scan the repo and generate the exhaustive .env.example". diff --git a/clients/web/apps/docs/astro.config.mjs b/clients/web/apps/docs/astro.config.mjs index 4043febc7..d1abeafc5 100644 --- a/clients/web/apps/docs/astro.config.mjs +++ b/clients/web/apps/docs/astro.config.mjs @@ -1,10 +1,40 @@ import starlight from "@astrojs/starlight"; import { defineConfig } from "astro/config"; import { viewTransitions } from "astro-vtbot/starlight-view-transitions"; +import { getPortFromUrl, PORTS, resolveSiteUrl } from "@corvus/shared/env"; +import { loadEnv } from "vite"; + +const DEFAULT_DEV_URL = `http://localhost:${PORTS.DOCS}`; +const DEFAULT_PROD_URL = "https://docs.profiletailors.com"; + +const mode = process.env.NODE_ENV || "production"; +const env = loadEnv(mode, process.cwd(), ""); +const docsUrl = resolveSiteUrl({ + env, + primaryKey: "DOCS_URL", + localDefault: DEFAULT_DEV_URL, + productionDefault: DEFAULT_PROD_URL, + genericKeys: ["SITE_URL"], + providerKeys: { + cloudflare: "CF_PAGES_URL", + vercel: "VERCEL_URL", + netlify: "URL", + }, + isProdLike: mode === "production", +}); +const resolvedPort = getPortFromUrl(docsUrl, PORTS.DOCS); export default defineConfig({ - site: "https://dallay.github.io", - base: "/corvus", + site: docsUrl, + base: "/", // ← Root level for custom domain + server: { + host: true, + port: resolvedPort, + }, + preview: { + host: true, + port: resolvedPort, + }, integrations: [ starlight({ title: "Corvus", From dad256920ccecf631302b2b8f9219a7b2a11b7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yuniel=20Acosta=20P=C3=A9rez?= <33158051+yacosta738@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:04:14 +0100 Subject: [PATCH 2/4] chore(changelog): fix templates, label extractors, and sort order; ensure schema link removed --- .github/config/changelog.json | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) mode change 100755 => 100644 .github/config/changelog.json diff --git a/.github/config/changelog.json b/.github/config/changelog.json old mode 100755 new mode 100644 index 1906b4a91..d9edd6f0e --- a/.github/config/changelog.json +++ b/.github/config/changelog.json @@ -1,8 +1,7 @@ { - "$schema": "https://raw.githubusercontent.com/mikepenz/release-changelog-builder-action/main/configs/json-schema.json", - "template": "## 📢 What's Changed\n\n#{{CHANGELOG}}\n
\n🔐 Uncategorized Changes\n\n#{{UNCATEGORIZED}}\n
\n\n## Contributors:\n- #{{CONTRIBUTORS}}", - "commit_template": "- #{{TITLE}} #{{MERGE_SHA}}", - "pr_template": "- #{{TITLE}} - #{{MERGE_SHA}} - ##{{NUMBER}}", + "template": "## 📢 What's Changed\n\n{{CHANGELOG}}\n\n
\n🔐 Uncategorized Changes\n\n{{UNCATEGORIZED}}\n
\n\n## Contributors:\n- {{CONTRIBUTORS}}", + "commit_template": "- {{TITLE}} ({{MERGE_SHA}})", + "pr_template": "- {{TITLE}} — #{{NUMBER}} (merge {{MERGE_SHA}})", "empty_template": "- No changes in this release", "exclude_merge_branches": ["hotfix/temp-*", "owner/qa", "owner/test"], "tag_resolver": { @@ -73,13 +72,13 @@ "on_property": "title", "method": "regexr", "pattern": "!!\\s*$", - "target": "$'pr|breaking-change" + "target": "pr|breaking-change" }, { "on_property": "title", "method": "regexr", - "pattern": "\\.\\.\\s*$", - "target": "$'pr|changelog-ignore" + "pattern": "\\.\\.\\.\\s*$", + "target": "pr|changelog-ignore" }, { "on_property": "title", @@ -89,7 +88,7 @@ } ], "sort": { - "order": "ASC", + "order": "DESC", "on_property": "mergedAt" }, "max_tags_to_fetch": 200, From 5b5c0b50aaae491b673a93b3ad67856188e85728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yuniel=20Acosta=20P=C3=A9rez?= <33158051+yacosta738@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:04:51 +0100 Subject: [PATCH 3/4] style(docs): fix imports and unused catch variable per lint --- clients/web/apps/docs/astro.config.mjs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/clients/web/apps/docs/astro.config.mjs b/clients/web/apps/docs/astro.config.mjs index d1abeafc5..1f4ca5c80 100644 --- a/clients/web/apps/docs/astro.config.mjs +++ b/clients/web/apps/docs/astro.config.mjs @@ -24,9 +24,23 @@ const docsUrl = resolveSiteUrl({ }); const resolvedPort = getPortFromUrl(docsUrl, PORTS.DOCS); +function computeBaseFromUrl(url) { + if (!url) return "/"; + try { + const pathname = new URL(url).pathname || "/"; + // Ensure leading slash and remove trailing slash except for root + const normalized = pathname === "/" ? "/" : `/${pathname.replace(/^\/+|\/+$/g, "").replace(/\/+$/, "")}`; + return normalized === "" ? "/" : normalized.replace(/\/$/, ""); + } catch (e) { + return "/"; + } +} + +const base = computeBaseFromUrl(docsUrl); + export default defineConfig({ site: docsUrl, - base: "/", // ← Root level for custom domain + base, // computed from the provider/site URL so subpath deployments work server: { host: true, port: resolvedPort, From 9bada94d544cc7ec11bc5f2a4890ae121c3e03c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yuniel=20Acosta=20P=C3=A9rez?= <33158051+yacosta738@users.noreply.github.com> Date: Sun, 1 Mar 2026 21:22:05 +0100 Subject: [PATCH 4/4] chore(env): sanitize .env.example placeholders; compute astro base from site URL; add env resolution tests --- .env.example | 57 ++++----- clients/web/apps/docs/astro.config.mjs | 7 +- clients/web/packages/shared/test/env.test.mjs | 115 ++++++++++++++++++ 3 files changed, 144 insertions(+), 35 deletions(-) create mode 100644 clients/web/packages/shared/test/env.test.mjs diff --git a/.env.example b/.env.example index c5ddc6d63..ab2e16195 100644 --- a/.env.example +++ b/.env.example @@ -19,96 +19,92 @@ SITE_URL=http://localhost:9988 # Local dev ports (sane defaults used by shared env helper) ######################################## # These are the PORTS mapping used across the web packages. Change only if you have port conflicts. +CHAT_PORT=4323 +DOCS_PORT=4321 MARKETING_PORT=9988 PLUGINS_PORT=9990 -DOCS_PORT=4321 -CHAT_PORT=4323 ######################################## # Web apps / Static sites ######################################## -# Marketing site - provider-aware. Local default uses MARKETING_PORT above. -MARKETING_URL=http://localhost:${MARKETING_PORT} # Docs site DOCS_URL=http://localhost:${DOCS_PORT} +# Marketing site - provider-aware. Local default uses MARKETING_PORT above. +MARKETING_URL=http://localhost:${MARKETING_PORT} # Hosting provider fallbacks (set by provider automatically in their build environments) # These are read by resolveSiteUrl({ providerKeys: { cloudflare: 'CF_PAGES_URL', vercel: 'VERCEL_URL', netlify: 'URL' }}) CF_PAGES_URL= -VERCEL_URL= URL= +VERCEL_URL= ######################################## # Frontend public keys (safe for client; mark explicitly if client-side) ######################################## # Public keys that are intentionally exposed to browser code should use PUBLIC_ or VITE_ prefix # Example (marketing analytics; safe to be public): -PUBLIC_AHREFS_KEY=pk_ahrefs_example +PUBLIC_AHREFS_KEY=your_ahrefs_public_key_here ######################################## # Agent runtime / backend (Corvus) ######################################## # CORVUS_API_KEY - API key for internal agent auth (keep secret) -CORVUS_API_KEY=sk_corvus_xxx - -# CORVUS workspace and routing -CORVUS_WORKSPACE=default +CORVUS_API_KEY=your_corvus_api_key_here CORVUS_GATEWAY_HOST=127.0.0.1 CORVUS_GATEWAY_PORT=4000 - -# Feature flags (optional) CORVUS_OPEN_SKILLS_ENABLED=false +CORVUS_WORKSPACE=default ######################################## # SurrealDB (primary app DB for agent runtime) ######################################## # Use a local dev SurrealDB for development. Production should be set via secrets. -CORVUS_SURREALDB_URL=http://127.0.0.1:8000 -CORVUS_SURREALDB_NAMESPACE=corvus CORVUS_SURREALDB_DATABASE=corvus_db -CORVUS_SURREALDB_USERNAME=root -CORVUS_SURREALDB_PASSWORD=local-dev-password +CORVUS_SURREALDB_NAMESPACE=corvus +CORVUS_SURREALDB_PASSWORD=your_local_dev_db_password_here CORVUS_SURREALDB_TOKEN= +CORVUS_SURREALDB_URL=http://127.0.0.1:8000 +CORVUS_SURREALDB_USERNAME=root # Test-specific SurrealDB (used by CI/tests when present) -CORVUS_TEST_SURREALDB_URL=http://127.0.0.1:8001 -CORVUS_TEST_SURREALDB_NAMESPACE=corvus_test CORVUS_TEST_SURREALDB_DATABASE=corvus_test_db +CORVUS_TEST_SURREALDB_NAMESPACE=corvus_test +CORVUS_TEST_SURREALDB_URL=http://127.0.0.1:8001 ######################################## # Third-party provider keys (examples/placeholders) ######################################## # Google APIs (if used) -GOOGLE_API_KEY=AIza...your_key_here +GOOGLE_API_KEY=your_google_api_key_here # Anthropic / OpenAI / LLM providers -ANTHROPIC_API_KEY=sk_antropic_xxx -OPENAI_API_KEY=sk_openai_xxx +ANTHROPIC_API_KEY=your_anthropic_api_key_here +OPENAI_API_KEY=your_openai_api_key_here # GitHub / CI tokens -GITHUB_TOKEN=ghp_xxx -GH_TOKEN=ghp_xxx +GH_TOKEN=your_gh_token_here +GITHUB_TOKEN=your_github_token_here ######################################## # CI, release & publishing secrets (store in GitHub Secrets / CI provider) ######################################## # GPG / signing used in release pipelines (example placeholders only) -SIGNING_IN_MEMORY_KEY=base64-private-key -SIGNING_IN_MEMORY_KEY_PASSWORD=changeit +SIGNING_IN_MEMORY_KEY=your_signing_key_base64_here +SIGNING_IN_MEMORY_KEY_PASSWORD=your_signing_key_password_here # Maven Central MAVEN_CENTRAL_USERNAME=your_maven_username MAVEN_CENTRAL_PASSWORD=your_maven_password # Cargo / crates.io -CARGO_REGISTRY_TOKEN=crates_io_token +CARGO_REGISTRY_TOKEN=your_crates_io_token_here # NPM / packages -NPM_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +NPM_TOKEN=your_npm_token_here # DockerHub -DOCKERHUB_USERNAME=your_dockerhub_user -DOCKERHUB_TOKEN=your_dockerhub_token +DOCKERHUB_USERNAME=your_dockerhub_username_here +DOCKERHUB_TOKEN=your_dockerhub_token_here # Code quality / telemetry SONAR_TOKEN= @@ -144,6 +140,3 @@ DEBUG=true # - Frontend/public variables: prefix with PUBLIC_ or VITE_ if they are safe to expose to the browser. # - The repo's shared env helper will prefer provider-specific envs (CF_PAGES_URL, VERCEL_URL, URL) when present # and fall back to the primary key (MARKETING_URL / DOCS_URL / SITE_URL) for site resolution. - -# If you want me to run an exhaustive scan of the codebase to collect every referenced env var and -# produce a fully authoritative .env.example, say "Yes, scan the repo and generate the exhaustive .env.example". diff --git a/clients/web/apps/docs/astro.config.mjs b/clients/web/apps/docs/astro.config.mjs index 1f4ca5c80..dbf9fbd1f 100644 --- a/clients/web/apps/docs/astro.config.mjs +++ b/clients/web/apps/docs/astro.config.mjs @@ -28,9 +28,10 @@ function computeBaseFromUrl(url) { if (!url) return "/"; try { const pathname = new URL(url).pathname || "/"; - // Ensure leading slash and remove trailing slash except for root - const normalized = pathname === "/" ? "/" : `/${pathname.replace(/^\/+|\/+$/g, "").replace(/\/+$/, "")}`; - return normalized === "" ? "/" : normalized.replace(/\/$/, ""); + // If root, keep '/' + if (pathname === "/") return "/"; + // Remove trailing slash for non-root paths + return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname; } catch (e) { return "/"; } diff --git a/clients/web/packages/shared/test/env.test.mjs b/clients/web/packages/shared/test/env.test.mjs new file mode 100644 index 000000000..4a151f878 --- /dev/null +++ b/clients/web/packages/shared/test/env.test.mjs @@ -0,0 +1,115 @@ +import { describe, it, expect } from "vitest"; +import { resolveSiteUrl, getPortFromUrl, PORTS } from "../env.mjs"; + +const DEFAULT_DEV_URL = `http://localhost:${PORTS.DOCS}`; +const DEFAULT_PROD_URL = "https://docs.profiletailors.com"; + +describe("resolveSiteUrl and getPortFromUrl matrix", () => { + it("resolveSiteUrl_prefers_DOCS_URL_over_providers_and_falls_back_to_SITE_URL", () => { + // Explicit DOCS_URL should win + const explicit = resolveSiteUrl({ + env: { DOCS_URL: "http://explicit.example/path" }, + primaryKey: "DOCS_URL", + localDefault: DEFAULT_DEV_URL, + productionDefault: DEFAULT_PROD_URL, + genericKeys: ["SITE_URL"], + providerKeys: { cloudflare: "CF_PAGES_URL", vercel: "VERCEL_URL", netlify: "URL" }, + isProdLike: false, + }); + expect(explicit).toBe("http://explicit.example/path"); + + // Cloudflare provider should be selected when DOCS_URL missing + const cloudflare = resolveSiteUrl({ + env: { CF_PAGES_URL: "docs.cloudflare.test/sub" }, + primaryKey: "DOCS_URL", + localDefault: DEFAULT_DEV_URL, + productionDefault: DEFAULT_PROD_URL, + genericKeys: ["SITE_URL"], + providerKeys: { cloudflare: "CF_PAGES_URL", vercel: "VERCEL_URL", netlify: "URL" }, + isProdLike: false, + }); + expect(cloudflare).toBe("https://docs.cloudflare.test/sub"); + + // Vercel provider + const vercel = resolveSiteUrl({ + env: { VERCEL_URL: "my-vercel.app/docs" }, + primaryKey: "DOCS_URL", + localDefault: DEFAULT_DEV_URL, + productionDefault: DEFAULT_PROD_URL, + genericKeys: ["SITE_URL"], + providerKeys: { cloudflare: "CF_PAGES_URL", vercel: "VERCEL_URL", netlify: "URL" }, + isProdLike: false, + }); + expect(vercel).toBe("https://my-vercel.app/docs"); + + // Netlify provider (URL) + const netlify = resolveSiteUrl({ + env: { URL: "https://netlify.example/base/" }, + primaryKey: "DOCS_URL", + localDefault: DEFAULT_DEV_URL, + productionDefault: DEFAULT_PROD_URL, + genericKeys: ["SITE_URL"], + providerKeys: { cloudflare: "CF_PAGES_URL", vercel: "VERCEL_URL", netlify: "URL" }, + isProdLike: false, + }); + expect(netlify).toBe("https://netlify.example/base"); + + // Generic SITE_URL fallback + const generic = resolveSiteUrl({ + env: { SITE_URL: "http://fallback.test:9001/fpath" }, + primaryKey: "DOCS_URL", + localDefault: DEFAULT_DEV_URL, + productionDefault: DEFAULT_PROD_URL, + genericKeys: ["SITE_URL"], + providerKeys: { cloudflare: "CF_PAGES_URL", vercel: "VERCEL_URL", netlify: "URL" }, + isProdLike: false, + }); + expect(generic).toBe("http://fallback.test:9001/fpath"); + + // No envs -> dev fallback + const fallbackDev = resolveSiteUrl({ + env: {}, + primaryKey: "DOCS_URL", + localDefault: DEFAULT_DEV_URL, + productionDefault: DEFAULT_PROD_URL, + genericKeys: ["SITE_URL"], + providerKeys: { cloudflare: "CF_PAGES_URL", vercel: "VERCEL_URL", netlify: "URL" }, + isProdLike: false, + }); + expect(fallbackDev).toBe(DEFAULT_DEV_URL); + + // No envs -> prod fallback when isProdLike + const fallbackProd = resolveSiteUrl({ + env: {}, + primaryKey: "DOCS_URL", + localDefault: DEFAULT_DEV_URL, + productionDefault: DEFAULT_PROD_URL, + genericKeys: ["SITE_URL"], + providerKeys: { cloudflare: "CF_PAGES_URL", vercel: "VERCEL_URL", netlify: "URL" }, + isProdLike: true, + }); + expect(fallbackProd).toBe(DEFAULT_PROD_URL); + }); + + it("getPortFromUrl_derives_port_from_docsUrl", () => { + // URL with explicit port + const withPort = "http://example.com:8080/path"; + expect(getPortFromUrl(withPort, PORTS.DOCS)).toBe(8080); + + // URL without port -> fallback + const withoutPort = "https://example.com/path"; + expect(getPortFromUrl(withoutPort, PORTS.DOCS)).toBe(PORTS.DOCS); + + // Derived from resolveSiteUrl result + const docsUrl = resolveSiteUrl({ + env: { DOCS_URL: "http://localhost:12345/docs" }, + primaryKey: "DOCS_URL", + localDefault: DEFAULT_DEV_URL, + productionDefault: DEFAULT_PROD_URL, + genericKeys: ["SITE_URL"], + providerKeys: { cloudflare: "CF_PAGES_URL", vercel: "VERCEL_URL", netlify: "URL" }, + isProdLike: false, + }); + expect(getPortFromUrl(docsUrl, PORTS.DOCS)).toBe(12345); + }); +});