hermes-agent/ui-tui/src/lib/memory.test.ts
Brooklyn Nicholson 62fe9fd101 style(desktop,tui): fix all lint/type/formatting issues
Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:

- Run prettier across both trees (printWidth/wrap drift; prettier is not
  CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
  import/export sorting.
- Manual fixes for non-auto-fixable rules:
  - remove unused node:net import in electron/main.cjs (uses Electron net)
  - replace inline `typeof import(...)` annotations with top-level
    `import type * as EnvModule` in two ui-tui test files
  - scoped eslint-disable no-control-regex on intentional sentinel/ANSI
    regexes (mathUnicode.ts, text.ts)
  - resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
    deps, collapse redundant session.* members, and justified disables on
    settings mount-only data-load effects to preserve run-once behavior

No behavior changes; test pass/fail counts are unchanged from the main
baseline.
2026-06-26 01:04:33 -05:00

168 lines
5.5 KiB
TypeScript

import { mkdtempSync, readdirSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { performHeapDump } from './memory.js'
const ENV_KEYS = ['HERMES_AUTO_HEAPDUMP', 'HERMES_HEAPDUMP_DIR', 'HERMES_HEAPDUMP_MAX_BYTES'] as const
describe('performHeapDump auto opt-in gate (#21767)', () => {
let saved: Record<string, string | undefined>
let dir: string
beforeEach(() => {
saved = {}
for (const k of ENV_KEYS) {
saved[k] = process.env[k]
delete process.env[k]
}
dir = mkdtempSync(join(tmpdir(), 'hermes-heapdump-test-'))
process.env.HERMES_HEAPDUMP_DIR = dir
})
afterEach(() => {
for (const k of ENV_KEYS) {
if (saved[k] === undefined) {
delete process.env[k]
} else {
process.env[k] = saved[k]
}
}
rmSync(dir, { force: true, recursive: true })
})
it('writes diagnostics only for auto-high without HERMES_AUTO_HEAPDUMP', async () => {
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
expect(result.suppressed).toBe(true)
expect(result.diagPath).toBeDefined()
expect(result.heapPath).toBeUndefined()
const files = readdirSync(dir)
expect(files.some(f => f.endsWith('.diagnostics.json'))).toBe(true)
expect(files.some(f => f.endsWith('.heapsnapshot'))).toBe(false)
})
it('writes diagnostics only for auto-critical without HERMES_AUTO_HEAPDUMP', async () => {
const result = await performHeapDump('auto-critical')
expect(result.success).toBe(true)
expect(result.suppressed).toBe(true)
expect(result.heapPath).toBeUndefined()
const files = readdirSync(dir)
expect(files.some(f => f.endsWith('.heapsnapshot'))).toBe(false)
})
it('writes both diagnostics and snapshot for auto-high when HERMES_AUTO_HEAPDUMP=1', async () => {
process.env.HERMES_AUTO_HEAPDUMP = '1'
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
expect(result.suppressed).toBeUndefined()
expect(result.diagPath).toBeDefined()
expect(result.heapPath).toBeDefined()
const files = readdirSync(dir)
expect(files.some(f => f.endsWith('.heapsnapshot'))).toBe(true)
})
it('accepts truthy spellings (true|yes|on, case-insensitive) as opt-in', async () => {
for (const value of ['true', 'YES', 'On']) {
process.env.HERMES_AUTO_HEAPDUMP = value
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
expect(result.heapPath).toBeDefined()
}
})
it('treats other values (0, off, garbage) as opt-out for auto triggers', async () => {
for (const value of ['0', 'off', 'nope']) {
process.env.HERMES_AUTO_HEAPDUMP = value
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
expect(result.suppressed).toBe(true)
expect(result.heapPath).toBeUndefined()
}
})
it('writes both for manual triggers regardless of HERMES_AUTO_HEAPDUMP', async () => {
const result = await performHeapDump('manual')
expect(result.success).toBe(true)
expect(result.suppressed).toBeUndefined()
expect(result.heapPath).toBeDefined()
const files = readdirSync(dir)
expect(files.some(f => f.endsWith('.heapsnapshot'))).toBe(true)
})
})
describe('heapdump retention guard (#21767)', () => {
let savedDir: string | undefined
let savedMax: string | undefined
let dir: string
beforeEach(() => {
savedDir = process.env.HERMES_HEAPDUMP_DIR
savedMax = process.env.HERMES_HEAPDUMP_MAX_BYTES
delete process.env.HERMES_AUTO_HEAPDUMP
dir = mkdtempSync(join(tmpdir(), 'hermes-heapdump-prune-'))
process.env.HERMES_HEAPDUMP_DIR = dir
})
afterEach(() => {
if (savedDir === undefined) {
delete process.env.HERMES_HEAPDUMP_DIR
} else {
process.env.HERMES_HEAPDUMP_DIR = savedDir
}
if (savedMax === undefined) {
delete process.env.HERMES_HEAPDUMP_MAX_BYTES
} else {
process.env.HERMES_HEAPDUMP_MAX_BYTES = savedMax
}
rmSync(dir, { force: true, recursive: true })
})
it('evicts oldest files when total bytes exceed the cap, retaining the newest', async () => {
// 4 pre-existing dumps, 1KB each, with ascending mtimes (oldest first).
const blob = 'x'.repeat(1024)
const now = Date.now()
for (let i = 0; i < 4; i++) {
const p = join(dir, `old-${i}.heapsnapshot`)
writeFileSync(p, blob)
const t = (now - (4 - i) * 60_000) / 1000
utimesSync(p, t, t)
}
// Cap at 2KB → a fresh diagnostics write should trigger a prune down to ~cap.
process.env.HERMES_HEAPDUMP_MAX_BYTES = String(2 * 1024)
const result = await performHeapDump('auto-high')
expect(result.success).toBe(true)
const remaining = readdirSync(dir)
const totalBytes = remaining.reduce((acc, f) => acc + statSync(join(dir, f)).size, 0)
// Contract: prune evicts oldest-first until total <= cap, but always keeps
// the single newest file even if it alone exceeds the cap. So either the
// total is under cap, or exactly one (newest) file remains.
expect(totalBytes <= 2 * 1024 || remaining.length === 1).toBe(true)
// The old 1KB dumps must have been pruned down from the original four.
expect(remaining.length).toBeLessThan(5)
// The brand-new diagnostics sidecar must survive the prune.
expect(remaining.some(f => f.endsWith('.diagnostics.json'))).toBe(true)
})
})