From f430c63b944012c9a92fff2b7b28a310d73140e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 06:31:09 +0000 Subject: [PATCH 1/2] Resolve a relative --store path instead of aborting startup [patch] The tool copied --store verbatim into GitLfsCache:Store:Root, and the options validator requires a fully qualified root, so `gitlfscache --store ./cache` failed at startup with an unhandled OptionsValidationException. The flag is now resolved against the working directory with Path.GetFullPath, the same way --config is. The validator's absolute-path rule is unchanged, so configuration files and environment variables stay strict for container deployments. Fixes #93 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Md2Tr7FWcbqPdwq2BjeG79 --- GitLfsCache.Tests/Tool/StoreFlagTests.cs | 30 ++++++++++++++++++++++++ GitLfsCache.Tool/Program.cs | 15 +++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 GitLfsCache.Tests/Tool/StoreFlagTests.cs diff --git a/GitLfsCache.Tests/Tool/StoreFlagTests.cs b/GitLfsCache.Tests/Tool/StoreFlagTests.cs new file mode 100644 index 0000000..027de95 --- /dev/null +++ b/GitLfsCache.Tests/Tool/StoreFlagTests.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.GitLfsCache.Tests.Tool; + +using ktsu.GitLfsCache.Tool; + +[TestClass] +public class StoreFlagTests +{ + [TestMethod] + public void Store_RelativeDirectory_ResolvesAgainstTheWorkingDirectory() + { + // The options validator refuses a root that is not fully qualified, so passing `--store ./cache` + // through verbatim aborted startup with a validation stack trace. + string resolved = Program.ResolveStoreRoot("./cache"); + + Assert.IsTrue(Path.IsPathFullyQualified(resolved), resolved); + Assert.AreEqual(Path.Combine(Environment.CurrentDirectory, "cache"), resolved); + } + + [TestMethod] + public void Store_FullyQualifiedDirectory_IsKeptAsGiven() + { + string root = Path.Combine( + Path.GetPathRoot(Path.GetTempPath()) ?? Path.DirectorySeparatorChar.ToString(), + "gitlfscache"); + + Assert.AreEqual(root, Program.ResolveStoreRoot(root)); + } +} diff --git a/GitLfsCache.Tool/Program.cs b/GitLfsCache.Tool/Program.cs index 94825bb..e557f87 100644 --- a/GitLfsCache.Tool/Program.cs +++ b/GitLfsCache.Tool/Program.cs @@ -110,7 +110,7 @@ private static async Task Main(string[] args) if (parseResult.GetValue(store) is string storeRoot) { - overrides[StoreRootKey] = storeRoot; + overrides[StoreRootKey] = ResolveStoreRoot(storeRoot); } if (parseResult.GetValue(maxSize) is string budget) @@ -285,6 +285,19 @@ private static bool TryApplyUpstreams( return true; } + /// + /// Resolves the --store flag against the working directory. + /// + /// + /// The options validator requires a fully qualified root, which keeps configuration files and + /// environment variables strict for container deployments. A relative directory is what someone + /// running the tool locally types, though, so the flag resolves it the way --config does + /// rather than letting startup fail with a validation stack trace. + /// + /// The directory as given on the command line. + /// The fully qualified directory. + internal static string ResolveStoreRoot(string storeRoot) => Path.GetFullPath(storeRoot); + /// /// Groups every --allow flag by the upstream it names. /// From 8e7549b2fa4ed8d53aa9622d9502124a448e869c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 06:39:20 +0000 Subject: [PATCH 2/2] Cover the --store override inside the command action SonarCloud's new-code coverage gate counted the changed line in Main's command action as uncovered, since no test reached Main. Main is now internal and a test runs it with a relative --store and an invalid --upstream, which reads every flag and then exits 1 before a server starts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Md2Tr7FWcbqPdwq2BjeG79 --- GitLfsCache.Tests/Tool/StoreFlagTests.cs | 10 ++++++++++ GitLfsCache.Tool/Program.cs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/GitLfsCache.Tests/Tool/StoreFlagTests.cs b/GitLfsCache.Tests/Tool/StoreFlagTests.cs index 027de95..e5a0944 100644 --- a/GitLfsCache.Tests/Tool/StoreFlagTests.cs +++ b/GitLfsCache.Tests/Tool/StoreFlagTests.cs @@ -18,6 +18,16 @@ public void Store_RelativeDirectory_ResolvesAgainstTheWorkingDirectory() Assert.AreEqual(Path.Combine(Environment.CurrentDirectory, "cache"), resolved); } + [TestMethod] + public async Task Store_RelativeDirectory_IsAcceptedByTheCommandLine() + { + // The invalid upstream stops the run after the flags are read and before a server starts, so a + // relative --store reaches the same one-line failure path as any other flag mistake. + int exitCode = await Program.Main(["--store", "./cache", "--upstream", "not-a-name-url-pair"]); + + Assert.AreEqual(1, exitCode); + } + [TestMethod] public void Store_FullyQualifiedDirectory_IsKeptAsGiven() { diff --git a/GitLfsCache.Tool/Program.cs b/GitLfsCache.Tool/Program.cs index e557f87..d7c0655 100644 --- a/GitLfsCache.Tool/Program.cs +++ b/GitLfsCache.Tool/Program.cs @@ -29,7 +29,7 @@ internal static class Program /// The configuration key the --token-key flag overrides. private const string TokenKeyKey = "GitLfsCache:TokenKeys:0"; - private static async Task Main(string[] args) + internal static async Task Main(string[] args) { Option port = new("--port", "-p") {