Files
makelore/tests/unit/opencode-client.test.ts
2026-07-29 17:22:35 +08:00

826 lines
27 KiB
TypeScript

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);
}
});
});