refactor: modernize plugin entry and add preflights

This commit is contained in:
Gianfranco Piana
2026-04-05 11:14:27 -03:00
parent cea64c21df
commit 51a4b7fbe5
17 changed files with 518 additions and 36 deletions
+1
View File
@@ -34,6 +34,7 @@ The design is file-first: any agent can pick up the repo-root files and continue
## Install
Requires OpenClaw `2026.3.13` or newer.
Needs bash, git, and a git repo.
Use OpenClaw's plugin installer:
+28 -11
View File
@@ -1,9 +1,12 @@
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
import {
definePluginEntry,
type OpenClawPluginApi,
type OpenClawPluginToolContext,
} from "openclaw/plugin-sdk/core";
import {
AUTORESEARCH_PLUGIN_DESCRIPTION,
AUTORESEARCH_PLUGIN_ID,
AUTORESEARCH_PLUGIN_NAME,
autoresearchPluginConfigSchema,
} from "./src/config.js";
import { createInitExperimentTool } from "./src/tools/init-experiment.js";
import { createRunExperimentTool } from "./src/tools/run-experiment.js";
@@ -22,19 +25,33 @@ function createToolFactory<TTool>(
return (toolContext: OpenClawPluginToolContext) => createTool(api, toolContext);
}
const plugin = {
function registerRequiredTool<TTool>(
api: OpenClawPluginApi,
name: string,
createTool: (
api: OpenClawPluginApi,
toolContext?: Pick<OpenClawPluginToolContext, "sessionKey" | "sessionId" | "workspaceDir">,
) => TTool,
) {
api.registerTool(
createToolFactory(createTool, api) as Parameters<OpenClawPluginApi["registerTool"]>[0],
{
name,
optional: false,
},
);
}
export default definePluginEntry({
id: AUTORESEARCH_PLUGIN_ID,
name: AUTORESEARCH_PLUGIN_NAME,
description: AUTORESEARCH_PLUGIN_DESCRIPTION,
configSchema: autoresearchPluginConfigSchema,
register(api: OpenClawPluginApi) {
registerAutoresearchHooks(api);
registerAutoresearchCommand(api);
api.registerTool(createToolFactory(createInitExperimentTool, api));
api.registerTool(createToolFactory(createRunExperimentTool, api));
api.registerTool(createToolFactory(createLogExperimentTool, api));
api.registerTool(createToolFactory(createAutoresearchStatusTool, api));
registerRequiredTool(api, "init_experiment", createInitExperimentTool);
registerRequiredTool(api, "run_experiment", createRunExperimentTool);
registerRequiredTool(api, "log_experiment", createLogExperimentTool);
registerRequiredTool(api, "autoresearch_status", createAutoresearchStatusTool);
},
};
export default plugin;
});
@@ -1,5 +1,5 @@
import * as fs from "node:fs";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import {
AUTORESEARCH_ROOT_FILES,
getAutoresearchRootFilePath,
@@ -1,4 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
const OUTPUT_TAIL_LINES = 80;
const DEFAULT_TIMEOUT_SECONDS = 600;
+1 -1
View File
@@ -1,4 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
const GIT_TIMEOUT_MS = 30_000;
@@ -1,4 +1,4 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { AUTORESEARCH_ROOT_FILES } from "./files.js";
import { reconstructStateFromJsonl } from "./state.js";
import { readAutoresearchCheckpoint } from "./checkpoint.js";
@@ -30,7 +30,7 @@ type HookContext = {
runId?: string;
};
type HookCapablePluginApi = OpenClawPluginApi & {
type HookCapablePluginApi = {
on?: (hookName: string, handler: (event: unknown, ctx: HookContext) => unknown) => void;
registerHook?: (
hookName: string,
@@ -39,7 +39,7 @@ type HookCapablePluginApi = OpenClawPluginApi & {
};
export function registerAutoresearchHooks(api: OpenClawPluginApi): void {
const hookApi = api as HookCapablePluginApi;
const hookApi = api as unknown as HookCapablePluginApi;
if (typeof hookApi.on === "function") {
hookApi.on("before_prompt_build", (_event, ctx) => {
const addition = buildBeforePromptBuildContext(resolveHookScope(ctx));
@@ -116,7 +116,7 @@ export function registerAutoresearchHooks(api: OpenClawPluginApi): void {
return;
}
hookApi.registerHook("before_agent_start", (event, ctx) => {
hookApi.registerHook("before_agent_start", (event: BeforeAgentStartEvent, ctx: HookContext) => {
const addition = buildBeforePromptBuildContext(resolveHookScope(ctx));
if (addition === null) {
return;
@@ -1,4 +1,4 @@
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
import { Type } from "@sinclair/typebox";
import { reconstructStateFromJsonl, type AutoresearchStateSnapshot } from "../state.js";
import {
@@ -16,6 +16,7 @@ import {
getAutoresearchSessionLockStatus,
type AutoresearchSessionLockStatus,
} from "../session-lock.js";
import { prepareAutoresearchToolExecution } from "./preflight.js";
export type AutoresearchStatusDiagnostics = {
readonly warnings: readonly string[];
@@ -55,10 +56,19 @@ export function createAutoresearchStatusTool(
_signal: AbortSignal,
_onUpdate: unknown,
) {
const scope = resolveToolExecutionScope({
const resolvedScope = resolveToolExecutionScope({
toolContext,
requestedCwd: params.cwd,
});
const prepared = await prepareAutoresearchToolExecution({
runCommandWithTimeout: api.runtime.system.runCommandWithTimeout,
scope: resolvedScope,
});
if (!prepared.ok) {
return prepared.failure;
}
const scope = prepared.scope;
const cwd = scope.repoDir;
const state = reconstructStateFromJsonl(cwd);
const runtimeState = getAutoresearchRuntimeState(scope);
@@ -1,4 +1,4 @@
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
import { InitExperimentParams } from "./schemas.js";
import { createConfigHeader, writeConfigHeader } from "../logging.js";
import {
@@ -13,6 +13,7 @@ import { syncAutoresearchSessionDoc } from "../session-doc.js";
import { readCurrentBranch, readShortHeadCommit } from "../git.js";
import { setAutoresearchPendingRun, setAutoresearchRunInFlight } from "../runtime-state.js";
import { resolveToolExecutionScope } from "./tool-cwd.js";
import { prepareAutoresearchToolExecution } from "./preflight.js";
import { acquireAutoresearchSessionLock } from "../session-lock.js";
export function createInitExperimentTool(
@@ -38,10 +39,19 @@ export function createInitExperimentTool(
_signal: AbortSignal,
_onUpdate: unknown,
) {
const scope = resolveToolExecutionScope({
const resolvedScope = resolveToolExecutionScope({
toolContext,
requestedCwd: params.cwd,
});
const prepared = await prepareAutoresearchToolExecution({
runCommandWithTimeout: api.runtime.system.runCommandWithTimeout,
scope: resolvedScope,
});
if (!prepared.ok) {
return prepared.failure;
}
const scope = prepared.scope;
const cwd = scope.repoDir;
const lockStatus = acquireAutoresearchSessionLock(scope);
if (lockStatus.state === "active" && !lockStatus.ownedByCurrentSession) {
@@ -1,5 +1,5 @@
import * as fs from "node:fs";
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
import { LogExperimentParams } from "./schemas.js";
import { commitKeptExperiment, readCurrentBranch, readShortHeadCommit } from "../git.js";
import { appendResultEntry, type AutoresearchResultEntry } from "../logging.js";
@@ -22,6 +22,7 @@ import { readAutoresearchCheckpoint, writeAutoresearchCheckpoint } from "../chec
import { syncAutoresearchSessionDoc } from "../session-doc.js";
import { computeConfidence, formatConfidenceLine } from "../confidence.js";
import { acquireAutoresearchSessionLock } from "../session-lock.js";
import { prepareAutoresearchToolExecution } from "./preflight.js";
export function createLogExperimentTool(
api: OpenClawPluginApi,
@@ -48,10 +49,19 @@ export function createLogExperimentTool(
_signal: AbortSignal,
_onUpdate: unknown,
) {
const scope = resolveToolExecutionScope({
const resolvedScope = resolveToolExecutionScope({
toolContext,
requestedCwd: params.cwd,
});
const prepared = await prepareAutoresearchToolExecution({
runCommandWithTimeout: api.runtime.system.runCommandWithTimeout,
scope: resolvedScope,
});
if (!prepared.ok) {
return prepared.failure;
}
const scope = prepared.scope;
const cwd = scope.repoDir;
const lockStatus = acquireAutoresearchSessionLock(scope);
if (lockStatus.state === "active" && !lockStatus.ownedByCurrentSession) {
@@ -0,0 +1,255 @@
import path from "node:path";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { resolveAutoresearchScope } from "../scope.js";
import type { ResolvedToolExecutionScope } from "./tool-cwd.js";
const PREFLIGHT_TIMEOUT_MS = 10_000;
const DETAIL_LIMIT = 200;
type RunCommandWithTimeout = OpenClawPluginApi["runtime"]["system"]["runCommandWithTimeout"];
type PreflightRequirement = "bash" | "git" | "git-repo";
type CommandCheckResult = Awaited<ReturnType<RunCommandWithTimeout>>;
export type AutoresearchToolFailure = {
content: [
{
type: "text";
text: string;
},
];
details: {
status: "error";
phase: "preflight";
requirement: PreflightRequirement;
cwd: string;
};
};
export type PreparedAutoresearchToolExecution =
| {
ok: true;
scope: ResolvedToolExecutionScope;
}
| {
ok: false;
failure: AutoresearchToolFailure;
};
type CommandCheck =
| {
ok: true;
result: CommandCheckResult;
}
| {
ok: false;
error: unknown;
};
export async function prepareAutoresearchToolExecution(options: {
runCommandWithTimeout: RunCommandWithTimeout;
scope: ResolvedToolExecutionScope;
requireBash?: boolean;
}): Promise<PreparedAutoresearchToolExecution> {
const gitCheck = await runCheck(options.runCommandWithTimeout, options.scope.repoDir, [
"git",
"--version",
]);
if (!gitCheck.ok) {
return {
ok: false,
failure: buildPreflightFailure({
requirement: "git",
cwd: options.scope.repoDir,
text: formatMissingCommandMessage({
command: "git",
fallback: "Autoresearch needs git. Install git and try again.",
error: gitCheck.error,
}),
}),
};
}
if (gitCheck.result.code !== 0) {
return {
ok: false,
failure: buildPreflightFailure({
requirement: "git",
cwd: options.scope.repoDir,
text: appendCheckDetails(
"Autoresearch could not run git. Make sure git works on this machine and try again.",
gitCheck.result,
),
}),
};
}
const repoCheck = await runCheck(options.runCommandWithTimeout, options.scope.repoDir, [
"git",
"rev-parse",
"--show-toplevel",
]);
if (!repoCheck.ok) {
return {
ok: false,
failure: buildPreflightFailure({
requirement: "git-repo",
cwd: options.scope.repoDir,
text: formatMissingCommandMessage({
command: "git",
fallback:
"Autoresearch only works inside a git repo. Run it in the repo you want to optimize, or pass cwd to that repo.",
error: repoCheck.error,
}),
}),
};
}
if (repoCheck.result.code !== 0 || repoCheck.result.stdout.trim().length === 0) {
return {
ok: false,
failure: buildPreflightFailure({
requirement: "git-repo",
cwd: options.scope.repoDir,
text: appendCheckDetails(
"Autoresearch only works inside a git repo. Run it in the repo you want to optimize, or pass cwd to that repo.",
repoCheck.result,
),
}),
};
}
const canonicalRepoDir = path.resolve(repoCheck.result.stdout.trim());
const scope = canonicalizeScope(options.scope, canonicalRepoDir);
if (options.requireBash) {
const bashCheck = await runCheck(options.runCommandWithTimeout, scope.repoDir, [
"bash",
"-lc",
"exit 0",
]);
if (!bashCheck.ok) {
return {
ok: false,
failure: buildPreflightFailure({
requirement: "bash",
cwd: scope.repoDir,
text: formatMissingCommandMessage({
command: "bash",
fallback: "Autoresearch needs bash to run experiment commands. Install bash and try again.",
error: bashCheck.error,
}),
}),
};
}
if (bashCheck.result.code !== 0) {
return {
ok: false,
failure: buildPreflightFailure({
requirement: "bash",
cwd: scope.repoDir,
text: appendCheckDetails(
"Autoresearch needs bash to run experiment commands. Install bash and try again.",
bashCheck.result,
),
}),
};
}
}
return {
ok: true,
scope,
};
}
async function runCheck(
runCommandWithTimeout: RunCommandWithTimeout,
cwd: string,
argv: string[],
): Promise<CommandCheck> {
try {
return {
ok: true,
result: await runCommandWithTimeout(argv, {
cwd,
timeoutMs: PREFLIGHT_TIMEOUT_MS,
}),
};
} catch (error) {
return {
ok: false,
error,
};
}
}
function canonicalizeScope(
scope: ResolvedToolExecutionScope,
repoDir: string,
): ResolvedToolExecutionScope {
if (scope.repoDir === repoDir) {
return scope;
}
const resolved = resolveAutoresearchScope({
sessionKey: scope.sessionKey,
sessionId: scope.sessionId,
workspaceDir: scope.workspaceDir,
repoDir,
runId: scope.runId,
});
return {
...resolved,
repoDir,
};
}
function buildPreflightFailure(params: {
requirement: PreflightRequirement;
cwd: string;
text: string;
}): AutoresearchToolFailure {
return {
content: [
{
type: "text",
text: params.text,
},
],
details: {
status: "error",
phase: "preflight",
requirement: params.requirement,
cwd: params.cwd,
},
};
}
function formatMissingCommandMessage(params: {
command: "bash" | "git";
fallback: string;
error: unknown;
}): string {
const err = params.error as NodeJS.ErrnoException | undefined;
const raw = err?.message ?? String(params.error ?? "");
if (err?.code === "ENOENT" || raw.includes(`spawn ${params.command} ENOENT`)) {
return params.fallback;
}
return appendInlineDetail(params.fallback, raw);
}
function appendCheckDetails(message: string, result: CommandCheckResult): string {
const output = `${result.stderr ?? ""}${result.stdout ?? ""}`.trim();
if (!output) {
return message;
}
return appendInlineDetail(message, output);
}
function appendInlineDetail(message: string, detail: string): string {
const normalized = detail.trim();
if (!normalized) {
return message;
}
const clipped = normalized.length > DETAIL_LIMIT ? `${normalized.slice(0, DETAIL_LIMIT)}...` : normalized;
return `${message}\nDetails: ${clipped}`;
}
@@ -1,4 +1,4 @@
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
import { RunExperimentParams } from "./schemas.js";
import { executeExperimentCommand } from "../execute.js";
import {
@@ -13,6 +13,7 @@ import { readAutoresearchCheckpoint, writeAutoresearchCheckpoint } from "../chec
import { readRecentLoggedRuns, reconstructStateFromJsonl } from "../state.js";
import { syncAutoresearchSessionDoc } from "../session-doc.js";
import { acquireAutoresearchSessionLock } from "../session-lock.js";
import { prepareAutoresearchToolExecution } from "./preflight.js";
export function createRunExperimentTool(
api: OpenClawPluginApi,
@@ -34,10 +35,20 @@ export function createRunExperimentTool(
signal: AbortSignal,
onUpdate: ((update: unknown) => void | Promise<void>) | undefined,
) {
const scope = resolveToolExecutionScope({
const resolvedScope = resolveToolExecutionScope({
toolContext,
requestedCwd: params.cwd,
});
const prepared = await prepareAutoresearchToolExecution({
runCommandWithTimeout: api.runtime.system.runCommandWithTimeout,
scope: resolvedScope,
requireBash: true,
});
if (!prepared.ok) {
return prepared.failure;
}
const scope = prepared.scope;
const cwd = scope.repoDir;
const lockStatus = acquireAutoresearchSessionLock(scope);
if (lockStatus.state === "active" && !lockStatus.ownedByCurrentSession) {
@@ -1,5 +1,5 @@
import path from "node:path";
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk";
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
import { type ResolvedAutoresearchScope, resolveAutoresearchScope } from "../scope.js";
export type ResolvedToolExecutionScope = Omit<ResolvedAutoresearchScope, "repoDir"> & {
+7 -1
View File
@@ -1,3 +1,4 @@
import { execFileSync } from "node:child_process";
import { afterEach, describe, expect, it, vi } from "vitest";
import fs from "node:fs";
import os from "node:os";
@@ -14,7 +15,12 @@ import {
import { runCommandWithTimeout } from "./helpers/fake-runtime.js";
function createTempDir(prefix: string): string {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
const rawCwd = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
const cwd = fs.realpathSync(rawCwd);
execFileSync("git", ["init", "-b", "main"], { cwd, stdio: "pipe" });
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "pipe" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "pipe" });
return cwd;
}
function createAbortSignal(): AbortSignal {
+14 -3
View File
@@ -1,9 +1,14 @@
import { describe, expect, it, vi } from "vitest";
import plugin from "../index.js";
import { runCommandWithTimeout } from "./helpers/fake-runtime.js";
vi.mock("openclaw/plugin-sdk/core", () => ({
definePluginEntry: <TEntry extends { id: string; name: string; description: string }>(entry: TEntry) =>
entry,
}));
describe("plugin registration", () => {
it("registers the command, OpenClaw hooks, and all tool surfaces", () => {
it("registers the command, OpenClaw hooks, and all tool surfaces", async () => {
const plugin = (await import("../index.js")).default;
const api = {
resolvePath: vi.fn(() => "/tmp/repo"),
runtime: {
@@ -16,7 +21,7 @@ describe("plugin registration", () => {
on: vi.fn(),
};
plugin.register(api);
plugin.register(api as never);
expect(api.registerCommand).toHaveBeenCalledTimes(1);
expect(api.on).toHaveBeenCalledTimes(5);
@@ -28,6 +33,12 @@ describe("plugin registration", () => {
"session_end",
]);
expect(api.registerTool).toHaveBeenCalledTimes(4);
expect(api.registerTool.mock.calls.map(([, options]) => options)).toEqual([
{ name: "init_experiment", optional: false },
{ name: "run_experiment", optional: false },
{ name: "log_experiment", optional: false },
{ name: "autoresearch_status", optional: false },
]);
expect(
api.registerTool.mock.calls.map(([tool]) =>
typeof tool === "function"
+15 -1
View File
@@ -22,6 +22,8 @@ export type ToolRegistration = {
name: string;
};
export type AnyAgentTool = ToolRegistration;
export type OpenClawPluginApi = {
resolvePath(path: string): string;
runtime: {
@@ -50,7 +52,10 @@ export type OpenClawPluginApi = {
}>;
};
};
registerTool(tool: ToolRegistration | ((ctx: OpenClawPluginToolContext) => ToolRegistration)): void;
registerTool(
tool: ToolRegistration | ((ctx: OpenClawPluginToolContext) => ToolRegistration),
options?: { name?: string; optional?: boolean },
): void;
registerCommand(command: CommandRegistration): void;
on?(
hookName: string,
@@ -80,6 +85,15 @@ export type OpenClawPluginApi = {
): void;
};
export function definePluginEntry<TEntry extends {
id: string;
name: string;
description: string;
register(api: OpenClawPluginApi): void;
}>(entry: TEntry): TEntry {
return entry;
}
export function emptyPluginConfigSchema(): Record<string, never> {
return {};
}
+139 -4
View File
@@ -1,3 +1,4 @@
import { execFileSync } from "node:child_process";
import { describe, expect, it, vi } from "vitest";
import fs from "node:fs";
import os from "node:os";
@@ -9,16 +10,39 @@ import { createLogExperimentTool } from "../extensions/openclaw-autoresearch/src
import { AUTORESEARCH_ROOT_FILES } from "../extensions/openclaw-autoresearch/src/files.js";
import { runCommandWithTimeout } from "./helpers/fake-runtime.js";
function createTempDir(): string {
return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "autoresearch-tool-test-")));
function initGitRepo(cwd: string): void {
execFileSync("git", ["init", "-b", "main"], { cwd, stdio: "pipe" });
execFileSync("git", ["config", "user.name", "Test User"], { cwd, stdio: "pipe" });
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd, stdio: "pipe" });
}
function createApi(cwd: string) {
function createTempDir(): string {
const cwd = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "autoresearch-tool-test-")));
initGitRepo(cwd);
return cwd;
}
function createPlainTempDir(): string {
return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "autoresearch-plain-tool-test-")));
}
function createMissingCommandRuntime(commandName: "bash" | "git") {
return async (...args: Parameters<typeof runCommandWithTimeout>) => {
if ((args[0]?.[0] ?? "") === commandName) {
const error = new Error(`spawn ${commandName} ENOENT`) as NodeJS.ErrnoException;
error.code = "ENOENT";
throw error;
}
return await runCommandWithTimeout(...args);
};
}
function createApi(cwd: string, commandRunner: typeof runCommandWithTimeout = runCommandWithTimeout) {
return {
resolvePath: vi.fn(() => cwd),
runtime: {
system: {
runCommandWithTimeout,
runCommandWithTimeout: commandRunner,
},
},
};
@@ -200,4 +224,115 @@ describe("autoresearch tools", () => {
expect(logResult.details).toMatchObject({ status: "ok" });
expect(logResult.content[0]?.text).toContain("Logged #1: discard - baseline");
});
it("normalizes cwd overrides to the enclosing git repo root", async () => {
const workspaceCwd = createTempDir();
const repoRoot = createTempDir();
const repoSubdir = path.join(repoRoot, "nested", "project");
fs.mkdirSync(repoSubdir, { recursive: true });
const api = createApi(workspaceCwd);
const toolContext = createToolContext(workspaceCwd, "session:subdir");
const initResult = await createInitExperimentTool(api as never, toolContext).execute(
"call-1",
{
cwd: repoSubdir,
name: "Subdir repo root",
metric_name: "escaped_mutations",
metric_unit: "",
direction: "lower",
},
new AbortController().signal,
undefined,
);
expect(initResult.details).toMatchObject({ status: "ok" });
expect(fs.existsSync(path.join(repoRoot, AUTORESEARCH_ROOT_FILES.resultsLog))).toBe(true);
expect(fs.existsSync(path.join(repoSubdir, AUTORESEARCH_ROOT_FILES.resultsLog))).toBe(false);
const runResult = await createRunExperimentTool(api as never, toolContext).execute(
"call-2",
{
cwd: repoSubdir,
command: "node -e \"console.log(process.cwd())\"",
timeout_seconds: 10,
},
new AbortController().signal,
undefined,
);
expect(runResult.details).toMatchObject({ passed: true, timedOut: false });
expect((runResult.details as { stdout: string }).stdout.trim()).toBe(repoRoot);
});
it("returns a clear preflight error when the target directory is not a git repo", async () => {
const cwd = createPlainTempDir();
const api = createApi(cwd);
const result = await createInitExperimentTool(api as never, createToolContext(cwd)).execute(
"call-1",
{
name: "Not a repo",
metric_name: "escaped_mutations",
metric_unit: "",
direction: "lower",
},
new AbortController().signal,
undefined,
);
expect(result.details).toMatchObject({
status: "error",
phase: "preflight",
requirement: "git-repo",
});
expect(result.content[0]?.text).toContain("only works inside a git repo");
});
it("returns a clear preflight error when git is unavailable", async () => {
const cwd = createTempDir();
const api = createApi(cwd, createMissingCommandRuntime("git"));
const result = await createInitExperimentTool(api as never, createToolContext(cwd)).execute(
"call-1",
{
name: "Missing git",
metric_name: "escaped_mutations",
metric_unit: "",
direction: "lower",
},
new AbortController().signal,
undefined,
);
expect(result.details).toMatchObject({
status: "error",
phase: "preflight",
requirement: "git",
});
expect(result.content[0]?.text).toContain("needs git");
});
it("returns a clear preflight error when bash is unavailable", async () => {
const cwd = createTempDir();
const api = createApi(cwd, createMissingCommandRuntime("bash"));
const result = await createRunExperimentTool(api as never, createToolContext(cwd)).execute(
"call-1",
{
command: "node -e \"console.log('ok')\"",
timeout_seconds: 10,
},
new AbortController().signal,
undefined,
);
expect(result.details).toMatchObject({
status: "error",
phase: "preflight",
requirement: "bash",
});
expect(result.content[0]?.text).toContain("needs bash");
});
});
+2
View File
@@ -10,6 +10,8 @@ export default defineConfig({
resolve: {
alias: {
"openclaw/plugin-sdk": shimPath,
"openclaw/plugin-sdk/core": shimPath,
"openclaw/plugin-sdk/plugin-entry": shimPath,
},
},
test: {