list-sessions.spec.ts 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. /**
  2. * Tests for the `dsh list-sessions` presentation layer: uptime formatting, row building
  3. * (newest first, absent-title placeholder) and table alignment without trailing
  4. * padding. Every displayed field comes from the record, so there is no log
  5. * reading to cover here.
  6. */
  7. import { describe, expect, it } from 'vitest'
  8. import { SessionId } from '@deepseek-ai/dsh-session'
  9. import { BootId, type SessionRegistryRecord } from '@deepseek-ai/dsh-session-registry'
  10. import { buildRows, formatUptime, renderTable } from '../src/list-sessions.ts'
  11. function record(overrides: Partial<SessionRegistryRecord> = {}): SessionRegistryRecord {
  12. return {
  13. sessionId: SessionId('sess-1'),
  14. pid: 4242,
  15. cwd: '/work/project',
  16. startedAt: 1_000,
  17. bootId: BootId('boot-1'),
  18. ...overrides,
  19. }
  20. }
  21. describe('formatUptime', () => {
  22. it.each([
  23. [0, '0s'],
  24. [999, '0s'],
  25. [12_000, '12s'],
  26. [59_999, '59s'],
  27. [60_000, '1m'],
  28. [3_540_000, '59m'],
  29. [3_600_000, '1h'],
  30. [8_040_000, '2h14m'],
  31. [86_400_000, '1d'],
  32. [90_000_000, '1d1h'],
  33. ])('renders %ims as %s', (ms, expected) => {
  34. expect(formatUptime(ms)).toBe(expected)
  35. })
  36. it('never renders a negative duration for a clock that moved backwards', () => {
  37. expect(formatUptime(-5_000)).toBe('0s')
  38. })
  39. })
  40. describe('buildRows', () => {
  41. it('orders newest first and marks a missing title', () => {
  42. const rows = buildRows([
  43. record({ sessionId: SessionId('older'), startedAt: 1_000 }),
  44. record({ sessionId: SessionId('newer'), startedAt: 5_000 }),
  45. ], 65_000)
  46. expect(rows.map(row => row[0])).toEqual(['newer', 'older'])
  47. expect(rows[0]).toEqual(['newer', '4242', '1m', '/work/project', '—'])
  48. })
  49. })
  50. describe('renderTable', () => {
  51. it('aligns columns and leaves no trailing whitespace', () => {
  52. const table = renderTable(buildRows([
  53. record({ sessionId: SessionId('short'), startedAt: 0, title: 'a title' }),
  54. record({ sessionId: SessionId('a-much-longer-session-id'), startedAt: 1, title: 'a title' }),
  55. ], 1_000))
  56. const lines = table.split('\n')
  57. expect(lines[0]).toMatch(/^SESSION {18}\s+PID/)
  58. for (const line of lines) expect(line).toBe(line.trimEnd())
  59. // The header and every row align on the same column starts.
  60. const pidColumn = (line: string): number => line.includes('4242') ? line.indexOf('4242') : line.indexOf('PID')
  61. expect(pidColumn(lines[1] ?? '')).toBe(pidColumn(lines[0] ?? ''))
  62. expect(pidColumn(lines[2] ?? '')).toBe(pidColumn(lines[0] ?? ''))
  63. })
  64. it('renders a header even with no rows, so the columns stay discoverable', () => {
  65. expect(renderTable([])).toBe('SESSION PID UPTIME WORKSPACE TITLE\n')
  66. })
  67. })