diff --git a/src/cli/commands/status/__tests__/action.test.ts b/src/cli/commands/status/__tests__/action.test.ts index f8c94c6bc..25520d9ce 100644 --- a/src/cli/commands/status/__tests__/action.test.ts +++ b/src/cli/commands/status/__tests__/action.test.ts @@ -18,6 +18,11 @@ vi.mock('../../../aws/agentcore-control', () => ({ getOnlineEvaluationConfig: (...args: unknown[]) => mockGetOnlineEvaluationConfig(...args), })); +const mockIsPreviewEnabled = vi.fn(() => true); +vi.mock('../../../feature-flags', () => ({ + isPreviewEnabled: () => mockIsPreviewEnabled(), +})); + vi.mock('../../../logging', () => { return { ExecLogger: class { @@ -409,6 +414,89 @@ describe('computeResourceStatuses', () => { expect(configEntry!.deploymentState).toBe('pending-removal'); }); + it('marks harness as deployed when in both local and deployed state', () => { + const project = { + ...baseProject, + harnesses: [{ name: 'my-harness', path: 'harnesses/my-harness' }], + } as unknown as AgentCoreProjectSpec; + + const resources: DeployedResourceState = { + harnesses: { + 'my-harness': { + harnessId: 'h-123', + harnessArn: 'arn:aws:bedrock:us-east-1:123456789:harness/h-123', + roleArn: 'arn:aws:iam::123456789:role/test', + status: 'ACTIVE', + }, + }, + }; + + const result = computeResourceStatuses(project, resources); + const harnessEntry = result.find(r => r.resourceType === 'harness' && r.name === 'my-harness'); + + expect(harnessEntry).toBeDefined(); + expect(harnessEntry!.deploymentState).toBe('deployed'); + expect(harnessEntry!.identifier).toBe('arn:aws:bedrock:us-east-1:123456789:harness/h-123'); + }); + + it('marks harness as local-only when not in deployed state', () => { + const project = { + ...baseProject, + harnesses: [{ name: 'my-harness', path: 'harnesses/my-harness' }], + } as unknown as AgentCoreProjectSpec; + + const result = computeResourceStatuses(project, undefined); + const harnessEntry = result.find(r => r.resourceType === 'harness' && r.name === 'my-harness'); + + expect(harnessEntry).toBeDefined(); + expect(harnessEntry!.deploymentState).toBe('local-only'); + }); + + it('marks harness as pending-removal when in deployed state but not in local schema', () => { + const resources: DeployedResourceState = { + harnesses: { + 'removed-harness': { + harnessId: 'h-456', + harnessArn: 'arn:aws:bedrock:us-east-1:123456789:harness/h-456', + roleArn: 'arn:aws:iam::123456789:role/test', + status: 'ACTIVE', + }, + }, + }; + + const result = computeResourceStatuses(baseProject, resources); + const harnessEntry = result.find(r => r.resourceType === 'harness' && r.name === 'removed-harness'); + + expect(harnessEntry).toBeDefined(); + expect(harnessEntry!.deploymentState).toBe('pending-removal'); + expect(harnessEntry!.identifier).toBe('arn:aws:bedrock:us-east-1:123456789:harness/h-456'); + }); + + it('does not include harnesses when preview is disabled', () => { + mockIsPreviewEnabled.mockReturnValueOnce(false); + + const project = { + ...baseProject, + harnesses: [{ name: 'my-harness', path: 'harnesses/my-harness' }], + } as unknown as AgentCoreProjectSpec; + + const resources: DeployedResourceState = { + harnesses: { + 'my-harness': { + harnessId: 'h-123', + harnessArn: 'arn:aws:bedrock:us-east-1:123456789:harness/h-123', + roleArn: 'arn:aws:iam::123456789:role/test', + status: 'ACTIVE', + }, + }, + }; + + const result = computeResourceStatuses(project, resources); + const harnessEntries = result.filter(r => r.resourceType === 'harness'); + + expect(harnessEntries).toHaveLength(0); + }); + it('handles mixed deployed and local-only resources', () => { const project = { ...baseProject, diff --git a/src/cli/commands/status/action.ts b/src/cli/commands/status/action.ts index cece9c03d..1432e2d1a 100644 --- a/src/cli/commands/status/action.ts +++ b/src/cli/commands/status/action.ts @@ -5,6 +5,7 @@ import { getAgentRuntimeStatus } from '../../aws'; import { getEvaluator, getOnlineEvaluationConfig } from '../../aws/agentcore-control'; import { dnsSuffix } from '../../aws/partition'; import { getErrorMessage } from '../../errors'; +import { isPreviewEnabled } from '../../feature-flags'; import { ExecLogger } from '../../logging'; import type { ResourceDeploymentState } from './constants'; import { buildRuntimeInvocationUrl } from './constants'; @@ -24,6 +25,7 @@ export interface ResourceStatusEntry { | 'config-bundle' | 'ab-test' | 'dataset' + | 'harness' | 'runtime-endpoint'; name: string; deploymentState: ResourceDeploymentState; @@ -296,6 +298,16 @@ export function computeResourceStatuses( getParentName: item => item.agentName, }); + const harnesses = isPreviewEnabled() + ? diffResourceSet({ + resourceType: 'harness', + localItems: project.harnesses ?? [], + deployedRecord: resources?.harnesses ?? {}, + getIdentifier: deployed => deployed.harnessArn, + getLocalDetail: () => undefined, + }) + : []; + return [ ...agents, ...runtimeEndpoints, @@ -309,6 +321,7 @@ export function computeResourceStatuses( ...datasets, ...configBundles, ...abTests, + ...harnesses, ]; } diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index eacc21943..94ed5328a 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -1,5 +1,6 @@ import { ValidationError, serializeResult } from '../../../lib'; import { getErrorMessage } from '../../errors'; +import { isPreviewEnabled } from '../../feature-flags'; import { getDatasetStatus } from '../../operations/dataset'; import type { DatasetStatusResult } from '../../operations/dataset'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; @@ -25,6 +26,7 @@ const VALID_RESOURCE_TYPES = [ 'config-bundle', 'ab-test', 'dataset', + ...(isPreviewEnabled() ? (['harness'] as const) : []), ] as const; const VALID_STATES = ['deployed', 'local-only', 'pending-removal'] as const; @@ -65,10 +67,7 @@ export const registerStatus = (program: Command) => { .description(COMMAND_DESCRIPTIONS.status) .option('--runtime-id ', 'Look up a specific runtime by ID') .option('--target ', 'Select deployment target') - .option( - '--type ', - 'Filter by resource type (agent, runtime-endpoint, memory, credential, gateway, evaluator, online-eval, policy-engine, policy, config-bundle, ab-test, dataset)' - ) + .option('--type ', `Filter by resource type (${VALID_RESOURCE_TYPES.join(', ')})`) .option('--state ', 'Filter by deployment state (deployed, local-only, pending-removal)') .option('--runtime ', 'Filter to a specific runtime') .option('--json', 'Output as JSON') @@ -170,6 +169,7 @@ export const registerStatus = (program: Command) => { const configBundles = filtered.filter(r => r.resourceType === 'config-bundle'); const abTests = filtered.filter(r => r.resourceType === 'ab-test'); const datasets = filtered.filter(r => r.resourceType === 'dataset'); + const harnesses = filtered.filter(r => r.resourceType === 'harness'); // TODO: Add http-gateway resource type when diffResourceSet for HTTP gateways is added to action.ts // Fetch enriched dataset info when --type dataset is specified @@ -381,6 +381,15 @@ export const registerStatus = (program: Command) => { {/* TODO: Add HTTP Gateways render section when diffResourceSet is added to action.ts */} + {harnesses.length > 0 && ( + + Harnesses + {harnesses.map(entry => ( + + ))} + + )} + {filtered.length === 0 && No resources match the given filters.} ); diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index e3aa86bad..552abfd5d 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -67,6 +67,8 @@ export const FilterType = z.enum([ 'policy', 'config-bundle', 'ab-test', + 'dataset', + 'harness', 'none', ]); export const AgentFramework = z.enum(['strands', 'langchain_langgraph', 'googleadk', 'openaiagents']); diff --git a/src/cli/tui/components/ResourceGraph.tsx b/src/cli/tui/components/ResourceGraph.tsx index 1295624f7..d6e6a3d04 100644 --- a/src/cli/tui/components/ResourceGraph.tsx +++ b/src/cli/tui/components/ResourceGraph.tsx @@ -23,6 +23,7 @@ const ICONS = { 'config-bundle': '⬡', 'ab-test': '⚗', dataset: '▤', + harness: '⬢', 'runtime-endpoint': '◉', } as const;