Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion dashboard/src/lib/transfer-board.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@ import { RECOVERY_GUIDE, recoveryModeLabel, type RecoveryMode } from './transfer

// The Grafana board (observability/) shows the same recovery commands as the
// SPA from its own copy: recovery-guide.json for the guide. These tests fail
// when the copy drifts from this file's builders.
// when the copy drifts from this file's builders. They also cover the board's
// status-history.js, which orders a row's status entries in its dialog.

const panels = new URL('../../../observability/liquidity-panels/', import.meta.url)

type StatusEntry = { time: number; kind: string; status: string; direction?: string }
type StatusHistory = {
latest: (
entries: StatusEntry[]
) => (StatusEntry & { first: number; history: StatusEntry[] }) | null
}

// A plain script in the board, an ES module through its export line here.
const statusHistory = (await import(new URL('status-history.js', panels).href)) as StatusHistory

describe('board recovery-guide.json', () => {
it('matches RECOVERY_GUIDE and the mode labels', () => {
const guide = JSON.parse(readFileSync(new URL('recovery-guide.json', panels), 'utf8')) as {
Expand All @@ -20,3 +31,58 @@ describe('board recovery-guide.json', () => {
}
})
})

describe('board status-history.js', () => {
const bridge = (status: string, time: number, direction = 'alpaca_to_base'): StatusEntry => ({
time,
kind: 'usdc_bridge',
status,
direction
})
// Cloud Logging returns the entries newest first.
const order = (entries: StatusEntry[]) => {
const row = statusHistory.latest(entries)
return {
status: row?.status,
first: row?.first,
history: row?.history.map((entry) => entry.status)
}
}

it('orders in-flight bridge statuses by lifecycle when their times go backwards', () => {
// WithdrawalComplete carries confirmed_at, the BridgingSubmitting after it
// the older initiated_at.
expect(order([bridge('withdrawing', 200), bridge('bridging', 100)])).toEqual({
status: 'bridging',
first: 100,
history: ['withdrawing', 'bridging']
})
expect(
order([bridge('bridging', 100, 'base_to_alpaca'), bridge('converting', 50, 'base_to_alpaca')])
).toEqual({ status: 'converting', first: 50, history: ['bridging', 'converting'] })
})

it('orders in-flight bridge statuses that share a time by lifecycle, in either order', () => {
for (const entries of [
[bridge('bridging', 100), bridge('withdrawing', 100)],
[bridge('withdrawing', 100), bridge('bridging', 100)]
]) {
expect(order(entries).history).toEqual(['withdrawing', 'bridging'])
}
})

it('orders a failed entry by time, so a recovered bridge shows its newer status', () => {
expect(
order([bridge('bridging', 300), bridge('failed', 200), bridge('bridging', 100)])
).toEqual({ status: 'bridging', first: 100, history: ['bridging', 'failed', 'bridging'] })
expect(order([bridge('failed', 200), bridge('depositing', 100)]).status).toBe('failed')
})

it('orders other rows by time, ties newest last as Cloud Logging returned them', () => {
const mint = (status: string, time: number): StatusEntry => ({ time, kind: 'mint', status })
expect(
order([mint('wrapping', 100), mint('minting', 100), mint('pending', 50)]).history
).toEqual(['pending', 'minting', 'wrapping'])
expect(statusHistory.latest([])).toBeNull()
})
})
8 changes: 8 additions & 0 deletions observability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ band verdict (a symbol rebalanced on Base, with a balance, with no vault on
another chain, while the default Base target is set), so it is grey on the
`exporter` source, which cannot see the other chains.

Trades and Rebalances are Grafana tables too. A row's ⓘ sets the hidden `detail`
variable to the row's id; the `detail` panel, a second Business Text panel in
the header row's last column, then opens that row's dialog. It reads the two
tables' results through the Dashboard datasource (so Rebalances queries as refId
B) and runs no query of its own, so a click opens the dialog at once. The dialog
shows the row's status history (`status-history.js`); the bot's event timeline
joins it once the bot logs its events. Closing the dialog clears the variable.

Keep the Dashboard tab's panel heights (`TRADES_H`, `TRANSFERS_H` and the
`native_inventory` heights in the generator) unless you check a new set on a
preview board: reload it at a few window heights and confirm both columns end
Expand Down
Loading
Loading