From e00d0b1541b50e55ec2ccf999ea510760dcc5793 Mon Sep 17 00:00:00 2001
From: Steven Pillow <89219344+S-Pillow@users.noreply.github.com>
Date: Thu, 20 Aug 2026 01:52:10 -0400
Subject: [PATCH 1/4] Add link parser regression tests
Cover email overlap, URL normalization, punctuation trimming, balanced delimiters, offset behavior, and document ordering with 13 xUnit cases.
---
WorkNotes.Tests/LinkTextParserTests.cs | 89 ++++++++++++++++++++++++++
WorkNotes.Tests/WorkNotes.Tests.csproj | 29 +++++++++
2 files changed, 118 insertions(+)
create mode 100644 WorkNotes.Tests/LinkTextParserTests.cs
create mode 100644 WorkNotes.Tests/WorkNotes.Tests.csproj
diff --git a/WorkNotes.Tests/LinkTextParserTests.cs b/WorkNotes.Tests/LinkTextParserTests.cs
new file mode 100644
index 0000000..ce45eee
--- /dev/null
+++ b/WorkNotes.Tests/LinkTextParserTests.cs
@@ -0,0 +1,89 @@
+using WorkNotes.Services;
+
+namespace WorkNotes.Tests;
+
+public sealed class LinkTextParserTests
+{
+ [Fact]
+ public void FindLinks_ReturnsNoLinksForEmptyText()
+ {
+ Assert.Empty(LinkTextParser.FindLinks(string.Empty));
+ }
+
+ [Fact]
+ public void FindLinks_DetectsEmailWithoutOverlappingDomain()
+ {
+ const string text = "Contact dev@example.com for help.";
+
+ var link = Assert.Single(LinkTextParser.FindLinks(text));
+
+ Assert.Equal("dev@example.com", link.DisplayText);
+ Assert.Equal("mailto:dev@example.com", link.Url);
+ Assert.Equal(text.IndexOf("dev@example.com", StringComparison.Ordinal), link.StartOffset);
+ Assert.Equal(link.StartOffset + link.DisplayText.Length, link.EndOffset);
+ }
+
+ [Theory]
+ [InlineData("https://example.com/docs", "https://example.com/docs")]
+ [InlineData("http://example.com", "http://example.com")]
+ [InlineData("www.example.com/path", "https://www.example.com/path")]
+ [InlineData("example.com/path?q=1#top", "https://example.com/path?q=1#top")]
+ public void FindLinks_NormalizesSupportedUrlForms(string text, string expectedUrl)
+ {
+ var link = Assert.Single(LinkTextParser.FindLinks(text));
+
+ Assert.Equal(text, link.DisplayText);
+ Assert.Equal(expectedUrl, link.Url);
+ }
+
+ [Theory]
+ [InlineData("Visit example.com.", "example.com")]
+ [InlineData("Visit example.com, then continue", "example.com")]
+ [InlineData("Visit (example.com)", "example.com")]
+ [InlineData("Visit [example.com]", "example.com")]
+ public void FindLinks_TrimsSentencePunctuation(string text, string expectedDisplayText)
+ {
+ var link = Assert.Single(LinkTextParser.FindLinks(text));
+
+ Assert.Equal(expectedDisplayText, link.DisplayText);
+ Assert.Equal("https://" + expectedDisplayText, link.Url);
+ }
+
+ [Fact]
+ public void FindLinks_PreservesBalancedParenthesesInUrl()
+ {
+ const string text = "https://example.com/wiki/Function_(mathematics)";
+
+ var link = Assert.Single(LinkTextParser.FindLinks(text));
+
+ Assert.Equal(text, link.DisplayText);
+ Assert.Equal(text.Length, link.EndOffset);
+ }
+
+ [Fact]
+ public void FindLinks_AppliesBaseOffsetToExclusiveRange()
+ {
+ const string text = "example.com";
+
+ var link = Assert.Single(LinkTextParser.FindLinks(text, baseOffset: 17));
+
+ Assert.Equal(17, link.StartOffset);
+ Assert.Equal(17 + text.Length, link.EndOffset);
+ }
+
+ [Fact]
+ public void FindLinks_ReturnsMixedLinksInDocumentOrder()
+ {
+ const string text = "See example.com or email team@example.org, then visit https://docs.example.net/start.";
+
+ var links = LinkTextParser.FindLinks(text);
+
+ Assert.Collection(
+ links,
+ link => Assert.Equal("example.com", link.DisplayText),
+ link => Assert.Equal("team@example.org", link.DisplayText),
+ link => Assert.Equal("https://docs.example.net/start", link.DisplayText));
+ Assert.True(links[0].EndOffset <= links[1].StartOffset);
+ Assert.True(links[1].EndOffset <= links[2].StartOffset);
+ }
+}
diff --git a/WorkNotes.Tests/WorkNotes.Tests.csproj b/WorkNotes.Tests/WorkNotes.Tests.csproj
new file mode 100644
index 0000000..f09b6d0
--- /dev/null
+++ b/WorkNotes.Tests/WorkNotes.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net8.0-windows
+ Exe
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
From 2d39e16afc6c65140698f733405563e83077183e Mon Sep 17 00:00:00 2001
From: Steven Pillow <89219344+S-Pillow@users.noreply.github.com>
Date: Thu, 20 Aug 2026 01:55:05 -0400
Subject: [PATCH 2/4] Add regression tests to the solution
Include WorkNotes.Tests in Debug and Release solution configurations.
---
WorkNotes.sln | 30 ++++++++++++++++++------------
1 file changed, 18 insertions(+), 12 deletions(-)
diff --git a/WorkNotes.sln b/WorkNotes.sln
index 68e4aee..9284638 100644
--- a/WorkNotes.sln
+++ b/WorkNotes.sln
@@ -1,19 +1,25 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31903.59
-MinimumVisualStudioVersion = 10.0.40219.1
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkNotes", "WorkNotes\WorkNotes.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkNotes.Tests", "WorkNotes.Tests\WorkNotes.Tests.csproj", "{B0DA7728-F09E-4076-96F1-D37A0C653DBA}"
+EndProject
Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B0DA7728-F09E-4076-96F1-D37A0C653DBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B0DA7728-F09E-4076-96F1-D37A0C653DBA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B0DA7728-F09E-4076-96F1-D37A0C653DBA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B0DA7728-F09E-4076-96F1-D37A0C653DBA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
From 0cc9a51dc83240ff3100cad788033ca3f754b29e Mon Sep 17 00:00:00 2001
From: Steven Pillow <89219344+S-Pillow@users.noreply.github.com>
Date: Thu, 20 Aug 2026 01:55:27 -0400
Subject: [PATCH 3/4] Allow the test assembly to access the parser
Expose internal production types only to WorkNotes.Tests without expanding the public API.
---
WorkNotes/WorkNotes.csproj | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/WorkNotes/WorkNotes.csproj b/WorkNotes/WorkNotes.csproj
index d28bab0..4c06999 100644
--- a/WorkNotes/WorkNotes.csproj
+++ b/WorkNotes/WorkNotes.csproj
@@ -11,12 +11,16 @@
1.5.0.0
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
From 0cb31d1caece3b0eb729dc03070b35846b81fcfe Mon Sep 17 00:00:00 2001
From: Steven Pillow <89219344+S-Pillow@users.noreply.github.com>
Date: Thu, 20 Aug 2026 01:55:47 -0400
Subject: [PATCH 4/4] Run build and tests in GitHub Actions
Restore, build Release, and run the test suite on Windows for pull requests and pushes to main.
---
.github/workflows/build-and-test.yml | 33 ++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
create mode 100644 .github/workflows/build-and-test.yml
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
new file mode 100644
index 0000000..7aee387
--- /dev/null
+++ b/.github/workflows/build-and-test.yml
@@ -0,0 +1,33 @@
+name: Build and test
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ build-and-test:
+ runs-on: windows-latest
+ timeout-minutes: 10
+
+ steps:
+ - name: Check out source
+ uses: actions/checkout@v6
+
+ - name: Set up .NET 8
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore WorkNotes.sln
+
+ - name: Build release configuration
+ run: dotnet build WorkNotes.sln --configuration Release --no-restore
+
+ - name: Run tests
+ run: dotnet test WorkNotes.sln --configuration Release --no-build --no-restore --verbosity normal