fix(app): scope the UI test fixture to its token, never in an installed build #146

Merged
james.maes merged 1 commits from fix/145-ui-fixture-scope into main 2026-11-10 21:46:48 +00:00
5 changed files with 50 additions and 6 deletions

No files matched your search

+1 -1
View File
@@ -65,7 +65,7 @@ flowchart LR
| Activity logs | out | Actions and errors; token *lengths* only | File ACL | `IActivityLog` callers never pass tokens |
| `.git/gclo-recovery.json`, `gclo.checkoutpending` config key | out/in | Path-recovery plan; checkout-pending marker | File ACL | Written atomically; read only for the repository it belongs to |
| Command line (CLI) | in | Options, org, target, filters | n/a | `OptionReader` / `RepoFilterSpec`; parallelism bounded 1..64; token options refused on argv |
| `GCLO_DATA_DIR`, `GCLO_UITEST_FIXTURE` env | in | Data root override; offline test seam | n/a | Fixture only answers for the exact fixture token; never set in production |
| `GCLO_DATA_DIR`, `GCLO_UITEST_FIXTURE` env | in | Data root override; offline test seam | n/a | Fixture only answers for the exact fixture token (every other token, and the question whether a folder is a repository, go to the real services); ignored by an installed build; logged when enabled; never set in production (#145) |
| `wsl.exe -e sh -s` | out/in | POSIX script on stdin, org/repo/url as positional args, `GCLO_GIT_TOKEN` via `WSLENV` | PAT via env | Nothing is re-parsed by wsl.exe's command line; token never on argv or disk; UTF-16 wsl.exe messages decoded |
| Update feed (Velopack) | in | `releases.<channel>.json`, packages | HTTPS; package hashes | Velopack verifies package hashes against the feed; channel never crosses |
| Release assets (users) | in | Setup, portable, CLI zip, nupkg, SBOMs | HTTPS | `SHA256SUMS`, build-provenance and SBOM attestations (`gh attestation verify`) |
+2 -1
View File
@@ -94,7 +94,8 @@ public sealed class AppSession : IDisposable
private bool LogContains(string fragment)
/// <summary>True when any of the app's activity-log files holds <paramref name="fragment"/>.</summary>
public bool LogContains(string fragment)
{
string logs = Path.Combine(DataDirectory, "logs");
if(!Directory.Exists(logs))
+8
View File
@@ -37,6 +37,14 @@ public sealed partial class SmokeTests : UiTestBase
// (1b) The offline fixture this suite runs against announces itself in the app's
// log: a session with it enabled is never mistaken for an ordinary one (#145).
[Fact]
public void ActivityLog_AfterLaunch_SaysTheUiTestFixtureIsEnabled()
=> Assert.True(Session.LogContains("UI test fixture enabled"), "the app logs one line when the fixture is honoured");
// (2) The navigation pane carries the pinned Quick Sync plus both footer commands.
[Fact]
public void NavigationPane_AfterLaunch_ShowsQuickSyncAddAccountAndSyncAll()
+6
View File
@@ -114,6 +114,12 @@ public sealed partial class MainWindow : Window
_log = new FileActivityLog(System.IO.Path.Combine(GcloPaths.DataRoot, "logs"));
App.CrashLog = _log; // the unhandled-exception net now reaches the activity log
_log.Info($"gclo {BuildVersion.Describe(typeof(MainWindow).Assembly)} starting.");
if(UiTestFixture.IsEnabled)
{
// Worth a line in every log it applies to: with it on, one fixed token is
// answered from a built-in organization instead of GitHub (#145).
_log.Info("UI test fixture enabled (GCLO_UITEST_FIXTURE=1): the fixture token is answered offline.");
}
_updateService = new UpdateService(_log);
_tokenVault = new CredentialManagerVault();
_accountsStore = new AccountsStore(_tokenVault, log: _log);
+33 -4
View File
@@ -20,7 +20,9 @@ namespace gclo;
/// network - a fixed organization with three repositories, and a git client that
/// succeeds without touching disk. Any other token (even with the variable set) goes
/// to the real services, so the existing offline-input tests keep their meaning
/// (a dummy token still gets GitHub's 401). Production never sets the variable.
/// (a dummy token still gets GitHub's 401). Production never sets the variable, and an
/// installed build ignores it: the seam exists for a build run from its output folder
/// by the test suite, not for the app people install (#145).
/// </summary>
internal static class UiTestFixture
{
@@ -30,8 +32,13 @@ internal static class UiTestFixture
/// <summary>The organization the fixture lists.</summary>
public const string Organization = "fixture-org";
/// <summary>True when the process was started for the UI suite with the fixture enabled.</summary>
public static bool IsEnabled { get; } = string.Equals(Environment.GetEnvironmentVariable("GCLO_UITEST_FIXTURE"), "1", StringComparison.Ordinal);
/// <summary>
/// True when the process was started for the UI suite with the fixture enabled, and
/// is not an installed build.
/// </summary>
public static bool IsEnabled { get; } =
string.Equals(Environment.GetEnvironmentVariable("GCLO_UITEST_FIXTURE"), "1", StringComparison.Ordinal)
&& !IsInstalledBuild();
private static readonly IReadOnlyList<RepoDescriptor> s_repositories =
[
@@ -42,6 +49,25 @@ internal static class UiTestFixture
/// <summary>
/// True when the app was installed by Velopack (it then has an app id). A build run
/// from its output folder, which is what the UI suite launches, has none.
/// </summary>
private static bool IsInstalledBuild()
{
try
{
return !string.IsNullOrEmpty(Velopack.Locators.VelopackLocator.Current?.AppId);
}
catch(Exception)
{
// No locator for this platform or layout: not an installed build.
return false;
}
}
public static IRepositoryLister WrapRepositoryLister(IRepositoryLister real)
=> IsEnabled ? new RepositoryLister(real) : real;
@@ -81,7 +107,10 @@ internal static class UiTestFixture
/// <summary>Succeeds without writing anything; the real client handles every other token.</summary>
private sealed class GitClient(IGitClient real) : IGitClient
{
public bool IsValidRepository(string path) => false;
// No token is passed here, so the real client answers for everyone. The
// fixture's own clones write nothing, so its folders are never repositories
// and the fixture token always takes the clone path (#145).
public bool IsValidRepository(string path) => real.IsValidRepository(path);