mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
3 Commits
v2026.5.2
...
fix/ci-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b0c5aea5 | ||
|
|
6edfb61d29 | ||
|
|
eb5caaf5b9 |
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -36,6 +36,7 @@ jobs:
|
||||
run_windows: ${{ steps.manifest.outputs.run_windows }}
|
||||
has_changed_extensions: ${{ steps.manifest.outputs.has_changed_extensions }}
|
||||
changed_extensions_matrix: ${{ steps.manifest.outputs.changed_extensions_matrix }}
|
||||
changed_paths_json: ${{ steps.manifest.outputs.changed_paths_json }}
|
||||
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
|
||||
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
|
||||
checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }}
|
||||
@@ -108,8 +109,16 @@ jobs:
|
||||
run: |
|
||||
node --input-type=module <<'EOF'
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { listChangedExtensionIds } from "./scripts/lib/changed-extensions.mjs";
|
||||
import {
|
||||
listChangedExtensionIds,
|
||||
listChangedPathsForScope,
|
||||
} from "./scripts/lib/changed-extensions.mjs";
|
||||
|
||||
const changedPaths = listChangedPathsForScope({
|
||||
base: process.env.BASE_SHA,
|
||||
head: "HEAD",
|
||||
fallbackBaseRef: process.env.BASE_REF,
|
||||
});
|
||||
const extensionIds = listChangedExtensionIds({
|
||||
base: process.env.BASE_SHA,
|
||||
head: "HEAD",
|
||||
@@ -117,9 +126,11 @@ jobs:
|
||||
unavailableBaseBehavior: "all",
|
||||
});
|
||||
const matrix = JSON.stringify({ include: extensionIds.map((extension) => ({ extension })) });
|
||||
const changedPathsJson = JSON.stringify(changedPaths);
|
||||
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `has_changed_extensions=${extensionIds.length > 0}\n`, "utf8");
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `changed_extensions_matrix=${matrix}\n`, "utf8");
|
||||
appendFileSync(process.env.GITHUB_OUTPUT, `changed_paths_json=${changedPathsJson}\n`, "utf8");
|
||||
EOF
|
||||
|
||||
- name: Build CI manifest
|
||||
@@ -135,6 +146,7 @@ jobs:
|
||||
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
|
||||
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
|
||||
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
|
||||
OPENCLAW_CI_CHANGED_PATHS_JSON: ${{ steps.changed_extensions.outputs.changed_paths_json || '[]' }}
|
||||
run: |
|
||||
node --input-type=module <<'EOF'
|
||||
import { appendFileSync } from "node:fs";
|
||||
@@ -203,6 +215,7 @@ jobs:
|
||||
run_windows: runWindows,
|
||||
has_changed_extensions: hasChangedExtensions,
|
||||
changed_extensions_matrix: changedExtensionsMatrix,
|
||||
changed_paths_json: process.env.OPENCLAW_CI_CHANGED_PATHS_JSON ?? "[]",
|
||||
run_build_artifacts: runNode,
|
||||
run_checks_fast: runNode,
|
||||
checks_fast_core_matrix: createMatrix(
|
||||
@@ -956,6 +969,8 @@ jobs:
|
||||
continue-on-error: true
|
||||
env:
|
||||
OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY: 4
|
||||
OPENCLAW_EXTENSION_BOUNDARY_CHANGED_EXTENSIONS_MATRIX: ${{ needs.preflight.outputs.changed_extensions_matrix }}
|
||||
OPENCLAW_EXTENSION_BOUNDARY_CHANGED_PATHS_JSON: ${{ needs.preflight.outputs.changed_paths_json }}
|
||||
run: pnpm run test:extensions:package-boundary
|
||||
|
||||
- name: Enforce safe external URL opening policy
|
||||
|
||||
@@ -28,6 +28,33 @@ const COMPILE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js",
|
||||
const ROOTDIR_BOUNDARY_CANARY_IMPORT_PATH =
|
||||
"../../src/plugins/contracts/rootdir-boundary-canary.ts";
|
||||
const ROOTDIR_BOUNDARY_CANARY_OUTPUT_HINT = "src/plugins/contracts/rootdir-boundary-canary.ts";
|
||||
const BOUNDARY_FULL_SWEEP_PATHS = new Set([
|
||||
"package.json",
|
||||
"pnpm-lock.yaml",
|
||||
"scripts/check-extension-package-tsc-boundary.mjs",
|
||||
"scripts/prepare-extension-package-boundary-artifacts.mjs",
|
||||
"scripts/write-plugin-sdk-entry-dts.ts",
|
||||
"scripts/lib/plugin-sdk-entries.mjs",
|
||||
"scripts/lib/plugin-sdk-entrypoints.json",
|
||||
"src/plugins/contracts/rootdir-boundary-canary.ts",
|
||||
"src/video-generation/dashscope-compatible.ts",
|
||||
"src/video-generation/types.ts",
|
||||
"tsconfig.json",
|
||||
"tsconfig.package-boundary.base.json",
|
||||
"tsconfig.plugin-sdk.dts.json",
|
||||
]);
|
||||
const BOUNDARY_FULL_SWEEP_PREFIXES = [
|
||||
"packages/plugin-sdk/",
|
||||
"src/channels/plugins/",
|
||||
"src/plugin-sdk/",
|
||||
"src/types/",
|
||||
];
|
||||
|
||||
function normalizeRepoPath(filePath) {
|
||||
return String(filePath ?? "")
|
||||
.replaceAll("\\", "/")
|
||||
.replace(/^\.\/+/, "");
|
||||
}
|
||||
|
||||
function parseMode(argv) {
|
||||
const modeArg = argv.find((arg) => arg.startsWith("--mode="));
|
||||
@@ -86,6 +113,9 @@ export function formatBoundaryCheckSuccessSummary(params = {}) {
|
||||
if (params.mode) {
|
||||
lines.push(`mode: ${params.mode}`);
|
||||
}
|
||||
if (params.scope) {
|
||||
lines.push(`scope: ${params.scope}`);
|
||||
}
|
||||
if (Number.isInteger(params.compileCount)) {
|
||||
lines.push(`compiled plugins: ${params.compileCount}`);
|
||||
}
|
||||
@@ -195,6 +225,97 @@ function collectCanaryExtensionIds(extensionIds) {
|
||||
];
|
||||
}
|
||||
|
||||
function isBoundaryFullSweepPath(filePath) {
|
||||
const normalizedPath = normalizeRepoPath(filePath);
|
||||
return (
|
||||
BOUNDARY_FULL_SWEEP_PATHS.has(normalizedPath) ||
|
||||
BOUNDARY_FULL_SWEEP_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix))
|
||||
);
|
||||
}
|
||||
|
||||
function parseChangedExtensionsMatrix(rawMatrix) {
|
||||
if (!rawMatrix || !rawMatrix.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawMatrix);
|
||||
if (!Array.isArray(parsed?.include)) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
...new Set(
|
||||
parsed.include
|
||||
.map((entry) => (typeof entry?.extension === "string" ? entry.extension : ""))
|
||||
.filter(Boolean),
|
||||
),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseChangedPathsJson(rawPaths) {
|
||||
if (!rawPaths || !rawPaths.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawPaths);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
return [...new Set(parsed.map((entry) => normalizeRepoPath(entry)).filter(Boolean))].toSorted();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveBoundaryCheckSelection(params = {}) {
|
||||
const optInExtensionIds = Array.isArray(params.optInExtensionIds)
|
||||
? [...params.optInExtensionIds]
|
||||
: [];
|
||||
const changedPaths = params.changedPaths;
|
||||
const changedExtensionIds = Array.isArray(params.changedExtensionIds)
|
||||
? [...params.changedExtensionIds]
|
||||
: [];
|
||||
const resolveCanaryIds = params.resolveCanaryExtensionIds ?? collectCanaryExtensionIds;
|
||||
|
||||
if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
|
||||
return {
|
||||
scope: "full",
|
||||
compileExtensionIds: optInExtensionIds,
|
||||
canaryExtensionIds: resolveCanaryIds(optInExtensionIds),
|
||||
};
|
||||
}
|
||||
|
||||
if (changedPaths.some((filePath) => isBoundaryFullSweepPath(filePath))) {
|
||||
return {
|
||||
scope: "full",
|
||||
compileExtensionIds: optInExtensionIds,
|
||||
canaryExtensionIds: resolveCanaryIds(optInExtensionIds),
|
||||
};
|
||||
}
|
||||
|
||||
const optInExtensionIdSet = new Set(optInExtensionIds);
|
||||
const scopedExtensionIds = changedExtensionIds
|
||||
.filter((extensionId) => optInExtensionIdSet.has(extensionId))
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
if (scopedExtensionIds.length === 0) {
|
||||
return {
|
||||
scope: "skip",
|
||||
compileExtensionIds: [],
|
||||
canaryExtensionIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scope: "scoped",
|
||||
compileExtensionIds: scopedExtensionIds,
|
||||
canaryExtensionIds: resolveCanaryIds(scopedExtensionIds),
|
||||
};
|
||||
}
|
||||
|
||||
function isRelevantCompileInput(filePath) {
|
||||
const basename = path.basename(filePath);
|
||||
if (
|
||||
@@ -768,7 +889,15 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
const startedAt = Date.now();
|
||||
const mode = parseMode(argv);
|
||||
const optInExtensionIds = collectOptInExtensionIds();
|
||||
const canaryExtensionIds = collectCanaryExtensionIds(optInExtensionIds);
|
||||
const selection = resolveBoundaryCheckSelection({
|
||||
optInExtensionIds,
|
||||
changedExtensionIds: parseChangedExtensionsMatrix(
|
||||
process.env.OPENCLAW_EXTENSION_BOUNDARY_CHANGED_EXTENSIONS_MATRIX,
|
||||
),
|
||||
changedPaths: parseChangedPathsJson(process.env.OPENCLAW_EXTENSION_BOUNDARY_CHANGED_PATHS_JSON),
|
||||
});
|
||||
const compileExtensionIds = selection.compileExtensionIds;
|
||||
const canaryExtensionIds = selection.canaryExtensionIds;
|
||||
const cleanupExtensionIds = optInExtensionIds;
|
||||
const shouldRunCanary = mode === "all" || mode === "canary";
|
||||
const releaseBoundaryLock = acquireBoundaryCheckLock();
|
||||
@@ -782,16 +911,17 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
|
||||
try {
|
||||
cleanupCanaryArtifactsForExtensions(cleanupExtensionIds);
|
||||
if (mode === "all" || mode === "compile") {
|
||||
if ((mode === "all" || mode === "compile") && compileExtensionIds.length > 0) {
|
||||
({ prepElapsedMs, compileCount, skippedCompileCount, compileElapsedMs, compileTimings } =
|
||||
await runCompileCheck(optInExtensionIds));
|
||||
await runCompileCheck(compileExtensionIds));
|
||||
}
|
||||
if (shouldRunCanary) {
|
||||
if (shouldRunCanary && canaryExtensionIds.length > 0) {
|
||||
({ canaryElapsedMs } = await runCanaryCheck(canaryExtensionIds));
|
||||
}
|
||||
process.stdout.write(
|
||||
formatBoundaryCheckSuccessSummary({
|
||||
mode,
|
||||
scope: selection.scope,
|
||||
compileCount,
|
||||
skippedCompileCount,
|
||||
canaryCount: shouldRunCanary ? canaryExtensionIds.length : 0,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { resolveBuildRequirement } from "./run-node.mjs";
|
||||
const DEFAULTS = {
|
||||
outputDir: path.join(process.cwd(), ".local", "gateway-watch-regression"),
|
||||
windowMs: 10_000,
|
||||
readyTimeoutMs: 20_000,
|
||||
readyTimeoutMs: 5_000,
|
||||
readySettleMs: 500,
|
||||
sigkillGraceMs: 10_000,
|
||||
cpuWarnMs: 1_000,
|
||||
@@ -34,6 +34,8 @@ const WATCH_GATEWAY_SKIP_ENV = {
|
||||
OPENCLAW_SKIP_GMAIL_WATCHER: "1",
|
||||
};
|
||||
|
||||
const ANSI_ESCAPE_PATTERN = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g");
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = { ...DEFAULTS };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
@@ -96,9 +98,54 @@ function normalizePath(filePath) {
|
||||
return filePath.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function listTreeEntries(rootName) {
|
||||
const rootPath = path.join(process.cwd(), rootName);
|
||||
if (!fs.existsSync(rootPath)) {
|
||||
function isMissingPathError(error) {
|
||||
return (
|
||||
Boolean(error) &&
|
||||
typeof error === "object" &&
|
||||
"code" in error &&
|
||||
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
||||
);
|
||||
}
|
||||
|
||||
function safeReaddirSync(fsImpl, dirPath) {
|
||||
try {
|
||||
return fsImpl.readdirSync(dirPath, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if (isMissingPathError(error)) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function safeLstatSync(fsImpl, filePath) {
|
||||
try {
|
||||
return fsImpl.lstatSync(filePath);
|
||||
} catch (error) {
|
||||
if (isMissingPathError(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function stripAnsi(text) {
|
||||
return String(text ?? "").replaceAll(ANSI_ESCAPE_PATTERN, "");
|
||||
}
|
||||
|
||||
export function hasGatewayReadyLog(text) {
|
||||
return stripAnsi(text).includes("[gateway] ready (");
|
||||
}
|
||||
|
||||
export function resolveReadyObservation(readyBeforeWindow, stdout, stderr) {
|
||||
return readyBeforeWindow || hasGatewayReadyLog(stdout) || hasGatewayReadyLog(stderr);
|
||||
}
|
||||
|
||||
export function listTreeEntries(rootName, params = {}) {
|
||||
const fsImpl = params.fs ?? fs;
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const rootPath = path.join(cwd, rootName);
|
||||
if (!fsImpl.existsSync(rootPath)) {
|
||||
return [`${rootName} (missing)`];
|
||||
}
|
||||
|
||||
@@ -109,10 +156,10 @@ function listTreeEntries(rootName) {
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
const dirents = fs.readdirSync(current, { withFileTypes: true });
|
||||
const dirents = safeReaddirSync(fsImpl, current);
|
||||
for (const dirent of dirents) {
|
||||
const fullPath = path.join(current, dirent.name);
|
||||
const relativePath = normalizePath(path.relative(process.cwd(), fullPath));
|
||||
const relativePath = normalizePath(path.relative(cwd, fullPath));
|
||||
entries.push(relativePath);
|
||||
if (dirent.isDirectory()) {
|
||||
queue.push(fullPath);
|
||||
@@ -135,10 +182,12 @@ function humanBytes(bytes) {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
|
||||
}
|
||||
|
||||
function snapshotTree(rootName) {
|
||||
const rootPath = path.join(process.cwd(), rootName);
|
||||
export function snapshotTree(rootName, params = {}) {
|
||||
const fsImpl = params.fs ?? fs;
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const rootPath = path.join(cwd, rootName);
|
||||
const stats = {
|
||||
exists: fs.existsSync(rootPath),
|
||||
exists: fsImpl.existsSync(rootPath),
|
||||
files: 0,
|
||||
directories: 0,
|
||||
symlinks: 0,
|
||||
@@ -156,11 +205,14 @@ function snapshotTree(rootName) {
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
const currentStats = fs.lstatSync(current);
|
||||
const currentStats = safeLstatSync(fsImpl, current);
|
||||
if (!currentStats) {
|
||||
continue;
|
||||
}
|
||||
stats.entries += 1;
|
||||
if (currentStats.isDirectory()) {
|
||||
stats.directories += 1;
|
||||
for (const dirent of fs.readdirSync(current, { withFileTypes: true })) {
|
||||
for (const dirent of safeReaddirSync(fsImpl, current)) {
|
||||
queue.push(path.join(current, dirent.name));
|
||||
}
|
||||
continue;
|
||||
@@ -312,13 +364,15 @@ function readProcessTreeCpuMs(rootPid) {
|
||||
|
||||
async function waitForGatewayReady(readText, timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (/\[gateway\] ready \(/.test(readText())) {
|
||||
while (true) {
|
||||
if (hasGatewayReadyLog(readText())) {
|
||||
return true;
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
return false;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function allocateLoopbackPort() {
|
||||
@@ -435,7 +489,7 @@ async function runTimedWatch(options, outputDir) {
|
||||
});
|
||||
|
||||
const exitPromise = new Promise((resolve) => {
|
||||
child.on("exit", (code, signal) => resolve({ code, signal }));
|
||||
child.on("close", (code, signal) => resolve({ code, signal }));
|
||||
});
|
||||
|
||||
let watchPid = null;
|
||||
@@ -447,7 +501,7 @@ async function runTimedWatch(options, outputDir) {
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
const readyBeforeWindow = await waitForGatewayReady(
|
||||
let readyBeforeWindow = await waitForGatewayReady(
|
||||
() => `${stdout}\n${stderr}`,
|
||||
options.readyTimeoutMs,
|
||||
);
|
||||
@@ -482,6 +536,7 @@ async function runTimedWatch(options, outputDir) {
|
||||
}
|
||||
|
||||
const exit = (await exitPromise) ?? { code: null, signal: null };
|
||||
readyBeforeWindow = resolveReadyObservation(readyBeforeWindow, stdout, stderr);
|
||||
fs.writeFileSync(stdoutPath, stdout, "utf8");
|
||||
fs.writeFileSync(stderrPath, stderr, "utf8");
|
||||
const timing = fs.existsSync(timeFilePath)
|
||||
@@ -559,11 +614,13 @@ function buildRunNodeDeps(env) {
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
export async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
ensureDir(options.outputDir);
|
||||
if (!options.skipBuild) {
|
||||
runCheckedCommand("pnpm", ["build"]);
|
||||
// This regression only needs runtime dist artifacts; plugin-sdk d.ts output
|
||||
// adds CI time without affecting watch startup behavior.
|
||||
runCheckedCommand("pnpm", ["build:ci-artifacts"]);
|
||||
// The watch harness must start from a completed-build baseline. Refresh
|
||||
// the build stamp after the full build pipeline finishes so run-node does
|
||||
// not spuriously rebuild inside the bounded watch window.
|
||||
@@ -697,4 +754,6 @@ async function main() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await main();
|
||||
if (import.meta.main) {
|
||||
await main();
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ function listChangedPaths(base, head = "HEAD") {
|
||||
.filter((line) => line.length > 0);
|
||||
}
|
||||
|
||||
export function listChangedPathsForScope(params = {}) {
|
||||
const head = params.head ?? "HEAD";
|
||||
return listChangedPaths(resolveChangedPathsBase(params), head);
|
||||
}
|
||||
|
||||
function hasExtensionPackage(extensionId) {
|
||||
return fs.existsSync(path.join(repoRoot, BUNDLED_PLUGIN_ROOT_DIR, extensionId, "package.json"));
|
||||
}
|
||||
@@ -122,8 +127,7 @@ export function listChangedExtensionIds(params = {}) {
|
||||
const unavailableBaseBehavior = params.unavailableBaseBehavior ?? "error";
|
||||
|
||||
try {
|
||||
const base = resolveChangedPathsBase(params);
|
||||
return detectChangedExtensionIds(listChangedPaths(base, head));
|
||||
return detectChangedExtensionIds(listChangedPathsForScope({ ...params, head }));
|
||||
} catch (error) {
|
||||
if (unavailableBaseBehavior === "all") {
|
||||
return listAvailableExtensionIds();
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
formatStepFailure,
|
||||
installCanaryArtifactCleanup,
|
||||
isBoundaryCompileFresh,
|
||||
resolveBoundaryCheckSelection,
|
||||
resolveBoundaryCheckLockPath,
|
||||
resolveCanaryArtifactPaths,
|
||||
runNodeStepAsync,
|
||||
@@ -165,6 +166,29 @@ describe("check-extension-package-tsc-boundary", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("adds the scope label when the boundary check was CI-scoped", () => {
|
||||
expect(
|
||||
formatBoundaryCheckSuccessSummary({
|
||||
mode: "all",
|
||||
scope: "scoped",
|
||||
compileCount: 2,
|
||||
skippedCompileCount: 0,
|
||||
canaryCount: 1,
|
||||
elapsedMs: 1_234,
|
||||
}),
|
||||
).toBe(
|
||||
[
|
||||
"extension package boundary check passed",
|
||||
"mode: all",
|
||||
"scope: scoped",
|
||||
"compiled plugins: 2",
|
||||
"canary plugins: 1",
|
||||
"elapsed: 1234ms",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
it("omits phase timings that never ran", () => {
|
||||
expect(
|
||||
formatBoundaryCheckSuccessSummary({
|
||||
@@ -220,6 +244,50 @@ describe("check-extension-package-tsc-boundary", () => {
|
||||
).toBe(["slowest plugin compiles:", "- slow: 900ms", "- medium: 250ms", ""].join("\n"));
|
||||
});
|
||||
|
||||
it("keeps the full sweep when shared plugin-sdk paths changed", () => {
|
||||
expect(
|
||||
resolveBoundaryCheckSelection({
|
||||
optInExtensionIds: ["browser", "matrix", "zalo"],
|
||||
changedExtensionIds: ["browser"],
|
||||
changedPaths: ["src/plugin-sdk/provider-entry.ts"],
|
||||
resolveCanaryExtensionIds: (extensionIds: string[]) => extensionIds.slice(0, 2),
|
||||
}),
|
||||
).toEqual({
|
||||
scope: "full",
|
||||
compileExtensionIds: ["browser", "matrix", "zalo"],
|
||||
canaryExtensionIds: ["browser", "matrix"],
|
||||
});
|
||||
});
|
||||
|
||||
it("scopes CI compiles to changed opt-in extensions when only extension-local paths changed", () => {
|
||||
expect(
|
||||
resolveBoundaryCheckSelection({
|
||||
optInExtensionIds: ["browser", "matrix", "zalo"],
|
||||
changedExtensionIds: ["browser", "matrix"],
|
||||
changedPaths: ["extensions/browser/src/runtime.ts", "docs/plugins/sdk-overview.md"],
|
||||
resolveCanaryExtensionIds: (extensionIds: string[]) => extensionIds,
|
||||
}),
|
||||
).toEqual({
|
||||
scope: "scoped",
|
||||
compileExtensionIds: ["browser", "matrix"],
|
||||
canaryExtensionIds: ["browser", "matrix"],
|
||||
});
|
||||
});
|
||||
|
||||
it("skips the boundary sweep when no opt-in extension or shared path changed", () => {
|
||||
expect(
|
||||
resolveBoundaryCheckSelection({
|
||||
optInExtensionIds: ["browser", "matrix", "zalo"],
|
||||
changedExtensionIds: ["docs-helper"],
|
||||
changedPaths: ["docs/reference/cli.md"],
|
||||
}),
|
||||
).toEqual({
|
||||
scope: "skip",
|
||||
compileExtensionIds: [],
|
||||
canaryExtensionIds: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("treats a plugin compile as fresh only when its outputs are newer than plugin and shared sdk inputs", () => {
|
||||
const { rootDir, extensionRoot } = createTempExtensionRoot();
|
||||
const extensionSourcePath = path.join(extensionRoot, "index.ts");
|
||||
|
||||
91
test/scripts/check-gateway-watch-regression.test.ts
Normal file
91
test/scripts/check-gateway-watch-regression.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
hasGatewayReadyLog,
|
||||
listTreeEntries,
|
||||
resolveReadyObservation,
|
||||
snapshotTree,
|
||||
stripAnsi,
|
||||
} from "../../scripts/check-gateway-watch-regression.mjs";
|
||||
|
||||
function createDirent(name: string, kind: "dir" | "file" | "symlink") {
|
||||
return {
|
||||
name,
|
||||
isDirectory: () => kind === "dir",
|
||||
isFile: () => kind === "file",
|
||||
isSymbolicLink: () => kind === "symlink",
|
||||
};
|
||||
}
|
||||
|
||||
describe("check-gateway-watch-regression", () => {
|
||||
it("detects the gateway ready line even when logs are ANSI-colorized", () => {
|
||||
const line =
|
||||
"\u001b[90m2026-04-17T16:47:21.723+00:00\u001b[39m \u001b[36m[gateway]\u001b[39m \u001b[36mready (5 plugins: acpx; 1.8s)\u001b[39m";
|
||||
|
||||
expect(stripAnsi(line)).toContain("[gateway] ready (5 plugins: acpx; 1.8s)");
|
||||
expect(hasGatewayReadyLog(line)).toBe(true);
|
||||
});
|
||||
|
||||
it("treats a buffered ready log as a successful ready observation", () => {
|
||||
expect(
|
||||
resolveReadyObservation(
|
||||
false,
|
||||
"2026-04-17T10:34:39.684-07:00 [gateway] ready (5 plugins: acpx; 3.9s)\n",
|
||||
"",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps missing trees explicit in snapshot path listings", () => {
|
||||
const entries = listTreeEntries("dist-runtime", {
|
||||
cwd: "/repo",
|
||||
fs: {
|
||||
existsSync: () => false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(entries).toEqual(["dist-runtime (missing)"]);
|
||||
});
|
||||
|
||||
it("ignores files that disappear between readdir and lstat while snapshotting", () => {
|
||||
const fakeFs = {
|
||||
existsSync(filePath: string) {
|
||||
return filePath === "/repo/dist";
|
||||
},
|
||||
readdirSync(filePath: string) {
|
||||
if (filePath === "/repo/dist") {
|
||||
return [createDirent("kept.js", "file"), createDirent("gone.js", "file")];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
lstatSync(filePath: string) {
|
||||
if (filePath === "/repo/dist") {
|
||||
return {
|
||||
isDirectory: () => true,
|
||||
isFile: () => false,
|
||||
isSymbolicLink: () => false,
|
||||
};
|
||||
}
|
||||
if (filePath === "/repo/dist/kept.js") {
|
||||
return {
|
||||
size: 7,
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
};
|
||||
}
|
||||
const error = new Error(`ENOENT: ${filePath}`) as NodeJS.ErrnoException;
|
||||
error.code = "ENOENT";
|
||||
throw error;
|
||||
},
|
||||
};
|
||||
|
||||
expect(snapshotTree("dist", { cwd: "/repo", fs: fakeFs })).toEqual({
|
||||
exists: true,
|
||||
files: 1,
|
||||
directories: 1,
|
||||
symlinks: 0,
|
||||
entries: 2,
|
||||
apparentBytes: 7,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user