refactor(codex): remove unused auth-scope fingerprinting #19
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@eleith-pi/codex": patch
|
||||
---
|
||||
|
||||
Remove unused Codex auth-scope fingerprinting and the auth-only lookup path. Return catalog JSON directly without unused metadata wrappers, preserving account-request authentication, endpoint restrictions, cancellation, timeouts, response bounds, and safe errors.
|
||||
@@ -1,12 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { Api, AuthResult, Model } from "@earendil-works/pi-ai";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
fetchCodexModelCatalog,
|
||||
fetchCodexUsage,
|
||||
readCodexAuthScope,
|
||||
type CodexAccountRegistry,
|
||||
} from "./client.ts";
|
||||
import { fetchCodexModelCatalog, fetchCodexUsage, type CodexAccountRegistry } from "./client.ts";
|
||||
|
||||
const BASE_URL = "https://chatgpt.com/backend-api";
|
||||
const model: Model<Api> = {
|
||||
@@ -72,67 +66,63 @@ it("uses Pi's existing Codex auth for the fixed account endpoint", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the same Pi credentials for the fixed versioned model catalog", async () => {
|
||||
it("returns raw catalog JSON using only Pi's resolved routing credentials", async () => {
|
||||
const f = fixture();
|
||||
f.auth.auth.headers = {
|
||||
"ChatGPT-Account-Id": "unused-account",
|
||||
Authorization: "unused-token",
|
||||
"X-Unrelated-Secret": "unused-secret",
|
||||
};
|
||||
const catalog = { models: [{ slug: "future-model", service_tiers: [{ id: "priority" }] }] };
|
||||
f.fetcher.mockResolvedValue(json(catalog));
|
||||
const response = json(catalog);
|
||||
f.fetcher.mockResolvedValue(response);
|
||||
const result = await fetchCodexModelCatalog(f.registry, undefined, f.fetcher);
|
||||
expect(result.data).toEqual(catalog);
|
||||
expect(result).toEqual(catalog);
|
||||
expect(f.registry.getProviderAuth).toHaveBeenCalledExactlyOnceWith("openai-codex");
|
||||
const requestOptions = expect.objectContaining({ method: "GET", redirect: "error" });
|
||||
expect(f.fetcher).toHaveBeenCalledExactlyOnceWith(
|
||||
`${BASE_URL}/codex/models?client_version=0.162.0`,
|
||||
expect.objectContaining({ method: "GET", redirect: "error" }),
|
||||
requestOptions,
|
||||
);
|
||||
const headers = new Headers(f.fetcher.mock.calls[0][1]?.headers);
|
||||
expect(headers.get("authorization")).toBe(`Bearer ${f.auth.auth.apiKey}`);
|
||||
expect(headers.get("chatgpt-account-id")).toBe("test-account");
|
||||
expect(result.authScope).toBe(
|
||||
createHash("sha256")
|
||||
.update(JSON.stringify([headers.get("authorization"), headers.get("chatgpt-account-id")]))
|
||||
.digest("hex"),
|
||||
);
|
||||
expect(Object.keys(result).sort()).toEqual(["authScope", "data"]);
|
||||
expect(JSON.stringify(result)).not.toContain(f.auth.auth.apiKey);
|
||||
expect(JSON.stringify(result)).not.toContain("test-account");
|
||||
});
|
||||
|
||||
it("reads a stable auth scope without fetching and ignores unused provider headers", async () => {
|
||||
const f = fixture();
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("must not fetch"));
|
||||
try {
|
||||
const scope = await readCodexAuthScope(f.registry);
|
||||
f.auth.auth.headers = {
|
||||
"ChatGPT-Account-Id": "unused-account",
|
||||
Authorization: "unused-token",
|
||||
"X-Unrelated-Secret": "unused-secret",
|
||||
};
|
||||
expect(await readCodexAuthScope(f.registry)).toBe(scope);
|
||||
expect(scope).toMatch(/^[a-f0-9]{64}$/);
|
||||
expect(scope).not.toContain(f.auth.auth.apiKey);
|
||||
expect(scope).not.toContain("test-account");
|
||||
expect(f.registry.getProviderAuth).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
const catalog = await fetchCodexModelCatalog(f.registry, undefined, f.fetcher);
|
||||
expect(catalog.authScope).toBe(scope);
|
||||
} finally {
|
||||
fetchSpy.mockRestore();
|
||||
}
|
||||
const actualHeaders = Object.fromEntries(headers);
|
||||
expect(actualHeaders).toEqual({
|
||||
accept: "application/json",
|
||||
authorization: `Bearer ${f.auth.auth.apiKey}`,
|
||||
"chatgpt-account-id": "test-account",
|
||||
});
|
||||
const serialized = JSON.stringify(result);
|
||||
expect(serialized).not.toContain(f.auth.auth.apiKey);
|
||||
expect(serialized).not.toContain("test-account");
|
||||
});
|
||||
|
||||
it.each(["token refresh", "account change"] as const)(
|
||||
"changes the scope on %s and binds the catalog to its own resolved credentials",
|
||||
"resolves fresh catalog credentials after %s",
|
||||
async (change) => {
|
||||
const f = fixture();
|
||||
const previous = await readCodexAuthScope(f.registry);
|
||||
f.auth.auth.apiKey =
|
||||
change === "token refresh" ? `${token()}-refreshed` : token("another-account");
|
||||
const catalog = await fetchCodexModelCatalog(f.registry, undefined, f.fetcher);
|
||||
expect(catalog.authScope).not.toBe(previous);
|
||||
expect(await readCodexAuthScope(f.registry)).toBe(catalog.authScope);
|
||||
const originalToken = f.auth.auth.apiKey;
|
||||
await fetchCodexModelCatalog(f.registry, undefined, f.fetcher);
|
||||
const nextAccount = change === "token refresh" ? "test-account" : "another-account";
|
||||
const nextToken =
|
||||
change === "token refresh" ? `${originalToken}-refreshed` : token(nextAccount);
|
||||
f.auth.auth.apiKey = nextToken;
|
||||
await fetchCodexModelCatalog(f.registry, undefined, f.fetcher);
|
||||
const credentials = f.fetcher.mock.calls.map(([, init]) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
return {
|
||||
authorization: headers.get("authorization"),
|
||||
account: headers.get("chatgpt-account-id"),
|
||||
};
|
||||
});
|
||||
expect(credentials).toEqual([
|
||||
{ authorization: `Bearer ${originalToken}`, account: "test-account" },
|
||||
{ authorization: `Bearer ${nextToken}`, account: nextAccount },
|
||||
]);
|
||||
expect(f.registry.getProviderAuth).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
);
|
||||
|
||||
describe("auth-only validation without HTTP", () => {
|
||||
describe("catalog credential validation before HTTP", () => {
|
||||
const unexpectedFetch = vi.fn<typeof fetch>(async () => {
|
||||
throw new Error("must not fetch");
|
||||
});
|
||||
@@ -156,19 +146,22 @@ describe("auth-only validation without HTTP", () => {
|
||||
it("rejects an overridden provider endpoint", async () => {
|
||||
const account = fixture();
|
||||
account.provider.baseUrl = "https://gateway.invalid";
|
||||
await expect(readCodexAuthScope(account.registry)).rejects.toMatchObject(unsupportedProvider);
|
||||
const request = fetchCodexModelCatalog(account.registry);
|
||||
await expect(request).rejects.toMatchObject(unsupportedProvider);
|
||||
});
|
||||
|
||||
it("rejects a catalog model at a different endpoint", async () => {
|
||||
const account = fixture();
|
||||
account.provider.getModels = () => [{ ...model, baseUrl: "https://gateway.invalid" }];
|
||||
await expect(readCodexAuthScope(account.registry)).rejects.toMatchObject(unsupportedProvider);
|
||||
const request = fetchCodexModelCatalog(account.registry);
|
||||
await expect(request).rejects.toMatchObject(unsupportedProvider);
|
||||
});
|
||||
|
||||
it("rejects an endpoint override in resolved credentials", async () => {
|
||||
const account = fixture();
|
||||
account.auth.auth.baseUrl = "https://gateway.invalid";
|
||||
await expect(readCodexAuthScope(account.registry)).rejects.toMatchObject({
|
||||
const request = fetchCodexModelCatalog(account.registry);
|
||||
await expect(request).rejects.toMatchObject({
|
||||
code: "unsupported",
|
||||
message: "Account requests do not support endpoint overrides.",
|
||||
});
|
||||
@@ -177,25 +170,30 @@ describe("auth-only validation without HTTP", () => {
|
||||
it("requires an OAuth availability snapshot", async () => {
|
||||
const account = fixture();
|
||||
account.registry.isUsingOAuth.mockReturnValue(false);
|
||||
await expect(readCodexAuthScope(account.registry)).rejects.toMatchObject(unavailableAuth);
|
||||
const request = fetchCodexModelCatalog(account.registry);
|
||||
await expect(request).rejects.toMatchObject(unavailableAuth);
|
||||
});
|
||||
|
||||
it("rejects a non-OAuth resolution even when the snapshot says OAuth", async () => {
|
||||
const account = fixture();
|
||||
account.auth.source = "models.json";
|
||||
await expect(readCodexAuthScope(account.registry)).rejects.toMatchObject(unavailableAuth);
|
||||
const request = fetchCodexModelCatalog(account.registry);
|
||||
await expect(request).rejects.toMatchObject(unavailableAuth);
|
||||
});
|
||||
|
||||
it("requires the token's account-routing claim", async () => {
|
||||
const account = fixture();
|
||||
account.auth.auth.apiKey = "e30.e30.private-token";
|
||||
await expect(readCodexAuthScope(account.registry)).rejects.toMatchObject(unavailableAuth);
|
||||
const request = fetchCodexModelCatalog(account.registry);
|
||||
await expect(request).rejects.toMatchObject(unavailableAuth);
|
||||
});
|
||||
|
||||
it("does not repeat private Pi refresh errors", async () => {
|
||||
const account = fixture();
|
||||
account.registry.getProviderAuth.mockRejectedValue(new Error("private refresh details"));
|
||||
await expect(readCodexAuthScope(account.registry)).rejects.toMatchObject(unavailableAuth);
|
||||
const refreshError = new Error("private refresh details");
|
||||
account.registry.getProviderAuth.mockRejectedValue(refreshError);
|
||||
const request = fetchCodexModelCatalog(account.registry);
|
||||
await expect(request).rejects.toMatchObject(unavailableAuth);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -428,63 +426,66 @@ it("reports invalid JSON without repeating returned content", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["usage", "catalog", "auth"] as const)(
|
||||
"cancels during the public %s wrapper handoff",
|
||||
async (resource) => {
|
||||
const f = fixture();
|
||||
const controller = new AbortController();
|
||||
const abort = AbortController.prototype.abort;
|
||||
// The private flow aborts its deadline in finally, immediately before handing off.
|
||||
const cleanup = vi.spyOn(AbortController.prototype, "abort").mockImplementation(function (
|
||||
this: AbortController,
|
||||
reason?: unknown,
|
||||
) {
|
||||
abort.call(this, reason);
|
||||
if (this !== controller) abort.call(controller);
|
||||
});
|
||||
try {
|
||||
let request: Promise<unknown>;
|
||||
if (resource === "auth") request = readCodexAuthScope(f.registry, controller.signal);
|
||||
else if (resource === "usage")
|
||||
request = fetchCodexUsage(f.registry, controller.signal, f.fetcher);
|
||||
else request = fetchCodexModelCatalog(f.registry, controller.signal, f.fetcher);
|
||||
await expect(request).rejects.toMatchObject({ code: "cancelled" });
|
||||
} finally {
|
||||
cleanup.mockRestore();
|
||||
}
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
["usage", fetchCodexUsage],
|
||||
["catalog", fetchCodexModelCatalog],
|
||||
] as const)("cancels during the public %s wrapper handoff", async (_resource, fetchData) => {
|
||||
const f = fixture();
|
||||
const controller = new AbortController();
|
||||
const abort = AbortController.prototype.abort;
|
||||
// The private flow aborts its deadline in finally, immediately before handing off.
|
||||
const cleanup = vi.spyOn(AbortController.prototype, "abort").mockImplementation(function (
|
||||
this: AbortController,
|
||||
reason?: unknown,
|
||||
) {
|
||||
abort.call(this, reason);
|
||||
if (this !== controller) abort.call(controller);
|
||||
});
|
||||
try {
|
||||
const request = fetchData(f.registry, controller.signal, f.fetcher);
|
||||
await expect(request).rejects.toMatchObject({ code: "cancelled" });
|
||||
} finally {
|
||||
cleanup.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not resolve auth for an already-cancelled scope read", async () => {
|
||||
it("does not resolve auth for an already-cancelled catalog request", async () => {
|
||||
const f = fixture();
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
await expect(readCodexAuthScope(f.registry, controller.signal)).rejects.toMatchObject({
|
||||
code: "cancelled",
|
||||
});
|
||||
const request = fetchCodexModelCatalog(f.registry, controller.signal, f.fetcher);
|
||||
await expect(request).rejects.toMatchObject({ code: "cancelled" });
|
||||
expect(f.registry.getProviderAuth).not.toHaveBeenCalled();
|
||||
expect(f.fetcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores auth resolving after a scope read is cancelled", async () => {
|
||||
it("ignores auth resolving after a catalog request is cancelled", async () => {
|
||||
const f = fixture();
|
||||
const auth = deferred<AuthResult>();
|
||||
f.registry.getProviderAuth.mockReturnValue(auth.promise);
|
||||
const controller = new AbortController();
|
||||
const request = readCodexAuthScope(f.registry, controller.signal);
|
||||
const request = fetchCodexModelCatalog(f.registry, controller.signal, f.fetcher);
|
||||
controller.abort();
|
||||
await expect(request).rejects.toMatchObject({ code: "cancelled" });
|
||||
auth.resolve(f.auth);
|
||||
await auth.promise;
|
||||
expect(f.fetcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("bounds auth-only resolution by the same ten-second deadline", async () => {
|
||||
it("bounds the catalog's wait for Pi auth by the ten-second deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
const f = fixture();
|
||||
f.registry.getProviderAuth.mockReturnValue(new Promise(() => {}));
|
||||
const result = expect(readCodexAuthScope(f.registry)).rejects.toMatchObject({ code: "timeout" });
|
||||
const auth = deferred<AuthResult>();
|
||||
f.registry.getProviderAuth.mockReturnValue(auth.promise);
|
||||
const request = fetchCodexModelCatalog(f.registry, undefined, f.fetcher);
|
||||
const failure = expect(request).rejects.toMatchObject({ code: "timeout" });
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
await result;
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
await failure;
|
||||
auth.resolve(f.auth);
|
||||
await auth.promise;
|
||||
expect(f.fetcher).not.toHaveBeenCalled();
|
||||
const timerCount = vi.getTimerCount();
|
||||
expect(timerCount).toBe(0);
|
||||
});
|
||||
|
||||
it("cancels during handoff of an already-buffered response", async () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { AuthResult, Provider } from "@earendil-works/pi-ai";
|
||||
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
@@ -8,8 +7,7 @@ const ACCOUNT_URLS = {
|
||||
usage: `${BASE_URL}/wham/usage`,
|
||||
catalog: `${BASE_URL}/codex/models?client_version=0.162.0`,
|
||||
} as const;
|
||||
type NetworkResource = keyof typeof ACCOUNT_URLS;
|
||||
type AccountResource = NetworkResource | "auth";
|
||||
type AccountResource = keyof typeof ACCOUNT_URLS;
|
||||
const TIMEOUT_MS = 10_000;
|
||||
const MAX_RESPONSE_BYTES = 1024 * 1024;
|
||||
|
||||
@@ -20,13 +18,6 @@ export type CodexAccountRegistry = Pick<
|
||||
getProvider(id: string): Pick<Provider, "baseUrl" | "getModels"> | undefined;
|
||||
};
|
||||
|
||||
interface AccountData {
|
||||
data: unknown;
|
||||
authScope: string;
|
||||
}
|
||||
|
||||
export type CodexModelCatalog = AccountData;
|
||||
|
||||
type Failure = "auth" | "unsupported" | "cancelled" | "timeout" | "network" | "response" | "http";
|
||||
|
||||
export class CodexAccountError extends Error {
|
||||
@@ -46,28 +37,19 @@ export async function fetchCodexUsage(
|
||||
signal?: AbortSignal,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<unknown> {
|
||||
const result = await readAccountData("usage", registry, signal, fetcher);
|
||||
const data = await readAccountData("usage", registry, signal, fetcher);
|
||||
if (signal?.aborted) throw new CodexAccountError("cancelled", "Codex account request cancelled.");
|
||||
return result.data;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchCodexModelCatalog(
|
||||
registry: CodexAccountRegistry,
|
||||
signal?: AbortSignal,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<CodexModelCatalog> {
|
||||
const result = await readAccountData("catalog", registry, signal, fetcher);
|
||||
): Promise<unknown> {
|
||||
const data = await readAccountData("catalog", registry, signal, fetcher);
|
||||
if (signal?.aborted) throw new CodexAccountError("cancelled", "Codex account request cancelled.");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function readCodexAuthScope(
|
||||
registry: CodexAccountRegistry,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string> {
|
||||
const result = await readAccountData("auth", registry, signal);
|
||||
if (signal?.aborted) throw new CodexAccountError("cancelled", "Codex account request cancelled.");
|
||||
return result.authScope;
|
||||
return data;
|
||||
}
|
||||
|
||||
async function readAccountData(
|
||||
@@ -75,7 +57,7 @@ async function readAccountData(
|
||||
registry: CodexAccountRegistry,
|
||||
signal: AbortSignal | undefined,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<AccountData> {
|
||||
): Promise<unknown> {
|
||||
const deadline = new AbortController();
|
||||
const timer = setTimeout(() => deadline.abort(), TIMEOUT_MS);
|
||||
const requestSignal = signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal;
|
||||
@@ -83,14 +65,9 @@ async function readAccountData(
|
||||
requestSignal.throwIfAborted();
|
||||
const headers = await resolveAccountHeaders(registry, requestSignal);
|
||||
requestSignal.throwIfAborted();
|
||||
// Hash only the normalized credentials we actually send, never persist raw headers.
|
||||
const authScope = createHash("sha256")
|
||||
.update(JSON.stringify([headers.get("authorization"), headers.get("chatgpt-account-id")]))
|
||||
.digest("hex");
|
||||
if (resource === "auth") return { data: undefined, authScope };
|
||||
const data = await requestAccountJson(resource, headers, requestSignal, fetcher);
|
||||
requestSignal.throwIfAborted();
|
||||
return { data, authScope };
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (signal?.aborted)
|
||||
throw new CodexAccountError("cancelled", "Codex account request cancelled.");
|
||||
@@ -194,7 +171,7 @@ function authError(): CodexAccountError {
|
||||
}
|
||||
|
||||
async function requestAccountJson(
|
||||
resource: NetworkResource,
|
||||
resource: AccountResource,
|
||||
headers: Headers,
|
||||
requestSignal: AbortSignal,
|
||||
fetcher: typeof fetch,
|
||||
|
||||
@@ -554,7 +554,6 @@ function installResponseTransports(transport: Transport) {
|
||||
|
||||
function createHttpResponses(transport: Transport, payloads: Record<string, unknown>[]) {
|
||||
const catalog = vi.fn<typeof fetch>(async () =>
|
||||
// The client wraps this internally as { data, authScope }; the network JSON does not.
|
||||
Response.json({ models: [{ slug: "gpt-6-sol", service_tiers: [{ id: "priority" }] }] }),
|
||||
);
|
||||
const sse = vi.fn<typeof fetch>(async (_url, init) => {
|
||||
|
||||
@@ -5,11 +5,7 @@ import type {
|
||||
SessionEntry,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
CodexAccountError,
|
||||
type CodexModelCatalog,
|
||||
type fetchCodexModelCatalog,
|
||||
} from "./client.ts";
|
||||
import { CodexAccountError, type fetchCodexModelCatalog } from "./client.ts";
|
||||
import { FastController, type FastHost } from "./fast.ts";
|
||||
import { FAST_ENTRY, type FastPreference } from "./fast-state.ts";
|
||||
import { CONTEXT_ENTRY } from "./state.ts";
|
||||
@@ -27,9 +23,8 @@ const model: Model<Api> = {
|
||||
maxTokens: 128_000,
|
||||
};
|
||||
const otherModel: Model<Api> = { ...model, id: "gpt-6-astra", name: "Astra" };
|
||||
const catalog: CodexModelCatalog = {
|
||||
authScope: "scope-a",
|
||||
data: { models: [{ slug: model.id, service_tiers: [{ id: "default" }, { id: "priority" }] }] },
|
||||
const catalog = {
|
||||
models: [{ slug: model.id, service_tiers: [{ id: "default" }, { id: "priority" }] }],
|
||||
};
|
||||
|
||||
describe("commands and ownership", () => {
|
||||
@@ -113,7 +108,7 @@ describe("commands and ownership", () => {
|
||||
it("persists on only after cost confirmation and matching catalog discovery complete", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
const confirmation = deferred<boolean>();
|
||||
const response = deferred<CodexModelCatalog>();
|
||||
const response = deferred<unknown>();
|
||||
const started = deferred<void>();
|
||||
fast.ctx.ui.confirm.mockReturnValue(confirmation.promise);
|
||||
fast.fetchCatalog.mockImplementationOnce(() => {
|
||||
@@ -134,7 +129,7 @@ describe("commands and ownership", () => {
|
||||
fast.ctx.modelRegistry,
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
// No separate auth-scope preflight; the account client resolves auth for its one request.
|
||||
// The account client resolves auth for its one request; no separate preflight.
|
||||
expect(fast.ctx.modelRegistry.getProviderAuth).not.toHaveBeenCalled();
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
@@ -166,7 +161,7 @@ describe("commands and ownership", () => {
|
||||
},
|
||||
])("does not enable without established model support: %j", async ({ data, reason }) => {
|
||||
const fast = createFastControllerFixture();
|
||||
fast.fetchCatalog.mockResolvedValue({ ...catalog, data });
|
||||
fast.fetchCatalog.mockResolvedValue(data);
|
||||
await fast.controller.handle("on", fast.ctx);
|
||||
expect(fast.appendEntry).not.toHaveBeenCalled();
|
||||
expect(fast.controller.transform({ model: model.id }, fast.ctx)).toBeUndefined();
|
||||
@@ -401,7 +396,7 @@ describe("asynchronous enable ownership", () => {
|
||||
"discards catalog discovery after the %s changes",
|
||||
async (changed) => {
|
||||
const fast = createFastControllerFixture();
|
||||
const response = deferred<CodexModelCatalog>();
|
||||
const response = deferred<unknown>();
|
||||
const started = deferred<void>();
|
||||
fast.fetchCatalog.mockImplementationOnce(() => {
|
||||
started.resolve();
|
||||
@@ -420,7 +415,7 @@ describe("asynchronous enable ownership", () => {
|
||||
|
||||
it("aborts invalidated discovery without letting a late result clear a newer operation", async () => {
|
||||
const fast = createFastControllerFixture();
|
||||
const oldCatalog = deferred<CodexModelCatalog>();
|
||||
const oldCatalog = deferred<unknown>();
|
||||
const oldStarted = deferred<void>();
|
||||
fast.fetchCatalog.mockImplementationOnce(() => {
|
||||
oldStarted.resolve();
|
||||
@@ -458,7 +453,7 @@ describe("asynchronous enable ownership", () => {
|
||||
for (const outcome of ["resolve", "reject"]) {
|
||||
const fast = createFastControllerFixture();
|
||||
const confirmation = deferred<boolean>();
|
||||
const response = deferred<CodexModelCatalog>();
|
||||
const response = deferred<unknown>();
|
||||
const started = deferred<void>();
|
||||
if (stage === "confirmation") fast.ctx.ui.confirm.mockReturnValue(confirmation.promise);
|
||||
fast.fetchCatalog.mockImplementationOnce(() => {
|
||||
|
||||
@@ -235,7 +235,7 @@ export class FastController {
|
||||
): Promise<boolean> {
|
||||
const catalog = await this.fetchCatalog(ctx.modelRegistry, preparation.abortController.signal);
|
||||
if (!this.canApplyPreparation(ctx, preparation) || !ctx.model) return false;
|
||||
const eligibility = codexFastEligibility(catalog.data, ctx.model.id);
|
||||
const eligibility = codexFastEligibility(catalog, ctx.model.id);
|
||||
if (eligibility === "supported") return true;
|
||||
const reason =
|
||||
eligibility === "unsupported"
|
||||
|
||||
@@ -8,14 +8,13 @@ import {
|
||||
import type { Api, Model } from "@earendil-works/pi-ai";
|
||||
import { afterAll, afterEach, expect, it, vi } from "vitest";
|
||||
import codex from "./index.ts";
|
||||
import { fetchCodexModelCatalog, fetchCodexUsage, readCodexAuthScope } from "./client.ts";
|
||||
import { fetchCodexModelCatalog, fetchCodexUsage } from "./client.ts";
|
||||
import { customInteractionUI } from "./custom-interaction.test-helper.ts";
|
||||
|
||||
vi.mock("./client.ts", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("./client.ts")>()),
|
||||
fetchCodexUsage: vi.fn(),
|
||||
fetchCodexModelCatalog: vi.fn(),
|
||||
readCodexAuthScope: vi.fn(),
|
||||
}));
|
||||
|
||||
const directory = await vi.hoisted(async () => {
|
||||
@@ -43,7 +42,6 @@ afterEach(() => {
|
||||
async function load(prefix = "eleith", mode: ExtensionCommandContext["mode"] = "print") {
|
||||
vi.mocked(fetchCodexUsage).mockReset();
|
||||
vi.mocked(fetchCodexModelCatalog).mockReset();
|
||||
vi.mocked(readCodexAuthScope).mockReset();
|
||||
writeFileSync(configPath, JSON.stringify({ commandPrefix: prefix }));
|
||||
type Command = Parameters<ExtensionAPI["registerCommand"]>[1];
|
||||
type Listener = (
|
||||
@@ -154,7 +152,6 @@ it("wires restore/reconcile hooks without provider calls for an unsupported mode
|
||||
expect(context.pi.on.mock.calls.map(([name]) => name)).not.toContain("before_agent_start");
|
||||
expect(fetchCodexUsage).not.toHaveBeenCalled();
|
||||
expect(fetchCodexModelCatalog).not.toHaveBeenCalled();
|
||||
expect(readCodexAuthScope).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects removed commands and additional control arguments without side effects", async () => {
|
||||
@@ -176,7 +173,6 @@ it("rejects removed commands and additional control arguments without side effec
|
||||
expect(context.notify).toHaveBeenLastCalledWith(usage, "warning");
|
||||
}
|
||||
expect(fetchCodexModelCatalog).not.toHaveBeenCalled();
|
||||
expect(readCodexAuthScope).not.toHaveBeenCalled();
|
||||
expect(fetchCodexUsage).not.toHaveBeenCalled();
|
||||
expect(context.pi.appendEntry).not.toHaveBeenCalled();
|
||||
expect(context.pi.setModel).not.toHaveBeenCalled();
|
||||
@@ -265,7 +261,6 @@ it("combines context and fast mode in a read-only dashboard with the configured
|
||||
expect(getProviderAuth).not.toHaveBeenCalled();
|
||||
expect(fetchCodexUsage).not.toHaveBeenCalled();
|
||||
expect(fetchCodexModelCatalog).not.toHaveBeenCalled();
|
||||
expect(readCodexAuthScope).not.toHaveBeenCalled();
|
||||
expect(context.pi.appendEntry).not.toHaveBeenCalled();
|
||||
expect(context.pi.setModel).not.toHaveBeenCalled();
|
||||
const dashboard = context.notify.mock.calls[0][0];
|
||||
@@ -277,7 +272,6 @@ it("combines context and fast mode in a read-only dashboard with the configured
|
||||
expect(getProviderAuth).not.toHaveBeenCalled();
|
||||
expect(fetchCodexUsage).not.toHaveBeenCalled();
|
||||
expect(fetchCodexModelCatalog).not.toHaveBeenCalled();
|
||||
expect(readCodexAuthScope).not.toHaveBeenCalled();
|
||||
expect(context.pi.appendEntry).not.toHaveBeenCalled();
|
||||
expect(context.pi.setModel).not.toHaveBeenCalled();
|
||||
await context.emit("session_shutdown");
|
||||
@@ -324,7 +318,6 @@ it("routes every TUI read-only alias to the shared overview inspector, not notif
|
||||
expect(context.notify).not.toHaveBeenCalled();
|
||||
expect(fetchCodexUsage).not.toHaveBeenCalled();
|
||||
expect(fetchCodexModelCatalog).not.toHaveBeenCalled();
|
||||
expect(readCodexAuthScope).not.toHaveBeenCalled();
|
||||
expect(context.pi.setModel).not.toHaveBeenCalled();
|
||||
expect(context.pi.appendEntry).not.toHaveBeenCalled();
|
||||
await context.emit("session_shutdown");
|
||||
|
||||
Reference in new issue
Block a user