mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
merge: refresh Daytona hydration branch from main
* origin/main: (57 commits) fix(media): fall back when providers return empty output (#118660) fix(nextcloud-talk): allow bounded parallel room deliveries (#118692) test(state): reuse current agent database fixture (#118677) fix(scripts): bound check-file-utils git lookup (#111582) fix(slack): scope interactive conversation bindings safely (#118662) fix(agents): avoid false compaction after mid-turn precheck (#117963) fix(ui): show original filename on chat history attachment cards instead of managed UUID suffix (#118628) fix(whatsapp): preserve terminal retry exhaustion lifecycle (#118659) fix(doctor): audit every agent workspace (#111840) fix(release): skip unavailable Buzz on frozen candidates (#118670) fix(ui): surface onboarding and pairing clipboard failures (#118651) fix(config): stop plugin schemas rejecting the channel key core writes (#117992) test(memory): isolate wiki plugin fixtures (#118654) fix(line): clear default access token when removing account (#118055) feat: enable rich setup controls in custodian chat (#114631) test(ui): reuse responsive browser fixtures (#118655) chore(tui): stabilize queued-turn admission in Gateway PTY test (#118638) fix(xai): classify exhausted credits as billing (#118615) fix(otel): fail closed when configured TLS material is invalid (#118648) fix(cli): preserve gateway request errors in health JSON (#118645) ...
This commit is contained in:
@@ -38,6 +38,31 @@ describe("scripts/lib/arg-utils parseFlagArgs", () => {
|
||||
expect(parsed.match).toEqual(["alpha", "beta"]);
|
||||
});
|
||||
|
||||
it("supports split-only, empty, transformed, and last-value-wins string contracts", () => {
|
||||
expect(() =>
|
||||
parseFlagArgs(["--value=inline"], { value: "" }, [
|
||||
stringFlag("--value", "value", { allowInline: false }),
|
||||
]),
|
||||
).toThrow("Unknown option: --value=inline");
|
||||
expect(
|
||||
parseFlagArgs(["--value", "", "--value", "SECOND"], { value: "" }, [
|
||||
stringFlag("--value", "value", {
|
||||
allowEmpty: true,
|
||||
repeatable: true,
|
||||
transform: (value) => value.toLowerCase(),
|
||||
}),
|
||||
]).value,
|
||||
).toBe("second");
|
||||
});
|
||||
|
||||
it("supports idempotent boolean flags", () => {
|
||||
expect(
|
||||
parseFlagArgs(["--verbose", "--verbose"], { verbose: false }, [
|
||||
booleanFlag("--verbose", "verbose", true, { repeatable: true }),
|
||||
]).verbose,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects duplicate single-value flags", () => {
|
||||
expect(() =>
|
||||
parseFlagArgs(["--label", "first", "--label=second"], { label: "" }, [
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
// Check File Utils tests cover check file utils script behavior.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
collectFilesSync,
|
||||
isCodeFile,
|
||||
listRepoFilesSync,
|
||||
relativeToCwd,
|
||||
toPosixPath,
|
||||
} from "../../scripts/check-file-utils.js";
|
||||
import { createScriptTestHarness } from "./test-helpers.js";
|
||||
|
||||
const execFileSyncMock = vi.hoisted(() => vi.fn(() => ""));
|
||||
|
||||
vi.mock("node:child_process", async (importOriginal) => {
|
||||
const original = (await importOriginal()) as typeof import("node:child_process");
|
||||
return { ...original, execFileSync: execFileSyncMock };
|
||||
});
|
||||
|
||||
const { createTempDir } = createScriptTestHarness();
|
||||
|
||||
describe("scripts/check-file-utils isCodeFile", () => {
|
||||
@@ -64,3 +72,45 @@ describe("scripts/check-file-utils relativeToCwd", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scripts/check-file-utils listRepoFilesSync", () => {
|
||||
afterEach(() => {
|
||||
execFileSyncMock.mockReset();
|
||||
});
|
||||
|
||||
it("bounds git ls-files with a timeout and kill signal", () => {
|
||||
execFileSyncMock.mockReturnValue("src/keep.ts\nsrc/skip.d.ts\n");
|
||||
|
||||
expect(
|
||||
listRepoFilesSync("/fake/repo", {
|
||||
includeFile: (filePath) => isCodeFile(filePath),
|
||||
}),
|
||||
).toEqual(["src/keep.ts"]);
|
||||
expect(execFileSyncMock).toHaveBeenCalledWith(
|
||||
"git",
|
||||
expect.arrayContaining(["-C", "/fake/repo", "ls-files", "--"]),
|
||||
expect.objectContaining({
|
||||
timeout: 30_000,
|
||||
killSignal: "SIGKILL",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to filesystem traversal when git ls-files times out", () => {
|
||||
const error: NodeJS.ErrnoException & { signal?: string } = new Error("Command timed out");
|
||||
error.code = "ETIMEDOUT";
|
||||
error.signal = "SIGKILL";
|
||||
execFileSyncMock.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
const rootDir = createTempDir("openclaw-check-file-utils-fallback-");
|
||||
fs.mkdirSync(path.join(rootDir, "src"), { recursive: true });
|
||||
fs.writeFileSync(path.join(rootDir, "src", "keep.ts"), "");
|
||||
|
||||
expect(
|
||||
listRepoFilesSync(rootDir, {
|
||||
includeFile: (filePath) => filePath.endsWith(".ts"),
|
||||
}),
|
||||
).toEqual(["src/keep.ts"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,13 +21,15 @@ describe("scripts/check", () => {
|
||||
});
|
||||
|
||||
it("rejects unknown args before running check stages", () => {
|
||||
const result = runCheck("--bogus");
|
||||
for (const args of [["--bogus"], ["bogus", "--help"]]) {
|
||||
const result = runCheck(...args);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("unknown argument: --bogus");
|
||||
expect(result.stderr).toContain("Usage: node scripts/check.mjs");
|
||||
expect(result.stderr).not.toContain("[check]");
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(`unknown argument: ${args[0]}`);
|
||||
expect(result.stderr).toContain("Usage: node scripts/check.mjs");
|
||||
expect(result.stderr).not.toContain("[check]");
|
||||
}
|
||||
});
|
||||
|
||||
it("runs pnpm commands through the managed child runner", async () => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
releaseEvidenceVerificationArgs,
|
||||
releaseEvidenceVerifierPath,
|
||||
resolveRemoteTargetRefSha,
|
||||
runGhRead,
|
||||
shouldDeleteTemporaryWorkflowRef,
|
||||
} from "../../scripts/full-release-validation-at-sha.mjs";
|
||||
|
||||
@@ -163,30 +162,9 @@ describe("full-release-validation-at-sha", () => {
|
||||
});
|
||||
|
||||
it("bounds GitHub reads without applying a timeout to workflow dispatch", () => {
|
||||
const calls: unknown[][] = [];
|
||||
expect(
|
||||
runGhRead(["api", "repos/openclaw/openclaw/actions/runs/123"], {
|
||||
execFileSyncImpl: (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
return " result ";
|
||||
},
|
||||
}),
|
||||
).toBe("result");
|
||||
expect(calls).toEqual([
|
||||
[
|
||||
"gh",
|
||||
["api", "repos/openclaw/openclaw/actions/runs/123"],
|
||||
expect.objectContaining({
|
||||
killSignal: "SIGKILL",
|
||||
timeout: 60_000,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
const source = readFileSync("scripts/full-release-validation-at-sha.mjs", "utf8");
|
||||
expect(source).toContain(
|
||||
'runGhRead(["api", `repos/openclaw/openclaw/actions/runs/${parentRunId}`])',
|
||||
);
|
||||
expect(source).toContain("timeout: GH_READ_TIMEOUT_MS");
|
||||
expect(source.match(/GH_READ_OPTIONS/gu)).toHaveLength(3);
|
||||
expect(source).toContain('const dispatchOutput = run("gh", dispatchArgs');
|
||||
});
|
||||
|
||||
|
||||
@@ -206,12 +206,14 @@ describe("generate-dependency-release-evidence", () => {
|
||||
});
|
||||
|
||||
it("reports CLI argument errors without a Node stack trace", () => {
|
||||
const result = runCli("--wat");
|
||||
for (const args of [["--wat"], ["wat", "--help"]]) {
|
||||
const result = runCli(...args);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr.trim()).toBe("Unsupported argument: --wat");
|
||||
expectNoNodeStack(result.stderr);
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr.trim()).toBe(`Unsupported argument: ${args[0]}`);
|
||||
expectNoNodeStack(result.stderr);
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to fetching tags when local previous-release resolution misses", () => {
|
||||
|
||||
@@ -454,4 +454,19 @@ describe("release Telegram QA workflow", () => {
|
||||
.status,
|
||||
).not.toBe(0);
|
||||
});
|
||||
|
||||
it("shares only the isolated workspace with the trusted scenario host", () => {
|
||||
const createSut = requireRun(
|
||||
"run_telegram",
|
||||
"Create isolated Telegram SUT identity and launcher",
|
||||
);
|
||||
|
||||
expect(createSut).toContain('workspace="${temp_root}/workspace"');
|
||||
expect(createSut).toContain('chown -R "$RUNNER_UID:$SUT_GID" "$workspace"');
|
||||
expect(createSut).toContain('chmod -R u=rwX,g=rwX,o= "$workspace"');
|
||||
expect(createSut).toContain('find "$workspace" -type d -exec chmod g+s {} +');
|
||||
expect(createSut).not.toContain(
|
||||
'for path in \\\n "$temp_root/workspace" \\\n "${OPENCLAW_HOME:?}"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3183,7 +3183,7 @@ describe("package artifact reuse", () => {
|
||||
);
|
||||
const requireBuzz = workflowStep(buzzJob, "Require requested Buzz QA runner");
|
||||
expect(requireBuzz.if).toBe(
|
||||
"always() && steps.resolve_buzz.outcome == 'success' && steps.resolve_buzz.outputs.available != 'true'",
|
||||
"always() && inputs.expected_sha == '' && steps.resolve_buzz.outcome == 'success' && steps.resolve_buzz.outputs.available != 'true'",
|
||||
);
|
||||
expect(requireBuzz.run).toContain(
|
||||
"The selected ref does not declare the requested Buzz QA runner.",
|
||||
|
||||
@@ -6,6 +6,8 @@ import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
execGhApiRead,
|
||||
execGhJson,
|
||||
execGhRead,
|
||||
execPlainGh,
|
||||
plainGhEnv,
|
||||
PLAIN_GH_SYSTEM_CANDIDATES,
|
||||
@@ -114,6 +116,59 @@ describe("plain gh helpers", () => {
|
||||
expect(output).toContain("OPENCLAW_GH_BIN_SET=");
|
||||
});
|
||||
|
||||
it("shares bounded PATH-shim reads and JSON parsing", () => {
|
||||
const calls: unknown[][] = [];
|
||||
const execFileSyncImpl = (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
return '{"ok":true}';
|
||||
};
|
||||
|
||||
expect(
|
||||
execGhJson(
|
||||
["api", "repos/openclaw/openclaw"],
|
||||
{
|
||||
killSignal: "SIGKILL",
|
||||
stdio: ["ignore", "pipe", "inherit"],
|
||||
timeout: 60_000,
|
||||
},
|
||||
{ execFileSyncImpl },
|
||||
),
|
||||
).toEqual({ ok: true });
|
||||
expect(calls).toEqual([
|
||||
[
|
||||
"gh",
|
||||
["api", "repos/openclaw/openclaw"],
|
||||
expect.objectContaining({
|
||||
encoding: "utf8",
|
||||
killSignal: "SIGKILL",
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
stdio: ["ignore", "pipe", "inherit"],
|
||||
timeout: 60_000,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
expect(
|
||||
execGhRead(
|
||||
["api", "rate_limit"],
|
||||
{ encoding: "utf8" },
|
||||
{ execFileSyncImpl: () => " result " },
|
||||
),
|
||||
).toBe(" result ");
|
||||
|
||||
const failure = new Error("gh read failed");
|
||||
expect(() =>
|
||||
execGhRead(
|
||||
["api", "rate_limit"],
|
||||
{},
|
||||
{
|
||||
execFileSyncImpl: () => {
|
||||
throw failure;
|
||||
},
|
||||
},
|
||||
),
|
||||
).toThrow(failure);
|
||||
});
|
||||
|
||||
it("runs the shell helper with color disabled", () => {
|
||||
const ghPath = makeFakeGh();
|
||||
const outputPath = path.join(path.dirname(path.dirname(ghPath)), "output.txt");
|
||||
|
||||
@@ -107,6 +107,11 @@ describe("plugin npm extended-stable workflow", () => {
|
||||
|
||||
it("overlays the complete trusted packaging helper dependency set", () => {
|
||||
const parsed = workflow();
|
||||
const lockGenerator = readFileSync("scripts/generate-npm-package-lock.mjs", "utf8");
|
||||
expect(lockGenerator).toContain(
|
||||
'path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")',
|
||||
);
|
||||
expect(lockGenerator).not.toContain("./lib/repo-root.mjs");
|
||||
const preflightCheckout = step(
|
||||
parsed.jobs?.preview_plugin_pack,
|
||||
"Checkout trusted packaging helper",
|
||||
|
||||
@@ -59,19 +59,21 @@ describe("plugin SDK surface report", () => {
|
||||
});
|
||||
|
||||
it("rejects unknown CLI options before collecting SDK stats", () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["scripts/plugin-sdk-surface-report.mjs", "--chekc"],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
for (const args of [["--chekc"], ["chekc", "--help"]]) {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
["scripts/plugin-sdk-surface-report.mjs", ...args],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr.trim()).toBe("Unknown plugin SDK surface report option: --chekc");
|
||||
expect(result.stderr).not.toContain("at ");
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr.trim()).toBe(`Unknown plugin SDK surface report option: ${args[0]}`);
|
||||
expect(result.stderr).not.toContain("at ");
|
||||
}
|
||||
});
|
||||
|
||||
it("prints help before collecting SDK stats", () => {
|
||||
|
||||
@@ -68,6 +68,50 @@ async function writeBaileysMediaFile(packageRoot: string, text: string) {
|
||||
}
|
||||
|
||||
describe("bundled plugin postinstall", () => {
|
||||
it("resolves TypeScript from NODE_PATH during external modules-dir installs", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-postinstall-node-path-");
|
||||
const scriptRoot = path.join(packageRoot, "scripts");
|
||||
const externalModulesDir = path.join(packageRoot, "external-node-modules");
|
||||
await fs.mkdir(path.join(scriptRoot, "lib"), { recursive: true });
|
||||
await fs.mkdir(externalModulesDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(packageRoot, "package.json"),
|
||||
'{"name":"openclaw","type":"module","version":"2026.7.2"}\n',
|
||||
);
|
||||
for (const relativePath of [
|
||||
"scripts/postinstall-bundled-plugins.mjs",
|
||||
"scripts/lib/package-dist-imports.mjs",
|
||||
"scripts/lib/guard-inventory-utils.mjs",
|
||||
]) {
|
||||
await fs.copyFile(
|
||||
fileURLToPath(new URL(`../../${relativePath}`, import.meta.url)),
|
||||
path.join(packageRoot, relativePath),
|
||||
);
|
||||
}
|
||||
await fs.symlink(
|
||||
fileURLToPath(new URL("../../node_modules/typescript", import.meta.url)),
|
||||
path.join(externalModulesDir, "typescript"),
|
||||
process.platform === "win32" ? "junction" : "dir",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(scriptRoot, "postinstall-bundled-plugins.mjs")],
|
||||
{
|
||||
cwd: packageRoot,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_PATH: [externalModulesDir, process.env.NODE_PATH]
|
||||
.filter(Boolean)
|
||||
.join(path.delimiter),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
});
|
||||
|
||||
it("recognizes direct invocation through symlinked temp prefixes", () => {
|
||||
const realpathSync = vi.fn((value: string) =>
|
||||
value.replace(/^\/var\/folders\//u, "/private/var/folders/"),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { testForceTesting } from "../../scripts/test-force.js";
|
||||
|
||||
describe("scripts/test-force.ts", () => {
|
||||
it("prints help without clearing ports or running tests", () => {
|
||||
const args = testForceTesting.parseArgs(["--help"]);
|
||||
const args = testForceTesting.parseArgs(["--help", "--bogus"]);
|
||||
|
||||
expect(args).toEqual({ help: true });
|
||||
expect(testForceTesting.usage()).toContain("Usage: node --import tsx scripts/test-force.ts");
|
||||
@@ -16,5 +16,11 @@ describe("scripts/test-force.ts", () => {
|
||||
expect(() => testForceTesting.parseArgs(["--bogus"])).toThrow(
|
||||
/unknown argument: --bogus[\s\S]*Usage: node --import tsx scripts\/test-force\.ts/u,
|
||||
);
|
||||
expect(() => testForceTesting.parseArgs(["bogus"])).toThrow(
|
||||
/unknown argument: bogus[\s\S]*Usage: node --import tsx scripts\/test-force\.ts/u,
|
||||
);
|
||||
expect(() => testForceTesting.parseArgs(["bogus", "--help"])).toThrow(
|
||||
/unknown argument: bogus[\s\S]*Usage: node --import tsx scripts\/test-force\.ts/u,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1931,6 +1931,7 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
"scripts/lib/ts-topology/analyze.ts": ["test/scripts/ts-topology.test.ts"],
|
||||
"scripts/lib/ts-topology/reports.ts": ["test/scripts/ts-topology.test.ts"],
|
||||
"scripts/lib/ts-topology/scope.ts": ["test/scripts/ts-topology.test.ts"],
|
||||
"scripts/lib/repo-root.mjs": ["test/scripts/ts-guard-utils.test.ts"],
|
||||
"scripts/lib/ts-guard-utils.mjs": ["test/scripts/ts-guard-utils.test.ts"],
|
||||
"scripts/lib/tsgo-sparse-guard.mjs": [
|
||||
"test/scripts/run-tsgo.test.ts",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Ts Guard Utils tests cover ts guard utils script behavior.
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveRepoRoot } from "../../scripts/lib/ts-guard-utils.mjs";
|
||||
import { resolveRepoRoot } from "../../scripts/lib/repo-root.mjs";
|
||||
|
||||
/**
|
||||
* Regression tests for resolveRepoRoot().
|
||||
@@ -49,4 +50,19 @@ describe("resolveRepoRoot", () => {
|
||||
expect(fromLib).toBe(fromScripts);
|
||||
expect(fromScripts).toBe(fromExtension);
|
||||
});
|
||||
|
||||
it("resolves an unpacked workspace without git metadata", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "openclaw-repo-root-"));
|
||||
try {
|
||||
mkdirSync(path.join(root, "scripts", "nested"), { recursive: true });
|
||||
writeFileSync(path.join(root, "package.json"), '{"name":"openclaw"}\n');
|
||||
writeFileSync(path.join(root, "pnpm-workspace.yaml"), "packages: []\n");
|
||||
|
||||
expect(
|
||||
resolveRepoRoot(pathToFileURL(path.join(root, "scripts", "nested", "tool.mjs")).href),
|
||||
).toBe(root);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,13 +21,15 @@ describe("scripts/verify", () => {
|
||||
});
|
||||
|
||||
it("rejects unknown args before running verify stages", () => {
|
||||
const result = runVerify("--bogus");
|
||||
for (const args of [["--bogus"], ["bogus", "--help"]]) {
|
||||
const result = runVerify(...args);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("unknown argument: --bogus");
|
||||
expect(result.stderr).toContain("Usage: node scripts/verify.mjs");
|
||||
expect(result.stderr).not.toContain("CRABBOX_PHASE:");
|
||||
expect(result.stderr).not.toContain("[verify]");
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(`unknown argument: ${args[0]}`);
|
||||
expect(result.stderr).toContain("Usage: node scripts/verify.mjs");
|
||||
expect(result.stderr).not.toContain("CRABBOX_PHASE:");
|
||||
expect(result.stderr).not.toContain("[verify]");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user