344 lines
22 KiB
TypeScript
344 lines
22 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createRequire } from "node:module";
|
|
import { createHash } from "node:crypto";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { exportJWK, generateKeyPair, SignJWT } from "jose";
|
|
const require = createRequire(import.meta.url);
|
|
const { createChatGPT, encryptedStore, allowedAuthEndpoint, SCOPES } = require("../electron/chatgpt.cjs");
|
|
const { privateKey, publicKey } = await generateKeyPair("RS256");
|
|
const jwk = { ...await exportJWK(publicKey), kid: "scratch-key", alg: "RS256", use: "sig" };
|
|
const tokenUrl = "https://auth.openai.com/api/accounts/oauth/token";
|
|
const initial = () => ({ version: 1, hostId: "urn:uuid:00000000-0000-4000-8000-000000000001", activeId: null as string | null, registrations: [] as any[] });
|
|
async function signed(client: string, nonce: string, subject = "scratch-user", overrides: Record<string, unknown> = {}) {
|
|
return new SignJWT({ nonce, email: "dummy@example.invalid", ...overrides }).setProtectedHeader({ alg: "RS256", kid: "scratch-key" }).setIssuer("https://auth.openai.com").setSubject(subject).setAudience(client).setIssuedAt().setExpirationTime("1h").sign(privateKey);
|
|
}
|
|
function deferred<T = void>() { let resolve!: (value: T) => void; const promise = new Promise<T>((done) => { resolve = done; }); return { resolve, promise }; }
|
|
function fixture(options: { state?: any; expires?: number; timeoutMs?: number; token?: (form: URLSearchParams, url: URL) => Promise<Response>; write?: (next: any) => Promise<void>; secure?: () => void; change?: (push: any) => void; provider?: (target: string, init: RequestInit) => Promise<Response | undefined> } = {}) {
|
|
let saved = structuredClone(options.state ?? initial());
|
|
let url!: URL;
|
|
let writeCount = 0;
|
|
const changed = deferred();
|
|
const requests: { url: string; form?: URLSearchParams }[] = [];
|
|
const service = createChatGPT({
|
|
store: { secure() { options.secure?.(); }, async read() { return structuredClone(saved); }, async write(next: any) { writeCount++; await options.write?.(next); saved = structuredClone(next); } },
|
|
openExternal: async (value: string) => { url = new URL(value); },
|
|
timeoutMs: options.timeoutMs ?? 5000,
|
|
onChange: (push: any) => { options.change?.(push); if (!push.status.pending) changed.resolve(); },
|
|
fetch: async (target: string, init: RequestInit = {}) => {
|
|
const form = init.body instanceof URLSearchParams ? init.body : undefined;
|
|
requests.push({ url: target, form });
|
|
const response = await options.provider?.(target, init); if (response) return response;
|
|
if (target.endsWith("jwks.json")) return Response.json({ keys: [jwk] });
|
|
if (target === tokenUrl) {
|
|
if (options.token) return options.token(form!, url);
|
|
return Response.json({ access_token: "scratch-oauth-token", refresh_token: "scratch-refresh", id_token: await signed(form!.get("client_id")!, url.searchParams.get("nonce")!), token_type: "Bearer", expires_in: options.expires ?? 3600, scope: SCOPES.join(" ") });
|
|
}
|
|
if (target.endsWith("openid-configuration")) return Response.json({ issuer: "https://auth.openai.com", revocation_endpoint: "https://auth.openai.com/api/accounts/oauth/revoke" });
|
|
if (target.endsWith("/revoke")) return new Response(null, { status: 200 });
|
|
if (target.endsWith("/models")) return Response.json({ models: [{ slug: "gpt-5", display_name: "Scratch model", visibility: "list" }, { slug: "hidden", display_name: "Hidden", visibility: "hide" }] });
|
|
throw new Error("Unexpected network request in scratch fixture");
|
|
},
|
|
});
|
|
const rawLease = service.lease;
|
|
let discovered: Promise<any> | undefined;
|
|
service.lease = async (background: boolean) => {
|
|
discovered ??= service.models();
|
|
const catalog = await discovered;
|
|
return rawLease(background, { expectedAccountId: catalog.accountId ?? saved.activeId ?? "scratch-missing", expectedAuthorityRevision: catalog.authorityRevision, selectedModelId: "gpt-5" });
|
|
};
|
|
return { service, rawLease, requests, changed, get url() { return url; }, get saved() { return saved; }, get writeCount() { return writeCount; }, async callback(params: Record<string, string> = {}) {
|
|
const callback = new URL(url.searchParams.get("redirect_uri")!);
|
|
callback.search = new URLSearchParams({ state: url.searchParams.get("state")!, code: "scratch-code", client_id: "oaiapp_scratch", ...params }).toString();
|
|
return fetch(callback);
|
|
} };
|
|
}
|
|
async function loggedIn(options: Parameters<typeof fixture>[0] = {}) {
|
|
const f = fixture(options);
|
|
await f.service.login();
|
|
await f.callback();
|
|
await f.changed.promise;
|
|
return f;
|
|
}
|
|
|
|
test("PKCE/state/nonce/host identity and exact ephemeral 127.0.0.1 callback; no secret renderer snapshot", async () => {
|
|
const f = fixture();
|
|
try {
|
|
await f.service.login();
|
|
const params = f.url.searchParams;
|
|
assert.equal(params.get("client_id"), "dynamic_agent_client");
|
|
assert.equal(params.get("agent_name_hint"), "LearnHarness");
|
|
assert.equal(params.get("ext_agent_host_id"), f.saved.hostId);
|
|
assert.equal(new URL(params.get("redirect_uri")!).hostname, "127.0.0.1");
|
|
assert.equal(new URL(params.get("redirect_uri")!).pathname, "/auth/callback");
|
|
assert.equal((await f.callback({ state: "wrong" })).status, 400);
|
|
assert.equal(f.requests.length, 0, "unverified callback must never exchange a code");
|
|
await f.callback(); await f.changed.promise;
|
|
const form = f.requests.find((r) => r.url === tokenUrl)!.form!;
|
|
assert.equal(form.get("redirect_uri"), params.get("redirect_uri"));
|
|
assert.equal(form.get("client_id"), "oaiapp_scratch");
|
|
assert.equal(createHash("sha256").update(form.get("code_verifier")!).digest("base64url"), params.get("code_challenge"));
|
|
assert.equal(form.get("resource"), "https://api.openai.com/v1");
|
|
const status = await f.service.status();
|
|
assert.equal(status.accounts[0].background, false);
|
|
assert.equal(status.activeId, f.saved.activeId);
|
|
assert(!JSON.stringify(status).includes("scratch-oauth-token"));
|
|
assert(!JSON.stringify(status).includes("scratch-refresh"));
|
|
assert(!JSON.stringify(status).includes("idToken"));
|
|
assert.equal((await f.service.lease(false)).accessToken, "scratch-oauth-token");
|
|
await assert.rejects(f.service.lease(true), /background/);
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("returning sign-in reuses issued client/host and fresh PKCE; rejects account/client mismatch without replacing active credentials", async () => {
|
|
const f = await loggedIn();
|
|
try {
|
|
const first = f.url;
|
|
const status = await f.service.status();
|
|
await f.service.login(status.activeId);
|
|
assert.equal(f.url.searchParams.get("client_id"), "oaiapp_scratch");
|
|
assert.equal(f.url.searchParams.get("agent_name_hint"), null);
|
|
assert(f.url.searchParams.get("id_token_hint"));
|
|
assert.notEqual(f.url.searchParams.get("state"), first.searchParams.get("state"));
|
|
assert.notEqual(f.url.searchParams.get("nonce"), first.searchParams.get("nonce"));
|
|
assert.equal(f.url.searchParams.get("ext_agent_host_id"), first.searchParams.get("ext_agent_host_id"));
|
|
await f.callback({ client_id: "oaiapp_other" });
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(f.saved.registrations.length, 1);
|
|
assert.equal((await f.service.lease(false)).accessToken, "scratch-oauth-token");
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
for (const [label, claims] of [["nonce", { nonce: "wrong" }], ["issuer", { iss: "https://evil.invalid" }], ["audience", { aud: "oaiapp_wrong" }], ["expiry", { exp: 1 }]] as const) {
|
|
test(`rejects invalid ID-token ${label}`, async () => {
|
|
const f = fixture({ token: async (form, url) => {
|
|
const jwt = new SignJWT(Object.assign({ nonce: url.searchParams.get("nonce")!, email: "dummy@example.invalid", iss: "https://auth.openai.com", aud: form.get("client_id")!, sub: "scratch-user", iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600 }, claims));
|
|
return Response.json({ access_token: "scratch-access", refresh_token: "scratch-refresh", id_token: await jwt.setProtectedHeader({ alg: "RS256", kid: "scratch-key" }).sign(privateKey), token_type: "Bearer", expires_in: 3600, scope: SCOPES.join(" ") });
|
|
} });
|
|
try { await f.service.login(); await f.callback(); await f.changed.promise; assert.equal(f.saved.activeId, null); assert.equal(f.saved.registrations.length, 0); }
|
|
finally { f.service.close(); }
|
|
});
|
|
}
|
|
|
|
test("cancel and timeout close the listener without exchanging or replacing an active account", async () => {
|
|
const f = fixture({ timeoutMs: 30 });
|
|
try {
|
|
await f.service.login(); const callback = f.url.searchParams.get("redirect_uri")!;
|
|
f.service.cancelLogin();
|
|
await assert.rejects(fetch(callback));
|
|
assert.equal(f.saved.activeId, null);
|
|
const timeout = fixture({ timeoutMs: 15 });
|
|
try {
|
|
await timeout.service.login();
|
|
await timeout.changed.promise;
|
|
assert.equal((await timeout.service.status()).pending, false);
|
|
assert.equal(timeout.requests.length, 0);
|
|
} finally { timeout.service.close(); }
|
|
assert.equal(f.requests.length, 0);
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("missing plan scope retains verified registration but refuses inference and catalog", async () => {
|
|
const f = await loggedIn({ token: async (form, url) => Response.json({ id_token: await signed(form.get("client_id")!, url.searchParams.get("nonce")!), scope: "openid profile email", token_type: "Bearer" }) });
|
|
try {
|
|
assert.equal(f.saved.registrations.length, 1);
|
|
assert.equal((await f.service.status()).accounts[0].planEnabled, false);
|
|
await assert.rejects(f.service.lease(false), /scope/);
|
|
assert.deepEqual((await f.service.models()).models, []);
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("durable encrypted save failure never activates a pending login", async () => {
|
|
const f = fixture({ write: async () => { throw new Error("scratch disk failure containing no real data"); } });
|
|
try {
|
|
await f.service.login(); await f.callback(); await f.changed.promise;
|
|
assert.equal(f.saved.activeId, null);
|
|
await assert.rejects(f.service.lease(false), /disconnected/);
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("serialized concurrent refresh rotates once and persists before leases", async () => {
|
|
let refreshes = 0;
|
|
const f = await loggedIn({ expires: 1, token: async (form, url) => {
|
|
if (form.get("grant_type") === "refresh_token") { refreshes++; assert.equal(form.get("scope"), null); return Response.json({ access_token: "scratch-new-access", refresh_token: "scratch-rotated", token_type: "Bearer", expires_in: 3600 }); }
|
|
return Response.json({ access_token: "scratch-old-access", refresh_token: "scratch-old-refresh", id_token: await signed(form.get("client_id")!, url.searchParams.get("nonce")!), token_type: "Bearer", expires_in: 1, scope: SCOPES.join(" ") });
|
|
} });
|
|
try {
|
|
const leases = await Promise.all([f.service.lease(false), f.service.lease(false), f.service.lease(false)]);
|
|
assert.equal(refreshes, 1);
|
|
assert(leases.every((lease) => lease.accessToken === "scratch-new-access"));
|
|
assert.equal(f.saved.registrations[0].tokens.refreshToken, "scratch-rotated");
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("logout wins an in-flight refresh and retains only account/client/host mapping", async () => {
|
|
const started = deferred(); const release = deferred();
|
|
const f = await loggedIn({ token: async (form, url) => {
|
|
if (form.get("grant_type") === "refresh_token") { started.resolve(); await release.promise; return Response.json({ access_token: "scratch-late", refresh_token: "scratch-rotated", token_type: "Bearer", expires_in: 3600 }); }
|
|
return Response.json({ access_token: "scratch-old", refresh_token: "scratch-refresh", id_token: await signed(form.get("client_id")!, url.searchParams.get("nonce")!), token_type: "Bearer", expires_in: 1, scope: SCOPES.join(" ") });
|
|
} });
|
|
try {
|
|
const id = f.saved.activeId;
|
|
const refresh = f.service.lease(false); const rejected = assert.rejects(refresh);
|
|
await started.promise;
|
|
const logout = f.service.logout(id);
|
|
await new Promise((resolve) => setImmediate(resolve)); release.resolve();
|
|
await rejected; await logout;
|
|
assert.equal(f.saved.activeId, null);
|
|
assert.equal(f.saved.registrations[0].clientId, "oaiapp_scratch");
|
|
assert.equal(f.saved.registrations[0].tokens, undefined);
|
|
assert(f.requests.some((r) => r.form?.get("token_type_hint") === "refresh_token"));
|
|
await f.service.login(id);
|
|
assert.equal(f.url.searchParams.get("id_token_hint"), null);
|
|
assert.equal(f.url.searchParams.get("client_id"), "oaiapp_scratch");
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("account catalog is scoped, visibility filtered, and cleared on disconnect", async () => {
|
|
const f = await loggedIn();
|
|
try {
|
|
assert.deepEqual((await f.service.models()).models, [{ id: "gpt-5", name: "Scratch model" }]);
|
|
await f.service.logout(f.saved.activeId);
|
|
assert.deepEqual((await f.service.models()).models, []);
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("safeStorage refuses basic_text/unavailable; encrypted file owner-only, atomic, no plaintext fallback", async () => {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "lh-chatgpt-storage-"));
|
|
const file = path.join(dir, "credentials.enc.json");
|
|
const secure = { isEncryptionAvailable: () => true, getSelectedStorageBackend: () => "keychain", encryptString: (s: string) => Buffer.from(s.split("").reverse().join("")), decryptString: (b: Buffer) => b.toString().split("").reverse().join("") };
|
|
try {
|
|
const store = encryptedStore(file, secure);
|
|
const data = { ...initial(), marker: "scratch-secret-never-plaintext" };
|
|
await store.write(data);
|
|
assert(!(await fs.readFile(file, "utf8")).includes(data.marker));
|
|
assert.deepEqual(await store.read(), data);
|
|
assert.equal((await fs.stat(file)).mode & 0o777, 0o600);
|
|
assert.deepEqual(await fs.readdir(dir), ["credentials.enc.json"]);
|
|
for (const backend of [{ ...secure, getSelectedStorageBackend: () => "basic_text" }, { ...secure, isEncryptionAvailable: () => false }]) {
|
|
await assert.rejects(encryptedStore(file, backend).read(), /secure_storage/);
|
|
await assert.rejects(encryptedStore(file, backend).write(data), /secure_storage/);
|
|
}
|
|
assert(!allowedAuthEndpoint("https://auth.openai.com.evil.invalid/revoke"));
|
|
assert(!allowedAuthEndpoint("http://127.0.0.1/revoke"));
|
|
assert(!allowedAuthEndpoint("https://auth.openai.com@evil.invalid/revoke"));
|
|
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("invalid rotating replacement is durably revocation-only; verified mapping and prior validated ID survive restart", async () => {
|
|
const f = await loggedIn({ expires: 1, token: async (form, url) => form.get("grant_type") === "refresh_token"
|
|
? Response.json({ access_token: "never-save-this-access", refresh_token: "likely-latest-revoke-handle", id_token: "invalid.signature.jwt", token_type: "Bearer", expires_in: 3600 })
|
|
: Response.json({ access_token: "old-verified-access", refresh_token: "old-refresh", id_token: await signed(form.get("client_id")!, url.searchParams.get("nonce")!), token_type: "Bearer", expires_in: 1, scope: SCOPES.join(" ") }) });
|
|
try {
|
|
const before = structuredClone(f.saved.registrations[0]);
|
|
await assert.rejects(f.service.lease(false));
|
|
const restricted = f.saved.registrations[0];
|
|
assert.equal(restricted.reconnectRequired, true);
|
|
for (const field of ["id", "issuer", "subject", "email", "clientId"]) assert.equal(restricted[field], before[field]);
|
|
assert.deepEqual(restricted.tokens, { ...before.tokens, refreshToken: "likely-latest-revoke-handle" });
|
|
assert(!JSON.stringify(f.saved).includes("never-save-this-access"));
|
|
const restarted = fixture({ state: f.saved });
|
|
try {
|
|
await restarted.service.status();
|
|
await restarted.service.select(restricted.id);
|
|
restarted.service.resume(); await restarted.service.consent(restricted.id, true);
|
|
await assert.rejects(restarted.service.lease(false));
|
|
assert.equal((await restarted.service.status()).accounts[0].connected, false);
|
|
assert.deepEqual((await restarted.service.models()).models, []);
|
|
assert.equal(restarted.requests.filter((r) => r.url === tokenUrl).length, 0, "restricted material cannot refresh for inference/catalog");
|
|
await restarted.service.logout(restricted.id);
|
|
assert.equal(restarted.requests.find((r) => r.form?.has("token_type_hint"))!.form!.get("token"), "likely-latest-revoke-handle");
|
|
assert.equal(restarted.saved.registrations[0].reconnectRequired, true);
|
|
await restarted.service.login(restricted.id); await restarted.callback(); await restarted.changed.promise;
|
|
// Wait for callback's asynchronous encrypted activation, not the prior status push.
|
|
for (let i = 0; i < 30 && restarted.saved.registrations[0].reconnectRequired; i++) await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(restarted.saved.registrations[0].reconnectRequired, undefined);
|
|
} finally { restarted.service.close(); }
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("detected secure backend loss emits full invalidation, denies all leases and cannot be repaired by consent/resume", async () => {
|
|
let lost = false; const pushes: any[] = [];
|
|
const f = await loggedIn({ secure: () => { if (lost) throw Object.assign(new Error("secure_storage"), { code: "secure_storage" }); }, change: (push) => pushes.push(push) });
|
|
try {
|
|
await f.service.lease(false); const epoch = f.service.stamp().epoch;
|
|
lost = true;
|
|
assert.equal((await f.service.status()).available, false);
|
|
assert(pushes.some((p) => p.invalidate === "all" && p.stamp.epoch > epoch));
|
|
await assert.rejects(f.service.lease(false)); await assert.rejects(f.service.lease(true));
|
|
lost = false; f.service.resume(); await f.service.consent(f.saved.activeId, true);
|
|
await assert.rejects(f.service.lease(false));
|
|
assert.equal((await f.service.status()).accounts[0].connected, false);
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("strict encrypted schema rejects malformed marker/duplicates/token/active identity without changing original bytes", async () => {
|
|
const f = await loggedIn(); f.service.close();
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "lh-chatgpt-corrupt-"));
|
|
const file = path.join(dir, "credentials.enc.json");
|
|
const secure = { isEncryptionAvailable: () => true, encryptString: (s: string) => Buffer.from(s), decryptString: (b: Buffer) => b.toString() };
|
|
try {
|
|
for (const mutate of [(s: any) => s.registrations[0].reconnectRequired = false, (s: any) => s.registrations[0].reconnectRequired = "true", (s: any) => s.registrations.push(s.registrations[0]), (s: any) => s.activeId = "missing", (s: any) => s.registrations[0].tokens.scopes = "not-array", (s: any) => s.registrations[0].tokens.expiresAt = -1]) {
|
|
const state = structuredClone(f.saved); mutate(state);
|
|
const bytes = JSON.stringify({ version: 1, encrypted: Buffer.from(JSON.stringify(state)).toString("base64") });
|
|
await fs.writeFile(file, bytes);
|
|
const service = createChatGPT({ store: encryptedStore(file, secure), openExternal: () => assert.fail("no OAuth"), fetch: () => assert.fail("no provider") });
|
|
try { assert.equal((await service.status()).available, false); await assert.rejects(service.login()); assert.equal(await fs.readFile(file, "utf8"), bytes); }
|
|
finally { service.close(); }
|
|
}
|
|
} finally { await fs.rm(dir, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test("consent OFF wins an older ON save; foreground catalog authority remains unchanged", async () => {
|
|
let hold = false; const entered = deferred(); const release = deferred();
|
|
const f = await loggedIn({ write: async (next) => { if (hold && next.registrations[0]?.background) { entered.resolve(); await release.promise; } } });
|
|
try {
|
|
const catalog = await f.service.models(); const id = f.saved.activeId;
|
|
hold = true;
|
|
const on = f.service.consent(id, true); await entered.promise;
|
|
const off = f.service.consent(id, false);
|
|
assert.equal((await f.service.status()).accounts[0].background, false);
|
|
release.resolve(); await on; await off;
|
|
assert.equal(f.saved.registrations[0].background, false);
|
|
assert.equal(f.service.stamp().authorityRevision, catalog.authorityRevision);
|
|
assert.equal((await f.rawLease(false, { expectedAccountId: id, expectedAuthorityRevision: catalog.authorityRevision, selectedModelId: "gpt-5" })).accountId, id);
|
|
await assert.rejects(f.rawLease(true, { expectedAccountId: id, expectedAuthorityRevision: catalog.authorityRevision, selectedModelId: "gpt-5" }), /background/);
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("cancellation during verified durable save cannot activate late credentials", async () => {
|
|
const entered = deferred(); const release = deferred(); let blocked = true;
|
|
const f = fixture({ write: async (next) => { if (blocked && next.registrations.length) { entered.resolve(); await release.promise; blocked = false; } } });
|
|
try {
|
|
await f.service.login(); await f.callback(); await entered.promise;
|
|
f.service.cancelLogin(); release.resolve();
|
|
for (let i = 0; i < 30 && f.writeCount < 2; i++) await new Promise((resolve) => setImmediate(resolve));
|
|
assert.equal(f.saved.activeId, null);
|
|
assert.equal((await f.service.status()).accounts.length, 0);
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("scope-disabled credentials remain revocable and unconfirmed revoke never claims success", async () => {
|
|
const f = await loggedIn({ token: async (form, url) => Response.json({ id_token: await signed(form.get("client_id")!, url.searchParams.get("nonce")!), refresh_token: "scope-disabled-refresh", scope: "openid profile email", token_type: "Bearer" }), provider: async (url) => url.endsWith("/revoke") ? new Response(null, { status: 500 }) : undefined });
|
|
try {
|
|
await assert.rejects(f.service.lease(false));
|
|
const status = await f.service.logout(f.saved.activeId);
|
|
assert.match(status.error, /Remote revocation was not confirmed/);
|
|
assert.equal(f.requests.filter((r) => r.form?.get("token") === "scope-disabled-refresh").length, 2);
|
|
assert.equal(f.saved.registrations[0].tokens, undefined);
|
|
} finally { f.service.close(); }
|
|
});
|
|
|
|
test("private leases bind active account, authority revision and raw visible model membership", async () => {
|
|
const f = await loggedIn();
|
|
try {
|
|
const catalog = await f.service.models();
|
|
const expected = { expectedAccountId: catalog.accountId, expectedAuthorityRevision: catalog.authorityRevision, selectedModelId: "gpt-5" };
|
|
await assert.rejects(f.rawLease(false));
|
|
for (const mutation of [{ expectedAccountId: "another" }, { expectedAuthorityRevision: catalog.authorityRevision - 1 }, { selectedModelId: "hidden" }]) await assert.rejects(f.rawLease(false, { ...expected, ...mutation }));
|
|
assert.equal((await f.rawLease(false, expected)).accountId, catalog.accountId);
|
|
} finally { f.service.close(); }
|
|
});
|