mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 05:52:45 +00:00
refactor(commands): dedupe session target resolution and fs tool test setup
This commit is contained in:
@@ -9,6 +9,42 @@ describe("FS tools with workspaceOnly=false", () => {
|
|||||||
let workspaceDir: string;
|
let workspaceDir: string;
|
||||||
let outsideFile: string;
|
let outsideFile: string;
|
||||||
|
|
||||||
|
const hasToolError = (result: { content: Array<{ type: string; text?: string }> }) =>
|
||||||
|
result.content.some((content) => {
|
||||||
|
if (content.type !== "text") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return content.text?.toLowerCase().includes("error") ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolsFor = (workspaceOnly: boolean | undefined) =>
|
||||||
|
createOpenClawCodingTools({
|
||||||
|
workspaceDir,
|
||||||
|
config:
|
||||||
|
workspaceOnly === undefined
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
tools: {
|
||||||
|
fs: {
|
||||||
|
workspaceOnly,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const runFsTool = async (
|
||||||
|
toolName: "write" | "edit" | "read",
|
||||||
|
callId: string,
|
||||||
|
input: Record<string, unknown>,
|
||||||
|
workspaceOnly: boolean | undefined,
|
||||||
|
) => {
|
||||||
|
const tool = toolsFor(workspaceOnly).find((candidate) => candidate.name === toolName);
|
||||||
|
expect(tool).toBeDefined();
|
||||||
|
const result = await tool!.execute(callId, input);
|
||||||
|
expect(hasToolError(result)).toBe(false);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-"));
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-"));
|
||||||
workspaceDir = path.join(tmpDir, "workspace");
|
workspaceDir = path.join(tmpDir, "workspace");
|
||||||
@@ -21,30 +57,15 @@ describe("FS tools with workspaceOnly=false", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should allow write outside workspace when workspaceOnly=false", async () => {
|
it("should allow write outside workspace when workspaceOnly=false", async () => {
|
||||||
const tools = createOpenClawCodingTools({
|
await runFsTool(
|
||||||
workspaceDir,
|
"write",
|
||||||
config: {
|
"test-call-1",
|
||||||
tools: {
|
{
|
||||||
fs: {
|
path: outsideFile,
|
||||||
workspaceOnly: false,
|
content: "test content",
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
false,
|
||||||
|
|
||||||
const writeTool = tools.find((t) => t.name === "write");
|
|
||||||
expect(writeTool).toBeDefined();
|
|
||||||
|
|
||||||
const result = await writeTool!.execute("test-call-1", {
|
|
||||||
path: outsideFile,
|
|
||||||
content: "test content",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if the operation succeeded (no error in content)
|
|
||||||
const hasError = result.content.some(
|
|
||||||
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
|
|
||||||
);
|
);
|
||||||
expect(hasError).toBe(false);
|
|
||||||
const content = await fs.readFile(outsideFile, "utf-8");
|
const content = await fs.readFile(outsideFile, "utf-8");
|
||||||
expect(content).toBe("test content");
|
expect(content).toBe("test content");
|
||||||
});
|
});
|
||||||
@@ -53,29 +74,15 @@ describe("FS tools with workspaceOnly=false", () => {
|
|||||||
const relativeOutsidePath = path.join("..", "outside-relative-write.txt");
|
const relativeOutsidePath = path.join("..", "outside-relative-write.txt");
|
||||||
const outsideRelativeFile = path.join(tmpDir, "outside-relative-write.txt");
|
const outsideRelativeFile = path.join(tmpDir, "outside-relative-write.txt");
|
||||||
|
|
||||||
const tools = createOpenClawCodingTools({
|
await runFsTool(
|
||||||
workspaceDir,
|
"write",
|
||||||
config: {
|
"test-call-1b",
|
||||||
tools: {
|
{
|
||||||
fs: {
|
path: relativeOutsidePath,
|
||||||
workspaceOnly: false,
|
content: "relative test content",
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
false,
|
||||||
|
|
||||||
const writeTool = tools.find((t) => t.name === "write");
|
|
||||||
expect(writeTool).toBeDefined();
|
|
||||||
|
|
||||||
const result = await writeTool!.execute("test-call-1b", {
|
|
||||||
path: relativeOutsidePath,
|
|
||||||
content: "relative test content",
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasError = result.content.some(
|
|
||||||
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
|
|
||||||
);
|
);
|
||||||
expect(hasError).toBe(false);
|
|
||||||
const content = await fs.readFile(outsideRelativeFile, "utf-8");
|
const content = await fs.readFile(outsideRelativeFile, "utf-8");
|
||||||
expect(content).toBe("relative test content");
|
expect(content).toBe("relative test content");
|
||||||
});
|
});
|
||||||
@@ -83,31 +90,16 @@ describe("FS tools with workspaceOnly=false", () => {
|
|||||||
it("should allow edit outside workspace when workspaceOnly=false", async () => {
|
it("should allow edit outside workspace when workspaceOnly=false", async () => {
|
||||||
await fs.writeFile(outsideFile, "old content");
|
await fs.writeFile(outsideFile, "old content");
|
||||||
|
|
||||||
const tools = createOpenClawCodingTools({
|
await runFsTool(
|
||||||
workspaceDir,
|
"edit",
|
||||||
config: {
|
"test-call-2",
|
||||||
tools: {
|
{
|
||||||
fs: {
|
path: outsideFile,
|
||||||
workspaceOnly: false,
|
oldText: "old content",
|
||||||
},
|
newText: "new content",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
false,
|
||||||
|
|
||||||
const editTool = tools.find((t) => t.name === "edit");
|
|
||||||
expect(editTool).toBeDefined();
|
|
||||||
|
|
||||||
const result = await editTool!.execute("test-call-2", {
|
|
||||||
path: outsideFile,
|
|
||||||
oldText: "old content",
|
|
||||||
newText: "new content",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if the operation succeeded (no error in content)
|
|
||||||
const hasError = result.content.some(
|
|
||||||
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
|
|
||||||
);
|
);
|
||||||
expect(hasError).toBe(false);
|
|
||||||
const content = await fs.readFile(outsideFile, "utf-8");
|
const content = await fs.readFile(outsideFile, "utf-8");
|
||||||
expect(content).toBe("new content");
|
expect(content).toBe("new content");
|
||||||
});
|
});
|
||||||
@@ -117,30 +109,16 @@ describe("FS tools with workspaceOnly=false", () => {
|
|||||||
const outsideRelativeFile = path.join(tmpDir, "outside-relative-edit.txt");
|
const outsideRelativeFile = path.join(tmpDir, "outside-relative-edit.txt");
|
||||||
await fs.writeFile(outsideRelativeFile, "old relative content");
|
await fs.writeFile(outsideRelativeFile, "old relative content");
|
||||||
|
|
||||||
const tools = createOpenClawCodingTools({
|
await runFsTool(
|
||||||
workspaceDir,
|
"edit",
|
||||||
config: {
|
"test-call-2b",
|
||||||
tools: {
|
{
|
||||||
fs: {
|
path: relativeOutsidePath,
|
||||||
workspaceOnly: false,
|
oldText: "old relative content",
|
||||||
},
|
newText: "new relative content",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
false,
|
||||||
|
|
||||||
const editTool = tools.find((t) => t.name === "edit");
|
|
||||||
expect(editTool).toBeDefined();
|
|
||||||
|
|
||||||
const result = await editTool!.execute("test-call-2b", {
|
|
||||||
path: relativeOutsidePath,
|
|
||||||
oldText: "old relative content",
|
|
||||||
newText: "new relative content",
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasError = result.content.some(
|
|
||||||
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
|
|
||||||
);
|
);
|
||||||
expect(hasError).toBe(false);
|
|
||||||
const content = await fs.readFile(outsideRelativeFile, "utf-8");
|
const content = await fs.readFile(outsideRelativeFile, "utf-8");
|
||||||
expect(content).toBe("new relative content");
|
expect(content).toBe("new relative content");
|
||||||
});
|
});
|
||||||
@@ -148,50 +126,27 @@ describe("FS tools with workspaceOnly=false", () => {
|
|||||||
it("should allow read outside workspace when workspaceOnly=false", async () => {
|
it("should allow read outside workspace when workspaceOnly=false", async () => {
|
||||||
await fs.writeFile(outsideFile, "test read content");
|
await fs.writeFile(outsideFile, "test read content");
|
||||||
|
|
||||||
const tools = createOpenClawCodingTools({
|
await runFsTool(
|
||||||
workspaceDir,
|
"read",
|
||||||
config: {
|
"test-call-3",
|
||||||
tools: {
|
{
|
||||||
fs: {
|
path: outsideFile,
|
||||||
workspaceOnly: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
false,
|
||||||
|
|
||||||
const readTool = tools.find((t) => t.name === "read");
|
|
||||||
expect(readTool).toBeDefined();
|
|
||||||
|
|
||||||
const result = await readTool!.execute("test-call-3", {
|
|
||||||
path: outsideFile,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if the operation succeeded (no error in content)
|
|
||||||
const hasError = result.content.some(
|
|
||||||
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
|
|
||||||
);
|
);
|
||||||
expect(hasError).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow write outside workspace when workspaceOnly is unset", async () => {
|
it("should allow write outside workspace when workspaceOnly is unset", async () => {
|
||||||
const outsideUnsetFile = path.join(tmpDir, "outside-unset-write.txt");
|
const outsideUnsetFile = path.join(tmpDir, "outside-unset-write.txt");
|
||||||
const tools = createOpenClawCodingTools({
|
await runFsTool(
|
||||||
workspaceDir,
|
"write",
|
||||||
config: {},
|
"test-call-3a",
|
||||||
});
|
{
|
||||||
|
path: outsideUnsetFile,
|
||||||
const writeTool = tools.find((t) => t.name === "write");
|
content: "unset write content",
|
||||||
expect(writeTool).toBeDefined();
|
},
|
||||||
|
undefined,
|
||||||
const result = await writeTool!.execute("test-call-3a", {
|
|
||||||
path: outsideUnsetFile,
|
|
||||||
content: "unset write content",
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasError = result.content.some(
|
|
||||||
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
|
|
||||||
);
|
);
|
||||||
expect(hasError).toBe(false);
|
|
||||||
const content = await fs.readFile(outsideUnsetFile, "utf-8");
|
const content = await fs.readFile(outsideUnsetFile, "utf-8");
|
||||||
expect(content).toBe("unset write content");
|
expect(content).toBe("unset write content");
|
||||||
});
|
});
|
||||||
@@ -199,40 +154,22 @@ describe("FS tools with workspaceOnly=false", () => {
|
|||||||
it("should allow edit outside workspace when workspaceOnly is unset", async () => {
|
it("should allow edit outside workspace when workspaceOnly is unset", async () => {
|
||||||
const outsideUnsetFile = path.join(tmpDir, "outside-unset-edit.txt");
|
const outsideUnsetFile = path.join(tmpDir, "outside-unset-edit.txt");
|
||||||
await fs.writeFile(outsideUnsetFile, "before");
|
await fs.writeFile(outsideUnsetFile, "before");
|
||||||
const tools = createOpenClawCodingTools({
|
await runFsTool(
|
||||||
workspaceDir,
|
"edit",
|
||||||
config: {},
|
"test-call-3b",
|
||||||
});
|
{
|
||||||
|
path: outsideUnsetFile,
|
||||||
const editTool = tools.find((t) => t.name === "edit");
|
oldText: "before",
|
||||||
expect(editTool).toBeDefined();
|
newText: "after",
|
||||||
|
},
|
||||||
const result = await editTool!.execute("test-call-3b", {
|
undefined,
|
||||||
path: outsideUnsetFile,
|
|
||||||
oldText: "before",
|
|
||||||
newText: "after",
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasError = result.content.some(
|
|
||||||
(c) => c.type === "text" && c.text.toLowerCase().includes("error"),
|
|
||||||
);
|
);
|
||||||
expect(hasError).toBe(false);
|
|
||||||
const content = await fs.readFile(outsideUnsetFile, "utf-8");
|
const content = await fs.readFile(outsideUnsetFile, "utf-8");
|
||||||
expect(content).toBe("after");
|
expect(content).toBe("after");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should block write outside workspace when workspaceOnly=true", async () => {
|
it("should block write outside workspace when workspaceOnly=true", async () => {
|
||||||
const tools = createOpenClawCodingTools({
|
const tools = toolsFor(true);
|
||||||
workspaceDir,
|
|
||||||
config: {
|
|
||||||
tools: {
|
|
||||||
fs: {
|
|
||||||
workspaceOnly: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const writeTool = tools.find((t) => t.name === "write");
|
const writeTool = tools.find((t) => t.name === "write");
|
||||||
expect(writeTool).toBeDefined();
|
expect(writeTool).toBeDefined();
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
|||||||
import { resolveStorePath } from "../config/sessions.js";
|
import { resolveStorePath } from "../config/sessions.js";
|
||||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
import { normalizeAgentId } from "../routing/session-key.js";
|
import { normalizeAgentId } from "../routing/session-key.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
export type SessionStoreSelectionOptions = {
|
export type SessionStoreSelectionOptions = {
|
||||||
store?: string;
|
store?: string;
|
||||||
@@ -78,3 +79,17 @@ export function resolveSessionStoreTargets(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveSessionStoreTargetsOrExit(params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
opts: SessionStoreSelectionOptions;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
}): SessionStoreTarget[] | null {
|
||||||
|
try {
|
||||||
|
return resolveSessionStoreTargets(params.cfg, params.opts);
|
||||||
|
} catch (error) {
|
||||||
|
params.runtime.error(error instanceof Error ? error.message : String(error));
|
||||||
|
params.runtime.exit(1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { RuntimeEnv } from "../runtime.js";
|
|||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
loadConfig: vi.fn(),
|
loadConfig: vi.fn(),
|
||||||
resolveSessionStoreTargets: vi.fn(),
|
resolveSessionStoreTargets: vi.fn(),
|
||||||
|
resolveSessionStoreTargetsOrExit: vi.fn(),
|
||||||
resolveMaintenanceConfig: vi.fn(),
|
resolveMaintenanceConfig: vi.fn(),
|
||||||
loadSessionStore: vi.fn(),
|
loadSessionStore: vi.fn(),
|
||||||
resolveSessionFilePath: vi.fn(),
|
resolveSessionFilePath: vi.fn(),
|
||||||
@@ -21,6 +22,7 @@ vi.mock("../config/config.js", () => ({
|
|||||||
|
|
||||||
vi.mock("./session-store-targets.js", () => ({
|
vi.mock("./session-store-targets.js", () => ({
|
||||||
resolveSessionStoreTargets: mocks.resolveSessionStoreTargets,
|
resolveSessionStoreTargets: mocks.resolveSessionStoreTargets,
|
||||||
|
resolveSessionStoreTargetsOrExit: mocks.resolveSessionStoreTargetsOrExit,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../config/sessions.js", () => ({
|
vi.mock("../config/sessions.js", () => ({
|
||||||
@@ -55,6 +57,17 @@ describe("sessionsCleanupCommand", () => {
|
|||||||
mocks.resolveSessionStoreTargets.mockReturnValue([
|
mocks.resolveSessionStoreTargets.mockReturnValue([
|
||||||
{ agentId: "main", storePath: "/resolved/sessions.json" },
|
{ agentId: "main", storePath: "/resolved/sessions.json" },
|
||||||
]);
|
]);
|
||||||
|
mocks.resolveSessionStoreTargetsOrExit.mockImplementation(
|
||||||
|
(params: { cfg: unknown; opts: unknown; runtime: RuntimeEnv }) => {
|
||||||
|
try {
|
||||||
|
return mocks.resolveSessionStoreTargets(params.cfg, params.opts);
|
||||||
|
} catch (error) {
|
||||||
|
params.runtime.error(error instanceof Error ? error.message : String(error));
|
||||||
|
params.runtime.exit(1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
mocks.resolveMaintenanceConfig.mockReturnValue({
|
mocks.resolveMaintenanceConfig.mockReturnValue({
|
||||||
mode: "warn",
|
mode: "warn",
|
||||||
pruneAfterMs: 7 * 24 * 60 * 60 * 1000,
|
pruneAfterMs: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ import {
|
|||||||
} from "../config/sessions.js";
|
} from "../config/sessions.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { isRich, theme } from "../terminal/theme.js";
|
import { isRich, theme } from "../terminal/theme.js";
|
||||||
import { resolveSessionStoreTargets, type SessionStoreTarget } from "./session-store-targets.js";
|
import {
|
||||||
|
resolveSessionStoreTargetsOrExit,
|
||||||
|
type SessionStoreTarget,
|
||||||
|
} from "./session-store-targets.js";
|
||||||
import {
|
import {
|
||||||
formatSessionAgeCell,
|
formatSessionAgeCell,
|
||||||
formatSessionFlagsCell,
|
formatSessionFlagsCell,
|
||||||
@@ -291,16 +294,16 @@ export async function sessionsCleanupCommand(opts: SessionsCleanupOptions, runti
|
|||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const displayDefaults = resolveSessionDisplayDefaults(cfg);
|
const displayDefaults = resolveSessionDisplayDefaults(cfg);
|
||||||
const mode = opts.enforce ? "enforce" : resolveMaintenanceConfig().mode;
|
const mode = opts.enforce ? "enforce" : resolveMaintenanceConfig().mode;
|
||||||
let targets: SessionStoreTarget[];
|
const targets = resolveSessionStoreTargetsOrExit({
|
||||||
try {
|
cfg,
|
||||||
targets = resolveSessionStoreTargets(cfg, {
|
opts: {
|
||||||
store: opts.store,
|
store: opts.store,
|
||||||
agent: opts.agent,
|
agent: opts.agent,
|
||||||
allAgents: opts.allAgents,
|
allAgents: opts.allAgents,
|
||||||
});
|
},
|
||||||
} catch (error) {
|
runtime,
|
||||||
runtime.error(error instanceof Error ? error.message : String(error));
|
});
|
||||||
runtime.exit(1);
|
if (!targets) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { info } from "../globals.js";
|
|||||||
import { parseAgentSessionKey } from "../routing/session-key.js";
|
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { isRich, theme } from "../terminal/theme.js";
|
import { isRich, theme } from "../terminal/theme.js";
|
||||||
import { resolveSessionStoreTargets } from "./session-store-targets.js";
|
import { resolveSessionStoreTargetsOrExit } from "./session-store-targets.js";
|
||||||
import {
|
import {
|
||||||
formatSessionAgeCell,
|
formatSessionAgeCell,
|
||||||
formatSessionFlagsCell,
|
formatSessionFlagsCell,
|
||||||
@@ -95,16 +95,16 @@ export async function sessionsCommand(
|
|||||||
cfg.agents?.defaults?.contextTokens ??
|
cfg.agents?.defaults?.contextTokens ??
|
||||||
lookupContextTokens(displayDefaults.model) ??
|
lookupContextTokens(displayDefaults.model) ??
|
||||||
DEFAULT_CONTEXT_TOKENS;
|
DEFAULT_CONTEXT_TOKENS;
|
||||||
let targets: ReturnType<typeof resolveSessionStoreTargets>;
|
const targets = resolveSessionStoreTargetsOrExit({
|
||||||
try {
|
cfg,
|
||||||
targets = resolveSessionStoreTargets(cfg, {
|
opts: {
|
||||||
store: opts.store,
|
store: opts.store,
|
||||||
agent: opts.agent,
|
agent: opts.agent,
|
||||||
allAgents: opts.allAgents,
|
allAgents: opts.allAgents,
|
||||||
});
|
},
|
||||||
} catch (error) {
|
runtime,
|
||||||
runtime.error(error instanceof Error ? error.message : String(error));
|
});
|
||||||
runtime.exit(1);
|
if (!targets) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user