From 3d42355189ad97cf8507cdab86c46f27791889a1 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Wed, 19 Aug 2026 11:11:08 +0200 Subject: [PATCH] The smoke test can be somebody MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without a token the signed-in half of a gated catalog is untestable here: the real credential store is the OS keychain reached through Electron, and there is no Electron in this process, so every title comes back `signInRequired` and "owned" and "not owned" never happen. SMOKE_TOKEN supplies one. Against a live Orbit it now reports `open:1, purchasable:1, entitled:1` — the free title, the one this account has not bought, and the one it has — which is the first end-to-end proof that the access block survives the whole path from the engine's policy to a card. It only ever reads. A smoke run must not leave a credential on the machine that ran it. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 1 + src/scripts/SmokeTest.ts | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 06ef69b..f47f469 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ names. `make` on its own lists everything. | `make check` | **typecheck, lint and both test suites** — the gate | | `make start` | run the app against whatever store is installed | | `make smoke` | drive the store with no window and no Electron at all | +| `SMOKE_HOME= SMOKE_TOKEN= npm run smoke` | the same, against a sandbox store and as a signed-in person | | `make uitest` | load the window once and report what rendered | | `SELFTEST_SHOT=shot.png npm run uitest` | the same, and the window photographs itself into that file | | `make test` | both test suites | diff --git a/src/scripts/SmokeTest.ts b/src/scripts/SmokeTest.ts index e1dbbcf..282dff5 100644 --- a/src/scripts/SmokeTest.ts +++ b/src/scripts/SmokeTest.ts @@ -5,6 +5,7 @@ import type { CatalogListing } from '../domain/models/CatalogListing' import type { InstalledStore } from '../domain/models/InstalledStore' import { DESKTOP_STORE_ENGINE } from '../domain/models/StoreEngine' import { deriveStoreId } from '../domain/models/StoreIdentity' +import type { CredentialRepository } from '../domain/ports/CredentialRepository' import { NativeStoreCatalogGateway } from '../infrastructure/engine/NativeStoreCatalogGateway' import { HttpTextClient } from '../infrastructure/http/HttpTextClient' import { FileSystemInstalledStoreRepository } from '../infrastructure/repositories/FileSystemInstalledStoreRepository' @@ -26,12 +27,18 @@ import { LOCALES } from '../shared/i18n/MessageBundle' * * npm run smoke the store on this machine * SMOKE_HOME=/path/to/store-home npm run smoke a sandbox store + * SMOKE_TOKEN= npm run smoke as a signed-in person + * + * `SMOKE_TOKEN` exists because the signed-in path is otherwise untestable here: the + * real credential store is the OS keychain, reached through Electron, and there is no + * Electron in this process. Without it a gated catalog can only ever be read as an + * anonymous caller, and "owned" and "not owned" never happen. */ class SmokeTest { private failed = false private readonly stores = new FileSystemInstalledStoreRepository() - private readonly catalogGateway = new NativeStoreCatalogGateway() + private readonly catalogGateway = new NativeStoreCatalogGateway(envCredentials()) private readonly httpClient = new HttpTextClient() private readonly registry = new HttpStoreRegistryRepository(this.httpClient) private readonly gameMapper = new GameDtoMapper() @@ -219,3 +226,19 @@ void new SmokeTest().run().then( process.exitCode = 1 } ) + +/** + * The token from the environment, for every store. + * + * Deliberately not per store: this is a test harness pointed at one catalog at a time, + * and a keyed map here would be ceremony around a single value. Nothing writes — a + * smoke run must not leave a credential behind on the machine that ran it. + */ +function envCredentials (): CredentialRepository { + const token = process.env['SMOKE_TOKEN'] ?? '' + return { + readToken: (): string | null => (token.length > 0 ? token : null), + writeToken: (storeId: string, value: string): void => { void storeId; void value }, + clearToken: (storeId: string): void => { void storeId } + } +}