mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Desktop-managed sessions were blocking the first visible token on slow or failing regular MCP connections, which made the UI feel much slower than direct bin runs. This keeps partial assistant streaming enabled for desktop sessions and moves regular MCP connection work off the critical path only for sdk-url startup. Constraint: Desktop sdk-url sessions must preserve chat responsiveness even when local MCP endpoints fail or hang Rejected: Leave MCP startup fully blocking in sdk-url mode | first visible token stayed several seconds behind direct CLI runs Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep blocking MCP startup for plain -p sessions unless you re-measure first-turn latency and tool availability tradeoffs Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/conversations.test.ts; same-prompt timing comparison for direct bin vs sdk-url session path Not-tested: Immediate first-turn availability of slow regular MCP tools in desktop sdk-url sessions
188 lines
7.3 KiB
TypeScript
188 lines
7.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
import * as fs from 'node:fs/promises'
|
|
import * as os from 'node:os'
|
|
import * as path from 'node:path'
|
|
import { ConversationService } from '../services/conversationService.js'
|
|
|
|
describe('ConversationService', () => {
|
|
let tmpDir: string
|
|
let originalConfigDir: string | undefined
|
|
let originalAuthToken: string | undefined
|
|
let originalBaseUrl: string | undefined
|
|
let originalModel: string | undefined
|
|
let originalEntrypoint: string | undefined
|
|
let originalOAuthToken: string | undefined
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cc-haha-conversation-service-'))
|
|
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
|
originalAuthToken = process.env.ANTHROPIC_AUTH_TOKEN
|
|
originalBaseUrl = process.env.ANTHROPIC_BASE_URL
|
|
originalModel = process.env.ANTHROPIC_MODEL
|
|
originalEntrypoint = process.env.CLAUDE_CODE_ENTRYPOINT
|
|
originalOAuthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
|
|
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
|
process.env.ANTHROPIC_AUTH_TOKEN = 'test-token'
|
|
process.env.ANTHROPIC_BASE_URL = 'https://example.invalid/anthropic'
|
|
process.env.ANTHROPIC_MODEL = 'test-model'
|
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'inherited-parent-oauth-token'
|
|
// Clear inherited CLAUDE_CODE_ENTRYPOINT so tests can assert whether
|
|
// buildChildEnv injects it or not without interference from the shell env.
|
|
delete process.env.CLAUDE_CODE_ENTRYPOINT
|
|
})
|
|
|
|
afterEach(async () => {
|
|
if (originalConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR
|
|
else process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
|
|
|
if (originalAuthToken === undefined) delete process.env.ANTHROPIC_AUTH_TOKEN
|
|
else process.env.ANTHROPIC_AUTH_TOKEN = originalAuthToken
|
|
|
|
if (originalBaseUrl === undefined) delete process.env.ANTHROPIC_BASE_URL
|
|
else process.env.ANTHROPIC_BASE_URL = originalBaseUrl
|
|
|
|
if (originalModel === undefined) delete process.env.ANTHROPIC_MODEL
|
|
else process.env.ANTHROPIC_MODEL = originalModel
|
|
|
|
if (originalEntrypoint === undefined) delete process.env.CLAUDE_CODE_ENTRYPOINT
|
|
else process.env.CLAUDE_CODE_ENTRYPOINT = originalEntrypoint
|
|
|
|
if (originalOAuthToken === undefined) delete process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
else process.env.CLAUDE_CODE_OAUTH_TOKEN = originalOAuthToken
|
|
|
|
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('keeps inherited provider env when no desktop provider config exists', async () => {
|
|
const service = new ConversationService() as any
|
|
const env = (await service.buildChildEnv('D:\\workspace\\code\\myself_code\\cc-haha')) as Record<string, string>
|
|
|
|
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('test-token')
|
|
expect(env.ANTHROPIC_BASE_URL).toBe('https://example.invalid/anthropic')
|
|
expect(env.ANTHROPIC_MODEL).toBe('test-model')
|
|
})
|
|
|
|
test('strips inherited provider env when desktop provider config exists', async () => {
|
|
const ccHahaDir = path.join(tmpDir, 'cc-haha')
|
|
await fs.mkdir(ccHahaDir, { recursive: true })
|
|
await fs.writeFile(
|
|
path.join(ccHahaDir, 'providers.json'),
|
|
JSON.stringify({ activeId: null, providers: [] }),
|
|
'utf-8',
|
|
)
|
|
|
|
const service = new ConversationService() as any
|
|
const env = (await service.buildChildEnv('D:\\workspace\\code\\myself_code\\cc-haha')) as Record<string, string>
|
|
|
|
expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
|
|
expect(env.ANTHROPIC_BASE_URL).toBeUndefined()
|
|
expect(env.ANTHROPIC_MODEL).toBeUndefined()
|
|
})
|
|
|
|
test('buildChildEnv injects CLAUDE_CODE_OAUTH_TOKEN when official mode + haha oauth token exists', async () => {
|
|
const ccHahaDir = path.join(tmpDir, 'cc-haha')
|
|
await fs.mkdir(ccHahaDir, { recursive: true })
|
|
await fs.writeFile(
|
|
path.join(ccHahaDir, 'settings.json'),
|
|
JSON.stringify({ env: {} }),
|
|
'utf-8',
|
|
)
|
|
|
|
const { hahaOAuthService } = await import('../services/hahaOAuthService.js')
|
|
await hahaOAuthService.saveTokens({
|
|
accessToken: 'haha-fresh-token',
|
|
refreshToken: 'haha-refresh-xxx',
|
|
expiresAt: Date.now() + 30 * 60_000,
|
|
scopes: ['user:inference'],
|
|
subscriptionType: 'max',
|
|
})
|
|
|
|
const service = new ConversationService() as any
|
|
const env = (await service.buildChildEnv('/tmp')) as Record<string, string>
|
|
|
|
expect(env.CLAUDE_CODE_ENTRYPOINT).toBe('claude-desktop')
|
|
expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBe('haha-fresh-token')
|
|
})
|
|
|
|
test('buildChildEnv does NOT inject CLAUDE_CODE_OAUTH_TOKEN when not official mode', async () => {
|
|
const ccHahaDir = path.join(tmpDir, 'cc-haha')
|
|
await fs.mkdir(ccHahaDir, { recursive: true })
|
|
await fs.writeFile(
|
|
path.join(ccHahaDir, 'settings.json'),
|
|
JSON.stringify({ env: { ANTHROPIC_AUTH_TOKEN: 'custom-provider-token' } }),
|
|
'utf-8',
|
|
)
|
|
|
|
const { hahaOAuthService } = await import('../services/hahaOAuthService.js')
|
|
await hahaOAuthService.saveTokens({
|
|
accessToken: 'haha-token-should-not-be-used',
|
|
refreshToken: null,
|
|
expiresAt: null,
|
|
scopes: [],
|
|
subscriptionType: null,
|
|
})
|
|
|
|
const service = new ConversationService() as any
|
|
const env = (await service.buildChildEnv('/tmp')) as Record<string, string>
|
|
|
|
expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined()
|
|
expect(env.CLAUDE_CODE_ENTRYPOINT).toBeUndefined()
|
|
})
|
|
|
|
test('buildChildEnv does not leak inherited CLAUDE_CODE_OAUTH_TOKEN when official token is unavailable', async () => {
|
|
const ccHahaDir = path.join(tmpDir, 'cc-haha')
|
|
await fs.mkdir(ccHahaDir, { recursive: true })
|
|
await fs.writeFile(
|
|
path.join(ccHahaDir, 'settings.json'),
|
|
JSON.stringify({ env: {} }),
|
|
'utf-8',
|
|
)
|
|
|
|
const service = new ConversationService() as any
|
|
const env = (await service.buildChildEnv('/tmp')) as Record<string, string>
|
|
|
|
expect(env.CLAUDE_CODE_ENTRYPOINT).toBe('claude-desktop')
|
|
expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined()
|
|
})
|
|
|
|
test('buildChildEnv injects desktop Computer Use host bundle id for sdk sessions', async () => {
|
|
const service = new ConversationService() as any
|
|
const env = (await service.buildChildEnv(
|
|
'/tmp',
|
|
'ws://127.0.0.1:3456/sdk/test-session?token=test-token',
|
|
)) as Record<string, string>
|
|
|
|
expect(env.CC_HAHA_COMPUTER_USE_HOST_BUNDLE_ID).toBe(
|
|
'com.claude-code-haha.desktop',
|
|
)
|
|
expect(env.CC_HAHA_DESKTOP_SERVER_URL).toBe('http://127.0.0.1:3456')
|
|
})
|
|
|
|
test('uses bun entrypoint fallback on Windows dev mode', () => {
|
|
const service = new ConversationService() as any
|
|
const args = service.resolveCliArgs(['--print'])
|
|
|
|
if (process.platform === 'win32') {
|
|
expect(args[0]).toBe(process.execPath)
|
|
expect(args[1]).toContain(path.join('src', 'entrypoints', 'cli.tsx'))
|
|
} else {
|
|
expect(args[0]).toContain(path.join('bin', 'claude-haha'))
|
|
}
|
|
})
|
|
|
|
test('buildSessionCliArgs enables partial assistant messages for desktop streaming', () => {
|
|
const service = new ConversationService() as any
|
|
const args = service.buildSessionCliArgs(
|
|
'123e4567-e89b-12d3-a456-426614174000',
|
|
'ws://127.0.0.1:3456/sdk/test-session?token=test-token',
|
|
false,
|
|
{ permissionMode: 'bypassPermissions' },
|
|
) as string[]
|
|
|
|
expect(args).toContain('--include-partial-messages')
|
|
expect(args).toContain('--sdk-url')
|
|
expect(args).toContain('--replay-user-messages')
|
|
})
|
|
})
|