refactor(ci): centralize changed-path facts (#104897)

This commit is contained in:
Vincent Koc
2026-07-12 10:26:32 +08:00
committed by GitHub
parent 68f0eee5ae
commit a5b70c5abe
7 changed files with 214 additions and 60 deletions
+17 -36
View File
@@ -2,18 +2,15 @@
import { execFileSync } from "node:child_process";
import { appendFileSync, existsSync, readFileSync } from "node:fs";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
export { normalizeChangedPath } from "./lib/changed-path-facts.mjs";
const GIT_OUTPUT_MAX_BUFFER = 64 * 1024 * 1024;
const IMPLAUSIBLE_NO_MERGE_BASE_DIFF_PATHS = 200;
const RAW_SYNC_CHANGED_LANES_ENV = "OPENCLAW_CHANGED_LANES_RAW_SYNC";
const DOCS_PATH_RE = /^(?:docs\/|README\.md$|AGENTS\.md$|.*\.mdx?$)/u;
const APP_PATH_RE = /^(?:apps\/|Swabble\/|appcast\.xml$)/u;
const EXTENSION_PATH_RE = /^extensions\/[^/]+(?:\/|$)/u;
const CORE_PATH_RE = /^(?:src\/|packages\/)/u;
const UI_PATH_RE = /^(?:ui\/|tsconfig\.ui\.json$)/u;
const SCRIPTS_TYPECHECK_PATH_RE =
/^(?:scripts\/.*\.(?:[cm]?ts|[cm]?tsx)|tsconfig\.scripts\.json)$/u;
// Keep aligned with tsconfig.strict-ratchet.json includes and its oxlint override.
@@ -38,11 +35,6 @@ export const STRICT_RATCHET_PACKAGE_DIRS = [
];
const TEST_ROOT_TYPECHECK_PATH_RE =
/^(?:test\/(?!fixtures\/).*\.(?:[cm]?ts|[cm]?tsx)|test\/tsconfig\/tsconfig\.test\.root\.json)$/u;
const TOOLING_PATH_RE =
/^(?:scripts\/|test\/vitest\/|\.github\/|\.vscode\/|config\/|deploy\/|git-hooks\/|Dockerfile\.sandbox(?:-(?:browser|common))?$|Makefile$|docker-setup\.sh$|setup-podman\.sh$|openclaw\.podman\.env$|skills\/pyproject\.toml$|vitest(?:\..+)?\.config\.ts$|tsconfig.*\.json$|\.dockerignore$|\.gitignore$|\.jscpd\.json$|\.npmignore$|\.pre-commit-config\.yaml$|\.swiftformat$|\.swiftlint\.yml$|\.oxlint.*|\.oxfmt.*)/u;
const ROOT_GLOBAL_PATH_RE =
/^(?:package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsdown\.config\.ts$|vitest\.config\.ts$)/u;
const LEGACY_ROOT_ASSET_PATH_RE = /^assets\//u;
export const LIVE_DOCKER_AUTH_SHELL_TARGETS = [
"scripts/lib/live-docker-auth.sh",
"scripts/test-live-acp-bind-docker.sh",
@@ -59,8 +51,6 @@ const LIVE_DOCKER_TOOLING_PATHS = new Set([
"src/gateway/live-agent-probes.test.ts",
]);
const LIVE_DOCKER_PACKAGE_SCRIPT_RE = /^test:docker:live-[\w:-]+$/u;
const TEST_PATH_RE =
/(?:^|\/)(?:test|__tests__)\/|(?:\.|\/)(?:test|spec|e2e|browser\.test)\.[cm]?[jt]sx?$/u;
const PUBLIC_EXTENSION_CONTRACT_RE =
/^(?:src\/plugin-sdk\/|src\/plugins\/contracts\/|src\/channels\/plugins\/|scripts\/lib\/plugin-sdk-entrypoints\.json$|scripts\/sync-plugin-sdk-exports\.mjs$|scripts\/generate-plugin-sdk-api-baseline\.ts$)/u;
/**
@@ -91,16 +81,6 @@ export const RELEASE_METADATA_PATHS = new Set([
* }} ChangedLaneResult
*/
/**
* Normalizes a changed file path into repo-relative POSIX form.
*/
export function normalizeChangedPath(inputPath) {
return String(inputPath ?? "")
.trim()
.replaceAll("\\", "/")
.replace(/^\.\/+/u, "");
}
/**
* Creates the default changed-lanes result object.
*/
@@ -124,7 +104,7 @@ export function createEmptyChangedLanes() {
}
export function isChangedLaneTestPath(changedPath) {
return TEST_PATH_RE.test(normalizeChangedPath(changedPath));
return getChangedPathFacts(normalizeChangedPath(changedPath)).isChangedLaneTest;
}
/**
@@ -160,7 +140,7 @@ export function detectChangedLanes(changedPaths, options = {}) {
paths.every((changedPath) => RELEASE_METADATA_PATHS.has(changedPath))
) {
lanes.releaseMetadata = true;
lanes.docs = paths.some((changedPath) => DOCS_PATH_RE.test(changedPath));
lanes.docs = paths.some((changedPath) => getChangedPathFacts(changedPath).surface === "docs");
for (const changedPath of paths) {
reasons.push(`${changedPath}: release metadata`);
}
@@ -168,6 +148,7 @@ export function detectChangedLanes(changedPaths, options = {}) {
}
for (const changedPath of paths) {
const facts = getChangedPathFacts(changedPath);
if (SCRIPTS_TYPECHECK_PATH_RE.test(changedPath)) {
lanes.scripts = true;
}
@@ -183,7 +164,7 @@ export function detectChangedLanes(changedPaths, options = {}) {
lanes.testRoot = true;
}
if (DOCS_PATH_RE.test(changedPath)) {
if (facts.surface === "docs") {
lanes.docs = true;
continue;
}
@@ -208,7 +189,7 @@ export function detectChangedLanes(changedPaths, options = {}) {
continue;
}
if (ROOT_GLOBAL_PATH_RE.test(changedPath)) {
if (facts.surface === "rootGlobal") {
lanes.all = true;
extensionImpactFromCore = true;
reasons.push(`${changedPath}: root config/package surface`);
@@ -225,8 +206,8 @@ export function detectChangedLanes(changedPaths, options = {}) {
continue;
}
if (EXTENSION_PATH_RE.test(changedPath)) {
if (isChangedLaneTestPath(changedPath)) {
if (facts.surface === "extension") {
if (facts.isChangedLaneTest) {
lanes.extensionTests = true;
reasons.push(`${changedPath}: extension test`);
} else {
@@ -237,8 +218,8 @@ export function detectChangedLanes(changedPaths, options = {}) {
continue;
}
if (CORE_PATH_RE.test(changedPath)) {
if (isChangedLaneTestPath(changedPath)) {
if (facts.surface === "source" || facts.surface === "package") {
if (facts.isChangedLaneTest) {
lanes.coreTests = true;
reasons.push(`${changedPath}: core test`);
} else {
@@ -249,8 +230,8 @@ export function detectChangedLanes(changedPaths, options = {}) {
continue;
}
if (UI_PATH_RE.test(changedPath)) {
if (isChangedLaneTestPath(changedPath)) {
if (facts.surface === "ui") {
if (facts.isChangedLaneTest) {
lanes.coreTests = true;
reasons.push(`${changedPath}: UI test`);
} else {
@@ -261,25 +242,25 @@ export function detectChangedLanes(changedPaths, options = {}) {
continue;
}
if (APP_PATH_RE.test(changedPath)) {
if (facts.surface === "app") {
lanes.apps = true;
reasons.push(`${changedPath}: app surface`);
continue;
}
if (changedPath.startsWith("test/") || changedPath.startsWith("test-fixtures/")) {
if (facts.surface === "rootTest" || facts.surface === "testFixture") {
lanes.tooling = true;
reasons.push(`${changedPath}: root test/support surface`);
continue;
}
if (TOOLING_PATH_RE.test(changedPath)) {
if (facts.surface === "rootTooling") {
lanes.tooling = true;
reasons.push(`${changedPath}: tooling surface`);
continue;
}
if (LEGACY_ROOT_ASSET_PATH_RE.test(changedPath)) {
if (facts.surface === "legacyRootAsset") {
lanes.tooling = true;
reasons.push(`${changedPath}: legacy root asset cleanup`);
continue;
+5 -4
View File
@@ -14,13 +14,12 @@ import { performance } from "node:perf_hooks";
import {
LIVE_DOCKER_AUTH_SHELL_TARGETS,
detectChangedLanesForPaths,
isChangedLaneTestPath,
listChangedPathsFromGit,
listStagedChangedPaths,
normalizeChangedPath,
} from "./changed-lanes.mjs";
import { shrinkwrapPackageDirsForChangedPaths } from "./generate-npm-shrinkwrap.mjs";
import { booleanFlag, parseFlagArgs, stringFlag } from "./lib/arg-utils.mjs";
import { getChangedPathFacts, normalizeChangedPath } from "./lib/changed-path-facts.mjs";
import { printTimingSummary } from "./lib/check-timing-summary.mjs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import {
@@ -220,7 +219,9 @@ export function shouldRunAppcastOwnerTest(paths) {
}
export function shouldRunTestTempCreationReport(paths) {
return paths.some((changedPath) => isChangedLaneTestPath(changedPath));
return paths.some(
(changedPath) => getChangedPathFacts(normalizeChangedPath(changedPath)).isChangedLaneTest,
);
}
export function createShrinkwrapGuardCommand(paths) {
@@ -439,7 +440,7 @@ export function createChangedCheckPlan(result, options = {}) {
}
if (
lanes.liveDockerTooling &&
result.paths.some((changedPath) => changedPath.startsWith("src/"))
result.paths.some((changedPath) => getChangedPathFacts(changedPath).surface === "source")
) {
addTypecheck("typecheck core tests", ["tsgo:core:test"]);
addLint("lint core", ["lint:core"]);
+15 -17
View File
@@ -1,6 +1,7 @@
// Determines CI scope from changed paths.
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { getChangedPathFacts } from "./lib/changed-path-facts.mjs";
import { isDirectRunUrl } from "./lib/direct-run.mjs";
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
@@ -32,7 +33,6 @@ const EMPTY_SCOPE = {
runUiTests: false,
};
const DOCS_PATH_RE = /^(docs\/|.*\.mdx?$)/;
const SKILLS_PYTHON_SCOPE_RE = /^(skills\/|skills\/pyproject\.toml$)/;
const INSTALL_SMOKE_WORKFLOW_SCOPE_RE = /^\.github\/workflows\/install-smoke\.yml$/;
const NATIVE_PROTOCOL_GEN_RE = /^apps\/shared\/OpenClawKit\/Sources\/OpenClawProtocol\//;
@@ -52,16 +52,12 @@ const WINDOWS_TEST_SCOPE_RE =
/^(src\/process\/(?:exec\.windows|windows-command)\.test\.ts$|src\/infra\/windows-install-roots\.test\.ts$|src\/shared\/runtime-import\.test\.ts$|test\/scripts\/(?:format-generated-module|npm-runner|openclaw-cross-os-release-workflow|pnpm-runner|ui|vitest-process-group)\.test\.ts$)/;
const WINDOWS_DAEMON_SCOPE_RE =
/^src\/daemon\/(?:schtasks(?:[-.][^/]+)?|runtime-hints\.windows-paths(?:\.test)?|test-helpers\/schtasks-(?:base-mocks|fixtures))\.ts$/;
const TEST_ONLY_PATH_RE =
/(^test\/|\/test\/|\/tests\/|(?:^|\/)[^/]+\.(?:test|spec|test-utils|test-support|test-harness|e2e-harness)\.[cm]?[jt]sx?$)/;
const CONTROL_UI_I18N_SCOPE_RE =
/^(ui\/src\/i18n\/|scripts\/control-ui-i18n\.ts$|\.github\/workflows\/control-ui-locale-refresh\.yml$)/;
const CONTROL_UI_TEST_SCOPE_RE =
/^(ui\/|test\/vitest\/vitest\.shared\.config\.ts$|scripts\/ensure-playwright-chromium\.mjs$)/;
const NATIVE_I18N_SCOPE_RE =
/^(?:apps\/\.i18n\/|apps\/android\/app\/src\/main\/|apps\/ios\/|apps\/macos\/Sources\/|apps\/shared\/OpenClawKit\/Sources\/|scripts\/(?:android-app-i18n|apple-app-i18n|native-app-i18n)\.ts$|test\/scripts\/(?:android-app-i18n|apple-app-i18n|native-app-i18n)\.test\.ts$|\.github\/workflows\/(?:ci|native-app-locale-refresh)\.yml$)/;
const NATIVE_ONLY_RE =
/^(apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/macos-mlx-tts\/|apps\/shared\/|apps\/swabble\/|Swabble\/|appcast\.xml$)/;
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)?|agents-delete-shared-workspace-docker\.sh|gateway-network-docker\.sh)$|extensions\/[^/]+\/(?:package\.json|openclaw\.plugin\.json)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
const FULL_INSTALL_SMOKE_SCOPE_RE =
@@ -71,7 +67,7 @@ const FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE =
const NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE =
/^src\/plugins\/contracts\/(?:inventory\/bundled-capability-metadata|registry|tts-contract-suites)\.ts$/;
const NODE_FAST_CI_ROUTING_SCOPE_RE =
/^(scripts\/(?:ci-changed-scope|check-changed|run-vitest|test-projects(?:\.test-support)?)\.mjs$|scripts\/test-projects\.test-support\.d\.mts$|src\/commands\/status\.scan-result\.test\.ts$|src\/scripts\/ci-changed-scope\.test\.ts$|test\/scripts\/(?:changed-lanes|run-vitest|test-projects)\.test\.ts$)/;
/^(scripts\/(?:ci-changed-scope|check-changed|run-vitest|test-projects(?:\.test-support)?)\.mjs$|scripts\/(?:test-projects\.test-support|lib\/changed-path-facts)\.d\.mts$|scripts\/lib\/changed-path-facts\.mjs$|src\/commands\/status\.scan-result\.test\.ts$|src\/scripts\/ci-changed-scope\.test\.ts$|test\/scripts\/(?:changed-lanes|changed-path-facts|run-vitest|test-projects)\.test\.ts$)/;
const NODE_FAST_SCOPE_RE = new RegExp(
`${NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE.source}|${NODE_FAST_CI_ROUTING_SCOPE_RE.source}`,
);
@@ -101,14 +97,15 @@ export function detectChangedScope(changedPaths) {
let hasNonNativeNonDocs = false;
for (const rawPath of changedPaths) {
const path = rawPath.trim();
const facts = getChangedPathFacts(rawPath);
const { path } = facts;
if (!path) {
continue;
}
const isAppleSwiftConfig = APPLE_SWIFT_CONFIG_RE.test(path);
if (DOCS_PATH_RE.test(path)) {
if (facts.surface === "docs") {
continue;
}
@@ -143,9 +140,7 @@ export function detectChangedScope(changedPaths) {
if (
(WINDOWS_SCOPE_RE.test(path) || WINDOWS_DAEMON_SCOPE_RE.test(path)) &&
(!TEST_ONLY_PATH_RE.test(path) ||
WINDOWS_TEST_SCOPE_RE.test(path) ||
WINDOWS_DAEMON_SCOPE_RE.test(path))
(!facts.isTestOnly || WINDOWS_TEST_SCOPE_RE.test(path) || WINDOWS_DAEMON_SCOPE_RE.test(path))
) {
runWindows = true;
}
@@ -162,7 +157,7 @@ export function detectChangedScope(changedPaths) {
runUiTests = true;
}
if (!NATIVE_ONLY_RE.test(path)) {
if (!facts.isNativeOnly) {
hasNonNativeNonDocs = true;
}
}
@@ -209,8 +204,9 @@ export function detectNodeFastScope(changedPaths) {
let runCiRouting = false;
for (const rawPath of changedPaths) {
const path = rawPath.trim();
if (!path || DOCS_PATH_RE.test(path)) {
const facts = getChangedPathFacts(rawPath);
const { path } = facts;
if (!path || facts.surface === "docs") {
continue;
}
@@ -236,11 +232,12 @@ export function detectNodeFastScope(changedPaths) {
* @returns {InstallSmokeScope}
*/
function detectInstallSmokeScopeForPath(path) {
const facts = getChangedPathFacts(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));
(FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE.test(path) && !facts.isTestOnly);
return { runFastInstallSmoke, runFullInstallSmoke };
}
@@ -259,8 +256,9 @@ export function detectInstallSmokeScope(changedPaths) {
let runFastInstallSmoke = false;
let runFullInstallSmoke = false;
for (const rawPath of changedPaths) {
const path = rawPath.trim();
if (!path || DOCS_PATH_RE.test(path)) {
const facts = getChangedPathFacts(rawPath);
const { path } = facts;
if (!path || facts.surface === "docs") {
continue;
}
const pathScope = detectInstallSmokeScopeForPath(path);
+24
View File
@@ -0,0 +1,24 @@
export type ChangedPathSurface =
| "docs"
| "source"
| "package"
| "ui"
| "extension"
| "app"
| "rootTest"
| "testFixture"
| "rootTooling"
| "rootGlobal"
| "legacyRootAsset"
| "unknown";
export type ChangedPathFacts = {
path: string;
surface: ChangedPathSurface;
isChangedLaneTest: boolean;
isTestOnly: boolean;
isNativeOnly: boolean;
};
export function normalizeChangedPath(inputPath: unknown): string;
export function getChangedPathFacts(inputPath: unknown): ChangedPathFacts;
+73
View File
@@ -0,0 +1,73 @@
const DOCS_PATH_RE = /^(?:docs\/|README\.md$|AGENTS\.md$|.*\.mdx?$)/u;
const APP_PATH_RE = /^(?:apps\/|Swabble\/|appcast\.xml$)/u;
const EXTENSION_PATH_RE = /^extensions\/[^/]+(?:\/|$)/u;
const SOURCE_PATH_RE = /^src\//u;
const PACKAGE_PATH_RE = /^packages\//u;
const UI_PATH_RE = /^(?:ui\/|tsconfig\.ui\.json$)/u;
const ROOT_TEST_PATH_RE = /^test\//u;
const TEST_FIXTURE_PATH_RE = /^test-fixtures\//u;
const ROOT_TOOLING_PATH_RE =
/^(?:scripts\/|test\/vitest\/|\.github\/|\.vscode\/|config\/|deploy\/|git-hooks\/|Dockerfile\.sandbox(?:-(?:browser|common))?$|Makefile$|docker-setup\.sh$|setup-podman\.sh$|openclaw\.podman\.env$|skills\/pyproject\.toml$|vitest(?:\..+)?\.config\.ts$|tsconfig.*\.json$|\.dockerignore$|\.gitignore$|\.jscpd\.json$|\.npmignore$|\.pre-commit-config\.yaml$|\.swiftformat$|\.swiftlint\.yml$|\.oxlint.*|\.oxfmt.*)/u;
const ROOT_GLOBAL_PATH_RE =
/^(?:package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsdown\.config\.ts$|vitest\.config\.ts$)/u;
const LEGACY_ROOT_ASSET_PATH_RE = /^assets\//u;
const CHANGED_LANE_TEST_PATH_RE =
/(?:^|\/)(?:test|__tests__)\/|(?:\.|\/)(?:test|spec|e2e|browser\.test)\.[cm]?[jt]sx?$/u;
const TEST_ONLY_PATH_RE =
/(^test\/|\/test\/|\/tests\/|(?:^|\/)[^/]+\.(?:test|spec|test-utils|test-support|test-harness|e2e-harness)\.[cm]?[jt]sx?$)/u;
const NATIVE_ONLY_PATH_RE =
/^(?:apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/macos-mlx-tts\/|apps\/shared\/|apps\/swabble\/|Swabble\/|appcast\.xml$)/u;
/**
* @typedef {"docs" | "source" | "package" | "ui" | "extension" | "app" | "rootTest" | "testFixture" | "rootTooling" | "rootGlobal" | "legacyRootAsset" | "unknown"} ChangedPathSurface
*/
/**
* Normalizes a changed file path into repo-relative POSIX form.
*/
export function normalizeChangedPath(inputPath) {
return String(inputPath ?? "")
.trim()
.replaceAll("\\", "/")
.replace(/^\.\/+/u, "");
}
/**
* Returns shared path facts without imposing a caller's lane-selection policy.
*/
export function getChangedPathFacts(inputPath) {
const path = String(inputPath ?? "").trim();
let surface = /** @type {ChangedPathSurface} */ ("unknown");
if (DOCS_PATH_RE.test(path)) {
surface = "docs";
} else if (ROOT_GLOBAL_PATH_RE.test(path)) {
surface = "rootGlobal";
} else if (EXTENSION_PATH_RE.test(path)) {
surface = "extension";
} else if (SOURCE_PATH_RE.test(path)) {
surface = "source";
} else if (PACKAGE_PATH_RE.test(path)) {
surface = "package";
} else if (UI_PATH_RE.test(path)) {
surface = "ui";
} else if (APP_PATH_RE.test(path)) {
surface = "app";
} else if (ROOT_TEST_PATH_RE.test(path)) {
surface = "rootTest";
} else if (TEST_FIXTURE_PATH_RE.test(path)) {
surface = "testFixture";
} else if (ROOT_TOOLING_PATH_RE.test(path)) {
surface = "rootTooling";
} else if (LEGACY_ROOT_ASSET_PATH_RE.test(path)) {
surface = "legacyRootAsset";
}
return {
path,
surface,
isChangedLaneTest: CHANGED_LANE_TEST_PATH_RE.test(path),
isTestOnly: TEST_ONLY_PATH_RE.test(path),
isNativeOnly: NATIVE_ONLY_PATH_RE.test(path),
};
}
+18 -3
View File
@@ -56,6 +56,7 @@ import {
detectChangedLanes,
listChangedPathsFromGit as listChangedPathsFromGitSource,
} from "./changed-lanes.mjs";
import { getChangedPathFacts } from "./lib/changed-path-facts.mjs";
import { isCiLikeEnv, resolveLocalFullSuiteProfile } from "./lib/vitest-local-scheduling.mjs";
import {
DEFAULT_VITEST_NO_OUTPUT_HEARTBEAT_MS,
@@ -3469,7 +3470,13 @@ function isRoutableChangedTarget(changedPath) {
if (changedPath.endsWith(".live.test.ts")) {
return false;
}
return /^(?:src|test|extensions|ui|packages)(?:\/|$)/u.test(changedPath);
const surface = getChangedPathFacts(changedPath).surface;
return (
["source", "package", "extension", "rootTest"].includes(surface) ||
changedPath === "ui" ||
changedPath.startsWith("ui/") ||
["src", "test", "extensions", "packages"].includes(changedPath)
);
}
function resolveSiblingTestTarget(changedPath, cwd) {
@@ -3523,7 +3530,15 @@ function resolvePreciseChangedTestTargets(changedPath, options) {
if (options.skipImportGraph === true) {
return null;
}
if (/^(?:src|test\/helpers|extensions|packages|ui\/src|ui\/config)\//u.test(changedPath)) {
const facts = getChangedPathFacts(changedPath);
if (
facts.surface === "source" ||
facts.surface === "package" ||
facts.surface === "extension" ||
changedPath.startsWith("test/helpers/") ||
changedPath.startsWith("ui/src/") ||
changedPath.startsWith("ui/config/")
) {
const affectedTests = resolveAffectedTestsFromImportGraph(changedPath, cwd, {
forceFull: options.forceFullImportGraph === true,
});
@@ -3671,7 +3686,7 @@ function classifyTarget(arg, cwd) {
if (relative === "extensions") {
return "extensionFull";
}
if (relative.startsWith("extensions/")) {
if (getChangedPathFacts(relative).surface === "extension") {
const extensionRoot = relative.split("/").slice(0, 2).join("/");
const splitChannelShard = resolveSplitChannelExtensionShard(extensionRoot);
if (splitChannelShard) {
+62
View File
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import {
getChangedPathFacts,
normalizeChangedPath,
} from "../../scripts/lib/changed-path-facts.mjs";
describe("changed path facts", () => {
it("preserves the existing surface classifications", () => {
const cases = [
["docs/ci.md", "docs"],
["README.md", "docs"],
["src/config/defaults.ts", "source"],
["packages/gateway-client/src/client.ts", "package"],
["extensions/slack/src/index.ts", "extension"],
["ui/src/app.ts", "ui"],
["apps/macos/Sources/OpenClaw/AppDelegate.swift", "app"],
["test/scripts/changed-lanes.test.ts", "rootTest"],
["test-fixtures/sample.ts", "testFixture"],
["scripts/check-changed.mjs", "rootTooling"],
[".github/workflows/ci.yml", "rootTooling"],
["package.json", "rootGlobal"],
["assets/legacy.png", "legacyRootAsset"],
[".crabbox.yaml", "unknown"],
] as const;
for (const [changedPath, surface] of cases) {
expect(getChangedPathFacts(changedPath).surface, changedPath).toBe(surface);
}
});
it("preserves test and native-only predicates independently from surfaces", () => {
expect(getChangedPathFacts("extensions/slack/src/index.test.ts")).toMatchObject({
surface: "extension",
isChangedLaneTest: true,
isTestOnly: true,
isNativeOnly: false,
});
expect(getChangedPathFacts("test/helpers/fixture.ts")).toMatchObject({
surface: "rootTest",
isChangedLaneTest: true,
isTestOnly: true,
isNativeOnly: false,
});
expect(getChangedPathFacts("apps/shared/OpenClawKit/Sources/Foo.swift")).toMatchObject({
surface: "app",
isChangedLaneTest: false,
isTestOnly: false,
isNativeOnly: true,
});
expect(getChangedPathFacts("apps/web/index.ts")).toMatchObject({
surface: "app",
isNativeOnly: false,
});
});
it("keeps normalization separate from classification", () => {
expect(normalizeChangedPath(" .\\extensions\\slack\\src\\index.test.ts ")).toBe(
"extensions/slack/src/index.test.ts",
);
expect(getChangedPathFacts("./src/config/defaults.ts").surface).toBe("unknown");
});
});