5ff277349c
- Added the `xd://` virtual device protocol (`internal-urls/xd-protocol.ts`, `tools/xdev.ts`): tools declaring `loadMode: "discoverable"` are unmounted from the request tools array and driven via `read xd://` (list/docs+schema) and `write xd://<tool>` (execute), gated by the `tools.xdev` setting (default on) and inlined into the system prompt. - Merged the `irc`, `job`, and `launch` tools into a single `hub` tool (`tools/hub/`, `async/job-manager.ts`): messaging keeps `send`/`inbox`/`list`, job control maps to `wait`/`cancel`/`jobs`, process supervision keeps `start`/`logs`/`stop`/`restart`/`describe` with `ps`, and the unified `wait` races background jobs against peer messages; SDK `IrcTool`/`JobTool`/`LaunchTool` are replaced by `HubTool`. - Removed the hidden `resolve` tool in favor of the `xd://resolve`/`xd://reject`/`xd://propose` resolution devices, auto-including `write` whenever a deferrable tool or plan mode is present. - Removed the BM25 tool-discovery system: the `search_tool_bm25` tool, the `tool-discovery` module, the `tools.discoveryMode`/`mcp.discoveryMode`/`mcp.discoveryDefaultServers`/`tools.essentialOverride` settings, per-tool MCP selection, and the `mcp_tool_selection` message type. - Unified tool presentation on `ToolLoadMode` (`essential`|`discoverable`), replacing the custom-tool `xdev?: boolean` opt-out; custom, extension, MCP, RPC host, image-generation, and TTS tools now default to `discoverable`, and added a `satisfies` predicate to `SoftToolRequirement`. - Removed the standalone `ssh` command tool and `ssh/ssh-executor` (the `ssh://` read/write/search protocol stays), and made `--tools` address hidden built-ins. - Updated collab-web to render `xd://` dispatches and `hub` op families, dropped the `search_tool_bm25`/`ssh`/`report-finding` renderers, refreshed tool docs and prompts, and migrated the affected tests and changelogs.
164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
adaptSchemaForStrict,
|
|
normalizeSchemaForCCA,
|
|
normalizeSchemaForGoogle,
|
|
type SchemaCompatibilityProvider,
|
|
type SchemaCompatibilityResult,
|
|
toolWireSchema,
|
|
validateSchemaCompatibility,
|
|
validateStrictSchemaEnforcement,
|
|
} from "@oh-my-pi/pi-ai/utils/schema";
|
|
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import { createTools, HIDDEN_TOOLS, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
|
import { createVibeTools } from "@oh-my-pi/pi-coding-agent/tools/vibe";
|
|
|
|
interface ToolSchemaEntry {
|
|
name: string;
|
|
schema: Record<string, unknown>;
|
|
}
|
|
|
|
function createTestSession(): ToolSession {
|
|
return {
|
|
cwd: "/tmp/test",
|
|
hasUI: true,
|
|
getSessionFile: () => null,
|
|
getSessionSpawns: () => "*",
|
|
settings: Settings.isolated({ "tools.xdev": false }),
|
|
};
|
|
}
|
|
|
|
function asSchemaObject(value: unknown): Record<string, unknown> | null {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
async function collectToolSchemas(): Promise<ToolSchemaEntry[]> {
|
|
const session = createTestSession();
|
|
const byToolName = new Map<string, Record<string, unknown>>();
|
|
|
|
for (const tool of await createTools(session)) {
|
|
const schema = toolWireSchema(tool);
|
|
if (!asSchemaObject(schema)) {
|
|
continue;
|
|
}
|
|
byToolName.set(tool.name, schema);
|
|
}
|
|
|
|
for (const [name, factory] of Object.entries(HIDDEN_TOOLS)) {
|
|
const tool = await factory(session);
|
|
if (!tool) {
|
|
continue;
|
|
}
|
|
const schema = toolWireSchema(tool);
|
|
if (!asSchemaObject(schema)) {
|
|
continue;
|
|
}
|
|
byToolName.set(name, schema);
|
|
}
|
|
|
|
for (const tool of createVibeTools(session)) {
|
|
const schema = toolWireSchema(tool);
|
|
if (!asSchemaObject(schema)) {
|
|
continue;
|
|
}
|
|
byToolName.set(tool.name, schema);
|
|
}
|
|
|
|
return [...byToolName.entries()]
|
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
.map(([name, schema]) => ({ name, schema }));
|
|
}
|
|
|
|
function formatCompatibilityIssues(
|
|
toolName: string,
|
|
provider: SchemaCompatibilityProvider,
|
|
result: SchemaCompatibilityResult,
|
|
): string {
|
|
if (result.compatible) {
|
|
return "";
|
|
}
|
|
const details = result.violations
|
|
.map(violation => ` - ${violation.rule} at ${violation.path}: ${violation.message}`)
|
|
.join("\n");
|
|
return `${toolName} (${provider}):\n${details}`;
|
|
}
|
|
|
|
describe("builtin tool schemas provider compatibility", () => {
|
|
it("keeps task and todo strict-compatible for OpenAI-style providers", async () => {
|
|
const toolSchemas = await collectToolSchemas();
|
|
for (const toolName of ["task", "todo"]) {
|
|
const entry = toolSchemas.find(tool => tool.name === toolName);
|
|
expect(entry).toBeDefined();
|
|
if (!entry) {
|
|
continue;
|
|
}
|
|
const strictResult = adaptSchemaForStrict(entry.schema, true);
|
|
expect(strictResult.strict).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("keeps all builtin and hidden tool schemas valid after provider enforcement", async () => {
|
|
const toolSchemas = await collectToolSchemas();
|
|
const failures: string[] = [];
|
|
|
|
for (const { name, schema } of toolSchemas) {
|
|
const strictResult = adaptSchemaForStrict(schema, true);
|
|
const strictCompatibility = validateStrictSchemaEnforcement(schema, strictResult);
|
|
if (!strictCompatibility.compatible) {
|
|
failures.push(formatCompatibilityIssues(name, "openai-strict", strictCompatibility));
|
|
}
|
|
|
|
try {
|
|
const googleSchema = normalizeSchemaForGoogle(schema);
|
|
const googleCompatibility = validateSchemaCompatibility(googleSchema, "google");
|
|
if (!googleCompatibility.compatible) {
|
|
failures.push(formatCompatibilityIssues(name, "google", googleCompatibility));
|
|
}
|
|
} catch (error) {
|
|
failures.push(`${name} (google): normalizeSchemaForGoogle threw: ${String(error)}`);
|
|
}
|
|
|
|
const cloudCodeAssistSchema = normalizeSchemaForCCA(schema);
|
|
const cloudCodeAssistCompatibility = validateSchemaCompatibility(
|
|
cloudCodeAssistSchema,
|
|
"cloud-code-assist-claude",
|
|
);
|
|
if (!cloudCodeAssistCompatibility.compatible) {
|
|
failures.push(formatCompatibilityIssues(name, "cloud-code-assist-claude", cloudCodeAssistCompatibility));
|
|
}
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
throw new Error(`Provider compatibility failures:\n\n${failures.join("\n\n")}`);
|
|
}
|
|
|
|
expect(failures).toEqual([]);
|
|
});
|
|
|
|
it("preserves the yield result schema for Cloud Code Assist", async () => {
|
|
const toolSchemas = await collectToolSchemas();
|
|
const yieldEntry = toolSchemas.find(tool => tool.name === "yield");
|
|
expect(yieldEntry).toBeDefined();
|
|
if (!yieldEntry) return;
|
|
|
|
const normalized = asSchemaObject(normalizeSchemaForCCA(yieldEntry.schema));
|
|
const properties = asSchemaObject(normalized?.properties);
|
|
const typeSchema = asSchemaObject(properties?.type);
|
|
|
|
expect(normalized?.type).toBe("object");
|
|
expect(properties?.result).toBeDefined();
|
|
expect(typeSchema?.type).toBe("string");
|
|
expect(typeSchema?.anyOf).toBeUndefined();
|
|
});
|
|
|
|
it('asserts that browser tool schema root stays `type: "object"` when discoverable tools are mounted', async () => {
|
|
const toolSchemas = await collectToolSchemas();
|
|
const browserEntry = toolSchemas.find(tool => tool.name === "browser");
|
|
expect(browserEntry).toBeDefined();
|
|
expect(asSchemaObject(browserEntry?.schema)?.type).toBe("object");
|
|
});
|
|
});
|