Skip to content
Merged
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
17 changes: 17 additions & 0 deletions src/cli/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AgentAlreadyExistsError,
getErrorMessage,
isChangesetInProgressError,
isExpiredTokenError,
Expand All @@ -8,6 +9,22 @@ import {
import { describe, expect, it } from 'vitest';

describe('errors', () => {
describe('AgentAlreadyExistsError', () => {
it('has correct message with agent name', () => {
const err = new AgentAlreadyExistsError('my-agent');
expect(err.message).toBe('An agent named "my-agent" already exists in the schema.');
});

it('has correct name', () => {
const err = new AgentAlreadyExistsError('test');
expect(err.name).toBe('AgentAlreadyExistsError');
});

it('is instance of Error', () => {
expect(new AgentAlreadyExistsError('test')).toBeInstanceOf(Error);
});
});

describe('getErrorMessage', () => {
it('returns message from Error instance', () => {
const err = new Error('test error');
Expand Down
124 changes: 124 additions & 0 deletions src/cli/aws/__tests__/account-extended.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { AwsCredentialsError, detectAccount, getCredentialProvider, validateAwsCredentials } from '../account.js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const { mockSend } = vi.hoisted(() => ({
mockSend: vi.fn(),
}));

vi.mock('@aws-sdk/client-sts', () => ({
STSClient: class {
send = mockSend;
},
GetCallerIdentityCommand: class {
constructor(public input: unknown) {}
},
}));

vi.mock('@aws-sdk/credential-providers', () => ({
fromEnv: vi.fn().mockReturnValue({}),
fromNodeProviderChain: vi.fn().mockReturnValue({}),
}));

function makeNamedError(message: string, name: string): Error {
const err = new Error(message);
Object.defineProperty(err, 'name', { value: name, writable: true });
return err;
}

describe('getCredentialProvider', () => {
const originalEnv = { ...process.env };

afterEach(() => {
process.env = { ...originalEnv };
});

it('returns a credential provider (function)', () => {
const provider = getCredentialProvider();
expect(provider).toBeDefined();
});
});

describe('detectAccount', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns account ID on success', async () => {
mockSend.mockResolvedValue({ Account: '123456789012' });

const account = await detectAccount();
expect(account).toBe('123456789012');
});

it('returns null when Account is undefined', async () => {
mockSend.mockResolvedValue({ Account: undefined });

const account = await detectAccount();
expect(account).toBeNull();
});

it('throws AwsCredentialsError for ExpiredTokenException', async () => {
mockSend.mockRejectedValue(makeNamedError('Token expired', 'ExpiredTokenException'));

await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
await expect(detectAccount()).rejects.toThrow('expired');
});

it('throws AwsCredentialsError for ExpiredToken', async () => {
mockSend.mockRejectedValue(makeNamedError('Token expired', 'ExpiredToken'));

await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
});

it('throws AwsCredentialsError for InvalidClientTokenId', async () => {
mockSend.mockRejectedValue(makeNamedError('Invalid token', 'InvalidClientTokenId'));

await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
await expect(detectAccount()).rejects.toThrow('invalid');
});

it('throws AwsCredentialsError for SignatureDoesNotMatch', async () => {
mockSend.mockRejectedValue(makeNamedError('Sig mismatch', 'SignatureDoesNotMatch'));

await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
});

it('throws AwsCredentialsError for AccessDenied', async () => {
mockSend.mockRejectedValue(makeNamedError('Access denied', 'AccessDenied'));

await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
await expect(detectAccount()).rejects.toThrow('permissions');
});

it('throws AwsCredentialsError for AccessDeniedException', async () => {
mockSend.mockRejectedValue(makeNamedError('Access denied', 'AccessDeniedException'));

await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
});

it('returns null for unknown errors', async () => {
mockSend.mockRejectedValue(new Error('Unknown error'));

const account = await detectAccount();
expect(account).toBeNull();
});
});

describe('validateAwsCredentials', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('does not throw when credentials are valid', async () => {
mockSend.mockResolvedValue({ Account: '123456789012' });

await expect(validateAwsCredentials()).resolves.toBeUndefined();
});

it('throws AwsCredentialsError when detectAccount returns null', async () => {
mockSend.mockRejectedValue(new Error('something'));

await expect(validateAwsCredentials()).rejects.toThrow(AwsCredentialsError);
await expect(validateAwsCredentials()).rejects.toThrow('No AWS credentials configured');
});
});
25 changes: 25 additions & 0 deletions src/cli/aws/__tests__/account.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AwsCredentialsError } from '../account.js';
import { describe, expect, it } from 'vitest';

describe('AwsCredentialsError', () => {
it('uses short message as default message', () => {
const err = new AwsCredentialsError('Short msg');
expect(err.message).toBe('Short msg');
expect(err.shortMessage).toBe('Short msg');
});

it('uses detailed message when provided', () => {
const err = new AwsCredentialsError('Short msg', 'Detailed explanation');
expect(err.message).toBe('Detailed explanation');
expect(err.shortMessage).toBe('Short msg');
});

it('has correct name', () => {
const err = new AwsCredentialsError('test');
expect(err.name).toBe('AwsCredentialsError');
});

it('is an instance of Error', () => {
expect(new AwsCredentialsError('test')).toBeInstanceOf(Error);
});
});
58 changes: 58 additions & 0 deletions src/cli/aws/__tests__/agentcore-control.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getAgentRuntimeStatus } from '../agentcore-control.js';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const { mockSend } = vi.hoisted(() => ({
mockSend: vi.fn(),
}));

vi.mock('@aws-sdk/client-bedrock-agentcore-control', () => ({
BedrockAgentCoreControlClient: class {
send = mockSend;
},
GetAgentRuntimeCommand: class {
constructor(public input: unknown) {}
},
}));

vi.mock('../account', () => ({
getCredentialProvider: vi.fn().mockReturnValue({}),
}));

describe('getAgentRuntimeStatus', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns runtime status', async () => {
mockSend.mockResolvedValue({ status: 'ACTIVE' });

const result = await getAgentRuntimeStatus({ region: 'us-east-1', runtimeId: 'rt-123' });
expect(result.runtimeId).toBe('rt-123');
expect(result.status).toBe('ACTIVE');
});

it('throws when no status returned', async () => {
mockSend.mockResolvedValue({ status: undefined });

await expect(getAgentRuntimeStatus({ region: 'us-east-1', runtimeId: 'rt-456' })).rejects.toThrow(
'No status returned for runtime rt-456'
);
});

it('passes correct runtimeId in command', async () => {
mockSend.mockResolvedValue({ status: 'CREATING' });

await getAgentRuntimeStatus({ region: 'us-west-2', runtimeId: 'rt-abc' });

const command = mockSend.mock.calls[0]![0];
expect(command.input.agentRuntimeId).toBe('rt-abc');
});

it('propagates SDK errors', async () => {
mockSend.mockRejectedValue(new Error('Service unavailable'));

await expect(getAgentRuntimeStatus({ region: 'us-east-1', runtimeId: 'rt-err' })).rejects.toThrow(
'Service unavailable'
);
});
});
99 changes: 99 additions & 0 deletions src/cli/aws/__tests__/agentcore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { extractResult, parseSSE, parseSSELine } from '../agentcore.js';
import { describe, expect, it } from 'vitest';

describe('parseSSELine', () => {
it('returns null content for non-data lines', () => {
expect(parseSSELine('event: message')).toEqual({ content: null, error: null });
expect(parseSSELine('')).toEqual({ content: null, error: null });
expect(parseSSELine('id: 123')).toEqual({ content: null, error: null });
});

it('parses JSON string data', () => {
const result = parseSSELine('data: "Hello world"');
expect(result.content).toBe('Hello world');
expect(result.error).toBeNull();
});

it('returns raw content for non-JSON data', () => {
const result = parseSSELine('data: plain text here');
expect(result.content).toBe('plain text here');
expect(result.error).toBeNull();
});

it('detects error objects', () => {
const result = parseSSELine('data: {"error": "Something went wrong"}');
expect(result.content).toBeNull();
expect(result.error).toBe('Something went wrong');
});

it('returns null for non-string non-error JSON objects', () => {
const result = parseSSELine('data: {"key": "value"}');
expect(result.content).toBeNull();
expect(result.error).toBeNull();
});

it('handles empty data field', () => {
const result = parseSSELine('data: ');
expect(result.content).toBe('');
expect(result.error).toBeNull();
});
});

describe('parseSSE', () => {
it('combines multiple data lines into single string', () => {
const text = 'data: "Hello "\ndata: "World"';
expect(parseSSE(text)).toBe('Hello World');
});

it('ignores non-data lines', () => {
const text = 'event: message\ndata: "content"\nid: 1';
expect(parseSSE(text)).toBe('content');
});

it('returns empty string for no data lines', () => {
expect(parseSSE('event: ping\n')).toBe('');
});

it('stops on error and returns error message', () => {
const text = 'data: "part1"\ndata: {"error": "fail"}\ndata: "part2"';
expect(parseSSE(text)).toBe('Error: fail');
});

it('handles single data line', () => {
expect(parseSSE('data: "only line"')).toBe('only line');
});

it('handles raw non-JSON data lines', () => {
const text = 'data: hello\ndata: world';
expect(parseSSE(text)).toBe('helloworld');
});
});

describe('extractResult', () => {
it('extracts string result from JSON object', () => {
expect(extractResult('{"result": "answer"}')).toBe('answer');
});

it('stringifies non-string result', () => {
const result = extractResult('{"result": {"key": "val"}}');
expect(result).toContain('key');
expect(result).toContain('val');
});

it('returns plain string from JSON string', () => {
expect(extractResult('"plain string"')).toBe('plain string');
});

it('stringifies JSON object without result field', () => {
const result = extractResult('{"data": 42}');
expect(result).toContain('42');
});

it('returns raw text for non-JSON input', () => {
expect(extractResult('not json at all')).toBe('not json at all');
});

it('handles empty string', () => {
expect(extractResult('')).toBe('');
});
});
47 changes: 47 additions & 0 deletions src/cli/aws/__tests__/aws-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { detectAccount } from '../account';
import { detectAwsContext } from '../aws-context.js';
import { detectRegion } from '../region';
import { beforeEach, describe, expect, it, vi } from 'vitest';

// Mock dependencies
vi.mock('../account', () => ({
detectAccount: vi.fn(),
}));

vi.mock('../region', () => ({
detectRegion: vi.fn(),
}));

describe('detectAwsContext', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('returns combined account and region info', async () => {
vi.mocked(detectAccount).mockResolvedValue('123456789012');
vi.mocked(detectRegion).mockResolvedValue({ region: 'us-west-2', source: 'env' });

const result = await detectAwsContext();
expect(result.accountId).toBe('123456789012');
expect(result.region).toBe('us-west-2');
expect(result.regionSource).toBe('env');
});

it('returns null accountId when detection fails', async () => {
vi.mocked(detectAccount).mockResolvedValue(null);
vi.mocked(detectRegion).mockResolvedValue({ region: 'us-east-1', source: 'default' });

const result = await detectAwsContext();
expect(result.accountId).toBeNull();
expect(result.region).toBe('us-east-1');
expect(result.regionSource).toBe('default');
});

it('uses config source', async () => {
vi.mocked(detectAccount).mockResolvedValue('111222333444');
vi.mocked(detectRegion).mockResolvedValue({ region: 'eu-west-1', source: 'config' });

const result = await detectAwsContext();
expect(result.regionSource).toBe('config');
});
});
Loading
Loading