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
174 changes: 174 additions & 0 deletions src/cli/commands/status/__tests__/action.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AgentCoreProjectSpec, DeployedResourceState } from '../../../../schema/index.js';
import { computeResourceStatuses, handleProjectStatus } from '../action.js';
import type { StatusContext } from '../action.js';
import { buildRuntimeInvocationUrl } from '../constants.js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const mockGetAgentRuntimeStatus = vi.fn();
Expand Down Expand Up @@ -631,3 +632,176 @@ describe('handleProjectStatus — live enrichment', () => {
expect(mockGetEvaluator).not.toHaveBeenCalled();
});
});

describe('buildRuntimeInvocationUrl', () => {
it('constructs the correct invocation URL with encoded ARN', () => {
const url = buildRuntimeInvocationUrl(
'us-east-1',
'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/travelplanner_FlightsMcp-abcdefgh'
);
expect(url).toBe(
'https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aus-east-1%3A123456789012%3Aruntime%2Ftravelplanner_FlightsMcp-abcdefgh/invocations'
);
});

it('handles different regions', () => {
const url = buildRuntimeInvocationUrl(
'eu-west-1',
'arn:aws:bedrock-agentcore:eu-west-1:111111111111:runtime/my-agent-xyz'
);
expect(url).toBe(
'https://bedrock-agentcore.eu-west-1.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aeu-west-1%3A111111111111%3Aruntime%2Fmy-agent-xyz/invocations'
);
});
});

describe('handleProjectStatus — invocation URL enrichment', () => {
beforeEach(() => {
mockGetAgentRuntimeStatus.mockReset();
mockGetEvaluator.mockReset();
mockGetOnlineEvaluationConfig.mockReset();
});

afterEach(() => vi.clearAllMocks());

it('sets invocationUrl on deployed agents after runtime status enrichment', async () => {
const runtimeArn = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/proj_MyAgent-abc123';

mockGetAgentRuntimeStatus.mockResolvedValue({
runtimeId: 'proj_MyAgent-abc123',
status: 'READY',
});

const ctx: StatusContext = {
project: {
...baseProject,
runtimes: [{ name: 'MyAgent' }],
} as unknown as AgentCoreProjectSpec,
awsTargets: [{ name: 'dev', region: 'us-east-1', account: '123456789012' }],
deployedState: {
targets: {
dev: {
resources: {
runtimes: {
MyAgent: {
runtimeId: 'proj_MyAgent-abc123',
runtimeArn,
roleArn: 'arn:aws:iam::123456789012:role/test',
},
},
},
},
},
},
} as unknown as StatusContext;

const result = await handleProjectStatus(ctx);

expect(result.success).toBe(true);
const agentEntry = result.resources.find(r => r.resourceType === 'agent' && r.name === 'MyAgent');
expect(agentEntry).toBeDefined();
expect(agentEntry!.invocationUrl).toBe(
`https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/${encodeURIComponent(runtimeArn)}/invocations`
);
});

it('does not set invocationUrl on local-only agents', async () => {
const ctx: StatusContext = {
project: {
...baseProject,
runtimes: [{ name: 'LocalAgent' }],
} as unknown as AgentCoreProjectSpec,
awsTargets: [{ name: 'dev', region: 'us-east-1', account: '123456789012' }],
deployedState: {
targets: {
dev: {
resources: {},
},
},
},
} as unknown as StatusContext;

const result = await handleProjectStatus(ctx);

expect(result.success).toBe(true);
const agentEntry = result.resources.find(r => r.resourceType === 'agent' && r.name === 'LocalAgent');
expect(agentEntry).toBeDefined();
expect(agentEntry!.invocationUrl).toBeUndefined();
});

it('still sets invocationUrl when runtime status fetch fails', async () => {
const runtimeArn = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/proj_FailAgent-xyz';
mockGetAgentRuntimeStatus.mockRejectedValue(new Error('Timeout'));

const ctx: StatusContext = {
project: {
...baseProject,
runtimes: [{ name: 'FailAgent' }],
} as unknown as AgentCoreProjectSpec,
awsTargets: [{ name: 'dev', region: 'us-east-1', account: '123456789012' }],
deployedState: {
targets: {
dev: {
resources: {
runtimes: {
FailAgent: {
runtimeId: 'proj_FailAgent-xyz',
runtimeArn,
roleArn: 'arn:aws:iam::123456789012:role/test',
},
},
},
},
},
},
} as unknown as StatusContext;

const result = await handleProjectStatus(ctx);

expect(result.success).toBe(true);
const agentEntry = result.resources.find(r => r.resourceType === 'agent' && r.name === 'FailAgent');
expect(agentEntry).toBeDefined();
expect(agentEntry!.error).toBe('Timeout');
expect(agentEntry!.invocationUrl).toBe(
`https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/${encodeURIComponent(runtimeArn)}/invocations`
);
});

it('does not set invocationUrl on pending-removal agents', async () => {
mockGetAgentRuntimeStatus.mockResolvedValue({
runtimeId: 'proj_OldAgent-abc',
status: 'READY',
});

const ctx: StatusContext = {
project: {
...baseProject,
runtimes: [],
} as unknown as AgentCoreProjectSpec,
awsTargets: [{ name: 'dev', region: 'us-east-1', account: '123456789012' }],
deployedState: {
targets: {
dev: {
resources: {
runtimes: {
OldAgent: {
runtimeId: 'proj_OldAgent-abc',
runtimeArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/proj_OldAgent-abc',
roleArn: 'arn:aws:iam::123456789012:role/test',
},
},
},
},
},
},
} as unknown as StatusContext;

const result = await handleProjectStatus(ctx);

expect(result.success).toBe(true);
const agentEntry = result.resources.find(r => r.resourceType === 'agent' && r.name === 'OldAgent');
expect(agentEntry).toBeDefined();
expect(agentEntry!.deploymentState).toBe('pending-removal');
expect(agentEntry!.invocationUrl).toBeUndefined();
});
});
10 changes: 8 additions & 2 deletions src/cli/commands/status/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getEvaluator, getOnlineEvaluationConfig } from '../../aws/agentcore-con
import { getErrorMessage } from '../../errors';
import { ExecLogger } from '../../logging';
import type { ResourceDeploymentState } from './constants';
import { buildRuntimeInvocationUrl } from './constants';

export type { ResourceDeploymentState };

Expand All @@ -23,6 +24,7 @@ export interface ResourceStatusEntry {
identifier?: string;
detail?: string;
error?: string;
invocationUrl?: string;
}

export interface ProjectStatusResult {
Expand Down Expand Up @@ -284,16 +286,20 @@ export async function handleProjectStatus(
const agentState = agentStates[entry.name];
if (!agentState) return;

const invocationUrl = entry.identifier
? buildRuntimeInvocationUrl(targetConfig.region, entry.identifier)
: undefined;

try {
const runtimeStatus = await getAgentRuntimeStatus({
region: targetConfig.region,
runtimeId: agentState.runtimeId,
});
resources[i] = { ...entry, detail: runtimeStatus.status };
resources[i] = { ...entry, detail: runtimeStatus.status, invocationUrl };
logger.log(` ${entry.name}: ${runtimeStatus.status} (${agentState.runtimeId})`);
} catch (error) {
const errorMsg = getErrorMessage(error);
resources[i] = { ...entry, error: errorMsg };
resources[i] = { ...entry, error: errorMsg, invocationUrl };
logger.log(` ${entry.name}: ERROR - ${errorMsg}`, 'error');
}
})
Expand Down
9 changes: 8 additions & 1 deletion src/cli/commands/status/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,14 @@
<Box flexDirection="column" marginTop={1}>
<Text bold>Agents</Text>
{agents.map(entry => (
<ResourceEntry key={`${entry.resourceType}-${entry.name}`} entry={entry} showRuntime />
<Box key={`${entry.resourceType}-${entry.name}`} flexDirection="column">
<ResourceEntry entry={entry} showRuntime />
{entry.invocationUrl && (
<Text dimColor>
{' '}URL: {entry.invocationUrl}
</Text>
)}
</Box>
))}
</Box>
)}
Expand Down Expand Up @@ -232,7 +239,7 @@
});
};

function ResourceEntry({ entry, showRuntime }: { entry: ResourceStatusEntry; showRuntime?: boolean }) {

Check warning on line 242 in src/cli/commands/status/command.tsx

View workflow job for this annotation

GitHub Actions / lint

Fast refresh only works when a file only exports components. Move your component(s) to a separate file. If all exports are HOCs, add them to the `extraHOCs` option
return (
<Text>
{' '}
Expand Down
5 changes: 5 additions & 0 deletions src/cli/commands/status/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ export const DEPLOYMENT_STATE_LABELS: Record<ResourceDeploymentState, string> =
'local-only': 'Local only',
'pending-removal': 'Removed locally',
};

export function buildRuntimeInvocationUrl(region: string, runtimeArn: string): string {
const encodedArn = encodeURIComponent(runtimeArn);
return `https://bedrock-agentcore.${region}.amazonaws.com/runtimes/${encodedArn}/invocations`;
}
8 changes: 8 additions & 0 deletions src/cli/tui/components/ResourceGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
statusColor,
deploymentState,
identifier,
invocationUrl,
}: {
icon: string;
color: string;
Expand All @@ -81,6 +82,7 @@
statusColor?: string;
deploymentState?: ResourceStatusEntry['deploymentState'];
identifier?: string;
invocationUrl?: string;
}) {
const badge = deploymentState ? getDeploymentBadge(deploymentState) : undefined;

Expand All @@ -98,11 +100,16 @@
{' '}ID: {identifier}
</Text>
)}
{invocationUrl && (
<Text dimColor>
{' '}URL: {invocationUrl}
</Text>
)}
</Box>
);
}

export function getTargetDisplayText(target: AgentCoreGatewayTarget): string {

Check warning on line 112 in src/cli/tui/components/ResourceGraph.tsx

View workflow job for this annotation

GitHub Actions / lint

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
if (target.targetType === 'mcpServer' && target.endpoint) return target.endpoint;
if (target.targetType === 'apiGateway' && target.apiGateway)
return `${target.apiGateway.restApiId}/${target.apiGateway.stage}`;
Expand Down Expand Up @@ -182,6 +189,7 @@
statusColor={runtimeStatusColor}
deploymentState={rsEntry?.deploymentState}
identifier={rsEntry?.identifier}
invocationUrl={rsEntry?.invocationUrl}
/>
);
})}
Expand Down
Loading