The failure.message passed to ensureOpenCodeProfileNodeModulesJunction comes from normalizeCommandFailure which may produce a JSON-escaped string when the error contains structured JSON in stdout. Using the raw runtimeMessage literal causes a mismatch in CI. Switch to expect.any(String) to accept any string value for the errorMessage parameter while still verifying the call happens.
1791 lines
64 KiB
TypeScript
1791 lines
64 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 {
|
|
isOpenCodeNodeModulesSymlinkError as isOpenCodeNodeModulesSymlinkErrorMock,
|
|
extractProfileIdFromSymlinkError as extractProfileIdFromSymlinkErrorMock,
|
|
ensureOpenCodeProfileNodeModulesJunction as ensureOpenCodeProfileNodeModulesJunctionMock,
|
|
} 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 next runtime update will include automatic junction fallback for Windows.',
|
|
'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 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',
|
|
},
|
|
})
|
|
);
|
|
});
|
|
});
|