-
Notifications
You must be signed in to change notification settings - Fork 99
Fix CLI port mismatch and centralize local dev URLs #1987
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'); | ||
|
Comment on lines
+38
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 MAJOR: Missing test coverage for user cancellation flows Issue: The production code in Why: If a regression causes Fix: Add at least one cancellation test per prompt type. Example: it('should exit gracefully when user cancels remote type selection', async () => {
vi.mocked(p.select).mockResolvedValueOnce(Symbol('cancel'));
vi.mocked(p.isCancel).mockReturnValueOnce(true);
await expect(profileAddCommand('test')).rejects.toThrow('process.exit called');
expect(p.cancel).toHaveBeenCalledWith('Profile creation cancelled');
expect(mockProfileManager.addProfile).not.toHaveBeenCalled();
});Refs: |
||
| 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', | ||
| }); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Minor: Missing test for URL validation in Custom path Issue: The production code at Why: If URL validation regresses, users could create profiles with invalid/empty URLs that would fail at runtime during login or push operations. Fix: Add validation tests to the Custom describe block: it('should reject empty API URL with validation error', async () => {
vi.mocked(p.select).mockResolvedValueOnce('custom');
let apiValidate: ((v: string) => string | undefined) | undefined;
vi.mocked(p.text).mockImplementation(async (opts: any) => {
if (opts.message === 'Agents API URL:') {
apiValidate = opts.validate;
return 'https://api.example.com';
}
if (opts.message === 'Manage UI URL:') return 'https://manage.example.com';
if (opts.message === 'Environment name:') return 'production';
if (opts.message === 'Credential reference:') return 'inkeep-test';
return '';
});
vi.mocked(p.confirm).mockResolvedValueOnce(false);
await profileAddCommand('test');
expect(apiValidate!('')).toBe('URL is required');
expect(apiValidate!(' ')).toBe('URL is required');
expect(apiValidate!('not-a-url')).toBe('Invalid URL format');
});Refs: |
||
|
|
||
| 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'); | ||
| }); | ||
| }); | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Minor: Missing test for profile switch confirmation Issue: The production code at Why: If the Fix: Add a test that confirms the switch: it('should switch to new profile when user confirms', async () => {
vi.mocked(p.select).mockResolvedValueOnce('local');
vi.mocked(p.text).mockResolvedValueOnce('development');
vi.mocked(p.confirm).mockResolvedValueOnce(true); // User wants to switch
await profileAddCommand('new-local');
expect(mockProfileManager.setActiveProfile).toHaveBeenCalledWith('new-local');
});Refs: |
||
| 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; | ||
|
|
@@ -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}', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Minor: Config format inconsistency with Issue: This template generates configs with flat Why: A user who runs Fix: Consider updating to use the nested format for consistency: export default defineConfig({
tenantId: '${key === 'tenantId' ? value : ''}',
agentsApi: {
url: '${key === 'apiUrl' ? value : LOCAL_REMOTE.api}',
},
});Refs: |
||
| }); | ||
| `; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 MAJOR: Missing test for "profile already exists" error path
Issue: Production code at
profile.tslines 67-71 checks if a profile name is already taken and exits with error code 1. This critical validation has no test coverage.Why: A regression that removes or breaks this check could allow users to accidentally overwrite existing profiles, losing their configuration.
Fix: Add test:
Refs: