diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b567cbf15f32..01b8cc49e199 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1539,14 +1539,15 @@ jobs: - *install_playwright_chromium - name: Test Control UI end-to-end + if: matrix.shard != 4 run: >- node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner - --shard ${{ matrix.shard }}/4 + --shard ${{ matrix.shard }}/3 - name: Test browser extension bootstrap end-to-end - if: matrix.shard == 1 + if: matrix.shard == 4 run: pnpm test:e2e:browser-extension checks-ui-e2e-real-gateway: diff --git a/config/knip.all-exports.config.ts b/config/knip.all-exports.config.ts index f0bda1a106b4..2be5e34811f7 100644 --- a/config/knip.all-exports.config.ts +++ b/config/knip.all-exports.config.ts @@ -56,8 +56,6 @@ const ROOT_TEST_ENTRY_GLOBS = [ // Invoked directly by the Docker image-auth scenario. "test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts!", "test/e2e/qa-lab/runtime/system-agent-first-run-docker-client.ts!", - // Loaded through NODE_OPTIONS by the Doctor SecretRef E2E; no static import edge exists. - "test/fixtures/windows-acl-tools-unavailable.mjs!", // QA scenario YAML dispatches these scripts/tests by path rather than import. ...QA_SCENARIO_EXECUTION_ENTRIES, // Invoked directly by the sandbox bind-conflict E2E verification script. diff --git a/docs/ci.md b/docs/ci.md index e6c12c027217..9397917a931d 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -186,7 +186,7 @@ for commands and recovery. | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ubuntu-24.04` | `security-fast`, manual CI dispatch and non-canonical repository fallbacks, pull-request retries of both UI E2E jobs, the QA Smoke aggregate, CodeQL security and quality scans, workflow-sanity, labeler, auto-response, the standalone Docs workflow, and the whole Install Smoke workflow | | `blacksmith-4vcpu-ubuntu-2404` | `preflight`, `pnpm-store-warmup`, `native-i18n`, `checks-fast-core` except QA Smoke CI, plugin/channel contract shards, most bundled/lower-weight Linux Node shards, `check-*` lanes except `check-lint`, selected `check-additional-*` shards, `check-docs`, and `skills-python` | -| `blacksmith-8vcpu-ubuntu-2404` | Retained heavy Linux Node suites, first-attempt same-repo pull requests and pushes for the serial Chromium/Vite `checks-ui-e2e` lane, boundary/extension-heavy `check-additional-*` shards, and `android` | +| `blacksmith-8vcpu-ubuntu-2404` | Retained heavy Linux Node suites, first-attempt same-repo pull requests and pushes for the serial Chromium/Vite `checks-ui-e2e` lane (three Control UI shards plus one browser extension shard), boundary/extension-heavy `check-additional-*` shards, and `android` | | `blacksmith-16vcpu-ubuntu-2404` | Automatic QA Smoke CI shards, first-attempt same-repo pull requests and pushes for `checks-ui-e2e-real-gateway`, `build-artifacts` in CI and Testbox, and `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved) | | `blacksmith-8vcpu-windows-2025` | `checks-windows` | | `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` | diff --git a/package.json b/package.json index 8e19ce01b45b..8246491dc76d 100644 --- a/package.json +++ b/package.json @@ -1965,7 +1965,7 @@ "test:unit:fast:audit": "node --import tsx scripts/test-unit-fast-audit.mts", "test:voicecall:closedloop": "node --import tsx scripts/test-voicecall-closedloop.mts", "test:watch": "node --import tsx scripts/test-projects.mts --watch", - "test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/process-env.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/utils.test.ts src/commands/agents.commands.list.test.ts src/cli/daemon-cli/status.print.test.ts src/cli/mcp-cli.path-case.windows.test.ts packages/terminal-core/src/display-string.test.ts src/agents/sandbox/fs-paths.test.ts src/agents/sessions/tools/render-utils.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts", + "test:windows:ci": "node --import tsx scripts/test-projects.mts src/shared/runtime-import.test.ts src/config/sessions/session-accessor.sqlite-archive.worker.test.ts src/commands/doctor-gateway-auth-token.windows.test.ts src/media/local-media-path.windows.test.ts src/infra/sqlite-snapshot.test.ts src/infra/ssh-client.windows.test.ts src/infra/update-managed-service-handoff-command.test.ts src/infra/update-managed-service-handoff-lifecycle.test.ts src/infra/exec-allowlist-pattern.test.ts src/infra/executable-path.test.ts src/infra/process-env.test.ts src/infra/fs-safe-remove.test.ts src/snapshot/local-repository.windows.test.ts src/state/openclaw-database-paths.windows.test.ts src/commands/backup-verify.test.ts src/infra/state-migrations.legacy-session-store.test.ts src/test-utils/openclaw-test-state.test.ts src/agents/sessions/windows-git-bash-path.test.ts src/agents/bash-tools.exec.script-preflight.test.ts src/process/exec.windows.test.ts src/process/exec.windows.integration.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts src/node-host/invoke-system-run-allowlist.test.ts src/auto-reply/usage-bar/template.windows.test.ts src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts src/media-understanding/attachments.file-url.windows.test.ts src/utils.test.ts src/commands/agents.commands.list.test.ts src/cli/daemon-cli/status.print.test.ts src/cli/mcp-cli.path-case.windows.test.ts packages/terminal-core/src/display-string.test.ts src/agents/sandbox/fs-paths.test.ts src/agents/sessions/tools/render-utils.test.ts src/daemon/schtasks.startup-fallback.test.ts src/media/web-media.file-url.windows.test.ts extensions/lobster/src/lobster-runner.test.ts extensions/msteams/src/media-helpers.test.ts extensions/msteams/src/messenger.test.ts extensions/mxc/test/mxc-backend.test.ts extensions/mxc/test/sandbox-policy-loader.test.ts test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts test/scripts/direct-run-entrypoints.test.ts test/scripts/format-generated-module.test.ts test/scripts/npm-runner.test.ts test/scripts/openclaw-cross-os-installer.windows.test.ts test/scripts/openclaw-cross-os-release-workflow.test.ts test/scripts/pnpm-runner.test.ts test/scripts/run-with-env.test.ts test/scripts/ts-topology.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts", "test:windows:schtasks:integration": "node --import tsx scripts/run-with-env.mts CI_WINDOWS_SCHTASKS_INTEGRATION=1 OPENCLAW_E2E_VERBOSE=1 OPENCLAW_VITEST_MAX_WORKERS=1 -- node scripts/run-vitest.mjs src/daemon/schtasks.integration.e2e.test.ts", "tool-display:check": "node --import tsx scripts/tool-display.ts --check", "tool-display:write": "node --import tsx scripts/tool-display.ts --write", diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index c5ac48fb6d9c..d140346d37ee 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -62,9 +62,9 @@ const WINDOWS_SCOPE_RE = const WINDOWS_TEST_SCOPE_RE = /^(extensions\/mxc\/test\/(?:mxc-backend|sandbox-policy-loader)\.test\.ts$|src\/agents\/bash-tools\.exec\.script-preflight\.test\.ts$|src\/config\/sessions\/(?:session-accessor\.sqlite-archive\.worker|store\.session-lifecycle-mutation)\.test\.ts$|src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/(?:exec-allowlist-pattern|fs-safe-remove|ssh-client\.windows|update-managed-service-handoff-(?:command|lifecycle)|windows-install-roots)\.test\.ts$|src\/shared\/runtime-import\.test\.ts$|src\/state\/openclaw-database-paths\.windows\.test\.ts$|src\/test-utils\/openclaw-test-state\.test\.ts$|test\/scripts\/(?:direct-run-entrypoints|format-generated-module|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/; const WINDOWS_SECRETREF_SCOPE_RE = - /^(?:src\/commands\/doctor-gateway-auth-token(?:\.windows\.test)?\.ts|src\/flows\/(?:doctor-core-checks|doctor-health-contributions)\.ts|src\/gateway\/(?:auth-token-resolution|resolve-configured-secret-input-string)\.ts|src\/infra\/(?:fs-safe|fs-safe-defaults|permissions)\.ts|src\/secrets\/(?:resolve|resolve-errors|test-node-command\.test-support)\.ts|src\/security\/audit-fs\.ts|src\/test-utils\/vitest-spies\.ts|test\/e2e\/qa-lab\/runtime\/doctor-auth-secretref-checks\.e2e\.test\.ts|test\/fixtures\/windows-acl-tools-unavailable\.mjs)$/; + /^(?:src\/commands\/doctor-gateway-auth-token(?:\.windows\.test)?\.ts|src\/flows\/(?:doctor-core-checks|doctor-health-contributions)\.ts|src\/gateway\/(?:auth-token-resolution|resolve-configured-secret-input-string)\.ts|src\/infra\/(?:fs-safe|fs-safe-defaults|permissions)\.ts|src\/secrets\/(?:resolve|resolve-errors)\.ts|src\/security\/audit-fs\.ts)$/; const WINDOWS_SECRETREF_TEST_SCOPE_RE = - /^(?:src\/commands\/doctor-gateway-auth-token\.windows\.test\.ts|src\/secrets\/test-node-command\.test-support\.ts|test\/e2e\/qa-lab\/runtime\/doctor-auth-secretref-checks\.e2e\.test\.ts|test\/fixtures\/windows-acl-tools-unavailable\.mjs)$/; + /^src\/commands\/doctor-gateway-auth-token\.windows\.test\.ts$/; const WINDOWS_DAEMON_SCOPE_RE = /^src\/daemon\/(?:schtasks(?:[-.][^/]+)?|runtime-hints\.windows-paths(?:\.test)?|test-helpers\/schtasks-(?:base-mocks|fixtures))\.ts$/; const WINDOWS_USAGE_TEMPLATE_SCOPE_RE = diff --git a/scripts/lib/ci-node-test-plan.mts b/scripts/lib/ci-node-test-plan.mts index 427d3f8247b5..8a08e1693f52 100644 --- a/scripts/lib/ci-node-test-plan.mts +++ b/scripts/lib/ci-node-test-plan.mts @@ -98,11 +98,10 @@ const COMPACT_EMBEDDED_GROUP_NAMES = [ const MAX_BUNDLED_NODE_TEST_PATTERNS = 64; // PR-only bundles trade a little serial work for fewer ephemeral runner registrations. // Keep runner classes and subprocess isolation intact while bounding each combined job. -// The group hints below are loaded-fleet CI walls. The 190s cap forbids -// pairings like core-runtime-media-ui (124) + -// core-unit-src-security (95) that produced a 195s real-wall straggler bin -// while the pack sat at ~160s; ~3 extra bins buy ~30-40s of run wall. -const COMPACT_NODE_TEST_JOB_SECONDS = 190; +// The group hints below are loaded-fleet CI walls. The 220s cap keeps the +// established 24-worker compact matrix after refreshing underestimated groups; +// expanded composite groups are then striped evenly across those jobs. +const COMPACT_NODE_TEST_JOB_SECONDS = 220; const COMPACT_NODE_TEST_JOB_GROUPS = 10; const COMPACT_TOOLING_NODE_TEST_GROUPS = 4; const COMPACT_WHOLE_NODE_TEST_TIMEOUT_MINUTES = 120; @@ -116,6 +115,7 @@ const UNIT_FAST_NODE_TEST_STRIPES = 2; // dropping cache-warm/contention outliers outside [median/1.5, median*1.5]. // Packing only: a stale entry skews job balance but never correctness. // Unknown shards fall back to a per-file estimate. +// Four outlier hints were refreshed from child-process walls in run 31450296338. const COMPACT_GROUP_SECONDS_HINTS = new Map([ ["agentic-agents-core-auth", 27], ["agentic-agents-core-isolated", 9], @@ -133,7 +133,7 @@ const COMPACT_GROUP_SECONDS_HINTS = new Map([ ["agentic-agents-core-runner-commands", 27], ["agentic-agents-core-runner-embedded", 20], ["agentic-agents-core-runner-sessions", 13], - ["agentic-agents-core-runtime", 79], + ["agentic-agents-core-runtime", 130], ["agentic-agents-core-subagents", 32], ["agentic-agents-core-tools", 52], // The composite hint sets the existing job count before its independent @@ -179,7 +179,7 @@ const COMPACT_GROUP_SECONDS_HINTS = new Map([ ["auto-reply-core-top-level", 30], ["auto-reply-reply-agent-runner", 40], ["auto-reply-reply-commands-1", 24], - ["auto-reply-reply-commands-2", 10], + ["auto-reply-reply-commands-2", 18], ["auto-reply-reply-commands-3", 12], ["auto-reply-reply-dispatch", 40], ["auto-reply-reply-session", 19], @@ -212,7 +212,7 @@ const COMPACT_GROUP_SECONDS_HINTS = new Map([ ["core-tooling-4", 125], ["core-tooling-isolated", 49], ["core-unit-fast-1", 41], - ["core-unit-fast-2", 35], + ["core-unit-fast-2", 92], // Fork-per-file isolation parallelizes poorly on 4 vCPU; keep it on the // 8 vCPU class, where it still runs a measured ~90s under fleet load. ["core-unit-fast-isolated", 90], diff --git a/scripts/test-projects.test-support.mts b/scripts/test-projects.test-support.mts index 7bf7e7d1cd2b..a21477c3ad7b 100644 --- a/scripts/test-projects.test-support.mts +++ b/scripts/test-projects.test-support.mts @@ -390,9 +390,12 @@ const RUNTIME_CONFIG_VITEST_CONFIG = "test/vitest/vitest.runtime-config.config.t const SECRETS_VITEST_CONFIG = "test/vitest/vitest.secrets.config.ts"; const SHARED_CORE_VITEST_CONFIG = "test/vitest/vitest.shared-core.config.ts"; const TASKS_VITEST_CONFIG = "test/vitest/vitest.tasks.config.ts"; +const PACKAGE_DOCKER_VITEST_CONFIG = "test/vitest/vitest.package-docker.config.ts"; const TOOLING_DOCKER_VITEST_CONFIG = "test/vitest/vitest.tooling-docker.config.ts"; const TOOLING_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.tooling-isolated.config.ts"; const TOOLING_VITEST_CONFIG = "test/vitest/vitest.tooling.config.ts"; +const PACKAGE_DOCKER_TEST_TARGET = + "test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts"; const TOOLING_DOCKER_TEST_TARGET = "test/scripts/docker-build-helper.test.ts"; const BROAD_TOOLING_SCRIPT_TEST_PATTERNS = new Set([ "test/scripts/**/*.test.ts", @@ -439,6 +442,7 @@ const VITEST_CONFIG_BY_KIND: Record = { daemon: DAEMON_VITEST_CONFIG, media: MEDIA_VITEST_CONFIG, logging: LOGGING_VITEST_CONFIG, + packageDocker: PACKAGE_DOCKER_VITEST_CONFIG, pluginSdkLight: PLUGIN_SDK_LIGHT_VITEST_CONFIG, pluginSdk: PLUGIN_SDK_VITEST_CONFIG, process: PROCESS_VITEST_CONFIG, @@ -3119,6 +3123,9 @@ function classifyTarget(arg: string, cwd: string) { if (isControlUiE2eTarget(relative)) { return "uiE2e"; } + if (relative === PACKAGE_DOCKER_TEST_TARGET) { + return "packageDocker"; + } if (isUiIsolatedTestFile(relative)) { return "uiIsolated"; } @@ -3642,6 +3649,7 @@ export function buildVitestRunPlans( const config = VITEST_CONFIG_BY_KIND[kind] ?? DEFAULT_VITEST_CONFIG; const useCliTargetArgs = kind === "e2e" || + kind === "packageDocker" || (kind === "default" && grouped.every((targetArg) => isFileLikeTarget(toRepoRelativeTarget(targetArg, cwd)))); const useWholeConfigTarget = grouped.some((targetArg) => diff --git a/src/scripts/ci-changed-scope.windows.test.ts b/src/scripts/ci-changed-scope.windows.test.ts index 7d08af6eaaf6..422ba45a9627 100644 --- a/src/scripts/ci-changed-scope.windows.test.ts +++ b/src/scripts/ci-changed-scope.windows.test.ts @@ -216,7 +216,7 @@ describe("detectChangedScope Windows routing", () => { } }); - it("routes SecretRef path-security changes and native fixtures to Windows", () => { + it("routes SecretRef path-security changes and focused owner coverage to Windows", () => { for (const secretRefPath of [ "src/commands/doctor-gateway-auth-token.ts", "src/commands/doctor-gateway-auth-token.windows.test.ts", @@ -229,11 +229,7 @@ describe("detectChangedScope Windows routing", () => { "src/infra/permissions.ts", "src/secrets/resolve-errors.ts", "src/secrets/resolve.ts", - "src/secrets/test-node-command.test-support.ts", "src/security/audit-fs.ts", - "src/test-utils/vitest-spies.ts", - "test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts", - "test/fixtures/windows-acl-tools-unavailable.mjs", ]) { expect(detectChangedScope([secretRefPath]), secretRefPath).toMatchObject({ runNode: true, @@ -242,10 +238,11 @@ describe("detectChangedScope Windows routing", () => { } }); - it("does not route SecretRef unit tests omitted from the Windows shard", () => { + it("does not route SecretRef tests owned by non-Windows lanes", () => { for (const testPath of [ "src/gateway/resolve-configured-secret-input-string.test.ts", "src/secrets/resolve.test.ts", + "test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts", ]) { expect(detectChangedScope([testPath]).runWindows, testPath).toBe(false); } diff --git a/src/scripts/test-projects.test.ts b/src/scripts/test-projects.test.ts index e99ad449a428..593a7a31914b 100644 --- a/src/scripts/test-projects.test.ts +++ b/src/scripts/test-projects.test.ts @@ -652,6 +652,19 @@ describe("test-projects args", () => { ]); }); + it("routes the Docker package contract without private-QA E2E setup", () => { + const target = "test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts"; + + expect(buildVitestRunPlans([target])).toEqual([ + { + config: "test/vitest/vitest.package-docker.config.ts", + forwardedArgs: [target], + includePatterns: null, + watchMode: false, + }, + ]); + }); + it("routes direct Discord extension file targets to the Discord config", () => { expect( buildVitestRunPlans(["extensions/discord/src/monitor/message-handler.preflight.test.ts"]), diff --git a/src/test-utils/vitest-spies.ts b/src/test-utils/vitest-spies.ts index 3c4555aca40f..05384f9c5b18 100644 --- a/src/test-utils/vitest-spies.ts +++ b/src/test-utils/vitest-spies.ts @@ -67,7 +67,6 @@ const WINDOWS_ACL_ENV_KEYS = new Set([ "systemroot", "windir", ]); -const NODE_OPTIONS_ENV_KEY = "node_options"; function takeWindowsAclEnvSnapshot(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { return Object.fromEntries( @@ -106,22 +105,6 @@ function forceWindowsAclVerificationUnavailable( env.WINDIR = missingSystemRoot; } -export function forceNativeWindowsAclToolsUnavailable( - env: NodeJS.ProcessEnv, - preloadUrl: string, -): void { - forceFsSafeNativeFallback(env); - let existingNodeOptions: string | undefined; - for (const key of Object.keys(env)) { - if (key.toLowerCase() !== NODE_OPTIONS_ENV_KEY) { - continue; - } - existingNodeOptions ??= env[key]; - delete env[key]; - } - env.NODE_OPTIONS = [existingNodeOptions, `--import=${preloadUrl}`].filter(Boolean).join(" "); -} - export function withMockedWindowsAclVerificationUnavailable( missingSystemRoot: string, run: () => Promise, diff --git a/test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts b/test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts index 7767c266cd7e..121ba8c03df3 100644 --- a/test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts +++ b/test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts @@ -1,14 +1,10 @@ // QA Lab product proof for doctor gateway auth and SecretRef behavior. -import { execFile } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; -import { pathToFileURL } from "node:url"; -import { promisify } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; import { stripAnsiSequences } from "../../../../packages/terminal-core/src/ansi.js"; import type { OpenClawConfig } from "../../../../src/config/types.openclaw.js"; import { withSecureTestNodeCommand } from "../../../../src/secrets/test-node-command.test-support.js"; -import { forceNativeWindowsAclToolsUnavailable } from "../../../../src/test-utils/vitest-spies.js"; import { createOpenClawTestInstance, type OpenClawTestInstance, @@ -16,11 +12,10 @@ import { let instance: OpenClawTestInstance | undefined; type GatewayToken = NonNullable["auth"]>["token"]; -const execFileAsync = promisify(execFile); const DOCTOR_CLI_TIMEOUT_MS = 120_000; const DOCTOR_CLI_CALL_COUNT = 6; // Entry-point preparation can precede the first CLI timeout; reserve one more -// command budget for instance, config, and fixture setup across the scenario. +// command budget for instance and config setup across the scenario. const DOCTOR_SCENARIO_TIMEOUT_MS = DOCTOR_CLI_TIMEOUT_MS * (DOCTOR_CLI_CALL_COUNT + 2); afterEach(async () => { @@ -55,25 +50,9 @@ function localGatewayConfig(token?: GatewayToken): OpenClawConfig { }; } -async function expectAclFixturePreservesExecFileContract(preloadUrl: string): Promise { - const probe = [ - 'import { execFile } from "node:child_process";', - 'import { promisify } from "node:util";', - 'const promise = promisify(execFile)(process.execPath, ["--version"], { encoding: "utf8" });', - 'if (!promise.child || typeof promise.child.kill !== "function") process.exit(2);', - "const result = await promise;", - 'if (!result || typeof result.stdout !== "string" || typeof result.stderr !== "string") process.exit(3);', - 'process.stdout.write("ok");', - ].join(""); - const result = await execFileAsync( - process.execPath, - [`--import=${preloadUrl}`, "--input-type=module", "--eval", probe], - { encoding: "utf8" }, - ); - expect(result).toEqual({ stdout: "ok", stderr: "" }); -} - -describe("doctor auth and SecretRef product proof", () => { +// Windows ACL failure diagnostics are owned by focused resolver and Doctor +// tests; this broad process proof owns the platform-neutral product flow. +describe.skipIf(process.platform === "win32")("doctor auth and SecretRef product proof", () => { it( "preserves SecretRef ownership while proving resolution, fallback, exec gating, and token generation", { timeout: DOCTOR_SCENARIO_TIMEOUT_MS }, @@ -131,14 +110,6 @@ describe("doctor auth and SecretRef product proof", () => { }; expect(unresolvedConfig.gateway?.auth?.token).toEqual(unresolvedRef); - const aclFixtureUrl = pathToFileURL( - path.resolve("test/fixtures/windows-acl-tools-unavailable.mjs"), - ).href; - await expectAclFixturePreservesExecFileContract(aclFixtureUrl); - if (process.platform === "win32") { - forceNativeWindowsAclToolsUnavailable(instance.env, aclFixtureUrl); - } - const filePath = path.join(instance.stateDir, "doctor-file-secretref.json"); const fileSecret = "qa-file-token"; await fs.writeFile(filePath, JSON.stringify({ gateway: { token: fileSecret } }), { @@ -165,14 +136,7 @@ describe("doctor auth and SecretRef product proof", () => { ); expect(fileResult.code).toBe(0); const fileOutput = normalizedOutputOf(fileResult); - if (process.platform === "win32") { - expect(fileOutput).toMatch( - /Gateway token SecretRef could not be resolved: .*Windows path security could not be verified\. Restore Windows path security verification, or use an existing secret file whose owner and ACLs OpenClaw can verify\./, - ); - expect(fileOutput).not.toContain(filePath); - } else { - expect(fileOutput).not.toContain("Gateway token SecretRef could not be resolved"); - } + expect(fileOutput).not.toContain("Gateway token SecretRef could not be resolved"); expect(fileOutput).not.toContain(fileSecret); const execMarker = path.join(instance.stateDir, "doctor-exec-secretref.marker"); @@ -216,16 +180,7 @@ describe("doctor auth and SecretRef product proof", () => { ); expect(execAllowed.code).toBe(0); const execAllowedOutput = normalizedOutputOf(execAllowed); - if (process.platform === "win32") { - expect(execAllowedOutput).toMatch( - /Gateway token SecretRef could not be resolved: .*Windows path security could not be verified\. Restore Windows path security verification, or use an existing provider command whose owner and ACLs OpenClaw can verify\./, - ); - expect(execAllowedOutput).not.toContain(command); - expect(execAllowedOutput).not.toContain(execMarker); - await expect(fs.access(execMarker)).rejects.toThrow(); - } else { - await expect(fs.readFile(execMarker, "utf8")).resolves.toBe("executed"); - } + await expect(fs.readFile(execMarker, "utf8")).resolves.toBe("executed"); expect(execAllowedOutput).not.toContain("qa-exec-token"); }); @@ -255,10 +210,8 @@ describe("doctor auth and SecretRef product proof", () => { unresolvedRefPreserved: true, ambientFallbackRejected: true, execRefGated: true, - execRefAllowed: process.platform !== "win32", - execRefWindowsAclBlocked: process.platform === "win32", - fileRefAllowed: process.platform !== "win32", - fileRefWindowsAclBlocked: process.platform === "win32", + execRefAllowed: true, + fileRefAllowed: true, generatedTokenPersisted: true, })}`, ); diff --git a/test/fixtures/windows-acl-tools-unavailable.mjs b/test/fixtures/windows-acl-tools-unavailable.mjs deleted file mode 100644 index d89fc8a8a0a2..000000000000 --- a/test/fixtures/windows-acl-tools-unavailable.mjs +++ /dev/null @@ -1,41 +0,0 @@ -import childProcess from "node:child_process"; -import { syncBuiltinESMExports } from "node:module"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { promisify } from "node:util"; - -const WINDOWS_ACL_TOOLS = new Set(["icacls.exe", "powershell.exe", "whoami.exe"]); -const realExecFile = childProcess.execFile; -const missingToolPath = fileURLToPath(new URL("missing-windows-acl-tool.exe", import.meta.url)); - -function fixtureExecFile(command, ...args) { - if (!WINDOWS_ACL_TOOLS.has(path.win32.basename(String(command)).toLowerCase())) { - return realExecFile.call(this, command, ...args); - } - return realExecFile.call(this, missingToolPath, ...args); -} - -Object.defineProperty(fixtureExecFile, promisify.custom, { - value: function promisifiedExecFile(...args) { - let child; - const promise = new Promise((resolve, reject) => { - child = fixtureExecFile(...args, (error, stdout, stderr) => { - if (error) { - error.stdout = stdout; - error.stderr = stderr; - reject(error); - return; - } - resolve({ stdout, stderr }); - }); - }); - promise.child = child; - return promise; - }, - configurable: false, - enumerable: false, - writable: false, -}); - -childProcess.execFile = fixtureExecFile; -syncBuiltinESMExports(); diff --git a/test/package-scripts.test.ts b/test/package-scripts.test.ts index adab7a1955ac..cde47cf8cc64 100644 --- a/test/package-scripts.test.ts +++ b/test/package-scripts.test.ts @@ -197,12 +197,6 @@ describe("package scripts", () => { ); }); - it("runs Doctor SecretRef ACL coverage in Windows CI", () => { - expect(readPackageJson().scripts["test:windows:ci"]).toContain( - "test/e2e/qa-lab/runtime/doctor-auth-secretref-checks.e2e.test.ts", - ); - }); - it("runs the Doctor managed-service SecretRef renderer in Windows CI", () => { expect(readPackageJson().scripts["test:windows:ci"]).toContain( "src/commands/doctor-gateway-auth-token.windows.test.ts", diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index a78c41152cde..e86dd75f09d7 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -209,8 +209,8 @@ describe("scripts/lib/ci-node-test-plan.mts", () => { compact: true, }); - expect(compact.length).toBeGreaterThanOrEqual(12); - expect(compact.length).toBeLessThanOrEqual(28); + // Rebalancing may change ownership but must not add CI workers. + expect(compact).toHaveLength(24); expect(compact.every((shard) => Array.isArray(shard.groups))).toBe(true); expect(compact.every((shard) => shard.groups.length <= 10)).toBe(true); expect(compact.some((shard) => shard.requiresDist)).toBe(true); @@ -231,6 +231,13 @@ describe("scripts/lib/ci-node-test-plan.mts", () => { // Cheap stripes may legally co-locate in one bin; only existence matters. expect(jobOf("core-unit-fast-1")).toBeGreaterThanOrEqual(0); expect(jobOf("core-unit-fast-2")).toBeGreaterThanOrEqual(0); + const refreshedOutliers = [ + "agentic-agents-core-runtime", + "agentic-agents-support", + "auto-reply-reply-commands-2", + "core-unit-fast-2", + ]; + expect(new Set(refreshedOutliers.map(jobOf)).size).toBe(refreshedOutliers.length); // Spawn/signal-timing suites never mix with regular groups, and every // compact bin runs serially: overlapping Vitest runs flake timing- // sensitive tests on both runner classes. diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 1e7d6ca0fa9f..90942aceec1e 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -5478,8 +5478,8 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" "needs.preflight.outputs.run_ui_tests == 'true' && needs.preflight.outputs.compatibility_target != 'true'", ); expect(uiE2e["runs-on"]).not.toBe(ui["runs-on"]); - // Each Chromium worker keeps serial file ownership while all four shards - // together remain required by the aggregate CI gate. + // Three serial workers own Control UI files while the fourth owns browser + // extension E2E; all four remain required by the aggregate CI gate. expect(uiE2e["timeout-minutes"]).toBe(25); expect(uiE2e.env).toEqual({ OPENCLAW_UI_E2E_SKIP_REAL_GATEWAY: "1" }); expect(uiE2e.strategy).toEqual({ @@ -5624,8 +5624,9 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" uiE2e.steps.find((step: WorkflowStep) => step.name === "Test Control UI end-to-end"), "Control UI E2E suite", ); + expect(scenario.if).toBe("matrix.shard != 4"); expect(scenario.run).toBe( - "node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner --shard ${{ matrix.shard }}/4", + "node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner --shard ${{ matrix.shard }}/3", ); const browserExtension = expectDefined( uiE2e.steps.find( @@ -5633,7 +5634,7 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" ), "browser extension bootstrap E2E suite", ); - expect(browserExtension.if).toBe("matrix.shard == 1"); + expect(browserExtension.if).toBe("matrix.shard == 4"); expect(browserExtension.run).toBe("pnpm test:e2e:browser-extension"); for (const { job } of routedUiE2eJobs) { const jobContract = JSON.stringify(job); diff --git a/test/vitest/vitest.package-docker.config.ts b/test/vitest/vitest.package-docker.config.ts new file mode 100644 index 000000000000..ac8f46cb953c --- /dev/null +++ b/test/vitest/vitest.package-docker.config.ts @@ -0,0 +1,16 @@ +// The package-builder contract does not launch the packaged CLI or private QA. +// Keep its process tests on the E2E runtime without the unrelated dist build. +import { defineConfig } from "vitest/config"; +import e2eConfig from "./vitest.e2e.config.ts"; + +export default defineConfig({ + ...e2eConfig, + test: { + ...e2eConfig.test, + fileParallelism: false, + globalSetup: [], + include: ["test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts"], + maxWorkers: 1, + name: "package-docker", + }, +});