diff --git a/src/oauth-vault.ts b/src/oauth-vault.ts index 80cb8fa..1a41201 100644 --- a/src/oauth-vault.ts +++ b/src/oauth-vault.ts @@ -6,8 +6,6 @@ import type { OAuthClientInformationMixed, OAuthTokens } from '@modelcontextprot import type { ServerDefinition } from './config.js'; import { readJsonFile, writeJsonFile } from './fs-json.js'; -const VAULT_PATH = path.join(os.homedir(), '.mcporter', 'credentials.json'); - type VaultKey = string; export interface VaultEntry { @@ -25,10 +23,14 @@ interface VaultFile { entries: Record; } +function vaultPath(): string { + return path.join(os.homedir(), '.mcporter', 'credentials.json'); +} + async function readVault(): Promise { let shouldRewrite = false; try { - const existing = await readJsonFile(VAULT_PATH); + const existing = await readJsonFile(vaultPath()); if (existing && existing.version === 1 && existing.entries && typeof existing.entries === 'object') { return existing; } @@ -46,9 +48,10 @@ async function readVault(): Promise { } async function writeVault(contents: VaultFile): Promise { - const dir = path.dirname(VAULT_PATH); + const filePath = vaultPath(); + const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); - await writeJsonFile(VAULT_PATH, contents); + await writeJsonFile(filePath, contents); } export function vaultKeyForDefinition(definition: ServerDefinition): VaultKey { diff --git a/tests/cli-call-errors.test.ts b/tests/cli-call-errors.test.ts index 2e5ab3a..982d8b9 100644 --- a/tests/cli-call-errors.test.ts +++ b/tests/cli-call-errors.test.ts @@ -24,4 +24,27 @@ describe('CLI call error reporting', () => { logSpy.mockRestore(); errorSpy.mockRestore(); }); + + it('emits structured http envelopes for non-auth transport failures', async () => { + const { handleCall } = await cliModulePromise; + const callTool = vi.fn().mockRejectedValue(new Error('SSE error: Non-200 status code (410)')); + const runtime = { + callTool, + close: vi.fn().mockResolvedValue(undefined), + } as unknown as Awaited>; + + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + await handleCall(runtime, ['deepwiki.read_wiki_structure', '--output', 'json']); + + const payload = JSON.parse(logSpy.mock.calls.at(-1)?.[0] ?? '{}'); + expect(payload.issue?.kind).toBe('http'); + expect(payload.issue?.statusCode).toBe(410); + expect(payload.tool).toBe('read_wiki_structure'); + expect(errorSpy.mock.calls.some((call) => call.join(' ').includes('responded with HTTP 410'))).toBe(true); + + logSpy.mockRestore(); + errorSpy.mockRestore(); + }); }); diff --git a/tests/cli-output-utils.test.ts b/tests/cli-output-utils.test.ts index 6364c2a..4faebf5 100644 --- a/tests/cli-output-utils.test.ts +++ b/tests/cli-output-utils.test.ts @@ -116,6 +116,14 @@ describe('printCallOutput format selection', () => { expect(String(logged)).toContain("type: 'json'"); }, ], + [ + 'auto falls back to readable raw output for plain object payloads', + 'auto', + { result: 'Available pages for facebook/react' }, + (logged: unknown) => { + expect(String(logged)).toContain("result: 'Available pages for facebook/react'"); + }, + ], ] as const)('%s', (_name, format, raw, assertLogged) => { const wrapped = createCallResult(raw); const log = vi.spyOn(console, 'log').mockImplementation(() => {}); diff --git a/vitest.config.ts b/vitest.config.ts index f494188..d25f57e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -14,5 +14,8 @@ export default defineConfig({ // Quiet mode hides console output for passing tests so CLI fixture logs // (e.g., the full `mcporter list` banners) don't overwhelm the reporter. ...quietReporterOptions, + // CLI-heavy suites import the full entrypoint in parallel and can exceed the + // default 5s timeout under local load even when behavior is correct. + testTimeout: 10_000, }, });