Black-box functional and performance tests for Light Portal deployments. The
initial target is portal-config-loc; tests are organized so additional
environments and feature areas can be added without changing the command-line
interface.
- Hurl runs functional HTTP tests and repeated smoke tests.
make mcpruns the non-billable daily MCP protocol suite, also included inmake all. See MCP coverage and setup.make mcp-sourceruns the complementary sibling-repository regression tests.- k6 runs controlled smoke, performance, soak, and later WebSocket workloads.
- Playwright runs the promotion UI canary in a real Chromium browser.
make workflow-process-uistarts test processes and exercises Process Info, Editor execution, and human approval. It is included inmake all.
Live LLM tests call billable providers. The ordinary functional lane sends only
a small number of requests. Performance tests that generate completions require
an explicit ALLOW_BILLABLE_TESTS=true opt-in.
- Podman or Docker; the runners automatically use pinned Hurl and k6 images.
- Alternatively, install Hurl and k6 natively. Native binaries take precedence over containers.
- Bash,
curl, andjq - Node.js 20 or newer for Playwright and workflow fixture preparation
The default container images are:
ghcr.io/orange-opensource/hurl:8.0.1
grafana/k6:2.0.0
Override them with HURL_IMAGE, K6_IMAGE, or select a specific runtime with
CONTAINER_ENGINE=podman|docker. Container execution uses host networking so
https://localhost:8444 addresses the dedicated llm-gateway service in
portal-config-loc on Linux.
The repository includes intentionally public JWT fixtures for the public
development users described in fixtures/tokens/README.md.
The default profile is portal-admin, so the initial LLM tests run without
copying a token into an environment file.
The runners also load ${LIGHT_PORTAL_ENV_FILE} when set, otherwise they load:
~/.config/lightapi/light-portal.env
The environment file is optional. Use it to override non-secret defaults or to select another committed token profile:
cp config/portal-config-loc.env.example ~/.config/lightapi/light-portal.envIf the file already contains provider keys, preserve them. Provider keys are server-side credentials and are not consumed by this test suite.
Required test configuration:
PORTAL_BASE_URL=https://localhost:8444
MCP_BASE_URL=https://localhost
TOKEN_PROFILE=portal-admin
LLM_PUBLIC_ALIAS=assistant-dev
EMBEDDING_QUERY_ALIAS=kb-query
EMBEDDING_INDEX_ALIAS=kb-index
EMBEDDING_SPACE_ID=nvidia-nemotron-3-embed-1b-float-v1
EMBEDDING_SPACE_REVISION=1
EMBEDDING_DIMENSION=2048
TLS_INSECURE=true
WORKFLOW_SMOKE_TOOL=workflow_mcp_smoke
CUSTOMER_360_TOOL=customer_360When PROMOTION_E2E_EMAIL and PROMOTION_E2E_PASSWORD are configured, the
runners authenticate through the Portal UI and use the resulting access token
without writing it to the environment file. A cached browser token is reused
only while it has at least five minutes remaining; otherwise the runner signs
in again. Set PORTAL_AUTO_LOGIN=false to disable this behavior or adjust the
margin with PORTAL_TOKEN_MIN_TTL_SECONDS.
Without UI credentials, an exported PORTAL_ACCESS_TOKEN overrides the
committed profile when a fresh or ad hoc token is useful. Environment variables
override values loaded from the file.
make validate
make smoke
make llm
make workflow-mcp
make workflow-mcp-publication
make api-gateway-publication-ui
make workflow-process-ui
make functionalworkflow-mcp-publication is a destructive, serial local E2E lane for the
dedicated workflow-mcp-e2e-loc Gateway. It normalizes that Gateway to an
unpublished WF0001 baseline, publishes the expected WF0001 Tool catalog, creates a current
snapshot and restarts the Gateway, unpublishes and deploys again, verifies a
repeat removal is a no-op, then republishes and deploys the final state. It
preserves unrelated Gateway Tools and must not target a shared or production
instance. Override the WORKFLOW_MCP_E2E_* values in the private environment
file when the local fixture IDs, URL, or container name differ.
The lane is included in make all as part of the daily suite.
After each restart, the lane waits for both listener health and application
admission. Gateway /health can return 200 before /mcp accepts requests.
A bounded, unauthenticated read-only MCP GET waits through the startup
503 service unavailable response; 401, 404 or 405 confirms admission is open.
It neither executes a Tool nor retries a publication/snapshot command. Other
HTTP failures fail immediately. Run its focused readiness regression with:
node --test --test-isolation=none tests/contracts/gateway-readiness.test.mjsworkflow-process-ui uses WORKFLOW_E2E_EMAIL and
WORKFLOW_E2E_PASSWORD, falling back to the configured PROMOTION_E2E_*
credentials. Set WORKFLOW_UI_BASE_URL to test a different Portal UI, or
WORKFLOW_AUTH_STATE_FILE to use an existing Playwright login state. Its
signed-in user needs access to Workflow Admin processes and permission to start
the fixture workflows. No pre-existing process is required: Process Info starts
a simple-set-assert instance and then checks the default, unfiltered list. The
Editor tests start simple-set-assert and test01. Portal's start command owns
definition/grant synchronization and its retry policy; the tests do not add a
separate synchronization gate. Override WORKFLOW_TEST_HOST_ID, WORKFLOW_SIMPLE_DEF_ID, and
WORKFLOW_APPROVAL_DEF_ID for a different baseline.
For one workflow-backed Tool, scripts/run-workflow-tool-unpublish-e2e.sh
performs the same unpublish, snapshot activation, no-op re-preview, and
republish lifecycle with an explicit protected ACL. It is opt-in and mutates
the named local Gateway instance; set WORKFLOW_TOOL_E2E_HOST_ID,
WORKFLOW_TOOL_E2E_INSTANCE_NAME, WORKFLOW_TOOL_E2E_TOOL_NAME,
WORKFLOW_TOOL_E2E_GATEWAY_URL, WORKFLOW_TOOL_E2E_CONTAINER, and
WORKFLOW_TOOL_E2E_REQUEST_RULE only for an isolated local test target.
Promotion testing combines a read-only Hurl preflight with a real browser journey. The browser promotes one configured Platform, Pipeline, and Product Version in dependency order, waits for each target projection to complete, rechecks the durable projection evidence, and verifies that a repeated execute is an idempotent replay. A separate browser case verifies that Select all N matching records spans server-side pagination instead of selecting only the visible ten-row page.
The suite never creates rows directly in projection tables. Create and update canary records through Portal commands or the corresponding admin pages.
Use a dedicated source/target host pair. The automation user needs portal.w
and host-admin access to both hosts. The source needs:
- at least 11 active records of the configured pagination canary type (Config by default), so server-side selection crosses the default ten-row page;
- one uniquely identifiable Platform;
- one Pipeline whose dependencies exist in the target after Platform promotion; and
- one Product Version whose Platform and Pipeline dependencies exist in the target after the first two promotions.
Add the following values only to the private environment file selected by
LIGHT_PORTAL_ENV_FILE (by default
~/.config/lightapi/light-portal.env):
PROMOTION_UI_BASE_URL=https://localhost:3000
PROMOTION_API_BASE_URL=https://localhost:3000
PROMOTION_SOURCE_HOST_ID=<source-host-uuid>
PROMOTION_SOURCE_HOST_LABEL='source-domain / source-subdomain'
PROMOTION_TARGET_HOST_ID=<target-host-uuid>
PROMOTION_TARGET_HOST_LABEL='target-domain / target-subdomain'
PROMOTION_PLATFORM_MATCH='unique visible platform text'
PROMOTION_PIPELINE_MATCH='pipeline name shown in the Pipeline table'
PROMOTION_PIPELINE_VERSION='pipeline version shown in the Pipeline table'
PROMOTION_PRODUCT_ID='product ID shown in the Product Version table'
PROMOTION_PRODUCT_VERSION='version shown in the Product Version table'
PROMOTION_SELECTION_ENTITY_TYPE=config
PROMOTION_SELECTION_ENTITY_LABEL=Config
PROMOTION_E2E_EMAIL=<dedicated-test-user>
PROMOTION_E2E_PASSWORD=<secret>
PROMOTION_E2E_USER_TYPE=EmployeeThe host labels must exactly match the Portal select options. The Platform match must identify exactly one visible row. Pipeline uses its visible name plus version as a composite identity, and Product Version uses the visible Product ID plus Version. Do not use hidden UUIDs for either entity.
Install the pinned test dependency and browser once:
npm ci
npx playwright install chromiumThe P4-P6 lifecycle assertions call
lightapi.net/user/promotionRecovery/0.1.0. A preserved local database must
contain both the endpoint registration and its portal gateway access-control
rule. Apply events 13 and 14 from event-importer/events/local/README.md,
publish a new current loc portal-gateway snapshot, and restart
light-gateway before running this suite.
Run the lanes with:
make promotion-api
make promotion-ui
make promotion-hourlypromotion-api validates history access and exports the configured Platform,
Pipeline, and Product Version without modifying the target; it requires a
current PORTAL_ACCESS_TOKEN. promotion-ui authenticates with the configured
browser user and performs the browser journey. promotion-hourly always runs
the browser lane and, in the default PROMOTION_API_PREFLIGHT=auto mode, runs
Hurl only when an explicit access token is configured. Use required to make
Hurl mandatory or skip to disable it. Reports are written below a timestamped
reports/runs/ directory.
The scheduled canary does not clean the target host before or after promotion.
It executes with orphanAction: keep, so target-only records are preserved.
On subsequent runs, an unchanged source/target entity is planned as NOOP and
no new event is appended; changed mutable fields are promoted as updates. The
suite also replays the same completed plan once to verify its idempotent result.
For a local interactive login, omit PROMOTION_REUSE_AUTH_STATE and provide
the test email/password. For an unattended runner, either keep those secrets
in its secret store or mount a short-lived Playwright state file and set:
PROMOTION_AUTH_STATE_FILE=/run/secrets/portal-playwright-state.json
PROMOTION_REUSE_AUTH_STATE=trueThe authentication state contains reusable cookies. Never commit it or publish it as a test artifact. Playwright tracing is disabled because traces can also capture authenticated cookies and request headers.
The asynchronous runner lets light-workflow trigger the suite without
waiting for the complete browser run:
PROMOTION_RUNNER_BEARER_TOKEN=<secret> make runnerIts API is:
GET /healthz
POST /test-runs
GET /test-runs/{runId}
Example trigger:
curl --fail-with-body \
-H "Authorization: Bearer $PROMOTION_RUNNER_BEARER_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: promotion-hourly-2026-08-28T20' \
--data '{"suite":"promotion-hourly","correlationId":"promotion-hourly-2026-08-28T20"}' \
http://127.0.0.1:8090/test-runsThe call returns HTTP 202 with a runId. Only one promotion run is admitted at
a time so overlapping schedules cannot mutate the canary target concurrently.
Repeated calls with the same idempotency key return the original run.
For deployment, build Dockerfile.playwright and mount the private environment
file or inject its values from the platform secret store. A non-loopback runner
refuses to start unless a bearer token is configured or
PROMOTION_RUNNER_TRUST_WORKFLOW_HEADERS=true is explicitly selected. The
latter mode requires both workflow Authorization headers and must only be used
on a network where direct access is restricted to light-workflow.
The workflow example is
examples/workflows/promotion-hourly.yaml.
Register its promotion-test-runner endpoint target with POST permission, then
have Portal Scheduler invoke the workflow with a unique correlationId hourly
or daily. The workflow receives HTTP 202 immediately; test completion and
failure alerting are owned by the runner/monitoring system.
The workflow-mcp lane verifies discovery and invocation of the
workflow_mcp_smoke and customer_360 tools. It also verifies that the smoke
tool rejects unexpected input properties. The default MCP endpoint is
https://localhost/mcp; override MCP_BASE_URL when the development
installation publishes light-gateway at another origin.
After a fresh database initialization and baseline event import, the workflow
fixture hook at tests/workflow-mcp/prepare.sh prepares the selected test tools.
It reads Portal's current pinned binding and compares its source binding ID,
workflow definition ID, version, and definition digest with the approved runtime
revision. Missing or stale bindings are synchronized and published through
Portal's publishWorkflowToolBindings command, then checked again through MCP.
Healthy reruns only read state. WORKFLOW_SMOKE_TOOL and CUSTOMER_360_TOOL
override the fixture names.
Run npm ci before this lane. Setup uses the public Gateway at MCP_BASE_URL
(default https://localhost) for Portal APIs as well as runtime binding checks.
The shared Hurl runner's PORTAL_BASE_URL defaults to the separate LLM Gateway
at :8444, which does not serve Portal queries. If the Portal APIs live elsewhere,
set WORKFLOW_SETUP_BASE_URL explicitly to the Portal backend associated with the
tested Gateway. This override never changes the MCP endpoint.
For ui-login, Portal requests reuse the working browser session and CSRF cookie,
plus the current user bearer token. Setup requires the Portal API and login to
share a hostname; it never silently redirects to PROMOTION_UI_BASE_URL. Other
authentication modes use the configured bearer token. MCP checks always use a
separate bearer-only request context at exactly Hurl's MCP_BASE_URL, so a UI's
Gateway or browser cookies cannot make the wrong runtime appear ready.
The baseline Portal events (including pinned published versions, bindings, and grants) and Gateway tool/ACL configuration must already be imported. Setup does not create missing catalog records, deploy Gateway configuration, approve pending bindings, or reactivate retired bindings. Pending/unconfirmed publication fails with its operation ID when available; reconcile that operation in Portal before retrying. If only the runtime database was reset while Portal retained sync acknowledgements/publication receipts, unresolved runtime absence remains a failure rather than being reported as ready.
The generic Hurl runner discovers optional prepare.sh hooks next to selected
Hurl files; it has no workflow fixture table or unconditional Node dependency.
It resolves checkout and input paths physically, so symlinked workspaces work.
Report paths and the Hurl/container backend are validated before preparation.
Unrelated files run even when a directory's hook fails. A failed hook skips only
that directory's files, returns a failing exit status, and records prepare-N.log
and a failed JUnit testcase in prepare-N.xml under REPORT_DIR. Hurl results
remain in junit.xml; consumers should collect both that file and prepare-*.xml.
Test the published NVIDIA embedding query and indexing Aliases through the gateway with two bounded requests:
make embeddings ALLOW_BILLABLE_TESTS=trueThis lane checks kb-query and kb-index independently, including the
declared embedding-space response headers and the 2048-value vector dimension.
It calls the gateway only; NVIDIA_API_KEY remains a server-side gateway
secret and is never read by this repository. Override any Alias or contract
value with the corresponding EMBEDDING_* variable when testing another
publication. If the embedding Aliases are bound to dedicated principals, set
EMBEDDING_QUERY_ACCESS_TOKEN and EMBEDDING_INDEX_ACCESS_TOKEN to gateway
caller tokens for knowledge-service and knowledge-indexer, respectively.
The tokens are passed to Hurl as secret variables and never as command-line
arguments. Each falls back to the ordinary Portal test token when omitted.
Run validation, functional tests, a short repeated run, and the non-billable models performance test in one batch:
make batchThe batch deliberately skips the billable LLM performance workload unless it is explicitly enabled. The normal functional and repeated chat cases still call the configured provider; the default batch performs one functional pass followed by three repeated passes.
make batch ALLOW_BILLABLE_TESTS=true VUS=1 LLM_ITERATIONS=10Run every test lane, including the billable LLM performance and embedding tests, with one command:
make allThis also runs the one-endpoint API Gateway publication lifecycle against
portal-bff-loc: create/reactivate, configure ACL, publish, retire, and delete
the test version and API, twice. See the lane documentation
for credentials, environment overrides, and projection assertions.
Calling make all is the explicit billable-test opt-in; no additional variable
or command-line switch is required. An explicit opt-out is honored and skips
the gated LLM performance and embedding lanes:
make all ALLOW_BILLABLE_TESTS=falseSelect a different committed identity with:
make functional TOKEN_PROFILE=another-profileOverride the configured alias to exercise another published model:
make llm LLM_PUBLIC_ALIAS=another-public-aliasRepeat the smoke and LLM cases 20 times with a ten-second pause:
make repeat COUNT=20 INTERVAL=10Each attempt gets its own Hurl JSON and JUnit report. At completion, the runner
writes summary.json with pass/fail counts plus overall and per-test p50, p95,
and maximum latency. Configure the performance signal with milliseconds:
make repeat COUNT=20 REPEAT_WARN_P95_MS=10000 REPEAT_FAIL_P95_MS=30000Crossing the warning threshold prints WARNING but exits successfully.
Crossing the failure threshold, or any failed functional attempt, exits
nonzero so a CI job can send its normal failure notification. This repository
does not directly send email or chat notifications.
Run continuously until the first failure:
make repeat COUNT=0 INTERVAL=30Use STOP_ON_FAILURE=false to collect all requested attempts even after an
individual failure.
Run a non-billable /v1/models performance smoke test:
make perf-smoke VUS=2 DURATION=30sRun a deliberately small live-provider completion workload. The default uses
one VU to execute exactly ten requests; increasing VUS changes concurrency,
not the total request count:
make perf-live ALLOW_BILLABLE_TESTS=true VUS=1 LLM_ITERATIONS=10Reports are written below timestamped directories in reports/runs/ and are
ignored by Git. Direct make smoke, make llm, and performance commands keep
their reports in the corresponding top-level reports/ directory.
Put HTTP behavior tests in tests/<feature>/*.hurl. Keep each file independent
unless a sequence is the behavior under test. Use {{base_url}},
{{access_token}}, and feature-specific variables rather than hard-coded
credentials or deployment IDs.
Put load tests in performance/. Every performance script must define explicit
thresholds and safe local defaults. Tests that can incur provider cost must be
invoked through the billable-test guard in scripts/run-performance.sh.
Every test should:
- use a unique correlation ID;
- have bounded timeouts;
- fail with a nonzero exit code;
- avoid logging credentials or complete provider responses;
- isolate and clean up data it creates;
- keep destructive and billable behavior opt-in.
GitHub-hosted runners cannot reach a developer's portal-config-loc instance.
The initial workflow validates repository structure and shell/JavaScript syntax
only. Runtime tests should run locally or on a self-hosted runner until the
complete Portal stack can be started ephemerally in CI.
npm run test:chat exercises the real /app/genai/chat UI: Portal login,
Tech Support Agent selection, WebSocket session admission, message submission,
turn acceptance, an LLM reply containing a unique per-run marker, and rendering
of that reply. It fails on Agent error frames (including session-limit errors
and gateway 403), rather than counting a successful socket upgrade as success.
No backend or model is mocked. Each run can incur one model request; retries
are disabled. This is a functional smoke test, not a concurrency/load test.
npm ci
npx playwright install chromium
export CHAT_E2E_EMAIL='steve.hu@lightapi.net'
read -rsp 'Portal password: ' CHAT_E2E_PASSWORD; echo
export CHAT_E2E_PASSWORD
npm run test:chatDefaults target portal-config-loc/all-in-lt: UI https://localhost:3000,
agent option Tech Support Agent Dev · dev. Override with CHAT_UI_BASE_URL
and CHAT_AGENT_LABEL (exact visible option label). Alternatively set
CHAT_AUTH_STATE_FILE to an existing, unexpired Playwright Portal storage-state
file; an API bearer token alone is not a browser login. Credentials are never
checked into the test. Local self-signed browser certificates are accepted by
default; use TLS_INSECURE=false with trusted certificates.
The suite starts a fresh Agent session on each run and disconnects in finally.
Disconnect does not end the durable server session; it expires under the Agent's
normal policy. This keeps policy replacements and Agent restarts from making a
saved session ID invalidate later smoke runs. The test does not modify policy
or delete database rows.
List without contacting the deployment: npm run test:chat:list.
JUnit and failure artifacts are under reports/genai-chat/. Traces and videos
are disabled to avoid recording authentication handshakes. A pass requires a
working gateway binding, model route, provider credential, and provider capacity;
publication or connectivity failures deliberately fail this test.
Run the Chat UI suite through Make, like make promotion-ui:
make genai-chat-uiThe Make wrapper loads LIGHT_PORTAL_ENV_FILE (default:
~/.config/lightapi/light-portal.env). Set CHAT_E2E_EMAIL and
CHAT_E2E_PASSWORD there, or supply CHAT_AUTH_STATE_FILE. For convenience it
also accepts the existing PROMOTION_E2E_EMAIL / PROMOTION_E2E_PASSWORD
credentials and PROMOTION_UI_BASE_URL when the corresponding Chat settings
are absent. Explicit Chat environment overrides take precedence.
make all now runs this suite after the existing batch and embedding suites.
Failures propagate to Make. As with other optional billable checks,
make all ALLOW_BILLABLE_TESTS=false skips the Chat UI suite; the explicit
make genai-chat-ui target runs it directly. Earlier suite failures stop
make all before Chat is reached. Reports remain in reports/genai-chat/.
make claude-personal-e2e runs the six-turn native Claude coding/review session
suite against portal-config-loc/all-in-lt, with readiness checks, overlap
protection, a timeout, and timestamped JSON/JUnit reports. It consumes native
subscription usage and is an explicit standalone target. See
setup, configuration, coverage and daily scheduling.