Procházet zdrojové kódy

test(core): add CodeMode search fixture catalog

Aiden Cline před 1 měsícem
rodič
revize
180109d8b6

+ 2 - 0
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,

+ 355 - 0
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)
+  }),
+}

+ 2 - 0
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",

+ 50 - 0
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." })
+  }),
+)