Skip to content

Commit ddca71b

Browse files
fix(harness): resolve memorySpec by deployed ARN for arn-only memory refs (#1407)
* fix(harness): resolve memorySpec by deployed ARN for arn-only memory refs When a harness references memory by ARN only (no name field), the previous lookup returned undefined, silently omitting retrievalConfig and excluding the memory from the configHash. resolveMemorySpec now walks deployedResources.memories to match by ARN and find the corresponding projectSpec memory, so name-less refs that point at a CLI-managed memory get the same treatment as name-based refs. HarnessMemoryRef is exported from the schema barrel so it can be used as the explicit parameter type on resolveMemorySpec. Adds unit tests for both the ARN-match path and the intentional undefined fallback for genuinely external memories. * fix(e2e): accept DELETE_FAILED as valid terminal state in harness teardown test The harness deletion can end in DELETE_FAILED due to backend issues unrelated to the CLI teardown logic. The e2e test should verify the deletion was attempted, not that the backend successfully completed it. * revert: keep DELETE_FAILED as a test failure The IAM permission fix (adding DeleteAgentRuntimeEndpoint) addresses the root cause. The e2e test should still fail on DELETE_FAILED to catch real issues.
1 parent eeaf4b6 commit ddca71b

3 files changed

Lines changed: 130 additions & 3 deletions

File tree

src/cli/operations/deploy/imperative/deployers/__tests__/harness-deployer.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,112 @@ describe('HarnessDeployer', () => {
313313
});
314314
});
315315

316+
describe('memorySpec resolution', () => {
317+
const ROLE_ARN = 'arn:aws:iam::123456789012:role/HarnessRole';
318+
const MEMORY_ARN = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/mem-123';
319+
const CDK_OUTPUTS = { ApplicationHarnessMyHarnessRoleArnOutput123: ROLE_ARN };
320+
const READY_HARNESS = {
321+
harnessId: 'h-new',
322+
harnessName: 'my_harness',
323+
arn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:harness/h-new',
324+
status: 'READY' as const,
325+
executionRoleArn: ROLE_ARN,
326+
createdAt: '2024-01-01T00:00:00Z',
327+
updatedAt: '2024-01-01T00:00:00Z',
328+
};
329+
330+
const HARNESS_SPEC_WITH_MEMORY_ARN_JSON = JSON.stringify({
331+
name: 'my_harness',
332+
model: { provider: 'bedrock', modelId: 'anthropic.claude-3-sonnet-20240229-v1:0' },
333+
tools: [],
334+
skills: [],
335+
memory: { arn: MEMORY_ARN },
336+
});
337+
338+
it('resolves memorySpec by deployed ARN when memory.name is absent', async () => {
339+
const { readFile: mockedReadFile } = await import('fs/promises');
340+
const { mapHarnessSpecToCreateOptions: mockedMapHarness } = await import('../harness-mapper');
341+
342+
vi.mocked(mockedReadFile)
343+
.mockResolvedValueOnce(HARNESS_SPEC_WITH_MEMORY_ARN_JSON as any)
344+
.mockRejectedValueOnce(new Error('ENOENT'));
345+
vi.mocked(mockedMapHarness).mockResolvedValueOnce({
346+
region: 'us-east-1',
347+
harnessName: 'my_harness',
348+
executionRoleArn: ROLE_ARN,
349+
} as any);
350+
vi.mocked(createHarness).mockResolvedValueOnce({
351+
harness: READY_HARNESS,
352+
} as any);
353+
354+
const ctx = makeContext({
355+
projectSpec: {
356+
name: 'proj',
357+
harnesses: [{ name: 'my_harness', path: 'harnesses/my_harness' }],
358+
memories: [
359+
{
360+
name: 'my_memory',
361+
eventExpiryDuration: 30,
362+
strategies: [{ type: 'SEMANTIC', namespaces: ['/users/{actorId}/facts'] }],
363+
},
364+
],
365+
} as any,
366+
deployedState: {
367+
targets: {
368+
dev: {
369+
resources: {
370+
memories: { my_memory: { memoryId: 'mem-123', memoryArn: MEMORY_ARN } },
371+
},
372+
},
373+
},
374+
} as any,
375+
cdkOutputs: CDK_OUTPUTS,
376+
});
377+
378+
await deployer.deploy(ctx);
379+
380+
expect(mockedMapHarness).toHaveBeenCalledWith(
381+
expect.objectContaining({
382+
memorySpec: {
383+
name: 'my_memory',
384+
eventExpiryDuration: 30,
385+
strategies: [{ type: 'SEMANTIC', namespaces: ['/users/{actorId}/facts'] }],
386+
},
387+
})
388+
);
389+
});
390+
391+
it('returns undefined memorySpec for a fully external ARN not in deployedResources', async () => {
392+
const { readFile: mockedReadFile } = await import('fs/promises');
393+
const { mapHarnessSpecToCreateOptions: mockedMapHarness } = await import('../harness-mapper');
394+
395+
vi.mocked(mockedReadFile)
396+
.mockResolvedValueOnce(HARNESS_SPEC_WITH_MEMORY_ARN_JSON as any)
397+
.mockRejectedValueOnce(new Error('ENOENT'));
398+
vi.mocked(mockedMapHarness).mockResolvedValueOnce({
399+
region: 'us-east-1',
400+
harnessName: 'my_harness',
401+
executionRoleArn: ROLE_ARN,
402+
} as any);
403+
vi.mocked(createHarness).mockResolvedValueOnce({
404+
harness: READY_HARNESS,
405+
} as any);
406+
407+
const ctx = makeContext({
408+
projectSpec: {
409+
name: 'proj',
410+
harnesses: [{ name: 'my_harness', path: 'harnesses/my_harness' }],
411+
memories: [],
412+
} as any,
413+
cdkOutputs: CDK_OUTPUTS,
414+
});
415+
416+
await deployer.deploy(ctx);
417+
418+
expect(mockedMapHarness).toHaveBeenCalledWith(expect.objectContaining({ memorySpec: undefined }));
419+
});
420+
});
421+
316422
describe('teardown', () => {
317423
it('deletes all deployed harnesses', async () => {
318424
const ctx = makeContext({

src/cli/operations/deploy/imperative/deployers/harness-deployer.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
* via the SigV4 API client. Harness role ARNs are resolved from CDK
66
* stack outputs, and harness specs are read from disk (harness.json).
77
*/
8-
import type { HarnessDeployedState, HarnessSpec, Memory } from '../../../../../schema';
8+
import type { HarnessDeployedState, HarnessMemoryRef, HarnessSpec, Memory } from '../../../../../schema';
99
import { HarnessSpecSchema } from '../../../../../schema';
10+
import type { DeployedResourceState } from '../../../../../schema/schemas/deployed-state';
1011
import type {
1112
CreateHarnessResult,
1213
Harness,
@@ -61,6 +62,20 @@ async function computeHarnessHash(
6162
return hash.digest('hex').slice(0, 16);
6263
}
6364

65+
function resolveMemorySpec(
66+
memories: Memory[] | undefined,
67+
memoryRef: HarnessMemoryRef | undefined,
68+
deployedResources: DeployedResourceState | undefined
69+
): Memory | undefined {
70+
if (!memoryRef) return undefined;
71+
if (memoryRef.name) return memories?.find(m => m.name === memoryRef.name);
72+
if (memoryRef.arn && deployedResources?.memories) {
73+
const entry = Object.entries(deployedResources.memories).find(([, v]) => v.memoryArn === memoryRef.arn);
74+
if (entry) return memories?.find(m => m.name === entry[0]);
75+
}
76+
return undefined;
77+
}
78+
6479
// ============================================================================
6580
// Deployer
6681
// ============================================================================
@@ -140,7 +155,7 @@ export class HarnessDeployer implements ImperativeDeployer<HarnessDeployedStateM
140155

141156
const deployedResources = deployedState.targets?.[targetName]?.resources;
142157
const existingHarness = deployedHarnesses[entry.name];
143-
const memorySpec = projectSpec.memories?.find(m => m.name === harnessSpec.memory?.name);
158+
const memorySpec = resolveMemorySpec(projectSpec.memories, harnessSpec.memory, deployedResources);
144159

145160
const configHash = await computeHarnessHash(harnessDir, harnessSpec, executionRoleArn, memorySpec);
146161

src/schema/schemas/agentcore-project.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ export type { ABTestMode, TargetRef, GatewayFilter, PerVariantOnlineEvaluationCo
7878
export { ABTestModeSchema, TargetRefSchema, GatewayFilterSchema } from './primitives/ab-test';
7979
export type { HttpGatewayTarget } from './primitives/http-gateway';
8080
export { HttpGatewayTargetSchema } from './primitives/http-gateway';
81-
export type { HarnessGatewayOutboundAuth, HarnessModel, HarnessSpec, HarnessModelProvider } from './primitives/harness';
81+
export type {
82+
HarnessGatewayOutboundAuth,
83+
HarnessMemoryRef,
84+
HarnessModel,
85+
HarnessSpec,
86+
HarnessModelProvider,
87+
} from './primitives/harness';
8288
export {
8389
GatewayOAuthGrantTypeSchema,
8490
HarnessGatewayOutboundAuthSchema,

0 commit comments

Comments
 (0)