File: Commands\AgentTelemetryCommandTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using Aspire.Cli.Commands;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Tests.Telemetry;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Cli.Tests.Commands;
 
public class AgentTelemetryCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task AgentTelemetry_EmitsReportedActivityWithProvidedTags()
    {
        var (capturedActivities, listener) = CreateCapturingListener(out var reportedSourceName);
        using (listener)
        {
            using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
            var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
            {
                options.TelemetryFactory = _ => TestTelemetryHelper.CreateInitializedTelemetry(reportedSourceName, $"Diag.{Path.GetRandomFileName()}");
            });
            using var provider = services.BuildServiceProvider();
 
            var command = provider.GetRequiredService<RootCommand>();
            var result = command.Parse("agent telemetry --event-type skill_invocation --client-name copilot-cli --session-id 11111111-1111-1111-1111-111111111111 --skill-name aspire --timestamp 2026-01-01T00:00:00Z");
 
            var exitCode = await result.InvokeAsync().DefaultTimeout();
 
            Assert.Equal(CliExitCodes.Success, exitCode);
 
            var activity = Assert.Single(capturedActivities);
            Assert.Equal(TelemetryConstants.Activities.AgentTelemetry, activity.OperationName);
 
            var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value);
            Assert.Equal("skill_invocation", tags[TelemetryConstants.Tags.AgentEventType]);
            Assert.Equal("copilot-cli", tags[TelemetryConstants.Tags.AgentClientName]);
            Assert.Equal("11111111-1111-1111-1111-111111111111", tags[TelemetryConstants.Tags.AgentSessionId]);
            Assert.Equal("aspire", tags[TelemetryConstants.Tags.AgentSkillName]);
            Assert.Equal("2026-01-01T00:00:00Z", tags[TelemetryConstants.Tags.AgentEventTimestamp]);
        }
    }
 
    [Fact]
    public async Task AgentTelemetry_DoesNotEmitTagsForMissingOptions()
    {
        var (capturedActivities, listener) = CreateCapturingListener(out var reportedSourceName);
        using (listener)
        {
            using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
            var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
            {
                options.TelemetryFactory = _ => TestTelemetryHelper.CreateInitializedTelemetry(reportedSourceName, $"Diag.{Path.GetRandomFileName()}");
            });
            using var provider = services.BuildServiceProvider();
 
            var command = provider.GetRequiredService<RootCommand>();
            var result = command.Parse("agent telemetry --event-type tool_invocation --tool-name aspire-list_resources");
 
            var exitCode = await result.InvokeAsync().DefaultTimeout();
 
            Assert.Equal(CliExitCodes.Success, exitCode);
 
            var activity = Assert.Single(capturedActivities);
            var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value);
            Assert.Equal("tool_invocation", tags[TelemetryConstants.Tags.AgentEventType]);
            Assert.Equal("aspire-list_resources", tags[TelemetryConstants.Tags.AgentToolName]);
            Assert.False(tags.ContainsKey(TelemetryConstants.Tags.AgentSkillName));
            Assert.False(tags.ContainsKey(TelemetryConstants.Tags.AgentFileReference));
            Assert.False(tags.ContainsKey(TelemetryConstants.Tags.AgentClientName));
        }
    }
 
    [Fact]
    public async Task AgentTelemetry_DropsOverlongAndUnsafeValues()
    {
        var (capturedActivities, listener) = CreateCapturingListener(out var reportedSourceName);
        using (listener)
        {
            using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
            var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
            {
                options.TelemetryFactory = _ => TestTelemetryHelper.CreateInitializedTelemetry(reportedSourceName, $"Diag.{Path.GetRandomFileName()}");
            });
            using var provider = services.BuildServiceProvider();
 
            var longValue = new string('a', 1000);
            var command = provider.GetRequiredService<RootCommand>();
            var result = command.Parse($"agent telemetry --event-type reference_file_read --file-reference {longValue}");
 
            var exitCode = await result.InvokeAsync().DefaultTimeout();
 
            Assert.Equal(CliExitCodes.Success, exitCode);
 
            var activity = Assert.Single(capturedActivities);
            var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value);
            // An overlong file reference is dropped (not truncated) so oversized values never reach the backend.
            Assert.False(tags.ContainsKey(TelemetryConstants.Tags.AgentFileReference));
            Assert.Equal("reference_file_read", tags[TelemetryConstants.Tags.AgentEventType]);
        }
    }
 
    [Theory]
    [InlineData("/etc/passwd")]
    [InlineData("C:\\Users\\someone\\secret.txt")]
    [InlineData("../../etc/passwd")]
    [InlineData("~/secret")]
    public async Task AgentTelemetry_DropsUnsafeFileReferences(string fileReference)
    {
        var (capturedActivities, listener) = CreateCapturingListener(out var reportedSourceName);
        using (listener)
        {
            using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
            var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
            {
                options.TelemetryFactory = _ => TestTelemetryHelper.CreateInitializedTelemetry(reportedSourceName, $"Diag.{Path.GetRandomFileName()}");
            });
            using var provider = services.BuildServiceProvider();
 
            var command = provider.GetRequiredService<RootCommand>();
            var result = command.Parse(["agent", "telemetry", "--event-type", "reference_file_read", "--file-reference", fileReference]);
 
            var exitCode = await result.InvokeAsync().DefaultTimeout();
 
            Assert.Equal(CliExitCodes.Success, exitCode);
 
            var activity = Assert.Single(capturedActivities);
            var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value);
            Assert.False(tags.ContainsKey(TelemetryConstants.Tags.AgentFileReference));
        }
    }
 
    [Fact]
    public async Task AgentTelemetry_DropsUnknownEventType()
    {
        var (capturedActivities, listener) = CreateCapturingListener(out var reportedSourceName);
        using (listener)
        {
            using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
            var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
            {
                options.TelemetryFactory = _ => TestTelemetryHelper.CreateInitializedTelemetry(reportedSourceName, $"Diag.{Path.GetRandomFileName()}");
            });
            using var provider = services.BuildServiceProvider();
 
            var command = provider.GetRequiredService<RootCommand>();
            var result = command.Parse("agent telemetry --event-type not_a_real_event --skill-name aspire");
 
            var exitCode = await result.InvokeAsync().DefaultTimeout();
 
            Assert.Equal(CliExitCodes.Success, exitCode);
 
            var activity = Assert.Single(capturedActivities);
            var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value);
            Assert.False(tags.ContainsKey(TelemetryConstants.Tags.AgentEventType));
            Assert.Equal("aspire", tags[TelemetryConstants.Tags.AgentSkillName]);
        }
    }
 
    [Fact]
    public async Task AgentTelemetry_EmitsNoActivity_WhenAllValuesInvalid()
    {
        var (capturedActivities, listener) = CreateCapturingListener(out var reportedSourceName);
        using (listener)
        {
            using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
            var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
            {
                options.TelemetryFactory = _ => TestTelemetryHelper.CreateInitializedTelemetry(reportedSourceName, $"Diag.{Path.GetRandomFileName()}");
            });
            using var provider = services.BuildServiceProvider();
 
            var command = provider.GetRequiredService<RootCommand>();
            // Every value fails validation (unknown event type, identifier with a space, absolute path).
            // When nothing survives, the command must emit no span at all rather than a tagless one.
            var result = command.Parse(["agent", "telemetry", "--event-type", "not_a_real_event", "--skill-name", "bad name", "--file-reference", "/etc/passwd"]);
 
            var exitCode = await result.InvokeAsync().DefaultTimeout();
 
            Assert.Equal(CliExitCodes.Success, exitCode);
            Assert.Empty(capturedActivities);
        }
    }
 
    [Fact]
    public async Task AgentTelemetry_ExitsZero_WithUnknownToken()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        // A newer hook script may pass a flag this CLI version does not understand; it must not fail.
        var result = command.Parse("agent telemetry --event-type skill_invocation --some-future-flag value");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
    }
 
    [Fact]
    public async Task AgentTelemetry_ExitsZero_WithNoOptions()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("agent telemetry");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, exitCode);
    }
 
    [Fact]
    public void AgentTelemetry_IsHidden()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<AgentTelemetryCommand>();
        Assert.True(command.Hidden);
    }
 
    [Fact]
    public async Task AgentTelemetry_RecordsValidRelativeFileReference()
    {
        var (capturedActivities, listener) = CreateCapturingListener(out var reportedSourceName);
        using (listener)
        {
            using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
            var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
            {
                options.TelemetryFactory = _ => TestTelemetryHelper.CreateInitializedTelemetry(reportedSourceName, $"Diag.{Path.GetRandomFileName()}");
            });
            using var provider = services.BuildServiceProvider();
 
            var command = provider.GetRequiredService<RootCommand>();
            var result = command.Parse("agent telemetry --event-type reference_file_read --file-reference aspire/references/deploy.md");
 
            var exitCode = await result.InvokeAsync().DefaultTimeout();
 
            Assert.Equal(CliExitCodes.Success, exitCode);
 
            var activity = Assert.Single(capturedActivities);
            var tags = activity.Tags.ToDictionary(t => t.Key, t => t.Value);
            Assert.Equal("aspire/references/deploy.md", tags[TelemetryConstants.Tags.AgentFileReference]);
        }
    }
 
    private static (List<Activity> Activities, ActivityListener Listener) CreateCapturingListener(out string reportedSourceName)
    {
        reportedSourceName = $"Test.{Path.GetRandomFileName()}";
        var captured = new List<Activity>();
        var sourceName = reportedSourceName;
 
        var listener = new ActivityListener
        {
            ShouldListenTo = source => source.Name == sourceName,
            Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
            ActivityStopped = activity => captured.Add(activity)
        };
        ActivitySource.AddActivityListener(listener);
 
        return (captured, listener);
    }
}