mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
improve: cut CI critical path without more workers (#121807)
* test(ci): remove redundant builds from critical path Amp-Thread-ID: https://ampcode.com/threads/T-019fee8d-665d-707b-a380-23f2a6a1ce03 Co-authored-by: Peter Steinberger <steipete@gmail.com> * docs(ci): format runner table Amp-Thread-ID: https://ampcode.com/threads/T-019fee8d-665d-707b-a380-23f2a6a1ce03 Co-authored-by: Peter Steinberger <steipete@gmail.com> --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
committed by
GitHub
parent
efe5d8d76e
commit
ebfd3ba154
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -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` |
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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<string, number>([
|
||||
["agentic-agents-core-auth", 27],
|
||||
["agentic-agents-core-isolated", 9],
|
||||
@@ -133,7 +133,7 @@ const COMPACT_GROUP_SECONDS_HINTS = new Map<string, number>([
|
||||
["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<string, number>([
|
||||
["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<string, number>([
|
||||
["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],
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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) =>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
@@ -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<T>(
|
||||
missingSystemRoot: string,
|
||||
run: () => Promise<T>,
|
||||
|
||||
@@ -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<NonNullable<OpenClawConfig["gateway"]>["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<void> {
|
||||
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,
|
||||
})}`,
|
||||
);
|
||||
|
||||
-41
@@ -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();
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user