112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
import { afterAll, describe, expect, it } from "vitest";
|
|
import { tmpdir } from "node:os";
|
|
import processPlugin from "@aoagents/ao-plugin-runtime-process";
|
|
import type { RuntimeHandle } from "@aoagents/ao-core";
|
|
import { sleep } from "./helpers/polling.js";
|
|
|
|
describe("runtime-process (integration)", () => {
|
|
const runtime = processPlugin.create();
|
|
const sessionId = `proc-inttest-${Date.now()}`;
|
|
let handle: RuntimeHandle;
|
|
|
|
// Platform-native stdin→stdout echo. We can't use `node -e ...` here:
|
|
// on Windows the launch command goes through `pwsh -Command "..."`, which
|
|
// treats a quoted path as a string literal rather than invoking it, so
|
|
// node never actually runs. `findstr "x*"` matches every line and prints it
|
|
// verbatim — Windows' closest builtin to `cat`.
|
|
const echoCommand = process.platform === "win32" ? `findstr "x*"` : "cat";
|
|
const workspacePath = tmpdir();
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await runtime.destroy(handle);
|
|
} catch {
|
|
/* best-effort cleanup */
|
|
}
|
|
}, 30_000);
|
|
|
|
it("creates a child process", async () => {
|
|
handle = await runtime.create({
|
|
sessionId,
|
|
workspacePath,
|
|
launchCommand: echoCommand,
|
|
environment: { AO_TEST: "1" },
|
|
});
|
|
|
|
expect(handle.id).toBe(sessionId);
|
|
expect(handle.runtimeName).toBe("process");
|
|
expect(handle.data.pid).toBeTypeOf("number");
|
|
});
|
|
|
|
it("isAlive returns true for running process", async () => {
|
|
expect(await runtime.isAlive(handle)).toBe(true);
|
|
});
|
|
|
|
it("sendMessage writes to stdin and output is captured", async () => {
|
|
await runtime.sendMessage(handle, "hello from test");
|
|
// Poll until the payload appears instead of using a fixed sleep: round-trip
|
|
// latency varies wildly across platforms and runners (Unix direct-stdin:
|
|
// ~ms; Windows ConPTY through named pipe + pwsh + findstr startup: hundreds
|
|
// of ms to seconds under AV / cold-cache conditions). A fixed sleep either
|
|
// flakes or wastes time. Polling for the substring (not just non-empty
|
|
// output) is also robust to incidental shell banners arriving first.
|
|
const deadline = Date.now() + 10_000;
|
|
let output = "";
|
|
while (Date.now() < deadline) {
|
|
output = await runtime.getOutput(handle);
|
|
if (output.includes("hello from test")) break;
|
|
await sleep(100);
|
|
}
|
|
expect(output).toContain("hello from test");
|
|
}, 15_000);
|
|
|
|
it("getMetrics returns uptime", async () => {
|
|
const metrics = await runtime.getMetrics!(handle);
|
|
expect(metrics.uptimeMs).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("getAttachInfo returns PID", async () => {
|
|
const info = await runtime.getAttachInfo!(handle);
|
|
expect(info.type).toBe("process");
|
|
expect(info.target).toMatch(/^\d+$/);
|
|
});
|
|
|
|
it("rejects duplicate session IDs", async () => {
|
|
await expect(
|
|
runtime.create({
|
|
sessionId,
|
|
workspacePath,
|
|
launchCommand: echoCommand,
|
|
environment: {},
|
|
}),
|
|
).rejects.toThrow("already exists");
|
|
});
|
|
|
|
it("sendMessage throws for unknown session", async () => {
|
|
await expect(
|
|
runtime.sendMessage({ id: "nonexistent", runtimeName: "process", data: {} }, "hi"),
|
|
).rejects.toThrow("No process found");
|
|
});
|
|
|
|
it("destroy kills the process", async () => {
|
|
await runtime.destroy(handle);
|
|
await sleep(200); // give time for exit handler
|
|
expect(await runtime.isAlive(handle)).toBe(false);
|
|
});
|
|
|
|
it("getOutput returns empty for destroyed session", async () => {
|
|
const output = await runtime.getOutput(handle);
|
|
expect(output).toBe("");
|
|
});
|
|
|
|
it("isAlive returns false for unknown session", async () => {
|
|
expect(await runtime.isAlive({ id: "nonexistent", runtimeName: "process", data: {} })).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("destroy is idempotent", async () => {
|
|
await runtime.destroy(handle); // should not throw
|
|
});
|
|
});
|