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 <noreply@anthropic.com>master
| @@ -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 | |||
| @@ -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 | |||
| @@ -2,30 +2,12 @@ namespace EnvelopeRenderer.Cli.Tests; | |||
| /// <summary> | |||
| /// 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 | |||
| /// <see cref="EnvelopeRenderer.Cli.DebenuLicenseKeyResolver"/> 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). | |||
| /// </summary> | |||
| 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); | |||
| } | |||
| @@ -0,0 +1,73 @@ | |||
| namespace EnvelopeRenderer.Cli.Tests; | |||
| /// <summary> | |||
| /// 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. | |||
| /// </summary> | |||
| 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); | |||
| } | |||
| } | |||
| @@ -0,0 +1,38 @@ | |||
| namespace EnvelopeRenderer.Cli; | |||
| /// <summary> | |||
| /// Resolves the Debenu Quick PDF Library license key: the <c>DEBENU_LICENSE_KEY</c> environment | |||
| /// variable first, then an untracked <c>key.txt</c> found by walking up from a starting directory | |||
| /// (the CLI's own executable directory in production, so a <c>key.txt</c> dropped next to | |||
| /// <c>EnvelopeRenderer.Cli.exe</c> — 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. | |||
| /// </summary> | |||
| 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; | |||
| } | |||
| } | |||
| @@ -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)) | |||
| { | |||
| @@ -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. | | |||
| @@ -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 <kind> ...` 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 <kind> ...` 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 | |||
Powered by TurnKey Linux.