Files
openclaw/extensions/openshell/src/backend.exec-workdir.test.ts
Renaud Cerrato 95b97e5b0b fix(exec): fail invalid explicit workdir before running (#94441)
* fix(exec): fail invalid explicit workdir before running

* test(exec): tighten invalid workdir regression

* fix(exec): clarify invalid workdir recovery

* refactor(exec): centralize workdir resolution

* test(exec): update invalid workdir assertion

* fix(exec): harden backend workdir contract

* fix(exec): map missing backend host workdirs

* fix(exec): reject control commands before workdir prep

* fix(exec): defer env hook until backend cwd validation

* chore(sdk): refresh plugin api baseline

* test(agents): drop redundant definition assertions

* test(exec): use real config workdirs

* test(exec): use tracked temp dirs

* test(openshell): keep temp setup local

* test: update temp-dir route fixture

---------

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
2026-06-26 08:02:00 +10:00

168 lines
5.2 KiB
TypeScript

// Openshell tests cover backend-owned exec workdir validation behavior.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { CreateSandboxBackendParams } from "openclaw/plugin-sdk/sandbox";
import {
createSandboxBrowserConfig,
createSandboxPruneConfig,
createSandboxSshConfig,
} from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createOpenShellSandboxBackendFactory } from "./backend.js";
import { resolveOpenShellPluginConfig } from "./config.js";
const sdkMocks = vi.hoisted(() => ({
runSshSandboxCommand: vi.fn(),
disposeSshSandboxSession: vi.fn(),
}));
const cliMocks = vi.hoisted(() => ({
runOpenShellCli: vi.fn(),
createOpenShellSshSession: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/sandbox", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/sandbox")>();
return {
...actual,
runSshSandboxCommand: sdkMocks.runSshSandboxCommand,
disposeSshSandboxSession: sdkMocks.disposeSshSandboxSession,
};
});
vi.mock("./cli.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./cli.js")>();
return {
...actual,
runOpenShellCli: cliMocks.runOpenShellCli,
createOpenShellSshSession: cliMocks.createOpenShellSshSession,
};
});
const tempDirs: string[] = [];
function createOpenShellBackendSandboxConfig(): CreateSandboxBackendParams["cfg"] {
return {
mode: "all",
backend: "openshell",
scope: "session",
workspaceAccess: "rw",
workspaceRoot: "/tmp/openclaw-sandboxes",
docker: {
image: "openclaw-sandbox:bookworm-slim",
containerPrefix: "openclaw-sbx-",
workdir: "/workspace",
readOnlyRoot: false,
tmpfs: [],
network: "none",
capDrop: [],
binds: [],
env: {},
},
ssh: createSandboxSshConfig("/tmp/openclaw-sandboxes"),
browser: createSandboxBrowserConfig(),
tools: { allow: ["*"], deny: [] },
prune: createSandboxPruneConfig(),
};
}
async function makeTempDir(prefix: string) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
describe("openshell backend exec workdir validation", () => {
beforeEach(() => {
vi.clearAllMocks();
cliMocks.createOpenShellSshSession.mockResolvedValue({
command: "ssh",
configPath: "/tmp/openclaw-openshell-test-ssh-config",
host: "openshell-test",
});
cliMocks.runOpenShellCli.mockResolvedValue({
code: 0,
stdout: "",
stderr: "",
});
sdkMocks.runSshSandboxCommand.mockImplementation(async ({ remoteCommand }) => ({
stdout: String(remoteCommand).includes("openclaw-validate-workdir")
? Buffer.from("/workspace\n")
: Buffer.alloc(0),
stderr: Buffer.alloc(0),
code: 0,
}));
});
afterEach(async () => {
await Promise.all(
tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })),
);
});
it("reuses validation-time workspace preparation for the following exec", async () => {
const workspaceDir = await makeTempDir("openclaw-openshell-workspace-");
await fs.writeFile(path.join(workspaceDir, "seed.txt"), "seed", "utf8");
const backendFactory = createOpenShellSandboxBackendFactory({
pluginConfig: resolveOpenShellPluginConfig({
command: "openshell",
mode: "mirror",
}),
});
const backend = await backendFactory({
sessionKey: "agent:main:turn",
scopeKey: "agent:main",
workspaceDir,
agentWorkspaceDir: workspaceDir,
cfg: createOpenShellBackendSandboxConfig(),
});
await expect(backend.validateWorkdir?.("/workspace")).resolves.toBe("/workspace");
const execSpec = await backend.buildExecSpec({
command: "pwd",
workdir: "/workspace",
env: {},
usePty: false,
});
const uploadCalls = cliMocks.runOpenShellCli.mock.calls.filter(
([params]) => params.args[0] === "sandbox" && params.args[1] === "upload",
);
expect(uploadCalls).toHaveLength(1);
expect(execSpec.argv).toContain("openshell-test");
});
it("does not reuse validation-time workspace preparation after discard", async () => {
const workspaceDir = await makeTempDir("openclaw-openshell-workspace-");
await fs.writeFile(path.join(workspaceDir, "seed.txt"), "seed", "utf8");
const backendFactory = createOpenShellSandboxBackendFactory({
pluginConfig: resolveOpenShellPluginConfig({
command: "openshell",
mode: "mirror",
}),
});
const backend = await backendFactory({
sessionKey: "agent:main:turn",
scopeKey: "agent:main",
workspaceDir,
agentWorkspaceDir: workspaceDir,
cfg: createOpenShellBackendSandboxConfig(),
});
await expect(backend.validateWorkdir?.("/workspace")).resolves.toBe("/workspace");
backend.discardPreparedWorkdir?.("/workspace");
await backend.buildExecSpec({
command: "pwd",
workdir: "/workspace",
env: {},
usePty: false,
});
const uploadCalls = cliMocks.runOpenShellCli.mock.calls.filter(
([params]) => params.args[0] === "sandbox" && params.args[1] === "upload",
);
expect(uploadCalls).toHaveLength(2);
});
});