mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
test(tui): cover heapdump opt-in gate + retention; add AUTHOR_MAP
On-disk vitest coverage for the auto-heapdump disk-safety guard: opt-in gating (suppressed diagnostics-only path), truthy-spelling acceptance, manual-trigger passthrough, and the retention prune. Test approach adapted from #21780 (briandevans) and #21822 (LeonSGP43), reconciled to the merged gate semantics. Maps alarcritty into AUTHOR_MAP for CI.
This commit is contained in:
parent
8ae0d054f4
commit
00c46b8ff9
2 changed files with 163 additions and 0 deletions
|
|
@ -65,6 +65,7 @@ AUTHOR_MAP = {
|
|||
"129007007+HeLLGURD@users.noreply.github.com": "HeLLGURD",
|
||||
"290859878+synapsesx@users.noreply.github.com": "synapsesx",
|
||||
"dirtyren@users.noreply.github.com": "dirtyren",
|
||||
"adityamalik2833@gmail.com": "alarcritty",
|
||||
"islam666@users.noreply.github.com": "islam666",
|
||||
"25539605+lsaether@users.noreply.github.com": "lsaether",
|
||||
"30080538+JimStenstrom@users.noreply.github.com": "JimStenstrom",
|
||||
|
|
|
|||
162
ui-tui/src/lib/memory.test.ts
Normal file
162
ui-tui/src/lib/memory.test.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
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)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue