import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, it } from "@effect/vitest";import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";import * as Cause from "effect/Cause";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import { vi } from "vite-plus/test"; import * as NodePtyAdapter from "./NodePtyAdapter.ts";import * as PtyAdapter from "./PtyAdapter.ts"; const spawn = vi.fn(() => ({ pid: 42, write: vi.fn(), resize: vi.fn(), kill: vi.fn(), onData: vi.fn(() => ({ dispose: vi.fn() })), onExit: vi.fn(() => ({ dispose: vi.fn() })),})); vi.mock("node-pty", () => ({ spawn })); const makeTestLayer = (platform: NodeJS.Platform = "win32") => NodePtyAdapter.layer.pipe( Layer.provide( Layer.mergeAll( NodeServices.layer, Layer.succeed(HostProcessPlatform, platform), Layer.succeed(HostProcessArchitecture, "x64"), ), ), ); const testLayer = makeTestLayer(); for (const platform of ["win32", "linux", "darwin"] as const) { it.effect(`terminates through node-pty using ${platform} semantics`, () => Effect.gen(function* () { const adapter = yield* PtyAdapter.PtyAdapter; const process = yield* adapter.spawn({ shell: "test-shell", cwd: ".", cols: 80, rows: 24, env: {}, }); const nativeProcess = spawn.mock.results.at(-1)!.value; nativeProcess.kill.mockImplementation((signal?: string) => { if (platform === "win32" && signal) { throw new Error("Signals not supported on windows."); } }); process.kill("SIGTERM"); process.kill("SIGKILL"); process.kill(); assert.deepEqual( nativeProcess.kill.mock.calls, platform === "win32" ? [[undefined], [undefined], [undefined]] : [["SIGTERM"], ["SIGKILL"], [undefined]], ); }).pipe(Effect.provide(makeTestLayer(platform))), );} it.effect("spawns through the public adapter with the provided host references", () => Effect.gen(function* () { spawn.mockClear(); const adapter = yield* PtyAdapter.PtyAdapter; const process = yield* adapter.spawn({ shell: "powershell.exe", args: ["-NoLogo"], cwd: "C:\\workspace", cols: 120, rows: 40, env: {}, }); assert.equal(process.pid, 42); assert.equal(spawn.mock.calls.length, 1); assert.deepEqual(spawn.mock.calls[0], [ "powershell.exe", ["-NoLogo"], { cwd: "C:\\workspace", cols: 120, rows: 40, env: { TERM: "xterm-256color" }, name: "xterm-256color", }, ]); }).pipe(Effect.provide(testLayer)),); it.effect("preserves a caller-provided TERM in the spawn env on win32", () => Effect.gen(function* () { spawn.mockClear(); const adapter = yield* PtyAdapter.PtyAdapter; yield* adapter.spawn({ shell: "powershell.exe", cwd: "C:\\workspace", cols: 80, rows: 24, env: { TERM: "xterm-direct" }, }); assert.equal(spawn.mock.calls.length, 1); assert.deepEqual(spawn.mock.calls[0], [ "powershell.exe", [], { cwd: "C:\\workspace", cols: 80, rows: 24, env: { TERM: "xterm-direct" }, name: "xterm-256color", }, ]); }).pipe(Effect.provide(testLayer)),); it.effect("reports native module load failures as structured startup defects", () => Effect.gen(function* () { const cause = new Error("native binding could not be loaded"); const exit = yield* NodePtyAdapter.make(() => Promise.reject(cause)).pipe(Effect.exit); assert.isTrue(Exit.isFailure(exit)); if (Exit.isFailure(exit)) { assert.isTrue(Cause.hasDies(exit.cause)); const error = Cause.squash(exit.cause); assert.instanceOf(error, NodePtyAdapter.NodePtyModuleLoadError); assert.deepInclude(error, { _tag: "NodePtyModuleLoadError", platform: "win32", architecture: "x64", }); assert.equal(error.message, "Failed to load node-pty for win32-x64."); } }).pipe( Effect.provide( Layer.mergeAll( NodeServices.layer, Layer.succeed(HostProcessPlatform, "win32"), Layer.succeed(HostProcessArchitecture, "x64"), ), ), ),);
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import { vi } from "vite-plus/test";
import * as NodePtyAdapter from "./NodePtyAdapter.ts";
import * as PtyAdapter from "./PtyAdapter.ts";
const spawn = vi.fn(() => ({
pid: 42,
write: vi.fn(),
resize: vi.fn(),
kill: vi.fn(),
onData: vi.fn(() => ({ dispose: vi.fn() })),
onExit: vi.fn(() => ({ dispose: vi.fn() })),
}));
vi.mock("node-pty", () => ({ spawn }));
const makeTestLayer = (platform: NodeJS.Platform = "win32") =>
NodePtyAdapter.layer.pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.succeed(HostProcessPlatform, platform),
Layer.succeed(HostProcessArchitecture, "x64"),
),
),
);
const testLayer = makeTestLayer();
for (const platform of ["win32", "linux", "darwin"] as const) {
it.effect(`terminates through node-pty using ${platform} semantics`, () =>
Effect.gen(function* () {
const adapter = yield* PtyAdapter.PtyAdapter;
const process = yield* adapter.spawn({
shell: "test-shell",
cwd: ".",
cols: 80,
rows: 24,
env: {},
});
const nativeProcess = spawn.mock.results.at(-1)!.value;
nativeProcess.kill.mockImplementation((signal?: string) => {
if (platform === "win32" && signal) {
throw new Error("Signals not supported on windows.");
}
});
process.kill("SIGTERM");
process.kill("SIGKILL");
process.kill();
assert.deepEqual(
nativeProcess.kill.mock.calls,
platform === "win32"
? [[undefined], [undefined], [undefined]]
: [["SIGTERM"], ["SIGKILL"], [undefined]],
);
}).pipe(Effect.provide(makeTestLayer(platform))),
);
}
it.effect("spawns through the public adapter with the provided host references", () =>
Effect.gen(function* () {
spawn.mockClear();
const adapter = yield* PtyAdapter.PtyAdapter;
const process = yield* adapter.spawn({
shell: "powershell.exe",
args: ["-NoLogo"],
cwd: "C:\\workspace",
cols: 120,
rows: 40,
env: {},
});
assert.equal(process.pid, 42);
assert.equal(spawn.mock.calls.length, 1);
assert.deepEqual(spawn.mock.calls[0], [
"powershell.exe",
["-NoLogo"],
{
cwd: "C:\\workspace",
cols: 120,
rows: 40,
env: { TERM: "xterm-256color" },
name: "xterm-256color",
},
]);
}).pipe(Effect.provide(testLayer)),
);
it.effect("preserves a caller-provided TERM in the spawn env on win32", () =>
Effect.gen(function* () {
spawn.mockClear();
const adapter = yield* PtyAdapter.PtyAdapter;
yield* adapter.spawn({
shell: "powershell.exe",
cwd: "C:\\workspace",
cols: 80,
rows: 24,
env: { TERM: "xterm-direct" },
});
assert.equal(spawn.mock.calls.length, 1);
assert.deepEqual(spawn.mock.calls[0], [
"powershell.exe",
[],
{
cwd: "C:\\workspace",
cols: 80,
rows: 24,
env: { TERM: "xterm-direct" },
name: "xterm-256color",
},
]);
}).pipe(Effect.provide(testLayer)),
);
it.effect("reports native module load failures as structured startup defects", () =>
Effect.gen(function* () {
const cause = new Error("native binding could not be loaded");
const exit = yield* NodePtyAdapter.make(() => Promise.reject(cause)).pipe(Effect.exit);
assert.isTrue(Exit.isFailure(exit));
if (Exit.isFailure(exit)) {
assert.isTrue(Cause.hasDies(exit.cause));
const error = Cause.squash(exit.cause);
assert.instanceOf(error, NodePtyAdapter.NodePtyModuleLoadError);
assert.deepInclude(error, {
_tag: "NodePtyModuleLoadError",
platform: "win32",
architecture: "x64",
});
assert.equal(error.message, "Failed to load node-pty for win32-x64.");
}
}).pipe(
Effect.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.succeed(HostProcessPlatform, "win32"),
Layer.succeed(HostProcessArchitecture, "x64"),
),
),
),
);