diff --git a/AGENTS.md b/AGENTS.md index 2c457d2..5259112 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,6 +59,20 @@ When modifying code that has associated tests (check `src/Tiger.Tests/`), you ** update or add tests to cover your changes. Run `dotnet test --nologo` to verify all tests pass before presenting changes for review. +### Assertion Style — All Tests + +Do **not** use `Assert.Contains` to spot-check fragments of output. Every test must assert +against the **complete expected value** using `Assert.Equal` with a raw string literal so the +full behavior is visible and regressions are caught precisely. + +- For single-value results: `Assert.Equal("expected value", actual);` +- For multi-line output: use a raw string literal (`"""..."""`) as the expected value and + `Assert.Equal(expected, actual)` (with `ignoreLineEndingDifferences: true` if needed). +- For list results: join lines and compare the full string, or assert each element by index. + Never use `Assert.Contains(collection, predicate)` to search for a matching element. + +This applies to **all** test classes — not just UI rendering tests. + ### UI Rendering Tests Tests that validate UI rendering **must** render through `IAnsiConsole` (using `TestConsole` diff --git a/src/Tiger.Tests/MarkdownRendererTests.cs b/src/Tiger.Tests/MarkdownRendererTests.cs index ac8ee93..385dcbf 100644 --- a/src/Tiger.Tests/MarkdownRendererTests.cs +++ b/src/Tiger.Tests/MarkdownRendererTests.cs @@ -424,4 +424,108 @@ public void RenderToLines_CodeBlockEndsCorrectly() """; Assert.Equal(expected, string.Join(Environment.NewLine, lines), ignoreLineEndingDifferences: true); } + + [Fact] + public void Blockquote_RenderedWithBarAndItalic() + { + var md = "> Generated: 2026-06-22 14:15 | Window: 3 days"; + + var lines = MarkdownRenderer.ToMarkupLines(md); + + Assert.Single(lines); + Assert.Equal(" [dim]│[/] [italic]Generated: 2026-06-22 14:15 | Window: 3 days[/]", lines[0]); + } + + [Fact] + public void NumberedList_RenderedWithBlueNumbers() + { + var md = "1. First item\n2. Second item with **bold**"; + + var lines = MarkdownRenderer.ToMarkupLines(md); + + var expected = """ + [blue]1.[/] First item + [blue]2.[/] Second item with [bold]bold[/] + """; + Assert.Equal(expected.ReplaceLineEndings("\n").Trim(), + string.Join("\n", lines.Select(l => l.TrimStart()))); + } + + [Fact] + public void Strikethrough_RenderedWithStrikethroughMarkup() + { + var result = MarkdownRenderer.FormatInlineMarkup("~~resolved issue~~"); + + Assert.Equal("[strikethrough]resolved issue[/]", result); + } + + [Fact] + public void HealthReport_RendersAllElements() + { + // Real-world health report content from a health agent run. + // Uses RenderToLines which properly tracks code blocks and skips tables. + var md = """ + # Health Report — dotnet/roslyn / roslyn-CI + + > Generated: 2026-06-22 2:14 PM PT | Window: 3 days + + **Overall Health: 🟡 YELLOW** + + **CI pass rate (main):** 3 succeeded / 5 failed in last 3 days. + + ### Active Problems + + | Problem | Since | Severity | Trend | + |---------|-------|----------|-------| + | Helix work item crashes | ~10+ days ago | 🟡 Medium | **Chronic** — infrastructure issue | + | `Fuzz_2` test host crash | Jun 22 | 🟠 Watch | **New** — first occurrence | + + ### Known Issue Correlations + - None of the 6 active known issues matched failures in this window. + + ### Recently Resolved + - ~~Main branch failure streak (Jun 19–22)~~: Partially resolved. + + ### Recommended Actions + 1. **Monitor `Fuzz_2` crash**: If it recurs, investigate the dump file. + 2. **Investigate recurring 120-min timeout** on `Test_Windows_CoreClr_Debug_Single_Machine`. + + ### Trends + - **Unstable YELLOW**: Main branch alternates between green and infrastructure-caused failures. + - **Upgrade to GREEN** if: Next 2+ main builds pass without timeout/crash issues. + """; + + var lines = MarkdownRenderer.RenderToLines(md); + var actual = string.Join("\n", lines); + + var expected = """ + [bold blue underline]Health Report — dotnet/roslyn / roslyn-CI[/] + + [dim]│[/] [italic]Generated: 2026-06-22 2:14 PM PT | Window: 3 days[/] + + [bold]Overall Health: 🟡 YELLOW[/] + + [bold]CI pass rate (main):[/] 3 succeeded / 5 failed in last 3 days. + + [bold]Active Problems[/] + + + [bold]Known Issue Correlations[/] + [blue]•[/] None of the 6 active known issues matched failures in this window. + + [bold]Recently Resolved[/] + [blue]•[/] [strikethrough]Main branch failure streak (Jun 19–22)[/]: Partially resolved. + + [bold]Recommended Actions[/] + [blue]1.[/] [bold]Monitor [grey]Fuzz_2[/] crash[/]: If it recurs, investigate the dump file. + [blue]2.[/] [bold]Investigate recurring 120-min timeout[/] on [grey]Test_Windows_CoreClr_Debug_Single_Machine[/]. + + [bold]Trends[/] + [blue]•[/] [bold]Unstable YELLOW[/]: Main branch alternates between green and infrastructure-caused failures. + [blue]•[/] [bold]Upgrade to GREEN[/] if: Next 2+ main builds pass without timeout/crash issues. + """; + Assert.Equal( + expected.ReplaceLineEndings("\n").Trim(), + actual.ReplaceLineEndings("\n").Trim()); + } } diff --git a/src/Tiger/Commands/HealthCommand.cs b/src/Tiger/Commands/HealthCommand.cs index d718041..7ae332f 100644 --- a/src/Tiger/Commands/HealthCommand.cs +++ b/src/Tiger/Commands/HealthCommand.cs @@ -88,15 +88,7 @@ private void ShowStatePage(HealthAgentService agent, string repository, string d var detailLines = new List(); if (state is not null) { - var lines = state.ReplaceLineEndings("\n").Split('\n'); - foreach (var line in lines.Take(30)) - { - detailLines.Add(Markup.Escape(line)); - } - if (lines.Length > 30) - { - detailLines.Add($"[dim]... ({lines.Length - 30} more lines)[/]"); - } + detailLines = MarkdownRenderer.ToMarkupLines(state); } else { @@ -237,14 +229,7 @@ private void ShowRunDetail(HealthRunInfo run) if (File.Exists(run.LogPath)) { var content = File.ReadAllText(run.LogPath); - var lines = content.ReplaceLineEndings("\n").Split('\n'); - detailLines = lines.Take(40) - .Select(line => Markup.Escape(line)) - .ToList(); - if (lines.Length > 40) - { - detailLines.Add($"[dim]... ({lines.Length - 40} more lines)[/]"); - } + detailLines = MarkdownRenderer.ToMarkupLines(content); } else { diff --git a/src/Tiger/MarkdownRenderer.cs b/src/Tiger/MarkdownRenderer.cs index 5e3c3ad..7d187d6 100644 --- a/src/Tiger/MarkdownRenderer.cs +++ b/src/Tiger/MarkdownRenderer.cs @@ -57,8 +57,12 @@ internal static List RenderToLines(string markdown) if (IsTableRow(trimmed) && i + 1 < lines.Length && IsTableSeparator(lines[i + 1].TrimEnd('\r'))) { - // Tables can't be rendered to lines — skip (handled by Render directly) - i++; + // Skip entire table — tables are rendered by Render() directly via Spectre.Console Table + i += 2; // skip header + separator + while (i < lines.Length && IsTableRow(lines[i].TrimEnd('\r'))) + { + i++; + } continue; } @@ -260,6 +264,27 @@ public static List ToMarkupLines(string markdown) continue; } + // Numbered lists + if (trimmed.Length > 2 && char.IsDigit(trimmed[0])) + { + var dotIndex = trimmed.IndexOf(". "); + if (dotIndex > 0 && dotIndex <= 3 && trimmed[..dotIndex].All(char.IsDigit)) + { + var number = trimmed[..dotIndex]; + var content = FormatInlineMarkup(trimmed[(dotIndex + 2)..]); + result.Add($" [blue]{number}.[/] {content}"); + continue; + } + } + + // Blockquotes + if (trimmed.StartsWith("> ")) + { + var content = FormatInlineMarkup(trimmed[2..]); + result.Add($" [dim]│[/] [italic]{content}[/]"); + continue; + } + // Empty lines if (string.IsNullOrWhiteSpace(trimmed)) { @@ -309,6 +334,9 @@ public static string FormatInlineMarkup(string text) // Inline code: `text` → [grey]text[/] escaped = Regex.Replace(escaped, @"`(.+?)`", "[grey]$1[/]"); + // Strikethrough: ~~text~~ → [strikethrough]text[/] + escaped = Regex.Replace(escaped, @"~~(.+?)~~", "[strikethrough]$1[/]"); + return escaped; } }