Files
my-pi/pi-subagents/test/e2e/isolated-provider.e2e.test.ts
T

82 lines
4.1 KiB
TypeScript

/**
* isolated-provider.e2e.test.ts — reachability guard for PR #152 (issue #151).
*
* PR #152 fixes isolated subagents dropping extension-registered custom providers
* on Pi >= 0.80.8 (the `modelRegistry` → `modelRuntime` migration). agent-runner
* forwards the parent's runtime, read off the ModelRegistry facade as
* `ctx.modelRegistry.runtime` via `as unknown as { runtime }`.
*
* That forwarding is already guarded by the unit test in test/agent-runner.test.ts
* ("passes the parent model runtime …") — but against a MOCK whose `.runtime` is
* hand-set. The mock cannot catch the one thing that would silently break the fix:
* `.runtime` is a `private readonly` field on the real ModelRegistry, absent from
* the public type AND the package exports. If a future Pi renames it, makes it a
* true #private, or moves the module, the cast quietly yields `undefined`, the fix
* omits `modelRuntime`, and the bug returns with no failing test.
*
* This test closes exactly that gap and nothing else: it asserts the real facade
* exposes a runtime-reachable `.runtime` that IS the runtime it wraps. It is not a
* guard for the forwarding itself (that's the unit test's job) — a fuller e2e that
* drives real `runAgent` end-to-end is tracked as a follow-up.
*
* VERSION GATE: `.runtime` only exists in the post-migration facade world (Pi >=
* 0.80.8, where `ModelRuntime` is first exported). The repo's dev dependency is
* pinned post-migration, so this runs by default — but it stays gated because CI
* also runs the suite against the peer-range floor (see .github/workflows/ci.yml),
* where Pi predates the migration. There we DYNAMICALLY import Pi and skip
* cleanly; a static `import { ModelRuntime }` would be a link-time error.
*/
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, describe, expect, it } from "vitest";
// Dynamic, so this file LOADS on pre-migration Pi (a static `import { ModelRuntime }`
// would be a link-time error there — 0.80.6 doesn't export it).
const pi = (await import("@earendil-works/pi-coding-agent")) as Record<string, unknown>;
const ModelRuntime = pi.ModelRuntime as
| { create(opts?: Record<string, unknown>): Promise<ModelRuntimeLike> }
| undefined;
// The migration is exactly "ModelRuntime now exists". Absent ⇒ pre-0.80.8 ⇒ there
// is no `.runtime` facade to guard.
const MIGRATED = typeof ModelRuntime?.create === "function";
const RT = ModelRuntime as { create(opts?: Record<string, unknown>): Promise<ModelRuntimeLike> };
// The one method the reach scenario needs; `.runtime` itself is private (reached below).
interface ModelRuntimeLike {
registerProvider(id: string, config: Record<string, unknown>): void;
}
const tmpDirs: string[] = [];
afterAll(() => {
for (const d of tmpDirs.splice(0)) rmSync(d, { recursive: true, force: true });
});
describe.skipIf(!MIGRATED)("PR #152 reach: real ModelRegistry exposes .runtime (Pi >= 0.80.8)", () => {
it("ctx.modelRegistry.runtime is reachable and IS the runtime it wraps", async () => {
// A real, configured runtime — as an extension leaves it after registerProvider.
const dir = mkdtempSync(join(tmpdir(), "iso-prov-"));
tmpDirs.push(dir);
const runtime = await RT.create({
authPath: join(dir, "auth.json"),
modelsPath: join(dir, "models.json"),
allowModelNetwork: false,
});
// `.runtime` is private and not in the package exports — reach the compiled
// class by file path, exactly the field the patch's cast depends on. If Pi
// moves/renames/#privates it, THIS line fails loudly instead of the fix
// silently no-op'ing back to the #151 bug.
const indexUrl = import.meta.resolve("@earendil-works/pi-coding-agent");
const mrUrl = indexUrl.replace(/index\.js$/, "core/model-registry.js");
const { ModelRegistry } = (await import(mrUrl)) as {
ModelRegistry: new (rt: ModelRuntimeLike) => { runtime?: unknown };
};
const facade = new ModelRegistry(runtime);
// This is the exact expression agent-runner reads (`ctx.modelRegistry.runtime`).
expect((facade as { runtime?: unknown }).runtime).toBe(runtime);
});
});