mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
This captures the pending worktree fixes before applying them to the current local main. The changes tighten IM adapter path and credential handling, preserve retry behavior for failed desktop notifications, and make Azure/OpenAI provider auth and stop reasons reflect actual runtime state. Constraint: Worktree was detached from an older local main with pending uncommitted fixes Rejected: Merge the detached HEAD directly | would also replay unrelated stale history Rejected: Leave notification dedupe as fire-and-forget | failed sends consumed retry keys Confidence: high Scope-risk: broad Directive: Keep adapter absolute-path matching constrained to configured work roots Tested: git diff --check Not-tested: full quality gate before local main integration
148 lines
5.2 KiB
TypeScript
148 lines
5.2 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'
|
|
import * as fs from 'node:fs'
|
|
import * as os from 'node:os'
|
|
import * as path from 'node:path'
|
|
import { AdapterHttpClient } from '../http-client.js'
|
|
|
|
describe('AdapterHttpClient', () => {
|
|
let client: AdapterHttpClient
|
|
const originalFetch = globalThis.fetch
|
|
|
|
beforeEach(() => {
|
|
client = new AdapterHttpClient('ws://127.0.0.1:3456')
|
|
})
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch
|
|
})
|
|
|
|
it('derives HTTP URL from WS URL', () => {
|
|
expect(client.httpBaseUrl).toBe('http://127.0.0.1:3456')
|
|
|
|
const secure = new AdapterHttpClient('wss://example.com:443')
|
|
expect(secure.httpBaseUrl).toBe('https://example.com:443')
|
|
})
|
|
|
|
it('createSession calls POST /api/sessions', async () => {
|
|
const mockSessionId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve(new Response(JSON.stringify({ sessionId: mockSessionId }), {
|
|
status: 201,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}))
|
|
) as any
|
|
|
|
const sessionId = await client.createSession('/path/to/project')
|
|
expect(sessionId).toBe(mockSessionId)
|
|
|
|
const call = (globalThis.fetch as any).mock.calls[0]
|
|
expect(call[0]).toBe('http://127.0.0.1:3456/api/sessions')
|
|
const body = JSON.parse(call[1].body)
|
|
expect(body.workDir).toBe('/path/to/project')
|
|
})
|
|
|
|
it('listRecentProjects calls GET /api/sessions/recent-projects', async () => {
|
|
const mockProjects = [
|
|
{ projectName: 'my-app', realPath: '/home/user/my-app', sessionCount: 3 },
|
|
]
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve(new Response(JSON.stringify({ projects: mockProjects }), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}))
|
|
) as any
|
|
|
|
const projects = await client.listRecentProjects()
|
|
expect(projects).toHaveLength(1)
|
|
expect(projects[0].projectName).toBe('my-app')
|
|
})
|
|
|
|
it('matchProject accepts an absolute local project path inside an allowed root without recent history', async () => {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'im-root-'))
|
|
const projectDir = fs.mkdtempSync(path.join(rootDir, 'project-'))
|
|
try {
|
|
client = new AdapterHttpClient('ws://127.0.0.1:3456', { allowedProjectRoots: [rootDir] })
|
|
globalThis.fetch = mock(() => {
|
|
throw new Error('recent projects should not be queried for absolute paths')
|
|
}) as any
|
|
|
|
const result = await client.matchProject(projectDir)
|
|
|
|
expect(result.project?.realPath).toBe(fs.realpathSync(projectDir))
|
|
expect(result.project?.projectName).toBe(path.basename(projectDir))
|
|
expect((globalThis.fetch as any).mock.calls).toHaveLength(0)
|
|
} finally {
|
|
fs.rmSync(rootDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('matchProject rejects absolute local project paths outside allowed roots', async () => {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'im-root-'))
|
|
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'im-project-'))
|
|
try {
|
|
client = new AdapterHttpClient('ws://127.0.0.1:3456', { allowedProjectRoots: [rootDir] })
|
|
globalThis.fetch = mock(() => {
|
|
throw new Error('recent projects should not be queried for rejected absolute paths')
|
|
}) as any
|
|
|
|
const result = await client.matchProject(projectDir)
|
|
|
|
expect(result.project).toBeUndefined()
|
|
expect(result.ambiguous).toBeUndefined()
|
|
expect((globalThis.fetch as any).mock.calls).toHaveLength(0)
|
|
} finally {
|
|
fs.rmSync(rootDir, { recursive: true, force: true })
|
|
fs.rmSync(projectDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('createSession throws on server error', async () => {
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve(new Response(JSON.stringify({ error: 'BAD_REQUEST', message: 'workDir required' }), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}))
|
|
) as any
|
|
|
|
expect(client.createSession('')).rejects.toThrow()
|
|
})
|
|
|
|
it('getGitInfo calls GET /api/sessions/:id/git-info', async () => {
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve(new Response(JSON.stringify({
|
|
branch: 'main',
|
|
repoName: 'claude-code-haha',
|
|
workDir: '/repo/claude-code-haha',
|
|
changedFiles: 2,
|
|
}), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}))
|
|
) as any
|
|
|
|
const gitInfo = await client.getGitInfo('session-123')
|
|
expect(gitInfo.repoName).toBe('claude-code-haha')
|
|
expect((globalThis.fetch as any).mock.calls[0][0]).toBe(
|
|
'http://127.0.0.1:3456/api/sessions/session-123/git-info',
|
|
)
|
|
})
|
|
|
|
it('getTasksForSession calls GET /api/tasks/lists/:id', async () => {
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve(new Response(JSON.stringify({
|
|
tasks: [
|
|
{ id: '1', subject: 'Fix bug', status: 'in_progress' },
|
|
{ id: '2', subject: 'Write docs', status: 'pending' },
|
|
],
|
|
}), {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}))
|
|
) as any
|
|
|
|
const tasks = await client.getTasksForSession('session-123')
|
|
expect(tasks).toHaveLength(2)
|
|
expect(tasks[0]?.status).toBe('in_progress')
|
|
expect((globalThis.fetch as any).mock.calls[0][0]).toBe(
|
|
'http://127.0.0.1:3456/api/tasks/lists/session-123',
|
|
)
|
|
})
|
|
})
|