Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
104 changes: 104 additions & 0 deletions src/Tiger.Tests/MarkdownRendererTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
19 changes: 2 additions & 17 deletions src/Tiger/Commands/HealthCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,7 @@ private void ShowStatePage(HealthAgentService agent, string repository, string d
var detailLines = new List<string>();
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
{
Expand Down Expand Up @@ -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
{
Expand Down
32 changes: 30 additions & 2 deletions src/Tiger/MarkdownRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ internal static List<string> 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;
}

Expand Down Expand Up @@ -260,6 +264,27 @@ public static List<string> 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))
{
Expand Down Expand Up @@ -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;
}
}
Loading