1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Sink from "effect/Sink";
5import * as Stream from "effect/Stream";
6import * as ChildProcess from "effect/process/ChildProcess";
7import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
8
9import { checkPiProviderStatus, MINIMUM_PI_VERSION } from "./status.ts";
10
11const encoder = new TextEncoder();
12
13function processHandle(input: {
14 readonly stdout?: string;
15 readonly stderr?: string;
16 readonly exitCode?: number;
17}) {
18 const bytes = (value: string | undefined) =>
19 value === undefined || value.length === 0
20 ? Stream.empty
21 : Stream.succeed(encoder.encode(value));
22 return ChildProcessSpawner.makeHandle({
23 pid: ChildProcessSpawner.ProcessId(900_000_001),
24 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(input.exitCode ?? 0)),
25 isRunning: Effect.succeed(false),
26 kill: () => Effect.void,
27 unref: Effect.succeed(Effect.void),
28 stdin: Sink.drain,
29 stdout: bytes(input.stdout),
30 stderr: bytes(input.stderr),
31 all: Stream.empty,
32 getInputFd: () => Sink.drain,
33 getOutputFd: () => Stream.empty,
34 });
35}
36
37function piProbeSpawner(version: string) {
38 return ChildProcessSpawner.make((command) => {
39 const args = ChildProcess.isStandardCommand(command) ? command.args : [];
40 return Effect.succeed(
41 args.includes("--version")
42 ? processHandle({ stdout: `pi ${version}\n` })
43 : processHandle({ stderr: "RPC startup failed", exitCode: 1 }),
44 );
45 });
46}
47
48const settings = {
49 enabled: true,
50 binaryPath: "pi",
51 launchArgs: "",
52 customModels: [],
53} as const;
54
55describe("PiProvider", () => {
56 it.effect("requires the first published Pi version with entries and settlement hooks", () =>
57 Effect.gen(function* () {
58 const snapshot = yield* checkPiProviderStatus(settings).pipe(
59 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, piProbeSpawner("0.80.3")),
60 );
61 assert.equal(snapshot.status, "error");
62 assert.equal(snapshot.version, "0.80.3");
63 assert.include(snapshot.message ?? "", `Pi ${MINIMUM_PI_VERSION} or newer`);
64 }).pipe(Effect.provide(NodeServices.layer)),
65 );
66
67 it.effect("keeps compatible Pi selectable when optional discovery fails", () =>
68 Effect.gen(function* () {
69 const snapshot = yield* checkPiProviderStatus(settings).pipe(
70 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, piProbeSpawner("0.84.3")),
71 );
72 assert.equal(snapshot.status, "ready");
73 assert.equal(snapshot.auth.status, "unknown");
74 assert.deepEqual(
75 snapshot.models.map((model) => model.slug),
76 ["default"],
77 );
78 assert.include(snapshot.message ?? "", "could not refresh its models and commands");
79 }).pipe(Effect.provide(NodeServices.layer)),
80 );
81});