mcporter/tests/cli-daemon-fast-path.test.ts
Qi Zhang 4813cdfe7a
fix(cli): keep CloudBase authentication alive (#193)
Keep CloudBase device-code polling alive after `AUTH_PENDING` while preserving explicit ephemeral lifecycle overrides.

Co-authored-by: Qi Zhang <sevenzhang51@gmail.com>
2026-06-09 22:07:47 -07:00

118 lines
3.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
process.env.MCPORTER_DISABLE_AUTORUN = '1';
const mocks = vi.hoisted(() => {
const daemonCallTool = vi.fn();
const daemonListTools = vi.fn();
const daemonCloseServer = vi.fn();
const DaemonClient = vi.fn(function MockDaemonClient() {
return {
callTool: daemonCallTool,
listTools: daemonListTools,
listResources: vi.fn(),
readResource: vi.fn(),
closeServer: daemonCloseServer,
};
});
return {
DaemonClient,
createRuntime: vi.fn(),
daemonCallTool,
daemonListTools,
daemonCloseServer,
};
});
vi.mock('../src/daemon/client.js', () => ({
DaemonClient: mocks.DaemonClient,
}));
vi.mock('../src/runtime.js', () => ({
MCPORTER_VERSION: 'test',
createRuntime: mocks.createRuntime,
}));
const originalEnv = { ...process.env };
describe('daemon call fast path', () => {
beforeEach(() => {
vi.restoreAllMocks();
process.env = { ...originalEnv };
mocks.DaemonClient.mockClear();
mocks.createRuntime.mockClear();
mocks.daemonCallTool.mockReset().mockResolvedValue({
content: [{ type: 'text', text: 'ok' }],
});
mocks.daemonListTools.mockReset().mockResolvedValue([]);
mocks.daemonCloseServer.mockReset().mockResolvedValue(undefined);
process.exitCode = undefined;
});
afterEach(() => {
process.env = { ...originalEnv };
});
it('routes explicit default keep-alive calls without building the full runtime', async () => {
const { runCli } = await import('../src/cli.js');
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
await runCli(['call', 'chrome-devtools.list_pages', '--output', 'json']);
expect(mocks.createRuntime).not.toHaveBeenCalled();
expect(mocks.DaemonClient).toHaveBeenCalledWith(
expect.objectContaining({
configExplicit: false,
})
);
expect(mocks.daemonCallTool).toHaveBeenCalledWith({
server: 'chrome-devtools',
tool: 'list_pages',
args: {},
timeoutMs: expect.any(Number),
});
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"text": "ok"'));
});
it('also routes inferred call tokens through the daemon fast path', async () => {
const { runCli } = await import('../src/cli.js');
vi.spyOn(console, 'log').mockImplementation(() => {});
await runCli(['chrome-devtools.list_pages', '--output', 'json']);
expect(mocks.createRuntime).not.toHaveBeenCalled();
expect(mocks.daemonCallTool).toHaveBeenCalledWith(
expect.objectContaining({
server: 'chrome-devtools',
tool: 'list_pages',
})
);
});
it('leaves CloudBase calls on the config-aware runtime path', async () => {
mocks.createRuntime.mockRejectedValue(new Error('runtime path used'));
const { runCli } = await import('../src/cli.js');
await expect(runCli(['call', 'cloudbase.auth', '--output', 'json'])).rejects.toThrow('runtime path used');
expect(mocks.createRuntime).toHaveBeenCalled();
expect(mocks.daemonCallTool).not.toHaveBeenCalled();
});
it.each(['MCPORTER_RECORD', 'MCPORTER_REPLAY'] as const)(
'bypasses the daemon fast path while %s is active',
async (modeEnv) => {
process.env[modeEnv] = 'demo';
mocks.createRuntime.mockRejectedValue(new Error('runtime path used'));
const { runCli } = await import('../src/cli.js');
await expect(runCli(['call', 'chrome-devtools.list_pages', '--output', 'json'])).rejects.toThrow(
'runtime path used'
);
expect(mocks.createRuntime).toHaveBeenCalled();
expect(mocks.daemonCallTool).not.toHaveBeenCalled();
}
);
});