1f3f3cf5d1
- Implemented persistent execution backends for Ruby and Julia using dedicated kernel processes and NDJSON-based IPC. - Integrated language-specific prelude environments, runtime path resolution, and security-focused environment variable filtering. - Exposed configuration options, tool schema updates, and lifecycle management for seamless agent interaction with both languages. - Added comprehensive integration tests and updated prompt documentation to support the new evaluation capabilities.
114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
/**
|
|
* End-to-end exercise of the subprocess-backed Ruby runner.
|
|
*
|
|
* Gated by `PI_RUBY_INTEGRATION=1` so CI without a real Ruby interpreter
|
|
* (or sandboxes where subprocess spawning is restricted) does not fail.
|
|
*/
|
|
import { afterEach, describe, expect, it } from "bun:test";
|
|
import { disposeAllRubyKernelSessions, executeRubyWithKernel } from "@oh-my-pi/pi-coding-agent/eval/rb/executor";
|
|
import { RubyKernel } from "@oh-my-pi/pi-coding-agent/eval/rb/kernel";
|
|
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
|
|
const SHOULD_RUN = Bun.env.PI_RUBY_INTEGRATION === "1";
|
|
|
|
describe.skipIf(!SHOULD_RUN)("ruby runner subprocess", () => {
|
|
afterEach(async () => {
|
|
await disposeAllRubyKernelSessions();
|
|
});
|
|
|
|
it("streams stdout chunks as they are produced", async () => {
|
|
using tempDir = TempDir.createSync("@ruby-runner-stream-");
|
|
const kernel = await RubyKernel.start({ cwd: tempDir.path() });
|
|
try {
|
|
const chunks: string[] = [];
|
|
const result = await executeRubyWithKernel(kernel, "5.times { |i| puts i }", {
|
|
onChunk: chunk => {
|
|
chunks.push(chunk);
|
|
},
|
|
});
|
|
expect(result.exitCode).toBe(0);
|
|
expect(chunks.join("")).toContain("0\n");
|
|
expect(chunks.join("")).toContain("4\n");
|
|
} finally {
|
|
await kernel.shutdown();
|
|
}
|
|
});
|
|
|
|
it("keeps local variables across cells on one kernel", async () => {
|
|
using tempDir = TempDir.createSync("@ruby-runner-state-");
|
|
const kernel = await RubyKernel.start({ cwd: tempDir.path() });
|
|
try {
|
|
const first = await executeRubyWithKernel(kernel, "x = 41", {});
|
|
expect(first.exitCode).toBe(0);
|
|
const second = await executeRubyWithKernel(kernel, "x + 1", {});
|
|
expect(second.exitCode).toBe(0);
|
|
expect(second.output).toContain("42");
|
|
} finally {
|
|
await kernel.shutdown();
|
|
}
|
|
});
|
|
|
|
it("auto-displays the last expression but suppresses assignments", async () => {
|
|
using tempDir = TempDir.createSync("@ruby-runner-display-");
|
|
const kernel = await RubyKernel.start({ cwd: tempDir.path() });
|
|
try {
|
|
const assigned = await executeRubyWithKernel(kernel, "y = { a: 1, b: [2, 3] }", {});
|
|
expect(assigned.displayOutputs).toHaveLength(0);
|
|
|
|
const expr = await executeRubyWithKernel(kernel, "y", {});
|
|
const json = expr.displayOutputs.find(o => o.type === "json");
|
|
expect(json).toBeDefined();
|
|
if (json?.type === "json") {
|
|
expect(json.data).toEqual({ a: 1, b: [2, 3] });
|
|
}
|
|
} finally {
|
|
await kernel.shutdown();
|
|
}
|
|
});
|
|
|
|
it("cancels a running cell via signal and keeps the kernel usable", async () => {
|
|
using tempDir = TempDir.createSync("@ruby-runner-cancel-");
|
|
const kernel = await RubyKernel.start({ cwd: tempDir.path() });
|
|
try {
|
|
const controller = new AbortController();
|
|
// Real delay: this interrupts a live OS subprocess mid-`sleep`, which runs on
|
|
// the platform clock — fake timers cannot drive it. Abort once the cell is in flight.
|
|
setTimeout(() => controller.abort(), 300);
|
|
const cancelled = await executeRubyWithKernel(kernel, "sleep 30", { signal: controller.signal });
|
|
expect(cancelled.cancelled).toBe(true);
|
|
|
|
const after = await executeRubyWithKernel(kernel, "20 + 22", {});
|
|
expect(after.exitCode).toBe(0);
|
|
expect(after.output).toContain("42");
|
|
} finally {
|
|
await kernel.shutdown();
|
|
}
|
|
});
|
|
|
|
it("surfaces Ruby errors with a non-zero exit and the message", async () => {
|
|
using tempDir = TempDir.createSync("@ruby-runner-error-");
|
|
const kernel = await RubyKernel.start({ cwd: tempDir.path() });
|
|
try {
|
|
const result = await executeRubyWithKernel(kernel, "raise 'boom'", {});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.output).toContain("boom");
|
|
} finally {
|
|
await kernel.shutdown();
|
|
}
|
|
});
|
|
|
|
it("exposes prelude file + text helpers", async () => {
|
|
using tempDir = TempDir.createSync("@ruby-runner-prelude-");
|
|
const kernel = await RubyKernel.start({ cwd: tempDir.path() });
|
|
try {
|
|
const sorted = await executeRubyWithKernel(kernel, 'sort("b\\na\\nb", unique: true)', {});
|
|
expect(sorted.output).toContain("a\nb");
|
|
|
|
const written = await executeRubyWithKernel(kernel, 'write("note.txt", "hello"); read("note.txt")', {});
|
|
expect(written.output).toContain("hello");
|
|
} finally {
|
|
await kernel.shutdown();
|
|
}
|
|
});
|
|
});
|