@@ -18,6 +18,9 @@ const {
1818 loggerErrorMock,
1919 logMock,
2020 tableMock,
21+ clientQueryMock,
22+ clientReleaseMock,
23+ poolConnectMock,
2124} = vi . hoisted ( ( ) => ( {
2225 getToAndFromDatesMock : vi . fn ( ( ) => Promise . resolve ( {
2326 from : dateUtc ( { year : 2026 , month : 1 , day : 1 } ) ,
@@ -90,16 +93,19 @@ const {
9093 } ,
9194 ] ) ) ,
9295 getClientMock : vi . fn ( ( ) => Promise . resolve ( {
93- connect : vi . fn ( ( ) => Promise . resolve ( {
94- query : vi . fn ( ( ) => Promise . resolve ( undefined ) ) ,
95- release : vi . fn ( ) ,
96- } ) ) ,
96+ connect : poolConnectMock ,
9797 } ) ) ,
9898 loggerWarnMock : vi . fn ( ) ,
9999 loggerInfoMock : vi . fn ( ) ,
100100 loggerErrorMock : vi . fn ( ) ,
101101 logMock : vi . fn ( ) ,
102102 tableMock : vi . fn ( ) ,
103+ clientQueryMock : vi . fn ( ( ) => Promise . resolve ( undefined ) ) ,
104+ clientReleaseMock : vi . fn ( ) ,
105+ poolConnectMock : vi . fn ( ( ) => Promise . resolve ( {
106+ query : clientQueryMock ,
107+ release : clientReleaseMock ,
108+ } ) ) ,
103109} ) ) ;
104110
105111vi . mock ( "../../../../../../../src/apps/hdb/commands/shared/date-range-utils.js" , ( ) => ( {
@@ -185,6 +191,16 @@ describe("hdb coinbase balance handlers", () => {
185191 expect ( logMock . mock . calls [ 0 ] ?. [ 0 ] ) . toContain ( "\"filters\"" ) ;
186192 } ) ;
187193
194+ it ( "prints current snapshot balances as json with current-balance metadata" , async ( ) => {
195+ await coinbaseBalancesBatch ( { current : true , remote : true , json : true , raw : true } ) ;
196+
197+ expect ( requestAccountsMock ) . toHaveBeenCalledTimes ( 1 ) ;
198+ expect ( tableMock ) . not . toHaveBeenCalled ( ) ;
199+ expect ( logMock . mock . calls [ 0 ] ?. [ 0 ] ) . toContain ( "\"mode\": \"snapshot\"" ) ;
200+ expect ( logMock . mock . calls [ 0 ] ?. [ 0 ] ) . toContain ( "\"includesCurrentBalance\": true" ) ;
201+ expect ( logMock . mock . calls [ 0 ] ?. [ 0 ] ) . toContain ( "\"raw\": true" ) ;
202+ } ) ;
203+
188204 it ( "queries batch snapshot and trace" , async ( ) => {
189205 await coinbaseBalancesBatch ( { quiet : false , raw : true } ) ;
190206 await coinbaseBalancesTrace ( "eth2" , { quiet : false , raw : true } ) ;
@@ -196,6 +212,24 @@ describe("hdb coinbase balance handlers", () => {
196212 ) ;
197213 } ) ;
198214
215+ it ( "warns when list or trace lookups return no balances" , async ( ) => {
216+ selectCoinbaseBalanceLedgerMock . mockResolvedValueOnce ( [ ] ) ;
217+ traceCoinbaseBalanceLedgerMock . mockResolvedValueOnce ( [ ] ) ;
218+
219+ await expect ( coinbaseBalances ( "btc" , { quiet : false } ) ) . resolves . toEqual ( [ ] ) ;
220+ await expect ( coinbaseBalancesTrace ( "btc" , { quiet : false } ) ) . resolves . toEqual ( [ ] ) ;
221+
222+ expect ( loggerWarnMock ) . toHaveBeenCalledTimes ( 2 ) ;
223+ } ) ;
224+
225+ it ( "uses drop flow when regenerating from scratch" , async ( ) => {
226+ await coinbaseBalancesRegenerate ( { drop : true , quiet : true } ) ;
227+
228+ expect ( dropCoinbaseBalanceLedgerTableMock ) . toHaveBeenCalledTimes ( 1 ) ;
229+ expect ( createCoinbaseBalanceLedgerTableMock ) . toHaveBeenCalledTimes ( 1 ) ;
230+ expect ( truncateCoinbaseBalanceLedgerTableMock ) . not . toHaveBeenCalled ( ) ;
231+ } ) ;
232+
199233 it ( "regenerates balance ledger and writes computed rows" , async ( ) => {
200234 const count = await coinbaseBalancesRegenerate ( { drop : false } ) ;
201235
@@ -222,4 +256,53 @@ describe("hdb coinbase balance handlers", () => {
222256 expect ( count ) . toBe ( 5 ) ;
223257 } ) ;
224258
259+ it ( "supports unwrap into ETH without a negative balance" , async ( ) => {
260+ selectCoinbaseTransactionsMock . mockResolvedValueOnce ( [
261+ {
262+ id : "tx-eth" ,
263+ timestamp : dateUtc ( { year : 2026 , month : 1 , day : 5 } ) ,
264+ type : "Unwrap" ,
265+ asset : "ETH" ,
266+ num_quantity : "0.25" ,
267+ notes : "unwrap eth" ,
268+ } ,
269+ ] ) ;
270+
271+ const count = await coinbaseBalancesRegenerate ( { quiet : true } ) ;
272+
273+ const firstBatch = insertCoinbaseBalanceLedgerBatchMock . mock . calls [ 0 ] ?. [ 0 ] ;
274+ expect ( firstBatch ?. [ 0 ] ?. asset ) . toBe ( "ETH" ) ;
275+ expect ( firstBatch ?. [ 1 ] ?. tx_id ) . toBe ( "tx-eth" ) ;
276+ expect ( firstBatch ?. [ 1 ] ?. balance ) . toBe ( "0.25" ) ;
277+ expect ( loggerErrorMock ) . not . toHaveBeenCalled ( ) ;
278+ expect ( count ) . toBe ( 2 ) ;
279+ } ) ;
280+
281+ it ( "rolls back and rethrows when batched inserts fail" , async ( ) => {
282+ insertCoinbaseBalanceLedgerBatchMock . mockRejectedValueOnce ( new Error ( "insert failed" ) ) ;
283+
284+ await expect ( coinbaseBalancesRegenerate ( { quiet : true } ) ) . rejects . toThrow ( "insert failed" ) ;
285+
286+ expect ( clientQueryMock ) . toHaveBeenNthCalledWith ( 1 , "BEGIN" ) ;
287+ expect ( clientQueryMock ) . toHaveBeenNthCalledWith ( 2 , "ROLLBACK" ) ;
288+ expect ( clientReleaseMock ) . toHaveBeenCalledTimes ( 1 ) ;
289+ } ) ;
290+
291+ it ( "throws when a transaction quantity is invalid during regenerate" , async ( ) => {
292+ selectCoinbaseTransactionsMock . mockResolvedValueOnce ( [
293+ {
294+ id : "tx-bad" ,
295+ timestamp : dateUtc ( { year : 2026 , month : 1 , day : 2 } ) ,
296+ type : "Buy" ,
297+ asset : "BTC" ,
298+ num_quantity : "not-a-number" ,
299+ notes : "bad row" ,
300+ } ,
301+ ] ) ;
302+
303+ await expect ( coinbaseBalancesRegenerate ( { quiet : true } ) ) . rejects . toThrow (
304+ "Invalid transaction quantity for tx-bad: not-a-number" ,
305+ ) ;
306+ } ) ;
307+
225308} ) ;
0 commit comments