Quellcode durchsuchen

Fix desktop app render failing with Debenu error 999 (no license key)

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
Daniel Covington vor 1 Woche
Ursprung
Commit
640865cf53
8 geänderte Dateien mit 145 neuen und 30 gelöschten Zeilen
  1. +19
    -5
      code/CLI_CONTRACT.md
  2. +6
    -0
      code/README.md
  3. +5
    -23
      code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKey.cs
  4. +73
    -0
      code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKeyResolverTests.cs
  5. +38
    -0
      code/src/EnvelopeRenderer.Cli/DebenuLicenseKeyResolver.cs
  6. +2
    -1
      code/src/EnvelopeRenderer.Cli/Program.cs
  7. +1
    -0
      logs/technical_debt_log.md
  8. +1
    -1
      state.md

+ 19
- 5
code/CLI_CONTRACT.md Datei anzeigen

@@ -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



+ 6
- 0
code/README.md Datei anzeigen

@@ -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


+ 5
- 23
code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKey.cs Datei anzeigen

@@ -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);
}

+ 73
- 0
code/src/EnvelopeRenderer.Cli.Tests/DebenuLicenseKeyResolverTests.cs Datei anzeigen

@@ -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);
}
}

+ 38
- 0
code/src/EnvelopeRenderer.Cli/DebenuLicenseKeyResolver.cs Datei anzeigen

@@ -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;
}
}

+ 2
- 1
code/src/EnvelopeRenderer.Cli/Program.cs Datei anzeigen

@@ -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))
{


+ 1
- 0
logs/technical_debt_log.md Datei anzeigen

@@ -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. |

+ 1
- 1
state.md Datei anzeigen

@@ -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



Laden…
Abbrechen
Speichern

Powered by TurnKey Linux.