Skip to content

Commit 60f8c1b

Browse files
committed
chore: add integration tests
1 parent 4a74be6 commit 60f8c1b

13 files changed

Lines changed: 1163 additions & 0 deletions

AWS.AgentCore.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
<Folder Name="/test/">
1616
<Project Path="test/AWS.AgentCore.UnitTests/AWS.AgentCore.UnitTests.csproj" />
1717
<Project Path="test/AWS.AgentCore.SourceGenerator.UnitTests/AWS.AgentCore.SourceGenerator.UnitTests.csproj" />
18+
<Project Path="test/AWS.AgentCore.IntegrationTests/AWS.AgentCore.IntegrationTests.csproj" />
1819
</Folder>
1920
</Solution>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="AWSSDK.BedrockAgentCore" Version="4.0.20.1" />
13+
<PackageReference Include="AWSSDK.BedrockAgentCoreControl" Version="4.0.30" />
14+
<PackageReference Include="AWSSDK.ECR" Version="4.0.13.3" />
15+
<PackageReference Include="AWSSDK.IdentityManagement" Version="4.0.9.20" />
16+
<PackageReference Include="AWSSDK.SecurityToken" Version="4.0.6.2" />
17+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
18+
<PackageReference Include="xunit.v3" Version="3.2.2" />
19+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using AWS.AgentCore.IntegrationTests.Infrastructure;
5+
6+
namespace AWS.AgentCore.IntegrationTests.Fixtures;
7+
8+
public class MicrosoftAgentFrameworkFixture : SampleAppFixture
9+
{
10+
public MicrosoftAgentFrameworkFixture() : base("MicrosoftAgentFrameworkSample") { }
11+
}
12+
13+
public class AnnotationsSampleFixture : SampleAppFixture
14+
{
15+
public AnnotationsSampleFixture() : base("AnnotationsSample") { }
16+
}
17+
18+
public class StreamingAgentFixture : SampleAppFixture
19+
{
20+
public StreamingAgentFixture() : base("StreamingAgent") { }
21+
}
22+
23+
public class AnnotationsStreamingAgentFixture : SampleAppFixture
24+
{
25+
public AnnotationsStreamingAgentFixture() : base("AnnotationsStreamingAgent") { }
26+
}
27+
28+
public class NativeAotExtensionsFixture : SampleAppFixture
29+
{
30+
public NativeAotExtensionsFixture() : base("NativeAotExtensions") { }
31+
}
32+
33+
public class NativeAotAnnotationsFixture : SampleAppFixture
34+
{
35+
public NativeAotAnnotationsFixture() : base("NativeAotAnnotations") { }
36+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
global using Xunit;
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using System.Text;
5+
using System.Text.Json;
6+
using Amazon;
7+
using Amazon.BedrockAgentCore;
8+
using Amazon.BedrockAgentCore.Model;
9+
10+
namespace AWS.AgentCore.IntegrationTests.Infrastructure;
11+
12+
/// <summary>
13+
/// Invokes AgentCore Runtime agents and parses responses.
14+
/// Supports both standard (JSON) and streaming (SSE) invocations.
15+
/// </summary>
16+
public sealed class AgentCoreInvoker : IDisposable
17+
{
18+
private readonly AmazonBedrockAgentCoreClient _client;
19+
20+
public AgentCoreInvoker(string region)
21+
{
22+
_client = new AmazonBedrockAgentCoreClient(RegionEndpoint.GetBySystemName(region));
23+
}
24+
25+
/// <summary>
26+
/// Invokes a non-streaming agent and returns the parsed message from the JSON response.
27+
/// </summary>
28+
public async Task<InvocationResult> InvokeAsync(string runtimeArn, string prompt, CancellationToken ct = default)
29+
{
30+
var payload = JsonSerializer.Serialize(new { prompt });
31+
32+
var request = new InvokeAgentRuntimeRequest
33+
{
34+
AgentRuntimeArn = runtimeArn,
35+
Payload = new MemoryStream(Encoding.UTF8.GetBytes(payload)),
36+
ContentType = "application/json",
37+
Accept = "application/json",
38+
};
39+
40+
var response = await _client.InvokeAgentRuntimeAsync(request, ct);
41+
42+
using var reader = new StreamReader(response.Response);
43+
var responseBody = await reader.ReadToEndAsync(ct);
44+
45+
string? message = null;
46+
try
47+
{
48+
using var doc = JsonDocument.Parse(responseBody);
49+
if (doc.RootElement.TryGetProperty("message", out var messageProp))
50+
message = messageProp.GetString();
51+
}
52+
catch (JsonException)
53+
{
54+
// Not JSON — use raw body
55+
}
56+
57+
return new InvocationResult
58+
{
59+
RawBody = responseBody,
60+
Message = message ?? responseBody,
61+
HttpStatusCode = (int)response.HttpStatusCode,
62+
};
63+
}
64+
65+
/// <summary>
66+
/// Invokes a streaming agent and collects all SSE chunks into a result.
67+
/// </summary>
68+
public async Task<StreamingInvocationResult> InvokeStreamingAsync(
69+
string runtimeArn, string prompt, CancellationToken ct = default)
70+
{
71+
var payload = JsonSerializer.Serialize(new { prompt });
72+
73+
var request = new InvokeAgentRuntimeRequest
74+
{
75+
AgentRuntimeArn = runtimeArn,
76+
Payload = new MemoryStream(Encoding.UTF8.GetBytes(payload)),
77+
ContentType = "application/json",
78+
Accept = "text/event-stream",
79+
};
80+
81+
var response = await _client.InvokeAgentRuntimeAsync(request, ct);
82+
83+
var chunks = new List<string>();
84+
string? finalMessage = null;
85+
86+
using var reader = new StreamReader(response.Response);
87+
while (true)
88+
{
89+
ct.ThrowIfCancellationRequested();
90+
91+
var line = await reader.ReadLineAsync(ct);
92+
if (line is null) break;
93+
if (!line.StartsWith("data: ")) continue;
94+
95+
var json = line["data: ".Length..];
96+
try
97+
{
98+
using var doc = JsonDocument.Parse(json);
99+
100+
if (doc.RootElement.TryGetProperty("done", out var doneProp) && doneProp.GetBoolean())
101+
{
102+
if (doc.RootElement.TryGetProperty("message", out var msgProp))
103+
finalMessage = msgProp.GetString();
104+
break;
105+
}
106+
107+
if (doc.RootElement.TryGetProperty("chunk", out var chunkProp))
108+
{
109+
var chunk = chunkProp.GetString();
110+
if (!string.IsNullOrEmpty(chunk))
111+
chunks.Add(chunk);
112+
}
113+
}
114+
catch (JsonException)
115+
{
116+
// Skip malformed SSE events
117+
}
118+
}
119+
120+
return new StreamingInvocationResult
121+
{
122+
Chunks = chunks,
123+
FinalMessage = finalMessage ?? string.Concat(chunks),
124+
HttpStatusCode = (int)response.HttpStatusCode,
125+
};
126+
}
127+
128+
public void Dispose() => _client.Dispose();
129+
}
130+
131+
public class InvocationResult
132+
{
133+
public string RawBody { get; set; } = "";
134+
public string Message { get; set; } = "";
135+
public int HttpStatusCode { get; set; }
136+
}
137+
138+
public class StreamingInvocationResult
139+
{
140+
public List<string> Chunks { get; set; } = new();
141+
public string FinalMessage { get; set; } = "";
142+
public int HttpStatusCode { get; set; }
143+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
using Amazon;
5+
using Amazon.BedrockAgentCoreControl;
6+
using Amazon.BedrockAgentCoreControl.Model;
7+
8+
namespace AWS.AgentCore.IntegrationTests.Infrastructure;
9+
10+
/// <summary>
11+
/// Creates and manages AgentCore Runtime resources for integration tests.
12+
/// </summary>
13+
public sealed class AgentCoreRuntimeHelper : IAsyncDisposable
14+
{
15+
private readonly AmazonBedrockAgentCoreControlClient _client;
16+
private readonly List<string> _createdRuntimeArns = new();
17+
18+
public AgentCoreRuntimeHelper(string region)
19+
{
20+
_client = new AmazonBedrockAgentCoreControlClient(RegionEndpoint.GetBySystemName(region));
21+
}
22+
23+
/// <summary>
24+
/// Creates an AgentCore Runtime backed by the given ECR image.
25+
/// Waits for the runtime to reach READY status before returning.
26+
/// </summary>
27+
/// <returns>The runtime ARN.</returns>
28+
public async Task<string> CreateRuntimeAsync(
29+
string runtimeName,
30+
string ecrImageUri,
31+
string roleArn,
32+
TimeSpan? timeout = null,
33+
CancellationToken ct = default)
34+
{
35+
var response = await _client.CreateAgentRuntimeAsync(new CreateAgentRuntimeRequest
36+
{
37+
AgentRuntimeName = runtimeName,
38+
RoleArn = roleArn,
39+
AgentRuntimeArtifact = new AgentRuntimeArtifact
40+
{
41+
ContainerConfiguration = new ContainerConfiguration
42+
{
43+
ContainerUri = ecrImageUri,
44+
}
45+
},
46+
NetworkConfiguration = new NetworkConfiguration
47+
{
48+
NetworkMode = NetworkMode.PUBLIC,
49+
},
50+
Tags = new Dictionary<string, string>
51+
{
52+
["CreatedBy"] = "IntegrationTests",
53+
["TestRunId"] = TestConfiguration.TestRunId,
54+
}
55+
}, ct);
56+
57+
var runtimeArn = response.AgentRuntimeArn;
58+
_createdRuntimeArns.Add(runtimeArn);
59+
60+
await WaitForRuntimeReadyAsync(runtimeArn, timeout ?? TimeSpan.FromMinutes(5), ct);
61+
62+
return runtimeArn;
63+
}
64+
65+
/// <summary>
66+
/// Polls the runtime status until it reaches READY or fails/times out.
67+
/// </summary>
68+
private async Task WaitForRuntimeReadyAsync(
69+
string runtimeArn,
70+
TimeSpan timeout,
71+
CancellationToken ct)
72+
{
73+
var deadline = DateTime.UtcNow + timeout;
74+
var runtimeId = ExtractRuntimeId(runtimeArn);
75+
var pollCount = 0;
76+
77+
while (DateTime.UtcNow < deadline)
78+
{
79+
ct.ThrowIfCancellationRequested();
80+
81+
var response = await _client.GetAgentRuntimeAsync(new GetAgentRuntimeRequest
82+
{
83+
AgentRuntimeId = runtimeId,
84+
}, ct);
85+
86+
var status = response.Status;
87+
pollCount++;
88+
89+
Console.WriteLine($"[AgentCore] Runtime {runtimeId} status: {status?.Value ?? "unknown"} (poll #{pollCount})");
90+
91+
if (status == AgentRuntimeStatus.READY)
92+
return;
93+
94+
if (status == AgentRuntimeStatus.CREATE_FAILED || status == AgentRuntimeStatus.UPDATE_FAILED)
95+
{
96+
var reason = response.FailureReason ?? "Unknown";
97+
throw new InvalidOperationException(
98+
$"Runtime {runtimeArn} entered {status.Value} status. Failure reason: {reason}");
99+
}
100+
101+
await Task.Delay(TimeSpan.FromSeconds(10), ct);
102+
}
103+
104+
throw new TimeoutException(
105+
$"Runtime {runtimeArn} did not reach READY within {timeout.TotalMinutes} minutes.");
106+
}
107+
108+
/// <summary>
109+
/// Deletes all runtimes created during this test run.
110+
/// </summary>
111+
public async ValueTask DisposeAsync()
112+
{
113+
foreach (var arn in _createdRuntimeArns)
114+
{
115+
try
116+
{
117+
await _client.DeleteAgentRuntimeAsync(new DeleteAgentRuntimeRequest
118+
{
119+
AgentRuntimeId = ExtractRuntimeId(arn),
120+
});
121+
}
122+
catch
123+
{
124+
// Best-effort cleanup
125+
}
126+
}
127+
128+
_client.Dispose();
129+
}
130+
131+
/// <summary>
132+
/// Extracts the runtime ID from an ARN (last segment after the final /).
133+
/// </summary>
134+
private static string ExtractRuntimeId(string arn) => arn.Split('/').Last();
135+
}

0 commit comments

Comments
 (0)