⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
runQueueCli (packages/loopover-miner/lib/portfolio-queue-cli.ts:623-640) dispatches eight subcommands. Seven of them handle a store failure identically, e.g. runQueueList:
try {
return withPortfolioQueue(options, (portfolioQueue) => { ... });
} catch (error) {
return reportCliFailure(parsed.json, describeCliError(error));
}
queue dashboard is the outlier. runPortfolioDashboard in packages/loopover-miner/lib/portfolio-dashboard.ts opens the store and calls the collector with only a finally:
const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();
try {
const summary = collectPortfolioDashboard({ portfolioQueue }, { nowMs: ... });
console.log(parsed.json ? JSON.stringify(summary, null, 2) : renderPortfolioDashboardTable(summary));
return 0;
} finally {
if (ownsQueue) portfolioQueue.close();
}
It also does not import describeCliError (only argsWantJson and reportCliFailure, line 11), and the store open itself sits outside the try entirely — so an initPortfolioQueueStore() failure (a corrupted portfolio-queue.sqlite3, an unreadable state dir, an invalid LOOPOVER_MINER_PORTFOLIO_QUEUE_DB) propagates out of runQueueCli, out of bin/loopover-miner.ts:124's process.exit(runQueueCli(...)), and terminates the CLI with a raw Node stack trace and exit code 1 — where every sibling queue subcommand prints { "ok": false, "error": ... } under --json and exits 2.
collectPortfolioDashboard throws invalid_portfolio_queue by design (line 59), and initPortfolioQueueStore throws invalid_portfolio_queue_db_path — both are unreachable as clean CLI errors today.
Requirements
- Move the store construction inside the
try in runPortfolioDashboard, so an opener failure is handled rather than propagated.
- Add a
catch (error) { return reportCliFailure(parsed.json, describeCliError(error)); } to runPortfolioDashboard, matching runQueueList's shape exactly.
- Import
describeCliError from ./cli-error.js.
- Guard the
finally close with ?. (the store variable may be unassigned when the opener threw), matching runQueueClaimBatch's own finally { if (ownsManager) manager?.close(); } (portfolio-queue-cli.ts:538-540).
- The success path, the parse-error path, and the
ownsQueue ownership rule must be unchanged.
⚠️ Required pattern: mirror runQueueClaimBatch in packages/loopover-miner/lib/portfolio-queue-cli.ts:520-541 — open inside the try, catch into reportCliFailure(parsed.json, describeCliError(error)), finally closes with ?.. It does NOT satisfy this issue to add a try/catch in runQueueCli's dispatcher instead (that would change the error shape for all eight subcommands), to catch and return 0, or to leave the opener outside the try.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the catch while leaving the opener outside the try, so an opener failure still escapes — does not resolve this issue.
Test Coverage Requirements
packages/loopover-miner/lib/**/*.ts IS inside Codecov's coverage.include in vitest.config.ts, so the 99%+ branch-counted codecov/patch gate applies exactly as for src/**. Both arms of the new catch need a test (success and failure), both arms of parsed.json on the failure path, and both arms of the ownsQueue / ?. close. The failure cases must be named regression tests that fail against the current code.
Expected Outcome
loopover-miner queue dashboard reports a corrupted or unopenable portfolio-queue store the same way every other queue subcommand does — exit 2, { ok: false, error } under --json — instead of crashing with an uncaught stack trace.
Links & Resources
packages/loopover-miner/lib/portfolio-dashboard.ts:140-164, packages/loopover-miner/lib/portfolio-queue-cli.ts:292-311, :520-541, packages/loopover-miner/lib/cli-error.ts.
Context
runQueueCli(packages/loopover-miner/lib/portfolio-queue-cli.ts:623-640) dispatches eight subcommands. Seven of them handle a store failure identically, e.g.runQueueList:queue dashboardis the outlier.runPortfolioDashboardinpackages/loopover-miner/lib/portfolio-dashboard.tsopens the store and calls the collector with only afinally:It also does not import
describeCliError(onlyargsWantJsonandreportCliFailure, line 11), and the store open itself sits outside thetryentirely — so aninitPortfolioQueueStore()failure (a corruptedportfolio-queue.sqlite3, an unreadable state dir, an invalidLOOPOVER_MINER_PORTFOLIO_QUEUE_DB) propagates out ofrunQueueCli, out ofbin/loopover-miner.ts:124'sprocess.exit(runQueueCli(...)), and terminates the CLI with a raw Node stack trace and exit code1— where every siblingqueuesubcommand prints{ "ok": false, "error": ... }under--jsonand exits2.collectPortfolioDashboardthrowsinvalid_portfolio_queueby design (line 59), andinitPortfolioQueueStorethrowsinvalid_portfolio_queue_db_path— both are unreachable as clean CLI errors today.Requirements
tryinrunPortfolioDashboard, so an opener failure is handled rather than propagated.catch (error) { return reportCliFailure(parsed.json, describeCliError(error)); }torunPortfolioDashboard, matchingrunQueueList's shape exactly.describeCliErrorfrom./cli-error.js.finallyclose with?.(the store variable may be unassigned when the opener threw), matchingrunQueueClaimBatch's ownfinally { if (ownsManager) manager?.close(); }(portfolio-queue-cli.ts:538-540).ownsQueueownership rule must be unchanged.Deliverables
runPortfolioDashboardconstructs the store inside itstry, has acatchreturningreportCliFailure(parsed.json, describeCliError(error)), and closes with?.infinally.test/unit/miner-portfolio-dashboard.test.tsinjects aninitPortfolioQueuethat throws and assertsrunPortfolioDashboard([], options)returns2(today it throws).--json, and that stdout received{"ok": false, "error": ...}.listQueuethrows mid-run still closes the store and returns2.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the
catchwhile leaving the opener outside thetry, so an opener failure still escapes — does not resolve this issue.Test Coverage Requirements
packages/loopover-miner/lib/**/*.tsIS inside Codecov'scoverage.includeinvitest.config.ts, so the 99%+ branch-countedcodecov/patchgate applies exactly as forsrc/**. Both arms of the newcatchneed a test (success and failure), both arms ofparsed.jsonon the failure path, and both arms of theownsQueue/?.close. The failure cases must be named regression tests that fail against the current code.Expected Outcome
loopover-miner queue dashboardreports a corrupted or unopenable portfolio-queue store the same way every otherqueuesubcommand does — exit2,{ ok: false, error }under--json— instead of crashing with an uncaught stack trace.Links & Resources
packages/loopover-miner/lib/portfolio-dashboard.ts:140-164,packages/loopover-miner/lib/portfolio-queue-cli.ts:292-311,:520-541,packages/loopover-miner/lib/cli-error.ts.