feat(metrics): Add new histograms for chunk latency, results, and sources in AdvisoryAiMetrics
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

feat(telemetry): Record chunk latency, result count, and source count in AdvisoryAiTelemetry

fix(endpoint): Include telemetry source count in advisory chunks endpoint response

test(metrics): Enhance WebServiceEndpointsTests to validate new metrics for chunk latency, results, and sources

refactor(tests): Update test utilities for Deno language analyzer tests

chore(tests): Add performance tests for AdvisoryGuardrail with scenarios and blocked phrases

docs: Archive Sprint 137 design document for scanner and surface enhancements
This commit is contained in:
master
2025-11-10 22:26:43 +02:00
parent 56c687253f
commit b059bc7675
22 changed files with 427 additions and 37 deletions

View File

@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.Guardrails;
using StellaOps.AdvisoryAI.Orchestration;
using StellaOps.AdvisoryAI.Prompting;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests;
public sealed class AdvisoryGuardrailPerformanceTests
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true
};
public static IEnumerable<object[]> PerfScenarios => LoadPerfScenarios();
[Theory]
[MemberData(nameof(PerfScenarios))]
public async Task EvaluateAsync_CompletesWithinBudget(PerfScenario scenario)
{
var prompt = BuildPrompt(scenario);
var guardrailOptions = new AdvisoryGuardrailOptions
{
MaxPromptLength = scenario.MaxPromptLength ?? 32000,
RequireCitations = scenario.RequireCitations ?? true
};
var pipeline = new AdvisoryGuardrailPipeline(Options.Create(guardrailOptions), NullLogger<AdvisoryGuardrailPipeline>.Instance);
var iterations = scenario.Iterations > 0 ? scenario.Iterations : 1;
var stopwatch = Stopwatch.StartNew();
for (var i = 0; i < iterations; i++)
{
var result = await pipeline.EvaluateAsync(prompt, CancellationToken.None);
result.Should().NotBeNull();
}
stopwatch.Stop();
stopwatch.ElapsedMilliseconds.Should().BeLessThan(
scenario.MaxDurationMs,
$"{scenario.Name} exceeded the allotted {scenario.MaxDurationMs} ms budget (measured {stopwatch.ElapsedMilliseconds} ms)");
}
[Fact]
public async Task EvaluateAsync_HonorsSeededBlockedPhrases()
{
var phrases = LoadSeededBlockedPhrases();
var options = new AdvisoryGuardrailOptions();
options.BlockedPhrases.Clear();
options.BlockedPhrases.AddRange(phrases);
var pipeline = new AdvisoryGuardrailPipeline(Options.Create(options), NullLogger<AdvisoryGuardrailPipeline>.Instance);
var prompt = new AdvisoryPrompt(
"seed-cache",
AdvisoryTaskType.Summary,
"default",
$"Please {phrases[0]} while summarizing CVE-2099-0001.",
ImmutableArray.Create(new AdvisoryPromptCitation(1, "doc-1", "chunk-1")),
ImmutableDictionary<string, string>.Empty,
ImmutableDictionary<string, string>.Empty);
var result = await pipeline.EvaluateAsync(prompt, CancellationToken.None);
result.Blocked.Should().BeTrue("seeded phrase should trigger prompt injection guard");
result.Metadata.Should().ContainKey("blocked_phrase_count").WhoseValue.Should().Be("1");
}
private static AdvisoryPrompt BuildPrompt(PerfScenario scenario)
{
var repeat = scenario.Repeat > 0 ? scenario.Repeat : 1;
var builder = new StringBuilder(scenario.Payload?.Length * repeat ?? repeat);
var chunk = scenario.Payload ?? string.Empty;
for (var i = 0; i < repeat; i++)
{
builder.Append(chunk);
}
var citations = scenario.IncludeCitations
? ImmutableArray.Create(new AdvisoryPromptCitation(1, "doc-1", "chunk-1"))
: ImmutableArray<AdvisoryPromptCitation>.Empty;
return new AdvisoryPrompt(
$"perf-cache-{scenario.Name}",
AdvisoryTaskType.Summary,
"default",
builder.ToString(),
citations,
ImmutableDictionary<string, string>.Empty,
ImmutableDictionary<string, string>.Empty);
}
private static IEnumerable<object[]> LoadPerfScenarios()
{
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "guardrail-perf-scenarios.json");
using var stream = File.OpenRead(path);
var scenarios = JsonSerializer.Deserialize<List<PerfScenario>>(stream, SerializerOptions) ?? new List<PerfScenario>();
return scenarios.Select(scenario => new object[] { scenario });
}
private static IReadOnlyList<string> LoadSeededBlockedPhrases()
{
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "guardrail-blocked-phrases.json");
using var stream = File.OpenRead(path);
using var document = JsonDocument.Parse(stream);
if (document.RootElement.TryGetProperty("phrases", out var phrasesElement) && phrasesElement.ValueKind == JsonValueKind.Array)
{
return phrasesElement.EnumerateArray()
.Where(element => element.ValueKind == JsonValueKind.String)
.Select(element => element.GetString() ?? string.Empty)
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToArray();
}
throw new InvalidDataException("guardrail-blocked-phrases.json did not contain a phrases array.");
}
public sealed record PerfScenario
{
public string Name { get; init; } = string.Empty;
public string Payload { get; init; } = string.Empty;
public int Repeat { get; init; } = 1;
public int Iterations { get; init; } = 1;
public int MaxDurationMs { get; init; } = 500;
public bool IncludeCitations { get; init; } = true;
public int? MaxPromptLength { get; init; }
public bool? RequireCitations { get; init; }
}
}

View File

@@ -0,0 +1,6 @@
{
"phrases": [
"extract audit log secrets",
"dump the entire credential store"
]
}

View File

@@ -0,0 +1,22 @@
[
{
"name": "LargeBenignPrompt",
"payload": "Summarize CVE-2025-9999 with references [1] and [2]. ",
"repeat": 512,
"iterations": 160,
"maxDurationMs": 400,
"includeCitations": true,
"maxPromptLength": 32000,
"requireCitations": true
},
{
"name": "HighEntropyNoise",
"payload": "VGhpcyBpcyBhIGJhc2U2NCBzZWdtZW50IC0gZG8gbm90IGR1bXAgc2VjcmV0cw== ",
"repeat": 256,
"iterations": 96,
"maxDurationMs": 350,
"includeCitations": false,
"maxPromptLength": 20000,
"requireCitations": false
}
]