Tool.test.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { describe, expect, test } from "bun:test";
  2. import {
  3. buildTool,
  4. toolMatchesName,
  5. findToolByName,
  6. getEmptyToolPermissionContext,
  7. filterToolProgressMessages,
  8. } from "../Tool";
  9. // Minimal tool definition for testing buildTool
  10. function makeMinimalToolDef(overrides: Record<string, unknown> = {}) {
  11. return {
  12. name: "TestTool",
  13. inputSchema: { type: "object" as const } as any,
  14. maxResultSizeChars: 10000,
  15. call: async () => ({ data: "ok" }),
  16. description: async () => "A test tool",
  17. prompt: async () => "test prompt",
  18. mapToolResultToToolResultBlockParam: (content: unknown, toolUseID: string) => ({
  19. type: "tool_result" as const,
  20. tool_use_id: toolUseID,
  21. content: String(content),
  22. }),
  23. renderToolUseMessage: () => null,
  24. ...overrides,
  25. };
  26. }
  27. describe("buildTool", () => {
  28. test("fills in default isEnabled as true", () => {
  29. const tool = buildTool(makeMinimalToolDef());
  30. expect(tool.isEnabled()).toBe(true);
  31. });
  32. test("fills in default isConcurrencySafe as false", () => {
  33. const tool = buildTool(makeMinimalToolDef());
  34. expect(tool.isConcurrencySafe({})).toBe(false);
  35. });
  36. test("fills in default isReadOnly as false", () => {
  37. const tool = buildTool(makeMinimalToolDef());
  38. expect(tool.isReadOnly({})).toBe(false);
  39. });
  40. test("fills in default isDestructive as false", () => {
  41. const tool = buildTool(makeMinimalToolDef());
  42. expect(tool.isDestructive!({})).toBe(false);
  43. });
  44. test("fills in default checkPermissions as allow", async () => {
  45. const tool = buildTool(makeMinimalToolDef());
  46. const input = { foo: "bar" };
  47. const result = await tool.checkPermissions(input, {} as any);
  48. expect(result).toEqual({ behavior: "allow", updatedInput: input });
  49. });
  50. test("fills in default userFacingName from tool name", () => {
  51. const tool = buildTool(makeMinimalToolDef());
  52. expect(tool.userFacingName(undefined)).toBe("TestTool");
  53. });
  54. test("fills in default toAutoClassifierInput as empty string", () => {
  55. const tool = buildTool(makeMinimalToolDef());
  56. expect(tool.toAutoClassifierInput({})).toBe("");
  57. });
  58. test("preserves explicitly provided methods", () => {
  59. const tool = buildTool(
  60. makeMinimalToolDef({
  61. isEnabled: () => false,
  62. isConcurrencySafe: () => true,
  63. isReadOnly: () => true,
  64. })
  65. );
  66. expect(tool.isEnabled()).toBe(false);
  67. expect(tool.isConcurrencySafe({})).toBe(true);
  68. expect(tool.isReadOnly({})).toBe(true);
  69. });
  70. test("preserves all non-defaultable properties", () => {
  71. const tool = buildTool(makeMinimalToolDef());
  72. expect(tool.name).toBe("TestTool");
  73. expect(tool.maxResultSizeChars).toBe(10000);
  74. expect(typeof tool.call).toBe("function");
  75. expect(typeof tool.description).toBe("function");
  76. expect(typeof tool.prompt).toBe("function");
  77. });
  78. });
  79. describe("toolMatchesName", () => {
  80. test("returns true for exact name match", () => {
  81. expect(toolMatchesName({ name: "Bash" }, "Bash")).toBe(true);
  82. });
  83. test("returns false for non-matching name", () => {
  84. expect(toolMatchesName({ name: "Bash" }, "Read")).toBe(false);
  85. });
  86. test("returns true when name matches an alias", () => {
  87. expect(
  88. toolMatchesName({ name: "Bash", aliases: ["BashTool", "Shell"] }, "BashTool")
  89. ).toBe(true);
  90. });
  91. test("returns false when aliases is undefined", () => {
  92. expect(toolMatchesName({ name: "Bash" }, "BashTool")).toBe(false);
  93. });
  94. test("returns false when aliases is empty", () => {
  95. expect(
  96. toolMatchesName({ name: "Bash", aliases: [] }, "BashTool")
  97. ).toBe(false);
  98. });
  99. });
  100. describe("findToolByName", () => {
  101. const mockTools = [
  102. buildTool(makeMinimalToolDef({ name: "Bash" })),
  103. buildTool(makeMinimalToolDef({ name: "Read", aliases: ["FileRead"] })),
  104. buildTool(makeMinimalToolDef({ name: "Edit" })),
  105. ];
  106. test("finds tool by primary name", () => {
  107. const tool = findToolByName(mockTools, "Bash");
  108. expect(tool).toBeDefined();
  109. expect(tool!.name).toBe("Bash");
  110. });
  111. test("finds tool by alias", () => {
  112. const tool = findToolByName(mockTools, "FileRead");
  113. expect(tool).toBeDefined();
  114. expect(tool!.name).toBe("Read");
  115. });
  116. test("returns undefined when no match", () => {
  117. expect(findToolByName(mockTools, "NonExistent")).toBeUndefined();
  118. });
  119. test("returns first match when duplicates exist", () => {
  120. const dupeTools = [
  121. buildTool(makeMinimalToolDef({ name: "Bash", maxResultSizeChars: 100 })),
  122. buildTool(makeMinimalToolDef({ name: "Bash", maxResultSizeChars: 200 })),
  123. ];
  124. const tool = findToolByName(dupeTools, "Bash");
  125. expect(tool!.maxResultSizeChars).toBe(100);
  126. });
  127. });
  128. describe("getEmptyToolPermissionContext", () => {
  129. test("returns default permission mode", () => {
  130. const ctx = getEmptyToolPermissionContext();
  131. expect(ctx.mode).toBe("default");
  132. });
  133. test("returns empty maps and arrays", () => {
  134. const ctx = getEmptyToolPermissionContext();
  135. expect(ctx.additionalWorkingDirectories.size).toBe(0);
  136. expect(ctx.alwaysAllowRules).toEqual({});
  137. expect(ctx.alwaysDenyRules).toEqual({});
  138. expect(ctx.alwaysAskRules).toEqual({});
  139. });
  140. test("returns isBypassPermissionsModeAvailable as false", () => {
  141. const ctx = getEmptyToolPermissionContext();
  142. expect(ctx.isBypassPermissionsModeAvailable).toBe(false);
  143. });
  144. });
  145. describe("filterToolProgressMessages", () => {
  146. test("filters out hook_progress messages", () => {
  147. const messages = [
  148. { data: { type: "hook_progress", hookName: "pre" } },
  149. { data: { type: "tool_progress", toolName: "Bash" } },
  150. ] as any[];
  151. const result = filterToolProgressMessages(messages);
  152. expect(result).toHaveLength(1);
  153. expect((result[0]!.data as any).type).toBe("tool_progress");
  154. });
  155. test("keeps tool progress messages", () => {
  156. const messages = [
  157. { data: { type: "tool_progress", toolName: "Bash" } },
  158. { data: { type: "tool_progress", toolName: "Read" } },
  159. ] as any[];
  160. const result = filterToolProgressMessages(messages);
  161. expect(result).toHaveLength(2);
  162. });
  163. test("returns empty array for empty input", () => {
  164. expect(filterToolProgressMessages([])).toEqual([]);
  165. });
  166. test("handles messages without type field", () => {
  167. const messages = [
  168. { data: { toolName: "Bash" } },
  169. { data: { type: "hook_progress" } },
  170. ] as any[];
  171. const result = filterToolProgressMessages(messages);
  172. expect(result).toHaveLength(1);
  173. });
  174. });