Ctrl K
ComunidadGuías…

packages/codemode/test/tool-paths.test.ts

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, expect, test } from "bun:test"
import { Effect, Schema } from "effect"
import { CodeMode, Namespace, Tool } from "./lib/index.js"

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

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

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

describe("dotted tool names", () => {
  const runtime = CodeMode.make({ tools: { api: { "issues.list": echo("List issues", "listed") } } })

  test("a dotted name becomes nested namespaces in the catalog", () => {
    const catalog = runtime.catalog
    expect(catalog).toHaveLength(1)
    expect(catalog[0]?.path).toBe("api.issues.list")
    expect(catalog[0]?.signature).toStartWith("tools.api.issues.list(")
  })

  test("the advertised dotted path is executable", async () => {
    expect(await value(runtime, `return await tools.api.issues.list({})`)).toBe("listed")
  })

  test("bracket access with a dotted segment spells the same canonical path", async () => {
    expect(await value(runtime, `return await tools.api["issues.list"]({})`)).toBe("listed")
    expect(await value(runtime, `return await tools["api.issues"].list({})`)).toBe("listed")
  })

  test("intermediate segments enumerate like ordinary namespaces", async () => {
    expect(await value(runtime, `return [Object.keys(tools.api), Object.keys(tools.api.issues)]`)).toEqual([
      ["issues"],
      ["list"],
    ])
    expect(await value(runtime, `return Object.keys(tools["api.issues"])`)).toEqual(["list"])
  })

  test("a top-level dotted name nests from the root", async () => {
    const flat = CodeMode.make({ tools: { "issues.list": echo("List issues", "flat") } })
    expect(flat.catalog[0]?.path).toBe("issues.list")
    expect(await value(flat, `return await tools.issues.list({})`)).toBe("flat")
  })

  test("search scopes to a nested namespace subtree", async () => {
    const nested = CodeMode.make({
      tools: {
        slack: {
          admin: echo("Admin", "admin"),
          "admin.invite": echo("Invite", "invite"),
          "admin.users.list": echo("List users", "users"),
          "administrator.list": echo("List administrators", "administrators"),
          read: echo("Read Slack", "read"),
        },
      },
    })

    const result = await value(nested, `return search({ query: "", namespace: "slack.admin" })`)
    expect((result as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual([
      "tools.slack.admin",
      "tools.slack.admin.invite",
      "tools.slack.admin.users.list",
    ])
  })
})

describe("callable namespaces", () => {
  const runtime = CodeMode.make({
    tools: { issues: echo("All issues", "all"), "issues.list": echo("List issues", "list") },
  })

  test("a path can hold a tool and child tools at once", async () => {
    expect(await value(runtime, `return await tools.issues({})`)).toBe("all")
    expect(await value(runtime, `return await tools.issues.list({})`)).toBe("list")
    expect(runtime.catalog.map((tool) => tool.path)).toEqual(["issues", "issues.list"])
  })

  test("a callable namespace enumerates its children", async () => {
    expect(await value(runtime, `return Object.keys(tools.issues)`)).toEqual(["list"])
  })

  test("search returns executable paths for both", async () => {
    const result = await value(runtime, `return search({ query: "", namespace: "issues" })`)
    expect((result as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual([
      "tools.issues",
      "tools.issues.list",
    ])
    const exact = await value(runtime, `return search({ query: "tools.issues.list" })`)
    expect((exact as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual(["tools.issues.list"])
  })

  test("an unknown child under a callable tool is an UnknownTool error", async () => {
    const diagnostic = await failure(runtime, `return await tools.issues.missing({})`)
    expect(diagnostic.kind).toBe("UnknownTool")
    expect(diagnostic.message).toContain("Unknown tool 'issues.missing'")
    expect(diagnostic.suggestions).toEqual(["Use search to find available tools."])
  })

  test("an unknown tool names the closest match", async () => {
    const diagnostic = await failure(runtime, `return await tools.issues["get-list"]({})`)
    expect(diagnostic.message).toBe("Unknown tool 'issues.get-list'. Did you mean tools.issues.list?")
  })

  test("a namespace without its own tool stays non-callable", async () => {
    const nested = CodeMode.make({ tools: { "issues.list": echo("List issues", "list") } })
    const diagnostic = await failure(nested, `return await tools.issues({})`)
    expect(diagnostic.kind).toBe("UnknownTool")
    expect(diagnostic.message).toContain("Tool 'issues' is not callable")
  })
})

describe("tool input diagnostics", () => {
  const runtime = CodeMode.make({
    tools: {
      "notes.echo": Tool.make({
        description: "Echo text",
        input: Schema.Struct({ text: Schema.String }),
        output: Schema.String,
        execute: ({ text }) => Effect.succeed(text),
      }),
    },
  })

  test("a schema mismatch suggests searching for the current signature", async () => {
    const diagnostic = await failure(runtime, `return await tools.notes.echo({ message: "hello" })`)
    expect(diagnostic.kind).toBe("InvalidToolInput")
    expect(diagnostic.suggestions).toEqual(["The signature may have changed. Use search to get the current signature."])
  })

  test("a wrong argument count keeps the existing error without a stale-signature hint", async () => {
    const diagnostic = await failure(runtime, `return await tools.notes.echo({}, {})`)
    expect(diagnostic.kind).toBe("InvalidToolInput")
    expect(diagnostic.suggestions).toBeUndefined()
  })

  test("an empty-input tool advertises () and runs with zero arguments", async () => {
    const empty = CodeMode.make({ tools: { ping: echo("Ping", "pong") } })
    expect(empty.catalog[0]?.signature).toBe("tools.ping(): Promise<string>")
    expect(await value(empty, `return await tools.ping()`)).toBe("pong")
  })
})

describe("blocked member names on tool paths", () => {
  const runtime = CodeMode.make({
    tools: {
      prototype: echo("Prototype tool", "proto"),
      "issues.constructor": echo("Constructor tool", "ctor"),
      nested: { ["__proto__"]: echo("Proto tool", "dunder") },
    },
  })

  test("tools may use blocked member names because path segments never touch real properties", async () => {
    expect(runtime.catalog.map((tool) => tool.path)).toEqual(["issues.constructor", "nested.__proto__", "prototype"])
    expect(await value(runtime, `return await tools.prototype({})`)).toBe("proto")
    expect(await value(runtime, `return await tools.issues.constructor({})`)).toBe("ctor")
    expect(await value(runtime, `return await tools["issues.constructor"]({})`)).toBe("ctor")
    expect(await value(runtime, `return await tools.nested.__proto__({})`)).toBe("dunder")
    expect(await value(runtime, `return Object.keys(tools.issues)`)).toEqual(["constructor"])
  })

  test("a literal __proto__ key cannot poison a namespace into a fake tool", async () => {
    const poisoned = CodeMode.make({
      tools: { ns: { __proto__: echo("Hidden", "hidden"), real: echo("Real tool", "real") } },
    })
    expect(poisoned.catalog.map((tool) => tool.path)).toEqual(["ns.real"])
    expect(await value(poisoned, `return await tools.ns.real({})`)).toBe("real")
  })

  test("prototypes are program objects; __proto__ is an ordinary key and the host is unreachable", async () => {
    expect(
      await value(
        runtime,
        `
        const object = {}
        const array = []
        object.__proto__ = { polluted: true }
        return [
          object.constructor === Object, array.constructor === Array, "".constructor === String,
          Math.constructor === Object, object.__proto__.polluted, ({}).polluted, array.__proto__,
          Object().__proto__, new Object().constructor === Object, ({}).constructor.constructor === Function,
          [].constructor.__proto__, typeof [].__proto__, (() => 1).constructor === Function,
        ]
      `,
      ),
    ).toEqual([true, true, true, true, true, null, null, null, true, true, null, "undefined", true])
    const escape = await failure(runtime, `return ({}).constructor.constructor("return 1")()`)
    expect(escape.message).toContain("The Function constructor is not supported")
    const poisoned = await failure(runtime, `const o = {}; o.__proto__.constructor("return 1")`)
    expect(poisoned.message).toContain("Cannot read properties of undefined")
    // Prototype mutation is confined to one run: the next program starts from fresh intrinsics.
    expect(await value(runtime, `Object.prototype.polluted = 1; Array.prototype.push = 2; return ({}).polluted`)).toBe(
      1,
    )
    expect(await value(runtime, `return [({}).polluted, typeof [].push]`)).toEqual([null, "function"])
    expect(Object.keys(Object.prototype)).toEqual([])
    expect(Object.keys(Array.prototype)).toEqual([])
  })
})

describe("namespace metadata", () => {
  const tools = {
    api: Namespace.make({
      description: "Manage the workspace",
      tools: {
        users: Namespace.make({
          description: "Directory and account administration",
          tools: { list: echo("List users", "users") },
        }),
        status: echo("Read service status", "ok"),
      },
    }),
    plain: { read: echo("Read plain data", "plain") },
  }
  const runtime = CodeMode.make({ tools })

  test("the wrapper does not add a segment to callable paths", async () => {
    expect(runtime.catalog.map((tool) => tool.path)).toEqual(["api.status", "api.users.list", "plain.read"])
    expect(await value(runtime, `return await tools.api.users.list({})`)).toBe("users")
  })

  test("search matches descriptions from every enclosing namespace", async () => {
    const workspace = await value(runtime, `return search({ query: "workspace" })`)
    expect((workspace as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual([
      "tools.api.status",
      "tools.api.users.list",
    ])

    const directory = await value(runtime, `return search({ query: "account administration" })`)
    expect((directory as { items: Array<{ path: string }> }).items.map((item) => item.path)).toEqual([
      "tools.api.users.list",
    ])
  })

  test("a namespace description is optional", async () => {
    const optional = CodeMode.make({
      tools: { api: Namespace.make({ tools: { read: echo("Read data", "read") } }) },
    })
    expect(await value(optional, `return await tools.api.read({})`)).toBe("read")
  })
})

describe("empty segments", () => {
  test("tool names with empty segments are rejected at make", () => {
    for (const name of ["", "a..b", "trail.", ".lead"]) {
      expect(() => CodeMode.make({ tools: { [name]: echo("Bad", "bad") } })).toThrow("empty segment")
    }
  })
})

describe("canonical path collisions", () => {
  test("the last tool supplied for a canonical path wins", async () => {
    const runtime = CodeMode.make({
      tools: { "issues.list": echo("First", "first"), issues: { list: echo("Second", "second") } },
    })
    expect(await value(runtime, `return await tools.issues.list({})`)).toBe("second")
    expect(runtime.catalog).toHaveLength(1)
    expect(runtime.catalog[0]?.description).toBe("Second")
  })

  test("overriding one path keeps sibling tools from both shapes", async () => {
    const runtime = CodeMode.make({
      tools: {
        "issues.list": echo("First list", "first"),
        issues: { list: echo("Second list", "second"), get: echo("Get issue", "got") },
        "issues.close": echo("Close issue", "closed"),
      },
    })
    expect(runtime.catalog.map((tool) => tool.path)).toEqual(["issues.close", "issues.get", "issues.list"])
    expect(await value(runtime, `return await tools.issues.list({})`)).toBe("second")
    expect(await value(runtime, `return await tools.issues.get({})`)).toBe("got")
    expect(await value(runtime, `return await tools.issues.close({})`)).toBe("closed")
  })
})

describe("tool argument prototype safety", () => {
  test("a __proto__ key never reaches tool code", async () => {
    let seen: unknown
    const runtime = CodeMode.make({
      tools: {
        inspect: Tool.make({
          description: "Inspect",
          input: Schema.Struct({ v: Schema.Unknown }),
          output: Schema.Unknown,
          execute: (input) =>
            Effect.sync(() => {
              seen = (input as { v: unknown }).v
              return null
            }),
        }),
      },
    })
    await value(runtime, `return await tools.inspect({ v: { __proto__: { polluted: true }, a: 1 } })`)
    expect(seen).toEqual({ a: 1 })
    const merged = Object.assign({}, seen as Record<string, unknown>)
    expect(merged.polluted).toBeUndefined()
    expect(Object.getPrototypeOf(merged)).toBe(Object.prototype)
  })

  test("program results drop __proto__ keys", async () => {
    const runtime = CodeMode.make({ tools: {} })
    expect(await value(runtime, `return { __proto__: { polluted: true }, a: 2 }`)).toEqual({ a: 2 })
    expect(await value(runtime, `return [{ __proto__: 1 }]`)).toEqual([{}])
  })
})

describe("tool arguments cross in a useful form where JSON.stringify would give {}", () => {
  test("Set and URLSearchParams; RegExp and Map stay {} like JSON", async () => {
    let seen: unknown
    const runtime = CodeMode.make({
      tools: {
        inspect: Tool.make({
          description: "Inspect",
          input: Schema.Struct({ v: Schema.Unknown }),
          output: Schema.Unknown,
          execute: (input) =>
            Effect.sync(() => {
              seen = input.v
              return null
            }),
        }),
      },
    })
    await value(
      runtime,
      `return await tools.inspect({ v: { s: new Set([1, 2]), r: /x/g, p: new URLSearchParams("a=1&b=2"), m: new Map([["k", 1]]) } })`,
    )
    expect(seen).toEqual({ s: [1, 2], r: {}, p: "a=1&b=2", m: {} })
  })
})

describe("tools.search alias", () => {
  test("tools.search(...) behaves like the bare search(...) when no tool owns that path", async () => {
    const runtime = CodeMode.make({ tools: { api: { list: echo("List things", "listed") } } })
    const direct = await value(runtime, `return search({ query: "list" })`)
    expect(await value(runtime, `return tools.search({ query: "list" })`)).toStrictEqual(direct)
    expect(await value(runtime, `return (await tools.search({ query: "list" })).items[0].path`)).toBe("tools.api.list")
  })

  test("a registered root-level search tool takes precedence", async () => {
    const runtime = CodeMode.make({ tools: { search: echo("Custom search", "custom") } })
    expect(await value(runtime, `return await tools.search({})`)).toBe("custom")
  })
})

describe("search results used like a Promise", () => {
  const runtime = CodeMode.make({ tools: { api: { list: echo("List things", "listed") } } })

  test("search(...) can be read, awaited, or chained", async () => {
    const call = "search"
    expect(
      await value(
        runtime,
        `
        const query = { query: "list" }
        return [
          ${call}(query).items[0].path,
          (await ${call}(query)).items[0].path,
          await ${call}(query).then((result) => result.items[0].path),
          (await ${call}(query).catch(() => "unused")).items[0].path,
          (await ${call}(query).finally(() => "ignored")).items[0].path,
          (await Promise.all([${call}(query), ${call}({})])).map((result) => result.items.length),
        ]
        `,
      ),
    ).toStrictEqual(["tools.api.list", "tools.api.list", "tools.api.list", "tools.api.list", "tools.api.list", [1, 1]])
  })

  test("the Promise methods stay out of data views", async () => {
    const expected = { items: [{ path: "tools.api.list" }], remaining: 0, next: null }
    const result = await value(
      runtime,
      `
      const result = search({ query: "list" })
      const forIn = []
      for (const key in result) forIn.push(key)
      return {
        json: JSON.parse(JSON.stringify(result)),
        keys: Object.keys(result),
        spread: Object.keys({ ...result }),
        forIn,
        returned: result,
      }
      `,
    )
    expect(result).toMatchObject({
      json: expected,
      keys: ["items", "remaining", "next"],
      spread: ["items", "remaining", "next"],
      forIn: ["items", "remaining", "next"],
      returned: expected,
    })
  })

  test("invalid input throws at the call, so try/catch sees it in either style", async () => {
    expect((await failure(runtime, `return search({ limit: "ten" }).items`)).message).toContain(
      "Invalid input for tool 'search'",
    )
    expect(
      await value(runtime, `try { await search({ limit: "ten" }) } catch (error) { return error.message }`),
    ).toContain("Invalid input for tool 'search'")
  })
})

describe("tool references under ==", () => {
  test("compare by identity against data objects without converting them", async () => {
    const runtime = CodeMode.make({ tools: { probe: echo("Probe", "ok") } })
    expect(
      await value(
        runtime,
        `let calls = 0
         const o = { valueOf() { calls++; return 1 } }
         return [o == tools.probe, tools == { a: 1 }, tools.probe == null, calls]`,
      ),
    ).toEqual([false, false, false, 0])
  })
})