File size: 5,914 Bytes
89a2873
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { describe, expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { CodeMode, Tool } from "../src/index.js"

// Key enumeration: Object.keys and for...in share one surface over plain objects, arrays
// (index strings), and tool references (namespace/tool names from the host tool tree), so a
// model can discover what it may call instead of guessing names from the instructions. The
// motivating transcript: `Object.keys(tools)` failed with the generic plain-objects-only
// message and `for (const key in tools)` was unsupported syntax, forcing blind guesses.

const echo = (description: string) =>
  Tool.make({
    description,
    input: Schema.Struct({ value: Schema.String }),
    output: Schema.String,
    run: ({ value }) => Effect.succeed(value),
  })

const tools = {
  github: { list_issues: echo("List issues"), get_issue: echo("Get one issue") },
  memory: { search: echo("Search memory") },
  playwright: { navigate: echo("Navigate somewhere") },
}

const run = (code: string) => Effect.runPromise(CodeMode.execute({ tools, code }))
const value = async (code: string) => {
  const result = await run(code)
  if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
  return result.value
}
const error = async (code: string) => {
  const result = await run(code)
  if (result.ok) throw new Error(`expected failure, got value ${JSON.stringify(result.value)}`)
  return result.error
}

describe("Object.keys over tool references", () => {
  test("enumerates top-level namespaces (the transcript program)", async () => {
    expect(
      await value(`
      const namespaces = Object.keys(tools)
      return { namespaces, count: namespaces.length }
    `),
    ).toEqual({ namespaces: ["github", "memory", "playwright", "$codemode"], count: 4 })
  })

  test("enumerates tool names at a nested namespace", async () => {
    expect(await value(`return Object.keys(tools.github)`)).toEqual(["list_issues", "get_issue"])
  })

  test("a callable tool is a leaf and enumerates as []", async () => {
    expect(await value(`return Object.keys(tools.github.list_issues)`)).toEqual([])
  })

  test("the internal discovery namespace enumerates its callable surface", async () => {
    expect(await value(`return Object.keys(tools.$codemode)`)).toEqual(["search"])
  })

  test("an unknown namespace is an UnknownTool error pointing at the discovery idioms", async () => {
    const failure = await error(`return Object.keys(tools.nonexistent)`)
    expect(failure.kind).toBe("UnknownTool")
    expect(failure.message).toContain("Unknown tool namespace 'nonexistent'")
    expect(failure.suggestions?.join(" ")).toContain("Object.keys(tools)")
  })

  test("Object.values/entries on a tool reference explain the working idioms", async () => {
    for (const method of ["values", "entries"] as const) {
      const failure = await error(`return Object.${method}(tools)`)
      expect(failure.kind).toBe("InvalidDataValue")
      expect(failure.message).toContain(
        `Object.${method}(...) cannot read tool references: they are not plain data. Use Object.keys(tools) for names, or tools.$codemode.search({ query }) for signatures.`,
      )
    }
    const nested = await error(`return Object.entries(tools.github)`)
    expect(nested.message).toContain("Use Object.keys(tools) for names")
  })
})

describe("Object.keys over arrays", () => {
  test("returns index strings, like JS", async () => {
    expect(await value(`return Object.keys(["a", "b", "c"])`)).toEqual(["0", "1", "2"])
    expect(await value(`return Object.keys([])`)).toEqual([])
  })

  test("objects keep their own enumerable keys", async () => {
    expect(await value(`return Object.keys({ a: 1, b: 2 })`)).toEqual(["a", "b"])
  })

  test("non-object inputs still fail clearly", async () => {
    const failure = await error(`return Object.keys("nope")`)
    expect(failure.message).toContain("Object.keys expects a data object or array")
  })
})

describe("for...in", () => {
  test("iterates own enumerable keys of a plain object with break/continue", async () => {
    expect(
      await value(`
      const seen = []
      for (const key in { a: 1, b: 2, c: 3, d: 4 }) {
        if (key === "b") continue
        if (key === "d") break
        seen.push(key)
      }
      return seen
    `),
    ).toEqual(["a", "c"])
  })

  test("iterates index strings over arrays", async () => {
    expect(
      await value(`
      const indexes = []
      for (const i in ["x", "y", "z"]) {
        if (i === "2") break
        indexes.push(i)
      }
      return indexes
    `),
    ).toEqual(["0", "1"])
  })

  test("supports let declarations and bare identifiers", async () => {
    expect(
      await value(`
      let last = ""
      for (let key in { a: 1, b: 2 }) last = key
      return last
    `),
    ).toBe("b")
    expect(
      await value(`
      let key = "before"
      for (key in { only: 1 }) {}
      return key
    `),
    ).toBe("only")
  })

  test("enumerates namespaces and tools from the callable tool tree", async () => {
    expect(
      await value(`
      const names = []
      for (const ns in tools) {
        for (const name in tools[ns]) names.push(ns + "." + name)
      }
      return names
    `),
    ).toEqual(["github.list_issues", "github.get_issue", "memory.search", "playwright.navigate", "$codemode.search"])
  })

  test("unsupported values fail with a hint at for...of and Object.keys", async () => {
    for (const expression of [`"text"`, "new Map([[1, 2]])", "new Set([1])", "42", "null"]) {
      const failure = await error(`for (const key in ${expression}) {}; return "no"`)
      expect(failure.message).toContain("for...in requires a plain object, array, or tools reference")
      expect(failure.message).toContain("Use for...of for arrays/strings/Maps/Sets, or Object.keys(value)")
    }
  })
})