mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:41:24 +00:00
refactor: eliminate remaining duplicate blocks across draft streams and tests
This commit is contained in:
@@ -196,6 +196,24 @@ function mockSingleSuccessfulAttempt() {
|
||||
);
|
||||
}
|
||||
|
||||
function mockSingleErrorAttempt(params: {
|
||||
errorMessage: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
}) {
|
||||
runEmbeddedAttemptMock.mockResolvedValueOnce(
|
||||
makeAttempt({
|
||||
assistantTexts: [],
|
||||
lastAssistant: buildAssistant({
|
||||
stopReason: "error",
|
||||
errorMessage: params.errorMessage,
|
||||
...(params.provider ? { provider: params.provider } : {}),
|
||||
...(params.model ? { model: params.model } : {}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function withTimedAgentWorkspace<T>(
|
||||
run: (ctx: { agentDir: string; workspaceDir: string; now: number }) => Promise<T>,
|
||||
) {
|
||||
@@ -347,15 +365,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
||||
try {
|
||||
await writeAuthStore(agentDir);
|
||||
|
||||
runEmbeddedAttemptMock.mockResolvedValueOnce(
|
||||
makeAttempt({
|
||||
assistantTexts: [],
|
||||
lastAssistant: buildAssistant({
|
||||
stopReason: "error",
|
||||
errorMessage: "rate limit",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
mockSingleErrorAttempt({ errorMessage: "rate limit" });
|
||||
|
||||
await runEmbeddedPiAgent({
|
||||
sessionId: "session:test",
|
||||
@@ -523,17 +533,11 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));
|
||||
try {
|
||||
await writeAuthStore(agentDir);
|
||||
runEmbeddedAttemptMock.mockResolvedValueOnce(
|
||||
makeAttempt({
|
||||
assistantTexts: [],
|
||||
lastAssistant: buildAssistant({
|
||||
stopReason: "error",
|
||||
errorMessage: "insufficient credits",
|
||||
provider: "openai",
|
||||
model: "mock-rotated",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
mockSingleErrorAttempt({
|
||||
errorMessage: "insufficient credits",
|
||||
provider: "openai",
|
||||
model: "mock-rotated",
|
||||
});
|
||||
|
||||
let thrown: unknown;
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import "./test-helpers/fast-coding-tools.js";
|
||||
import { createOpenClawCodingTools } from "./pi-tools.js";
|
||||
import { expectReadWriteEditTools } from "./test-helpers/pi-tools-fs-helpers.js";
|
||||
|
||||
describe("createOpenClawCodingTools", () => {
|
||||
it("uses workspaceDir for Read tool path resolution", async () => {
|
||||
@@ -88,12 +89,7 @@ describe("createOpenClawCodingTools", () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-alias-"));
|
||||
try {
|
||||
const tools = createOpenClawCodingTools({ workspaceDir: tmpDir });
|
||||
const readTool = tools.find((tool) => tool.name === "read");
|
||||
const writeTool = tools.find((tool) => tool.name === "write");
|
||||
const editTool = tools.find((tool) => tool.name === "edit");
|
||||
expect(readTool).toBeDefined();
|
||||
expect(writeTool).toBeDefined();
|
||||
expect(editTool).toBeDefined();
|
||||
const { readTool, writeTool, editTool } = expectReadWriteEditTools(tools);
|
||||
|
||||
const filePath = "alias-test.txt";
|
||||
await writeTool?.execute("tool-alias-1", {
|
||||
|
||||
@@ -7,6 +7,11 @@ import { createOpenClawCodingTools } from "./pi-tools.js";
|
||||
import type { SandboxContext } from "./sandbox.js";
|
||||
import type { SandboxFsBridge, SandboxResolvedPath } from "./sandbox/fs-bridge.js";
|
||||
import { createSandboxFsBridgeFromResolver } from "./test-helpers/host-sandbox-fs-bridge.js";
|
||||
import {
|
||||
expectReadWriteEditTools,
|
||||
expectReadWriteTools,
|
||||
getTextContent,
|
||||
} from "./test-helpers/pi-tools-fs-helpers.js";
|
||||
import { createPiToolsSandboxContext } from "./test-helpers/pi-tools-sandbox-context.js";
|
||||
|
||||
vi.mock("../infra/shell-env.js", async (importOriginal) => {
|
||||
@@ -14,11 +19,6 @@ vi.mock("../infra/shell-env.js", async (importOriginal) => {
|
||||
return { ...mod, getShellPathFromLoginShell: () => null };
|
||||
});
|
||||
|
||||
function getTextContent(result?: { content?: Array<{ type: string; text?: string }> }) {
|
||||
const textBlock = result?.content?.find((block) => block.type === "text");
|
||||
return textBlock?.text ?? "";
|
||||
}
|
||||
|
||||
function createUnsafeMountedBridge(params: {
|
||||
root: string;
|
||||
agentHostRoot: string;
|
||||
@@ -96,10 +96,7 @@ describe("tools.fs.workspaceOnly", () => {
|
||||
await fs.writeFile(path.join(agentRoot, "secret.txt"), "shh", "utf8");
|
||||
|
||||
const tools = createOpenClawCodingTools({ sandbox, workspaceDir: sandboxRoot });
|
||||
const readTool = tools.find((tool) => tool.name === "read");
|
||||
const writeTool = tools.find((tool) => tool.name === "write");
|
||||
expect(readTool).toBeDefined();
|
||||
expect(writeTool).toBeDefined();
|
||||
const { readTool, writeTool } = expectReadWriteTools(tools);
|
||||
|
||||
const readResult = await readTool?.execute("t1", { path: "/agent/secret.txt" });
|
||||
expect(getTextContent(readResult)).toContain("shh");
|
||||
@@ -115,12 +112,7 @@ describe("tools.fs.workspaceOnly", () => {
|
||||
|
||||
const cfg = { tools: { fs: { workspaceOnly: true } } } as unknown as OpenClawConfig;
|
||||
const tools = createOpenClawCodingTools({ sandbox, workspaceDir: sandboxRoot, config: cfg });
|
||||
const readTool = tools.find((tool) => tool.name === "read");
|
||||
const writeTool = tools.find((tool) => tool.name === "write");
|
||||
const editTool = tools.find((tool) => tool.name === "edit");
|
||||
expect(readTool).toBeDefined();
|
||||
expect(writeTool).toBeDefined();
|
||||
expect(editTool).toBeDefined();
|
||||
const { readTool, writeTool, editTool } = expectReadWriteEditTools(tools);
|
||||
|
||||
await expect(readTool?.execute("t1", { path: "/agent/secret.txt" })).rejects.toThrow(
|
||||
/Path escapes sandbox root/i,
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createOpenClawCodingTools } from "./pi-tools.js";
|
||||
import { createHostSandboxFsBridge } from "./test-helpers/host-sandbox-fs-bridge.js";
|
||||
import { expectReadWriteEditTools, getTextContent } from "./test-helpers/pi-tools-fs-helpers.js";
|
||||
import { createPiToolsSandboxContext } from "./test-helpers/pi-tools-sandbox-context.js";
|
||||
|
||||
vi.mock("../infra/shell-env.js", async (importOriginal) => {
|
||||
@@ -19,11 +20,6 @@ async function withTempDir<T>(prefix: string, fn: (dir: string) => Promise<T>) {
|
||||
}
|
||||
}
|
||||
|
||||
function getTextContent(result?: { content?: Array<{ type: string; text?: string }> }) {
|
||||
const textBlock = result?.content?.find((block) => block.type === "text");
|
||||
return textBlock?.text ?? "";
|
||||
}
|
||||
|
||||
describe("workspace path resolution", () => {
|
||||
it("reads relative paths against workspaceDir even after cwd changes", async () => {
|
||||
await withTempDir("openclaw-ws-", async (workspaceDir) => {
|
||||
@@ -171,13 +167,7 @@ describe("sandboxed workspace paths", () => {
|
||||
await fs.writeFile(path.join(workspaceDir, testFile), "workspace read", "utf8");
|
||||
|
||||
const tools = createOpenClawCodingTools({ workspaceDir, sandbox });
|
||||
const readTool = tools.find((tool) => tool.name === "read");
|
||||
const writeTool = tools.find((tool) => tool.name === "write");
|
||||
const editTool = tools.find((tool) => tool.name === "edit");
|
||||
|
||||
expect(readTool).toBeDefined();
|
||||
expect(writeTool).toBeDefined();
|
||||
expect(editTool).toBeDefined();
|
||||
const { readTool, writeTool, editTool } = expectReadWriteEditTools(tools);
|
||||
|
||||
const result = await readTool?.execute("sbx-read", { path: testFile });
|
||||
expect(getTextContent(result)).toContain("sandbox read");
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js";
|
||||
import { writeSkill } from "./skills.e2e-test-helpers.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "./skills.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
async function createTempDir(prefix: string) {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
const tempDirs = createTrackedTempDirs();
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
||||
await tempDirs.cleanup();
|
||||
});
|
||||
|
||||
describe("buildWorkspaceSkillSnapshot", () => {
|
||||
it("returns an empty snapshot when skills dirs are missing", async () => {
|
||||
const workspaceDir = await createTempDir("openclaw-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
|
||||
const snapshot = buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||
managedSkillsDir: path.join(workspaceDir, ".managed"),
|
||||
@@ -31,7 +25,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
||||
});
|
||||
|
||||
it("omits disable-model-invocation skills from the prompt", async () => {
|
||||
const workspaceDir = await createTempDir("openclaw-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
await writeSkill({
|
||||
dir: path.join(workspaceDir, "skills", "visible-skill"),
|
||||
name: "visible-skill",
|
||||
@@ -58,7 +52,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
||||
});
|
||||
|
||||
it("truncates the skills prompt when it exceeds the configured char budget", async () => {
|
||||
const workspaceDir = await createTempDir("openclaw-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
|
||||
// Make a bunch of skills with very long descriptions.
|
||||
for (let i = 0; i < 25; i += 1) {
|
||||
@@ -88,8 +82,8 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
||||
});
|
||||
|
||||
it("limits discovery for nested repo-style skills roots (dir/skills/*)", async () => {
|
||||
const workspaceDir = await createTempDir("openclaw-");
|
||||
const repoDir = await createTempDir("openclaw-skills-repo-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
const repoDir = await tempDirs.make("openclaw-skills-repo-");
|
||||
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
const name = `repo-skill-${String(i).padStart(2, "0")}`;
|
||||
@@ -123,7 +117,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
||||
});
|
||||
|
||||
it("skips skills whose SKILL.md exceeds maxSkillFileBytes", async () => {
|
||||
const workspaceDir = await createTempDir("openclaw-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
|
||||
await writeSkill({
|
||||
dir: path.join(workspaceDir, "skills", "small-skill"),
|
||||
@@ -157,8 +151,8 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
||||
});
|
||||
|
||||
it("detects nested skills roots beyond the first 25 entries", async () => {
|
||||
const workspaceDir = await createTempDir("openclaw-");
|
||||
const repoDir = await createTempDir("openclaw-skills-repo-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
const repoDir = await tempDirs.make("openclaw-skills-repo-");
|
||||
|
||||
// Create 30 nested dirs, but only the last one is an actual skill.
|
||||
for (let i = 0; i < 30; i += 1) {
|
||||
@@ -194,8 +188,8 @@ describe("buildWorkspaceSkillSnapshot", () => {
|
||||
});
|
||||
|
||||
it("enforces maxSkillFileBytes for root-level SKILL.md", async () => {
|
||||
const workspaceDir = await createTempDir("openclaw-");
|
||||
const rootSkillDir = await createTempDir("openclaw-root-skill-");
|
||||
const workspaceDir = await tempDirs.make("openclaw-");
|
||||
const rootSkillDir = await tempDirs.make("openclaw-root-skill-");
|
||||
|
||||
await writeSkill({
|
||||
dir: rootSkillDir,
|
||||
|
||||
33
src/agents/test-helpers/pi-tools-fs-helpers.ts
Normal file
33
src/agents/test-helpers/pi-tools-fs-helpers.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { expect } from "vitest";
|
||||
|
||||
type TextResultBlock = { type: string; text?: string };
|
||||
|
||||
export function getTextContent(result?: { content?: TextResultBlock[] }) {
|
||||
const textBlock = result?.content?.find((block) => block.type === "text");
|
||||
return textBlock?.text ?? "";
|
||||
}
|
||||
|
||||
export function expectReadWriteEditTools<T extends { name: string }>(tools: T[]) {
|
||||
const readTool = tools.find((tool) => tool.name === "read");
|
||||
const writeTool = tools.find((tool) => tool.name === "write");
|
||||
const editTool = tools.find((tool) => tool.name === "edit");
|
||||
expect(readTool).toBeDefined();
|
||||
expect(writeTool).toBeDefined();
|
||||
expect(editTool).toBeDefined();
|
||||
return {
|
||||
readTool: readTool as T,
|
||||
writeTool: writeTool as T,
|
||||
editTool: editTool as T,
|
||||
};
|
||||
}
|
||||
|
||||
export function expectReadWriteTools<T extends { name: string }>(tools: T[]) {
|
||||
const readTool = tools.find((tool) => tool.name === "read");
|
||||
const writeTool = tools.find((tool) => tool.name === "write");
|
||||
expect(readTool).toBeDefined();
|
||||
expect(writeTool).toBeDefined();
|
||||
return {
|
||||
readTool: readTool as T,
|
||||
writeTool: writeTool as T,
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,7 @@ export type VolcModelCatalogEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
reasoning: boolean;
|
||||
input: readonly string[];
|
||||
input: ReadonlyArray<ModelDefinitionConfig["input"][number]>;
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user