mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
test(release): repair scheduled E2E regressions (#107173)
* test(release): repair scheduled E2E regressions * test(release): refresh generated protocol bindings * docs(agents): harden remote proof invocations
This commit is contained in:
committed by
GitHub
parent
a2b4c4e17d
commit
044392684b
@@ -145,7 +145,9 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Crabbox request means real scenario proof: install/update/call/repro user path; not just copy tests and run them remotely.
|
||||
- Visual proof: use Crabbox, set up like a user, then screenshot-verify. No harness/bypass/shortcut unless explicitly asked.
|
||||
- Local agent work is limited to lightweight non-test checks such as `git diff --check`, targeted formatting, and cheap static probes. Tests and computationally intensive work default to the selected remote box.
|
||||
- In Codex or linked worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
|
||||
- Agent direct local `pnpm` can reconcile dependencies instead of running the requested script. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated work.
|
||||
- Direct Blacksmith lease: use `blacksmith testbox run`; Crabbox wrapper reuse needs a wrapper-created lease.
|
||||
- Dirty-sync generator proof: compare hashes before/after; `git diff` includes the synced patch.
|
||||
- Crabbox wrapper `stop` has no `--timing-json`; use `node scripts/crabbox-wrapper.mjs stop --provider <provider> --id <id>`.
|
||||
- Repo-native PR worktree may omit `node_modules`; prove remotely, then use `git commit --no-verify`, not `scripts/committer`.
|
||||
- Release-branch formatting: Testbox or existing binary; never local `pnpm exec` reconciliation.
|
||||
|
||||
@@ -18,12 +18,6 @@ type CommandResult = {
|
||||
|
||||
const COMMAND_TIMEOUT_MS = 120_000;
|
||||
const tempDirs: string[] = [];
|
||||
const WORKSPACE_PACKAGE_NAMES = [
|
||||
"@openclaw/gateway-protocol",
|
||||
"@openclaw/retry",
|
||||
"@openclaw/gateway-client",
|
||||
"@openclaw/sdk",
|
||||
] as const;
|
||||
|
||||
type PackageManifest = {
|
||||
name: string;
|
||||
@@ -37,6 +31,14 @@ type PackedPackage = {
|
||||
tarball: string;
|
||||
};
|
||||
|
||||
function resolveWorkspacePackageRoot(repoRoot: string, packageName: string): string {
|
||||
const prefix = "@openclaw/";
|
||||
if (!packageName.startsWith(prefix)) {
|
||||
throw new Error(`unsupported workspace package name: ${packageName}`);
|
||||
}
|
||||
return path.join(repoRoot, "packages", packageName.slice(prefix.length));
|
||||
}
|
||||
|
||||
function runCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
@@ -191,20 +193,57 @@ function normalizeWorkspaceDependencies(
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const [name, spec] of Object.entries(dependencies)) {
|
||||
normalized[name] =
|
||||
name.startsWith("@openclaw/") && spec === "workspace:*" ? "0.0.0-private" : spec;
|
||||
name.startsWith("@openclaw/") && spec.startsWith("workspace:") ? "0.0.0-private" : spec;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function readPackageManifest(packageRoot: string): Promise<PackageManifest> {
|
||||
async function readRawPackageManifest(packageRoot: string): Promise<PackageManifest> {
|
||||
const packageJson = await fs.readFile(path.join(packageRoot, "package.json"), "utf8");
|
||||
const manifest = JSON.parse(packageJson) as PackageManifest;
|
||||
return JSON.parse(packageJson) as PackageManifest;
|
||||
}
|
||||
|
||||
async function readPackageManifest(packageRoot: string): Promise<PackageManifest> {
|
||||
const manifest = await readRawPackageManifest(packageRoot);
|
||||
return {
|
||||
...manifest,
|
||||
dependencies: normalizeWorkspaceDependencies(manifest.dependencies),
|
||||
};
|
||||
}
|
||||
|
||||
async function collectWorkspacePackageRoots(params: {
|
||||
repoRoot: string;
|
||||
entryRoot: string;
|
||||
}): Promise<string[]> {
|
||||
const orderedRoots: string[] = [];
|
||||
const visitedNames = new Set<string>();
|
||||
|
||||
async function visit(packageRoot: string, expectedName?: string): Promise<void> {
|
||||
const manifest = await readRawPackageManifest(packageRoot);
|
||||
if (expectedName && manifest.name !== expectedName) {
|
||||
throw new Error(
|
||||
`workspace dependency ${expectedName} resolved to unexpected package ${manifest.name}`,
|
||||
);
|
||||
}
|
||||
if (visitedNames.has(manifest.name)) {
|
||||
return;
|
||||
}
|
||||
visitedNames.add(manifest.name);
|
||||
|
||||
const workspaceDependencies = Object.entries(manifest.dependencies ?? {})
|
||||
.filter(([, spec]) => spec.startsWith("workspace:"))
|
||||
.map(([name]) => name)
|
||||
.toSorted();
|
||||
for (const dependencyName of workspaceDependencies) {
|
||||
await visit(resolveWorkspacePackageRoot(params.repoRoot, dependencyName), dependencyName);
|
||||
}
|
||||
orderedRoots.push(packageRoot);
|
||||
}
|
||||
|
||||
await visit(params.entryRoot);
|
||||
return orderedRoots;
|
||||
}
|
||||
|
||||
function tarballFileName(manifest: PackageManifest): string {
|
||||
return `${manifest.name.replace(/^@/, "").replace("/", "-")}-${manifest.version}.tgz`;
|
||||
}
|
||||
@@ -344,17 +383,16 @@ describe("OpenClaw SDK package e2e", () => {
|
||||
|
||||
it("packs and imports from an external temp consumer", async () => {
|
||||
const repoRoot = process.cwd();
|
||||
const packageRoots = [
|
||||
path.join(repoRoot, "packages", "gateway-protocol"),
|
||||
path.join(repoRoot, "packages", "retry"),
|
||||
path.join(repoRoot, "packages", "gateway-client"),
|
||||
path.join(repoRoot, "packages", "sdk"),
|
||||
];
|
||||
const packageRoots = await collectWorkspacePackageRoots({
|
||||
repoRoot,
|
||||
entryRoot: path.join(repoRoot, "packages", "sdk"),
|
||||
});
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sdk-consumer-"));
|
||||
tempDirs.push(tempDir);
|
||||
|
||||
for (const packageName of WORKSPACE_PACKAGE_NAMES) {
|
||||
await runPnpmCommand(["--filter", packageName, "build"], {
|
||||
for (const packageRoot of packageRoots) {
|
||||
const manifest = await readRawPackageManifest(packageRoot);
|
||||
await runPnpmCommand(["--filter", manifest.name, "build"], {
|
||||
cwd: repoRoot,
|
||||
timeoutMs: 180_000,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Verifies agent-specific sandbox config, workspace roots, and Docker setup commands.
|
||||
import { EventEmitter } from "node:events";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createRestrictedAgentSandboxConfig } from "./test-helpers/sandbox-agent-config-fixtures.js";
|
||||
@@ -11,41 +9,29 @@ type SpawnCall = {
|
||||
args: string[];
|
||||
};
|
||||
|
||||
const spawnCalls: SpawnCall[] = [];
|
||||
const spawnCalls = vi.hoisted(() => [] as SpawnCall[]);
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
execFile: (...args: unknown[]) => {
|
||||
// Docker availability probes should succeed without invoking real Docker.
|
||||
const callback = args.findLast(
|
||||
(arg): arg is (error: null, stdout: string, stderr: string) => void =>
|
||||
typeof arg === "function",
|
||||
);
|
||||
queueMicrotask(() => callback?.(null, "", ""));
|
||||
return new EventEmitter();
|
||||
},
|
||||
spawn: (command: string, args: string[]) => {
|
||||
spawnCalls.push({ command, args });
|
||||
const child = new EventEmitter() as {
|
||||
stdout?: Readable;
|
||||
stderr?: Readable;
|
||||
on: (event: string, cb: (...args: unknown[]) => void) => void;
|
||||
emit: (event: string, ...args: unknown[]) => boolean;
|
||||
};
|
||||
child.stdout = new Readable({ read() {} });
|
||||
child.stderr = new Readable({ read() {} });
|
||||
async function spawnDockerProcess(commandAndArgs: string[]) {
|
||||
const [command = "", ...args] = commandAndArgs;
|
||||
spawnCalls.push({ command, args });
|
||||
const shouldFailContainerInspect =
|
||||
command === "docker" &&
|
||||
args[0] === "inspect" &&
|
||||
args[1] === "-f" &&
|
||||
args[2] === "{{.State.Running}}";
|
||||
const code = command === "docker" && !shouldFailContainerInspect ? 0 : 1;
|
||||
return {
|
||||
failed: code !== 0,
|
||||
isCanceled: false,
|
||||
exitCode: code,
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.from(code === 0 ? "" : "No such container"),
|
||||
};
|
||||
}
|
||||
|
||||
const dockerArgs = command === "docker" ? args : [];
|
||||
const shouldFailContainerInspect =
|
||||
dockerArgs[0] === "inspect" &&
|
||||
dockerArgs[1] === "-f" &&
|
||||
dockerArgs[2] === "{{.State.Running}}";
|
||||
const shouldSucceedImageInspect = dockerArgs[0] === "image" && dockerArgs[1] === "inspect";
|
||||
|
||||
queueMicrotask(() =>
|
||||
child.emit("close", shouldFailContainerInspect && !shouldSucceedImageInspect ? 1 : 0),
|
||||
);
|
||||
return child;
|
||||
},
|
||||
vi.mock("../process/exec.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../process/exec.js")>()),
|
||||
spawnCommand: spawnDockerProcess,
|
||||
}));
|
||||
|
||||
vi.mock("../skills/loading/workspace.js", () => ({
|
||||
@@ -67,15 +53,14 @@ async function resolveContext(config: OpenClawConfig, sessionKey: string, worksp
|
||||
|
||||
function expectDockerSetupCommand(command: string) {
|
||||
// Setup commands are executed through docker exec in the resolved container.
|
||||
expect(
|
||||
spawnCalls.some(
|
||||
(call) =>
|
||||
call.command === "docker" &&
|
||||
call.args[0] === "exec" &&
|
||||
call.args.includes("-lc") &&
|
||||
call.args.includes(command),
|
||||
),
|
||||
).toBe(true);
|
||||
const matched = spawnCalls.some(
|
||||
(call) =>
|
||||
call.command === "docker" &&
|
||||
call.args[0] === "exec" &&
|
||||
call.args.includes("-lc") &&
|
||||
call.args.includes(command),
|
||||
);
|
||||
expect(matched, `expected docker setup command; calls=${JSON.stringify(spawnCalls)}`).toBe(true);
|
||||
}
|
||||
|
||||
function createDefaultsSandboxConfig(
|
||||
@@ -257,31 +242,31 @@ describe("Agent-specific sandbox config", () => {
|
||||
expect(sandbox.mode).toBe("all");
|
||||
});
|
||||
|
||||
it("should resolve setupCommand overrides based on sandbox scope", async () => {
|
||||
for (const scenario of [
|
||||
{
|
||||
scope: "agent" as const,
|
||||
expectedSetup: "echo work",
|
||||
expectedContainerFragment: "agent-work",
|
||||
},
|
||||
{
|
||||
scope: "shared" as const,
|
||||
expectedSetup: "echo global",
|
||||
expectedContainerFragment: "shared",
|
||||
},
|
||||
]) {
|
||||
const cfg = createWorkSetupCommandConfig(scenario.scope);
|
||||
it.each([
|
||||
{
|
||||
scope: "agent" as const,
|
||||
expectedSetup: "echo work",
|
||||
expectedContainerFragment: "agent-work",
|
||||
},
|
||||
{
|
||||
scope: "shared" as const,
|
||||
expectedSetup: "echo global",
|
||||
expectedContainerFragment: "shared",
|
||||
},
|
||||
])(
|
||||
"should resolve $scope setupCommand overrides",
|
||||
async ({ scope, expectedSetup, expectedContainerFragment }) => {
|
||||
const cfg = createWorkSetupCommandConfig(scope);
|
||||
const context = await resolveContext(cfg, "agent:work:main", "/tmp/test-work");
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`Expected sandbox context for ${scenario.scope} scoped setup`);
|
||||
throw new Error(`Expected sandbox context for ${scope} scoped setup`);
|
||||
}
|
||||
expect(context.docker?.setupCommand).toBe(scenario.expectedSetup);
|
||||
expect(context.containerName).toContain(scenario.expectedContainerFragment);
|
||||
expectDockerSetupCommand(scenario.expectedSetup);
|
||||
spawnCalls.length = 0;
|
||||
}
|
||||
});
|
||||
expect(context.docker?.setupCommand).toBe(expectedSetup);
|
||||
expect(context.containerName).toContain(expectedContainerFragment);
|
||||
expectDockerSetupCommand(expectedSetup);
|
||||
},
|
||||
);
|
||||
|
||||
it("should allow agent-specific docker settings beyond setupCommand", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
|
||||
@@ -10,10 +10,8 @@ import {
|
||||
disposeAllSessionMcpRuntimes,
|
||||
getOrCreateSessionMcpRuntime,
|
||||
} from "../../../../dist/agents/agent-bundle-mcp-runtime.js";
|
||||
import {
|
||||
applyFinalEffectiveToolPolicy,
|
||||
resolveConversationCapabilityProfile,
|
||||
} from "../../../../dist/agents/embedded-agent-runner/effective-tool-policy.js";
|
||||
import { resolveConversationCapabilityProfile } from "../../../../dist/agents/conversation-capability-profile.js";
|
||||
import { applyFinalEffectiveToolPolicy } from "../../../../dist/agents/embedded-agent-runner/effective-tool-policy.js";
|
||||
import { splitSdkTools } from "../../../../dist/agents/embedded-agent-runner/tool-split.js";
|
||||
import type { OpenClawConfig } from "../../../../dist/config/types.openclaw.js";
|
||||
import { getPluginToolMeta } from "../../../../dist/plugins/tools.js";
|
||||
|
||||
+2
-2
@@ -311,10 +311,10 @@ function buildCoreDistEntries(): Record<string, string> {
|
||||
|
||||
function buildDockerE2eHarnessEntries(): Record<string, string> {
|
||||
return {
|
||||
// Mounted Docker harnesses run against the npm tarball image, so any
|
||||
// internal module they assert must have a stable package dist entry.
|
||||
// Mounted Docker harnesses need stable package dist entries for asserted internal modules.
|
||||
"agents/agent-bundle-mcp-materialize": "src/agents/agent-bundle-mcp-materialize.ts",
|
||||
"agents/agent-bundle-mcp-runtime": "src/agents/agent-bundle-mcp-runtime.ts",
|
||||
"agents/conversation-capability-profile": "src/agents/conversation-capability-profile.ts",
|
||||
"agents/embedded-agent-runner/effective-tool-policy":
|
||||
"src/agents/embedded-agent-runner/effective-tool-policy.ts",
|
||||
"agents/embedded-agent-runner/tool-split": "src/agents/embedded-agent-runner/tool-split.ts",
|
||||
|
||||
Reference in New Issue
Block a user