1829 lines
66 KiB
TypeScript
1829 lines
66 KiB
TypeScript
import { EventEmitter } from 'node:events';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const buildProviderAwareCliEnvMock = vi.fn();
|
|
const resolveBinaryMock = vi.fn();
|
|
const clearBinaryCacheMock = vi.fn();
|
|
const execCliMock = vi.fn();
|
|
const spawnCliMock = vi.fn();
|
|
const resolveInteractiveShellEnvMock = vi.fn();
|
|
|
|
function createSpawnProcess(stdoutPayload: unknown, exitCode = 0): {
|
|
child: {
|
|
stdout: EventEmitter;
|
|
stderr: EventEmitter;
|
|
stdin: {
|
|
write: ReturnType<typeof vi.fn>;
|
|
end: ReturnType<typeof vi.fn>;
|
|
once: EventEmitter['once'];
|
|
};
|
|
once: EventEmitter['once'];
|
|
};
|
|
stdinWrite: ReturnType<typeof vi.fn>;
|
|
} {
|
|
const processEvents = new EventEmitter();
|
|
const stdinEvents = new EventEmitter();
|
|
const stdout = new EventEmitter();
|
|
const stderr = new EventEmitter();
|
|
const stdinWrite = vi.fn();
|
|
const stdinEnd = vi.fn(() => {
|
|
queueMicrotask(() => {
|
|
stdout.emit('data', Buffer.from(JSON.stringify(stdoutPayload)));
|
|
processEvents.emit('close', exitCode);
|
|
});
|
|
});
|
|
|
|
return {
|
|
child: {
|
|
stdout,
|
|
stderr,
|
|
stdin: {
|
|
write: stdinWrite,
|
|
end: stdinEnd,
|
|
once: stdinEvents.once.bind(stdinEvents),
|
|
},
|
|
once: processEvents.once.bind(processEvents),
|
|
},
|
|
stdinWrite,
|
|
};
|
|
}
|
|
|
|
vi.mock('@main/services/runtime/providerAwareCliEnv', () => ({
|
|
buildProviderAwareCliEnv: (...args: unknown[]) => buildProviderAwareCliEnvMock(...args),
|
|
}));
|
|
|
|
vi.mock('@main/services/team/ClaudeBinaryResolver', () => ({
|
|
ClaudeBinaryResolver: {
|
|
resolve: () => resolveBinaryMock(),
|
|
clearCache: () => clearBinaryCacheMock(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('@main/utils/childProcess', () => ({
|
|
execCli: (...args: unknown[]) => execCliMock(...args),
|
|
spawnCli: (...args: unknown[]) => spawnCliMock(...args),
|
|
killProcessTree: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@main/utils/shellEnv', () => ({
|
|
resolveInteractiveShellEnvBestEffort: () => resolveInteractiveShellEnvMock(),
|
|
}));
|
|
|
|
vi.mock(
|
|
'../../../../src/features/runtime-provider-management/main/infrastructure/openCodeWindowsNodeModulesJunction',
|
|
() => ({
|
|
isOpenCodeNodeModulesSymlinkError: vi.fn(),
|
|
extractProfileIdFromSymlinkError: vi.fn(),
|
|
ensureOpenCodeProfileNodeModulesJunction: vi.fn(),
|
|
})
|
|
);
|
|
|
|
import { AgentTeamsRuntimeProviderManagementCliClient } from '../../../../src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient';
|
|
import {
|
|
ensureOpenCodeProfileNodeModulesJunction as ensureOpenCodeProfileNodeModulesJunctionMock,
|
|
extractProfileIdFromSymlinkError as extractProfileIdFromSymlinkErrorMock,
|
|
isOpenCodeNodeModulesSymlinkError as isOpenCodeNodeModulesSymlinkErrorMock,
|
|
} from '../../../../src/features/runtime-provider-management/main/infrastructure/openCodeWindowsNodeModulesJunction';
|
|
|
|
describe('AgentTeamsRuntimeProviderManagementCliClient', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
resolveBinaryMock.mockResolvedValue('/repo/cli-dev');
|
|
resolveInteractiveShellEnvMock.mockResolvedValue({ PATH: '/Users/test/.bun/bin:/usr/bin' });
|
|
buildProviderAwareCliEnvMock.mockResolvedValue({
|
|
env: { PATH: '/Users/test/.bun/bin:/usr/bin' },
|
|
connectionIssues: {},
|
|
providerArgs: [],
|
|
});
|
|
});
|
|
|
|
it('returns stderr details for failed model tests instead of hiding them behind the command', async () => {
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers test-model');
|
|
Object.assign(error, {
|
|
stderr: './cli-dev: line 47: exec: bun: not found\n',
|
|
stdout: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.testModel({
|
|
runtimeId: 'opencode',
|
|
providerId: 'opencode',
|
|
modelId: 'opencode/nemotron-3-super-free',
|
|
});
|
|
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings could not read the runtime response.'
|
|
);
|
|
expect(response.error?.message).toContain('stderr preview:');
|
|
expect(response.error?.message).toContain('./cli-dev: line 47: exec: bun: not found');
|
|
expect(response.error?.diagnostics?.command).toContain('runtime providers test-model');
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe(
|
|
'./cli-dev: line 47: exec: bun: not found'
|
|
);
|
|
});
|
|
|
|
it('redacts secrets from generic command stderr details', async () => {
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(error, {
|
|
stderr: 'Provider failed with api_key: sk-secret-value-123456\n',
|
|
stdout: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toContain('Provider failed with api_key: ...redacted');
|
|
expect(response.error?.message).not.toContain('sk-secret-value-123456');
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe(
|
|
'Provider failed with api_key: ...redacted'
|
|
);
|
|
expect(response.error?.diagnostics?.command).toBe(
|
|
'/repo/cli-dev runtime providers view --runtime opencode --json --compact'
|
|
);
|
|
});
|
|
|
|
it('strips terminal formatting and redacts bearer tokens from command previews', async () => {
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers models');
|
|
Object.assign(error, {
|
|
stderr:
|
|
'\u001B]8;;https://logs.example/secret\u0007\u001B[31mAuthorization: Bearer live-token-123456789\u001B[0m\u001B]8;;\u0007\n',
|
|
stdout: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadModels({
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
});
|
|
|
|
expect(response.error?.message).toContain('Authorization: Bearer ...redacted');
|
|
expect(response.error?.message).not.toContain('live-token-123456789');
|
|
expect(response.error?.message).not.toContain('logs.example/secret');
|
|
expect(response.error?.message).not.toContain('[31m');
|
|
expect(response.error?.message).not.toContain(']8;;');
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe(
|
|
'Authorization: Bearer ...redacted'
|
|
);
|
|
});
|
|
|
|
it('redacts non-OpenAI provider keys and generic token labels from diagnostics', async () => {
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(error, {
|
|
stderr:
|
|
'Google key=AIzaSyD-test-secret-value-123456789 and token=provider-token-123456789 and OPENAI_API_KEY=plain_provider_secret_123456 and PROVIDER_TOKEN=provider_token_value_123456\n',
|
|
stdout: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toContain('key=...redacted');
|
|
expect(response.error?.message).toContain('token=...redacted');
|
|
expect(response.error?.message).toContain('OPENAI_API_KEY=...redacted');
|
|
expect(response.error?.message).toContain('PROVIDER_TOKEN=...redacted');
|
|
expect(response.error?.message).not.toContain('AIzaSyD-test-secret-value-123456789');
|
|
expect(response.error?.message).not.toContain('provider-token-123456789');
|
|
expect(response.error?.message).not.toContain('plain_provider_secret_123456');
|
|
expect(response.error?.message).not.toContain('provider_token_value_123456');
|
|
expect(response.error?.diagnostics?.stderrPreview).toContain('key=...redacted');
|
|
expect(response.error?.diagnostics?.stderrPreview).toContain('token=...redacted');
|
|
});
|
|
|
|
it('returns structured diagnostics for empty non-JSON command output', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: '',
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toContain('No stdout or stderr was captured');
|
|
expect(response.error?.diagnostics?.command).toBe(
|
|
'/repo/cli-dev runtime providers view --runtime opencode --json --compact'
|
|
);
|
|
expect(response.error?.diagnostics?.stdoutPreview).toBeNull();
|
|
expect(response.error?.diagnostics?.stderrPreview).toBeNull();
|
|
});
|
|
|
|
it('keeps stderr diagnostics when a zero-exit command prints malformed stdout', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: 'not json',
|
|
stderr: 'warning: api_key: sk-secret-value-123456\n',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toContain('stderr preview:');
|
|
expect(response.error?.message).toContain('warning: api_key: ...redacted');
|
|
expect(response.error?.message).not.toContain('sk-secret-value-123456');
|
|
expect(response.error?.diagnostics?.stdoutPreview).toBe('not json');
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe('warning: api_key: ...redacted');
|
|
});
|
|
|
|
it('returns structured diagnostics when the runtime binary cannot be resolved', async () => {
|
|
resolveBinaryMock.mockResolvedValue(null);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
projectPath: '/Users/test/project',
|
|
});
|
|
|
|
expect(response.error?.code).toBe('runtime-missing');
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings could not find the Agent Teams runtime binary.'
|
|
);
|
|
expect(response.error?.diagnostics?.summary).toBe(
|
|
'OpenCode provider settings could not find the Agent Teams runtime binary.'
|
|
);
|
|
expect(response.error?.diagnostics?.binaryPath).toBeNull();
|
|
expect(response.error?.diagnostics?.command).toBeNull();
|
|
expect(response.error?.diagnostics?.projectPath).toBe('/Users/test/project');
|
|
expect(response.error?.diagnostics?.hints).toContain(
|
|
'The expected binary is the Agent Teams runtime/orchestrator CLI, not the OpenCode CLI.'
|
|
);
|
|
expect(buildProviderAwareCliEnvMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns structured diagnostics for process errors without stdout or stderr', async () => {
|
|
execCliMock.mockRejectedValue(
|
|
new Error('spawn EACCES /repo/cli-dev with api_key: sk-secret-value-123456')
|
|
);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
projectPath: '/Users/test/project',
|
|
});
|
|
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings could not run the runtime command.'
|
|
);
|
|
expect(response.error?.message).toContain(
|
|
'Error:\nspawn EACCES /repo/cli-dev with api_key: ...redacted'
|
|
);
|
|
expect(response.error?.message).not.toContain('sk-secret-value-123456');
|
|
expect(response.error?.diagnostics?.command).toBe(
|
|
'/repo/cli-dev runtime providers view --runtime opencode --json --compact --project-path /Users/test/project'
|
|
);
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe(
|
|
'spawn EACCES /repo/cli-dev with api_key: ...redacted'
|
|
);
|
|
});
|
|
|
|
it('returns structured diagnostics when provider directory loading times out', async () => {
|
|
const error = new Error(
|
|
'Command timed out after 45000ms: /repo/cli-dev runtime providers directory --runtime opencode --json'
|
|
);
|
|
Object.assign(error, {
|
|
stdout: 'inventory started\n',
|
|
stderr: 'OpenCode provider key=sk-secret-value-123456 still probing\n',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadProviderDirectory({
|
|
runtimeId: 'opencode',
|
|
projectPath: '/Users/test/project',
|
|
query: null,
|
|
filter: 'all',
|
|
limit: 50,
|
|
cursor: null,
|
|
refresh: false,
|
|
});
|
|
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings timed out while waiting for the Agent Teams runtime.'
|
|
);
|
|
expect(response.error?.message).toContain(
|
|
'This is not enough evidence to conclude that OpenCode auth is missing.'
|
|
);
|
|
expect(response.error?.message).toContain('OpenCode provider key=...redacted');
|
|
expect(response.error?.message).not.toContain('sk-secret-value-123456');
|
|
expect(response.error?.diagnostics?.summary).toBe(
|
|
'OpenCode provider settings timed out while waiting for the Agent Teams runtime.'
|
|
);
|
|
expect(response.error?.diagnostics?.command).toBe(
|
|
'/repo/cli-dev runtime providers directory --runtime opencode --json --project-path /Users/test/project --filter all --limit 50'
|
|
);
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe(
|
|
'OpenCode provider key=...redacted still probing'
|
|
);
|
|
expect(response.error?.diagnostics?.stdoutPreview).toBe('inventory started');
|
|
expect(response.error?.diagnostics?.hints).toContain(
|
|
'If the runtime binary is stale, update Agent Teams so the runtime can return a degraded OpenCode diagnostic instead of timing out.'
|
|
);
|
|
});
|
|
|
|
it('preserves runtime-side degraded JSON errors from rejected command output', async () => {
|
|
const error = new Error('Command failed after runtime returned degraded JSON');
|
|
Object.assign(error, {
|
|
stdout: '',
|
|
stderr: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: {
|
|
code: 'runtime-unhealthy',
|
|
message:
|
|
'OpenCode inventory probe timed out after 12000ms during opencode providers list',
|
|
recoverable: true,
|
|
diagnostics: {
|
|
summary: 'OpenCode inventory probe timed out',
|
|
likelyCause: 'OpenCode providers list did not finish before the runtime budget.',
|
|
command:
|
|
'/repo/cli-dev runtime providers view --runtime opencode --json --compact',
|
|
stderrPreview: 'provider api_key: sk-secret-value-123456',
|
|
hints: ['Check OpenCode CLI startup and local OpenCode plugins.'],
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toBe(
|
|
'OpenCode inventory probe timed out after 12000ms during opencode providers list'
|
|
);
|
|
expect(response.error?.diagnostics?.summary).toBe('OpenCode inventory probe timed out');
|
|
expect(response.error?.diagnostics?.likelyCause).toBe(
|
|
'OpenCode providers list did not finish before the runtime budget.'
|
|
);
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe(
|
|
'provider api_key: ...redacted'
|
|
);
|
|
expect(response.error?.diagnostics?.stderrPreview).not.toContain('sk-secret-value-123456');
|
|
expect(response.error?.diagnostics?.hints).toContain(
|
|
'Check OpenCode CLI startup and local OpenCode plugins.'
|
|
);
|
|
});
|
|
|
|
it('preserves degraded JSON from stderr when stdout contains noisy logs', async () => {
|
|
const error = new Error('Command failed after mixed runtime output');
|
|
Object.assign(error, {
|
|
stdout: 'runtime preflight log {not json}\n',
|
|
stderr: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: {
|
|
code: 'runtime-unhealthy',
|
|
message:
|
|
'OpenCode inventory probe timed out after 12000ms during opencode agent list',
|
|
recoverable: true,
|
|
diagnostics: {
|
|
summary: 'OpenCode inventory probe timed out',
|
|
likelyCause: 'OpenCode agent inventory did not finish before the runtime budget.',
|
|
stderrPreview: 'agent token=sk-secret-value-123456',
|
|
hints: ['Check OpenCode agent listing and local OpenCode plugins.'],
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toBe(
|
|
'OpenCode inventory probe timed out after 12000ms during opencode agent list'
|
|
);
|
|
expect(response.error?.diagnostics?.likelyCause).toBe(
|
|
'OpenCode agent inventory did not finish before the runtime budget.'
|
|
);
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe(
|
|
'agent token=...redacted'
|
|
);
|
|
expect(JSON.stringify(response.error?.diagnostics)).not.toContain('sk-secret-value-123456');
|
|
});
|
|
|
|
it('preserves degraded JSON printed to stdout before a desktop timeout', async () => {
|
|
const error = new Error(
|
|
'Command timed out after 45000ms: /repo/cli-dev runtime providers view --runtime opencode --json --compact'
|
|
);
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: {
|
|
code: 'runtime-unhealthy',
|
|
message:
|
|
'OpenCode inventory probe timed out after 12000ms during opencode models --verbose',
|
|
recoverable: true,
|
|
diagnostics: {
|
|
summary: 'OpenCode inventory probe timed out',
|
|
likelyCause: 'OpenCode model inventory did not finish before the runtime budget.',
|
|
command:
|
|
'/repo/cli-dev runtime providers view --runtime opencode --json --compact',
|
|
stdoutPreview: 'model api_key: sk-secret-value-123456',
|
|
hints: ['Check OpenCode model listing and local OpenCode plugins.'],
|
|
},
|
|
},
|
|
}),
|
|
stderr: 'outer timeout after runtime json\n',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toBe(
|
|
'OpenCode inventory probe timed out after 12000ms during opencode models --verbose'
|
|
);
|
|
expect(response.error?.diagnostics?.summary).toBe('OpenCode inventory probe timed out');
|
|
expect(response.error?.diagnostics?.likelyCause).toBe(
|
|
'OpenCode model inventory did not finish before the runtime budget.'
|
|
);
|
|
expect(response.error?.diagnostics?.stdoutPreview).toBe(
|
|
'model api_key: ...redacted'
|
|
);
|
|
expect(JSON.stringify(response.error?.diagnostics)).not.toContain('sk-secret-value-123456');
|
|
});
|
|
|
|
it('parses the runtime JSON response after noisy brace logs', async () => {
|
|
const validResponse = {
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.15.6',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
};
|
|
execCliMock.mockResolvedValue({
|
|
stdout: `debug {"noise":true}\n${JSON.stringify(validResponse)}\n`,
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error).toBeUndefined();
|
|
expect(response.view?.runtime.state).toBe('ready');
|
|
expect(response.view?.runtime.cliPath).toBe('/opt/homebrew/bin/opencode');
|
|
});
|
|
|
|
it('accepts successful runtime responses that include an explicit null error field', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: null,
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.15.6',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error).toBeUndefined();
|
|
expect(response.view?.runtime.state).toBe('ready');
|
|
});
|
|
|
|
it('skips contract-looking noise that does not include a response payload', async () => {
|
|
const validResponse = {
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.15.6',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
};
|
|
execCliMock.mockResolvedValue({
|
|
stdout: [
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
debug: 'preflight',
|
|
}),
|
|
JSON.stringify(validResponse),
|
|
].join('\n'),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error).toBeUndefined();
|
|
expect(response.view?.runtime.state).toBe('ready');
|
|
expect(response.view?.title).toBe('OpenCode');
|
|
});
|
|
|
|
it('does not treat JSON logs without a response payload as a successful runtime response', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
debug: 'preflight',
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings could not read the runtime response.'
|
|
);
|
|
expect(response.error?.diagnostics?.stdoutPreview).toContain('"debug":"preflight"');
|
|
expect(response.view).toBeUndefined();
|
|
});
|
|
|
|
it('does not treat malformed view payloads as successful runtime responses', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings could not read the runtime response.'
|
|
);
|
|
expect(response.error?.diagnostics?.stdoutPreview).toContain('"title":"OpenCode"');
|
|
expect(response.view).toBeUndefined();
|
|
});
|
|
|
|
it('does not pass malformed provider entries to the renderer', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.15.6',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [
|
|
{
|
|
providerId: 'openrouter',
|
|
displayName: 'OpenRouter',
|
|
state: 'connected',
|
|
ownership: ['managed'],
|
|
recommended: true,
|
|
modelCount: 4,
|
|
defaultModelId: null,
|
|
authMethods: ['api'],
|
|
detail: null,
|
|
},
|
|
],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings could not read the runtime response.'
|
|
);
|
|
expect(response.view).toBeUndefined();
|
|
});
|
|
|
|
it('parses JSON error responses from stdout when the CLI exits non-zero', async () => {
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers test-model');
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: {
|
|
code: 'auth-required',
|
|
message: 'Provider opencode must be connected before testing a model',
|
|
recoverable: true,
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.testModel({
|
|
runtimeId: 'opencode',
|
|
providerId: 'opencode',
|
|
modelId: 'opencode/nemotron-3-super-free',
|
|
});
|
|
|
|
expect(response.error?.code).toBe('auth-required');
|
|
expect(response.error?.message).toBe(
|
|
'Provider opencode must be connected before testing a model'
|
|
);
|
|
});
|
|
|
|
it('redacts secrets from structured JSON error responses returned by the runtime', async () => {
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: {
|
|
code: 'auth-failed',
|
|
message: 'Provider failed with api_key: sk-secret-value-123456',
|
|
recoverable: true,
|
|
diagnostics: {
|
|
summary: 'Auth failed for sk-secret-value-123456',
|
|
likelyCause: 'Authorization: Bearer live-token-123456789 was rejected',
|
|
binaryPath: '/repo/cli-dev',
|
|
command: '/repo/cli-dev runtime providers view',
|
|
projectPath: null,
|
|
exitCode: 1,
|
|
stderrPreview: 'api_key: sk-secret-value-123456',
|
|
stdoutPreview: 'Authorization: Bearer live-token-123456789',
|
|
hints: ['Remove sk-secret-value-123456 from config output.'],
|
|
},
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
const serialized = JSON.stringify(response);
|
|
|
|
expect(response.error?.message).toContain('api_key: ...redacted');
|
|
expect(response.error?.diagnostics?.summary).toBe('Auth failed for sk-...redacted');
|
|
expect(response.error?.diagnostics?.errorCode).toBe('auth-failed');
|
|
expect(response.error?.diagnostics?.likelyCause).toBe(
|
|
'Authorization: Bearer ...redacted was rejected'
|
|
);
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe('api_key: ...redacted');
|
|
expect(response.error?.diagnostics?.stdoutPreview).toBe(
|
|
'Authorization: Bearer ...redacted'
|
|
);
|
|
expect(response.error?.diagnostics?.hints[0]).toBe(
|
|
'Remove sk-...redacted from config output.'
|
|
);
|
|
expect(serialized).not.toContain('sk-secret-value-123456');
|
|
expect(serialized).not.toContain('live-token-123456789');
|
|
});
|
|
|
|
it('redacts secrets from successful runtime diagnostics before they reach the renderer', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.15.6',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [
|
|
{
|
|
providerId: 'openrouter',
|
|
displayName: 'OpenRouter',
|
|
state: 'connected',
|
|
ownership: ['managed'],
|
|
recommended: true,
|
|
modelCount: 4,
|
|
defaultModelId: null,
|
|
authMethods: ['api'],
|
|
actions: [],
|
|
detail: 'Connected with api_key: sk-secret-value-123456',
|
|
},
|
|
],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [
|
|
'Authorization: Bearer live-token-123456789',
|
|
'\u001B[31mapi_key: sk-secret-value-123456\u001B[0m',
|
|
],
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
const serialized = JSON.stringify(response);
|
|
|
|
expect(response.view?.diagnostics).toEqual([
|
|
'Authorization: Bearer ...redacted',
|
|
'api_key: ...redacted',
|
|
]);
|
|
expect(response.view?.providers[0]?.detail).toBe('Connected with api_key: ...redacted');
|
|
expect(serialized).not.toContain('sk-secret-value-123456');
|
|
expect(serialized).not.toContain('live-token-123456789');
|
|
expect(serialized).not.toContain('[31m');
|
|
});
|
|
|
|
it('keeps structured runtime errors when optional diagnostic fields are malformed', async () => {
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: {
|
|
code: 'runtime-unhealthy',
|
|
message: 'Runtime returned malformed diagnostics',
|
|
recoverable: true,
|
|
diagnostics: {
|
|
summary: 'Runtime returned malformed diagnostics',
|
|
likelyCause: null,
|
|
binaryPath: '/repo/cli-dev',
|
|
command: '/repo/cli-dev runtime providers view',
|
|
projectPath: null,
|
|
exitCode: '1',
|
|
stderrPreview: null,
|
|
stdoutPreview: null,
|
|
},
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toBe('Runtime returned malformed diagnostics');
|
|
expect(response.error?.diagnostics?.summary).toBe('Runtime returned malformed diagnostics');
|
|
expect(response.error?.diagnostics?.exitCode).toBeNull();
|
|
expect(response.error?.diagnostics?.hints).toEqual([]);
|
|
});
|
|
|
|
it('normalizes malformed structured runtime error objects instead of leaking them to the renderer', async () => {
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: {
|
|
code: 'not-a-real-code',
|
|
message: 123,
|
|
recoverable: 'yes',
|
|
diagnostics: {
|
|
summary: 'api_key: sk-secret-value-123456',
|
|
},
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.code).toBe('runtime-unhealthy');
|
|
expect(response.error?.message).toBe('Runtime provider management command failed');
|
|
expect(response.error?.diagnostics?.summary).toBe('api_key: ...redacted');
|
|
expect(JSON.stringify(response)).not.toContain('sk-secret-value-123456');
|
|
});
|
|
|
|
it('adds actionable diagnostics for OpenCode managed profile node_modules symlink failures', async () => {
|
|
const runtimeMessage = [
|
|
'Runtime provider management command failed unexpectedly:',
|
|
"EPERM: operation not permitted, symlink 'C:\\Users\\Swarog\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
|
|
"-> 'C:\\Users\\Swarog\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
|
|
].join(' ');
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: {
|
|
code: 'runtime-unhealthy',
|
|
message: runtimeMessage,
|
|
recoverable: true,
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error?.message).toBe(runtimeMessage);
|
|
expect(response.error?.diagnostics?.summary).toBe(
|
|
'OpenCode managed profile node_modules link was blocked.'
|
|
);
|
|
expect(response.error?.diagnostics?.likelyCause).toContain(
|
|
'Windows denied creating the managed OpenCode profile node_modules link'
|
|
);
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe(runtimeMessage);
|
|
expect(response.error?.diagnostics?.hints).toEqual(
|
|
expect.arrayContaining([
|
|
'The app attempts automatic junction fallback for this Windows link failure before showing this error.',
|
|
'As a temporary workaround, enable Windows Developer Mode or run Agent Teams AI as Administrator.',
|
|
])
|
|
);
|
|
});
|
|
|
|
it('attempts junction pre-seed and retry on Windows when EPERM symlink error is detected in loadView', async () => {
|
|
const runtimeMessage = [
|
|
'Runtime provider management command failed unexpectedly:',
|
|
"EPERM: operation not permitted, symlink 'C:\\Users\\test\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
|
|
"-> 'C:\\Users\\test\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
|
|
].join(' ');
|
|
const firstError = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(firstError, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: { code: 'runtime-unhealthy', message: runtimeMessage, recoverable: true },
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const successResponse = {
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: { state: 'ready', cliPath: '/repo/cli-dev', version: '1.15.6', managedProfile: 'active', localAuth: 'synced' },
|
|
providers: [],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
};
|
|
|
|
execCliMock
|
|
.mockRejectedValueOnce(firstError)
|
|
.mockResolvedValueOnce({ stdout: JSON.stringify(successResponse), stderr: '' });
|
|
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
(isOpenCodeNodeModulesSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
(extractProfileIdFromSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue('abc123');
|
|
(ensureOpenCodeProfileNodeModulesJunctionMock as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
|
|
try {
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({ runtimeId: 'opencode' });
|
|
|
|
expect(ensureOpenCodeProfileNodeModulesJunctionMock).toHaveBeenCalledWith('abc123', expect.any(String));
|
|
expect(execCliMock).toHaveBeenCalledTimes(2);
|
|
expect(response.error).toBeUndefined();
|
|
expect(response.view?.runtime?.state).toBe('ready');
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
vi.mocked(isOpenCodeNodeModulesSymlinkErrorMock).mockRestore();
|
|
vi.mocked(extractProfileIdFromSymlinkErrorMock).mockRestore();
|
|
vi.mocked(ensureOpenCodeProfileNodeModulesJunctionMock).mockRestore();
|
|
}
|
|
});
|
|
|
|
it('falls back to error response when junction pre-seed succeeds but retry also fails in loadView', async () => {
|
|
const runtimeMessage = [
|
|
'Runtime provider management command failed unexpectedly:',
|
|
"EPERM: operation not permitted, symlink 'C:\\Users\\test\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
|
|
"-> 'C:\\Users\\test\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
|
|
].join(' ');
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: { code: 'runtime-unhealthy', message: runtimeMessage, recoverable: true },
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
(isOpenCodeNodeModulesSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
(extractProfileIdFromSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue('abc123');
|
|
(ensureOpenCodeProfileNodeModulesJunctionMock as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
|
|
try {
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({ runtimeId: 'opencode' });
|
|
|
|
expect(ensureOpenCodeProfileNodeModulesJunctionMock).toHaveBeenCalledWith('abc123', expect.any(String));
|
|
expect(execCliMock).toHaveBeenCalledTimes(2);
|
|
expect(response.error?.message).toBe(runtimeMessage);
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
vi.mocked(isOpenCodeNodeModulesSymlinkErrorMock).mockRestore();
|
|
vi.mocked(extractProfileIdFromSymlinkErrorMock).mockRestore();
|
|
vi.mocked(ensureOpenCodeProfileNodeModulesJunctionMock).mockRestore();
|
|
}
|
|
});
|
|
|
|
it('does not retry when junction pre-seed fails in loadView', async () => {
|
|
const runtimeMessage = [
|
|
'Runtime provider management command failed unexpectedly:',
|
|
"EPERM: operation not permitted, symlink 'C:\\Users\\test\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
|
|
"-> 'C:\\Users\\test\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\abc123\\config\\opencode\\node_modules'",
|
|
].join(' ');
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: { code: 'runtime-unhealthy', message: runtimeMessage, recoverable: true },
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
(isOpenCodeNodeModulesSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
(extractProfileIdFromSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue('abc123');
|
|
(ensureOpenCodeProfileNodeModulesJunctionMock as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
|
|
|
try {
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({ runtimeId: 'opencode' });
|
|
|
|
expect(ensureOpenCodeProfileNodeModulesJunctionMock).toHaveBeenCalledWith('abc123', expect.any(String));
|
|
expect(execCliMock).toHaveBeenCalledTimes(1);
|
|
expect(response.error?.message).toBe(runtimeMessage);
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
vi.mocked(isOpenCodeNodeModulesSymlinkErrorMock).mockRestore();
|
|
vi.mocked(extractProfileIdFromSymlinkErrorMock).mockRestore();
|
|
vi.mocked(ensureOpenCodeProfileNodeModulesJunctionMock).mockRestore();
|
|
}
|
|
});
|
|
|
|
it('does not attempt junction retry on non-Windows platforms in loadView', async () => {
|
|
const runtimeMessage = [
|
|
'Runtime provider management command failed unexpectedly:',
|
|
"EPERM: operation not permitted, symlink 'opencode' -> 'node_modules'",
|
|
].join(' ');
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers view');
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: { code: 'runtime-unhealthy', message: runtimeMessage, recoverable: true },
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
(isOpenCodeNodeModulesSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
(extractProfileIdFromSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue('abc123');
|
|
|
|
try {
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({ runtimeId: 'opencode' });
|
|
|
|
expect(ensureOpenCodeProfileNodeModulesJunctionMock).not.toHaveBeenCalled();
|
|
expect(execCliMock).toHaveBeenCalledTimes(1);
|
|
expect(response.error?.message).toBe(runtimeMessage);
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
vi.mocked(isOpenCodeNodeModulesSymlinkErrorMock).mockRestore();
|
|
vi.mocked(extractProfileIdFromSymlinkErrorMock).mockRestore();
|
|
}
|
|
});
|
|
|
|
it('attempts junction pre-seed and retry on Windows for loadProviderDirectory', async () => {
|
|
const runtimeMessage = [
|
|
'Runtime provider management command failed unexpectedly:',
|
|
"EPERM: operation not permitted, symlink 'C:\\Users\\test\\AppData\\Local\\claude-multimodel-nodejs\\Cache\\opencode\\shared-cache\\config-node_modules'",
|
|
"-> 'C:\\Users\\test\\AppData\\Local\\claude-multimodel-nodejs\\Data\\opencode\\profiles\\def456\\config\\opencode\\node_modules'",
|
|
].join(' ');
|
|
const firstError = new Error('Command failed: /repo/cli-dev runtime providers directory');
|
|
Object.assign(firstError, {
|
|
stdout: '',
|
|
stderr: runtimeMessage,
|
|
});
|
|
|
|
const successResponse = {
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
directory: {
|
|
runtimeId: 'opencode',
|
|
totalCount: 0,
|
|
returnedCount: 0,
|
|
query: null,
|
|
filter: 'all',
|
|
limit: 50,
|
|
cursor: null,
|
|
nextCursor: null,
|
|
entries: [],
|
|
diagnostics: [],
|
|
fetchedAt: new Date().toISOString(),
|
|
},
|
|
};
|
|
|
|
execCliMock
|
|
.mockRejectedValueOnce(firstError)
|
|
.mockResolvedValueOnce({ stdout: JSON.stringify(successResponse), stderr: '' });
|
|
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
(isOpenCodeNodeModulesSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
(extractProfileIdFromSymlinkErrorMock as ReturnType<typeof vi.fn>).mockReturnValue('def456');
|
|
(ensureOpenCodeProfileNodeModulesJunctionMock as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
|
|
try {
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadProviderDirectory({ runtimeId: 'opencode' });
|
|
|
|
expect(ensureOpenCodeProfileNodeModulesJunctionMock).toHaveBeenCalledWith('def456', expect.any(String));
|
|
expect(execCliMock).toHaveBeenCalledTimes(2);
|
|
expect(response.directory?.entries).toEqual([]);
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
vi.mocked(isOpenCodeNodeModulesSymlinkErrorMock).mockRestore();
|
|
vi.mocked(extractProfileIdFromSymlinkErrorMock).mockRestore();
|
|
vi.mocked(ensureOpenCodeProfileNodeModulesJunctionMock).mockRestore();
|
|
}
|
|
});
|
|
|
|
it('does not let non-object error logs shadow a later valid runtime response', async () => {
|
|
const validResponse = {
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.15.6',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
};
|
|
execCliMock.mockResolvedValue({
|
|
stdout: [
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: 'debug preflight',
|
|
}),
|
|
JSON.stringify(validResponse),
|
|
].join('\n'),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error).toBeUndefined();
|
|
expect(response.view?.runtime.state).toBe('ready');
|
|
});
|
|
|
|
it('does not let non-contract error object logs shadow a later valid runtime response', async () => {
|
|
const validResponse = {
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.15.6',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
};
|
|
execCliMock.mockResolvedValue({
|
|
stdout: [
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: { debug: true },
|
|
}),
|
|
JSON.stringify(validResponse),
|
|
].join('\n'),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error).toBeUndefined();
|
|
expect(response.view?.runtime.state).toBe('ready');
|
|
});
|
|
|
|
it('parses JSON error responses from failed forget commands', async () => {
|
|
const error = new Error('Command failed: /repo/cli-dev runtime providers forget');
|
|
Object.assign(error, {
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
error: {
|
|
code: 'unsupported-action',
|
|
message: 'This OpenCode runtime does not advertise credential removal through /doc',
|
|
recoverable: true,
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
execCliMock.mockRejectedValue(error);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.forgetCredential({
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
});
|
|
|
|
expect(response.error?.code).toBe('unsupported-action');
|
|
expect(response.error?.message).toBe(
|
|
'This OpenCode runtime does not advertise credential removal through /doc'
|
|
);
|
|
});
|
|
|
|
it('rejects the OpenCode CLI binary before running runtime provider commands', async () => {
|
|
resolveBinaryMock.mockResolvedValue('/opt/homebrew/bin/opencode');
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({ shouldNotRun: true }),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
projectPath: '/Users/test/My Project',
|
|
});
|
|
|
|
expect(execCliMock).not.toHaveBeenCalled();
|
|
expect(buildProviderAwareCliEnvMock).not.toHaveBeenCalled();
|
|
expect(clearBinaryCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(response.error?.code).toBe('runtime-misconfigured');
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings are using the wrong runtime binary.'
|
|
);
|
|
expect(response.error?.message).toContain(
|
|
'Command that was blocked: /opt/homebrew/bin/opencode runtime providers view --runtime opencode --json --compact --project-path'
|
|
);
|
|
expect(response.error?.message).toContain(
|
|
'The app resolved the OpenCode CLI itself as the Agent Teams runtime binary.'
|
|
);
|
|
expect(response.error?.diagnostics?.errorCode).toBe('runtime-misconfigured');
|
|
expect(response.error?.diagnostics?.binaryPath).toBe('/opt/homebrew/bin/opencode');
|
|
expect(response.error?.diagnostics?.command).toBe(
|
|
"/opt/homebrew/bin/opencode runtime providers view --runtime opencode --json --compact --project-path '/Users/test/My Project'"
|
|
);
|
|
expect(response.error?.diagnostics?.projectPath).toBe('/Users/test/My Project');
|
|
expect(response.error?.diagnostics?.stdoutPreview).toBeNull();
|
|
expect(response.error?.diagnostics?.stderrPreview).toBeNull();
|
|
expect(response.error?.diagnostics?.hints).toContain(
|
|
'Those environment variables must not point to opencode.'
|
|
);
|
|
});
|
|
|
|
it('rejects runtime symlinks that resolve to the OpenCode CLI binary', async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-runtime-'));
|
|
const opencodeTarget = path.join(tempDir, 'opencode');
|
|
const runtimeLink = path.join(tempDir, 'claude-multimodel');
|
|
try {
|
|
fs.writeFileSync(opencodeTarget, '#!/bin/sh\n');
|
|
fs.symlinkSync(opencodeTarget, runtimeLink);
|
|
resolveBinaryMock.mockResolvedValue(runtimeLink);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(execCliMock).not.toHaveBeenCalled();
|
|
expect(buildProviderAwareCliEnvMock).not.toHaveBeenCalled();
|
|
expect(clearBinaryCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(response.error?.code).toBe('runtime-misconfigured');
|
|
expect(response.error?.diagnostics?.binaryPath).toBe(runtimeLink);
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings are using the wrong runtime binary.'
|
|
);
|
|
} finally {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('rejects OpenCode CLI connect commands before spawning or writing secrets', async () => {
|
|
resolveBinaryMock.mockResolvedValue('/opt/homebrew/bin/opencode.cmd');
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.connectProvider({
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
method: 'api',
|
|
apiKey: 'sk-secret-value-123456',
|
|
metadata: {
|
|
region: 'us',
|
|
},
|
|
projectPath: '/Users/test/project',
|
|
});
|
|
|
|
expect(spawnCliMock).not.toHaveBeenCalled();
|
|
expect(buildProviderAwareCliEnvMock).not.toHaveBeenCalled();
|
|
expect(clearBinaryCacheMock).toHaveBeenCalledTimes(1);
|
|
expect(response.error?.code).toBe('runtime-misconfigured');
|
|
expect(response.error?.diagnostics?.binaryPath).toBe('/opt/homebrew/bin/opencode.cmd');
|
|
expect(response.error?.diagnostics?.command).toBe(
|
|
'/opt/homebrew/bin/opencode.cmd runtime providers connect --runtime opencode --provider openrouter --stdin-json --json --project-path /Users/test/project'
|
|
);
|
|
expect(JSON.stringify(response)).not.toContain('sk-secret-value-123456');
|
|
});
|
|
|
|
it('does not reject valid orchestrator paths that only contain opencode in a parent directory', async () => {
|
|
resolveBinaryMock.mockResolvedValue('/repo/opencode-runtime/cli-source');
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.15.6',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
});
|
|
|
|
expect(response.error).toBeUndefined();
|
|
expect(response.view?.runtime.cliPath).toBe('/opt/homebrew/bin/opencode');
|
|
expect(execCliMock).toHaveBeenCalledWith(
|
|
'/repo/opencode-runtime/cli-source',
|
|
expect.arrayContaining(['runtime', 'providers', 'view']),
|
|
expect.any(Object)
|
|
);
|
|
expect(execCliMock.mock.calls[0]?.[2]).toMatchObject({ timeout: 90_000 });
|
|
});
|
|
|
|
it('explains OpenCode CLI help output instead of returning a generic JSON error', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: [
|
|
'Usage: opencode [command]',
|
|
'',
|
|
'Commands:',
|
|
' opencode providers',
|
|
' opencode models',
|
|
'api_key: sk-secret-value-123456',
|
|
].join('\n'),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadView({
|
|
runtimeId: 'opencode',
|
|
projectPath: '/Users/test/My Project',
|
|
});
|
|
|
|
expect(response.error?.message).toContain(
|
|
'OpenCode provider settings could not read the runtime response.'
|
|
);
|
|
expect(response.error?.message).toContain(
|
|
'Expected a JSON object from the Agent Teams runtime provider command.'
|
|
);
|
|
expect(response.error?.message).toContain(
|
|
'Resolved runtime binary: /repo/cli-dev'
|
|
);
|
|
expect(response.error?.message).toContain(
|
|
"Command: /repo/cli-dev runtime providers view --runtime opencode --json --compact --project-path '/Users/test/My Project'"
|
|
);
|
|
expect(response.error?.message).toContain(
|
|
'Likely cause: The app is launching the OpenCode CLI itself instead of the Agent Teams runtime'
|
|
);
|
|
expect(response.error?.message).toContain('CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH');
|
|
expect(response.error?.message).toContain('stdout preview:');
|
|
expect(response.error?.message).toContain('opencode providers');
|
|
expect(response.error?.message).not.toContain('sk-secret-value-123456');
|
|
expect(response.error?.message).toContain('api_key: ...redacted');
|
|
expect(response.error?.diagnostics?.binaryPath).toBe('/repo/cli-dev');
|
|
expect(response.error?.diagnostics?.command).toBe(
|
|
"/repo/cli-dev runtime providers view --runtime opencode --json --compact --project-path '/Users/test/My Project'"
|
|
);
|
|
expect(response.error?.diagnostics?.projectPath).toBe('/Users/test/My Project');
|
|
expect(response.error?.diagnostics?.likelyCause).toContain('OpenCode CLI itself');
|
|
expect(response.error?.diagnostics?.hints).toContain(
|
|
'Those environment variables must not point to opencode.'
|
|
);
|
|
expect(response.error?.diagnostics?.stdoutPreview).toContain('api_key: ...redacted');
|
|
expect(response.error?.diagnostics?.stdoutPreview).not.toContain('sk-secret-value-123456');
|
|
});
|
|
|
|
it('formats non-JSON spawn output with exit code and stderr preview', async () => {
|
|
const { child } = createSpawnProcess('not-json', 1);
|
|
const processEvents = new EventEmitter();
|
|
const stdinEvents = new EventEmitter();
|
|
const stdout = new EventEmitter();
|
|
const stderr = new EventEmitter();
|
|
const stdinWrite = vi.fn();
|
|
const stdinEnd = vi.fn(() => {
|
|
queueMicrotask(() => {
|
|
stdout.emit('data', Buffer.from('not-json'));
|
|
stderr.emit('data', Buffer.from('runtime crashed before JSON'));
|
|
processEvents.emit('close', 1);
|
|
});
|
|
});
|
|
spawnCliMock.mockReturnValue({
|
|
...child,
|
|
stdout,
|
|
stderr,
|
|
stdin: {
|
|
write: stdinWrite,
|
|
end: stdinEnd,
|
|
once: stdinEvents.once.bind(stdinEvents),
|
|
},
|
|
once: processEvents.once.bind(processEvents),
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.connectProvider({
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
method: 'api',
|
|
apiKey: 'sk-secret-value-123456',
|
|
metadata: {},
|
|
});
|
|
|
|
expect(response.error?.message).toContain('Exit code: 1');
|
|
expect(response.error?.message).toContain('stderr preview:');
|
|
expect(response.error?.message).toContain('runtime crashed before JSON');
|
|
expect(response.error?.message).toContain('stdout preview:');
|
|
expect(response.error?.message).toContain('not-json');
|
|
expect(response.error?.diagnostics?.exitCode).toBe(1);
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe('runtime crashed before JSON');
|
|
expect(response.error?.diagnostics?.stdoutPreview).toBe('not-json');
|
|
expect(stdinWrite).toHaveBeenCalledWith(
|
|
JSON.stringify({
|
|
method: 'api',
|
|
apiKey: 'sk-secret-value-123456',
|
|
metadata: {},
|
|
})
|
|
);
|
|
});
|
|
|
|
it('captures provider stdin errors without dropping runtime diagnostics', async () => {
|
|
const processEvents = new EventEmitter();
|
|
const stdinEvents = new EventEmitter();
|
|
const stdout = new EventEmitter();
|
|
const stderr = new EventEmitter();
|
|
const stdinWrite = vi.fn(() => {
|
|
queueMicrotask(() => {
|
|
stdinEvents.emit('error', new Error('write EPIPE sk-secret-value-123456'));
|
|
stdout.emit('data', Buffer.from('not-json'));
|
|
processEvents.emit('close', 1);
|
|
});
|
|
});
|
|
const stdinEnd = vi.fn();
|
|
spawnCliMock.mockReturnValue({
|
|
stdout,
|
|
stderr,
|
|
stdin: {
|
|
write: stdinWrite,
|
|
end: stdinEnd,
|
|
once: stdinEvents.once.bind(stdinEvents),
|
|
},
|
|
once: processEvents.once.bind(processEvents),
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.connectWithApiKey({
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
apiKey: 'sk-input-secret-value-123456',
|
|
});
|
|
|
|
expect(response.error?.message).toContain('stdin error: write EPIPE sk-...redacted');
|
|
expect(response.error?.message).toContain('stdout preview:');
|
|
expect(response.error?.message).toContain('not-json');
|
|
expect(response.error?.message).not.toContain('sk-secret-value-123456');
|
|
expect(response.error?.message).not.toContain('sk-input-secret-value-123456');
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe(
|
|
'stdin error: write EPIPE sk-...redacted'
|
|
);
|
|
expect(response.error?.diagnostics?.stdoutPreview).toBe('not-json');
|
|
expect(stdinWrite).toHaveBeenCalledWith('sk-input-secret-value-123456');
|
|
});
|
|
|
|
it('keeps partial spawn stdout and stderr when a provider command times out', async () => {
|
|
vi.useFakeTimers();
|
|
const processEvents = new EventEmitter();
|
|
const stdinEvents = new EventEmitter();
|
|
const stdout = new EventEmitter();
|
|
const stderr = new EventEmitter();
|
|
const stdinWrite = vi.fn();
|
|
const stdinEnd = vi.fn(() => {
|
|
stdout.emit('data', Buffer.from('partial non-json stdout'));
|
|
stderr.emit('data', Buffer.from('api_key: sk-secret-value-123456'));
|
|
});
|
|
spawnCliMock.mockReturnValue({
|
|
stdout,
|
|
stderr,
|
|
stdin: {
|
|
write: stdinWrite,
|
|
end: stdinEnd,
|
|
once: stdinEvents.once.bind(stdinEvents),
|
|
},
|
|
once: processEvents.once.bind(processEvents),
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const responsePromise = client.connectWithApiKey({
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
apiKey: 'sk-input-secret-value-123456',
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(90_000);
|
|
const response = await responsePromise;
|
|
vi.useRealTimers();
|
|
|
|
expect(response.error?.message).toContain('stderr preview:');
|
|
expect(response.error?.message).toContain('api_key: ...redacted');
|
|
expect(response.error?.message).toContain('partial non-json stdout');
|
|
expect(response.error?.message).not.toContain('sk-secret-value-123456');
|
|
expect(response.error?.message).not.toContain('sk-input-secret-value-123456');
|
|
expect(response.error?.diagnostics?.stderrPreview).toBe('api_key: ...redacted');
|
|
expect(response.error?.diagnostics?.stdoutPreview).toBe('partial non-json stdout');
|
|
expect(stdinWrite).toHaveBeenCalledWith('sk-input-secret-value-123456');
|
|
});
|
|
|
|
it('passes project path as cwd and CLI flag for project-aware provider management', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.0.0',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [],
|
|
defaultModel: null,
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
await client.loadView({
|
|
runtimeId: 'opencode',
|
|
projectPath: '/Users/test/project',
|
|
});
|
|
|
|
expect(execCliMock).toHaveBeenCalledWith(
|
|
'/repo/cli-dev',
|
|
expect.arrayContaining(['--project-path', '/Users/test/project']),
|
|
expect.objectContaining({ cwd: '/Users/test/project' })
|
|
);
|
|
});
|
|
|
|
it('loads provider directory with optional args and omits absent values', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
directory: {
|
|
runtimeId: 'opencode',
|
|
totalCount: 1,
|
|
returnedCount: 1,
|
|
query: 'deep',
|
|
filter: 'connectable',
|
|
limit: 10,
|
|
cursor: null,
|
|
nextCursor: null,
|
|
fetchedAt: '2026-04-25T00:00:00.000Z',
|
|
entries: [],
|
|
diagnostics: [],
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadProviderDirectory({
|
|
runtimeId: 'opencode',
|
|
projectPath: '/Users/test/project',
|
|
query: 'deep',
|
|
filter: 'connectable',
|
|
limit: 10,
|
|
refresh: true,
|
|
});
|
|
|
|
expect(response.directory?.query).toBe('deep');
|
|
expect(execCliMock).toHaveBeenCalledWith(
|
|
'/repo/cli-dev',
|
|
[
|
|
'runtime',
|
|
'providers',
|
|
'directory',
|
|
'--runtime',
|
|
'opencode',
|
|
'--json',
|
|
'--project-path',
|
|
'/Users/test/project',
|
|
'--query',
|
|
'deep',
|
|
'--filter',
|
|
'connectable',
|
|
'--limit',
|
|
'10',
|
|
'--refresh',
|
|
],
|
|
expect.objectContaining({ cwd: '/Users/test/project' })
|
|
);
|
|
expect(execCliMock.mock.calls[0]?.[2]).toMatchObject({ maxBuffer: 8 * 1024 * 1024 });
|
|
expect(JSON.stringify(execCliMock.mock.calls[0])).not.toContain('undefined');
|
|
});
|
|
|
|
it('passes all-projects default scope to the runtime CLI', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
view: {
|
|
runtimeId: 'opencode',
|
|
title: 'OpenCode',
|
|
runtime: {
|
|
state: 'ready',
|
|
cliPath: '/opt/homebrew/bin/opencode',
|
|
version: '1.0.0',
|
|
managedProfile: 'active',
|
|
localAuth: 'synced',
|
|
},
|
|
providers: [],
|
|
configuredModels: [],
|
|
projectPath: '/Users/test/project',
|
|
projectDefaultModel: null,
|
|
allProjectsDefaultModel: 'openrouter/qwen/qwen3-coder',
|
|
defaultModelSource: 'all_projects',
|
|
defaultModel: 'openrouter/qwen/qwen3-coder',
|
|
fallbackModel: null,
|
|
diagnostics: [],
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
await client.setDefaultModel({
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
modelId: 'openrouter/qwen/qwen3-coder',
|
|
scope: 'all_projects',
|
|
projectPath: '/Users/test/project',
|
|
});
|
|
|
|
expect(execCliMock).toHaveBeenCalledWith(
|
|
'/repo/cli-dev',
|
|
expect.arrayContaining(['--scope', 'all-projects']),
|
|
expect.objectContaining({ cwd: '/Users/test/project' })
|
|
);
|
|
});
|
|
|
|
it('loads provider setup forms through the CLI contract', async () => {
|
|
execCliMock.mockResolvedValue({
|
|
stdout: JSON.stringify({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
setupForm: {
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
displayName: 'OpenRouter',
|
|
method: 'api',
|
|
supported: true,
|
|
title: 'Connect OpenRouter',
|
|
description: null,
|
|
submitLabel: 'Connect',
|
|
disabledReason: null,
|
|
source: 'curated',
|
|
secret: {
|
|
key: 'key',
|
|
label: 'API key',
|
|
placeholder: 'Paste API key',
|
|
required: true,
|
|
},
|
|
prompts: [],
|
|
},
|
|
}),
|
|
stderr: '',
|
|
});
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.loadSetupForm({
|
|
runtimeId: 'opencode',
|
|
providerId: 'openrouter',
|
|
projectPath: '/Users/test/project',
|
|
});
|
|
|
|
expect(response.setupForm?.providerId).toBe('openrouter');
|
|
expect(execCliMock).toHaveBeenCalledWith(
|
|
'/repo/cli-dev',
|
|
[
|
|
'runtime',
|
|
'providers',
|
|
'setup-form',
|
|
'--runtime',
|
|
'opencode',
|
|
'--provider',
|
|
'openrouter',
|
|
'--json',
|
|
'--project-path',
|
|
'/Users/test/project',
|
|
],
|
|
expect.objectContaining({ cwd: '/Users/test/project' })
|
|
);
|
|
});
|
|
|
|
it('passes generic provider setup payload through stdin JSON only', async () => {
|
|
const { child, stdinWrite } = createSpawnProcess({
|
|
schemaVersion: 1,
|
|
runtimeId: 'opencode',
|
|
provider: {
|
|
providerId: 'cloudflare-ai-gateway',
|
|
displayName: 'Cloudflare AI Gateway',
|
|
state: 'connected',
|
|
ownership: ['managed'],
|
|
recommended: false,
|
|
modelCount: 0,
|
|
defaultModelId: null,
|
|
authMethods: ['api'],
|
|
actions: [],
|
|
detail: null,
|
|
},
|
|
});
|
|
spawnCliMock.mockReturnValue(child);
|
|
|
|
const client = new AgentTeamsRuntimeProviderManagementCliClient();
|
|
const response = await client.connectProvider({
|
|
runtimeId: 'opencode',
|
|
providerId: 'cloudflare-ai-gateway',
|
|
method: 'api',
|
|
apiKey: 'sk-secret-value',
|
|
metadata: {
|
|
accountId: 'account-123',
|
|
gatewayId: 'gateway-456',
|
|
},
|
|
projectPath: '/Users/test/project',
|
|
});
|
|
|
|
expect(response.provider?.providerId).toBe('cloudflare-ai-gateway');
|
|
expect(spawnCliMock).toHaveBeenCalledWith(
|
|
'/repo/cli-dev',
|
|
[
|
|
'runtime',
|
|
'providers',
|
|
'connect',
|
|
'--runtime',
|
|
'opencode',
|
|
'--provider',
|
|
'cloudflare-ai-gateway',
|
|
'--stdin-json',
|
|
'--json',
|
|
'--project-path',
|
|
'/Users/test/project',
|
|
],
|
|
expect.objectContaining({ cwd: '/Users/test/project' })
|
|
);
|
|
expect(JSON.stringify(spawnCliMock.mock.calls[0])).not.toContain('sk-secret-value');
|
|
expect(stdinWrite).toHaveBeenCalledWith(
|
|
JSON.stringify({
|
|
method: 'api',
|
|
apiKey: 'sk-secret-value',
|
|
metadata: {
|
|
accountId: 'account-123',
|
|
gatewayId: 'gateway-456',
|
|
},
|
|
})
|
|
);
|
|
});
|
|
});
|