/**
* Offline checks on the extension: what it registers, with no network and no Prime Agent.
*
* npm test
*
* The live claims (membership, prices, streaming, cache accounting) are `verify.ts`; this
* covers the derivation and the key precedence, where a silent mistake would be invisible.
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { OpenAICompletionsCompat } from "@earendil-works/pi-ai";
import type { ExtensionAPI, ProviderConfig } from "@earendil-works/pi-coding-agent";
import { type ModelFacts, toProviderModel } from "./catalog.js";
import { API_KEY_CONFIG, ENV_VAR, resolveKey, storedKey, storedKeyFrom } from "./hyper.js";
import hyper from "./index.js";
const here = dirname(fileURLToPath(import.meta.url));
const catalog = JSON.parse(readFileSync(join(here, "catalog.json"), "utf-8")) as { models: ModelFacts[] };
// A provider entry is one credential or a list of them; both shapes must resolve.
assert.equal(storedKeyFrom({ hyper: { type: "api_key", key: "k1" } }), "k1");
assert.equal(
storedKeyFrom({ hyper: [{ type: "oauth", access: "a" }, { type: "api_key", key: "k2" }] }),
"k2",
"a subscription entry must not hide the API key beside it",
);
assert.equal(storedKeyFrom({ hyper: [{ type: "oauth", access: "a" }] }), undefined);
assert.equal(storedKeyFrom({}), undefined);
assert.equal(storedKeyFrom(null), undefined);
process.env.PI_OFFLINE = "1";
assert.equal(
API_KEY_CONFIG,
`!printf %s "$${ENV_VAR}"`,
"the config fallback must read the env var and resolve to nothing when it is unset",
);
delete process.env[ENV_VAR];
assert.equal(resolveKey(), storedKey(), "without HYPER_API_KEY the stored credential is the source");
assert.ok(storedKey() === undefined || !storedKey()?.includes(" "), "a stored key is a single token");
assert.ok(
storedKey() === undefined || resolveKey() === storedKey(),
"nothing but auth.json or the env var may supply a key",
);
process.env[ENV_VAR] = "sk-hyper-test";
assert.equal(resolveKey(), "sk-hyper-test", "HYPER_API_KEY must win over the stored credential");
const registrations: Array<{ name: string; config: ProviderConfig }> = [];
const commands: string[] = [];
await hyper({
registerProvider: (name: string, config: ProviderConfig) => {
registrations.push({ name, config });
},
registerCommand: (name: string) => {
commands.push(name);
},
} as unknown as ExtensionAPI);
assert.equal(registrations.length, 1, "the extension must register exactly one provider");
const provider = registrations[0] as { name: string; config: ProviderConfig };
assert.equal(provider.name, "hyper");
assert.equal(provider.config.name, "Charm Hyper");
assert.equal(provider.config.baseUrl, "https://hyper.charm.land/v1");
assert.equal(provider.config.api, "openai-completions");
assert.equal(provider.config.models?.length, catalog.models.length, "every catalog model must be registered");
assert.ok(provider.config.oauth, "subscription login must be offered");
assert.equal(typeof provider.config.oauth?.login, "function");
assert.equal(typeof provider.config.oauth?.refreshToken, "function");
assert.deepEqual(commands, ["hyper"]);
const ids = (provider.config.models ?? []).map((model) => model.id);
assert.deepEqual(ids, catalog.models.map((model) => model.id), "registered models must follow the catalog order");
for (const model of catalog.models) {
const config = toProviderModel(model);
const compat = config.compat as OpenAICompletionsCompat;
assert.equal(compat.maxTokensField, "max_tokens", `${model.id}: Hyper takes max_tokens`);
assert.equal(compat.supportsStore, false, `${model.id}: Hyper has no response store`);
assert.equal(compat.thinkingFormat, "openai", `${model.id}: reasoning goes through reasoning_effort`);
assert.ok(config.contextWindow > 0 && config.maxTokens > 0, `${model.id}: limits must be positive`);
assert.deepEqual(config.cost, model.cost, `${model.id}: cost must be the catalog price`);
assert.deepEqual(
config.input,
model.vision ? ["text", "image"] : ["text"],
`${model.id}: image input must follow vision support`,
);
if (!model.reasoning) {
assert.equal(config.reasoning, false, `${model.id}: must not claim reasoning`);
assert.equal(config.thinkingLevelMap, undefined, `${model.id}: must not offer thinking controls`);
assert.equal(compat.supportsReasoningEffort, false, `${model.id}: must not send reasoning_effort`);
continue;
}
assert.equal(config.reasoning, true, `${model.id}: must claim reasoning`);
assert.equal(compat.supportsReasoningEffort, true, `${model.id}: must be allowed to send reasoning_effort`);
const on = model.levels.filter((level) => level !== "none" && level !== "off");
const offered = Object.entries(config.thinkingLevelMap ?? {})
.filter(([, value]) => value !== null && value !== "none" && value !== "off")
.map(([level]) => level);
assert.deepEqual(offered.sort(), (on.length > 0 ? on : ["max"]).sort(), `${model.id}: offered levels must be the advertised ones`);
assert.equal(
config.thinkingLevelMap?.off,
model.levels.includes("none") ? "none" : null,
`${model.id}: the off level must follow an advertised none`,
);
}
process.stdout.write(`ok: ${ids.length} models registered, key precedence and derivation checked\n`);