mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-13 12:14:47 +00:00
refactor: apply minimal tfcode branding
- Rename packages/opencode → packages/tfcode (directory only) - Rename bin/opencode → bin/tfcode (CLI binary) - Rename .opencode → .tfcode (config directory) - Update package.json name and bin field - Update config directory path references (.tfcode) - Keep internal code references as 'opencode' for easy upstream sync - Keep @opencode-ai/* workspace package names This minimal branding approach allows clean merges from upstream opencode repository while providing tfcode branding for users.
This commit is contained in:
249
packages/tfcode/test/mcp/oauth-browser.test.ts
Normal file
249
packages/tfcode/test/mcp/oauth-browser.test.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { test, expect, mock, beforeEach } from "bun:test"
|
||||
import { EventEmitter } from "events"
|
||||
|
||||
// Track open() calls and control failure behavior
|
||||
let openShouldFail = false
|
||||
let openCalledWith: string | undefined
|
||||
|
||||
mock.module("open", () => ({
|
||||
default: async (url: string) => {
|
||||
openCalledWith = url
|
||||
|
||||
// Return a mock subprocess that emits an error if openShouldFail is true
|
||||
const subprocess = new EventEmitter()
|
||||
if (openShouldFail) {
|
||||
// Emit error asynchronously like a real subprocess would
|
||||
setTimeout(() => {
|
||||
subprocess.emit("error", new Error("spawn xdg-open ENOENT"))
|
||||
}, 10)
|
||||
}
|
||||
return subprocess
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock UnauthorizedError
|
||||
class MockUnauthorizedError extends Error {
|
||||
constructor() {
|
||||
super("Unauthorized")
|
||||
this.name = "UnauthorizedError"
|
||||
}
|
||||
}
|
||||
|
||||
// Track what options were passed to each transport constructor
|
||||
const transportCalls: Array<{
|
||||
type: "streamable" | "sse"
|
||||
url: string
|
||||
options: { authProvider?: unknown }
|
||||
}> = []
|
||||
|
||||
// Mock the transport constructors
|
||||
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
||||
StreamableHTTPClientTransport: class MockStreamableHTTP {
|
||||
url: string
|
||||
authProvider: { redirectToAuthorization?: (url: URL) => Promise<void> } | undefined
|
||||
constructor(url: URL, options?: { authProvider?: { redirectToAuthorization?: (url: URL) => Promise<void> } }) {
|
||||
this.url = url.toString()
|
||||
this.authProvider = options?.authProvider
|
||||
transportCalls.push({
|
||||
type: "streamable",
|
||||
url: url.toString(),
|
||||
options: options ?? {},
|
||||
})
|
||||
}
|
||||
async start() {
|
||||
// Simulate OAuth redirect by calling the authProvider's redirectToAuthorization
|
||||
if (this.authProvider?.redirectToAuthorization) {
|
||||
await this.authProvider.redirectToAuthorization(new URL("https://auth.example.com/authorize?client_id=test"))
|
||||
}
|
||||
throw new MockUnauthorizedError()
|
||||
}
|
||||
async finishAuth(_code: string) {
|
||||
// Mock successful auth completion
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
|
||||
SSEClientTransport: class MockSSE {
|
||||
constructor(url: URL) {
|
||||
transportCalls.push({
|
||||
type: "sse",
|
||||
url: url.toString(),
|
||||
options: {},
|
||||
})
|
||||
}
|
||||
async start() {
|
||||
throw new Error("Mock SSE transport cannot connect")
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the MCP SDK Client to trigger OAuth flow
|
||||
mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
|
||||
Client: class MockClient {
|
||||
async connect(transport: { start: () => Promise<void> }) {
|
||||
await transport.start()
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock UnauthorizedError in the auth module
|
||||
mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||
UnauthorizedError: MockUnauthorizedError,
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
openShouldFail = false
|
||||
openCalledWith = undefined
|
||||
transportCalls.length = 0
|
||||
})
|
||||
|
||||
// Import modules after mocking
|
||||
const { MCP } = await import("../../src/mcp/index")
|
||||
const { Bus } = await import("../../src/bus")
|
||||
const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
|
||||
const { Instance } = await import("../../src/project/instance")
|
||||
const { tmpdir } = await import("../fixture/fixture")
|
||||
|
||||
test("BrowserOpenFailed event is published when open() throws", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
`${dir}/opencode.json`,
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
"test-oauth-server": {
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
openShouldFail = true
|
||||
|
||||
const events: Array<{ mcpName: string; url: string }> = []
|
||||
const unsubscribe = Bus.subscribe(MCP.BrowserOpenFailed, (evt) => {
|
||||
events.push(evt.properties)
|
||||
})
|
||||
|
||||
// Run authenticate with a timeout to avoid waiting forever for the callback
|
||||
// Attach a handler immediately so callback shutdown rejections
|
||||
// don't show up as unhandled between tests.
|
||||
const authPromise = MCP.authenticate("test-oauth-server").catch(() => undefined)
|
||||
|
||||
// Config.get() can be slow in tests, so give it plenty of time.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000))
|
||||
|
||||
// Stop the callback server and cancel any pending auth
|
||||
await McpOAuthCallback.stop()
|
||||
|
||||
await authPromise
|
||||
|
||||
unsubscribe()
|
||||
|
||||
// Verify the BrowserOpenFailed event was published
|
||||
expect(events.length).toBe(1)
|
||||
expect(events[0].mcpName).toBe("test-oauth-server")
|
||||
expect(events[0].url).toContain("https://")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("BrowserOpenFailed event is NOT published when open() succeeds", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
`${dir}/opencode.json`,
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
"test-oauth-server-2": {
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
openShouldFail = false
|
||||
|
||||
const events: Array<{ mcpName: string; url: string }> = []
|
||||
const unsubscribe = Bus.subscribe(MCP.BrowserOpenFailed, (evt) => {
|
||||
events.push(evt.properties)
|
||||
})
|
||||
|
||||
// Run authenticate with a timeout to avoid waiting forever for the callback
|
||||
const authPromise = MCP.authenticate("test-oauth-server-2").catch(() => undefined)
|
||||
|
||||
// Config.get() can be slow in tests; also covers the ~500ms open() error-detection window.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000))
|
||||
|
||||
// Stop the callback server and cancel any pending auth
|
||||
await McpOAuthCallback.stop()
|
||||
|
||||
await authPromise
|
||||
|
||||
unsubscribe()
|
||||
|
||||
// Verify NO BrowserOpenFailed event was published
|
||||
expect(events.length).toBe(0)
|
||||
// Verify open() was still called
|
||||
expect(openCalledWith).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("open() is called with the authorization URL", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
`${dir}/opencode.json`,
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
"test-oauth-server-3": {
|
||||
type: "remote",
|
||||
url: "https://example.com/mcp",
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
openShouldFail = false
|
||||
openCalledWith = undefined
|
||||
|
||||
// Run authenticate with a timeout to avoid waiting forever for the callback
|
||||
const authPromise = MCP.authenticate("test-oauth-server-3").catch(() => undefined)
|
||||
|
||||
// Config.get() can be slow in tests; also covers the ~500ms open() error-detection window.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000))
|
||||
|
||||
// Stop the callback server and cancel any pending auth
|
||||
await McpOAuthCallback.stop()
|
||||
|
||||
await authPromise
|
||||
|
||||
// Verify open was called with a URL
|
||||
expect(openCalledWith).toBeDefined()
|
||||
expect(typeof openCalledWith).toBe("string")
|
||||
expect(openCalledWith!).toContain("https://")
|
||||
},
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user