What happens
ledger::app::App's report runner dispatches RunReportJob for every Pending
row. If the row's ledger_id names a ledger that does not exist, the
aggregation does not fail — it finds no accounts, produces [], and the job
settles Done.
Measured on branch fix/160-ledger-app-layer (PR #243):
orphan-ledger job status=1 (Done) body=[]
Why it matters
The result is indistinguishable from a successful report over a real ledger
that happens to have no activity. Both are Done with [].
For a rung whose point is auditable financial records, that is the wrong
default: "I computed your report and there was nothing in it" and "the ledger
you asked about is not there" are different answers, and only one of them should
satisfy a caller.
It also means the runner's own failure arm is unreachable this way — see below.
How it was found
While raising patch coverage on PR #243. The uncovered lines in
examples/ledger/src/app/app.cpp are its error arms — the .then() branch that
logs a Failed result, and the .onError() branch with its describe() helper
— and the obvious way to reach them was a job whose aggregation fails. Pointing
a job at a missing ledger turned out not to fail, which is how this surfaced.
A characterisation test asserting the current behaviour is landed in PR #243
(test_app.cpp, "A job whose ledger no longer exists settles Done with an empty
report") so it cannot change silently while this is open. That test asserts
Done deliberately — it pins today's behaviour, and should be updated by
whoever decides the intended one.
The adjacent, larger point
The App's error arms appear unreachable through any existing seam. The
aggregation is robust in every direction I could reach from outside:
- a missing ledger yields an empty report (above);
- malformed
params_json does not throw — decodeMonthlyParams returns
std::nullopt and the report falls back to all-time;
DbFaultFixture contends on a named advisory lock, and LedgerModel takes
none, so it cannot fault the aggregation.
So covering those lines needs a fault seam that does not exist, which is a
design decision rather than a missing test. Recorded here rather than papered
over with tests that execute the lines without asserting anything.
Options
- Refuse the job:
RunReportJob throws NotFound when the ledger is gone,
settling the row Failed with a reason. Makes the two cases distinguishable
and incidentally makes the runner's failure arm reachable.
- Report it in the body: keep
Done but emit a document that says the
ledger was not found, so the distinction survives to the caller.
- Keep it and document it: defensible only if a vanished ledger is
considered impossible — but the row can outlive its ledger, which is exactly
how this was produced.
(1) looks right and is the smallest change; it also removes the need for a
separate fault seam to test the failure path.
Verification status
Measured, not inferred: the status and body above are actual output from a
compiled run against a real SQLite store. The three unreachability claims were
each checked by reading the relevant code (decodeMonthlyParams,
db_fault_fixture.hpp, and a grep for SqlScopedLock in LedgerModel), not by
attempting each in a test.
What happens
ledger::app::App's report runner dispatchesRunReportJobfor everyPendingrow. If the row's
ledger_idnames a ledger that does not exist, theaggregation does not fail — it finds no accounts, produces
[], and the jobsettles Done.
Measured on branch
fix/160-ledger-app-layer(PR #243):Why it matters
The result is indistinguishable from a successful report over a real ledger
that happens to have no activity. Both are
Donewith[].For a rung whose point is auditable financial records, that is the wrong
default: "I computed your report and there was nothing in it" and "the ledger
you asked about is not there" are different answers, and only one of them should
satisfy a caller.
It also means the runner's own failure arm is unreachable this way — see below.
How it was found
While raising patch coverage on PR #243. The uncovered lines in
examples/ledger/src/app/app.cppare its error arms — the.then()branch thatlogs a
Failedresult, and the.onError()branch with itsdescribe()helper— and the obvious way to reach them was a job whose aggregation fails. Pointing
a job at a missing ledger turned out not to fail, which is how this surfaced.
A characterisation test asserting the current behaviour is landed in PR #243
(
test_app.cpp, "A job whose ledger no longer exists settles Done with an emptyreport") so it cannot change silently while this is open. That test asserts
Donedeliberately — it pins today's behaviour, and should be updated bywhoever decides the intended one.
The adjacent, larger point
The App's error arms appear unreachable through any existing seam. The
aggregation is robust in every direction I could reach from outside:
params_jsondoes not throw —decodeMonthlyParamsreturnsstd::nulloptand the report falls back to all-time;DbFaultFixturecontends on a named advisory lock, andLedgerModeltakesnone, so it cannot fault the aggregation.
So covering those lines needs a fault seam that does not exist, which is a
design decision rather than a missing test. Recorded here rather than papered
over with tests that execute the lines without asserting anything.
Options
RunReportJobthrowsNotFoundwhen the ledger is gone,settling the row
Failedwith a reason. Makes the two cases distinguishableand incidentally makes the runner's failure arm reachable.
Donebut emit a document that says theledger was not found, so the distinction survives to the caller.
considered impossible — but the row can outlive its ledger, which is exactly
how this was produced.
(1) looks right and is the smallest change; it also removes the need for a
separate fault seam to test the failure path.
Verification status
Measured, not inferred: the status and body above are actual output from a
compiled run against a real SQLite store. The three unreachability claims were
each checked by reading the relevant code (
decodeMonthlyParams,db_fault_fixture.hpp, and a grep forSqlScopedLockinLedgerModel), not byattempting each in a test.