diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 4d1d3816ce4..bb39a2b6b5f 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -5,6 +5,8 @@ on: branches: [main] pull_request: types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] + schedule: + - cron: "17 3 * * *" workflow_dispatch: permissions: @@ -24,6 +26,8 @@ jobs: outputs: docs_only: ${{ steps.manifest.outputs.docs_only }} run_install_smoke: ${{ steps.manifest.outputs.run_install_smoke }} + run_fast_install_smoke: ${{ steps.manifest.outputs.run_fast_install_smoke }} + run_full_install_smoke: ${{ steps.manifest.outputs.run_full_install_smoke }} steps: - name: Checkout uses: actions/checkout@v6 @@ -34,7 +38,7 @@ jobs: submodules: false - name: Ensure preflight base commit - if: github.event_name != 'workflow_dispatch' + if: github.event_name != 'workflow_dispatch' && github.event_name != 'schedule' uses: ./.github/actions/ensure-base-commit with: base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }} @@ -46,7 +50,7 @@ jobs: - name: Detect changed smoke scope id: changed_scope - if: github.event_name != 'workflow_dispatch' && steps.docs_scope.outputs.docs_only != 'true' + if: github.event_name != 'workflow_dispatch' && github.event_name != 'schedule' && steps.docs_scope.outputs.docs_only != 'true' shell: bash run: | set -euo pipefail @@ -63,26 +67,125 @@ jobs: id: manifest env: OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }} - OPENCLAW_CI_FORCE_INSTALL_SMOKE: ${{ github.event_name == 'workflow_dispatch' && 'true' || 'false' }} - OPENCLAW_CI_RUN_CHANGED_SMOKE: ${{ steps.changed_scope.outputs.run_changed_smoke || 'false' }} + OPENCLAW_CI_FORCE_FULL_INSTALL_SMOKE: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event_name == 'push') && 'true' || 'false' }} + OPENCLAW_CI_RUN_FAST_INSTALL_SMOKE: ${{ steps.changed_scope.outputs.run_fast_install_smoke || steps.changed_scope.outputs.run_changed_smoke || 'false' }} + OPENCLAW_CI_RUN_FULL_INSTALL_SMOKE: ${{ steps.changed_scope.outputs.run_full_install_smoke || 'false' }} run: | docs_only="${OPENCLAW_CI_DOCS_ONLY:-false}" - force_install_smoke="${OPENCLAW_CI_FORCE_INSTALL_SMOKE:-false}" - run_changed_smoke="${OPENCLAW_CI_RUN_CHANGED_SMOKE:-false}" + force_full_install_smoke="${OPENCLAW_CI_FORCE_FULL_INSTALL_SMOKE:-false}" + run_changed_fast_install_smoke="${OPENCLAW_CI_RUN_FAST_INSTALL_SMOKE:-false}" + run_changed_full_install_smoke="${OPENCLAW_CI_RUN_FULL_INSTALL_SMOKE:-false}" + run_fast_install_smoke=false + run_full_install_smoke=false run_install_smoke=false - if [ "$force_install_smoke" = "true" ]; then + if [ "$force_full_install_smoke" = "true" ]; then + run_fast_install_smoke=true + run_full_install_smoke=true run_install_smoke=true - elif [ "$docs_only" != "true" ] && [ "$run_changed_smoke" = "true" ]; then + elif [ "$docs_only" != "true" ] && [ "$run_changed_full_install_smoke" = "true" ]; then + run_fast_install_smoke=true + run_full_install_smoke=true + run_install_smoke=true + elif [ "$docs_only" != "true" ] && [ "$run_changed_fast_install_smoke" = "true" ]; then + run_fast_install_smoke=true run_install_smoke=true fi { echo "docs_only=$docs_only" echo "run_install_smoke=$run_install_smoke" + echo "run_fast_install_smoke=$run_fast_install_smoke" + echo "run_full_install_smoke=$run_full_install_smoke" } >> "$GITHUB_OUTPUT" + install-smoke-fast: + needs: [preflight] + if: needs.preflight.outputs.run_fast_install_smoke == 'true' && needs.preflight.outputs.run_full_install_smoke != 'true' + runs-on: blacksmith-16vcpu-ubuntu-2404 + env: + DOCKER_BUILD_SUMMARY: "false" + DOCKER_BUILD_RECORD_UPLOAD: "false" + steps: + - name: Checkout CLI + uses: actions/checkout@v6 + + - name: Set up Blacksmith Docker Builder + uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1 + + # Blacksmith's builder owns the Docker layer cache; keep smoke builds off + # explicit gha cache directives so local tags still load cleanly. + - name: Build root Dockerfile smoke image + uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2 + with: + context: . + file: ./Dockerfile + build-args: | + OPENCLAW_DOCKER_APT_UPGRADE=0 + OPENCLAW_EXTENSIONS=matrix + tags: | + openclaw-dockerfile-smoke:local + openclaw-ext-smoke:local + load: true + push: false + provenance: false + + - name: Run root Dockerfile CLI smoke + run: | + docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version' + + - name: Run Docker gateway network e2e + env: + OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE: openclaw-dockerfile-smoke:local + OPENCLAW_GATEWAY_NETWORK_E2E_SKIP_BUILD: "1" + run: bash scripts/e2e/gateway-network-docker.sh + + - name: Smoke test Dockerfile with matrix extension build arg + run: | + docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc ' + which openclaw && + openclaw --version && + node -e " + const Module = require(\"node:module\"); + const matrixPackage = require(\"/app/extensions/matrix/package.json\"); + const requireFromMatrix = Module.createRequire(\"/app/extensions/matrix/package.json\"); + const runtimeDeps = Object.keys(matrixPackage.dependencies ?? {}); + if (runtimeDeps.length === 0) { + throw new Error( + \"matrix package has no declared runtime dependencies; smoke cannot validate install mirroring\", + ); + } + for (const dep of runtimeDeps) { + requireFromMatrix.resolve(dep); + } + const { spawnSync } = require(\"node:child_process\"); + const run = spawnSync(\"openclaw\", [\"plugins\", \"list\", \"--json\"], { encoding: \"utf8\" }); + if (run.status !== 0) { + process.stderr.write(run.stderr || run.stdout || \"plugins list failed\\n\"); + process.exit(run.status ?? 1); + } + const parsed = JSON.parse(run.stdout); + const matrix = (parsed.plugins || []).find((entry) => entry.id === \"matrix\"); + if (!matrix) { + throw new Error(\"matrix plugin missing from bundled plugin list\"); + } + const matrixDiag = (parsed.diagnostics || []).filter( + (diag) => + typeof diag.source === \"string\" && + diag.source.includes(\"/extensions/matrix\") && + typeof diag.message === \"string\" && + diag.message.includes(\"extension entry escapes package directory\"), + ); + if (matrixDiag.length > 0) { + throw new Error( + \"unexpected matrix diagnostics: \" + + matrixDiag.map((diag) => diag.message).join(\"; \"), + ); + } + " + ' + install-smoke: needs: [preflight] - if: needs.preflight.outputs.run_install_smoke == 'true' + if: needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 env: DOCKER_BUILD_SUMMARY: "false" @@ -224,7 +327,7 @@ jobs: docker-e2e-fast: needs: [preflight] - if: needs.preflight.outputs.run_install_smoke == 'true' + if: needs.preflight.outputs.run_fast_install_smoke == 'true' || needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 8 env: diff --git a/docs/ci.md b/docs/ci.md index cf44dcc5d84..aaffa2ac448 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -91,7 +91,7 @@ Jobs are ordered so cheap checks fail before expensive ones run: Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. CI workflow edits validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes. Windows Node checks are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes so they do not reserve a 16-vCPU Windows worker for coverage that is already exercised by the normal test shards. -The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It computes `run_install_smoke` from the narrower changed-smoke signal, so Docker/install smoke runs for install, packaging, container-relevant changes, bundled extension production changes, and the core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Test-only and docs-only edits do not reserve Docker workers. Its QR package smoke forces the Docker `pnpm install` layer to rerun while preserving the BuildKit pnpm store cache, so it still exercises installation without redownloading dependencies on every run. Its gateway-network e2e reuses the runtime image built earlier in the job, so it adds real container-to-container WebSocket coverage without adding another Docker build. Local `test:docker:all` prebuilds one shared live-test image and one shared `scripts/e2e/Dockerfile` built-app image, then runs the live/E2E smoke lanes in parallel with `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default concurrency of 4 with `OPENCLAW_DOCKER_ALL_PARALLELISM`. The local aggregate stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`. Startup- or provider-sensitive lanes run exclusively after the parallel pool. The reusable live/E2E workflow mirrors the shared-image pattern by building and pushing one SHA-tagged GHCR Docker E2E image before the Docker matrix, then running the matrix with `OPENCLAW_SKIP_DOCKER_BUILD=1`. The scheduled live/E2E workflow runs the full release-path Docker suite daily. QR and installer Docker tests keep their own install-focused Dockerfiles. A separate `docker-e2e-fast` job runs the bounded bundled-plugin Docker profile under a 120-second command timeout: setup-entry dependency repair plus synthetic bundled-loader failure isolation. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes. +The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 120-second command timeout. The full path keeps QR package install, Bun global install, and installer Docker/update coverage for `main` pushes, nightly scheduled runs, manual dispatches, and true installer/package/Docker changes. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image and one shared `scripts/e2e/Dockerfile` built-app image, then runs the live/E2E smoke lanes in parallel with `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default concurrency of 4 with `OPENCLAW_DOCKER_ALL_PARALLELISM`. The local aggregate stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`. Startup- or provider-sensitive lanes run exclusively after the parallel pool. The reusable live/E2E workflow mirrors the shared-image pattern by building and pushing one SHA-tagged GHCR Docker E2E image before the Docker matrix, then running the matrix with `OPENCLAW_SKIP_DOCKER_BUILD=1`. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes. Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by `scripts/check-changed.mjs`. That local gate is stricter about architecture boundaries than the broad CI platform scope: core production changes run core prod typecheck plus core tests, core test-only changes run only core test typecheck/tests, extension production changes run extension prod typecheck plus extension tests, and extension test-only changes run only extension test typecheck/tests. Public Plugin SDK or plugin-contract changes expand to extension validation because extensions depend on those core contracts. Release metadata-only version bumps run targeted version/config/root-dependency checks. Unknown root/config changes fail safe to all lanes. diff --git a/scripts/ci-changed-scope.d.mts b/scripts/ci-changed-scope.d.mts index 50ab11d3aea..76c7c65c329 100644 --- a/scripts/ci-changed-scope.d.mts +++ b/scripts/ci-changed-scope.d.mts @@ -8,6 +8,16 @@ export type ChangedScope = { runControlUiI18n: boolean; }; +export type InstallSmokeScope = { + runFastInstallSmoke: boolean; + runFullInstallSmoke: boolean; +}; + export function detectChangedScope(changedPaths: string[]): ChangedScope; +export function detectInstallSmokeScope(changedPaths: string[]): InstallSmokeScope; export function listChangedPaths(base: string, head?: string): string[]; -export function writeGitHubOutput(scope: ChangedScope, outputPath?: string): void; +export function writeGitHubOutput( + scope: ChangedScope, + outputPath?: string, + installSmokeScope?: InstallSmokeScope, +): void; diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index 2dac3a1d9c8..d750a561e2a 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process"; import { appendFileSync } from "node:fs"; /** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */ +/** @typedef {{ runFastInstallSmoke: boolean; runFullInstallSmoke: boolean }} InstallSmokeScope */ const FULL_SCOPE = { runNode: true, @@ -43,10 +44,11 @@ const CONTROL_UI_I18N_SCOPE_RE = /^(ui\/src\/i18n\/|scripts\/control-ui-i18n\.ts$|\.github\/workflows\/control-ui-locale-refresh\.yml$)/; const NATIVE_ONLY_RE = /^(apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/macos-mlx-tts\/|apps\/shared\/|Swabble\/|appcast\.xml$)/; -const CHANGED_SMOKE_SCOPE_RE = - /^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/install\.sh$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|.*\.sh)$|src\/plugins\/bundled-runtime-deps\.ts$|extensions\/[^/]+\/package\.json$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/; -const CHANGED_SMOKE_RUNTIME_SCOPE_RE = - /^(src\/(?:channels|gateway|plugin-sdk|plugins)\/|extensions\/)/; +const FAST_INSTALL_SMOKE_SCOPE_RE = + /^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|gateway-network-docker\.sh|bundled-channel-runtime-deps-docker\.sh)$|src\/plugins\/bundled-runtime-deps\.ts$|extensions\/[^/]+\/(?:package\.json|openclaw\.plugin\.json)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/; +const FULL_INSTALL_SMOKE_SCOPE_RE = + /^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/install\.sh$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|qr-import-docker\.sh|bun-global-install-smoke\.sh)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/; +const FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE = /^src\/(?:channels|gateway|plugin-sdk|plugins)\//; /** * @param {string[]} changedPaths @@ -114,10 +116,7 @@ export function detectChangedScope(changedPaths) { runWindows = true; } - if ( - CHANGED_SMOKE_SCOPE_RE.test(path) || - (CHANGED_SMOKE_RUNTIME_SCOPE_RE.test(path) && !TEST_ONLY_PATH_RE.test(path)) - ) { + if (detectInstallSmokeScopeForPath(path).runFastInstallSmoke) { runChangedSmoke = true; } @@ -145,6 +144,42 @@ export function detectChangedScope(changedPaths) { }; } +/** + * @param {string} path + * @returns {InstallSmokeScope} + */ +function detectInstallSmokeScopeForPath(path) { + const runFullInstallSmoke = FULL_INSTALL_SMOKE_SCOPE_RE.test(path); + const runFastInstallSmoke = + runFullInstallSmoke || + FAST_INSTALL_SMOKE_SCOPE_RE.test(path) || + (FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE.test(path) && !TEST_ONLY_PATH_RE.test(path)); + return { runFastInstallSmoke, runFullInstallSmoke }; +} + +/** + * @param {string[]} changedPaths + * @returns {InstallSmokeScope} + */ +export function detectInstallSmokeScope(changedPaths) { + if (!Array.isArray(changedPaths) || changedPaths.length === 0) { + return { runFastInstallSmoke: true, runFullInstallSmoke: true }; + } + + let runFastInstallSmoke = false; + let runFullInstallSmoke = false; + for (const rawPath of changedPaths) { + const path = rawPath.trim(); + if (!path || DOCS_PATH_RE.test(path)) { + continue; + } + const pathScope = detectInstallSmokeScopeForPath(path); + runFastInstallSmoke ||= pathScope.runFastInstallSmoke; + runFullInstallSmoke ||= pathScope.runFullInstallSmoke; + } + return { runFastInstallSmoke, runFullInstallSmoke }; +} + /** * @param {string} base * @param {string} [head] @@ -167,8 +202,16 @@ export function listChangedPaths(base, head = "HEAD") { /** * @param {ChangedScope} scope * @param {string} [outputPath] + * @param {InstallSmokeScope} [installSmokeScope] */ -export function writeGitHubOutput(scope, outputPath = process.env.GITHUB_OUTPUT) { +export function writeGitHubOutput( + scope, + outputPath = process.env.GITHUB_OUTPUT, + installSmokeScope = { + runFastInstallSmoke: scope.runChangedSmoke, + runFullInstallSmoke: scope.runChangedSmoke, + }, +) { if (!outputPath) { throw new Error("GITHUB_OUTPUT is required"); } @@ -178,6 +221,16 @@ export function writeGitHubOutput(scope, outputPath = process.env.GITHUB_OUTPUT) appendFileSync(outputPath, `run_windows=${scope.runWindows}\n`, "utf8"); appendFileSync(outputPath, `run_skills_python=${scope.runSkillsPython}\n`, "utf8"); appendFileSync(outputPath, `run_changed_smoke=${scope.runChangedSmoke}\n`, "utf8"); + appendFileSync( + outputPath, + `run_fast_install_smoke=${installSmokeScope.runFastInstallSmoke}\n`, + "utf8", + ); + appendFileSync( + outputPath, + `run_full_install_smoke=${installSmokeScope.runFullInstallSmoke}\n`, + "utf8", + ); appendFileSync(outputPath, `run_control_ui_i18n=${scope.runControlUiI18n}\n`, "utf8"); } @@ -211,7 +264,11 @@ if (isDirectRun()) { writeGitHubOutput(EMPTY_SCOPE); process.exit(0); } - writeGitHubOutput(detectChangedScope(changedPaths)); + writeGitHubOutput( + detectChangedScope(changedPaths), + process.env.GITHUB_OUTPUT, + detectInstallSmokeScope(changedPaths), + ); } catch { writeGitHubOutput(FULL_SCOPE); } diff --git a/src/infra/scripts-modules.d.ts b/src/infra/scripts-modules.d.ts index 15ad8d79d5b..fdd12dd1b31 100644 --- a/src/infra/scripts-modules.d.ts +++ b/src/infra/scripts-modules.d.ts @@ -27,4 +27,8 @@ declare module "../../scripts/ci-changed-scope.mjs" { runChangedSmoke: boolean; runControlUiI18n: boolean; }; + export function detectInstallSmokeScope(paths: string[]): { + runFastInstallSmoke: boolean; + runFullInstallSmoke: boolean; + }; } diff --git a/src/scripts/ci-changed-scope.test.ts b/src/scripts/ci-changed-scope.test.ts index fdcdc1b6534..214d65a9cbd 100644 --- a/src/scripts/ci-changed-scope.test.ts +++ b/src/scripts/ci-changed-scope.test.ts @@ -5,7 +5,7 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { bundledPluginFile } from "../../test/helpers/bundled-plugin-paths.js"; -const { detectChangedScope, listChangedPaths } = +const { detectChangedScope, detectInstallSmokeScope, listChangedPaths } = (await import("../../scripts/ci-changed-scope.mjs")) as unknown as { detectChangedScope: (paths: string[]) => { runNode: boolean; @@ -16,6 +16,10 @@ const { detectChangedScope, listChangedPaths } = runChangedSmoke: boolean; runControlUiI18n: boolean; }; + detectInstallSmokeScope: (paths: string[]) => { + runFastInstallSmoke: boolean; + runFullInstallSmoke: boolean; + }; listChangedPaths: (base: string, head?: string) => string[]; }; @@ -319,7 +323,7 @@ describe("detectChangedScope", () => { runAndroid: false, runWindows: false, runSkillsPython: false, - runChangedSmoke: true, + runChangedSmoke: false, runControlUiI18n: false, }); expect(detectChangedScope(["scripts/postinstall-bundled-plugins.mjs"])).toEqual({ @@ -351,7 +355,7 @@ describe("detectChangedScope", () => { }); }); - it("runs changed-smoke for Docker-covered core and extension runtime surfaces", () => { + it("runs changed-smoke for Docker-covered core runtime surfaces", () => { expect(detectChangedScope(["src/plugins/loader.ts"])).toEqual({ runNode: true, runMacos: false, @@ -394,11 +398,42 @@ describe("detectChangedScope", () => { runAndroid: false, runWindows: false, runSkillsPython: false, - runChangedSmoke: true, + runChangedSmoke: false, runControlUiI18n: false, }); }); + it("splits install smoke into fast and full scopes", () => { + expect(detectInstallSmokeScope([])).toEqual({ + runFastInstallSmoke: true, + runFullInstallSmoke: true, + }); + expect(detectInstallSmokeScope(["docs/ci.md"])).toEqual({ + runFastInstallSmoke: false, + runFullInstallSmoke: false, + }); + expect(detectInstallSmokeScope(["scripts/install.sh"])).toEqual({ + runFastInstallSmoke: true, + runFullInstallSmoke: true, + }); + expect(detectInstallSmokeScope(["Dockerfile"])).toEqual({ + runFastInstallSmoke: true, + runFullInstallSmoke: true, + }); + expect(detectInstallSmokeScope([bundledPluginFile("matrix", "package.json")])).toEqual({ + runFastInstallSmoke: true, + runFullInstallSmoke: false, + }); + expect(detectInstallSmokeScope(["src/plugins/loader.ts"])).toEqual({ + runFastInstallSmoke: true, + runFullInstallSmoke: false, + }); + expect(detectInstallSmokeScope([bundledPluginFile("matrix", "index.ts")])).toEqual({ + runFastInstallSmoke: false, + runFullInstallSmoke: false, + }); + }); + it("keeps changed-smoke off for runtime-surface tests", () => { expect(detectChangedScope(["src/plugins/loader.test.ts"])).toEqual({ runNode: true, @@ -483,6 +518,8 @@ describe("detectChangedScope", () => { run_windows: "false", run_skills_python: "false", run_changed_smoke: "false", + run_fast_install_smoke: "false", + run_full_install_smoke: "false", run_control_ui_i18n: "false", }); }); diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index 226ccc7ca42..105b4ceb7e0 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -148,8 +148,11 @@ describe("bun global install smoke", () => { "OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE: openclaw-dockerfile-smoke:local", ); expect(workflow).toContain("format('{0}-manual-{1}', github.workflow, github.run_id)"); - expect(workflow).toContain("OPENCLAW_CI_FORCE_INSTALL_SMOKE"); - expect(workflow).toContain('if [ "$force_install_smoke" = "true" ]; then'); + expect(workflow).toContain("OPENCLAW_CI_FORCE_FULL_INSTALL_SMOKE"); + expect(workflow).toContain('if [ "$force_full_install_smoke" = "true" ]; then'); + expect(workflow).toContain("install-smoke-fast:"); + expect(workflow).toContain("run_fast_install_smoke"); + expect(workflow).toContain("run_full_install_smoke"); expect(workflow).toContain('OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1"'); }); });