mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 05:00:07 +00:00
75197a4184
* updated commands * feat(backend): logparser pattern test endpoint with agent-exact semantics POST /api/rules/gather/test-pattern evaluates a logparser gather-rule regex against pasted sample lines exactly the way the agent would on the device. Motivated by a field incident: a customer verified a pattern with a PHP regex tester and the .NET engine matched differently. - CmTraceLogParser moves Agent -> AutopilotMonitor.Shared.Logging so agent (IME tracker, logparser collector, stall probe, IME matching mode) and backend run the SAME parsing implementation. Pure mechanical move, agent behavior unchanged (V2.Core suite 2147/2147 green). - TestLogPatternFunction mirrors LogParserCollector verbatim: Regex(Compiled, 1s timeout, NO IgnoreCase -> case-sensitive), first match per line, group "0" excluded / unsuccessful groups omitted, format cmtrace (default, match against parsed message) vs text (raw line), per-line outcomes incl. parse_failed/regex_timeout, and the agent's "set parameter format=text" hint verbatim. Pure compute, no tenant data. Caps: 200 lines, 8KB/line. - Policy: TenantAdminOrGlobalReader (same tier as the analyze dry-run). - Tests pin the parity showcases: case-sensitivity, envelope invisibility in cmtrace mode, optional-group omission, format hint, request validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(mcp): test_log_pattern tool + logparser lint and guide hardening - test_log_pattern: wraps POST /api/rules/gather/test-pattern — tests a logparser regex against pasted sample lines with the agent-exact .NET semantics (case-sensitive, cmtrace/text mode). Description steers clients away from JS/PHP/Python regex testing. - validate_rule: logparser parameter lint (missing/non-compiling pattern, unknown format value = silently cmtrace on the agent) + info pointing to test_log_pattern; nextStep for valid logparser drafts routes there too. - rule_authoring_guide: logparser section now states the matching semantics (case-SENSITIVE, .NET, first match per line, cmtrace default) and two new pitfalls (engine mismatch, plain-text log under cmtrace default). - Catalog snapshot + handler/validator tests extended (MCP suite 532 green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(okf): test-pattern endpoint + shared CmTraceLogParser in rule authoring surface Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
146 lines
5.7 KiB
C#
146 lines
5.7 KiB
C#
using System.Text.RegularExpressions;
|
|
using AutopilotMonitor.Functions.Functions.Rules;
|
|
|
|
namespace AutopilotMonitor.Functions.Tests;
|
|
|
|
/// <summary>
|
|
/// Pins the agent-parity semantics of the logparser pattern test endpoint: case-sensitive
|
|
/// matching (the field incident that motivated the endpoint: a pattern verified with a PHP
|
|
/// regex tester behaved differently under .NET), first-match-per-line, group extraction
|
|
/// identical to what LogParserCollector puts into the emitted event data, CMTrace parsing
|
|
/// via the SHARED CmTraceLogParser, and the format=text hint.
|
|
/// </summary>
|
|
public class TestLogPatternFunctionTests
|
|
{
|
|
private static Regex AgentRegex(string pattern)
|
|
=> new(pattern, RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
|
|
|
private const string CmTraceLine =
|
|
"<![LOG[Installation failed for app Ivanti Secure Access Client with error 0x87D1041C]LOG]!>" +
|
|
"<time=\"07:48:35.1783405\" date=\"8-4-2026\" component=\"AppWorkload\" context=\"\" type=\"3\" thread=\"42\" file=\"\">";
|
|
|
|
// ===== text mode =====
|
|
|
|
[Fact]
|
|
public void TextMode_NamedGroups_LandInGroups()
|
|
{
|
|
var result = TestLogPatternFunction.EvaluatePattern(
|
|
AgentRegex(@"(?<level>ERROR|FATAL)\s+(?<component>\w+):\s+(?<message>.+)"),
|
|
isTextMode: true,
|
|
new[] { "ERROR Installer: disk full", "INFO Installer: all good" });
|
|
|
|
Assert.Equal(1, result.MatchCount);
|
|
Assert.Equal("matched", result.Lines[0].Outcome);
|
|
Assert.Equal("ERROR", result.Lines[0].Groups!["level"]);
|
|
Assert.Equal("Installer", result.Lines[0].Groups!["component"]);
|
|
Assert.Equal("disk full", result.Lines[0].Groups!["message"]);
|
|
Assert.Equal("no_match", result.Lines[1].Outcome);
|
|
}
|
|
|
|
/// <summary>The motivating field case: logparser matching has NO IgnoreCase — a pattern
|
|
/// that matched in a case-insensitive tester silently never fires on the device.</summary>
|
|
[Fact]
|
|
public void TextMode_IsCaseSensitive_AndSaysSoInTheNote()
|
|
{
|
|
var result = TestLogPatternFunction.EvaluatePattern(
|
|
AgentRegex("ERROR"), isTextMode: true, new[] { "error: something broke" });
|
|
|
|
Assert.Equal(0, result.MatchCount);
|
|
Assert.Contains(result.Notes, n => n.Contains("case-SENSITIVE"));
|
|
}
|
|
|
|
[Fact]
|
|
public void TextMode_UnsuccessfulOptionalGroup_IsOmitted()
|
|
{
|
|
var result = TestLogPatternFunction.EvaluatePattern(
|
|
AgentRegex(@"ERROR(?<code>\s0x[0-9A-F]+)?"), isTextMode: true, new[] { "ERROR without code" });
|
|
|
|
Assert.Equal(1, result.MatchCount);
|
|
Assert.False(result.Lines[0].Groups!.ContainsKey("code"));
|
|
}
|
|
|
|
// ===== cmtrace mode =====
|
|
|
|
[Fact]
|
|
public void CmTraceMode_MatchesAgainstParsedMessage_AndReportsComponentAndType()
|
|
{
|
|
var result = TestLogPatternFunction.EvaluatePattern(
|
|
AgentRegex(@"error (?<errorCode>0x[0-9A-Fa-f]+)"), isTextMode: false, new[] { CmTraceLine });
|
|
|
|
Assert.Equal(1, result.MatchCount);
|
|
var row = result.Lines[0];
|
|
Assert.Equal("matched", row.Outcome);
|
|
Assert.Equal("0x87D1041C", row.Groups!["errorCode"]);
|
|
Assert.Equal("AppWorkload", row.Component);
|
|
Assert.Equal(3, row.CmTraceType);
|
|
Assert.Contains("Installation failed", row.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void CmTraceMode_RegexDoesNotSeeTheEnvelope()
|
|
{
|
|
// "component=" only exists in the CMTrace envelope, never in the message the
|
|
// agent matches against — a pattern targeting it must NOT match.
|
|
var result = TestLogPatternFunction.EvaluatePattern(
|
|
AgentRegex("component="), isTextMode: false, new[] { CmTraceLine });
|
|
|
|
Assert.Equal(0, result.MatchCount);
|
|
Assert.Equal("no_match", result.Lines[0].Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void CmTraceMode_PlainTextLines_ParseFail_WithFormatHint()
|
|
{
|
|
var result = TestLogPatternFunction.EvaluatePattern(
|
|
AgentRegex("ERROR"), isTextMode: false, new[] { "ERROR plain line", "another plain line" });
|
|
|
|
Assert.Equal(2, result.ParseFailureCount);
|
|
Assert.All(result.Lines, l => Assert.Equal("parse_failed", l.Outcome));
|
|
Assert.Contains(result.Notes, n => n.Contains("format=text"));
|
|
}
|
|
|
|
// ===== request validation =====
|
|
|
|
[Fact]
|
|
public void Validate_RejectsMissingPatternAndLines()
|
|
{
|
|
Assert.Contains("pattern", TestLogPatternFunction.ValidateRequest(
|
|
new TestLogPatternFunction.TestLogPatternRequest { SampleLines = new List<string> { "x" } })!);
|
|
Assert.Contains("sampleLines", TestLogPatternFunction.ValidateRequest(
|
|
new TestLogPatternFunction.TestLogPatternRequest { Pattern = "a" })!);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_RejectsTooManyLines_AndBadFormat()
|
|
{
|
|
var tooMany = new TestLogPatternFunction.TestLogPatternRequest
|
|
{
|
|
Pattern = "a",
|
|
SampleLines = Enumerable.Repeat("line", TestLogPatternFunction.MaxSampleLines + 1).ToList(),
|
|
};
|
|
Assert.Contains("at most", TestLogPatternFunction.ValidateRequest(tooMany)!);
|
|
|
|
var badFormat = new TestLogPatternFunction.TestLogPatternRequest
|
|
{
|
|
Pattern = "a",
|
|
Format = "json",
|
|
SampleLines = new List<string> { "x" },
|
|
};
|
|
Assert.Contains("format", TestLogPatternFunction.ValidateRequest(badFormat)!);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_AcceptsBothFormatsAndDefault()
|
|
{
|
|
foreach (var format in new[] { null, "cmtrace", "text", "TEXT" })
|
|
{
|
|
Assert.Null(TestLogPatternFunction.ValidateRequest(new TestLogPatternFunction.TestLogPatternRequest
|
|
{
|
|
Pattern = "a",
|
|
Format = format,
|
|
SampleLines = new List<string> { "x" },
|
|
}));
|
|
}
|
|
}
|
|
}
|