Ctrl K
ComunidadGuías…

packages/codemode/test/limits.test.ts

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

Archivo original ↗Preguntar a KiBot
import { describe, expect, test } from "bun:test"
import { Cause, Deferred, Effect, Exit, Fiber, Schema } from "effect"
import { CodeMode, Tool } from "./lib/index.js"
import { MAX_ARRAY_LENGTH, MAX_PENDING_PROMISES, MAX_STRING_LENGTH } from "../src/interpreter/limits.js"

const runtime = CodeMode.make({ tools: {} })

const value = async (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 (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.message
}

const S = MAX_STRING_LENGTH
const A = MAX_ARRAY_LENGTH

describe("one built-in cannot build an unbounded value", () => {
  test("strings: repeat, pad, concat, join, +, template, JSON.stringify", async () => {
    expect(await value(`return "x".repeat(${S}).length`)).toBe(S)
    for (const code of [
      `"x".repeat(${S + 1})`,
      `"x".padStart(${S + 1})`,
      `"x".padEnd(${S + 1}, "y")`,
      `"x".repeat(${S}).concat("y")`,
      `["x".repeat(${S}), "y"].join("")`,
      `"x".repeat(${S}) + "y"`,
      `\`\${"x".repeat(${S})}y\``,
      `JSON.stringify("x".repeat(${S}))`,
    ]) {
      expect(await failure(`try { ${code} } catch (e) { throw Error(e.name + ": " + e.message) }`)).toContain(
        "RangeError: Invalid string length",
      )
    }
  })

  // split and matchAll share the same result check but must build ten million items first to reach it,
  // which is too slow for CI.
  test("arrays: constructor, length, Array.from, concat, flat", async () => {
    expect(await value(`return Array(${A}).length`)).toBe(A)
    for (const code of [
      `Array(${A + 1})`,
      `const a = []; a.length = ${A + 1}`,
      `Array.from({ length: ${A + 1} })`,
      `Array(${A}).concat([1])`,
      `[Array(${A}).fill(0), [1]].flat()`,
    ]) {
      expect(await failure(`try { ${code} } catch (e) { throw Error(e.name + ": " + e.message) }`)).toContain(
        "RangeError: Invalid array length",
      )
    }
  })

  test("promises: too many pending at once, while settled ones do not count", async () => {
    const n = MAX_PENDING_PROMISES
    expect(await value(`for (let i = 0; i < ${n * 2}; i++) Promise.resolve(i); return 1`)).toBe(1)
    expect(
      await failure(
        `try { for (let i = 0; i <= ${n}; i++) new Promise(() => {}) } catch (e) { throw Error(e.name + ": " + e.message) }`,
      ),
    ).toContain("RangeError: Too many pending promises")
    expect(await failure(`await Promise.all(Array(${n + 1}).fill(0).map(() => new Promise(() => {})))`)).toContain(
      "Too many pending promises",
    )
  }, 10_000)
})

describe("the host stays in control", () => {
  test("other host work keeps running while one long job runs", async () => {
    // Once the loop starts, the runner yields to the host every millisecond, so the host takes its 20 turns within
    // about 20 slices: at most about a million iterations on a fast machine, far short of the job's 30 million. A job
    // that kept the thread would finish first. Counting turns instead of timing them keeps this independent of how
    // fast the loop runs, and waiting for the start keeps a cold engine's setup out of the count.
    const exit = await Effect.runPromise(
      Effect.gen(function* () {
        const started = yield* Deferred.make<void>()
        const job = yield* Effect.forkChild(
          CodeMode.make({
            tools: {
              host: {
                started: Tool.make({
                  description: "Mark the loop as started",
                  input: Schema.Struct({}),
                  output: Schema.Boolean,
                  execute: () => Deferred.succeed(started, undefined),
                }),
              },
            },
          }).execute(
            `await tools.host.started({}); let t = 0; for (let i = 0; i < 30_000_000; i++) t += i % 3; return t`,
          ),
        )
        yield* Deferred.await(started)
        for (let turn = 0; turn < 20; turn++) yield* Effect.yieldNow
        yield* Fiber.interrupt(job)
        return yield* Fiber.await(job)
      }),
    )
    expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
  })

  // Each case would run for seconds or forever; the timeout must still stop it promptly.
  test.each([
    ["a loop of only built-in calls", `return Iterator.from({ next: Object }).drop(Infinity).next()`],
    [
      "serializing shared references",
      `let a = [0]; for (let i = 0; i < 24; i++) a = [a, a]; return JSON.stringify(a).length`,
    ],
    ["copying out shared references", `let a = [0]; for (let i = 0; i < 24; i++) a = [a, a]; return a`],
    [
      "reporting many unhandled rejections",
      `const a = Array(10_000).fill(0); for (let i = 0; i < 20_000; i++) Promise.reject(a); return 1`,
    ],
  ])("%s still times out", async (_, code) => {
    const result = await Effect.runPromise(CodeMode.make({ limits: { timeoutMs: 100 } }).execute(code))
    expect(result).toMatchObject({ ok: false, error: { kind: "TimeoutExceeded" } })
  })

  test("nesting deeper than the host stack fails cleanly, and the next run is unaffected", async () => {
    expect(await failure(`const f = () => f; f${"()".repeat(100_000)}; return 1`)).toBe(
      "Execution exceeded the maximum nesting depth.",
    )
    expect(await value(`const f = () => f; f${"()".repeat(500)}; return 1`)).toBe(1)
  })
})