enumeration.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { describe, expect, test } from "bun:test"
  2. import { Effect, Schema } from "effect"
  3. import { CodeMode, Tool } from "../src/index.js"
  4. // Key enumeration: Object.keys and for...in share one surface over plain objects, arrays
  5. // (index strings), and tool references (namespace/tool names from the host tool tree), so a
  6. // model can discover what it may call instead of guessing names from the instructions. The
  7. // motivating transcript: `Object.keys(tools)` failed with the generic plain-objects-only
  8. // message and `for (const key in tools)` was unsupported syntax, forcing blind guesses.
  9. const echo = (description: string) =>
  10. Tool.make({
  11. description,
  12. input: Schema.Struct({ value: Schema.String }),
  13. output: Schema.String,
  14. run: ({ value }) => Effect.succeed(value),
  15. })
  16. const tools = {
  17. github: { list_issues: echo("List issues"), get_issue: echo("Get one issue") },
  18. memory: { search: echo("Search memory") },
  19. playwright: { navigate: echo("Navigate somewhere") },
  20. }
  21. const run = (code: string) => Effect.runPromise(CodeMode.execute({ tools, code }))
  22. const value = async (code: string) => {
  23. const result = await run(code)
  24. if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
  25. return result.value
  26. }
  27. const error = async (code: string) => {
  28. const result = await run(code)
  29. if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
  30. return result.error
  31. }
  32. describe("Object.keys over tool references", () => {
  33. test("enumerates top-level namespaces (the transcript program)", async () => {
  34. expect(
  35. await value(`
  36. const namespaces = Object.keys(tools)
  37. return { namespaces, count: namespaces.length }
  38. `),
  39. ).toEqual({ namespaces: ["github", "memory", "playwright", "$codemode"], count: 4 })
  40. })
  41. test("enumerates tool names at a nested namespace", async () => {
  42. expect(await value(`return Object.keys(tools.github)`)).toEqual(["list_issues", "get_issue"])
  43. })
  44. test("a callable tool is a leaf and enumerates as []", async () => {
  45. expect(await value(`return Object.keys(tools.github.list_issues)`)).toEqual([])
  46. })
  47. test("the internal discovery namespace enumerates its callable surface", async () => {
  48. expect(await value(`return Object.keys(tools.$codemode)`)).toEqual(["search"])
  49. })
  50. test("an unknown namespace is an UnknownTool error pointing at the discovery idioms", async () => {
  51. const failure = await error(`return Object.keys(tools.nonexistent)`)
  52. expect(failure.kind).toBe("UnknownTool")
  53. expect(failure.message).toContain("Unknown tool namespace 'nonexistent'")
  54. expect(failure.suggestions?.join(" ")).toContain("Object.keys(tools)")
  55. })
  56. test("Object.values/entries on a tool reference explain the working idioms", async () => {
  57. for (const method of ["values", "entries"] as const) {
  58. const failure = await error(`return Object.${method}(tools)`)
  59. expect(failure.kind).toBe("InvalidDataValue")
  60. expect(failure.message).toContain(
  61. `Object.${method}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or tools.$codemode.search({ query }) for signatures.`,
  62. )
  63. }
  64. const nested = await error(`return Object.entries(tools.github)`)
  65. expect(nested.message).toContain("Use Object.keys(tools) for names")
  66. })
  67. })
  68. describe("Object.keys over arrays", () => {
  69. test("returns index strings, like JS", async () => {
  70. expect(await value(`return Object.keys(["a", "b", "c"])`)).toEqual(["0", "1", "2"])
  71. expect(await value(`return Object.keys([])`)).toEqual([])
  72. })
  73. test("objects keep their own enumerable keys", async () => {
  74. expect(await value(`return Object.keys({ a: 1, b: 2 })`)).toEqual(["a", "b"])
  75. })
  76. test("non-object inputs still fail clearly", async () => {
  77. const failure = await error(`return Object.keys("nope")`)
  78. expect(failure.message).toContain("Object.keys expects a data object or array")
  79. })
  80. })
  81. describe("for...in", () => {
  82. test("iterates own enumerable keys of a plain object with break/continue", async () => {
  83. expect(
  84. await value(`
  85. const seen = []
  86. for (const key in { a: 1, b: 2, c: 3, d: 4 }) {
  87. if (key === "b") continue
  88. if (key === "d") break
  89. seen.push(key)
  90. }
  91. return seen
  92. `),
  93. ).toEqual(["a", "c"])
  94. })
  95. test("iterates index strings over arrays", async () => {
  96. expect(
  97. await value(`
  98. const indexes = []
  99. for (const i in ["x", "y", "z"]) {
  100. if (i === "2") break
  101. indexes.push(i)
  102. }
  103. return indexes
  104. `),
  105. ).toEqual(["0", "1"])
  106. })
  107. test("supports let declarations and bare identifiers", async () => {
  108. expect(
  109. await value(`
  110. let last = ""
  111. for (let key in { a: 1, b: 2 }) last = key
  112. return last
  113. `),
  114. ).toBe("b")
  115. expect(
  116. await value(`
  117. let key = "before"
  118. for (key in { only: 1 }) {}
  119. return key
  120. `),
  121. ).toBe("only")
  122. })
  123. test("enumerates namespaces and tools from the callable tool tree", async () => {
  124. expect(
  125. await value(`
  126. const names = []
  127. for (const ns in tools) {
  128. for (const name in tools[ns]) names.push(ns + "." + name)
  129. }
  130. return names
  131. `),
  132. ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate", "$codemode.search"])
  133. })
  134. test("unsupported values fail with a hint at for...of and Object.keys", async () => {
  135. for (const expression of [`"text"`, "new Map([[1, 2]])", "new Set([1])", "42", "null"]) {
  136. const failure = await error(`for (const key in ${expression}) {}; return "no"`)
  137. expect(failure.message).toContain("for...in requires a plain object, array, or tools reference")
  138. expect(failure.message).toContain("Use for...of for arrays/strings/Maps/Sets, or Object.keys(value)")
  139. }
  140. })
  141. })