tool-execute.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import { expect, test } from "bun:test"
  2. import { CodeModeTool } from "@opencode-ai/core/codemode/tool"
  3. import { Tool } from "@opencode-ai/core/tool"
  4. import { execute } from "@opencode-ai/core/tool/runtime"
  5. import { Agent } from "@opencode-ai/schema/agent"
  6. import { Session } from "@opencode-ai/schema/session"
  7. import { SessionMessage } from "@opencode-ai/schema/session-message"
  8. import type { Info } from "@opencode-ai/schema/tool"
  9. import { Effect, Schema } from "effect"
  10. const context = {
  11. sessionID: Session.ID.make("ses_execute"),
  12. agent: Agent.ID.make("build"),
  13. messageID: SessionMessage.ID.make("msg_execute"),
  14. callID: Tool.CallID.make("call_execute"),
  15. progress: () => Effect.void,
  16. }
  17. const createCodeMode = (tools: ReadonlyMap<string, Info>) =>
  18. CodeModeTool.create(tools, (_, tool, input, context) => execute(tool, input, context))
  19. test("execute describes invariant Code Mode behavior", () => {
  20. expect(createCodeMode(new Map()).description).toBe(
  21. [
  22. "Run JavaScript in a confined Code Mode runtime to orchestrate tool calls and compose their results.",
  23. "Imports, direct filesystem access, and timers are unavailable. Do not use `fetch`; all external access goes through `tools`.",
  24. "Within `{ code }`, the only callable tools are those explicitly listed in the Code Mode catalog instructions or returned by `search`. Inside `{ code }`, ignore tools shown outside the Code Mode catalog. They are not available in the Code Mode runtime.",
  25. 'Call tools through `tools` using only exact paths and signatures from the catalog. Do not infer or normalize tool names; preserve bracket notation such as `tools.<namespace>["tool-name"](input)`.',
  26. "Prefer an explicit `return`; if omitted, the final top-level expression becomes the result.",
  27. "Await every call whose completion matters; pending calls are interrupted when execution ends. Run independent calls concurrently with `Promise.all`.",
  28. ].join("\n"),
  29. )
  30. })
  31. test("canonical execution distinguishes declared, model-only, and raw schema outputs", async () => {
  32. const declared: Info = ({
  33. name: "declared",
  34. description: "Declared",
  35. input: Schema.Struct({ value: Schema.String }),
  36. output: Schema.Struct({ value: Schema.String }),
  37. execute: ({ value }) => Effect.succeed({ output: { value } }),
  38. })
  39. const modelOnlyInput = Schema.Struct({})
  40. const modelOnly = ({
  41. name: "model_only",
  42. description: "Model only",
  43. input: modelOnlyInput,
  44. execute: () => Effect.succeed({ content: "visible only", metadata: { kind: "model" } }),
  45. }) satisfies Info<typeof modelOnlyInput, undefined>
  46. const raw: Info = ({
  47. name: "raw",
  48. description: "Raw",
  49. input: {},
  50. output: {},
  51. execute: (input) => Effect.succeed({ output: input, content: "raw" }),
  52. })
  53. expect(await Effect.runPromise(execute(declared, { value: "encoded" }, context))).toEqual({
  54. output: { value: "encoded" },
  55. content: [{ type: "text", text: '{"value":"encoded"}' }],
  56. })
  57. expect(await Effect.runPromise(execute(modelOnly, {}, context))).toEqual({
  58. output: undefined,
  59. content: [{ type: "text", text: "visible only" }],
  60. metadata: { kind: "model" },
  61. })
  62. expect(await Effect.runPromise(execute(raw, { unchecked: true }, context))).toEqual({
  63. output: { unchecked: true },
  64. content: [{ type: "text", text: "raw" }],
  65. })
  66. })
  67. test("declared outputs cannot bypass validation and raw outputs stay JSON-compatible", async () => {
  68. const missing: Info = {
  69. name: "missing",
  70. description: "Missing output",
  71. input: Schema.Struct({}),
  72. output: Schema.String,
  73. execute: () => Effect.succeed({ content: "not an output" }),
  74. }
  75. const invalid: Info = {
  76. name: "invalid",
  77. description: "Invalid raw output",
  78. input: {},
  79. output: {},
  80. execute: () => Effect.succeed({ output: 1n, content: "not JSON" }),
  81. }
  82. expect((await Effect.runPromiseExit(execute(missing, {}, context))).toString()).toContain(
  83. "Tool did not return its declared output",
  84. )
  85. expect((await Effect.runPromiseExit(execute(invalid, {}, context))).toString()).toContain(
  86. "Tool returned a non-JSON value",
  87. )
  88. })
  89. test("execute supports callable namespace tools", async () => {
  90. const callable: Info = ({
  91. name: "admin",
  92. description: "Administer Slack",
  93. input: Schema.Struct({}),
  94. output: Schema.String,
  95. options: { namespace: "slack" },
  96. execute: () => Effect.succeed({ output: "admin" }),
  97. })
  98. const child: Info = ({
  99. name: "create",
  100. description: "Create a Slack resource",
  101. input: Schema.Struct({}),
  102. output: Schema.String,
  103. options: { namespace: "slack.admin" },
  104. execute: () => Effect.succeed({ output: "created" }),
  105. })
  106. const codeMode = createCodeMode(
  107. new Map([
  108. ["slack_admin", callable],
  109. ["slack_admin_create", child],
  110. ]),
  111. )
  112. const result = await Effect.runPromise(
  113. codeMode.execute(
  114. { code: "return [await tools.slack.admin({}), await tools.slack.admin.create({})]" },
  115. context,
  116. ),
  117. )
  118. expect(result.metadata).toEqual({
  119. toolCalls: [
  120. { tool: "slack.admin", status: "completed" },
  121. { tool: "slack.admin.create", status: "completed" },
  122. ],
  123. })
  124. expect(result.content).toEqual([{ type: "text", text: '[\n "admin",\n "created"\n]' }])
  125. })