mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
Merge branches 'main' and 'main' of https://github.com/openclaw/openclaw
* 'main' of https://github.com/openclaw/openclaw: docs: explain telegram package artifact testing ci: let telegram e2e use package artifacts docs: explain release validation entrypoints ci: tolerate legacy qa inventory entries ci(testbox): save build artifact cache before wait fix: allow heavyweight docker lanes at low parallelism test(docker): use packaged gateway expect-final smoke test(live): accept current Codex status text * 'main' of https://github.com/openclaw/openclaw: docs: explain telegram package artifact testing ci: let telegram e2e use package artifacts docs: explain release validation entrypoints ci: tolerate legacy qa inventory entries ci(testbox): save build artifact cache before wait fix: allow heavyweight docker lanes at low parallelism test(docker): use packaged gateway expect-final smoke test(live): accept current Codex status text
This commit is contained in:
@@ -311,9 +311,12 @@ gh workflow run package-acceptance.yml --ref main \
|
||||
-f telegram_mode=none
|
||||
```
|
||||
|
||||
Use `telegram_mode=mock-openai` or `telegram_mode=live-frontier` only with
|
||||
`source=npm`; that path reuses the published npm Telegram E2E workflow and the
|
||||
`qa-live-shared` environment.
|
||||
Use `telegram_mode=mock-openai` or `telegram_mode=live-frontier` when the same
|
||||
resolved `package-under-test` tarball should also run through the Telegram QA
|
||||
workflow in the `qa-live-shared` environment. The standalone Telegram workflow
|
||||
still accepts a published npm spec for post-publish checks, but Package
|
||||
Acceptance passes the resolved artifact for `source=npm`, `ref`, `url`, and
|
||||
`artifact`.
|
||||
|
||||
Docker E2E images never copy repo sources as the app under test: the bare image
|
||||
is a Node/Git runner, and the functional image installs the same prebuilt npm
|
||||
|
||||
60
.github/workflows/npm-telegram-beta-e2e.yml
vendored
60
.github/workflows/npm-telegram-beta-e2e.yml
vendored
@@ -4,10 +4,20 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package_spec:
|
||||
description: Published OpenClaw package spec to test
|
||||
description: Published OpenClaw package spec to test when no artifact is supplied
|
||||
required: true
|
||||
default: openclaw@beta
|
||||
type: string
|
||||
package_label:
|
||||
description: Optional display label for an artifact-backed package candidate
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
package_artifact_name:
|
||||
description: Advanced package-under-test artifact name; leave blank for registry install
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
provider_mode:
|
||||
description: QA provider mode
|
||||
required: true
|
||||
@@ -23,9 +33,19 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
package_spec:
|
||||
description: Published OpenClaw package spec to test
|
||||
description: Published OpenClaw package spec to test when no artifact is supplied
|
||||
required: true
|
||||
type: string
|
||||
package_artifact_name:
|
||||
description: Optional package-under-test artifact from the current workflow run
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
package_label:
|
||||
description: Optional display label for an artifact-backed package candidate
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
provider_mode:
|
||||
description: QA provider mode
|
||||
required: false
|
||||
@@ -58,7 +78,7 @@ env:
|
||||
|
||||
jobs:
|
||||
run_npm_telegram_beta_e2e:
|
||||
name: Run published npm Telegram E2E
|
||||
name: Run package Telegram E2E
|
||||
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
environment: qa-live-shared
|
||||
@@ -101,6 +121,7 @@ jobs:
|
||||
- name: Validate inputs and secrets
|
||||
env:
|
||||
PACKAGE_SPEC: ${{ inputs.package_spec }}
|
||||
PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }}
|
||||
PROVIDER_MODE: ${{ inputs.provider_mode }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
@@ -109,9 +130,11 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then
|
||||
echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2
|
||||
exit 1
|
||||
if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then
|
||||
if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then
|
||||
echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
case "${PROVIDER_MODE}" in
|
||||
mock-openai | live-frontier) ;;
|
||||
@@ -135,7 +158,14 @@ jobs:
|
||||
require_var OPENAI_API_KEY
|
||||
fi
|
||||
|
||||
- name: Run npm Telegram beta E2E
|
||||
- name: Download package-under-test artifact
|
||||
if: inputs.package_artifact_name != ''
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ inputs.package_artifact_name }}
|
||||
path: .artifacts/telegram-package-under-test
|
||||
|
||||
- name: Run package Telegram E2E
|
||||
id: run_lane
|
||||
shell: bash
|
||||
env:
|
||||
@@ -143,6 +173,7 @@ jobs:
|
||||
OPENCLAW_SKIP_DOCKER_BUILD: "1"
|
||||
OPENCLAW_DOCKER_E2E_IMAGE: openclaw-docker-e2e:local
|
||||
OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.package_spec }}
|
||||
OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL: ${{ inputs.package_label }}
|
||||
OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.provider_mode }}
|
||||
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE: convex
|
||||
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE: ci
|
||||
@@ -151,6 +182,7 @@ jobs:
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
INPUT_SCENARIO: ${{ inputs.scenario }}
|
||||
PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -158,6 +190,20 @@ jobs:
|
||||
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
|
||||
export OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="${output_dir}"
|
||||
|
||||
if [[ -n "${PACKAGE_ARTIFACT_NAME// }" ]]; then
|
||||
mapfile -t package_tgzs < <(find .artifacts/telegram-package-under-test -type f -name "*.tgz" | sort)
|
||||
if [[ "${#package_tgzs[@]}" -ne 1 ]]; then
|
||||
echo "package artifact ${PACKAGE_ARTIFACT_NAME} must contain exactly one .tgz; found ${#package_tgzs[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgzs[0]}"
|
||||
if [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then
|
||||
export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$(basename "${package_tgzs[0]}")"
|
||||
fi
|
||||
elif [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then
|
||||
export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC}"
|
||||
fi
|
||||
|
||||
if [[ -n "${INPUT_SCENARIO// }" ]]; then
|
||||
export OPENCLAW_NPM_TELEGRAM_SCENARIOS="${INPUT_SCENARIO}"
|
||||
fi
|
||||
|
||||
12
.github/workflows/package-acceptance.yml
vendored
12
.github/workflows/package-acceptance.yml
vendored
@@ -65,7 +65,7 @@ on:
|
||||
default: ""
|
||||
type: string
|
||||
telegram_mode:
|
||||
description: Optional published-npm Telegram QA lane
|
||||
description: Optional Telegram QA lane for the resolved package candidate
|
||||
required: true
|
||||
default: none
|
||||
type: choice
|
||||
@@ -125,7 +125,7 @@ on:
|
||||
default: ""
|
||||
type: string
|
||||
telegram_mode:
|
||||
description: Optional published-npm Telegram QA lane
|
||||
description: Optional Telegram QA lane for the resolved package candidate
|
||||
required: false
|
||||
default: none
|
||||
type: string
|
||||
@@ -366,10 +366,6 @@ jobs:
|
||||
|
||||
telegram_enabled=false
|
||||
if [[ "$TELEGRAM_MODE" != "none" ]]; then
|
||||
if [[ "$SOURCE" != "npm" ]]; then
|
||||
echo "telegram_mode requires source=npm because the Telegram workflow installs a published package spec." >&2
|
||||
exit 1
|
||||
fi
|
||||
telegram_enabled=true
|
||||
fi
|
||||
|
||||
@@ -476,12 +472,14 @@ jobs:
|
||||
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
|
||||
|
||||
npm_telegram:
|
||||
name: Published npm Telegram acceptance
|
||||
name: Telegram package acceptance
|
||||
needs: resolve_package
|
||||
if: needs.resolve_package.outputs.telegram_enabled == 'true'
|
||||
uses: ./.github/workflows/npm-telegram-beta-e2e.yml
|
||||
with:
|
||||
package_spec: ${{ inputs.package_spec }}
|
||||
package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}
|
||||
package_label: openclaw@${{ needs.resolve_package.outputs.package_version }}
|
||||
provider_mode: ${{ needs.resolve_package.outputs.telegram_mode }}
|
||||
secrets:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
115
docs/ci.md
115
docs/ci.md
File diff suppressed because one or more lines are too long
@@ -136,10 +136,13 @@ runs the same lanes before release approval.
|
||||
then seeds an affected broken session JSONL and verifies
|
||||
`openclaw doctor --fix` rewrites it to the active branch with a backup.
|
||||
- `pnpm test:docker:npm-telegram-live`
|
||||
- Installs a published OpenClaw package in Docker, runs installed-package
|
||||
- Installs an OpenClaw package candidate in Docker, runs installed-package
|
||||
onboarding, configures Telegram through the installed CLI, then reuses the
|
||||
live Telegram QA lane with that installed package as the SUT Gateway.
|
||||
- Defaults to `OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC=openclaw@beta`.
|
||||
- Defaults to `OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC=openclaw@beta`; set
|
||||
`OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ=/path/to/openclaw-current.tgz` or
|
||||
`OPENCLAW_CURRENT_PACKAGE_TGZ` to test a resolved local tarball instead of
|
||||
installing from the registry.
|
||||
- Uses the same Telegram env credentials or Convex credential source as
|
||||
`pnpm openclaw qa telegram`. For CI/release automation, set
|
||||
`OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE=convex` plus
|
||||
@@ -156,8 +159,8 @@ runs the same lanes before release approval.
|
||||
HTTPS tarball URL plus SHA-256, or tarball artifact from another run, uploads
|
||||
the normalized `openclaw-current.tgz` as `package-under-test`, then runs the
|
||||
existing Docker E2E scheduler with smoke, package, product, full, or custom
|
||||
lane profiles. Published npm candidates can additionally run the Telegram QA
|
||||
workflow.
|
||||
lane profiles. Set `telegram_mode=mock-openai` or `live-frontier` to run the
|
||||
Telegram QA workflow against the same `package-under-test` artifact.
|
||||
- Latest beta product proof:
|
||||
|
||||
```bash
|
||||
@@ -643,7 +646,7 @@ These Docker runners split into two buckets:
|
||||
`OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=45000`, and
|
||||
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you
|
||||
explicitly want the larger exhaustive scan.
|
||||
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, packs OpenClaw once as an npm tarball through `scripts/package-openclaw-for-docker.mjs`, then builds/reuses two `scripts/e2e/Dockerfile` images. The bare image is only the Node/Git runner for install/update/plugin-dependency lanes; those lanes mount the prebuilt tarball. The functional image installs the same tarball into `/app` for built-app functionality lanes. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker, or `node scripts/test-docker-all.mjs --plan-json` to print the CI plan for selected lanes, package/image needs, and credentials.
|
||||
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, packs OpenClaw once as an npm tarball through `scripts/package-openclaw-for-docker.mjs`, then builds/reuses two `scripts/e2e/Dockerfile` images. The bare image is only the Node/Git runner for install/update/plugin-dependency lanes; those lanes mount the prebuilt tarball. The functional image installs the same tarball into `/app` for built-app functionality lanes. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. If a single lane is heavier than the active caps, the scheduler can still start it when the pool is empty and then keeps it running alone until capacity is available again. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker, or `node scripts/test-docker-all.mjs --plan-json` to print the CI plan for selected lanes, package/image needs, and credentials.
|
||||
- `Package Acceptance` is the GitHub-native package gate for "does this installable tarball work as a product?" It resolves one candidate package from `source=npm`, `source=ref`, `source=url`, or `source=artifact`, uploads it as `package-under-test`, then runs the reusable Docker E2E lanes against that exact tarball instead of repacking the selected ref. `workflow_ref` selects the trusted workflow/harness scripts, while `package_ref` selects the source commit/branch/tag to pack when `source=ref`; this lets current acceptance logic validate older trusted commits. Profiles are ordered by breadth: `smoke` is quick install/channel/agent plus gateway/config, `package` is the package/update/plugin contract and the default native replacement for most Parallels package/update coverage, `product` adds MCP channels, cron/subagent cleanup, OpenAI web search, and OpenWebUI, and `full` runs the release-path Docker chunks with OpenWebUI. Release validation runs the `package` profile for the target ref.
|
||||
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:update-channel-switch`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
|
||||
|
||||
|
||||
@@ -66,9 +66,9 @@ the maintainer-only release runbook.
|
||||
6. Run `OpenClaw NPM Release` with `preflight_only=true`. Before a tag exists,
|
||||
a full 40-character release-branch SHA is allowed for validation-only
|
||||
preflight. Save the successful `preflight_run_id`.
|
||||
7. Run `Full Release Validation` for the release branch, tag, or full commit
|
||||
SHA. This is the umbrella run for the four big release test boxes: Vitest,
|
||||
Docker, QA Lab, and Package.
|
||||
7. Kick off all pre-release tests with `Full Release Validation` for the
|
||||
release branch, tag, or full commit SHA. This is the one manual entrypoint
|
||||
for the four big release test boxes: Vitest, Docker, QA Lab, and Package.
|
||||
8. If validation fails, fix on the release branch and rerun the smallest failed
|
||||
file, lane, workflow job, package profile, provider, or model allowlist that
|
||||
proves the fix. Rerun the full umbrella only when the changed surface makes
|
||||
@@ -96,15 +96,14 @@ the maintainer-only release runbook.
|
||||
- Run `pnpm build && pnpm ui:build` before `pnpm release:check` so the expected
|
||||
`dist/*` release artifacts and Control UI bundle exist for the pack
|
||||
validation step
|
||||
- Run the manual `Full Release Validation` workflow before release approval
|
||||
when you need the whole release validation suite from one entrypoint. It
|
||||
accepts a branch, tag, or full commit SHA, dispatches manual `CI`, and
|
||||
dispatches `OpenClaw Release Checks` for install smoke, package acceptance,
|
||||
Docker release-path suites, live/E2E, OpenWebUI, QA Lab parity, Matrix, and
|
||||
Telegram lanes.
|
||||
Provide `npm_telegram_package_spec` only after a package has been published
|
||||
and the post-publish Telegram E2E should run too.
|
||||
Example: `gh workflow run full-release-validation.yml --ref main -f ref=release/YYYY.M.D`
|
||||
- Run the manual `Full Release Validation` workflow before release approval to
|
||||
kick off all pre-release test boxes from one entrypoint. It accepts a branch,
|
||||
tag, or full commit SHA, dispatches manual `CI`, and dispatches
|
||||
`OpenClaw Release Checks` for install smoke, package acceptance, Docker
|
||||
release-path suites, live/E2E, OpenWebUI, QA Lab parity, Matrix, and Telegram
|
||||
lanes. Provide `npm_telegram_package_spec` only after a package has been
|
||||
published and the post-publish Telegram E2E should run too. Example:
|
||||
`gh workflow run full-release-validation.yml --ref main -f ref=release/YYYY.M.D`
|
||||
- Run the manual `Package Acceptance` workflow when you want side-channel proof
|
||||
for a package candidate while release work continues. Use `source=npm` for
|
||||
`openclaw@beta`, `openclaw@latest`, or an exact release version; `source=ref`
|
||||
@@ -113,7 +112,7 @@ the maintainer-only release runbook.
|
||||
SHA-256; or `source=artifact` for a tarball uploaded by another GitHub
|
||||
Actions run. The workflow resolves the candidate to
|
||||
`package-under-test`, reuses the Docker E2E release scheduler against that
|
||||
tarball, and can optionally run published-npm Telegram QA.
|
||||
tarball, and can optionally run Telegram QA against the same tarball.
|
||||
Example: `gh workflow run package-acceptance.yml --ref main -f workflow_ref=main -f source=npm -f package_spec=openclaw@beta -f suite_profile=product`
|
||||
Common profiles:
|
||||
- `smoke`: install/channel/agent, gateway network, and config reload lanes
|
||||
@@ -221,8 +220,9 @@ Validation` or from the `main`/release workflow ref so workflow logic and
|
||||
|
||||
## Release test boxes
|
||||
|
||||
`Full Release Validation` is the manual umbrella that operators use when they
|
||||
want all release validation from one entrypoint:
|
||||
`Full Release Validation` is how operators kick off all pre-release tests from
|
||||
one entrypoint. Run it from the trusted `main` workflow ref and pass the release
|
||||
branch, tag, or full commit SHA as `ref`:
|
||||
|
||||
```bash
|
||||
gh workflow run full-release-validation.yml \
|
||||
@@ -236,9 +236,48 @@ gh workflow run full-release-validation.yml \
|
||||
The workflow resolves the target ref, dispatches manual `CI` with
|
||||
`target_ref=<release-ref>`, dispatches `OpenClaw Release Checks`, and
|
||||
optionally dispatches post-publish Telegram E2E when
|
||||
`npm_telegram_package_spec` is set. A full run is only acceptable when both
|
||||
child workflows succeed or an intentionally skipped optional child is recorded
|
||||
in the summary.
|
||||
`npm_telegram_package_spec` is set. `OpenClaw Release Checks` then fans out
|
||||
install smoke, cross-OS release checks, live/E2E Docker release-path coverage,
|
||||
Package Acceptance, QA Lab parity, live Matrix, and live Telegram. A full run is
|
||||
only acceptable when the `Full Release Validation` summary shows `normal_ci` and
|
||||
`release_checks` as successful, and any optional `npm_telegram` child is either
|
||||
successful or intentionally skipped.
|
||||
|
||||
Use these variants depending on release stage:
|
||||
|
||||
```bash
|
||||
# Validate an unpublished release candidate branch.
|
||||
gh workflow run full-release-validation.yml \
|
||||
--ref main \
|
||||
-f ref=release/YYYY.M.D \
|
||||
-f workflow_ref=main \
|
||||
-f provider=openai \
|
||||
-f mode=both
|
||||
|
||||
# Validate an exact pushed commit.
|
||||
gh workflow run full-release-validation.yml \
|
||||
--ref main \
|
||||
-f ref=<40-char-sha> \
|
||||
-f workflow_ref=main \
|
||||
-f provider=openai \
|
||||
-f mode=both
|
||||
|
||||
# After publishing a beta, add published-package Telegram E2E.
|
||||
gh workflow run full-release-validation.yml \
|
||||
--ref main \
|
||||
-f ref=release/YYYY.M.D \
|
||||
-f workflow_ref=main \
|
||||
-f provider=openai \
|
||||
-f mode=both \
|
||||
-f npm_telegram_package_spec=openclaw@YYYY.M.D-beta.N \
|
||||
-f npm_telegram_provider_mode=mock-openai
|
||||
```
|
||||
|
||||
Do not use the full umbrella as the first rerun after a focused fix. If one box
|
||||
fails, use the failed child workflow, job, Docker lane, package profile, model
|
||||
provider, or QA lane for the next proof. Run the full umbrella again only when
|
||||
the fix changed shared release orchestration or made earlier all-box evidence
|
||||
stale.
|
||||
|
||||
### Vitest
|
||||
|
||||
@@ -354,10 +393,10 @@ Common package profiles:
|
||||
- `full`: Docker release-path chunks with OpenWebUI
|
||||
- `custom`: exact `docker_lanes` list for focused reruns
|
||||
|
||||
For post-publish beta proof, use `source=npm` with the exact beta package or
|
||||
`openclaw@beta`. Enable `telegram_mode=mock-openai` or
|
||||
`telegram_mode=live-frontier` only for published npm packages, because that
|
||||
path reuses the published-npm Telegram E2E workflow.
|
||||
For package-candidate Telegram proof, enable `telegram_mode=mock-openai` or
|
||||
`telegram_mode=live-frontier` on Package Acceptance. The workflow passes the
|
||||
resolved `package-under-test` tarball into the Telegram lane; the standalone
|
||||
Telegram workflow still accepts a published npm spec for post-publish checks.
|
||||
|
||||
## NPM workflow inputs
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ title: "Tests"
|
||||
- Gateway integration: opt-in via `OPENCLAW_TEST_INCLUDE_GATEWAY=1 pnpm test` or `pnpm test:gateway`.
|
||||
- `pnpm test:e2e`: Runs gateway end-to-end smoke tests (multi-instance WS/HTTP/node pairing). Defaults to `threads` + `isolate: false` with adaptive workers in `vitest.e2e.config.ts`; tune with `OPENCLAW_E2E_WORKERS=<n>` and set `OPENCLAW_E2E_VERBOSE=1` for verbose logs.
|
||||
- `pnpm test:live`: Runs provider live tests (minimax/zai). Requires API keys and `LIVE=1` (or provider-specific `*_LIVE_TEST=1`) to unskip.
|
||||
- `pnpm test:docker:all`: Builds the shared live-test image, packs OpenClaw once as an npm tarball, builds/reuses a bare Node/Git runner image plus a functional image that installs that tarball into `/app`, then runs Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. The bare image (`OPENCLAW_DOCKER_E2E_BARE_IMAGE`) is used for installer/update/plugin-dependency lanes; those lanes mount the prebuilt tarball instead of using copied repo sources. The functional image (`OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`) is used for normal built-app functionality lanes. `scripts/package-openclaw-for-docker.mjs` is the single local/CI package packer and validates the tarball plus `dist/postinstall-inventory.json` before Docker consumes it. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. `node scripts/test-docker-all.mjs --plan-json` emits the scheduler-owned CI plan for selected lanes, image kinds, package/live-image needs, and credential checks without building or running Docker. `OPENCLAW_DOCKER_ALL_PARALLELISM=<n>` controls process slots and defaults to 10; `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM=<n>` controls the provider-sensitive tail pool and defaults to 10. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; provider caps default to one heavy lane per provider via `OPENCLAW_DOCKER_ALL_LIVE_CLAUDE_LIMIT=4`, `OPENCLAW_DOCKER_ALL_LIVE_CODEX_LIMIT=4`, and `OPENCLAW_DOCKER_ALL_LIVE_GEMINI_LIMIT=4`. Use `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` for larger hosts. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=<ms>`. The runner preflights Docker by default, cleans stale OpenClaw E2E containers, emits active-lane status every 30 seconds, shares provider CLI tool caches between compatible lanes, retries transient live-provider failures once by default (`OPENCLAW_DOCKER_ALL_LIVE_RETRIES=<n>`), and stores lane timings in `.artifacts/docker-tests/lane-timings.json` for longest-first ordering on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the lane manifest without running Docker, `OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS=<ms>` to tune status output, or `OPENCLAW_DOCKER_ALL_TIMINGS=0` to disable timing reuse. Use `OPENCLAW_DOCKER_ALL_LIVE_MODE=skip` for deterministic/local lanes only or `OPENCLAW_DOCKER_ALL_LIVE_MODE=only` for live-provider lanes only; package aliases are `pnpm test:docker:local:all` and `pnpm test:docker:live:all`. Live-only mode merges main and tail live lanes into one longest-first pool so provider buckets can pack Claude, Codex, and Gemini work together. The runner stops scheduling new pooled lanes after the first failure unless `OPENCLAW_DOCKER_ALL_FAIL_FAST=0` is set, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. CLI backend Docker setup commands have their own timeout via `OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS` (default 180). Per-lane logs, `summary.json`, `failures.json`, and phase timings are written under `.artifacts/docker-tests/<run-id>/`; use `pnpm test:docker:timings <summary.json>` to inspect slow lanes and `pnpm test:docker:rerun <run-id|summary.json|failures.json>` to print cheap targeted rerun commands.
|
||||
- `pnpm test:docker:all`: Builds the shared live-test image, packs OpenClaw once as an npm tarball, builds/reuses a bare Node/Git runner image plus a functional image that installs that tarball into `/app`, then runs Docker smoke lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1` through a weighted scheduler. The bare image (`OPENCLAW_DOCKER_E2E_BARE_IMAGE`) is used for installer/update/plugin-dependency lanes; those lanes mount the prebuilt tarball instead of using copied repo sources. The functional image (`OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`) is used for normal built-app functionality lanes. `scripts/package-openclaw-for-docker.mjs` is the single local/CI package packer and validates the tarball plus `dist/postinstall-inventory.json` before Docker consumes it. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`; planner logic lives in `scripts/lib/docker-e2e-plan.mjs`; `scripts/test-docker-all.mjs` executes the selected plan. `node scripts/test-docker-all.mjs --plan-json` emits the scheduler-owned CI plan for selected lanes, image kinds, package/live-image needs, and credential checks without building or running Docker. `OPENCLAW_DOCKER_ALL_PARALLELISM=<n>` controls process slots and defaults to 10; `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM=<n>` controls the provider-sensitive tail pool and defaults to 10. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; provider caps default to one heavy lane per provider via `OPENCLAW_DOCKER_ALL_LIVE_CLAUDE_LIMIT=4`, `OPENCLAW_DOCKER_ALL_LIVE_CODEX_LIMIT=4`, and `OPENCLAW_DOCKER_ALL_LIVE_GEMINI_LIMIT=4`. Use `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` for larger hosts. If one lane exceeds the effective weight or resource cap on a low-parallelism host, it can still start from an empty pool and will run alone until it releases capacity. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=<ms>`. The runner preflights Docker by default, cleans stale OpenClaw E2E containers, emits active-lane status every 30 seconds, shares provider CLI tool caches between compatible lanes, retries transient live-provider failures once by default (`OPENCLAW_DOCKER_ALL_LIVE_RETRIES=<n>`), and stores lane timings in `.artifacts/docker-tests/lane-timings.json` for longest-first ordering on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the lane manifest without running Docker, `OPENCLAW_DOCKER_ALL_STATUS_INTERVAL_MS=<ms>` to tune status output, or `OPENCLAW_DOCKER_ALL_TIMINGS=0` to disable timing reuse. Use `OPENCLAW_DOCKER_ALL_LIVE_MODE=skip` for deterministic/local lanes only or `OPENCLAW_DOCKER_ALL_LIVE_MODE=only` for live-provider lanes only; package aliases are `pnpm test:docker:local:all` and `pnpm test:docker:live:all`. Live-only mode merges main and tail live lanes into one longest-first pool so provider buckets can pack Claude, Codex, and Gemini work together. The runner stops scheduling new pooled lanes after the first failure unless `OPENCLAW_DOCKER_ALL_FAIL_FAST=0` is set, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. CLI backend Docker setup commands have their own timeout via `OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS` (default 180). Per-lane logs, `summary.json`, `failures.json`, and phase timings are written under `.artifacts/docker-tests/<run-id>/`; use `pnpm test:docker:timings <summary.json>` to inspect slow lanes and `pnpm test:docker:rerun <run-id|summary.json|failures.json>` to print cheap targeted rerun commands.
|
||||
- `pnpm test:docker:browser-cdp-snapshot`: Builds a Chromium-backed source E2E container, starts raw CDP plus an isolated Gateway, runs `browser doctor --deep`, and verifies CDP role snapshots include link URLs, cursor-promoted clickables, iframe refs, and frame metadata.
|
||||
- CLI backend live Docker probes can be run as focused lanes, for example `pnpm test:docker:live-cli-backend:codex`, `pnpm test:docker:live-cli-backend:codex:resume`, or `pnpm test:docker:live-cli-backend:codex:mcp`. Claude and Gemini have matching `:resume` and `:mcp` aliases.
|
||||
- `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites.
|
||||
|
||||
@@ -37,6 +37,36 @@ const entries = list.stdout
|
||||
const normalized = entries.map((entry) => entry.replace(/^package\//u, ""));
|
||||
const entrySet = new Set(normalized);
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
const LEGACY_OMITTED_PRIVATE_QA_INVENTORY_PREFIXES = [
|
||||
"dist/extensions/qa-channel/",
|
||||
"dist/extensions/qa-lab/",
|
||||
"dist/extensions/qa-matrix/",
|
||||
"dist/plugin-sdk/extensions/qa-channel/",
|
||||
"dist/plugin-sdk/extensions/qa-lab/",
|
||||
];
|
||||
const LEGACY_OMITTED_PRIVATE_QA_INVENTORY_FILES = new Set([
|
||||
"dist/plugin-sdk/qa-channel.d.ts",
|
||||
"dist/plugin-sdk/qa-channel.js",
|
||||
"dist/plugin-sdk/qa-channel-protocol.d.ts",
|
||||
"dist/plugin-sdk/qa-channel-protocol.js",
|
||||
"dist/plugin-sdk/qa-lab.d.ts",
|
||||
"dist/plugin-sdk/qa-lab.js",
|
||||
"dist/plugin-sdk/qa-runtime.d.ts",
|
||||
"dist/plugin-sdk/qa-runtime.js",
|
||||
"dist/plugin-sdk/src/plugin-sdk/qa-channel.d.ts",
|
||||
"dist/plugin-sdk/src/plugin-sdk/qa-channel-protocol.d.ts",
|
||||
"dist/plugin-sdk/src/plugin-sdk/qa-lab.d.ts",
|
||||
"dist/plugin-sdk/src/plugin-sdk/qa-runtime.d.ts",
|
||||
]);
|
||||
|
||||
function isLegacyOmittedPrivateQaInventoryEntry(relativePath) {
|
||||
return (
|
||||
LEGACY_OMITTED_PRIVATE_QA_INVENTORY_FILES.has(relativePath) ||
|
||||
LEGACY_OMITTED_PRIVATE_QA_INVENTORY_PREFIXES.some((prefix) => relativePath.startsWith(prefix))
|
||||
);
|
||||
}
|
||||
|
||||
function readTarEntry(entryPath) {
|
||||
const candidates = [entryPath, `package/${entryPath}`];
|
||||
@@ -76,6 +106,12 @@ if (entrySet.has("dist/postinstall-inventory.json")) {
|
||||
for (const inventoryEntry of inventory) {
|
||||
const normalizedEntry = inventoryEntry.replace(/\\/gu, "/");
|
||||
if (!entrySet.has(normalizedEntry)) {
|
||||
if (isLegacyOmittedPrivateQaInventoryEntry(normalizedEntry)) {
|
||||
warnings.push(
|
||||
`legacy inventory references omitted private QA tar entry ${normalizedEntry}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
errors.push(`inventory references missing tar entry ${normalizedEntry}`);
|
||||
}
|
||||
}
|
||||
@@ -93,4 +129,7 @@ if (errors.length > 0) {
|
||||
fail(`OpenClaw package tarball integrity failed:\n${errors.join("\n")}`);
|
||||
}
|
||||
|
||||
for (const warning of warnings) {
|
||||
console.warn(`OpenClaw package tarball integrity warning: ${warning}`);
|
||||
}
|
||||
console.log("OpenClaw package tarball integrity passed.");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs a published OpenClaw npm package in Docker, performs Telegram
|
||||
# Installs an OpenClaw package candidate in Docker, performs Telegram
|
||||
# onboarding/doctor recovery, then runs the Telegram QA live harness.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -9,6 +9,8 @@ source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-npm-telegram-live-e2e" OPENCLAW_NPM_TELEGRAM_LIVE_E2E_IMAGE)"
|
||||
DOCKER_TARGET="${OPENCLAW_NPM_TELEGRAM_DOCKER_TARGET:-build}"
|
||||
PACKAGE_SPEC="${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC:-openclaw@beta}"
|
||||
PACKAGE_TGZ="${OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ:-${OPENCLAW_CURRENT_PACKAGE_TGZ:-}}"
|
||||
PACKAGE_LABEL="${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL:-}"
|
||||
OUTPUT_DIR="${OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR:-.artifacts/qa-e2e/npm-telegram-live}"
|
||||
|
||||
resolve_credential_source() {
|
||||
@@ -46,7 +48,45 @@ validate_openclaw_package_spec() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
validate_openclaw_package_spec "$PACKAGE_SPEC"
|
||||
resolve_package_tgz() {
|
||||
local candidate="$1"
|
||||
if [ -z "$candidate" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$candidate" ]; then
|
||||
echo "OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ must point to an existing .tgz file; got: $candidate" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$candidate" in
|
||||
*.tgz) ;;
|
||||
*)
|
||||
echo "OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ must point to a .tgz file; got: $candidate" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
local dir
|
||||
local base
|
||||
dir="$(cd "$(dirname "$candidate")" && pwd)"
|
||||
base="$(basename "$candidate")"
|
||||
printf "%s/%s" "$dir" "$base"
|
||||
}
|
||||
|
||||
package_mount_args=()
|
||||
package_install_source="$PACKAGE_SPEC"
|
||||
resolved_package_tgz="$(resolve_package_tgz "$PACKAGE_TGZ")"
|
||||
if [ -n "$resolved_package_tgz" ]; then
|
||||
package_install_source="/package-under-test/$(basename "$resolved_package_tgz")"
|
||||
package_mount_args=(-v "$resolved_package_tgz:$package_install_source:ro")
|
||||
else
|
||||
validate_openclaw_package_spec "$PACKAGE_SPEC"
|
||||
fi
|
||||
if [ -z "$PACKAGE_LABEL" ]; then
|
||||
if [ -n "$resolved_package_tgz" ]; then
|
||||
PACKAGE_LABEL="$(basename "$resolved_package_tgz")"
|
||||
else
|
||||
PACKAGE_LABEL="$PACKAGE_SPEC"
|
||||
fi
|
||||
fi
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" npm-telegram-live "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET"
|
||||
docker_e2e_harness_mount_args
|
||||
@@ -64,6 +104,7 @@ fi
|
||||
docker_env=(
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
-e OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC="$PACKAGE_SPEC"
|
||||
-e OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$PACKAGE_LABEL"
|
||||
-e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR"
|
||||
-e OPENCLAW_NPM_TELEGRAM_FAST="${OPENCLAW_NPM_TELEGRAM_FAST:-1}"
|
||||
)
|
||||
@@ -124,10 +165,12 @@ run_logged() {
|
||||
>"$run_log"
|
||||
}
|
||||
|
||||
echo "Running published npm Telegram live Docker E2E ($PACKAGE_SPEC)..."
|
||||
echo "Running package Telegram live Docker E2E ($PACKAGE_LABEL)..."
|
||||
run_logged docker run --rm \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC="$PACKAGE_SPEC" \
|
||||
-e OPENCLAW_NPM_TELEGRAM_INSTALL_SOURCE="$package_install_source" \
|
||||
-e OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$PACKAGE_LABEL" \
|
||||
"${package_mount_args[@]}" \
|
||||
-v "$npm_prefix_host:/npm-global" \
|
||||
-i "$IMAGE_NAME" bash -s <<'EOF'
|
||||
set -euo pipefail
|
||||
@@ -136,15 +179,16 @@ export HOME="$(mktemp -d "/tmp/openclaw-npm-telegram-install.XXXXXX")"
|
||||
export NPM_CONFIG_PREFIX="/npm-global"
|
||||
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
|
||||
|
||||
package_spec="${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC:?missing OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC}"
|
||||
echo "Installing ${package_spec}..."
|
||||
npm install -g "$package_spec" --no-fund --no-audit
|
||||
install_source="${OPENCLAW_NPM_TELEGRAM_INSTALL_SOURCE:?missing OPENCLAW_NPM_TELEGRAM_INSTALL_SOURCE}"
|
||||
package_label="${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL:-$install_source}"
|
||||
echo "Installing ${package_label} from ${install_source}..."
|
||||
npm install -g "$install_source" --no-fund --no-audit
|
||||
|
||||
command -v openclaw
|
||||
openclaw --version
|
||||
EOF
|
||||
|
||||
# Mount only test harness/plugin QA sources; the SUT itself is the npm install.
|
||||
# Mount only test harness/plugin QA sources; the SUT itself is the installed package candidate.
|
||||
run_logged docker run --rm \
|
||||
"${docker_env[@]}" \
|
||||
-v "$ROOT_DIR/.artifacts:/app/.artifacts" \
|
||||
@@ -161,7 +205,7 @@ export OPENCLAW_NPM_TELEGRAM_REPO_ROOT="/app"
|
||||
|
||||
dump_hotpath_logs() {
|
||||
local status="$1"
|
||||
echo "installed npm onboarding recovery hot path failed with exit code $status" >&2
|
||||
echo "installed-package onboarding recovery hot path failed with exit code $status" >&2
|
||||
for file in \
|
||||
/tmp/openclaw-npm-telegram-onboard.json \
|
||||
/tmp/openclaw-npm-telegram-channel-add.log \
|
||||
@@ -178,11 +222,11 @@ trap 'status=$?; dump_hotpath_logs "$status"; exit "$status"' ERR
|
||||
command -v openclaw
|
||||
openclaw --version
|
||||
# The mounted QA harness imports openclaw/plugin-sdk; point that package import
|
||||
# at the installed npm package without copying source into the test image.
|
||||
# at the installed package without copying source into the test image.
|
||||
mkdir -p /app/node_modules
|
||||
ln -sfn /npm-global/lib/node_modules/openclaw /app/node_modules/openclaw
|
||||
|
||||
echo "Running installed npm onboarding recovery hot path..."
|
||||
echo "Running installed-package onboarding recovery hot path..."
|
||||
OPENAI_API_KEY="${OPENAI_API_KEY:-sk-openclaw-npm-telegram-hotpath}" openclaw onboard --non-interactive --accept-risk \
|
||||
--mode local \
|
||||
--auth-choice openai-api-key \
|
||||
@@ -210,4 +254,4 @@ trap - ERR
|
||||
tsx scripts/e2e/npm-telegram-live-runner.ts
|
||||
EOF
|
||||
|
||||
echo "published npm Telegram live Docker E2E passed ($PACKAGE_SPEC)"
|
||||
echo "package Telegram live Docker E2E passed ($PACKAGE_LABEL)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
// Telegram npm-live Docker harness.
|
||||
// Runs QA live transport code against the published package installed in Docker.
|
||||
// Telegram package Docker harness.
|
||||
// Runs QA live transport code against the package candidate installed in Docker.
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
@@ -78,9 +78,9 @@ async function main() {
|
||||
credentialRole: resolveCredentialRole(process.env),
|
||||
});
|
||||
|
||||
process.stdout.write(`NPM Telegram QA report: ${result.reportPath}\n`);
|
||||
process.stdout.write(`NPM Telegram QA summary: ${result.summaryPath}\n`);
|
||||
process.stdout.write(`NPM Telegram QA observed messages: ${result.observedMessagesPath}\n`);
|
||||
process.stdout.write(`Package Telegram QA report: ${result.reportPath}\n`);
|
||||
process.stdout.write(`Package Telegram QA summary: ${result.summaryPath}\n`);
|
||||
process.stdout.write(`Package Telegram QA observed messages: ${result.observedMessagesPath}\n`);
|
||||
if (
|
||||
!parseBoolean(process.env.OPENCLAW_NPM_TELEGRAM_ALLOW_FAILURES) &&
|
||||
result.scenarios.some((scenario) => scenario.status === "fail")
|
||||
@@ -101,7 +101,7 @@ async function formatRunnerErrorMessage(error: unknown) {
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main().catch(async (error) => {
|
||||
process.stderr.write(
|
||||
`npm telegram live e2e failed: ${await formatRunnerErrorMessage(error)}\n`,
|
||||
`package telegram live e2e failed: ${await formatRunnerErrorMessage(error)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -359,8 +359,9 @@ node "$entry" gateway health \
|
||||
--json >/dev/null
|
||||
|
||||
cat >/tmp/openclaw-openai-web-search-minimal-client.mjs <<'NODE'
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const entry = process.env.OPENCLAW_ENTRY;
|
||||
const port = process.env.PORT;
|
||||
const token = process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
const mode = process.argv[2];
|
||||
@@ -371,47 +372,59 @@ const message =
|
||||
: "Return exactly OPENCLAW_SCHEMA_E2E_OK.";
|
||||
const id = mode === "reject" ? "schema-reject" : "schema-success";
|
||||
|
||||
if (!port || !token) throw new Error("missing PORT/OPENCLAW_GATEWAY_TOKEN");
|
||||
const callGatewayUrl = new URL("dist/gateway/call.js", pathToFileURL(`${process.cwd()}/`));
|
||||
const { callGateway } = await import(callGatewayUrl.href);
|
||||
if (!entry || !port || !token) throw new Error("missing OPENCLAW_ENTRY/PORT/OPENCLAW_GATEWAY_TOKEN");
|
||||
|
||||
async function runAgent() {
|
||||
const gatewayArgs = [
|
||||
entry,
|
||||
"gateway",
|
||||
"call",
|
||||
"--url",
|
||||
`ws://127.0.0.1:${port}`,
|
||||
"--token",
|
||||
token,
|
||||
"--timeout",
|
||||
"240000",
|
||||
"--expect-final",
|
||||
"--json",
|
||||
];
|
||||
|
||||
function gatewayAgent(params) {
|
||||
try {
|
||||
return await callGateway({
|
||||
method: "agent",
|
||||
params: {
|
||||
sessionKey,
|
||||
message,
|
||||
thinking: "minimal",
|
||||
deliver: false,
|
||||
timeout: 180,
|
||||
idempotencyKey: id,
|
||||
},
|
||||
expectFinal: true,
|
||||
url: `ws://127.0.0.1:${port}`,
|
||||
token,
|
||||
timeoutMs: 240000,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
value: JSON.parse(execFileSync("node", [...gatewayArgs, "agent", "--params", JSON.stringify(params)], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
if (mode === "reject") {
|
||||
console.error(String(error));
|
||||
process.exit(0);
|
||||
}
|
||||
throw error;
|
||||
const stderr = typeof error?.stderr === "string" ? error.stderr : "";
|
||||
const stdout = typeof error?.stdout === "string" ? error.stdout : "";
|
||||
const combined = [String(error), stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
||||
return { ok: false, error: new Error(combined) };
|
||||
}
|
||||
}
|
||||
|
||||
const result = await runAgent();
|
||||
const result = gatewayAgent({
|
||||
sessionKey,
|
||||
message,
|
||||
thinking: "minimal",
|
||||
deliver: false,
|
||||
timeout: 180,
|
||||
idempotencyKey: id,
|
||||
});
|
||||
|
||||
if (mode === "reject") {
|
||||
console.error(JSON.stringify(result));
|
||||
console.error(result.ok ? JSON.stringify(result.value) : String(result.error));
|
||||
process.exit(0);
|
||||
}
|
||||
if (result?.status !== "ok") {
|
||||
throw new Error(`agent run did not complete successfully: ${JSON.stringify(result)}`);
|
||||
if (!result.ok) throw result.error;
|
||||
if (result.value?.status !== "ok") {
|
||||
throw new Error(`agent run did not complete successfully: ${JSON.stringify(result.value)}`);
|
||||
}
|
||||
NODE
|
||||
|
||||
PORT="$PORT" OPENCLAW_GATEWAY_TOKEN="$TOKEN" node /tmp/openclaw-openai-web-search-minimal-client.mjs success >/tmp/openclaw-openai-web-search-minimal-client-success.log 2>&1
|
||||
OPENCLAW_ENTRY="$entry" PORT="$PORT" OPENCLAW_GATEWAY_TOKEN="$TOKEN" node /tmp/openclaw-openai-web-search-minimal-client.mjs success >/tmp/openclaw-openai-web-search-minimal-client-success.log 2>&1
|
||||
|
||||
node - "$MOCK_REQUEST_LOG" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
@@ -435,7 +448,7 @@ if (success.body.reasoning?.effort === "minimal") {
|
||||
}
|
||||
NODE
|
||||
|
||||
PORT="$PORT" OPENCLAW_GATEWAY_TOKEN="$TOKEN" node /tmp/openclaw-openai-web-search-minimal-client.mjs reject >/tmp/openclaw-openai-web-search-minimal-client-reject.log 2>&1
|
||||
OPENCLAW_ENTRY="$entry" PORT="$PORT" OPENCLAW_GATEWAY_TOKEN="$TOKEN" node /tmp/openclaw-openai-web-search-minimal-client.mjs reject >/tmp/openclaw-openai-web-search-minimal-client-reject.log 2>&1
|
||||
|
||||
for _ in $(seq 1 80); do
|
||||
if grep -Fq "$RAW_SCHEMA_ERROR" "$GATEWAY_LOG"; then
|
||||
|
||||
@@ -36,10 +36,15 @@ const DEFAULT_STATUS_INTERVAL_MS = 30_000;
|
||||
const DEFAULT_PREFLIGHT_RUN_TIMEOUT_MS = 60_000;
|
||||
const DEFAULT_TIMINGS_FILE = path.join(ROOT_DIR, ".artifacts/docker-tests/lane-timings.json");
|
||||
const DEFAULT_GITHUB_WORKFLOW = "openclaw-live-and-e2e-checks-reusable.yml";
|
||||
const cliArgs = new Set(process.argv.slice(2));
|
||||
for (const arg of cliArgs) {
|
||||
if (arg !== "--plan-json") {
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
const IS_MAIN = process.argv[1]
|
||||
? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
: false;
|
||||
const cliArgs = new Set(IS_MAIN ? process.argv.slice(2) : []);
|
||||
if (IS_MAIN) {
|
||||
for (const arg of cliArgs) {
|
||||
if (arg !== "--plan-json") {
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +87,12 @@ function resourceLimitEnvName(resource) {
|
||||
return `OPENCLAW_DOCKER_ALL_${resource.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_LIMIT`;
|
||||
}
|
||||
|
||||
export function describeDockerSchedulerLimits(parallelism, options) {
|
||||
return `parallelism=${parallelism} weightLimit=${options.weightLimit} resources=${resourceLimitsSummary(
|
||||
options.resourceLimits,
|
||||
)}`;
|
||||
}
|
||||
|
||||
function parseResourceLimit(env, resource, parallelism, fallback) {
|
||||
const envName = resourceLimitEnvName(resource);
|
||||
return parsePositiveInt(env[envName], Math.min(parallelism, fallback), envName);
|
||||
@@ -103,6 +114,26 @@ function parseSchedulerOptions(env, parallelism) {
|
||||
};
|
||||
}
|
||||
|
||||
export function canStartSchedulerLane(candidate, active, parallelism, options) {
|
||||
const weight = laneWeight(candidate);
|
||||
if (active.count >= parallelism) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const exceedsWeightLimit = active.weight + weight > options.weightLimit;
|
||||
const exceedsResourceLimit = laneResources(candidate).some((resource) => {
|
||||
const limit = options.resourceLimits[resource] ?? options.weightLimit;
|
||||
const current = active.resources.get(resource) ?? 0;
|
||||
return current + weight > limit;
|
||||
});
|
||||
|
||||
if (!exceedsWeightLimit && !exceedsResourceLimit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return active.count === 0;
|
||||
}
|
||||
|
||||
function timingSeconds(timingStore, poolLane) {
|
||||
const fromStore = timingStore?.lanes?.[poolLane.name]?.durationSeconds;
|
||||
if (typeof fromStore === "number" && Number.isFinite(fromStore) && fromStore > 0) {
|
||||
@@ -746,18 +777,7 @@ async function runLanePool(poolLanes, baseEnv, logDir, parallelism, options) {
|
||||
}
|
||||
|
||||
function canStartLane(candidate) {
|
||||
const weight = laneWeight(candidate);
|
||||
if (active.count >= parallelism || active.weight + weight > options.weightLimit) {
|
||||
return false;
|
||||
}
|
||||
for (const resource of laneResources(candidate)) {
|
||||
const limit = options.resourceLimits[resource] ?? options.weightLimit;
|
||||
const current = active.resources.get(resource) ?? 0;
|
||||
if (current + weight > limit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return canStartSchedulerLane(candidate, active, parallelism, options);
|
||||
}
|
||||
|
||||
function reserve(candidate) {
|
||||
@@ -818,7 +838,12 @@ async function runLanePool(poolLanes, baseEnv, logDir, parallelism, options) {
|
||||
}
|
||||
if (running.size === 0) {
|
||||
const blocked = pending.map(laneSummary).join(", ");
|
||||
throw new Error(`No Docker lanes fit scheduler limits: ${blocked}`);
|
||||
throw new Error(
|
||||
`No Docker lanes fit scheduler limits (${describeDockerSchedulerLimits(
|
||||
parallelism,
|
||||
options,
|
||||
)}): ${blocked}. Tune OPENCLAW_DOCKER_ALL_PARALLELISM, OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT, or OPENCLAW_DOCKER_ALL_<RESOURCE>_LIMIT.`,
|
||||
);
|
||||
}
|
||||
|
||||
const { promise, result } = await Promise.race(running);
|
||||
@@ -1217,7 +1242,9 @@ async function main() {
|
||||
console.log("==> Docker test suite passed");
|
||||
}
|
||||
|
||||
await main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
if (IS_MAIN) {
|
||||
await main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
EXPECTED_CODEX_MODELS_COMMAND_TEXT,
|
||||
EXPECTED_CODEX_STATUS_COMMAND_TEXT,
|
||||
isExpectedCodexModelsCommandText,
|
||||
isExpectedCodexStatusCommandText,
|
||||
} from "./gateway-codex-harness.live-helpers.js";
|
||||
|
||||
describe("gateway codex harness live helpers", () => {
|
||||
it("accepts the current codex status prose from the live harness", () => {
|
||||
const text =
|
||||
"OpenClaw is running on `openai/gpt-5.5` with low reasoning/text settings. Context is at `22k/272k` tokens, no compactions, and the current session is `agent:dev:live-codex-harness`.";
|
||||
|
||||
expect(
|
||||
EXPECTED_CODEX_STATUS_COMMAND_TEXT.some((expectedText) => text.includes(expectedText)),
|
||||
).toBe(false);
|
||||
expect(isExpectedCodexStatusCommandText(text)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects status prose for a different codex session", () => {
|
||||
const text =
|
||||
"OpenClaw is running on `openai/gpt-5.5` with low reasoning/text settings. Context is at `22k/272k` tokens, no compactions, and the current session is `agent:dev:other`.";
|
||||
|
||||
expect(isExpectedCodexStatusCommandText(text)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts the interactive model-selection summary emitted by current codex", () => {
|
||||
const text = [
|
||||
"`/codex models` opened an interactive model-selection prompt rather than printing a plain list.",
|
||||
|
||||
@@ -71,6 +71,39 @@ export const EXPECTED_CODEX_MODELS_COMMAND_TEXT = [
|
||||
"Current OpenClaw session status reports the active model as:",
|
||||
] as const;
|
||||
|
||||
export const EXPECTED_CODEX_STATUS_COMMAND_TEXT = [
|
||||
"Codex app-server:",
|
||||
"Model: `codex/",
|
||||
"Model: codex/",
|
||||
"Session: `agent:dev:live-codex-harness`",
|
||||
"Session: agent:dev:live-codex-harness",
|
||||
"OpenClaw `",
|
||||
"OpenClaw status:",
|
||||
"model `codex/",
|
||||
"session `agent:dev:live-codex-harness`",
|
||||
"Model/status card shown above",
|
||||
"Status shown above.",
|
||||
] as const;
|
||||
|
||||
export function isExpectedCodexStatusCommandText(text: string): boolean {
|
||||
const normalized = text.toLowerCase();
|
||||
const mentionsOpenClawStatus =
|
||||
normalized.includes("openclaw is running on") || normalized.includes("openclaw status:");
|
||||
const mentionsHarnessSession =
|
||||
normalized.includes("session: `agent:dev:live-codex-harness`") ||
|
||||
normalized.includes("session: agent:dev:live-codex-harness") ||
|
||||
normalized.includes("session `agent:dev:live-codex-harness`") ||
|
||||
normalized.includes("current session is `agent:dev:live-codex-harness`") ||
|
||||
normalized.includes("current session is agent:dev:live-codex-harness");
|
||||
const mentionsModel =
|
||||
normalized.includes("`openai/") ||
|
||||
normalized.includes(" openai/") ||
|
||||
normalized.includes("`codex/") ||
|
||||
normalized.includes(" codex/");
|
||||
|
||||
return mentionsOpenClawStatus && mentionsHarnessSession && mentionsModel;
|
||||
}
|
||||
|
||||
export function isExpectedCodexModelsCommandText(text: string): boolean {
|
||||
const normalized = text.toLowerCase();
|
||||
const mentionsCodexModelsCommand =
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
} from "./gateway-cli-backend.live-helpers.js";
|
||||
import {
|
||||
EXPECTED_CODEX_MODELS_COMMAND_TEXT,
|
||||
EXPECTED_CODEX_STATUS_COMMAND_TEXT,
|
||||
isExpectedCodexModelsCommandText,
|
||||
isExpectedCodexStatusCommandText,
|
||||
} from "./gateway-codex-harness.live-helpers.js";
|
||||
import {
|
||||
assertCronJobMatches,
|
||||
@@ -790,19 +792,8 @@ describeLive("gateway live (Codex harness)", () => {
|
||||
client,
|
||||
sessionKey,
|
||||
command: "/codex status",
|
||||
expectedText: [
|
||||
"Codex app-server:",
|
||||
"Model: `codex/",
|
||||
"Model: codex/",
|
||||
"Session: `agent:dev:live-codex-harness`",
|
||||
"Session: agent:dev:live-codex-harness",
|
||||
"OpenClaw `",
|
||||
"OpenClaw status:",
|
||||
"model `codex/",
|
||||
"session `agent:dev:live-codex-harness`",
|
||||
"Model/status card shown above",
|
||||
"Status shown above.",
|
||||
],
|
||||
expectedText: [...EXPECTED_CODEX_STATUS_COMMAND_TEXT],
|
||||
isExpectedText: isExpectedCodexStatusCommandText,
|
||||
});
|
||||
logCodexLiveStep("codex-status-command", { statusText });
|
||||
|
||||
|
||||
70
test/scripts/check-openclaw-package-tarball.test.ts
Normal file
70
test/scripts/check-openclaw-package-tarball.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const CHECK_SCRIPT = "scripts/check-openclaw-package-tarball.mjs";
|
||||
|
||||
function withTarball(
|
||||
inventory: string[],
|
||||
files: Record<string, string>,
|
||||
testBody: (tarball: string) => void,
|
||||
) {
|
||||
const root = mkdtempSync(join(tmpdir(), "openclaw-package-tarball-test-"));
|
||||
try {
|
||||
const packageRoot = join(root, "package");
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "0.0.0" }),
|
||||
);
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "postinstall-inventory.json"),
|
||||
JSON.stringify(inventory),
|
||||
);
|
||||
for (const [relativePath, body] of Object.entries(files)) {
|
||||
const filePath = join(packageRoot, relativePath);
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
writeFileSync(filePath, body);
|
||||
}
|
||||
|
||||
const tarball = join(root, "openclaw.tgz");
|
||||
const pack = spawnSync("tar", ["-czf", tarball, "-C", root, "package"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(pack.status, pack.stderr).toBe(0);
|
||||
testBody(tarball);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe("check-openclaw-package-tarball", () => {
|
||||
it("allows legacy private QA inventory entries omitted from shipped tarballs", () => {
|
||||
withTarball(
|
||||
["dist/index.js", "dist/extensions/qa-channel/runtime-api.js"],
|
||||
{ "dist/index.js": "export {};\n" },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stderr).toContain("legacy inventory references omitted private QA");
|
||||
expect(result.stdout).toContain("OpenClaw package tarball integrity passed.");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("still rejects non-legacy missing inventory entries", () => {
|
||||
withTarball(
|
||||
["dist/index.js", "dist/cli.js"],
|
||||
{ "dist/index.js": "export {};\n" },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain("inventory references missing tar entry dist/cli.js");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
138
test/scripts/docker-all-scheduler.test.ts
Normal file
138
test/scripts/docker-all-scheduler.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
canStartSchedulerLane,
|
||||
describeDockerSchedulerLimits,
|
||||
} from "../../scripts/test-docker-all.mjs";
|
||||
|
||||
const limits = {
|
||||
resourceLimits: {
|
||||
docker: 2,
|
||||
npm: 2,
|
||||
},
|
||||
weightLimit: 2,
|
||||
};
|
||||
|
||||
function activePool({
|
||||
count = 0,
|
||||
resources = {},
|
||||
weight = 0,
|
||||
}: {
|
||||
count?: number;
|
||||
resources?: Record<string, number>;
|
||||
weight?: number;
|
||||
} = {}) {
|
||||
return {
|
||||
count,
|
||||
resources: new Map(Object.entries(resources)),
|
||||
weight,
|
||||
};
|
||||
}
|
||||
|
||||
describe("scripts/test-docker-all scheduler", () => {
|
||||
it("allows an overweight lane to start alone under low parallelism", () => {
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "install-e2e",
|
||||
resources: ["npm"],
|
||||
weight: 4,
|
||||
},
|
||||
activePool(),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not co-schedule another lane while an overweight lane is active", () => {
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "package-update",
|
||||
resources: ["npm"],
|
||||
weight: 1,
|
||||
},
|
||||
activePool({
|
||||
count: 1,
|
||||
resources: {
|
||||
docker: 4,
|
||||
npm: 4,
|
||||
},
|
||||
weight: 4,
|
||||
}),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves the parallelism count cap", () => {
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "package-update",
|
||||
resources: ["npm"],
|
||||
weight: 1,
|
||||
},
|
||||
activePool({
|
||||
count: 2,
|
||||
resources: {
|
||||
docker: 1,
|
||||
npm: 1,
|
||||
},
|
||||
weight: 1,
|
||||
}),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps resource and weight limits as co-scheduling limits", () => {
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "npm-smoke",
|
||||
resources: ["npm"],
|
||||
weight: 1,
|
||||
},
|
||||
activePool({
|
||||
count: 1,
|
||||
resources: {
|
||||
docker: 1,
|
||||
npm: 1,
|
||||
},
|
||||
weight: 1,
|
||||
}),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
canStartSchedulerLane(
|
||||
{
|
||||
name: "npm-heavy",
|
||||
resources: ["npm"],
|
||||
weight: 2,
|
||||
},
|
||||
activePool({
|
||||
count: 1,
|
||||
resources: {
|
||||
docker: 1,
|
||||
npm: 1,
|
||||
},
|
||||
weight: 1,
|
||||
}),
|
||||
2,
|
||||
limits,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("describes effective scheduler limits for operator errors", () => {
|
||||
expect(describeDockerSchedulerLimits(2, limits)).toBe(
|
||||
"parallelism=2 weightLimit=2 resources=docker=2 npm=2",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -91,8 +91,8 @@ describe("docker build helper", () => {
|
||||
it("keeps OpenAI web search smoke on one gateway agent connection", () => {
|
||||
const runner = readFileSync(OPENAI_WEB_SEARCH_MINIMAL_E2E_PATH, "utf8");
|
||||
|
||||
expect(runner).toContain('new URL("dist/gateway/call.js"');
|
||||
expect(runner).toContain("expectFinal: true");
|
||||
expect(runner).toContain('"--expect-final"');
|
||||
expect(runner).toContain('[...gatewayArgs, "agent", "--params"');
|
||||
expect(runner).not.toContain('"agent.wait"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { __testing } from "../../scripts/e2e/npm-telegram-live-runner.ts";
|
||||
const TEST_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DOCKER_SCRIPT_PATH = path.resolve(TEST_DIR, "../../scripts/e2e/npm-telegram-live-docker.sh");
|
||||
|
||||
describe("npm Telegram live Docker E2E", () => {
|
||||
describe("package Telegram live Docker E2E", () => {
|
||||
it("supports npm-specific Convex credential aliases", () => {
|
||||
const script = readFileSync(DOCKER_SCRIPT_PATH, "utf8");
|
||||
|
||||
@@ -28,18 +28,33 @@ describe("npm Telegram live Docker E2E", () => {
|
||||
expect(script).toContain('printf "convex"');
|
||||
});
|
||||
|
||||
it("installs the npm package before forwarding runtime secrets", () => {
|
||||
it("installs the package candidate before forwarding runtime secrets", () => {
|
||||
const script = readFileSync(DOCKER_SCRIPT_PATH, "utf8");
|
||||
const installRunStart = script.indexOf('echo "Running published npm Telegram live Docker E2E');
|
||||
const installRunStart = script.indexOf('echo "Running package Telegram live Docker E2E');
|
||||
const installRunEnd = script.indexOf('run_logged docker run --rm \\\n "${docker_env[@]}"');
|
||||
const installRun = script.slice(installRunStart, installRunEnd);
|
||||
|
||||
expect(installRun).toContain('npm install -g "$package_spec" --no-fund --no-audit');
|
||||
expect(installRun).toContain('npm install -g "$install_source" --no-fund --no-audit');
|
||||
expect(installRun).toContain('"${package_mount_args[@]}"');
|
||||
expect(installRun).not.toContain('"${docker_env[@]}"');
|
||||
expect(script).toContain('if [ -z "$credential_role" ] && [ -n "${CI:-}" ]');
|
||||
expect(script).toContain('credential_role="ci"');
|
||||
});
|
||||
|
||||
it("can install a resolved package tarball instead of a registry spec", () => {
|
||||
const script = readFileSync(DOCKER_SCRIPT_PATH, "utf8");
|
||||
|
||||
expect(script).toContain("OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ");
|
||||
expect(script).toContain("OPENCLAW_CURRENT_PACKAGE_TGZ");
|
||||
expect(script).toContain(
|
||||
'package_mount_args=(-v "$resolved_package_tgz:$package_install_source:ro")',
|
||||
);
|
||||
expect(script).toContain('validate_openclaw_package_spec "$PACKAGE_SPEC"');
|
||||
expect(script.indexOf('if [ -n "$resolved_package_tgz" ]; then')).toBeLessThan(
|
||||
script.indexOf('validate_openclaw_package_spec "$PACKAGE_SPEC"'),
|
||||
);
|
||||
});
|
||||
|
||||
it("lets npm-specific credential aliases override shared QA env", () => {
|
||||
expect(
|
||||
__testing.resolveCredentialSource({
|
||||
|
||||
@@ -34,15 +34,21 @@ describe("package acceptance workflow", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("offers bounded product profiles and keeps Telegram published-npm only", () => {
|
||||
it("offers bounded product profiles and can run Telegram against the resolved artifact", () => {
|
||||
const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
||||
|
||||
expect(workflow).toContain("suite_profile:");
|
||||
expect(workflow).toContain("npm-onboard-channel-agent gateway-network config-reload");
|
||||
expect(workflow).toContain("install-e2e npm-onboard-channel-agent doctor-switch");
|
||||
expect(workflow).toContain("include_release_path_suites=true");
|
||||
expect(workflow).toContain("telegram_mode requires source=npm");
|
||||
expect(workflow).not.toContain("telegram_mode requires source=npm");
|
||||
expect(workflow).toContain("uses: ./.github/workflows/npm-telegram-beta-e2e.yml");
|
||||
expect(workflow).toContain(
|
||||
"package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}",
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
"package_label: openclaw@${{ needs.resolve_package.outputs.package_version }}",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,10 +68,13 @@ describe("package artifact reuse", () => {
|
||||
expect(action).toContain("name: ${{ inputs.package-artifact-name }}");
|
||||
});
|
||||
|
||||
it("allows the npm Telegram lane to run from reusable package acceptance", () => {
|
||||
it("allows the Telegram lane to run from reusable package acceptance artifacts", () => {
|
||||
const workflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8");
|
||||
|
||||
expect(workflow).toContain("workflow_call:");
|
||||
expect(workflow).toContain("package_artifact_name:");
|
||||
expect(workflow).toContain("Download package-under-test artifact");
|
||||
expect(workflow).toContain("OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ");
|
||||
expect(workflow).toContain("provider_mode:");
|
||||
expect(workflow).toContain("provider_mode must be mock-openai or live-frontier");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user