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:
Peter Steinberger
2026-07-13 23:49:36 -07:00
committed by GitHub
parent a2b4c4e17d
commit 044392684b
5 changed files with 112 additions and 89 deletions
+3 -1
View File
@@ -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.
+55 -17
View File
@@ -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
View File
@@ -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",