From 7cae07497b91ab9df4d1578851c7862d511bf8a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Hern=C3=A1ndez?= Date: Mon, 7 Sep 2026 23:44:57 -0400 Subject: [PATCH] Move the page to /word-addin: the deploy already owns /word MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The navigation link led to the task pane, not to the page written for it. On the host a real directory beats a client-side route, and the deploy publishes the pane at /word/ — so /word answered with the pane, which outside Word says it has no document to read. Invisible locally, because a dev server has neither directory. Invisible in the diff, because the two halves are in different files: an @page in a component and a cp in a workflow. The pane keeps its address, because that address is in a manifest people have already installed. The page moves. DeployedPathTests guards both ends of the seam. One reads every @page route from the components and the pane's directory from the workflow, and fails when they collide; the other checks the manifest's SourceLocation still points at the directory the deploy actually writes, which is the failure that made the first install report an add-in error. Verified by mutation: put /word back and the first fails by name. 434 tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015PEbbiYSNPw7jE3LrPNhyF --- README.md | 2 +- src/SignsOfAI.UI/Layout/MainLayout.razor | 2 +- src/SignsOfAI.UI/Pages/Download.razor | 2 +- src/SignsOfAI.UI/Pages/WordAddin.razor | 5 +- .../SignsOfAI.Core.Tests/DeployedPathTests.cs | 90 +++++++++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 tests/SignsOfAI.Core.Tests/DeployedPathTests.cs diff --git a/README.md b/README.md index 85123cb..eb4e0a8 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ A tool that answers everything is easy to build. This is the other kind. ### Installing it -**[The add-in's page →](https://peopleworks.github.io/SignsofAI/word)** — what it does, both +**[The add-in's page →](https://peopleworks.github.io/SignsofAI/word-addin)** — what it does, both install routes, and what it refuses to do, in English and Spanish. The short version: on the web it is Home → Add-ins → More Add-ins → My Add-ins → **Upload My Add-in** diff --git a/src/SignsOfAI.UI/Layout/MainLayout.razor b/src/SignsOfAI.UI/Layout/MainLayout.razor index 1a28574..6984f0e 100644 --- a/src/SignsOfAI.UI/Layout/MainLayout.razor +++ b/src/SignsOfAI.UI/Layout/MainLayout.razor @@ -24,7 +24,7 @@ @L["nav.catalog"] @* No host condition, unlike the two below: somebody using the desktop app writes in Word too, and the add-in is useful to them for the same reasons. *@ - @L["nav.word"] + @L["nav.word"] @* Only where there is something to gain by downloading it. The same signal as the folder link above, and for the same reason: this host cannot open a folder, so a better host exists for this machine and hiding that would be the lowest-common- diff --git a/src/SignsOfAI.UI/Pages/Download.razor b/src/SignsOfAI.UI/Pages/Download.razor index cffcd10..d112919 100644 --- a/src/SignsOfAI.UI/Pages/Download.razor +++ b/src/SignsOfAI.UI/Pages/Download.razor @@ -113,7 +113,7 @@ else

@L["dl.word.title"]

@L["dl.word.lede"]

diff --git a/src/SignsOfAI.UI/Pages/WordAddin.razor b/src/SignsOfAI.UI/Pages/WordAddin.razor index 7f45b16..c15e143 100644 --- a/src/SignsOfAI.UI/Pages/WordAddin.razor +++ b/src/SignsOfAI.UI/Pages/WordAddin.razor @@ -1,4 +1,7 @@ -@page "/word" +@* /word-addin, not /word: the deploy publishes the task pane itself at /word/, and a directory + on the host wins over a route in the router. The pane's address is in a manifest people have + already installed, so the pane keeps it and the page moves. *@ +@page "/word-addin" @using SignsOfAI.Core.Calibration @inherits LocalizedComponent diff --git a/tests/SignsOfAI.Core.Tests/DeployedPathTests.cs b/tests/SignsOfAI.Core.Tests/DeployedPathTests.cs new file mode 100644 index 0000000..3bf020e --- /dev/null +++ b/tests/SignsOfAI.Core.Tests/DeployedPathTests.cs @@ -0,0 +1,90 @@ +using System.Text.RegularExpressions; +using Xunit; + +namespace SignsOfAI.Core.Tests; + +/// +/// The deploy publishes two applications onto one host: the web app at the site root, and the Word +/// task pane in a subdirectory of it. A real directory beats a client-side route, so any page whose +/// @page path matches that subdirectory is unreachable — the visitor gets the task pane, +/// which outside Word says it has no document to read. +/// +/// That happened. /word was given to a page while the deploy was already publishing the pane +/// at /word/, and the navigation link led to the pane for as long as it took to notice. It is +/// invisible locally, because a dev server has neither directory, and invisible in the diff, because +/// the two halves live in different files. +/// +public class DeployedPathTests +{ + private static string RepoRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, "src"))) + dir = dir.Parent; + + Assert.NotNull(dir); + return dir!.FullName; + } + + /// Every route the router owns, from the @page directives themselves. + private static IReadOnlyList Routes() + { + string pages = Path.Combine(RepoRoot(), "src", "SignsOfAI.UI", "Pages"); + var routes = new List(); + + foreach (string file in Directory.EnumerateFiles(pages, "*.razor")) + foreach (Match m in Regex.Matches(File.ReadAllText(file), @"^@page\s+""/([^""]*)""", RegexOptions.Multiline)) + routes.Add(m.Groups[1].Value.Trim('/')); + + Assert.NotEmpty(routes); + return routes; + } + + /// + /// The directory the deploy copies the pane into, taken from the workflow rather than repeated + /// here — the point of this test is that the two are compared, not that both are typed twice. + /// + private static string PaneDirectory() + { + string workflow = File.ReadAllText( + Path.Combine(RepoRoot(), ".github", "workflows", "deploy-pages.yml")); + + var m = Regex.Match(workflow, @"cp -r publish-word/wwwroot publish/wwwroot/(\S+)"); + Assert.True(m.Success, + "deploy-pages.yml no longer copies the task pane where this test expects. If the step " + + "moved or was renamed, update this pattern — do not delete the check."); + + return m.Groups[1].Value.Trim('/'); + } + + [Fact] + public void No_page_route_is_shadowed_by_the_task_pane_directory() + { + string pane = PaneDirectory(); + var clash = Routes().FirstOrDefault(r => string.Equals(r, pane, StringComparison.OrdinalIgnoreCase)); + + Assert.True(clash is null, + $"A page is routed at \"/{clash}\" and the deploy publishes the task pane at \"/{pane}/\". " + + "On the host the directory wins and the page is unreachable — visitors get the pane. " + + "Move the page's route; the pane's address is in a manifest people have installed."); + } + + [Fact] + public void The_manifest_points_at_the_directory_the_deploy_publishes() + { + // The other half of the same seam: the pane's URL lives in the manifest Word loads, and the + // deploy decides where the files land. If those two disagree the add-in 404s, which Word + // reports as "this add-in may not load properly" — an error a long way from its cause. + string manifest = File.ReadAllText( + Path.Combine(RepoRoot(), "src", "SignsOfAI.Word", "manifest.xml")); + + var source = Regex.Match(manifest, @"= 2, $"SourceLocation \"{path}\" has no subdirectory to compare."); + Assert.Equal(PaneDirectory(), parts[^2]); + } +}