From 640865cf53d0c16dddfa0518cb8b57aee36313b7 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Fri, 4 Sep 2026 19:39:23 -0400 Subject: [PATCH] Fix desktop app render failing with Debenu error 999 (no license key) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EnvelopeRenderer.Cli only read DEBENU_LICENSE_KEY from the process environment. That works for `dotnet run` (inherits the invoking shell's env), but EnvelopeRenderer.Desktop launches the CLI as a child process, which only inherits whatever environment a double-clicked .exe or Start Menu shortcut already had — normally nothing. Every desktop-launched render therefore failed at the Debenu save step (error 999) regardless of a valid key existing on disk. Add DebenuLicenseKeyResolver: env var first, then a key.txt walked up from the CLI executable's own directory, matching the fallback the test suite already used internally. Verified against the actual built EnvelopeRenderer.Cli.exe with no environment variable set and a key.txt next to it: exit 0, valid 392-page PDF. Logged in logs/technical_debt_log.md as a real Definition-of-Done verification gap from Sprint 1 (Batches 4-5 verified via an in-process harness, never the built desktop .exe). Co-Authored-By: Claude Sonnet 5 --- code/CLI_CONTRACT.md | 24 ++++-- code/README.md | 6 ++ .../DebenuLicenseKey.cs | 28 ++----- .../DebenuLicenseKeyResolverTests.cs | 73 +++++++++++++++++++ .../DebenuLicenseKeyResolver.cs | 38 ++++++++++ code/src/EnvelopeRenderer.Cli/Program.cs | 3 +- logs/technical_debt_log.md | 1 + state.md | 2 +- 8 files changed, 145 insertions(+), 30 deletions(-) create mode 100644 code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKeyResolverTests.cs create mode 100644 code/src/EnvelopeRenderer.Cli/DebenuLicenseKeyResolver.cs diff --git a/code/CLI_CONTRACT.md b/code/CLI_CONTRACT.md index 86076cd..7df571b 100644 --- a/code/CLI_CONTRACT.md +++ b/code/CLI_CONTRACT.md @@ -106,11 +106,25 @@ The text-only template format (`--template`) is documented separately in ## Debenu license key Rendering (not argument validation) requires a Debenu Quick PDF Library 10.13 license key at -runtime, read from the `DEBENU_LICENSE_KEY` environment variable. Without it, every render fails -at the save step with exit `1` — confirmed directly against the vendor DLL: page creation, font -embedding, and text drawing all succeed, but `SaveToFile`/`SaveToString` both return error code -999 regardless of content. A working key for local development is kept at the project root in -`key.txt` (untracked — never commit it or hardcode it into source). +runtime. Without it, every render fails at the save step with exit `1` — confirmed directly +against the vendor DLL: page creation, font embedding, and text drawing all succeed, but +`SaveToFile`/`SaveToString` both return error code 999 regardless of content. + +The key is resolved in this order (`DebenuLicenseKeyResolver.cs`): + +1. The `DEBENU_LICENSE_KEY` environment variable, if set to a non-blank value. +2. Otherwise, a `key.txt` file, walked up from the CLI executable's own directory (its own + directory first, then each parent, up to 10 levels) — so a `key.txt` dropped next to + `EnvelopeRenderer.Cli.exe` is picked up automatically, and a `key.txt` at the project root + still works for local `dotnet run` development without needing to set anything. + +This matters for the desktop app in particular: `EnvelopeRenderer.Desktop` launches +`EnvelopeRenderer.Cli.exe` as a child process, which by default only inherits environment +variables that were already present in whatever process launched the desktop app itself (a +double-clicked `.exe` or Start Menu shortcut typically has none). Requiring an operator to set an +environment variable before every launch is not viable, so the key.txt fallback is the intended +path for that workflow — never commit a real key.txt or hardcode a key into source; it's +untracked (`.gitignore`) for exactly this reason. ## UNC paths diff --git a/code/README.md b/code/README.md index 47f32c4..c1e0ca5 100644 --- a/code/README.md +++ b/code/README.md @@ -40,6 +40,12 @@ executable has been moved or is deployed separately, point the desktop app at it `ENVELOPERENDERER_CLI_PATH` environment variable (see [`Launch/CliExecutablePathResolver.cs`](src/EnvelopeRenderer.Desktop.Core/Launch/CliExecutablePathResolver.cs)). +Rendering needs a Debenu license key (see "Debenu license key" in `CLI_CONTRACT.md`) — since the +desktop app's child CLI process only inherits whatever environment variables were already present +when the desktop app itself was launched (typically none, for a double-clicked `.exe`), the +simplest way to supply it here is to drop a `key.txt` file next to `EnvelopeRenderer.Cli.exe` in +the build output folder; the CLI finds it automatically without any environment variable setup. + The render runs in a hidden child process — the desktop app now reads its `PROGRESS`/`ERROR:` stdout/stderr streams itself (Sprint 1, Batch 5: "Show render progress and completion summary") rather than leaving a separate visible console window, per `CLI_CONTRACT.md`. While a render is diff --git a/code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKey.cs b/code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKey.cs index a9d113a..61f9d99 100644 --- a/code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKey.cs +++ b/code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKey.cs @@ -2,30 +2,12 @@ namespace EnvelopeRenderer.Cli.Tests; /// /// Resolves a local Debenu Quick PDF Library license key for integration tests that need to -/// verify real save output — never committed, never hardcoded. Checked in order: the -/// DEBENU_LICENSE_KEY environment variable, then an untracked key.txt found by walking up from -/// the test output directory (the project root, where the developer's local copy lives). +/// verify real save output — never committed, never hardcoded. Delegates to the same +/// the shipped CLI uses, so tests and +/// production share one resolution rule (env var, then a key.txt walked up from the executable's +/// own directory). /// public static class DebenuLicenseKey { - public static string? Resolve() - { - var fromEnv = Environment.GetEnvironmentVariable("DEBENU_LICENSE_KEY"); - if (!string.IsNullOrWhiteSpace(fromEnv)) - { - return fromEnv; - } - - var directory = new DirectoryInfo(AppContext.BaseDirectory); - for (var i = 0; i < 10 && directory is not null; i++, directory = directory.Parent) - { - var candidate = Path.Combine(directory.FullName, "key.txt"); - if (File.Exists(candidate)) - { - return File.ReadAllText(candidate).Trim(); - } - } - - return null; - } + public static string? Resolve() => EnvelopeRenderer.Cli.DebenuLicenseKeyResolver.Resolve(AppContext.BaseDirectory); } diff --git a/code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKeyResolverTests.cs b/code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKeyResolverTests.cs new file mode 100644 index 0000000..61eacef --- /dev/null +++ b/code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKeyResolverTests.cs @@ -0,0 +1,73 @@ +namespace EnvelopeRenderer.Cli.Tests; + +/// +/// Mutates the process-wide DEBENU_LICENSE_KEY environment variable, so these tests always +/// save/restore it around a narrow act phase and never run assertions while it differs from the +/// ambient value other tests (e.g. the real-DLL integration test) expect. +/// +public class DebenuLicenseKeyResolverTests : IDisposable +{ + private readonly string? _originalEnvValue = Environment.GetEnvironmentVariable("DEBENU_LICENSE_KEY"); + private readonly string _tempRoot = Directory.CreateTempSubdirectory("EnvelopeRendererLicenseKeyTests-").FullName; + + public void Dispose() + { + Environment.SetEnvironmentVariable("DEBENU_LICENSE_KEY", _originalEnvValue); + Directory.Delete(_tempRoot, recursive: true); + } + + [Fact] + public void Resolve_PrefersEnvironmentVariableOverKeyFile() + { + File.WriteAllText(Path.Combine(_tempRoot, "key.txt"), "from-file"); + Environment.SetEnvironmentVariable("DEBENU_LICENSE_KEY", "from-env"); + + var result = DebenuLicenseKeyResolver.Resolve(_tempRoot); + + Assert.Equal("from-env", result); + } + + [Fact] + public void Resolve_FallsBackToKeyFileInStartDirectory_WhenEnvVarUnset() + { + Environment.SetEnvironmentVariable("DEBENU_LICENSE_KEY", null); + File.WriteAllText(Path.Combine(_tempRoot, "key.txt"), " from-file-with-whitespace \r\n"); + + var result = DebenuLicenseKeyResolver.Resolve(_tempRoot); + + Assert.Equal("from-file-with-whitespace", result); + } + + [Fact] + public void Resolve_WalksUpToAnAncestorDirectory_WhenEnvVarUnset() + { + Environment.SetEnvironmentVariable("DEBENU_LICENSE_KEY", null); + var nested = Directory.CreateDirectory(Path.Combine(_tempRoot, "bin", "Debug", "net10.0")).FullName; + File.WriteAllText(Path.Combine(_tempRoot, "key.txt"), "from-ancestor"); + + var result = DebenuLicenseKeyResolver.Resolve(nested); + + Assert.Equal("from-ancestor", result); + } + + [Fact] + public void Resolve_ReturnsNull_WhenNeitherEnvVarNorKeyFileExist() + { + Environment.SetEnvironmentVariable("DEBENU_LICENSE_KEY", null); + + var result = DebenuLicenseKeyResolver.Resolve(_tempRoot); + + Assert.Null(result); + } + + [Fact] + public void Resolve_TreatsABlankKeyFileAsAbsent() + { + Environment.SetEnvironmentVariable("DEBENU_LICENSE_KEY", null); + File.WriteAllText(Path.Combine(_tempRoot, "key.txt"), " \r\n "); + + var result = DebenuLicenseKeyResolver.Resolve(_tempRoot); + + Assert.Null(result); + } +} diff --git a/code/src/EnvelopeRenderer.Cli/DebenuLicenseKeyResolver.cs b/code/src/EnvelopeRenderer.Cli/DebenuLicenseKeyResolver.cs new file mode 100644 index 0000000..10be9b5 --- /dev/null +++ b/code/src/EnvelopeRenderer.Cli/DebenuLicenseKeyResolver.cs @@ -0,0 +1,38 @@ +namespace EnvelopeRenderer.Cli; + +/// +/// Resolves the Debenu Quick PDF Library license key: the DEBENU_LICENSE_KEY environment +/// variable first, then an untracked key.txt found by walking up from a starting directory +/// (the CLI's own executable directory in production, so a key.txt dropped next to +/// EnvelopeRenderer.Cli.exe — or in any ancestor directory, such as the repo root during +/// local development — is picked up automatically without requiring an operator to configure an +/// environment variable). Never committed, never hardcoded — see "Debenu license key" in +/// CLI_CONTRACT.md. +/// +public static class DebenuLicenseKeyResolver +{ + public static string? Resolve(string startDirectory) + { + var fromEnv = Environment.GetEnvironmentVariable("DEBENU_LICENSE_KEY"); + if (!string.IsNullOrWhiteSpace(fromEnv)) + { + return fromEnv; + } + + var directory = new DirectoryInfo(startDirectory); + for (var i = 0; i < 10 && directory is not null; i++, directory = directory.Parent) + { + var candidate = Path.Combine(directory.FullName, "key.txt"); + if (File.Exists(candidate)) + { + var content = File.ReadAllText(candidate).Trim(); + if (!string.IsNullOrEmpty(content)) + { + return content; + } + } + } + + return null; + } +} diff --git a/code/src/EnvelopeRenderer.Cli/Program.cs b/code/src/EnvelopeRenderer.Cli/Program.cs index 262c5b4..1c36d24 100644 --- a/code/src/EnvelopeRenderer.Cli/Program.cs +++ b/code/src/EnvelopeRenderer.Cli/Program.cs @@ -1,3 +1,4 @@ +using EnvelopeRenderer.Cli; using EnvelopeRenderer.Cli.Cli; using EnvelopeRenderer.Cli.Progress; using EnvelopeRenderer.Cli.Render; @@ -77,7 +78,7 @@ static int RunRender(CliArguments args) var dllPath = Path.Combine( AppContext.BaseDirectory, Environment.Is64BitProcess ? "DebenuPDFLibrary64DLL1013.dll" : "DebenuPDFLibraryDLL1013.dll"); - var licenseKey = Environment.GetEnvironmentVariable("DEBENU_LICENSE_KEY"); + var licenseKey = DebenuLicenseKeyResolver.Resolve(AppContext.BaseDirectory); if (!DebenuPdfRenderer.TryCreate(dllPath, licenseKey, out var renderer, out var createError)) { diff --git a/logs/technical_debt_log.md b/logs/technical_debt_log.md index a8e9123..ed998b2 100644 --- a/logs/technical_debt_log.md +++ b/logs/technical_debt_log.md @@ -7,3 +7,4 @@ Append-only log of known technical debt. Maintained by `.claude/agents/qa-tech-d | 2026-09-08 | `EnvelopeRenderer.Cli` exits `64` ("render not implemented") for any argument-valid run, since the Debenu render engine isn't wired in yet. Self-documenting and expected to disappear once "Render text-only PDFs through Debenu Quick PDF" (Sprint 1 Batch 2) lands and exit `0` becomes reachable. | Deliberate | Low | Paid Down | `RenderNotImplemented` branch removed from `Program.cs`; exit `0` is now reachable and exit `1` covers all template/CSV/render failures (see `code/CLI_CONTRACT.md`). | | 2026-09-08 | `DebenuPdfRenderer` always embeds TrueType fonts fully (`AddTrueTypeFont(..., Embed: 1)`), cached once per unique font name per document. Fine at today's scale (one sample render: ~1.3 MB for 392 pages, one font) but full embedding could add up if a template ever uses many distinct fonts/styles at very high page counts, working against the sub-2GB PDF constraint. | Deliberate | Low | Open | Revisit if the "Time-box the first high-volume benchmark" story (not in this sprint) shows file size becoming an issue; Debenu also exposes `AddSubsettedFont` as a smaller-footprint alternative if needed. | | 2026-09-08 | `EnvelopeRenderer.Desktop`'s Render button re-enables as soon as the CLI process is confirmed *started* (`CliProcessLauncher.LaunchAsync` returning), not once it finishes rendering — this story (Sprint 1 Batch 4) intentionally does not track render completion. An operator can click Render again (e.g. against the same output path) while a prior render is still in progress, since nothing yet observes the child process's lifetime or exit code. | Unavoidable (scope boundary of this story) | Low | Resolved | Sprint 1 Batch 5 ("Show render progress and completion summary") replaced `CliProcessLauncher.LaunchAsync`/`Launch` with `RunAsync`/`Run`, which stream the CLI's redirected stdout/stderr, wait for the process to actually exit, and return exit code + final progress event. `MainForm.OnRenderClick` now only re-enables the Render button in the `finally` block after that awaited call completes, not when the process starts — verified with a real launch against the sample CSV (button stayed disabled for the full render, both success and forced-failure runs). | +| 2026-09-04 | `EnvelopeRenderer.Cli` only read `DEBENU_LICENSE_KEY` from the process environment. That's fine for `dotnet run --project ... --` (the CLI inherits the invoking shell's env directly), but `EnvelopeRenderer.Desktop` launches the CLI as a child process, which only inherits whatever environment variables were already present in whatever launched the desktop app itself (a double-clicked `.exe` or Start Menu shortcut typically has none) — so every desktop-launched render failed with Debenu error 999 regardless of a valid key existing on disk. Real-user-reported: the operator correctly guessed a `key.txt` dropped next to the exe should work (matching how the CLI's own test helper already resolved keys), but production code had no such fallback. This is a real Definition-of-Done verification gap from Sprint 1 Batches 4-5: the "real success run" verification used an in-process test harness with the env var set directly in that process, never the actual built `.exe` launched the way an operator would, so the gap wasn't caught before Sprint Review. | Unintentional | High (silently broke the desktop app's core success path for any non-`dotnet run` launch) | Resolved | Added `DebenuLicenseKeyResolver` (`code/src/EnvelopeRenderer.Cli/DebenuLicenseKeyResolver.cs`) to the shipped CLI: env var first, then a `key.txt` walked up from the executable's own directory — the same rule the test-only helper already used, now shared via delegation instead of duplicated. Documented in `CLI_CONTRACT.md`'s "Debenu license key" section and `code/README.md`'s desktop-app instructions. Verified by running the actual built `EnvelopeRenderer.Cli.exe` from the Desktop app's own output folder with no environment variable set at all, `key.txt` sitting next to it: exit `0`, valid 1.3 MB `%PDF-1.4` output, all 392 records. 5 new unit tests added (`DebenuLicenseKeyResolverTests.cs`); full suite 111/111 passing. | diff --git a/state.md b/state.md index cb501d2..8bba6cd 100644 --- a/state.md +++ b/state.md @@ -11,7 +11,7 @@ **Sprint goal:** Prove a thin text-only desktop-to-CLI rendering spine so an operator can select files, launch a job, watch progress, and receive a PDF generated through Debenu from a known XML template. **Current sprint backlog:** `backlog/sprints/sprint-1.md` -**Next action:** All 5 committed Sprint 1 items are Done (exit criteria for Phase 3 met — see `backlog/sprints/sprint-1.md`). The walking skeleton is real end-to-end: an operator picks template/CSV/output paths in the WinForms desktop app (`EnvelopeRenderer.Desktop`), launches a render without freezing the UI, watches live progress parsed from the CLI's `PROGRESS ...` stdout stream, and gets a completion summary — verified with a real success run (392/392 pages, valid PDF) and a real forced-failure run against the actual sample CSV. 106 tests passing. `product-owner` runs Sprint Review next per `process/04_sprint_review.md`. Carry-forward items to raise at review: template-path and UNC-timeout impediments still open (`logs/impediment_log.md`); the 100k-record/300 DPI benchmark hasn't been re-run since Batch 2; no "warning" concept exists in the product yet, so the "warning count" acceptance task for the last story was satisfied by its documented absence rather than a real counter — product owner may want to scope that once image/overflow handling exists. +**Next action:** All 5 committed Sprint 1 items are Done (exit criteria for Phase 3 met — see `backlog/sprints/sprint-1.md`). The walking skeleton is real end-to-end: an operator picks template/CSV/output paths in the WinForms desktop app (`EnvelopeRenderer.Desktop`), launches a render without freezing the UI, watches live progress parsed from the CLI's `PROGRESS ...` stdout stream, and gets a completion summary. Post-Sprint-Review-handoff, the user hit a real bug using the desktop app directly (Debenu error 999): the CLI only read `DEBENU_LICENSE_KEY` from the process environment, which a double-clicked desktop app never has — fixed same-day with `DebenuLicenseKeyResolver` (env var, then a `key.txt` walked up from the executable's own directory), logged in `logs/technical_debt_log.md`, verified against the actual built `.exe` (exit 0, valid 392-page PDF). 111 tests passing. `product-owner` runs Sprint Review next per `process/04_sprint_review.md`. Carry-forward items to raise at review: template-path and UNC-timeout impediments still open (`logs/impediment_log.md`); the 100k-record/300 DPI benchmark hasn't been re-run since Batch 2; no "warning" concept exists in the product yet, so the "warning count" acceptance task for the last story was satisfied by its documented absence rather than a real counter; and the license-key gap above is a signal that Sprint 2 should verify GUI stories against the actual built artifact, not just an in-process harness. ## Phase reference