mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 09:01:39 -06:00
bf9b25ab7e
* fix(gateway): avoid repeated control-plane scans * fix(tooling): allow concurrent worktree validation * fix(ci): refresh protocol and runner inputs * perf(ui): defer hidden session refreshes * fix(ui): resolve session refresh lint failure * fix(update): preserve pre-cache update channel * fix(update): normalize cached update channel * fix(gateway): lifecycle-cache update install identity * fix(ui): preserve manual history retry after layout scroll * test(codex): repair side-question tool schema fixture
427 lines
13 KiB
TypeScript
427 lines
13 KiB
TypeScript
// Local Check Runtime tests cover local check runtime script behavior.
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
applyLocalOxlintPolicy,
|
|
applyLocalTsgoPolicy,
|
|
ensureRepoToolNodeModulesLink,
|
|
resolveLocalCheckEnv,
|
|
resolveRepoToolBinPath,
|
|
} from "../../scripts/lib/local-check-runtime.mts";
|
|
import { createScriptTestHarness } from "./test-helpers.js";
|
|
|
|
const { createTempDir } = createScriptTestHarness();
|
|
const GIB = 1024 ** 3;
|
|
const CONSTRAINED_HOST = {
|
|
totalMemoryBytes: 16 * GIB,
|
|
logicalCpuCount: 8,
|
|
};
|
|
const ROOMY_HOST = {
|
|
totalMemoryBytes: 128 * GIB,
|
|
logicalCpuCount: 16,
|
|
};
|
|
|
|
function makeEnv(overrides: Record<string, string | undefined> = {}) {
|
|
const env: NodeJS.ProcessEnv = {
|
|
...process.env,
|
|
OPENCLAW_LOCAL_CHECK: "1",
|
|
...overrides,
|
|
};
|
|
if (!Object.hasOwn(overrides, "OPENCLAW_LOCAL_CHECK_MODE")) {
|
|
delete env.OPENCLAW_LOCAL_CHECK_MODE;
|
|
}
|
|
if (!Object.hasOwn(overrides, "GITHUB_ACTIONS")) {
|
|
delete env.GITHUB_ACTIONS;
|
|
}
|
|
return env;
|
|
}
|
|
|
|
describe("local-check-runtime", () => {
|
|
it("resolves repo tools from the primary checkout for dependency-less worktrees", () => {
|
|
const primaryRoot = createTempDir("openclaw-primary-checkout-");
|
|
const cwd = path.join(primaryRoot, ".codex", "worktrees", "task", "openclaw");
|
|
const commonDir = path.join(primaryRoot, ".git");
|
|
const localPath = path.resolve(cwd, "node_modules", ".bin", "oxlint");
|
|
const primaryPath = path.join(primaryRoot, "node_modules", ".bin", "oxlint");
|
|
|
|
expect(
|
|
resolveRepoToolBinPath("oxlint", {
|
|
cwd,
|
|
fileExists: (candidate) => candidate === primaryPath,
|
|
resolveCommonDir: () => commonDir,
|
|
}),
|
|
).toBe(primaryPath);
|
|
expect(
|
|
resolveRepoToolBinPath("oxlint", {
|
|
cwd,
|
|
fileExists: (candidate) => candidate === localPath || candidate === primaryPath,
|
|
resolveCommonDir: () => commonDir,
|
|
}),
|
|
).toBe(localPath);
|
|
});
|
|
|
|
it("links dependency-less worktrees to the selected checkout's modules", () => {
|
|
const primaryRoot = createTempDir("openclaw-primary-toolchain-");
|
|
const cwd = path.join(primaryRoot, ".codex", "worktrees", "task", "openclaw");
|
|
const commonDir = path.join(primaryRoot, ".git");
|
|
const primaryTsgo = path.join(primaryRoot, "node_modules", ".bin", "tsgo");
|
|
const primaryNodeModules = path.join(primaryRoot, "node_modules");
|
|
const localNodeModules = path.join(cwd, "node_modules");
|
|
fs.mkdirSync(path.dirname(primaryTsgo), { recursive: true });
|
|
fs.mkdirSync(cwd, { recursive: true });
|
|
|
|
expect(
|
|
ensureRepoToolNodeModulesLink(primaryTsgo, {
|
|
cwd,
|
|
resolveCommonDir: () => commonDir,
|
|
}),
|
|
).toBe(localNodeModules);
|
|
expect(fs.realpathSync(localNodeModules)).toBe(fs.realpathSync(primaryNodeModules));
|
|
|
|
// The stable link is idempotent for concurrent and later local runners.
|
|
expect(
|
|
ensureRepoToolNodeModulesLink(primaryTsgo, {
|
|
cwd,
|
|
resolveCommonDir: () => commonDir,
|
|
}),
|
|
).toBe(localNodeModules);
|
|
});
|
|
|
|
it("leaves existing worktree node_modules directories locally owned", () => {
|
|
const primaryRoot = createTempDir("openclaw-primary-toolchain-");
|
|
const commonDir = path.join(primaryRoot, ".git");
|
|
const primaryTsgo = path.join(primaryRoot, "node_modules", ".bin", "tsgo");
|
|
const cwd = path.join(primaryRoot, "worktree");
|
|
const localNodeModules = path.join(cwd, "node_modules");
|
|
fs.mkdirSync(path.dirname(primaryTsgo), { recursive: true });
|
|
fs.mkdirSync(localNodeModules, { recursive: true });
|
|
|
|
ensureRepoToolNodeModulesLink(primaryTsgo, {
|
|
cwd,
|
|
resolveCommonDir: () => commonDir,
|
|
});
|
|
|
|
expect(fs.lstatSync(localNodeModules).isDirectory()).toBe(true);
|
|
expect(fs.lstatSync(localNodeModules).isSymbolicLink()).toBe(false);
|
|
});
|
|
|
|
it("reenables local check policy for local wrapper entrypoints", () => {
|
|
expect(resolveLocalCheckEnv({ OPENCLAW_LOCAL_CHECK: "0", PATH: "/usr/bin" })).toEqual({
|
|
OPENCLAW_LOCAL_CHECK: "1",
|
|
PATH: "/usr/bin",
|
|
});
|
|
expect(resolveLocalCheckEnv({ OPENCLAW_LOCAL_CHECK: "false", PATH: "/usr/bin" })).toEqual({
|
|
OPENCLAW_LOCAL_CHECK: "1",
|
|
PATH: "/usr/bin",
|
|
});
|
|
});
|
|
|
|
it("preserves local-check disablement in CI", () => {
|
|
expect(
|
|
resolveLocalCheckEnv({
|
|
CI: "true",
|
|
OPENCLAW_LOCAL_CHECK: "0",
|
|
PATH: "/usr/bin",
|
|
}),
|
|
).toEqual({
|
|
CI: "true",
|
|
OPENCLAW_LOCAL_CHECK: "0",
|
|
PATH: "/usr/bin",
|
|
});
|
|
});
|
|
|
|
it("tightens local tsgo runs on constrained hosts", () => {
|
|
const { args, env } = applyLocalTsgoPolicy([], makeEnv(), CONSTRAINED_HOST);
|
|
|
|
expect(args).toEqual([
|
|
"--declaration",
|
|
"false",
|
|
"--incremental",
|
|
"--tsBuildInfoFile",
|
|
".artifacts/tsgo-cache/root.tsbuildinfo",
|
|
"--singleThreaded",
|
|
"--checkers",
|
|
"1",
|
|
]);
|
|
expect(env.GOMAXPROCS).toBe("2");
|
|
expect(env.GOGC).toBe("30");
|
|
expect(env.GOMEMLIMIT).toBe("3GiB");
|
|
});
|
|
|
|
it("skips declaration transforms for no-emit tsgo checks", () => {
|
|
const { args } = applyLocalTsgoPolicy([], makeEnv({ OPENCLAW_LOCAL_CHECK: "0" }), ROOMY_HOST);
|
|
|
|
expect(args).toEqual(["--declaration", "false"]);
|
|
});
|
|
|
|
it("keeps explicit tsgo flags and Go env overrides intact when throttled", () => {
|
|
const { args, env } = applyLocalTsgoPolicy(
|
|
["--checkers", "4", "--singleThreaded", "--pprofDir", "/tmp/existing"],
|
|
makeEnv({
|
|
GOMAXPROCS: "3",
|
|
GOGC: "80",
|
|
GOMEMLIMIT: "5GiB",
|
|
OPENCLAW_TSGO_PPROF_DIR: "/tmp/profile",
|
|
}),
|
|
CONSTRAINED_HOST,
|
|
);
|
|
|
|
expect(args).toEqual([
|
|
"--checkers",
|
|
"4",
|
|
"--singleThreaded",
|
|
"--pprofDir",
|
|
"/tmp/existing",
|
|
"--declaration",
|
|
"false",
|
|
]);
|
|
expect(env.GOMAXPROCS).toBe("3");
|
|
expect(env.GOGC).toBe("80");
|
|
expect(env.GOMEMLIMIT).toBe("5GiB");
|
|
});
|
|
|
|
it("keeps explicit tsgo declaration flags intact", () => {
|
|
const env = makeEnv({ OPENCLAW_LOCAL_CHECK_MODE: "full" });
|
|
const longFlag = applyLocalTsgoPolicy(["--declaration"], env, ROOMY_HOST);
|
|
const shortFlag = applyLocalTsgoPolicy(["-d"], env, ROOMY_HOST);
|
|
|
|
expect(longFlag.args).toEqual(["--declaration"]);
|
|
expect(shortFlag.args).toEqual(["-d"]);
|
|
});
|
|
|
|
it("defaults local tsgo to full-speed mode on roomy hosts", () => {
|
|
const { args, env } = applyLocalTsgoPolicy([], makeEnv(), ROOMY_HOST);
|
|
|
|
expect(args).toEqual([
|
|
"--declaration",
|
|
"false",
|
|
"--incremental",
|
|
"--tsBuildInfoFile",
|
|
".artifacts/tsgo-cache/root.tsbuildinfo",
|
|
]);
|
|
expect(env.GOMAXPROCS).toBeUndefined();
|
|
expect(env.GOGC).toBeUndefined();
|
|
expect(env.GOMEMLIMIT).toBeUndefined();
|
|
});
|
|
|
|
it("uses the configured local tsgo build info file", () => {
|
|
const { args } = applyLocalTsgoPolicy(
|
|
[],
|
|
makeEnv({
|
|
OPENCLAW_LOCAL_CHECK_MODE: "full",
|
|
OPENCLAW_TSGO_BUILD_INFO_FILE: ".artifacts/custom/tsgo.tsbuildinfo",
|
|
}),
|
|
ROOMY_HOST,
|
|
);
|
|
|
|
expect(args).toEqual([
|
|
"--declaration",
|
|
"false",
|
|
"--incremental",
|
|
"--tsBuildInfoFile",
|
|
".artifacts/custom/tsgo.tsbuildinfo",
|
|
]);
|
|
});
|
|
|
|
it("avoids incremental cache reuse for ad hoc tsgo runs", () => {
|
|
const { args } = applyLocalTsgoPolicy(
|
|
["--extendedDiagnostics"],
|
|
makeEnv({ OPENCLAW_LOCAL_CHECK_MODE: "full" }),
|
|
ROOMY_HOST,
|
|
);
|
|
|
|
expect(args).toEqual(["--extendedDiagnostics", "--declaration", "false"]);
|
|
});
|
|
|
|
it("allows forcing the throttled tsgo policy on roomy hosts", () => {
|
|
const { args, env } = applyLocalTsgoPolicy(
|
|
[],
|
|
makeEnv({
|
|
OPENCLAW_LOCAL_CHECK_MODE: "throttled",
|
|
}),
|
|
ROOMY_HOST,
|
|
);
|
|
|
|
expect(args).toEqual([
|
|
"--declaration",
|
|
"false",
|
|
"--incremental",
|
|
"--tsBuildInfoFile",
|
|
".artifacts/tsgo-cache/root.tsbuildinfo",
|
|
"--singleThreaded",
|
|
"--checkers",
|
|
"1",
|
|
]);
|
|
expect(env.GOMAXPROCS).toBe("2");
|
|
expect(env.GOGC).toBe("30");
|
|
expect(env.GOMEMLIMIT).toBe("3GiB");
|
|
});
|
|
|
|
it("does not oversubscribe a single-CPU host", () => {
|
|
const { env } = applyLocalTsgoPolicy([], makeEnv({ OPENCLAW_LOCAL_CHECK_MODE: "throttled" }), {
|
|
logicalCpuCount: 1,
|
|
totalMemoryBytes: 16 * 1024 ** 3,
|
|
});
|
|
|
|
expect(env.GOMAXPROCS).toBe("1");
|
|
});
|
|
|
|
it("allows forcing full-speed tsgo runs on roomy hosts", () => {
|
|
const { args, env } = applyLocalTsgoPolicy(
|
|
[],
|
|
makeEnv({
|
|
OPENCLAW_LOCAL_CHECK_MODE: "full",
|
|
}),
|
|
ROOMY_HOST,
|
|
);
|
|
|
|
expect(args).toEqual([
|
|
"--declaration",
|
|
"false",
|
|
"--incremental",
|
|
"--tsBuildInfoFile",
|
|
".artifacts/tsgo-cache/root.tsbuildinfo",
|
|
]);
|
|
expect(env.GOMAXPROCS).toBeUndefined();
|
|
expect(env.GOGC).toBeUndefined();
|
|
expect(env.GOMEMLIMIT).toBeUndefined();
|
|
});
|
|
|
|
it("serializes local oxlint runs onto one thread on constrained hosts", () => {
|
|
const { args, env } = applyLocalOxlintPolicy([], makeEnv(), CONSTRAINED_HOST);
|
|
|
|
expect(args).toEqual([
|
|
"--type-aware",
|
|
"--tsconfig",
|
|
"config/tsconfig/oxlint.json",
|
|
"--report-unused-disable-directives-severity",
|
|
"error",
|
|
"--threads=1",
|
|
]);
|
|
expect(env.GOMAXPROCS).toBe("2");
|
|
expect(env.GOGC).toBe("30");
|
|
expect(env.GOMEMLIMIT).toBe("3GiB");
|
|
});
|
|
|
|
it("defaults local oxlint to one thread on roomy hosts", () => {
|
|
const { args, env } = applyLocalOxlintPolicy([], makeEnv(), ROOMY_HOST);
|
|
|
|
expect(args).toEqual([
|
|
"--type-aware",
|
|
"--tsconfig",
|
|
"config/tsconfig/oxlint.json",
|
|
"--report-unused-disable-directives-severity",
|
|
"error",
|
|
"--threads=1",
|
|
]);
|
|
expect(env.GOMAXPROCS).toBe("2");
|
|
expect(env.GOGC).toBe("30");
|
|
expect(env.GOMEMLIMIT).toBe("3GiB");
|
|
});
|
|
|
|
it("honors an explicit oxlint thread count", () => {
|
|
const { args, env } = applyLocalOxlintPolicy(
|
|
["--threads=8"],
|
|
makeEnv({ GOMAXPROCS: "3", GOGC: "80", GOMEMLIMIT: "5GiB" }),
|
|
ROOMY_HOST,
|
|
);
|
|
|
|
expect(args).toEqual([
|
|
"--threads=8",
|
|
"--type-aware",
|
|
"--tsconfig",
|
|
"config/tsconfig/oxlint.json",
|
|
"--report-unused-disable-directives-severity",
|
|
"error",
|
|
]);
|
|
expect(env.GOMAXPROCS).toBe("3");
|
|
expect(env.GOGC).toBe("80");
|
|
expect(env.GOMEMLIMIT).toBe("5GiB");
|
|
});
|
|
|
|
it("passes the throttled Go concurrency limit to the oxlint child", () => {
|
|
const cwd = createTempDir("openclaw-oxlint-go-limit-");
|
|
const binDir = path.join(cwd, "node_modules", ".bin");
|
|
const capturePath = path.join(cwd, "gomaxprocs.txt");
|
|
const oxlintPath = path.join(binDir, "oxlint");
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
oxlintPath,
|
|
"#!/usr/bin/env node\nrequire('node:fs').writeFileSync(process.env.CAPTURE_PATH, process.env.GOMAXPROCS || '');\n",
|
|
"utf8",
|
|
);
|
|
fs.chmodSync(oxlintPath, 0o755);
|
|
const env: NodeJS.ProcessEnv = {
|
|
...process.env,
|
|
CAPTURE_PATH: capturePath,
|
|
OPENCLAW_LOCAL_CHECK: "1",
|
|
OPENCLAW_LOCAL_CHECK_MODE: "throttled",
|
|
OPENCLAW_OXLINT_SKIP_PREPARE: "1",
|
|
};
|
|
delete env.GOMAXPROCS;
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[path.resolve("scripts/run-oxlint.mjs"), "--tsconfig", "config/tsconfig/oxlint.core.json"],
|
|
{ cwd, encoding: "utf8", env },
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(fs.readFileSync(capturePath, "utf8")).toBe(
|
|
String(Math.min(2, Math.max(1, os.availableParallelism()))),
|
|
);
|
|
});
|
|
|
|
it("allows forcing full-speed oxlint runs on roomy hosts", () => {
|
|
const { args, env } = applyLocalOxlintPolicy(
|
|
[],
|
|
makeEnv({
|
|
OPENCLAW_LOCAL_CHECK_MODE: "full",
|
|
}),
|
|
ROOMY_HOST,
|
|
);
|
|
|
|
expect(args).toEqual([
|
|
"--type-aware",
|
|
"--tsconfig",
|
|
"config/tsconfig/oxlint.json",
|
|
"--report-unused-disable-directives-severity",
|
|
"error",
|
|
]);
|
|
expect(env.GOGC).toBeUndefined();
|
|
expect(env.GOMEMLIMIT).toBeUndefined();
|
|
});
|
|
|
|
it("uses stylish oxlint output in GitHub Actions before the command separator", () => {
|
|
const { args } = applyLocalOxlintPolicy(
|
|
["--", "src/example.ts"],
|
|
makeEnv({
|
|
GITHUB_ACTIONS: "true",
|
|
OPENCLAW_LOCAL_CHECK_MODE: "full",
|
|
}),
|
|
ROOMY_HOST,
|
|
);
|
|
|
|
expect(args.slice(-4)).toEqual(["--format", "stylish", "--", "src/example.ts"]);
|
|
});
|
|
|
|
it.each(["--format", "--format=json", "-f", "-f=json", "-fjson"])(
|
|
"preserves an explicit oxlint format argument: %s",
|
|
(formatArg) => {
|
|
const { args } = applyLocalOxlintPolicy(
|
|
[formatArg],
|
|
makeEnv({
|
|
GITHUB_ACTIONS: "true",
|
|
OPENCLAW_LOCAL_CHECK_MODE: "full",
|
|
}),
|
|
ROOMY_HOST,
|
|
);
|
|
|
|
expect(args).not.toContain("stylish");
|
|
},
|
|
);
|
|
});
|