fix: give sub-agents the provider their pinned model needs
Sub-agent sessions are created with `noExtensions: true`, which also
removes every provider an extension registered through
`pi.registerProvider` from the sub-session's own ModelRuntime. Since
bff187d pinned sub-agents to the invoking session's model, every check
depends on a provider registration it deliberately does not load, so
scans died in about 20ms with "No API key found for neuralwatt" on a
perfectly healthy login — the key was in auth.json, the provider simply
did not exist in that session's runtime.
Thread the invoking session's registry down to the runner and
re-register just the pinned model's provider there, through an inline
extension factory: inline factories load regardless of `noExtensions`,
so the sub-agent authenticates exactly as the host does while every
other extension stays out of its scope. Registrations are narrowed once
at the extension entry point (`asProviderRegistry`) and relayed
untouched — a native provider keeps its own stream and auth wiring.
The generated omp port keeps its isolation: it disables extension
discovery and has no extension registrations to forward, so it carries
the same shapes with a stub for the helper.
Tests: new tests/provider-forwarding.test.ts pins the narrowing and the
relay (native wins over config-only, nothing forwarded for a built-in);
it fails on the previous tree, where the helper does not exist. Full
suite 162 pass with one pre-existing failure (MISC-46, worktree-only).
bun run typecheck clean; port-to-omp regenerates and typechecks.
MISC-45
Co-Authored-By: nw-flash-flex <noreply@neuralwatt.com>
This commit is contained in:
105
tests/provider-forwarding.test.ts
Normal file
105
tests/provider-forwarding.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* provider-forwarding.test.ts — the provider hand-off from the invoking session
|
||||
* into a sub-agent session.
|
||||
*
|
||||
* A sub-agent session is created with `noExtensions: true`, so a provider that
|
||||
* exists only because an extension called `pi.registerProvider` is absent from
|
||||
* its `ModelRuntime` and its credentials cannot be resolved — pinning the
|
||||
* invoking session's model then fails with `No API key found for <provider>`
|
||||
* before a single token, on a perfectly healthy login. These tests pin the two
|
||||
* halves of the fix: narrowing the host registry at the boundary, and the inline
|
||||
* extension that re-registers the pinned model's provider inside the sub-session
|
||||
* (inline factories load even under `noExtensions`).
|
||||
*/
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
asProviderRegistry,
|
||||
providerForwardingExtension,
|
||||
type ProviderRegistry,
|
||||
} from "../src/agent-runner.js";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
/** Record `registerProvider` calls so the relay can be asserted on. */
|
||||
function recorder(): {
|
||||
pi: ExtensionAPI;
|
||||
calls: unknown[][];
|
||||
} {
|
||||
const calls: unknown[][] = [];
|
||||
const pi = {
|
||||
registerProvider: (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
},
|
||||
} as unknown as ExtensionAPI;
|
||||
return { pi, calls };
|
||||
}
|
||||
|
||||
/** Registry stub holding one provider id, native and/or config form. */
|
||||
function registry(entry: {
|
||||
native?: unknown;
|
||||
config?: unknown;
|
||||
}): ProviderRegistry {
|
||||
return {
|
||||
getRegisteredNativeProvider: (_id) =>
|
||||
entry.native === undefined ? undefined : entry.native,
|
||||
getRegisteredProviderConfig: (_id) =>
|
||||
entry.config === undefined ? undefined : entry.config,
|
||||
};
|
||||
}
|
||||
|
||||
describe("asProviderRegistry", () => {
|
||||
it("accepts a registry that exposes both registration lookups", () => {
|
||||
const host = registry({ native: { id: "neuralwatt" } });
|
||||
expect(asProviderRegistry(host)).toBe(host);
|
||||
});
|
||||
|
||||
it("rejects values that cannot answer both lookups", () => {
|
||||
expect(asProviderRegistry(undefined)).toBeUndefined();
|
||||
expect(asProviderRegistry(null)).toBeUndefined();
|
||||
expect(asProviderRegistry("modelRegistry")).toBeUndefined();
|
||||
// A registry from a host that never records extension providers, such as
|
||||
// the omp port, degrades to "nothing to forward" instead of throwing.
|
||||
expect(asProviderRegistry({})).toBeUndefined();
|
||||
expect(
|
||||
asProviderRegistry({ getRegisteredNativeProvider: () => undefined }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerForwardingExtension", () => {
|
||||
it("is a no-op without a registry, a pinned model, or a registered provider", () => {
|
||||
// Built-in and models.json providers need no forwarding: pi's own
|
||||
// catalogue already answers for them.
|
||||
const empty = registry({});
|
||||
expect(providerForwardingExtension(undefined, "neuralwatt")).toBeUndefined();
|
||||
expect(providerForwardingExtension(empty, undefined)).toBeUndefined();
|
||||
expect(providerForwardingExtension(empty, "neuralwatt")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("re-registers a native provider, keeping its own stream and auth wiring", () => {
|
||||
const native = { id: "neuralwatt" };
|
||||
const extension = providerForwardingExtension(
|
||||
registry({ native, config: { baseUrl: "ignored-when-native" } }),
|
||||
"neuralwatt",
|
||||
);
|
||||
if (!extension) throw new Error("expected a forwarding extension");
|
||||
expect(extension.name).toBe("pygienium-provider");
|
||||
// Hidden: the sub-agent's extension list must not advertise an extension
|
||||
// the user never installed for it.
|
||||
expect(extension.hidden).toBe(true);
|
||||
|
||||
const { pi, calls } = recorder();
|
||||
extension.factory(pi);
|
||||
// The native provider wins over a config-only registration, which would
|
||||
// only augment a built-in provider and lose the wrapped stream.
|
||||
expect(calls).toEqual([[native]]);
|
||||
});
|
||||
|
||||
it("re-registers a config-only provider under its provider id", () => {
|
||||
const config = { baseUrl: "config-only-provider" };
|
||||
const extension = providerForwardingExtension(registry({ config }), "probe");
|
||||
if (!extension) throw new Error("expected a forwarding extension");
|
||||
const { pi, calls } = recorder();
|
||||
extension.factory(pi);
|
||||
expect(calls).toEqual([["probe", config]]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user