import { afterEach, describe, expect, it, vi } from 'vitest'; import { canonicalizeOpencodeDirectory, createOpencodeClient, decorateOpencodeRequest, } from '@electron/opencode/client'; import { logger } from '@electron/utils/logger'; afterEach(() => { vi.restoreAllMocks(); }); describe('opencode client', () => { it('uses the filesystem real path for Windows directory-scoped requests', () => { const realpath = vi.fn(() => 'D:\\Datas\\ScriptProjects\\ShotGame\\测试顶蘑菇'); const directory = canonicalizeOpencodeDirectory( 'd:\\datas\\scriptprojects\\shotgame\\测试顶蘑菇', realpath, ); expect(realpath).toHaveBeenCalledWith('d:\\datas\\scriptprojects\\shotgame\\测试顶蘑菇'); expect(directory).toBe('D:\\Datas\\ScriptProjects\\ShotGame\\测试顶蘑菇'); }); it('decorates requests with the canonical directory used by runtime sessions', () => { const request = decorateOpencodeRequest( { baseUrl: 'http://127.0.0.1:4096', path: '/session', directory: 'd:\\datas\\scriptprojects\\shotgame\\测试顶蘑菇', }, () => 'D:\\Datas\\ScriptProjects\\ShotGame\\测试顶蘑菇', ); const url = new URL(request.url); expect(url.searchParams.get('directory')).toBe( 'D:\\Datas\\ScriptProjects\\ShotGame\\测试顶蘑菇', ); expect(request.headers['x-opencode-directory']).toBe( encodeURIComponent('D:\\Datas\\ScriptProjects\\ShotGame\\测试顶蘑菇'), ); }); it('adds directory as query and header for scoped requests', () => { const request = decorateOpencodeRequest({ baseUrl: 'http://127.0.0.1:4096', path: '/session', directory: 'D:/work/app', }); const url = new URL(request.url); expect(url.pathname).toBe('/session'); expect(url.searchParams.get('directory')).toBe('D:/work/app'); expect(request.headers['x-opencode-directory']).toBe( encodeURIComponent('D:/work/app'), ); }); it('lists sessions scoped to the selected folder', async () => { const sessions = [{ id: 'ses_123', title: 'Runtime Foundation' }]; const fetchImpl = vi.fn(async () => { return new Response(JSON.stringify(sessions), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.listSessions()).resolves.toEqual(sessions); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('lists skills scoped to the selected folder', async () => { const skills = [{ name: 'repo-audit', description: 'Use when auditing repository structure.', location: 'D:/work/app/.opencode/skills/repo-audit/SKILL.md', content: '# Repo audit', }]; const fetchImpl = vi.fn(async () => { return new Response(JSON.stringify(skills), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.listSkills()).resolves.toEqual(skills); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/skill?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('posts a text message to a session scoped to the selected folder', async () => { const fetchImpl = vi.fn(async () => { return new Response(JSON.stringify({ id: 'msg_1', role: 'user' }), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.sendSessionMessage('ses_123', { text: 'Ship it' })).resolves.toEqual({ id: 'msg_1', role: 'user', }); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123/message?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', body: JSON.stringify({ parts: [ { type: 'text', text: 'Ship it' }, ], }), headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('starts an async prompt for a session scoped to the selected folder', async () => { const logInfo = vi.spyOn(logger, 'info').mockImplementation(() => undefined); const fetchImpl = vi.fn(async () => new Response(null, { status: 204 })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.promptSessionAsync('ses_123', { text: 'Ship it', system: '当前用户叫小明', agent: 'game-design' })).resolves.toBeUndefined(); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123/prompt_async?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', body: JSON.stringify({ parts: [ { type: 'text', text: 'Ship it' }, ], system: '当前用户叫小明', agent: 'game-design', }), headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); expect(logInfo).toHaveBeenCalledWith( '[opencode-client] Forwarding request to runtime', { method: 'POST', pathname: '/session/ses_123/prompt_async', }, ); }); it('includes an explicit model when starting an async prompt', async () => { const fetchImpl = vi.fn(async () => new Response(null, { status: 204 })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.promptSessionAsync('ses_123', { text: 'Ship it', model: { providerID: 'niancode-user-models', modelID: 'deepseek-chat', }, })).resolves.toBeUndefined(); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123/prompt_async?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', body: JSON.stringify({ parts: [ { type: 'text', text: 'Ship it' }, ], model: { providerID: 'niancode-user-models', modelID: 'deepseek-chat', }, }), }), ); }); it('forwards uploaded image files as opencode file parts when starting an async prompt', async () => { const logInfo = vi.spyOn(logger, 'info').mockImplementation(() => undefined); const fetchImpl = vi.fn(async () => new Response(null, { status: 204 })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.promptSessionAsync('ses_123', { text: 'Describe this image', files: [{ type: 'file', filename: 'wireframe.png', mime: 'image/png', url: 'data:image/png;base64,AAA', }], })).resolves.toBeUndefined(); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123/prompt_async?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', body: JSON.stringify({ parts: [ { type: 'text', text: 'Describe this image' }, { type: 'file', filename: 'wireframe.png', mime: 'image/png', url: 'data:image/png;base64,AAA', }, ], }), }), ); expect(logInfo).toHaveBeenCalledWith( '[opencode-client] Sending prompt_async payload', { sessionID: 'ses_123', model: null, textLength: 19, partCount: 2, filePartCount: 1, fileMimes: ['image/png'], }, ); expect(JSON.stringify(logInfo.mock.calls)).not.toContain('wireframe.png'); expect(JSON.stringify(logInfo.mock.calls)).not.toContain('data:image/png;base64,AAA'); }); it('renames a session inside the selected folder', async () => { const updatedSession = { id: 'ses_123', title: 'Polish context rail' }; const fetchImpl = vi.fn(async () => new Response(JSON.stringify(updatedSession), { headers: { 'Content-Type': 'application/json' }, })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.updateSession('ses_123', { title: 'Polish context rail' })).resolves.toEqual(updatedSession); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ title: 'Polish context rail' }), }), ); }); it('deletes a session inside the selected folder', async () => { const fetchImpl = vi.fn(async () => new Response(null, { status: 204 })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.deleteSession('ses_123')).resolves.toBeUndefined(); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'DELETE', }), ); }); it('aborts a running session inside the selected folder', async () => { const fetchImpl = vi.fn(async () => new Response(null, { status: 204 })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.abortSession('ses_123')).resolves.toBeUndefined(); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123/abort?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', }), ); }); it('loads a session diff inside the selected folder', async () => { const diff = [ { path: 'src/pages/Chat/OpencodeChatPanel.tsx', status: 'modified', additions: 3, deletions: 1, before: 'const status = "idle";', after: 'const status = "busy";', }, ]; const fetchImpl = vi.fn(async () => new Response(JSON.stringify(diff), { headers: { 'Content-Type': 'application/json' }, })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.getSessionDiff('ses_123')).resolves.toEqual(diff); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123/diff?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('reverts a session message inside the selected folder', async () => { const fetchImpl = vi.fn(async () => new Response(JSON.stringify(true), { headers: { 'Content-Type': 'application/json' }, })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.revertSessionMessage('ses_123', { messageID: 'msg_456' })).resolves.toBe(true); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123/revert?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', body: JSON.stringify({ messageID: 'msg_456' }), }), ); }); it('restores reverted session changes inside the selected folder', async () => { const fetchImpl = vi.fn(async () => new Response(JSON.stringify(true), { headers: { 'Content-Type': 'application/json' }, })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.unrevertSession('ses_123')).resolves.toBe(true); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123/unrevert?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', }), ); }); it('loads session todos inside the selected folder', async () => { const todos = [ { id: 'todo_1', content: 'Inspect the store', status: 'in_progress', priority: 'high' }, ]; const fetchImpl = vi.fn(async () => new Response(JSON.stringify(todos), { headers: { 'Content-Type': 'application/json' }, })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.getSessionTodos('ses_123')).resolves.toEqual(todos); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/ses_123/todo?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('loads session statuses for the selected folder', async () => { const statuses = { ses_123: { type: 'busy' }, ses_456: { type: 'retry', attempt: 2, message: 'Rate limited', next: 1747051200 }, }; const fetchImpl = vi.fn(async () => { return new Response(JSON.stringify(statuses), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.getSessionStatuses()).resolves.toEqual(statuses); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/session/status?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('lists pending questions for the selected folder', async () => { const questions = [ { id: 'que_123', sessionID: 'ses_123', questions: [ { header: 'Need a decision', question: 'How should I proceed?', options: [ { label: 'Apply patch', description: 'Make the change now.' }, { label: 'Skip patch', description: 'Leave the file unchanged.' }, ], custom: true, }, ], }, ]; const fetchImpl = vi.fn(async () => { return new Response(JSON.stringify(questions), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.listQuestions()).resolves.toEqual(questions); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/question?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('replies to a pending question inside the selected folder', async () => { const fetchImpl = vi.fn(async () => new Response(null, { status: 204 })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.replyQuestion('que_123', [['Apply patch']])).resolves.toBeUndefined(); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/question/que_123/reply?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', body: JSON.stringify({ answers: [['Apply patch']] }), headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('rejects a pending question inside the selected folder', async () => { const fetchImpl = vi.fn(async () => new Response(null, { status: 204 })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.rejectQuestion('que_123')).resolves.toBeUndefined(); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/question/que_123/reject?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('lists pending permissions for the selected folder', async () => { const permissions = [ { id: 'per_123', sessionID: 'ses_123', title: 'Run command', metadata: { command: 'npm test' }, }, ]; const fetchImpl = vi.fn(async () => { return new Response(JSON.stringify(permissions), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.listPermissions()).resolves.toEqual(permissions); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/permission?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('replies to a pending permission inside the selected folder', async () => { const fetchImpl = vi.fn(async () => new Response(null, { status: 204 })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.replyPermission('per_123', 'once', 'Looks safe.')).resolves.toBeUndefined(); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/permission/per_123/reply?directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ method: 'POST', body: JSON.stringify({ reply: 'once', message: 'Looks safe.' }), headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('finds files by fuzzy query inside the selected folder', async () => { const files = ['src/pages/Chat/OpencodeChatPanel.tsx', 'src/stores/opencode.ts']; const fetchImpl = vi.fn(async () => { return new Response(JSON.stringify(files), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.findFiles('opencode', { limit: 20 })).resolves.toEqual(files); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/find/file?query=opencode&limit=20&directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('searches project content by pattern inside the selected folder', async () => { const matches = [ { path: 'src/stores/opencode.ts', line_number: 128, lines: { text: 'const fallbackText = typeof input.text === \'string\'' }, submatches: [{ match: { text: 'input.text' }, start: 33, end: 43 }], }, ]; const fetchImpl = vi.fn(async () => { return new Response(JSON.stringify(matches), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.searchText('input.text')).resolves.toEqual(matches); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/find?pattern=input.text&directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('reads file content scoped to the selected folder', async () => { const file = { path: 'src/pages/Chat/OpencodeChatPanel.tsx', content: 'export function Chat() {}', }; const fetchImpl = vi.fn(async () => { return new Response(JSON.stringify(file), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await expect(client.getFileContent('src/pages/Chat/OpencodeChatPanel.tsx')).resolves.toEqual(file); expect(fetchImpl).toHaveBeenCalledWith( 'http://127.0.0.1:4096/file/content?path=src%2Fpages%2FChat%2FOpencodeChatPanel.tsx&directory=D%3A%2Fwork%2Fapp', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', 'x-opencode-directory': encodeURIComponent('D:/work/app'), }), }), ); }); it('forwards typed command and session operations without logging payload bodies', async () => { const logInfo = vi.spyOn(logger, 'info').mockImplementation(() => undefined); const fetchImpl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => { const pathname = new URL(String(url)).pathname; if (pathname === '/command' && init?.method !== 'POST') { return new Response(JSON.stringify([{ name: 'review', template: 'SECRET TEMPLATE BODY', }]), { headers: { 'Content-Type': 'application/json' }, }); } return new Response(JSON.stringify({ id: 'ses_result' }), { headers: { 'Content-Type': 'application/json' }, }); }); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/work/app', fetchImpl, }); await client.listCommands(); await client.forkSession('ses_1', { messageID: 'msg_1' }); await client.shareSession('ses_1'); await client.unshareSession('ses_1'); await client.summarizeSession('ses_1', { providerID: 'niancode-user-models', modelID: 'qwen3.7-plus', }); await client.executeSessionCommand('ses_1', { command: 'review', arguments: ' PRIVATE ARGUMENTS ', agent: 'game-development', model: 'niancode-user-models/qwen3.7-plus', variant: 'high', parts: [ { type: 'text', text: 'Read PRIVATE D:/repo/spec.md context', synthetic: true, }, { type: 'file', mime: 'image/webp', filename: 'PRIVATE-screen.webp', url: 'data:image/webp;base64,UVVFTi1QUklWQVRF', }, ], }); const requests = fetchImpl.mock.calls.map(([url, init]) => ({ path: new URL(String(url)).pathname, method: init?.method ?? 'GET', body: init?.body, })); expect(requests).toEqual([ expect.objectContaining({ path: '/command', method: 'GET' }), expect.objectContaining({ path: '/session/ses_1/fork', method: 'POST', body: JSON.stringify({ messageID: 'msg_1' }), }), expect.objectContaining({ path: '/session/ses_1/share', method: 'POST' }), expect.objectContaining({ path: '/session/ses_1/share', method: 'DELETE' }), expect.objectContaining({ path: '/session/ses_1/summarize', method: 'POST', body: JSON.stringify({ providerID: 'niancode-user-models', modelID: 'qwen3.7-plus', }), }), expect.objectContaining({ path: '/session/ses_1/command', method: 'POST', body: JSON.stringify({ command: 'review', arguments: ' PRIVATE ARGUMENTS ', agent: 'game-development', model: 'niancode-user-models/qwen3.7-plus', variant: 'high', parts: [ { type: 'text', text: 'Read PRIVATE D:/repo/spec.md context', synthetic: true, }, { type: 'file', mime: 'image/webp', filename: 'PRIVATE-screen.webp', url: 'data:image/webp;base64,UVVFTi1QUklWQVRF', }, ], }), }), ]); const logged = JSON.stringify(logInfo.mock.calls); for (const secret of [ 'SECRET TEMPLATE BODY', 'PRIVATE ARGUMENTS', 'D:/repo/spec.md', 'PRIVATE-screen.webp', 'UVVFTi1QUklWQVRF', 'D:/work/app', 'D%3A%2Fwork%2Fapp', 'directory=', ]) { expect(logged).not.toContain(secret); } expect(logged).toContain('/session/ses_1/command'); }); it('logs only request methods and pathnames for query and path inputs', async () => { const logInfo = vi.spyOn(logger, 'info').mockImplementation(() => undefined); const fetchImpl = vi.fn(async () => new Response(JSON.stringify([]), { headers: { 'Content-Type': 'application/json' }, })); const client = createOpencodeClient({ baseUrl: 'http://127.0.0.1:4096', directory: 'D:/PRIVATE/project', fetchImpl, }); await client.findFiles('PRIVATE query'); await client.searchText('PRIVATE pattern'); await client.getFileContent('PRIVATE/path/context.ts'); expect(logInfo).toHaveBeenCalledWith( '[opencode-client] Forwarding request to runtime', { method: 'GET', pathname: '/find/file' }, ); expect(logInfo).toHaveBeenCalledWith( '[opencode-client] Forwarding request to runtime', { method: 'GET', pathname: '/find' }, ); expect(logInfo).toHaveBeenCalledWith( '[opencode-client] Forwarding request to runtime', { method: 'GET', pathname: '/file/content' }, ); const logged = JSON.stringify(logInfo.mock.calls); for (const secret of [ 'PRIVATE query', 'PRIVATE+query', 'PRIVATE pattern', 'PRIVATE+pattern', 'PRIVATE/path/context.ts', 'PRIVATE%2Fpath%2Fcontext.ts', 'D:/PRIVATE/project', 'D%3A%2FPRIVATE%2Fproject', 'directory=', ]) { expect(logged).not.toContain(secret); } }); });