sync: update MCP client from GameCraftTable
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
/**
|
||||
* 覆盖系统凭据存储适配器的令牌分块、轮换清理与旧格式兼容行为。
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||
import { KeyringTokenStore } from "../src/credentials.js";
|
||||
import { credentialAccount, resolveClientOptions } from "../src/config.js";
|
||||
import { logoutRemote } from "../src/remote.js";
|
||||
|
||||
class MemoryEntry {
|
||||
constructor(private readonly values: Map<string, string>, private readonly account: string) {}
|
||||
@@ -64,3 +72,49 @@ test("keyring token store still reads and removes legacy single-entry credential
|
||||
await store.delete("account");
|
||||
assert.equal(values.size, 0);
|
||||
});
|
||||
|
||||
for (const damage of ["invalid-json", "missing-token-fields", "missing-chunk"] as const) {
|
||||
test(`local-only logout removes ${damage} credentials without remote requests`, async () => {
|
||||
const options = resolveClientOptions({ url: "https://example.test/mcp", env: {} });
|
||||
const account = credentialAccount(options);
|
||||
const values = new Map<string, string>();
|
||||
const store = memoryStore(values);
|
||||
if (damage === "missing-chunk") {
|
||||
await store.set(account, { access_token: "a".repeat(3000), token_type: "Bearer" });
|
||||
values.delete([...values.keys()].find((key) => key !== account)!);
|
||||
} else {
|
||||
values.set(account, damage === "invalid-json" ? "{broken" : "{}");
|
||||
}
|
||||
values.set("other-account", "untouched");
|
||||
await assert.rejects(store.get(account), /credential is invalid/);
|
||||
const discovery = new Map([[account, "cached"], ["other-account", "untouched"]]);
|
||||
const result = await logoutRemote(options, store, {
|
||||
async delete(key: string) { discovery.delete(key); },
|
||||
} as never, true, async () => { throw new Error("unexpected network request"); });
|
||||
|
||||
assert.deepEqual(result, { hadCredential: true, revoked: false });
|
||||
assert.deepEqual([...values], [["other-account", "untouched"]]);
|
||||
assert.deepEqual([...discovery], [["other-account", "untouched"]]);
|
||||
assert.equal(await store.get(account), undefined);
|
||||
assert.deepEqual(await logoutRemote(options, store, {
|
||||
async delete(key: string) { discovery.delete(key); },
|
||||
} as never, true), { hadCredential: false, revoked: false });
|
||||
});
|
||||
}
|
||||
|
||||
test("local-only logout reports credential store failures without claiming success", async () => {
|
||||
const store = new KeyringTokenStore(async () => { throw new Error("access denied"); });
|
||||
await assert.rejects(logoutRemote(resolveClientOptions({ env: {} }), store, {
|
||||
async delete() { assert.fail("discovery must be retained when credential deletion fails"); },
|
||||
} as never, true), /credential store is unavailable.*access denied/);
|
||||
});
|
||||
|
||||
test("keyring null for an absent Windows credential is treated as missing", async () => {
|
||||
const store = new KeyringTokenStore(async () => ({
|
||||
async getPassword() { return null; },
|
||||
async setPassword() { assert.fail("unexpected credential write"); },
|
||||
async deleteCredential() { return false; },
|
||||
}));
|
||||
assert.equal(await store.get("account"), undefined);
|
||||
assert.equal(await store.delete("account"), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user