Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 17 additions & 16 deletions agents-cli/src/__tests__/commands/init.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as p from '@clack/prompts';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { initCommand } from '../../commands/init';
import { LOCAL_REMOTE } from '../../utils/profiles';

// Mock @clack/prompts
vi.mock('@clack/prompts');
Expand Down Expand Up @@ -70,7 +71,7 @@ describe('Init Command', () => {
vi.mocked(p.text)
.mockResolvedValueOnce('./inkeep.config.ts') // confirmedPath
.mockResolvedValueOnce('test-tenant-123') // tenantId
.mockResolvedValueOnce('http://localhost:3002'); // apiUrl
.mockResolvedValueOnce(LOCAL_REMOTE.api); // apiUrl
vi.mocked(p.isCancel).mockReturnValue(false);

await initCommand({ local: true });
Expand All @@ -84,7 +85,7 @@ describe('Init Command', () => {
// Verify all required parts are present
expect(writtenContent).toContain("tenantId: 'test-tenant-123'");
expect(writtenContent).toContain('agentsApi:');
expect(writtenContent).toContain("url: 'http://localhost:3002'");
expect(writtenContent).toContain(`url: '${LOCAL_REMOTE.api}'`);

// Verify it's using nested format (not flat)
expect(writtenContent).not.toContain('agentsApiUrl:');
Expand Down Expand Up @@ -157,7 +158,7 @@ describe('Init Command', () => {
return 'valid-tenant';
}
if (options.message.includes('Agents API')) {
return 'http://localhost:3002';
return LOCAL_REMOTE.api;
}
return './inkeep.config.ts';
});
Expand All @@ -184,7 +185,7 @@ describe('Init Command', () => {
vi.mocked(p.text).mockImplementation(async (options: any) => {
if (options.message.includes('Agents API URL')) {
validateFn = options.validate;
return 'http://localhost:3002';
return LOCAL_REMOTE.api;
}
if (options.message.includes('tenant')) {
return 'test-tenant';
Expand All @@ -198,7 +199,7 @@ describe('Init Command', () => {
// Test validation function
if (validateFn) {
expect(validateFn('not-a-url')).toBe('Please enter a valid URL');
expect(validateFn('http://localhost:3002')).toBe(undefined);
expect(validateFn(LOCAL_REMOTE.api)).toBe(undefined);
expect(validateFn('https://agents-api.example.com')).toBe(undefined);
}
});
Expand All @@ -211,7 +212,7 @@ describe('Init Command', () => {
// Mock clack prompts
vi.mocked(p.text)
.mockResolvedValueOnce('test-tenant') // tenantId
.mockResolvedValueOnce('http://localhost:3002'); // apiUrl
.mockResolvedValueOnce(LOCAL_REMOTE.api); // apiUrl
vi.mocked(p.isCancel).mockReturnValue(false);

await initCommand({ path: './custom/path', local: true });
Expand All @@ -232,7 +233,7 @@ describe('Init Command', () => {
vi.mocked(p.text)
.mockResolvedValueOnce('./inkeep.config.ts') // confirmedPath
.mockResolvedValueOnce('test-tenant') // tenantId
.mockResolvedValueOnce('http://localhost:3002'); // apiUrl
.mockResolvedValueOnce(LOCAL_REMOTE.api); // apiUrl
vi.mocked(p.isCancel).mockReturnValue(false);

vi.mocked(writeFileSync).mockImplementation(() => {
Expand Down Expand Up @@ -262,7 +263,7 @@ describe('Init Command', () => {
vi.mocked(p.text)
.mockResolvedValueOnce('./inkeep.config.ts') // confirmedPath
.mockResolvedValueOnce('test-tenant-123') // tenantId
.mockResolvedValueOnce('http://localhost:3002'); // apiUrl
.mockResolvedValueOnce(LOCAL_REMOTE.api); // apiUrl
vi.mocked(p.isCancel).mockReturnValue(false);

await initCommand({ local: true });
Expand All @@ -272,8 +273,8 @@ describe('Init Command', () => {
profiles: {
local: {
remote: {
api: 'http://localhost:3002',
manageUi: 'http://localhost:3001',
api: LOCAL_REMOTE.api,
manageUi: LOCAL_REMOTE.manageUi,
},
credential: 'none',
environment: 'development',
Expand Down Expand Up @@ -302,15 +303,15 @@ describe('Init Command', () => {
vi.mocked(p.text)
.mockResolvedValueOnce('./inkeep.config.ts') // confirmedPath
.mockResolvedValueOnce('test-tenant') // tenantId
.mockResolvedValueOnce('http://localhost:3002'); // apiUrl
.mockResolvedValueOnce(LOCAL_REMOTE.api); // apiUrl
vi.mocked(p.isCancel).mockReturnValue(false);

await initCommand({ local: true });

expect(mockProfileManager.addProfile).toHaveBeenCalledWith('local', {
remote: {
api: 'http://localhost:3002',
manageUi: 'http://localhost:3001',
api: LOCAL_REMOTE.api,
manageUi: LOCAL_REMOTE.manageUi,
},
credential: 'none',
environment: 'development',
Expand All @@ -333,8 +334,8 @@ describe('Init Command', () => {
cloud: { remote: 'cloud', credential: 'inkeep-cloud', environment: 'production' },
local: {
remote: {
api: 'http://localhost:3002',
manageUi: 'http://localhost:3001',
api: LOCAL_REMOTE.api,
manageUi: LOCAL_REMOTE.manageUi,
},
credential: 'none',
environment: 'development',
Expand All @@ -346,7 +347,7 @@ describe('Init Command', () => {
vi.mocked(p.text)
.mockResolvedValueOnce('./inkeep.config.ts') // confirmedPath
.mockResolvedValueOnce('test-tenant') // tenantId
.mockResolvedValueOnce('http://localhost:3002'); // apiUrl
.mockResolvedValueOnce(LOCAL_REMOTE.api); // apiUrl
vi.mocked(p.isCancel).mockReturnValue(false);

await initCommand({ local: true });
Expand Down
182 changes: 182 additions & 0 deletions agents-cli/src/__tests__/commands/profile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import * as p from '@clack/prompts';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const mockProfileManager = vi.hoisted(() => ({
getProfile: vi.fn().mockReturnValue(undefined),
addProfile: vi.fn(),
setActiveProfile: vi.fn(),
checkCredentialExists: vi.fn().mockResolvedValue(false),
}));

vi.mock('@clack/prompts');

vi.mock('../../utils/profiles', async () => {
const actual = await vi.importActual('../../utils/profiles');
return {
...actual,
ProfileManager: vi.fn(() => mockProfileManager),
};
});

import { profileAddCommand } from '../../commands/profile';
import { LOCAL_REMOTE } from '../../utils/profiles';

describe('profileAddCommand', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
vi.clearAllMocks();
mockProfileManager.checkCredentialExists.mockResolvedValue(false);
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('Local remote type', () => {
it('should create profile with LOCAL_REMOTE URLs without URL prompts', async () => {
vi.mocked(p.select).mockResolvedValueOnce('local');
vi.mocked(p.text).mockResolvedValueOnce('development'); // environment
vi.mocked(p.confirm).mockResolvedValueOnce(false); // switch profile

await profileAddCommand('test-local');

expect(mockProfileManager.addProfile).toHaveBeenCalledWith('test-local', {
remote: { api: LOCAL_REMOTE.api, manageUi: LOCAL_REMOTE.manageUi },
credential: 'none',
environment: 'development',
});
});

it('should not prompt for credential when Local is selected', async () => {
vi.mocked(p.select).mockResolvedValueOnce('local');
vi.mocked(p.text).mockResolvedValueOnce('development'); // environment
vi.mocked(p.confirm).mockResolvedValueOnce(false);

await profileAddCommand('test-local');

// p.text called once (environment only), not for API URL, Manage UI URL, or credential
expect(p.text).toHaveBeenCalledTimes(1);
expect(p.text).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Environment name:' })
);
});

it('should skip credential keychain warning for local profiles', async () => {
vi.mocked(p.select).mockResolvedValueOnce('local');
vi.mocked(p.text).mockResolvedValueOnce('development');
vi.mocked(p.confirm).mockResolvedValueOnce(false);

await profileAddCommand('test-local');

expect(mockProfileManager.checkCredentialExists).not.toHaveBeenCalled();
});

it('should default environment to development for local', async () => {
vi.mocked(p.select).mockResolvedValueOnce('local');
vi.mocked(p.text).mockResolvedValueOnce('development');
vi.mocked(p.confirm).mockResolvedValueOnce(false);

await profileAddCommand('my-local');

expect(p.text).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Environment name:',
initialValue: 'development',
})
);
});
});

describe('Cloud remote type', () => {
it('should create profile with cloud remote string', async () => {
vi.mocked(p.select).mockResolvedValueOnce('cloud');
vi.mocked(p.text)
.mockResolvedValueOnce('production') // environment
.mockResolvedValueOnce('inkeep-my-cloud'); // credential
vi.mocked(p.confirm).mockResolvedValueOnce(false);

await profileAddCommand('my-cloud');

expect(mockProfileManager.addProfile).toHaveBeenCalledWith('my-cloud', {
remote: 'cloud',
credential: 'inkeep-my-cloud',
environment: 'production',
});
});

it('should default environment to production for cloud', async () => {
vi.mocked(p.select).mockResolvedValueOnce('cloud');
vi.mocked(p.text).mockResolvedValueOnce('production').mockResolvedValueOnce('inkeep-test');
vi.mocked(p.confirm).mockResolvedValueOnce(false);

await profileAddCommand('test-cloud');

expect(p.text).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Environment name:',
initialValue: 'production',
})
);
});
});

describe('Custom remote type', () => {
it('should prompt for URLs with no initialValue', async () => {
vi.mocked(p.select).mockResolvedValueOnce('custom');
vi.mocked(p.text)
.mockResolvedValueOnce('https://api.staging.example.com') // api URL
.mockResolvedValueOnce('https://manage.staging.example.com') // manage UI URL
.mockResolvedValueOnce('staging') // environment
.mockResolvedValueOnce('inkeep-staging'); // credential
vi.mocked(p.confirm).mockResolvedValueOnce(false);

await profileAddCommand('staging');

expect(mockProfileManager.addProfile).toHaveBeenCalledWith('staging', {
remote: {
api: 'https://api.staging.example.com',
manageUi: 'https://manage.staging.example.com',
},
credential: 'inkeep-staging',
environment: 'staging',
});
});

it('should default environment to production for custom', async () => {
vi.mocked(p.select).mockResolvedValueOnce('custom');
vi.mocked(p.text)
.mockResolvedValueOnce('https://api.example.com')
.mockResolvedValueOnce('https://manage.example.com')
.mockResolvedValueOnce('production')
.mockResolvedValueOnce('inkeep-prod');
vi.mocked(p.confirm).mockResolvedValueOnce(false);

await profileAddCommand('prod');

expect(p.text).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Environment name:',
initialValue: 'production',
})
);
});

it('should check keychain for credential and warn if missing', async () => {
mockProfileManager.checkCredentialExists.mockResolvedValueOnce(false);
vi.mocked(p.select).mockResolvedValueOnce('custom');
vi.mocked(p.text)
.mockResolvedValueOnce('https://api.example.com')
.mockResolvedValueOnce('https://manage.example.com')
.mockResolvedValueOnce('production')
.mockResolvedValueOnce('inkeep-prod');
vi.mocked(p.confirm).mockResolvedValueOnce(false);

await profileAddCommand('prod');

expect(mockProfileManager.checkCredentialExists).toHaveBeenCalledWith('inkeep-prod');
});
});
});
3 changes: 2 additions & 1 deletion agents-cli/src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import chalk from 'chalk';
import { LOCAL_REMOTE } from '../utils/profiles';

export interface ConfigOptions {
config?: string;
Expand Down Expand Up @@ -87,7 +88,7 @@ export async function configSetCommand(

export default defineConfig({
tenantId: '${key === 'tenantId' ? value : ''}',
apiUrl: '${key === 'apiUrl' ? value : 'http://localhost:3002'}',
apiUrl: '${key === 'apiUrl' ? value : LOCAL_REMOTE.api}',
});
`;

Expand Down
9 changes: 5 additions & 4 deletions agents-cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import chalk from 'chalk';
import { checkKeychainAvailability, loadCredentials } from '../utils/credentials';
import {
DEFAULT_PROFILES_CONFIG,
LOCAL_REMOTE,
type Profile,
ProfileManager,
type ProfilesConfig,
Expand Down Expand Up @@ -387,7 +388,7 @@ async function localInitCommand(options?: InitOptions): Promise<void> {

if (options?.interactive === false) {
tenantId = 'default';
apiUrl = 'http://localhost:3002';
apiUrl = LOCAL_REMOTE.api;
} else {
const tenantIdInput = await p.text({
message: 'Enter your tenant ID:',
Expand Down Expand Up @@ -420,8 +421,8 @@ async function localInitCommand(options?: InitOptions): Promise<void> {

const apiUrlInput = await p.text({
message: 'Enter the Agents API URL:',
placeholder: 'http://localhost:3002',
initialValue: 'http://localhost:3002',
placeholder: LOCAL_REMOTE.api,
initialValue: LOCAL_REMOTE.api,
validate: validateUrl,
});

Expand Down Expand Up @@ -453,7 +454,7 @@ export default defineConfig({
const localProfile: Profile = {
remote: {
api: apiUrl,
manageUi: 'http://localhost:3001',
manageUi: LOCAL_REMOTE.manageUi,
},
credential: 'none',
environment: 'development',
Expand Down
Loading
Loading