From 879d02d2ebe308cb6d39897d0338c9ac3d8c63c2 Mon Sep 17 00:00:00 2001 From: Nikolaos Protopapas Date: Tue, 7 Jul 2026 16:51:36 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(cli):=20polish=20the=20interactive=20s?= =?UTF-8?q?hell=20=E2=80=94=20markdown=20detail=20modal=20+=20live=20filte?= =?UTF-8?q?r=20portal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump SharpConsoleUI to 2.5.10 (markdown ruler/table rendering fixes) and use its markdown capabilities to polish two interactive-shell surfaces. Detail modal (skills/bundles/agents/installed): - Render descriptions and the SKILL.md preview as real markdown via MarkupControl.SetMarkdown instead of escaped plain text. - Show the FULL SKILL.md (dropped the 10-line cap) in its own fill-height scrolling panel with a "## Preview" heading and a left margin; the skill modal folds the summary into one cohesive markdown document. - The static (Spectre stdout) detail panel keeps a short capped excerpt. Filter: - Replace the heavy centered "/" search modal with a slim non-blocking portal (SearchFilterPortal) that filters the list/table behind it live as you type, driving the existing _searchFilter + RebuildActivePage path so grouped, flat, and non-table pages all filter uniformly. - The on-page filter chip gains a clickable [Clear] link (MarkupControl link support) and a subtle separator rule. Build + 614 tests pass. --- .../ManagedCode.Agents.csproj | 2 +- .../ManagedCode.DotnetAgents.csproj | 2 +- .../InteractiveConsoleApp.Catalog.cs | 12 +- .../InteractiveConsoleApp.Shell.cs | 147 ++++++++++++------ .../InteractiveConsoleApp.Workspace.cs | 2 +- .../InteractiveConsoleApp.cs | 54 +++++-- .../ManagedCode.DotnetSkills.csproj | 2 +- .../UI/SearchFilterPortal.cs | 94 +++++++++++ 8 files changed, 248 insertions(+), 67 deletions(-) create mode 100644 cli/ManagedCode.DotnetSkills/UI/SearchFilterPortal.cs diff --git a/cli/ManagedCode.Agents/ManagedCode.Agents.csproj b/cli/ManagedCode.Agents/ManagedCode.Agents.csproj index 4d61aa5..076de65 100644 --- a/cli/ManagedCode.Agents/ManagedCode.Agents.csproj +++ b/cli/ManagedCode.Agents/ManagedCode.Agents.csproj @@ -50,7 +50,7 @@ - + diff --git a/cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj b/cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj index 6a9a74f..5233d85 100644 --- a/cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj +++ b/cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj @@ -50,7 +50,7 @@ - + diff --git a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Catalog.cs b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Catalog.cs index dc9ee15..148c778 100644 --- a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Catalog.cs +++ b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Catalog.cs @@ -93,6 +93,11 @@ private void BuildSkillBrowserPage(ConsoleWindowSystem ws, ScrollablePanelContro private void ShowSkillDetailModal(ConsoleWindowSystem ws, ScrollablePanelControl owner, SkillEntry skill) { + // Fold the one-line summary into the top of the scrolling markdown document so the modal + // reads as one cohesive skill page (summary lead-in, then the full SKILL.md) rather than + // three stacked blocks. The identity strip stays pinned above. + var body = ComposeSkillMarkdown(skill.Description, LoadSkillPreview(skill)); + var detail = new IWindowControl[] { BuildPropertyPanel(ToAlias(skill.Name), AccentTurquoise, @@ -101,8 +106,7 @@ private void ShowSkillDetailModal(ConsoleWindowSystem ws, ScrollablePanelControl ("lane", Escape(skill.Lane)), ("version", Escape(skill.Version)), ("tokens", FormatTokenCount(skill.TokenCount))), - BuildModalBlock("summary", Escape(skill.Description)), - BuildModalBlock("preview", Escape(LoadSkillPreview(skill))), + BuildScrollingMarkdownBlock("preview", body), }; ShowModalNative(ws, $"Skill · {ToAlias(skill.Name)}", detail, @@ -489,7 +493,7 @@ private void ShowBundleModal(ConsoleWindowSystem ws, ScrollablePanelControl owne ("title", Escape(package.Title)), ("skills", package.Skills.Count.ToString()), ("includes", Escape(string.Join(", ", package.Skills.Take(10).Select(ToAlias))))), - BuildModalBlock("summary", Escape(package.Description)), + BuildMarkdownBlock("summary", package.Description), }; ShowModalNative(ws, $"Bundle · {package.Name}", detail, @@ -672,7 +676,7 @@ private void ShowAgentModal(ConsoleWindowSystem ws, ScrollablePanelControl owner ("agent", Escape(agent.Name)), ("skills", agent.Skills.Count == 0 ? "[grey50]-[/]" : Escape(string.Join(", ", agent.Skills.Select(ToAlias)))), ("platform", Escape(Session.Agent.ToString()))), - BuildModalBlock("summary", Escape(agent.Description)), + BuildMarkdownBlock("summary", agent.Description), }; var buttons = new List<(string, Action)>(); diff --git a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs index 364fe22..0a85e5a 100644 --- a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs +++ b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs @@ -88,6 +88,8 @@ internal sealed partial class InteractiveConsoleApp private Window? _mainWindow; private CommandPalettePortal? _palettePortal; private LayoutNode? _palettePortalNode; + private SearchFilterPortal? _searchPortal; + private LayoutNode? _searchPortalNode; private StatusBarControl? _statusBar; private StatusBarItem? _clockItem; private StatusBarItem? _statusMessage; @@ -397,6 +399,13 @@ private void HandlePreviewKey(KeyPressedEventArgs e) { _palettePortal.ProcessKey(e.KeyInfo); e.Handled = true; + return; + } + + if (_searchPortal != null) + { + _searchPortal.ProcessKey(e.KeyInfo); + e.Handled = true; } } @@ -424,7 +433,7 @@ private void HandleGlobalKey(KeyPressedEventArgs e) { if (key.KeyChar == '/' && IsListBearingPage(_currentPage)) { - ShowSearchOverlay(); + ShowSearchPortal(); e.Handled = true; } return; @@ -695,10 +704,47 @@ private static void AddInfoBlock(ScrollablePanelControl panel, string title, par panel.AddControl(new MarkupControl(rows)); } - /// De-emphasized titled block for use INSIDE a detail modal (which already has a frame). - /// A bold caption + body, no accent box. - private static IWindowControl BuildModalBlock(string title, string body) - => new MarkupControl(new List { $"[bold]{Escape(title)}[/]", body }); + /// Titled block whose body is rendered as MARKDOWN via + /// MarkupControl.SetMarkdown. For short, pinned content (e.g. a one-line summary or a + /// description). The markdown body must NOT be pre-escaped — escaping turns markup/markdown + /// syntax into literals. Only the title is escaped. + private static IWindowControl BuildMarkdownBlock(string title, string markdown) + // Bold caption line, a blank spacer, then the markdown body — one MarkupControl built via + // the markup builder so the body renders as markdown (AddMarkdown) while the caption stays + // literal markup (AddLine). Matches BuildModalBlock's caption-above-body shape. + => Controls.Markdown() + .AddLine($"[bold]{Escape(title)}[/]") + .AddLine(string.Empty) + .AddMarkdown(markdown) + .Build(); + + /// Titled block whose FULL markdown body scrolls inside its own fill-height + /// ScrollablePanel. Use for long content (e.g. the whole SKILL.md preview) so it scrolls in the + /// modal body while the header/summary/toolbar stay pinned. The markdown body must NOT be + /// pre-escaped. + private static IWindowControl BuildScrollingMarkdownBlock(string title, string markdown) + { + // Render the title as a real markdown H2 heading at the top of the body (so it uses the + // markdown heading style, not a separate bold caption), then the markdown itself. + var heading = string.IsNullOrWhiteSpace(title) ? string.Empty : $"## {Capitalize(title)}\n\n"; + var content = new MarkupControl(new List { string.Empty }) + { + // Left margin so the markdown body isn't flush against the panel edge. + Margin = new Margin(1, 0, 0, 0), + }; + content.SetMarkdown(heading + markdown); + + var scroller = new ScrollablePanelControl + { + VerticalAlignment = VerticalAlignment.Fill, + }; + scroller.AddControl(content); + return scroller; + } + + // Upper-cases the first character of a block title (e.g. "preview" -> "Preview") for headings. + private static string Capitalize(string value) + => string.IsNullOrEmpty(value) ? value : char.ToUpperInvariant(value[0]) + value[1..]; /// /// Standard sortable rounded table with a left-aligned title and the accent border color. @@ -1134,53 +1180,36 @@ void Close() /// _searchFilter and rebuilds it. Esc dismisses without changing the filter. Triggered by /// `/` from any list-bearing page. /// - private void ShowSearchOverlay() + // Live list filter as a slim portal overlay. Replaces the old centered modal: a slim search bar + // near the top filters the table/list behind it on every keystroke via _searchFilter + + // RebuildActivePage (the same filter path used everywhere), so grouped, flat, and non-table pages + // all filter uniformly. Enter/Esc close it; the filter persists (Esc again on the page clears it). + private void ShowSearchPortal() { - if (_ws is null) return; - Window? modal = null; + if (_searchPortal != null) { DismissSearchPortal(); return; } // toggle closed + if (_mainWindow is null || _nav is null) return; - void Close() + var portal = new SearchFilterPortal(_searchFilter, _mainWindow.Width, _mainWindow.Height) { - if (modal is not null) _ws.CloseWindow(modal); - } - - var prompt = Controls.Prompt($" / ") - .UnfocusOnEnter(false) - .OnEntered((_, query) => - { - _searchFilter = (query ?? string.Empty).Trim(); - Close(); - RebuildActivePage(); - }) - .Build(); + Container = _mainWindow, + }; + _searchPortal = portal; + _searchPortalNode = _mainWindow.CreatePortal(_nav, portal); - var hint = new MarkupControl(new List + portal.TextChanged += (_, text) => { - "[grey50]Type to filter the current list. [bold]Enter[/] applies, [bold]Esc[/] cancels.[/]", - string.IsNullOrEmpty(_searchFilter) ? string.Empty : $"[grey50]current:[/] [yellow]{Escape(_searchFilter)}[/]", - }); + _searchFilter = (text ?? string.Empty).Trim(); + RebuildActivePage(); + }; + portal.Closed += (_, _) => DismissSearchPortal(); + } - modal = new WindowBuilder(_ws) - .WithTitle("search") - .WithSize(Math.Clamp(SafeConsole(() => Console.WindowWidth, 100) - 20, 50, 80), 9) - .Centered() - .AsModal() - .WithBackgroundGradient(ElevatedModalGradient, GradientDirection.Vertical) - .Minimizable(false) - .Maximizable(false) - .WithBorderStyle(BorderStyle.Rounded) - .WithBorderColor(AccentYellow) - .OnKeyPressed((_, e) => - { - if (e.KeyInfo.Key == ConsoleKey.Escape) - { - Close(); - e.Handled = true; - } - }) - .AddControl(hint) - .AddControl(prompt) - .BuildAndShow(); + private void DismissSearchPortal() + { + if (_searchPortalNode is not null && _mainWindow is not null && _nav is not null) + _mainWindow.RemovePortal(_nav, _searchPortalNode); + _searchPortal = null; + _searchPortalNode = null; } /// @@ -1320,7 +1349,7 @@ private void RebuildStatusBar(HomeAction? page) bar.AddLeft("Enter", page is HomeAction.SyncProject ? "Install" : page is HomeAction.Workspace ? "Change" : "Open"); if (IsListBearingPage(page)) { - bar.AddLeft("/", "Search", ShowSearchOverlay); + bar.AddLeft("/", "Search", ShowSearchPortal); } bar.AddLeft("Ctrl+P", "Palette", () => { if (_ws is not null) ShowCommandPalette(_ws); }); foreach (var (key, label, action) in PageShortcuts(page)) @@ -1441,7 +1470,29 @@ private bool MatchesFilter(params string?[] tokens) private void AddSearchChip(ScrollablePanelControl panel) { if (string.IsNullOrWhiteSpace(_searchFilter)) return; - AddInlineNote(panel, $"matching “{Escape(_searchFilter)}” [grey50]· press[/] [bold]Esc[/] [grey50]to clear[/]", NoteSeverity.Info); + + // Passive filter note plus a clickable [Clear] link. MarkupControl supports link clicks + // ([link=url] + OnLinkClicked), so the chip stays a single lightweight markup line — no + // toolbar/button row — while still offering a mouse affordance alongside the Esc hint. + var chip = Controls.Markup() + .AddLine($"[#82aaff]⌕[/] [grey62]filtering “{Escape(_searchFilter)}”[/] [grey50]· press[/] [bold]Esc[/] [grey50]or[/] [link=clear][#82aaff]Clear[/][/]") + .OnLinkClicked((_, e) => + { + if (e.Url == "clear") + { + _searchFilter = string.Empty; + RebuildActivePage(); + } + }) + .Build(); + panel.AddControl(chip); + + // Subtle separator below the chip so the filter status reads as its own band, distinct from + // the table beneath. Quiet desaturated grey (GridLineColor) — a hairline, not a section rule. + panel.AddControl(Controls.RuleBuilder() + .WithColor(GridLineColor) + .WithBorderStyle(BorderStyle.Single) + .Build()); } private async Task ClockLoopAsync(Window window, CancellationToken cancellationToken) diff --git a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Workspace.cs b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Workspace.cs index 6c8c9c1..e4c6d31 100644 --- a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Workspace.cs +++ b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Workspace.cs @@ -297,7 +297,7 @@ private void ShowInstalledSkillModal(ConsoleWindowSystem ws, ScrollablePanelCont ("latest", Escape(record.Skill.Version)), ("status", record.IsCurrent ? "[green]✓ current[/]" : "[yellow]↻ update available[/]"), ("tokens", FormatTokenCount(record.Skill.TokenCount))), - BuildModalBlock("summary", Escape(record.Skill.Description)), + BuildMarkdownBlock("summary", record.Skill.Description), }; var buttons = new List<(string, Action)>(); diff --git a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.cs b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.cs index 2f0091f..7f5d7c8 100644 --- a/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.cs +++ b/cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.cs @@ -3186,7 +3186,10 @@ private void RenderSkillDetailPanel(SkillEntry skill, InstalledSkillRecord? inst SpectreConsole.Write(BuildRichTwoColumn( BuildRichShellPanel("summary", new Spectre.Console.Markup(Escape(skill.Description))), - BuildRichShellPanel("preview", new Spectre.Console.Markup(Escape(LoadSkillPreview(skill)))), + // Static (non-scrolling) Spectre panel: cap the preview to a handful of lines so it + // doesn't flood stdout. The interactive modal renders the full SKILL.md in a scrolling + // markdown block instead (see BuildScrollingMarkdownBlock). + BuildRichShellPanel("preview", new Spectre.Console.Markup(Escape(PreviewExcerpt(LoadSkillPreview(skill), 10)))), gap: 3)); AnsiConsole.WriteLine(); SpectreConsole.Write(BuildRichShellPanel( @@ -3482,16 +3485,13 @@ private string LoadSkillPreview(SkillEntry skill) } } - var previewLines = text - .Split('\n', StringSplitOptions.RemoveEmptyEntries) - .Select(line => line.Trim()) - .Where(line => line.Length > 0) - .Take(10) - .ToArray(); - - return previewLines.Length == 0 - ? "No previewable markdown lines were found in SKILL.md." - : string.Join(Environment.NewLine, previewLines); + // Return the FULL SKILL.md body (frontmatter already stripped above) as markdown so the + // detail modal can render and scroll all of it. Do not cap lines or flatten structure — + // the scrolling markdown block owns overflow. + var body = text.Trim(); + return body.Length == 0 + ? "No previewable markdown content was found in SKILL.md." + : body; } catch (Exception exception) { @@ -3499,6 +3499,38 @@ private string LoadSkillPreview(SkillEntry skill) } } + // Compose the skill detail modal's single markdown document: the one-line summary as an + // italicized lead paragraph, a horizontal rule, then the full SKILL.md body. Folding the summary + // in (rather than a separate block) makes the modal read as one cohesive skill page. The summary + // is skipped when it's empty or the body already opens with it, to avoid a duplicated lead line. + private static string ComposeSkillMarkdown(string summary, string body) + { + summary = (summary ?? string.Empty).Trim(); + body = (body ?? string.Empty).Trim(); + + if (summary.Length == 0) + return body; + + if (body.Contains(summary, StringComparison.Ordinal)) + return body; + + return $"*{summary}*{Environment.NewLine}{Environment.NewLine}---{Environment.NewLine}{Environment.NewLine}{body}"; + } + + // Short excerpt of a full markdown body for static (non-scrolling) surfaces: the first + // `maxLines` non-empty lines, trimmed. The interactive modal shows the full body in a scrolling + // block, so this cap applies only to the Spectre stdout detail panel. + private static string PreviewExcerpt(string markdown, int maxLines) + { + var lines = markdown + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => line.Length > 0) + .Take(maxLines) + .ToArray(); + return lines.Length == 0 ? markdown : string.Join(Environment.NewLine, lines); + } + private static string CompactDescription(string description) { const string useWhenMarker = ". Use when"; diff --git a/cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj b/cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj index 00c237c..b3a2676 100644 --- a/cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj +++ b/cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj @@ -46,7 +46,7 @@ - + diff --git a/cli/ManagedCode.DotnetSkills/UI/SearchFilterPortal.cs b/cli/ManagedCode.DotnetSkills/UI/SearchFilterPortal.cs new file mode 100644 index 0000000..3f86d54 --- /dev/null +++ b/cli/ManagedCode.DotnetSkills/UI/SearchFilterPortal.cs @@ -0,0 +1,94 @@ +using SharpConsoleUI; +using SharpConsoleUI.Builders; +using SharpConsoleUI.Controls; +using SharpConsoleUI.Drawing; +using SharpConsoleUI.Layout; +using Rectangle = System.Drawing.Rectangle; + +namespace ManagedCode.DotnetSkills; + +/// +/// Live list/table filter as a slim portal overlay (not a modal window). A single search input sits +/// in a slim bar near the top of the window; every keystroke raises so the +/// shell can re-filter the page behind it live. Enter accepts (closes, keeps the filter), Esc closes. +/// Modeled on . +/// +internal sealed class SearchFilterPortal : PortalContentContainer +{ + private const int MaxWidth = 90; + + private readonly PromptControl _searchInput; + private readonly MarkupControl _resultCaption; + + /// Raised on every keystroke with the current (untrimmed) query text. + public event EventHandler? TextChanged; + + /// Raised when the user closes the portal (Esc) or accepts it (Enter). + public event EventHandler? Closed; + + public SearchFilterPortal(string initialQuery, int windowWidth, int windowHeight) + { + DismissOnOutsideClick = true; + BorderStyle = BoxChars.Rounded; + BorderColor = Color.Gold1; + BorderBackgroundColor = Color.Grey15; + BackgroundColor = Color.Grey15; + ForegroundColor = Color.Grey93; + + AddChild(Controls.Markup() + .AddLine("[grey62] ⌕ Filter the current list — [bold]Enter[/] apply · [bold]Esc[/] close[/]") + .WithAlignment(HorizontalAlignment.Left) + .WithMargin(1, 0, 1, 0) + .Build()); + + AddChild(Controls.RuleBuilder().WithColor(Color.Grey23).Build()); + + _searchInput = Controls.Prompt(" / ") + .WithInput(initialQuery ?? string.Empty) + .WithAlignment(HorizontalAlignment.Stretch) + .WithMargin(1, 0, 1, 0) + .Build(); + AddChild(_searchInput); + + _resultCaption = Controls.Markup() + .AddLine(string.Empty) + .WithAlignment(HorizontalAlignment.Left) + .WithMargin(1, 0, 1, 0) + .StickyBottom() + .Build(); + AddChild(_resultCaption); + + // Slim bar near the top: most of the width, only a few rows tall, so the filtered table + // stays visible below it. + int w = Math.Min(MaxWidth, Math.Max(40, windowWidth - 4)); + int h = 6; // border(2) + hint(1) + rule(1) + input(1) + caption(1) + int x = (windowWidth - w) / 2; + PortalBounds = new Rectangle(x, 1, w, h); + + _searchInput.InputChanged += (_, text) => TextChanged?.Invoke(this, text); + + SetFocusOnFirstChild(); + } + + /// Updates the small result caption (e.g. "12 / 40 shown"). Shell calls this after a rebuild. + public void SetResultCaption(string markup) + => _resultCaption.SetContent(new List { markup }); + + // The portal isn't part of the host window's focus tree (the shell forwards keys here manually). + // The prompt is always active for typing; Enter accepts, Esc closes, everything else types. + public new bool ProcessKey(ConsoleKeyInfo key) + { + switch (key.Key) + { + case ConsoleKey.Escape: + case ConsoleKey.Enter: + Closed?.Invoke(this, EventArgs.Empty); + return true; + } + + // Typing / backspace goes to the focused child (the prompt), which raises InputChanged and + // re-filters live. + base.ProcessKey(key); + return true; // swallow all keys while the portal is open + } +} From 8ef000375f971132a25648f81b53c976c513fdb9 Mon Sep 17 00:00:00 2001 From: Nikolaos Protopapas Date: Tue, 7 Jul 2026 16:55:48 +0300 Subject: [PATCH 2/2] chore(external-sources): re-sync vendored upstreams Refresh the checked-in external-source snapshots to the latest upstream so the "verify imported external sources are committed" CI gate passes. Regenerated via scripts/sync_external_catalog_sources.sh; no manual edits. Unrelated to the shell polish in this branch. --- .../agents/code-testing-builder/AGENT.md | 2 +- .../agents/code-testing-fixer/AGENT.md | 2 +- .../agents/code-testing-generator/AGENT.md | 2 +- .../agents/code-testing-implementer/AGENT.md | 2 +- .../agents/code-testing-linter/AGENT.md | 2 +- .../agents/code-testing-planner/AGENT.md | 2 +- .../agents/code-testing-researcher/AGENT.md | 2 +- .../agents/code-testing-tester/AGENT.md | 2 +- .../skills/assertion-quality/SKILL.md | 2 + .../generate-testability-wrappers/SKILL.md | 19 +++--- .../skills/migrate-static-to-wrapper/SKILL.md | 60 +++++++++++++++---- .../skills/run-tests/SKILL.md | 4 +- .../skills/writing-mstest-tests/SKILL.md | 11 ++-- .../optimizing-dotnet-performance/AGENT.md | 2 +- .../optimizing-dotnet-performance.agent.md | 2 +- .../agents/code-testing-builder.agent.md | 2 +- .../agents/code-testing-fixer.agent.md | 2 +- .../agents/code-testing-generator.agent.md | 2 +- .../agents/code-testing-implementer.agent.md | 2 +- .../agents/code-testing-linter.agent.md | 2 +- .../agents/code-testing-planner.agent.md | 2 +- .../agents/code-testing-researcher.agent.md | 2 +- .../agents/code-testing-tester.agent.md | 2 +- .../skills/assertion-quality/SKILL.md | 2 + .../generate-testability-wrappers/SKILL.md | 19 +++--- .../skills/migrate-static-to-wrapper/SKILL.md | 60 +++++++++++++++---- .../dotnet-test/skills/run-tests/SKILL.md | 4 +- .../skills/writing-mstest-tests/SKILL.md | 11 ++-- external-sources/vendir.lock.yml | 7 +-- 29 files changed, 157 insertions(+), 78 deletions(-) diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-builder/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-builder/AGENT.md index c333c1a..3155edf 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-builder/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-builder/AGENT.md @@ -6,7 +6,7 @@ description: >- errors, verifying project builds successfully. name: code-testing-builder user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-fixer/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-fixer/AGENT.md index 9787511..2745add 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-fixer/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-fixer/AGENT.md @@ -6,7 +6,7 @@ description: >- imports, correcting type mismatches, fixing compilation failures. name: code-testing-fixer user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-generator/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-generator/AGENT.md index 3127b7c..1c21e65 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-generator/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-generator/AGENT.md @@ -6,7 +6,7 @@ description: >- coverage plateaus or project-wide coverage/CRAP analysis without writing tests (use coverage-analysis); targeted method/class CRAP scores (use crap-score). name: code-testing-generator -tools: ["agent", "skill", "read", "search", "edit", "execute"] +tools: ["agent", "skill", "read", "search", "edit", "execute", "Task", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] agents: - code-testing-researcher - code-testing-planner diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-implementer/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-implementer/AGENT.md index 18ed700..7cb2d09 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-implementer/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-implementer/AGENT.md @@ -7,7 +7,7 @@ description: >- running build-test-fix cycle for generated tests. name: code-testing-implementer user-invocable: false -tools: ["agent", "skill", "read", "search", "edit", "execute"] +tools: ["agent", "skill", "read", "search", "edit", "execute", "Task", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] agents: - code-testing-builder - code-testing-tester diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-linter/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-linter/AGENT.md index ff8de44..d81a733 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-linter/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-linter/AGENT.md @@ -6,7 +6,7 @@ description: >- applying lint fixes. name: code-testing-linter user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-planner/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-planner/AGENT.md index af02e32..17a47de 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-planner/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-planner/AGENT.md @@ -6,7 +6,7 @@ description: >- creating .testagent/plan.md from research. name: code-testing-planner user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-researcher/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-researcher/AGENT.md index 56c277c..a03eadf 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-researcher/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-researcher/AGENT.md @@ -6,7 +6,7 @@ description: >- discovering test frameworks and build commands, producing .testagent/research.md. name: code-testing-researcher user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/catalog/Testing/Official-DotNet-Test/agents/code-testing-tester/AGENT.md b/catalog/Testing/Official-DotNet-Test/agents/code-testing-tester/AGENT.md index c9c196a..e34e50e 100644 --- a/catalog/Testing/Official-DotNet-Test/agents/code-testing-tester/AGENT.md +++ b/catalog/Testing/Official-DotNet-Test/agents/code-testing-tester/AGENT.md @@ -6,7 +6,7 @@ description: >- checking test results and failures. name: code-testing-tester user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/catalog/Testing/Official-DotNet-Test/skills/assertion-quality/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/assertion-quality/SKILL.md index a77e41f..fa8b630 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/assertion-quality/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/assertion-quality/SKILL.md @@ -116,6 +116,8 @@ Before reporting, calibrate findings: ### Step 6: Report findings +**Scale the report depth to the size and complexity of the suite.** The structure below is the full template for a substantial suite (roughly 15+ tests or a multi-file project). For a small or simple input (a single file with only a handful of tests), do not emit every section — a padded multi-section dashboard on a trivial input reads as noise and buries the answer. Instead, answer the user's question directly and concisely: which tests are assertion-free or trivial-only, the overall assertion-quality verdict, and concrete recommendations (still distinguishing intentional smoke tests from tests masquerading as real verification). Use only the sections that carry real signal for the input at hand; a short metric summary plus the assertion-free list and recommendations is often enough. Never omit the rubric-relevant substance (assertion-free/trivial identification, the quality verdict, and concrete recommendations) — only trim structural overhead that adds no information. + Present the analysis in this structure: 1. **Summary Dashboard** — A quick-reference table of key metrics: diff --git a/catalog/Testing/Official-DotNet-Test/skills/generate-testability-wrappers/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/generate-testability-wrappers/SKILL.md index ec4415b..6fab470 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/generate-testability-wrappers/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/generate-testability-wrappers/SKILL.md @@ -1,17 +1,18 @@ --- name: generate-testability-wrappers description: > - Generate wrapper interfaces and DI registration for hard-to-test static dependencies in C#. - Produces IFileSystem, IEnvironmentProvider, IConsole, IProcessRunner wrappers, or guides adoption - of TimeProvider and IHttpClientFactory. + Generate wrapper interfaces and DI registration for hard-to-test static dependencies in C#, + when the abstraction does NOT exist yet. Produces IFileSystem, IEnvironmentProvider, IConsole, + IProcessRunner wrappers, or guides first-time adoption of TimeProvider and IHttpClientFactory + and registering them in DI. USE FOR: generate wrapper for static, create IFileSystem wrapper, wrap DateTime.Now, make static testable, make class testable, create abstraction for File.*, generate - DI registration, TimeProvider adoption, IHttpClientFactory setup, testability wrapper, - mock-friendly interface, mock time in tests, create the right abstraction to mock, - how to mock DateTime, test code using File.ReadAllText, what abstraction for Environment, - how to make statics injectable, adopt System.IO.Abstractions, make file calls testable. - DO NOT USE FOR: detecting statics (use detect-static-dependencies), migrating call - sites (use migrate-static-to-wrapper), general interface design not about testability. + DI registration, set up/adopt TimeProvider when it is not registered yet, IHttpClientFactory + setup, testability wrapper, create the right abstraction to mock, what abstraction for + Environment, how to make statics injectable, adopt System.IO.Abstractions. + DO NOT USE FOR: detecting statics (use detect-static-dependencies), migrating + call sites or replacing existing DateTime.*/File.* usages once the wrapper is created + or already registered in DI (use migrate-static-to-wrapper), general interface design. license: MIT --- diff --git a/catalog/Testing/Official-DotNet-Test/skills/migrate-static-to-wrapper/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/migrate-static-to-wrapper/SKILL.md index 9234863..c85c139 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/migrate-static-to-wrapper/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/migrate-static-to-wrapper/SKILL.md @@ -1,18 +1,19 @@ --- name: migrate-static-to-wrapper description: > - Mechanically replace static dependency call sites with wrapper or built-in - abstraction calls across a bounded scope (file, project, or namespace). - Performs codemod-style bulk replacement of DateTime.UtcNow to TimeProvider.GetUtcNow(), - File.ReadAllText to IFileSystem, and similar transformations. Adds constructor - injection parameters and updates DI registration. - USE FOR: replace DateTime.Now/UtcNow with TimeProvider, migrate static calls - to wrapper, bulk replace File.* with IFileSystem, codemod static to - injectable, add constructor injection for a dependency, mechanical or scoped - migration of statics, convert static calls to use an abstraction, update call - sites. - DO NOT USE FOR: detecting statics (use detect-static-dependencies), generating - wrappers (use generate-testability-wrappers), migrating between test frameworks. + Replace existing static dependency call sites with wrapper or built-in + abstraction calls when the abstraction already exists or is already registered + in DI. Codemod-style bulk replacement of DateTime.Now/UtcNow to TimeProvider, + File.ReadAllText to IFileSystem, and similar, across a bounded scope (file, + project, or namespace). Adds the constructor injection parameter to affected classes. + USE FOR: replace all DateTime.UtcNow/DateTime.Now calls with TimeProvider and add + the constructor parameter, TimeProvider already registered in DI so migrate the call + sites, migrate static calls to wrapper, bulk replace File.* with IFileSystem, codemod + static to injectable, add constructor injection for an existing dependency, scoped + migration of statics, migrate statics in only certain scoped files. + DO NOT USE FOR: detecting statics (use detect-static-dependencies), creating the + wrapper or registering it when it does not exist yet (use + generate-testability-wrappers), migrating between test frameworks. license: MIT --- @@ -89,6 +90,39 @@ Add the new dependency following the class's existing pattern: - **Primary constructor** (C# 12+): Add parameter to primary constructor: `public class OrderProcessor(ILogger logger, TimeProvider timeProvider)` - **Traditional constructor**: Add `private readonly` field + constructor parameter, matching the existing field naming convention (`_camelCase` or `m_camelCase`) +#### Static classes: use ambient context (no constructor injection) + +A `static` class with only static members **cannot** receive constructor injection — adding an instance constructor or instance field would break it. Do **not** convert it to a non-static class just to inject the dependency; that changes its design and every call site. Instead, apply the **ambient context** pattern: expose a static, settable seam that defaults to the real implementation and is overridden once at composition/test setup. + +```csharp +public static class TimestampFormatter +{ + // Ambient seam — defaults to the real clock, swap in tests. + public static TimeProvider Clock { get; set; } = TimeProvider.System; + + public static string Now() => Clock.GetUtcNow().ToString("O"); +} +``` + +- Production: leave `Clock` at its `TimeProvider.System` default, or assign the DI-resolved `TimeProvider` once at startup (`TimestampFormatter.Clock = app.Services.GetRequiredService();`). +- Tests: override `Clock` with a `FakeTimeProvider` and **always restore it in a `finally`** so a failing assertion can't leak the fake into other tests: + + ```csharp + var original = TimestampFormatter.Clock; + TimestampFormatter.Clock = new FakeTimeProvider(instant); + try + { + // exercise code under test + } + finally + { + TimestampFormatter.Clock = original; + } + ``` + +- **Parallelism caveat**: a mutable static seam is process-global. Tests that mutate it must **not** run in parallel with each other (or with code that reads it) — put them in a non-parallel collection/class (e.g. xUnit `[Collection]` with parallelization disabled, or MSTest `[DoNotParallelize]`). If tests must run in parallel, prefer constructor injection (convert the caller) over an ambient static. +- The same seam works for other statics (`IFileSystem`, custom wrappers): a `public static X { get; set; }` defaulting to the real implementation, with the same restore-in-`finally` and non-parallel discipline. + ### Step 4: Replace call sites Perform each replacement mechanically. For each call site: @@ -171,7 +205,7 @@ Summarize what was done: | Pitfall | Solution | |---------|----------| | Replacing statics in test code | Only replace in production code; tests should use fakes/mocks | -| Breaking static classes | Static classes can't have constructors — use ambient context for these | +| Breaking static classes | Static classes can't have constructors — use the ambient context seam (Step 3) instead of converting them to non-static | | Missing `FakeTimeProvider` NuGet | Add `Microsoft.Extensions.TimeProvider.Testing` to test project | | Replacing in expression-bodied members without updating return type | `DateTime` → `DateTimeOffset` when using `TimeProvider.GetUtcNow()` — verify type compatibility | | Migrating too much at once | Stick to the defined scope — one project or namespace per run | diff --git a/catalog/Testing/Official-DotNet-Test/skills/run-tests/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/run-tests/SKILL.md index d273bd0..2dfb674 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/run-tests/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/run-tests/SKILL.md @@ -40,7 +40,7 @@ Detect the test platform and framework, run tests, and apply filters using `dotn | Input | Required | Description | |-------|----------|-------------| -| Project or solution path | No | Path to the test project (.csproj) or solution (.sln). Defaults to current directory. | +| Project or solution path | No | Path to the test project (.csproj) or solution (.sln, .slnf, .slnx). Defaults to current directory. | | Filter expression | No | Filter expression to select specific tests | | Target framework | No | Target framework moniker to run against (e.g., `net8.0`) | @@ -154,6 +154,8 @@ dotnet test --project path/to/ # Run all tests in a solution (sln, slnf, slnx) dotnet test --solution path/to/MySolution.sln +dotnet test --solution path/to/MySolution.slnf +dotnet test --solution path/to/MySolution.slnx # Run all tests in a directory containing a solution dotnet test --solution path/to/ diff --git a/catalog/Testing/Official-DotNet-Test/skills/writing-mstest-tests/SKILL.md b/catalog/Testing/Official-DotNet-Test/skills/writing-mstest-tests/SKILL.md index c14b8b2..f9d0264 100644 --- a/catalog/Testing/Official-DotNet-Test/skills/writing-mstest-tests/SKILL.md +++ b/catalog/Testing/Official-DotNet-Test/skills/writing-mstest-tests/SKILL.md @@ -2,17 +2,18 @@ name: writing-mstest-tests description: > Write, create, modernize, or fix comprehensive MSTest unit tests with MSTest 3.x/4.x APIs. - USE FOR: write or create MSTest unit tests, fix/modernize MSTest assertions, - better MSTest assertion than Assert.IsTrue, replace hard cast with type check (IsInstanceOfType), + USE FOR: write, create, review, or modernize MSTest tests and assertions, + better MSTest assertion than Assert.IsTrue, replace hard cast with IsInstanceOfType, MSTest assertion APIs (Contains, ContainsSingle, HasCount, IsEmpty, IsNotEmpty, DoesNotContain, AreSame, IsNull, StartsWith, EndsWith, MatchesRegex, IsGreaterThan, IsLessThan, IsInRange), - swapped Assert.AreEqual args, replace ExpectedException with Assert.Throws, + swapped/reversed Assert.AreEqual args (Expected/Actual backwards), + replace ExpectedException with Assert.Throws, data-driven (DataRow, DynamicData, ValueTuples), lifecycle (TestInitialize, TestCleanup, TestContext), - async tests and cancellation tokens, conditional execution/retry/cleanup (OSCondition, Retry), + async and cancellation tests, conditional execution/retry/cleanup (OSCondition, Retry), parallelization (Parallelize/DoNotParallelize), MSTest.Sdk setup, MSTESTxxxx analyzer fixes. DO NOT USE FOR: test quality audits (use test-anti-patterns), - running tests (use run-tests), MSTest version migration (use the migrate-mstest skills), + running tests (use run-tests), MSTest version migration (use migrate-mstest skills), xUnit/NUnit/TUnit, or non-.NET languages. license: MIT --- diff --git a/catalog/Tools/Official-DotNet-Diagnostics/agents/optimizing-dotnet-performance/AGENT.md b/catalog/Tools/Official-DotNet-Diagnostics/agents/optimizing-dotnet-performance/AGENT.md index a0af47d..6a71cd4 100644 --- a/catalog/Tools/Official-DotNet-Diagnostics/agents/optimizing-dotnet-performance/AGENT.md +++ b/catalog/Tools/Official-DotNet-Diagnostics/agents/optimizing-dotnet-performance/AGENT.md @@ -1,7 +1,7 @@ --- description: "Analyzes .NET code for performance bottlenecks, recommends concrete optimizations, and guides benchmarking. Scans for ~50 anti-patterns across async, memory, strings, collections, LINQ, regex, serialization, and I/O. Use when reviewing .NET code performance, optimizing hot paths, reducing allocations, or tuning async/concurrency patterns." name: optimizing-dotnet-performance -tools: ['read', 'search', 'edit', 'task', 'skill', 'web_search', 'web_fetch', 'ask_user'] +tools: ['read', 'search', 'edit', 'task', 'skill', 'web_search', 'web_fetch', 'ask_user', 'Read', 'Glob', 'Grep', 'Edit', 'Write', 'Skill', 'read_file', 'replace', 'write_file', 'glob', 'grep_search'] license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-diag/agents/optimizing-dotnet-performance.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-diag/agents/optimizing-dotnet-performance.agent.md index a0af47d..6a71cd4 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-diag/agents/optimizing-dotnet-performance.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-diag/agents/optimizing-dotnet-performance.agent.md @@ -1,7 +1,7 @@ --- description: "Analyzes .NET code for performance bottlenecks, recommends concrete optimizations, and guides benchmarking. Scans for ~50 anti-patterns across async, memory, strings, collections, LINQ, regex, serialization, and I/O. Use when reviewing .NET code performance, optimizing hot paths, reducing allocations, or tuning async/concurrency patterns." name: optimizing-dotnet-performance -tools: ['read', 'search', 'edit', 'task', 'skill', 'web_search', 'web_fetch', 'ask_user'] +tools: ['read', 'search', 'edit', 'task', 'skill', 'web_search', 'web_fetch', 'ask_user', 'Read', 'Glob', 'Grep', 'Edit', 'Write', 'Skill', 'read_file', 'replace', 'write_file', 'glob', 'grep_search'] license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-builder.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-builder.agent.md index c333c1a..3155edf 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-builder.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-builder.agent.md @@ -6,7 +6,7 @@ description: >- errors, verifying project builds successfully. name: code-testing-builder user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-fixer.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-fixer.agent.md index 9787511..2745add 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-fixer.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-fixer.agent.md @@ -6,7 +6,7 @@ description: >- imports, correcting type mismatches, fixing compilation failures. name: code-testing-fixer user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-generator.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-generator.agent.md index 3127b7c..1c21e65 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-generator.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-generator.agent.md @@ -6,7 +6,7 @@ description: >- coverage plateaus or project-wide coverage/CRAP analysis without writing tests (use coverage-analysis); targeted method/class CRAP scores (use crap-score). name: code-testing-generator -tools: ["agent", "skill", "read", "search", "edit", "execute"] +tools: ["agent", "skill", "read", "search", "edit", "execute", "Task", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] agents: - code-testing-researcher - code-testing-planner diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-implementer.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-implementer.agent.md index 18ed700..7cb2d09 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-implementer.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-implementer.agent.md @@ -7,7 +7,7 @@ description: >- running build-test-fix cycle for generated tests. name: code-testing-implementer user-invocable: false -tools: ["agent", "skill", "read", "search", "edit", "execute"] +tools: ["agent", "skill", "read", "search", "edit", "execute", "Task", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] agents: - code-testing-builder - code-testing-tester diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-linter.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-linter.agent.md index ff8de44..d81a733 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-linter.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-linter.agent.md @@ -6,7 +6,7 @@ description: >- applying lint fixes. name: code-testing-linter user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-planner.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-planner.agent.md index af02e32..17a47de 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-planner.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-planner.agent.md @@ -6,7 +6,7 @@ description: >- creating .testagent/plan.md from research. name: code-testing-planner user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-researcher.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-researcher.agent.md index 56c277c..a03eadf 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-researcher.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-researcher.agent.md @@ -6,7 +6,7 @@ description: >- discovering test frameworks and build commands, producing .testagent/research.md. name: code-testing-researcher user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-tester.agent.md b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-tester.agent.md index c9c196a..e34e50e 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-tester.agent.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/agents/code-testing-tester.agent.md @@ -6,7 +6,7 @@ description: >- checking test results and failures. name: code-testing-tester user-invocable: false -tools: ["skill", "read", "search", "edit", "execute"] +tools: ["skill", "read", "search", "edit", "execute", "Skill", "Read", "Glob", "Grep", "Edit", "Write", "Bash", "read_file", "replace", "write_file", "glob", "grep_search", "run_shell_command"] license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/assertion-quality/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/assertion-quality/SKILL.md index a77e41f..fa8b630 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/assertion-quality/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/assertion-quality/SKILL.md @@ -116,6 +116,8 @@ Before reporting, calibrate findings: ### Step 6: Report findings +**Scale the report depth to the size and complexity of the suite.** The structure below is the full template for a substantial suite (roughly 15+ tests or a multi-file project). For a small or simple input (a single file with only a handful of tests), do not emit every section — a padded multi-section dashboard on a trivial input reads as noise and buries the answer. Instead, answer the user's question directly and concisely: which tests are assertion-free or trivial-only, the overall assertion-quality verdict, and concrete recommendations (still distinguishing intentional smoke tests from tests masquerading as real verification). Use only the sections that carry real signal for the input at hand; a short metric summary plus the assertion-free list and recommendations is often enough. Never omit the rubric-relevant substance (assertion-free/trivial identification, the quality verdict, and concrete recommendations) — only trim structural overhead that adds no information. + Present the analysis in this structure: 1. **Summary Dashboard** — A quick-reference table of key metrics: diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/generate-testability-wrappers/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/generate-testability-wrappers/SKILL.md index ec4415b..6fab470 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/generate-testability-wrappers/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/generate-testability-wrappers/SKILL.md @@ -1,17 +1,18 @@ --- name: generate-testability-wrappers description: > - Generate wrapper interfaces and DI registration for hard-to-test static dependencies in C#. - Produces IFileSystem, IEnvironmentProvider, IConsole, IProcessRunner wrappers, or guides adoption - of TimeProvider and IHttpClientFactory. + Generate wrapper interfaces and DI registration for hard-to-test static dependencies in C#, + when the abstraction does NOT exist yet. Produces IFileSystem, IEnvironmentProvider, IConsole, + IProcessRunner wrappers, or guides first-time adoption of TimeProvider and IHttpClientFactory + and registering them in DI. USE FOR: generate wrapper for static, create IFileSystem wrapper, wrap DateTime.Now, make static testable, make class testable, create abstraction for File.*, generate - DI registration, TimeProvider adoption, IHttpClientFactory setup, testability wrapper, - mock-friendly interface, mock time in tests, create the right abstraction to mock, - how to mock DateTime, test code using File.ReadAllText, what abstraction for Environment, - how to make statics injectable, adopt System.IO.Abstractions, make file calls testable. - DO NOT USE FOR: detecting statics (use detect-static-dependencies), migrating call - sites (use migrate-static-to-wrapper), general interface design not about testability. + DI registration, set up/adopt TimeProvider when it is not registered yet, IHttpClientFactory + setup, testability wrapper, create the right abstraction to mock, what abstraction for + Environment, how to make statics injectable, adopt System.IO.Abstractions. + DO NOT USE FOR: detecting statics (use detect-static-dependencies), migrating + call sites or replacing existing DateTime.*/File.* usages once the wrapper is created + or already registered in DI (use migrate-static-to-wrapper), general interface design. license: MIT --- diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-static-to-wrapper/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-static-to-wrapper/SKILL.md index 9234863..c85c139 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-static-to-wrapper/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/migrate-static-to-wrapper/SKILL.md @@ -1,18 +1,19 @@ --- name: migrate-static-to-wrapper description: > - Mechanically replace static dependency call sites with wrapper or built-in - abstraction calls across a bounded scope (file, project, or namespace). - Performs codemod-style bulk replacement of DateTime.UtcNow to TimeProvider.GetUtcNow(), - File.ReadAllText to IFileSystem, and similar transformations. Adds constructor - injection parameters and updates DI registration. - USE FOR: replace DateTime.Now/UtcNow with TimeProvider, migrate static calls - to wrapper, bulk replace File.* with IFileSystem, codemod static to - injectable, add constructor injection for a dependency, mechanical or scoped - migration of statics, convert static calls to use an abstraction, update call - sites. - DO NOT USE FOR: detecting statics (use detect-static-dependencies), generating - wrappers (use generate-testability-wrappers), migrating between test frameworks. + Replace existing static dependency call sites with wrapper or built-in + abstraction calls when the abstraction already exists or is already registered + in DI. Codemod-style bulk replacement of DateTime.Now/UtcNow to TimeProvider, + File.ReadAllText to IFileSystem, and similar, across a bounded scope (file, + project, or namespace). Adds the constructor injection parameter to affected classes. + USE FOR: replace all DateTime.UtcNow/DateTime.Now calls with TimeProvider and add + the constructor parameter, TimeProvider already registered in DI so migrate the call + sites, migrate static calls to wrapper, bulk replace File.* with IFileSystem, codemod + static to injectable, add constructor injection for an existing dependency, scoped + migration of statics, migrate statics in only certain scoped files. + DO NOT USE FOR: detecting statics (use detect-static-dependencies), creating the + wrapper or registering it when it does not exist yet (use + generate-testability-wrappers), migrating between test frameworks. license: MIT --- @@ -89,6 +90,39 @@ Add the new dependency following the class's existing pattern: - **Primary constructor** (C# 12+): Add parameter to primary constructor: `public class OrderProcessor(ILogger logger, TimeProvider timeProvider)` - **Traditional constructor**: Add `private readonly` field + constructor parameter, matching the existing field naming convention (`_camelCase` or `m_camelCase`) +#### Static classes: use ambient context (no constructor injection) + +A `static` class with only static members **cannot** receive constructor injection — adding an instance constructor or instance field would break it. Do **not** convert it to a non-static class just to inject the dependency; that changes its design and every call site. Instead, apply the **ambient context** pattern: expose a static, settable seam that defaults to the real implementation and is overridden once at composition/test setup. + +```csharp +public static class TimestampFormatter +{ + // Ambient seam — defaults to the real clock, swap in tests. + public static TimeProvider Clock { get; set; } = TimeProvider.System; + + public static string Now() => Clock.GetUtcNow().ToString("O"); +} +``` + +- Production: leave `Clock` at its `TimeProvider.System` default, or assign the DI-resolved `TimeProvider` once at startup (`TimestampFormatter.Clock = app.Services.GetRequiredService();`). +- Tests: override `Clock` with a `FakeTimeProvider` and **always restore it in a `finally`** so a failing assertion can't leak the fake into other tests: + + ```csharp + var original = TimestampFormatter.Clock; + TimestampFormatter.Clock = new FakeTimeProvider(instant); + try + { + // exercise code under test + } + finally + { + TimestampFormatter.Clock = original; + } + ``` + +- **Parallelism caveat**: a mutable static seam is process-global. Tests that mutate it must **not** run in parallel with each other (or with code that reads it) — put them in a non-parallel collection/class (e.g. xUnit `[Collection]` with parallelization disabled, or MSTest `[DoNotParallelize]`). If tests must run in parallel, prefer constructor injection (convert the caller) over an ambient static. +- The same seam works for other statics (`IFileSystem`, custom wrappers): a `public static X { get; set; }` defaulting to the real implementation, with the same restore-in-`finally` and non-parallel discipline. + ### Step 4: Replace call sites Perform each replacement mechanically. For each call site: @@ -171,7 +205,7 @@ Summarize what was done: | Pitfall | Solution | |---------|----------| | Replacing statics in test code | Only replace in production code; tests should use fakes/mocks | -| Breaking static classes | Static classes can't have constructors — use ambient context for these | +| Breaking static classes | Static classes can't have constructors — use the ambient context seam (Step 3) instead of converting them to non-static | | Missing `FakeTimeProvider` NuGet | Add `Microsoft.Extensions.TimeProvider.Testing` to test project | | Replacing in expression-bodied members without updating return type | `DateTime` → `DateTimeOffset` when using `TimeProvider.GetUtcNow()` — verify type compatibility | | Migrating too much at once | Stick to the defined scope — one project or namespace per run | diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/run-tests/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/run-tests/SKILL.md index d273bd0..2dfb674 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/run-tests/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/run-tests/SKILL.md @@ -40,7 +40,7 @@ Detect the test platform and framework, run tests, and apply filters using `dotn | Input | Required | Description | |-------|----------|-------------| -| Project or solution path | No | Path to the test project (.csproj) or solution (.sln). Defaults to current directory. | +| Project or solution path | No | Path to the test project (.csproj) or solution (.sln, .slnf, .slnx). Defaults to current directory. | | Filter expression | No | Filter expression to select specific tests | | Target framework | No | Target framework moniker to run against (e.g., `net8.0`) | @@ -154,6 +154,8 @@ dotnet test --project path/to/ # Run all tests in a solution (sln, slnf, slnx) dotnet test --solution path/to/MySolution.sln +dotnet test --solution path/to/MySolution.slnf +dotnet test --solution path/to/MySolution.slnx # Run all tests in a directory containing a solution dotnet test --solution path/to/ diff --git a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/writing-mstest-tests/SKILL.md b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/writing-mstest-tests/SKILL.md index c14b8b2..f9d0264 100644 --- a/external-sources/upstreams/dotnet-skills/dotnet-test/skills/writing-mstest-tests/SKILL.md +++ b/external-sources/upstreams/dotnet-skills/dotnet-test/skills/writing-mstest-tests/SKILL.md @@ -2,17 +2,18 @@ name: writing-mstest-tests description: > Write, create, modernize, or fix comprehensive MSTest unit tests with MSTest 3.x/4.x APIs. - USE FOR: write or create MSTest unit tests, fix/modernize MSTest assertions, - better MSTest assertion than Assert.IsTrue, replace hard cast with type check (IsInstanceOfType), + USE FOR: write, create, review, or modernize MSTest tests and assertions, + better MSTest assertion than Assert.IsTrue, replace hard cast with IsInstanceOfType, MSTest assertion APIs (Contains, ContainsSingle, HasCount, IsEmpty, IsNotEmpty, DoesNotContain, AreSame, IsNull, StartsWith, EndsWith, MatchesRegex, IsGreaterThan, IsLessThan, IsInRange), - swapped Assert.AreEqual args, replace ExpectedException with Assert.Throws, + swapped/reversed Assert.AreEqual args (Expected/Actual backwards), + replace ExpectedException with Assert.Throws, data-driven (DataRow, DynamicData, ValueTuples), lifecycle (TestInitialize, TestCleanup, TestContext), - async tests and cancellation tokens, conditional execution/retry/cleanup (OSCondition, Retry), + async and cancellation tests, conditional execution/retry/cleanup (OSCondition, Retry), parallelization (Parallelize/DoNotParallelize), MSTest.Sdk setup, MSTESTxxxx analyzer fixes. DO NOT USE FOR: test quality audits (use test-anti-patterns), - running tests (use run-tests), MSTest version migration (use the migrate-mstest skills), + running tests (use run-tests), MSTest version migration (use migrate-mstest skills), xUnit/NUnit/TUnit, or non-.NET languages. license: MIT --- diff --git a/external-sources/vendir.lock.yml b/external-sources/vendir.lock.yml index 74dfd3b..43a0472 100644 --- a/external-sources/vendir.lock.yml +++ b/external-sources/vendir.lock.yml @@ -2,11 +2,10 @@ apiVersion: vendir.k14s.io/v1alpha1 directories: - contents: - git: - commitTitle: 'dotnet-test: raise timeouts for chronic-timeout eval scenarios - (#850)...' - sha: ce75c355694bb358162464058081121645767d6f + commitTitle: Fix migrate-static-to-wrapper skill activation on evals (#864)... + sha: 1270722c951dece787074c8fde3bdab2f8ef54db tags: - - skill-validator-nightly + - skill-validator-nightly-5-g1270722 path: dotnet-skills path: upstreams kind: LockConfig