Skip to content

ledger: RunReportJob and UndoTransaction cannot be driven successfully by any client #362

Description

@Yaraslaut

Summary

Two ledger actions cannot be dispatched successfully by any out-of-process client, for different reasons. Both are reachable only as refusals. Recording them so the workflow-coverage work (morph#359's second axis) allowlists them with a stated reason rather than chasing them, and so a client author does not assume they are usable.

1. RunReportJob — service principal only

LedgerModel::execute(const RunReportJob&) refuses any principal but kReportRunnerPrincipal (src/models/ledger_model.cpp:1205-1206):

if (ctx == nullptr || ctx->principal != kReportRunnerPrincipal) {
    throw Forbidden{"RunReportJob: only the report runner may run a report job"};
}

This is deliberate and well documented — dto/report_dto.hpp explains the submit→run→poll split and why the run is an action rather than a lambda. No complaint about the design.

The consequence for a client is that the submit → run → poll triple cannot be completed from outside: a scenario submits a report, sees Pending, and cannot advance it. The server's own runner ticks on a timer, and waiting on a timer is precisely what the scenario runner forbids by design (its README: "No sleep, and no wall-clock waits").

Verified against a live server: SubmitReport returns a job id, GetReportStatus returns status == 0 (Pending), and RunReportJob from a user client returns exactly the message above.

2. UndoTransaction — needs a journalId nothing hands out

UndoTransaction{ledgerId, journalId} reverses a journal entry. No action in the rung's wire surface returns a journalId:

  • StoreTransaction returns GetLedgerResult — the accounts list, verified on the wire as {"accounts":[{"id":…,"name":…,"kind":…,"currency":…,"balance":{…}}]}. No journal id, no receipt.
  • GetLedger returns the same shape.
  • No GetJournal/ListTransactions action exists.

So a client can only ever call UndoTransaction with an id it guessed. Verified: journalId=999999 returns a not-found error naming the journal, which is the only assertable outcome.

This one looks less like a decision and more like a gap — undo is in the rung's "What to implement" (step 6) and its journal-derived-undo story is one of the subsystems it exists to exercise, but the client-side half of it is unreachable. The in-process tests get the id from the DB.

Verification status

Both reproduced on master against a live ladder_ledger_server, inside a 22-step workflow scenario that otherwise passes 48 assertions with zero surviving mutants. Neither is inferred from reading.

Not verified: whether UndoTransaction's missing id-return is deliberate. Unlike RunReportJob, whose restriction is documented at length in its own DTO comment, I found nothing recording this one.

What would close this

  • For RunReportJob: nothing to change in the rung. A note in the README that the run half is service-only, so client authors do not plan around it, would help — and the coverage allowlist entry cites this issue.
  • For UndoTransaction: either an action that returns journal ids (a GetJournal, or StoreTransaction answering with the created journalId alongside the ledger state), or a recorded decision that undo is a server-side/administrative path with no client story.

Until then both are allowlisted on the workflow-coverage axis with these reasons, which means the gate will not chase them but a reader can see exactly why.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ladderSubsystem: laddertriage: rescopeReal problem, wrong framing; rewrite before building

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions