Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/lib/mcp/browser-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// <reference types="bun-types" />

import { expect, test } from "bun:test";
import { buildBrowserCreateConfig } from "@/lib/mcp/browser-config";

test("browser create config rejects an empty start URL", () => {
expect(buildBrowserCreateConfig({ start_url: "" })).toEqual({
ok: false,
error: "Error: start_url must be a valid URL.",
});
});
32 changes: 26 additions & 6 deletions src/lib/mcp/browser-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export type BrowserCreateConfigParams = BrowserProfileParams &
start_url?: string;
};

export type BrowserSharedConfigParams = BrowserProfileParams &
BrowserExtensionParams &
BrowserViewportParams;

export type BrowserUpdateConfigParams = BrowserProfileParams &
BrowserViewportUpdateParams;

Expand Down Expand Up @@ -67,6 +71,11 @@ export type BrowserCreateConfig = Pick<
"profile" | "extensions" | "viewport" | "start_url"
>;

export type BrowserSharedConfig = Pick<
BrowserCreateParams,
"profile" | "extensions" | "viewport"
>;

export type BrowserUpdateConfig = Pick<
BrowserUpdateParams,
"profile" | "viewport"
Expand Down Expand Up @@ -188,9 +197,9 @@ function buildBrowserViewportUpdate(
});
}

export function buildBrowserCreateConfig(
params: BrowserCreateConfigParams,
): BrowserConfigResult<BrowserCreateConfig> {
export function buildBrowserSharedConfig(
params: BrowserSharedConfigParams,
): BrowserConfigResult<BrowserSharedConfig> {
const profile = buildBrowserProfile(params);
if (!profile.ok) return profile;

Expand All @@ -200,13 +209,24 @@ export function buildBrowserCreateConfig(
const viewport = buildBrowserViewport(params);
if (!viewport.ok) return viewport;

const startUrl = buildBrowserStartUrl(params.start_url);
if (!startUrl.ok) return startUrl;

return configValue({
...(profile.value && { profile: profile.value }),
...(extensions.value && { extensions: extensions.value }),
...(viewport.value && { viewport: viewport.value }),
});
}

export function buildBrowserCreateConfig(
params: BrowserCreateConfigParams,
): BrowserConfigResult<BrowserCreateConfig> {
const sharedConfig = buildBrowserSharedConfig(params);
if (!sharedConfig.ok) return sharedConfig;

const startUrl = buildBrowserStartUrl(params.start_url);
if (!startUrl.ok) return startUrl;

return configValue({
...sharedConfig.value,
...(startUrl.value !== undefined && { start_url: startUrl.value }),
});
}
Expand Down
24 changes: 24 additions & 0 deletions src/lib/mcp/kernel-client.test-fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { mock } from "bun:test";

export const unusedKernelClient = new Proxy(
{},
{
get: () => {
throw new Error("unexpected Kernel client use");
},
},
);

export const kernelClientMock: {
factory: (token: string) => any;
} = {
factory: () => unusedKernelClient,
};

export function resetKernelClientFactory() {
kernelClientMock.factory = () => unusedKernelClient;
}

mock.module("@/lib/mcp/kernel-client", () => ({
createKernelClient: (token: string) => kernelClientMock.factory(token),
}));
30 changes: 6 additions & 24 deletions src/lib/mcp/tools/auth-connections.test-fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,17 @@
import { expect, mock } from "bun:test";
import { expect } from "bun:test";
import type { KernelClient } from "@/lib/mcp/kernel-client";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type {
ManagedAuth,
ManagedAuthTimelineEvent,
} from "@onkernel/sdk/resources/auth/connections";
export {
kernelClientMock,
resetKernelClientFactory,
unusedKernelClient,
} from "@/lib/mcp/kernel-client.test-fixtures";
import { registerAuthConnectionTools } from "./auth-connections";

export const unusedKernelClient = new Proxy(
{},
{
get: () => {
throw new Error("unexpected Kernel client use");
},
},
);

export const kernelClientMock: {
factory: (token: string) => any;
} = {
factory: () => unusedKernelClient,
};

mock.module("@/lib/mcp/kernel-client", () => ({
createKernelClient: (token: string) => kernelClientMock.factory(token),
}));

export function resetKernelClientFactory() {
kernelClientMock.factory = () => unusedKernelClient;
}

export function connection(overrides: Partial<ManagedAuth> = {}): ManagedAuth {
return {
id: "conn_1",
Expand Down
31 changes: 8 additions & 23 deletions src/lib/mcp/tools/auth-login-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { encodeSessionId } from "@posthog/mcp";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { MANAGED_AUTH_APP_HTML } from "@/lib/mcp/apps/generated/managed-auth-app";
import {
kernelClientMock,
resetKernelClientFactory,
} from "@/lib/mcp/kernel-client.test-fixtures";
import { verifyAuthFlowCheckpoint } from "@/lib/mcp/tools/managed-auth-checkpoint";
import {
initializeDeclaresMcpApps,
Expand All @@ -14,32 +18,13 @@ import {

process.env.CLERK_SECRET_KEY ??= "test-clerk-secret";

// Tests that exercise API-backed handlers substitute a fake Kernel client.
// The default stub errors if any API method is actually invoked.
const unusedKernelClient = new Proxy(
{},
{
get: () => {
throw new Error("unexpected Kernel client use");
},
},
);
let kernelClientFactory: (token: string) => any = () => unusedKernelClient;
function resetKernelClientFactory() {
kernelClientFactory = () => unusedKernelClient;
}

// The capability gate falls back to a Redis marker (recorded by the route
// layer at initialize) on stateless transports. Tests control it directly.
let redisMarkerPresent = false;
mock.module("@/lib/redis", () => ({
hasMcpAppsClient: async () => redisMarkerPresent,
markMcpAppsClient: async () => {},
}));
mock.module("@/lib/mcp/kernel-client", () => ({
createKernelClient: (token: string) => kernelClientFactory(token),
}));

type ToolRegistration = {
config: Record<string, any>;
handler: (params: any, extra: any) => Promise<any>;
Expand Down Expand Up @@ -243,7 +228,7 @@ describe("managed-auth MCP App registration", () => {
// Simulates the streamable-HTTP path: no client capabilities on the
// per-request server, but the route layer recorded the capability.
redisMarkerPresent = true;
kernelClientFactory = () => ({
kernelClientMock.factory = () => ({
auth: {
connections: {
retrieve: async () => ({
Expand Down Expand Up @@ -361,7 +346,7 @@ describe("managed-auth MCP App registration", () => {
});

test("reauth launcher issues a signed server checkpoint, never a guessed flow type", async () => {
kernelClientFactory = () => ({
kernelClientMock.factory = () => ({
auth: {
connections: {
retrieve: async () => ({
Expand Down Expand Up @@ -416,7 +401,7 @@ describe("managed-auth MCP App registration", () => {
});

test("reauth launcher preserves an explicitly empty timeline baseline", async () => {
kernelClientFactory = () => ({
kernelClientMock.factory = () => ({
auth: {
connections: {
retrieve: async () => ({
Expand Down Expand Up @@ -454,7 +439,7 @@ describe("managed-auth MCP App registration", () => {
});

test("reauth launcher identifies an already-live flow", async () => {
kernelClientFactory = () => ({
kernelClientMock.factory = () => ({
auth: {
connections: {
retrieve: async () => ({
Expand Down
Loading
Loading