Skip to content

Commit f288df1

Browse files
committed
Merge branch 'main' into patch-1
2 parents 2645a72 + 81cb70e commit f288df1

89 files changed

Lines changed: 9188 additions & 54 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@
22
"name": "@aws/agentcore",
33
"version": "0.3.0-preview.1.0",
44
"description": "CLI for Amazon Bedrock AgentCore",
5+
"license": "Apache-2.0",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/aws/agentcore-cli.git"
9+
},
10+
"homepage": "https://github.com/aws/agentcore-cli",
11+
"bugs": {
12+
"url": "https://github.com/aws/agentcore-cli/issues"
13+
},
14+
"keywords": [
15+
"aws",
16+
"amazon",
17+
"bedrock",
18+
"agentcore",
19+
"cli",
20+
"agents",
21+
"ai",
22+
"cdk",
23+
"langchain",
24+
"langgraph",
25+
"openai",
26+
"anthropic",
27+
"google-adk",
28+
"strands"
29+
],
530
"main": "./dist/index.js",
631
"types": "./dist/index.d.ts",
732
"bin": {

src/cli/__tests__/errors.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AgentAlreadyExistsError,
23
getErrorMessage,
34
isChangesetInProgressError,
45
isExpiredTokenError,
@@ -8,6 +9,22 @@ import {
89
import { describe, expect, it } from 'vitest';
910

1011
describe('errors', () => {
12+
describe('AgentAlreadyExistsError', () => {
13+
it('has correct message with agent name', () => {
14+
const err = new AgentAlreadyExistsError('my-agent');
15+
expect(err.message).toBe('An agent named "my-agent" already exists in the schema.');
16+
});
17+
18+
it('has correct name', () => {
19+
const err = new AgentAlreadyExistsError('test');
20+
expect(err.name).toBe('AgentAlreadyExistsError');
21+
});
22+
23+
it('is instance of Error', () => {
24+
expect(new AgentAlreadyExistsError('test')).toBeInstanceOf(Error);
25+
});
26+
});
27+
1128
describe('getErrorMessage', () => {
1229
it('returns message from Error instance', () => {
1330
const err = new Error('test error');
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { AwsCredentialsError, detectAccount, getCredentialProvider, validateAwsCredentials } from '../account.js';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
const { mockSend } = vi.hoisted(() => ({
5+
mockSend: vi.fn(),
6+
}));
7+
8+
vi.mock('@aws-sdk/client-sts', () => ({
9+
STSClient: class {
10+
send = mockSend;
11+
},
12+
GetCallerIdentityCommand: class {
13+
constructor(public input: unknown) {}
14+
},
15+
}));
16+
17+
vi.mock('@aws-sdk/credential-providers', () => ({
18+
fromEnv: vi.fn().mockReturnValue({}),
19+
fromNodeProviderChain: vi.fn().mockReturnValue({}),
20+
}));
21+
22+
function makeNamedError(message: string, name: string): Error {
23+
const err = new Error(message);
24+
Object.defineProperty(err, 'name', { value: name, writable: true });
25+
return err;
26+
}
27+
28+
describe('getCredentialProvider', () => {
29+
const originalEnv = { ...process.env };
30+
31+
afterEach(() => {
32+
process.env = { ...originalEnv };
33+
});
34+
35+
it('returns a credential provider (function)', () => {
36+
const provider = getCredentialProvider();
37+
expect(provider).toBeDefined();
38+
});
39+
});
40+
41+
describe('detectAccount', () => {
42+
beforeEach(() => {
43+
vi.clearAllMocks();
44+
});
45+
46+
it('returns account ID on success', async () => {
47+
mockSend.mockResolvedValue({ Account: '123456789012' });
48+
49+
const account = await detectAccount();
50+
expect(account).toBe('123456789012');
51+
});
52+
53+
it('returns null when Account is undefined', async () => {
54+
mockSend.mockResolvedValue({ Account: undefined });
55+
56+
const account = await detectAccount();
57+
expect(account).toBeNull();
58+
});
59+
60+
it('throws AwsCredentialsError for ExpiredTokenException', async () => {
61+
mockSend.mockRejectedValue(makeNamedError('Token expired', 'ExpiredTokenException'));
62+
63+
await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
64+
await expect(detectAccount()).rejects.toThrow('expired');
65+
});
66+
67+
it('throws AwsCredentialsError for ExpiredToken', async () => {
68+
mockSend.mockRejectedValue(makeNamedError('Token expired', 'ExpiredToken'));
69+
70+
await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
71+
});
72+
73+
it('throws AwsCredentialsError for InvalidClientTokenId', async () => {
74+
mockSend.mockRejectedValue(makeNamedError('Invalid token', 'InvalidClientTokenId'));
75+
76+
await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
77+
await expect(detectAccount()).rejects.toThrow('invalid');
78+
});
79+
80+
it('throws AwsCredentialsError for SignatureDoesNotMatch', async () => {
81+
mockSend.mockRejectedValue(makeNamedError('Sig mismatch', 'SignatureDoesNotMatch'));
82+
83+
await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
84+
});
85+
86+
it('throws AwsCredentialsError for AccessDenied', async () => {
87+
mockSend.mockRejectedValue(makeNamedError('Access denied', 'AccessDenied'));
88+
89+
await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
90+
await expect(detectAccount()).rejects.toThrow('permissions');
91+
});
92+
93+
it('throws AwsCredentialsError for AccessDeniedException', async () => {
94+
mockSend.mockRejectedValue(makeNamedError('Access denied', 'AccessDeniedException'));
95+
96+
await expect(detectAccount()).rejects.toThrow(AwsCredentialsError);
97+
});
98+
99+
it('returns null for unknown errors', async () => {
100+
mockSend.mockRejectedValue(new Error('Unknown error'));
101+
102+
const account = await detectAccount();
103+
expect(account).toBeNull();
104+
});
105+
});
106+
107+
describe('validateAwsCredentials', () => {
108+
beforeEach(() => {
109+
vi.clearAllMocks();
110+
});
111+
112+
it('does not throw when credentials are valid', async () => {
113+
mockSend.mockResolvedValue({ Account: '123456789012' });
114+
115+
await expect(validateAwsCredentials()).resolves.toBeUndefined();
116+
});
117+
118+
it('throws AwsCredentialsError when detectAccount returns null', async () => {
119+
mockSend.mockRejectedValue(new Error('something'));
120+
121+
await expect(validateAwsCredentials()).rejects.toThrow(AwsCredentialsError);
122+
await expect(validateAwsCredentials()).rejects.toThrow('No AWS credentials configured');
123+
});
124+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { AwsCredentialsError } from '../account.js';
2+
import { describe, expect, it } from 'vitest';
3+
4+
describe('AwsCredentialsError', () => {
5+
it('uses short message as default message', () => {
6+
const err = new AwsCredentialsError('Short msg');
7+
expect(err.message).toBe('Short msg');
8+
expect(err.shortMessage).toBe('Short msg');
9+
});
10+
11+
it('uses detailed message when provided', () => {
12+
const err = new AwsCredentialsError('Short msg', 'Detailed explanation');
13+
expect(err.message).toBe('Detailed explanation');
14+
expect(err.shortMessage).toBe('Short msg');
15+
});
16+
17+
it('has correct name', () => {
18+
const err = new AwsCredentialsError('test');
19+
expect(err.name).toBe('AwsCredentialsError');
20+
});
21+
22+
it('is an instance of Error', () => {
23+
expect(new AwsCredentialsError('test')).toBeInstanceOf(Error);
24+
});
25+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { getAgentRuntimeStatus } from '../agentcore-control.js';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
const { mockSend } = vi.hoisted(() => ({
5+
mockSend: vi.fn(),
6+
}));
7+
8+
vi.mock('@aws-sdk/client-bedrock-agentcore-control', () => ({
9+
BedrockAgentCoreControlClient: class {
10+
send = mockSend;
11+
},
12+
GetAgentRuntimeCommand: class {
13+
constructor(public input: unknown) {}
14+
},
15+
}));
16+
17+
vi.mock('../account', () => ({
18+
getCredentialProvider: vi.fn().mockReturnValue({}),
19+
}));
20+
21+
describe('getAgentRuntimeStatus', () => {
22+
beforeEach(() => {
23+
vi.clearAllMocks();
24+
});
25+
26+
it('returns runtime status', async () => {
27+
mockSend.mockResolvedValue({ status: 'ACTIVE' });
28+
29+
const result = await getAgentRuntimeStatus({ region: 'us-east-1', runtimeId: 'rt-123' });
30+
expect(result.runtimeId).toBe('rt-123');
31+
expect(result.status).toBe('ACTIVE');
32+
});
33+
34+
it('throws when no status returned', async () => {
35+
mockSend.mockResolvedValue({ status: undefined });
36+
37+
await expect(getAgentRuntimeStatus({ region: 'us-east-1', runtimeId: 'rt-456' })).rejects.toThrow(
38+
'No status returned for runtime rt-456'
39+
);
40+
});
41+
42+
it('passes correct runtimeId in command', async () => {
43+
mockSend.mockResolvedValue({ status: 'CREATING' });
44+
45+
await getAgentRuntimeStatus({ region: 'us-west-2', runtimeId: 'rt-abc' });
46+
47+
const command = mockSend.mock.calls[0]![0];
48+
expect(command.input.agentRuntimeId).toBe('rt-abc');
49+
});
50+
51+
it('propagates SDK errors', async () => {
52+
mockSend.mockRejectedValue(new Error('Service unavailable'));
53+
54+
await expect(getAgentRuntimeStatus({ region: 'us-east-1', runtimeId: 'rt-err' })).rejects.toThrow(
55+
'Service unavailable'
56+
);
57+
});
58+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { extractResult, parseSSE, parseSSELine } from '../agentcore.js';
2+
import { describe, expect, it } from 'vitest';
3+
4+
describe('parseSSELine', () => {
5+
it('returns null content for non-data lines', () => {
6+
expect(parseSSELine('event: message')).toEqual({ content: null, error: null });
7+
expect(parseSSELine('')).toEqual({ content: null, error: null });
8+
expect(parseSSELine('id: 123')).toEqual({ content: null, error: null });
9+
});
10+
11+
it('parses JSON string data', () => {
12+
const result = parseSSELine('data: "Hello world"');
13+
expect(result.content).toBe('Hello world');
14+
expect(result.error).toBeNull();
15+
});
16+
17+
it('returns raw content for non-JSON data', () => {
18+
const result = parseSSELine('data: plain text here');
19+
expect(result.content).toBe('plain text here');
20+
expect(result.error).toBeNull();
21+
});
22+
23+
it('detects error objects', () => {
24+
const result = parseSSELine('data: {"error": "Something went wrong"}');
25+
expect(result.content).toBeNull();
26+
expect(result.error).toBe('Something went wrong');
27+
});
28+
29+
it('returns null for non-string non-error JSON objects', () => {
30+
const result = parseSSELine('data: {"key": "value"}');
31+
expect(result.content).toBeNull();
32+
expect(result.error).toBeNull();
33+
});
34+
35+
it('handles empty data field', () => {
36+
const result = parseSSELine('data: ');
37+
expect(result.content).toBe('');
38+
expect(result.error).toBeNull();
39+
});
40+
});
41+
42+
describe('parseSSE', () => {
43+
it('combines multiple data lines into single string', () => {
44+
const text = 'data: "Hello "\ndata: "World"';
45+
expect(parseSSE(text)).toBe('Hello World');
46+
});
47+
48+
it('ignores non-data lines', () => {
49+
const text = 'event: message\ndata: "content"\nid: 1';
50+
expect(parseSSE(text)).toBe('content');
51+
});
52+
53+
it('returns empty string for no data lines', () => {
54+
expect(parseSSE('event: ping\n')).toBe('');
55+
});
56+
57+
it('stops on error and returns error message', () => {
58+
const text = 'data: "part1"\ndata: {"error": "fail"}\ndata: "part2"';
59+
expect(parseSSE(text)).toBe('Error: fail');
60+
});
61+
62+
it('handles single data line', () => {
63+
expect(parseSSE('data: "only line"')).toBe('only line');
64+
});
65+
66+
it('handles raw non-JSON data lines', () => {
67+
const text = 'data: hello\ndata: world';
68+
expect(parseSSE(text)).toBe('helloworld');
69+
});
70+
});
71+
72+
describe('extractResult', () => {
73+
it('extracts string result from JSON object', () => {
74+
expect(extractResult('{"result": "answer"}')).toBe('answer');
75+
});
76+
77+
it('stringifies non-string result', () => {
78+
const result = extractResult('{"result": {"key": "val"}}');
79+
expect(result).toContain('key');
80+
expect(result).toContain('val');
81+
});
82+
83+
it('returns plain string from JSON string', () => {
84+
expect(extractResult('"plain string"')).toBe('plain string');
85+
});
86+
87+
it('stringifies JSON object without result field', () => {
88+
const result = extractResult('{"data": 42}');
89+
expect(result).toContain('42');
90+
});
91+
92+
it('returns raw text for non-JSON input', () => {
93+
expect(extractResult('not json at all')).toBe('not json at all');
94+
});
95+
96+
it('handles empty string', () => {
97+
expect(extractResult('')).toBe('');
98+
});
99+
});

0 commit comments

Comments
 (0)