From 180109d8b65c1b11b4e376288977b7062368cd84 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 10 Jul 2026 23:20:55 +0000 Subject: [PATCH] test(core): add CodeMode search fixture catalog --- packages/core/src/plugin/internal.ts | 2 + packages/core/src/tool/search-fixture.ts | 355 ++++++++++++++++++ packages/core/test/location-layer.test.ts | 2 + .../core/test/tool-search-fixture.test.ts | 50 +++ 4 files changed, 409 insertions(+) create mode 100644 packages/core/src/tool/search-fixture.ts create mode 100644 packages/core/test/tool-search-fixture.test.ts diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 9b75645db4b2..7d413912da25 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -38,6 +38,7 @@ import { QuestionTool } from "../tool/question" import { ReadToolFileSystem } from "../tool/read-filesystem" import { ReadTool } from "../tool/read" import { ShellTool } from "../tool/shell" +import { SearchFixtureTool } from "../tool/search-fixture" import { SkillTool } from "../tool/skill" import { SubagentTool } from "../tool/subagent" import { Tools } from "../tool/tools" @@ -129,6 +130,7 @@ const pre = [ GrepTool.Plugin, QuestionTool.Plugin, ReadTool.Plugin, + SearchFixtureTool.Plugin, ShellTool.Plugin, SkillTool.Plugin, SubagentTool.Plugin, diff --git a/packages/core/src/tool/search-fixture.ts b/packages/core/src/tool/search-fixture.ts new file mode 100644 index 000000000000..c76cdb99b2c4 --- /dev/null +++ b/packages/core/src/tool/search-fixture.ts @@ -0,0 +1,355 @@ +export * as SearchFixtureTool from "./search-fixture" + +import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin" +import { Effect, Schema } from "effect" +import { Tool } from "./tool" + +export const catalog = [ + { + namespace: "slack", + label: "Slack", + operations: [ + "send_message", + "schedule_message", + "create_channel", + "archive_channel", + "invite_user", + "add_reaction", + "pin_message", + "set_topic", + "start_huddle", + "export_thread", + ], + }, + { + namespace: "github", + label: "GitHub", + operations: [ + "create_issue", + "close_issue", + "assign_issue", + "label_issue", + "create_pull_request", + "request_review", + "merge_pull_request", + "create_release", + "dispatch_workflow", + "protect_branch", + ], + }, + { + namespace: "linear", + label: "Linear", + operations: [ + "create_issue", + "update_issue", + "assign_issue", + "move_issue", + "add_comment", + "create_project", + "update_project", + "create_cycle", + "add_label", + "archive_issue", + ], + }, + { + namespace: "notion", + label: "Notion", + operations: [ + "create_page", + "update_page", + "archive_page", + "search_workspace", + "append_blocks", + "create_database", + "add_database_row", + "update_database_row", + "duplicate_page", + "share_page", + ], + }, + { + namespace: "jira", + label: "Jira", + operations: [ + "create_ticket", + "transition_ticket", + "assign_ticket", + "add_comment", + "link_ticket", + "create_epic", + "add_to_sprint", + "log_work", + "set_priority", + "close_ticket", + ], + }, + { + namespace: "google_drive", + label: "Google Drive", + operations: [ + "create_folder", + "upload_file", + "move_file", + "copy_file", + "share_file", + "revoke_access", + "rename_file", + "trash_file", + "restore_file", + "search_files", + ], + }, + { + namespace: "gmail", + label: "Gmail", + operations: [ + "send_email", + "create_draft", + "reply_thread", + "forward_email", + "add_label", + "remove_label", + "archive_thread", + "mark_read", + "mark_unread", + "schedule_email", + ], + }, + { + namespace: "calendar", + label: "Google Calendar", + operations: [ + "create_event", + "update_event", + "cancel_event", + "invite_attendee", + "remove_attendee", + "find_availability", + "reserve_room", + "add_conference", + "set_reminder", + "list_events", + ], + }, + { + namespace: "stripe", + label: "Stripe", + operations: [ + "create_customer", + "update_customer", + "create_invoice", + "finalize_invoice", + "refund_payment", + "cancel_subscription", + "pause_subscription", + "resume_subscription", + "create_coupon", + "send_receipt", + ], + }, + { + namespace: "salesforce", + label: "Salesforce", + operations: [ + "create_lead", + "convert_lead", + "update_contact", + "create_account", + "update_opportunity", + "advance_opportunity", + "add_activity", + "create_case", + "close_case", + "assign_owner", + ], + }, + { + namespace: "hubspot", + label: "HubSpot", + operations: [ + "create_contact", + "update_contact", + "create_company", + "create_deal", + "move_deal_stage", + "enroll_workflow", + "add_note", + "create_task", + "merge_contacts", + "assign_owner", + ], + }, + { + namespace: "zendesk", + label: "Zendesk", + operations: [ + "create_ticket", + "update_ticket", + "assign_ticket", + "add_internal_note", + "reply_ticket", + "set_priority", + "add_tag", + "remove_tag", + "merge_ticket", + "solve_ticket", + ], + }, + { + namespace: "datadog", + label: "Datadog", + operations: [ + "create_monitor", + "mute_monitor", + "unmute_monitor", + "create_dashboard", + "add_dashboard_widget", + "annotate_event", + "create_incident", + "update_incident", + "resolve_incident", + "schedule_downtime", + ], + }, + { + namespace: "pagerduty", + label: "PagerDuty", + operations: [ + "trigger_incident", + "acknowledge_incident", + "resolve_incident", + "reassign_incident", + "add_responder", + "add_note", + "create_escalation_policy", + "override_schedule", + "create_maintenance_window", + "snooze_incident", + ], + }, + { + namespace: "aws", + label: "AWS", + operations: [ + "launch_instance", + "stop_instance", + "restart_instance", + "scale_service", + "deploy_lambda", + "invalidate_cache", + "rotate_secret", + "create_queue", + "publish_topic", + "snapshot_database", + ], + }, + { + namespace: "vercel", + label: "Vercel", + operations: [ + "create_project", + "deploy_project", + "promote_deployment", + "rollback_deployment", + "add_domain", + "remove_domain", + "set_env_variable", + "remove_env_variable", + "purge_cache", + "inspect_deployment", + ], + }, + { + namespace: "figma", + label: "Figma", + operations: [ + "create_file", + "duplicate_file", + "create_page", + "rename_layer", + "add_comment", + "resolve_comment", + "publish_library", + "create_branch", + "merge_branch", + "export_frame", + ], + }, + { + namespace: "airtable", + label: "Airtable", + operations: [ + "create_base", + "create_table", + "add_field", + "create_record", + "update_record", + "delete_record", + "find_records", + "link_records", + "create_view", + "sort_view", + ], + }, + { + namespace: "dropbox", + label: "Dropbox", + operations: [ + "upload_file", + "download_file", + "move_file", + "copy_file", + "delete_file", + "restore_file", + "share_link", + "revoke_link", + "create_folder", + "search_files", + ], + }, + { + namespace: "snowflake", + label: "Snowflake", + operations: [ + "create_warehouse", + "resize_warehouse", + "suspend_warehouse", + "resume_warehouse", + "create_database", + "create_schema", + "create_table", + "load_stage", + "run_query", + "grant_role", + ], + }, +] as const + +const Input = Schema.Struct({}) + +export const Plugin = { + id: "opencode.tool.search-fixture", + effect: Effect.fn("SearchFixtureTool.Plugin")(function* (ctx: PluginContext) { + yield* ctx.tool + .transform((draft) => { + catalog.forEach((group) => + group.operations.forEach((operation) => { + const action = operation.replaceAll("_", " ") + draft.add( + operation, + Tool.make({ + description: `${action[0]?.toUpperCase()}${action.slice(1)} in ${group.label}.`, + input: Input, + output: Schema.String, + execute: () => Effect.succeed(`Completed ${action}.`), + }), + { group: group.namespace, deferred: true }, + ) + }), + ) + }) + .pipe(Effect.orDie) + }), +} diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index d4ae3b7cf503..15d0e42a14b2 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -550,6 +550,7 @@ describe("LocationServiceMap", () => { expect(blockedState.providers.some((provider) => provider.id === allowedID)).toBe(false) expect(blockedState.tools.map((tool) => tool.name).sort()).toEqual([ "edit", + "execute", "glob", "grep", "patch", @@ -567,6 +568,7 @@ describe("LocationServiceMap", () => { expect(allowedState.providers.some((provider) => provider.id === blockedID)).toBe(false) expect(allowedState.tools.map((tool) => tool.name).sort()).toEqual([ "edit", + "execute", "glob", "grep", "patch", diff --git a/packages/core/test/tool-search-fixture.test.ts b/packages/core/test/tool-search-fixture.test.ts new file mode 100644 index 000000000000..c33d199b1db3 --- /dev/null +++ b/packages/core/test/tool-search-fixture.test.ts @@ -0,0 +1,50 @@ +import { expect } from "bun:test" +import { AgentV2 } from "@opencode-ai/core/agent" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { SessionV2 } from "@opencode-ai/core/session" +import { SessionMessage } from "@opencode-ai/core/session/message" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { SearchFixtureTool } from "@opencode-ai/core/tool/search-fixture" +import { Effect, Layer } from "effect" +import { testEffect } from "./lib/effect" +import { registerToolPlugin } from "./lib/tool" + +const outputStore = Layer.mock(ToolOutputStore.Service, { + bound: (input) => Effect.succeed({ output: input.output, outputPaths: [] }), +}) +const it = testEffect(AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])) + +it.effect("registers 200 searchable no-op tools across namespaces", () => + Effect.gen(function* () { + expect(SearchFixtureTool.catalog).toHaveLength(20) + expect(SearchFixtureTool.catalog.flatMap((group) => group.operations)).toHaveLength(200) + + yield* registerToolPlugin(SearchFixtureTool.Plugin) + const registry = yield* ToolRegistry.Service + const tools = yield* registry.materialize() + expect(tools.definitions.map((tool) => tool.name)).toEqual(["execute"]) + + const invoke = (code: string, id: string) => + tools.settle({ + sessionID: SessionV2.ID.make("ses_search_fixture"), + agent: AgentV2.ID.make("build"), + assistantMessageID: SessionMessage.ID.make("msg_search_fixture"), + call: { type: "tool-call", id, name: "execute", input: { code } }, + }) + + const searched = yield* invoke( + 'return await tools.$codemode.search({ query: "refund payment", namespace: "stripe" })', + "call_search_fixture", + ) + expect(searched.result.type).toBe("text") + if (searched.result.type !== "text") return + expect(JSON.parse(String(searched.result.value)).items[0]).toMatchObject({ + path: "tools.stripe.refund_payment", + description: "Refund payment in Stripe.", + }) + + const completed = yield* invoke("return await tools.stripe.refund_payment({})", "call_run_fixture") + expect(completed.result).toEqual({ type: "text", value: "Completed refund payment." }) + }), +)