test: cover tool descriptor planner

This commit is contained in:
Shakker
2026-05-02 07:10:56 +01:00
committed by Shakker
parent c5224a341e
commit ae82da61e3
3 changed files with 404 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
import { describe, expect, it } from "vitest";
import { evaluateToolAvailability } from "./availability.js";
import type { ToolDescriptor } from "./types.js";
const baseDescriptor: ToolDescriptor = {
name: "example",
description: "Example tool",
inputSchema: { type: "object" },
owner: { kind: "core" },
executor: { kind: "core", executorId: "example" },
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
describe("evaluateToolAvailability", () => {
it("treats descriptors without signals as available", () => {
expect(evaluateToolAvailability({ descriptor: baseDescriptor })).toEqual([]);
});
it("evaluates auth, env, config, plugin, and context signals from data only", () => {
const descriptor: ToolDescriptor = {
...baseDescriptor,
availability: {
allOf: [
{ kind: "auth", providerId: "openai" },
{ kind: "env", name: "OPENAI_API_KEY" },
{ kind: "config", path: ["plugins", "entries", "demo", "config"], check: "non-empty" },
{ kind: "plugin-enabled", pluginId: "demo" },
{ kind: "context", key: "channel", equals: "telegram" },
],
},
};
expect(
evaluateToolAvailability({
descriptor,
context: {
authProviderIds: new Set(["openai"]),
env: { OPENAI_API_KEY: "set" },
config: { plugins: { entries: { demo: { config: { mode: "local" } } } } },
enabledPluginIds: new Set(["demo"]),
values: { channel: "telegram" },
},
}),
).toEqual([]);
});
it("returns deterministic diagnostics for missing signals", () => {
const descriptor: ToolDescriptor = {
...baseDescriptor,
availability: {
allOf: [
{ kind: "auth", providerId: "openai" },
{ kind: "env", name: "OPENAI_API_KEY" },
{ kind: "config", path: ["plugins", "entries", "demo", "config"], check: "non-empty" },
{ kind: "plugin-enabled", pluginId: "demo" },
{ kind: "context", key: "channel", equals: "telegram" },
],
},
};
expect(
evaluateToolAvailability({
descriptor,
context: {
authProviderIds: new Set(),
env: {},
config: { plugins: { entries: { demo: { config: {} } } } },
enabledPluginIds: new Set(),
values: { channel: "discord" },
},
}).map((entry) => entry.reason),
).toEqual([
"auth-missing",
"env-missing",
"config-missing",
"plugin-disabled",
"context-mismatch",
]);
});
it("does not treat credential config values as available without an injected resolver", () => {
const descriptor: ToolDescriptor = {
...baseDescriptor,
availability: {
kind: "config",
path: ["models", "providers", "openai", "apiKey"],
check: "available",
},
};
expect(
evaluateToolAvailability({
descriptor,
context: {
config: {
models: {
providers: {
openai: {
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
},
},
},
env: {},
},
}).map((entry) => entry.reason),
).toEqual(["config-missing"]);
});
it("accepts credential config values only through an injected availability resolver", () => {
const descriptor: ToolDescriptor = {
...baseDescriptor,
availability: {
kind: "config",
path: ["models", "providers", "openai", "apiKey"],
check: "available",
},
};
expect(
evaluateToolAvailability({
descriptor,
context: {
config: {
models: {
providers: {
openai: {
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
},
},
},
env: { OPENAI_API_KEY: "set" },
isConfigValueAvailable: ({ value }) =>
isRecord(value) &&
value.source === "env" &&
value.provider === "default" &&
value.id === "OPENAI_API_KEY",
},
}),
).toEqual([]);
});
it("does not infer env-template strings as configured credentials", () => {
const descriptor: ToolDescriptor = {
...baseDescriptor,
availability: {
kind: "config",
path: ["models", "providers", "openai", "apiKey"],
check: "available",
},
};
expect(
evaluateToolAvailability({
descriptor,
context: {
config: {
models: {
providers: {
openai: { apiKey: "${OPENAI_API_KEY}" },
},
},
},
env: { OPENAI_API_KEY: "set" },
},
}).map((entry) => entry.reason),
).toEqual(["config-missing"]);
});
it("does not infer ordinary objects with source/provider/id fields as credentials", () => {
const descriptor: ToolDescriptor = {
...baseDescriptor,
availability: {
kind: "config",
path: ["tools", "example"],
check: "non-empty",
},
};
expect(
evaluateToolAvailability({
descriptor,
context: {
config: {
tools: {
example: { source: "manual", provider: "docs", id: "readme" },
},
},
},
}),
).toEqual([]);
});
it("supports anyOf availability expressions", () => {
const descriptor: ToolDescriptor = {
...baseDescriptor,
availability: {
anyOf: [
{ kind: "auth", providerId: "openai" },
{ kind: "env", name: "OPENAI_API_KEY" },
{
allOf: [
{ kind: "config", path: ["plugins", "entries", "local"], check: "non-empty" },
{ kind: "plugin-enabled", pluginId: "local" },
],
},
],
},
};
expect(
evaluateToolAvailability({
descriptor,
context: {
authProviderIds: new Set(),
env: { OPENAI_API_KEY: "set" },
enabledPluginIds: new Set(),
},
}),
).toEqual([]);
expect(
evaluateToolAvailability({
descriptor,
context: {
authProviderIds: new Set(),
env: {},
enabledPluginIds: new Set(),
},
}).map((entry) => entry.reason),
).toEqual(["auth-missing", "env-missing", "config-missing", "plugin-disabled"]);
});
});

View File

@@ -0,0 +1,45 @@
import { readdirSync, readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const toolsDir = new URL("./", import.meta.url);
const moduleReferencePattern =
/\b(?:import|export)\s+(?:type\s+)?(?:[^"'`]*?\s+from\s+)?["']([^"']+)["']/gu;
function collectStaticModuleReferences(
source: string,
): readonly { line: number; specifier: string }[] {
const references: { line: number; specifier: string }[] = [];
const lines = source.split("\n");
for (const [index, line] of lines.entries()) {
const trimmed = line.trimStart();
if (trimmed.startsWith("//")) {
continue;
}
for (const match of line.matchAll(moduleReferencePattern)) {
const specifier = match[1];
if (specifier) {
references.push({ line: index + 1, specifier });
}
}
}
return references;
}
describe("tool system boundary", () => {
it("keeps production tool modules independent from OpenClaw subsystems", () => {
const violations = readdirSync(toolsDir, { withFileTypes: true }).flatMap((entry) => {
if (!entry.isFile() || !entry.name.endsWith(".ts") || entry.name.endsWith(".test.ts")) {
return [];
}
const source = readFileSync(new URL(entry.name, toolsDir), "utf8");
return collectStaticModuleReferences(source)
.filter(
(reference) =>
!reference.specifier.startsWith("./") && !reference.specifier.startsWith("node:"),
)
.map((reference) => `${entry.name}:${reference.line} ${reference.specifier}`);
});
expect(violations).toEqual([]);
});
});

122
src/tools/planner.test.ts Normal file
View File

@@ -0,0 +1,122 @@
import { describe, expect, it } from "vitest";
import { ToolPlanContractError } from "./diagnostics.js";
import { formatToolExecutorRef } from "./execution.js";
import { buildToolPlan } from "./planner.js";
import { toToolProtocolDescriptors } from "./protocol.js";
import type { ToolDescriptor } from "./types.js";
function descriptor(name: string, overrides: Partial<ToolDescriptor> = {}): ToolDescriptor {
return {
name,
description: `${name} description`,
inputSchema: { type: "object" },
owner: { kind: "core" },
executor: { kind: "core", executorId: name },
...overrides,
};
}
describe("buildToolPlan", () => {
it("sorts visible and hidden tools deterministically", () => {
const plan = buildToolPlan({
descriptors: [
descriptor("zeta"),
descriptor("alpha"),
descriptor("hidden", {
sortKey: "middle",
availability: { kind: "env", name: "MISSING_ENV" },
}),
],
availability: { env: {} },
});
expect(plan.visible.map((entry) => entry.descriptor.name)).toEqual(["alpha", "zeta"]);
expect(plan.hidden.map((entry) => entry.descriptor.name)).toEqual(["hidden"]);
expect(plan.hidden[0]?.diagnostics.map((entry) => entry.reason)).toEqual(["env-missing"]);
});
it("fails deterministically on duplicate tool names", () => {
let error: unknown;
try {
buildToolPlan({
descriptors: [descriptor("read"), descriptor("read")],
});
} catch (caught) {
error = caught;
}
expect(error).toBeInstanceOf(ToolPlanContractError);
expect(error).toMatchObject({
code: "duplicate-tool-name",
toolName: "read",
});
});
it("fails closed when a visible descriptor has no executor", () => {
let error: unknown;
try {
buildToolPlan({
descriptors: [descriptor("read", { executor: undefined })],
});
} catch (caught) {
error = caught;
}
expect(error).toBeInstanceOf(ToolPlanContractError);
expect(error).toMatchObject({
code: "missing-executor",
toolName: "read",
});
});
it("does not require an executor for unavailable descriptors", () => {
const plan = buildToolPlan({
descriptors: [
descriptor("plugin_tool", {
executor: undefined,
availability: { kind: "plugin-enabled", pluginId: "demo" },
}),
],
availability: { enabledPluginIds: new Set() },
});
expect(plan.visible).toEqual([]);
expect(plan.hidden[0]?.descriptor.name).toBe("plugin_tool");
expect(plan.hidden[0]?.diagnostics[0]?.reason).toBe("plugin-disabled");
});
it("hides descriptors with malformed empty allOf availability", () => {
const plan = buildToolPlan({
descriptors: [descriptor("malformed", { availability: { allOf: [] } })],
});
expect(plan.visible).toEqual([]);
expect(plan.hidden[0]?.descriptor.name).toBe("malformed");
expect(plan.hidden[0]?.diagnostics).toEqual([
{
reason: "unsupported-signal",
message: "Empty availability allOf group",
},
]);
});
it("keeps protocol conversion separate from executor refs and model normalization", () => {
const plan = buildToolPlan({
descriptors: [
descriptor("plugin_tool", {
owner: { kind: "plugin", pluginId: "demo" },
executor: { kind: "plugin", pluginId: "demo", toolName: "plugin_tool" },
}),
],
});
expect(formatToolExecutorRef(plan.visible[0].executor)).toBe("plugin:demo:plugin_tool");
expect(toToolProtocolDescriptors(plan.visible)).toEqual([
{
name: "plugin_tool",
description: "plugin_tool description",
inputSchema: { type: "object" },
},
]);
});
});