Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,549 @@
|
||||
import React from 'react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { StatusRule } from '../components/appChrome.js'
|
||||
import { DEFAULT_THEME } from '../theme.js'
|
||||
|
||||
type ReactNodeLike = React.ReactNode
|
||||
|
||||
const textContent = (node: ReactNodeLike): string => {
|
||||
if (node === null || node === undefined || typeof node === 'boolean') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof node === 'string' || typeof node === 'number') {
|
||||
return String(node)
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
return node.map(textContent).join('')
|
||||
}
|
||||
|
||||
if (React.isValidElement(node)) {
|
||||
return textContent(node.props.children)
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const findClickableWithText = (node: ReactNodeLike, needle: string): React.ReactElement | null => {
|
||||
if (node === null || node === undefined || typeof node === 'boolean') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const child of node) {
|
||||
const found = findClickableWithText(child, needle)
|
||||
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (!React.isValidElement(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof node.props.onClick === 'function' && textContent(node).includes(needle)) {
|
||||
return node
|
||||
}
|
||||
|
||||
return findClickableWithText(node.props.children, needle)
|
||||
}
|
||||
|
||||
// Find the innermost element whose own (direct) text content includes the
|
||||
// needle. Used to assert the colour the notice text is rendered with.
|
||||
const findElementWithText = (node: ReactNodeLike, needle: string): React.ReactElement | null => {
|
||||
if (node === null || node === undefined || typeof node === 'boolean') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const child of node) {
|
||||
const found = findElementWithText(child, needle)
|
||||
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (!React.isValidElement(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Prefer the deepest matching element so we get the leaf <Text> that
|
||||
// actually carries the colour, not an ancestor Box.
|
||||
const deeper = findElementWithText(node.props.children, needle)
|
||||
|
||||
if (deeper) {
|
||||
return deeper
|
||||
}
|
||||
|
||||
return textContent(node).includes(needle) ? node : null
|
||||
}
|
||||
|
||||
const baseProps = {
|
||||
bgCount: 0,
|
||||
busy: false,
|
||||
cols: 100,
|
||||
cwdLabel: '~/repo',
|
||||
liveSessionCount: 0,
|
||||
model: 'opus-4.8',
|
||||
sessionStartedAt: null,
|
||||
status: 'ready',
|
||||
statusColor: DEFAULT_THEME.color.ok,
|
||||
t: DEFAULT_THEME,
|
||||
turnStartedAt: null,
|
||||
usage: { context_max: 200_000, context_percent: 25, context_used: 50_000, total: 50_000 },
|
||||
voiceLabel: ''
|
||||
}
|
||||
|
||||
describe('StatusRule session title', () => {
|
||||
it('pins the named session at the far-right edge instead of the cwd label', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
sessionTitle: 'weekly-digest'
|
||||
})
|
||||
|
||||
const rendered = textContent(element)
|
||||
const title = findElementWithText(element, 'weekly-digest')
|
||||
|
||||
expect(rendered).toContain('weekly-digest')
|
||||
expect(rendered).not.toContain('~/repo')
|
||||
// Regression for issue #82465: a raw, full-saturation accent-hue
|
||||
// background (e.g. #FFBF00 on DARK_SEEDS) paired with statusFg (a
|
||||
// near-white tone never designed to sit on it) rendered at roughly a
|
||||
// 1.5-2:1 contrast ratio -- unreadable. No background fill at all;
|
||||
// the accent color goes on the text instead, matching the theme's
|
||||
// own convention that a raw accent hue is never used as a solid
|
||||
// fill elsewhere (fills are always softened, e.g. activeRow).
|
||||
expect(title?.props.backgroundColor).toBeUndefined()
|
||||
expect(title?.props.color).toBe(DEFAULT_THEME.color.accent)
|
||||
})
|
||||
})
|
||||
|
||||
describe('StatusRule background-subagent indicator', () => {
|
||||
it('renders ⛓ N on a wide terminal when subagents are running', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
usage: { ...baseProps.usage, active_subagents: 3 }
|
||||
})
|
||||
|
||||
expect(textContent(element)).toContain('⛓ 3')
|
||||
})
|
||||
|
||||
it('omits the segment when no subagents are running', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
usage: { ...baseProps.usage, active_subagents: 0 }
|
||||
})
|
||||
|
||||
expect(textContent(element)).not.toContain('⛓')
|
||||
})
|
||||
|
||||
it('omits the segment when the field is absent', () => {
|
||||
const element = StatusRule({ ...baseProps })
|
||||
|
||||
expect(textContent(element)).not.toContain('⛓')
|
||||
})
|
||||
|
||||
it('spells out the auto-resume hint when idle with subagents in flight', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
usage: { ...baseProps.usage, active_subagents: 1 }
|
||||
})
|
||||
|
||||
expect(textContent(element)).toContain('resumes when subagent finishes')
|
||||
})
|
||||
|
||||
it('pluralizes the resume hint for multiple in-flight subagents', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
usage: { ...baseProps.usage, active_subagents: 3 }
|
||||
})
|
||||
|
||||
expect(textContent(element)).toContain('resumes when 3 subagents finish')
|
||||
})
|
||||
|
||||
it('hides the resume hint mid-turn (a busy turn owns the indicator)', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
busy: true,
|
||||
turnStartedAt: Date.now(),
|
||||
usage: { ...baseProps.usage, active_subagents: 2 }
|
||||
})
|
||||
|
||||
expect(textContent(element)).not.toContain('resumes when')
|
||||
})
|
||||
|
||||
it('omits the resume hint when no subagents are running', () => {
|
||||
const element = StatusRule({ ...baseProps })
|
||||
|
||||
expect(textContent(element)).not.toContain('resumes when')
|
||||
})
|
||||
|
||||
it('drops the subagent segment before the bg segment on a narrow terminal', () => {
|
||||
// cols=44 is below the subagents breakpoint (92) but the bg breakpoint
|
||||
// (88) too — both gone. Assert the lower-priority subagent indicator is
|
||||
// not shown when space is tight even with a live count.
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
cols: 44,
|
||||
bgCount: 1,
|
||||
usage: { ...baseProps.usage, active_subagents: 2 }
|
||||
})
|
||||
|
||||
expect(textContent(element)).not.toContain('⛓')
|
||||
})
|
||||
})
|
||||
|
||||
describe('StatusRule session count click target', () => {
|
||||
it('makes the live session count itself clickable', () => {
|
||||
const openSwitcher = vi.fn()
|
||||
|
||||
const element = StatusRule({
|
||||
bgCount: 0,
|
||||
busy: false,
|
||||
cols: 100,
|
||||
cwdLabel: '~/repo',
|
||||
liveSessionCount: 1,
|
||||
model: 'kimi-k2.6',
|
||||
onSessionCountClick: openSwitcher,
|
||||
sessionStartedAt: null,
|
||||
status: 'ready',
|
||||
statusColor: DEFAULT_THEME.color.ok,
|
||||
t: DEFAULT_THEME,
|
||||
turnStartedAt: null,
|
||||
usage: { total: 0 },
|
||||
voiceLabel: ''
|
||||
})
|
||||
|
||||
const clickableSessionCount = findClickableWithText(element, '1 session')
|
||||
|
||||
expect(clickableSessionCount).not.toBeNull()
|
||||
clickableSessionCount!.props.onClick({ stopImmediatePropagation: vi.fn() })
|
||||
expect(openSwitcher).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps status + model and drops the low-value tail on a narrow terminal', () => {
|
||||
const element = StatusRule({
|
||||
bgCount: 0,
|
||||
busy: false,
|
||||
cols: 44,
|
||||
cwdLabel: '~/src/hermes-agent/apps/desktop (bb/tui-statusbar-responsive)',
|
||||
liveSessionCount: 3,
|
||||
model: 'opus-4.8',
|
||||
onSessionCountClick: vi.fn(),
|
||||
sessionStartedAt: Date.now() - 60_000,
|
||||
status: 'ready',
|
||||
statusColor: DEFAULT_THEME.color.ok,
|
||||
t: DEFAULT_THEME,
|
||||
turnStartedAt: null,
|
||||
usage: {
|
||||
calls: 0,
|
||||
context_max: 200_000,
|
||||
context_percent: 25,
|
||||
context_used: 50_000,
|
||||
input: 0,
|
||||
output: 0,
|
||||
total: 50_000
|
||||
},
|
||||
voiceLabel: 'voice off'
|
||||
})
|
||||
|
||||
const rendered = textContent(element)
|
||||
|
||||
// Must-keep essentials survive intact …
|
||||
expect(rendered).toContain('ready')
|
||||
expect(rendered).toContain('opus 4.8')
|
||||
// … while the low-value tail (session count) is dropped, not truncated.
|
||||
expect(rendered).not.toContain('3 sessions')
|
||||
})
|
||||
})
|
||||
|
||||
describe('StatusRule credits notice render priority', () => {
|
||||
it('replaces the idle status with the notice text and keeps model + context', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
notice: { key: 'credits.depleted', kind: 'sticky', level: 'error', text: '✕ credits exhausted' }
|
||||
})
|
||||
|
||||
const rendered = textContent(element)
|
||||
|
||||
// Notice replaces the status verb slot …
|
||||
expect(rendered).toContain('✕ credits exhausted')
|
||||
expect(rendered).not.toContain('ready')
|
||||
// … but model + context stay visible.
|
||||
expect(rendered).toContain('opus 4.8')
|
||||
expect(rendered).toContain('50k')
|
||||
})
|
||||
|
||||
it('busy wins: the FaceTicker shows, the notice is hidden mid-turn', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
busy: true,
|
||||
notice: { key: 'credits.90', kind: 'sticky', level: 'warn', text: '⚠ 90% used' },
|
||||
turnStartedAt: Date.now()
|
||||
})
|
||||
|
||||
const rendered = textContent(element)
|
||||
|
||||
// Notice must NOT render while busy.
|
||||
expect(rendered).not.toContain('⚠ 90% used')
|
||||
// Model still visible.
|
||||
expect(rendered).toContain('opus 4.8')
|
||||
})
|
||||
|
||||
it('colours the notice by level (error → theme error, success → statusGood)', () => {
|
||||
const errEl = StatusRule({
|
||||
...baseProps,
|
||||
notice: { key: 'credits.depleted', kind: 'sticky', level: 'error', text: '✕ exhausted' }
|
||||
})
|
||||
|
||||
const errText = findElementWithText(errEl, '✕ exhausted')
|
||||
expect(errText?.props.color).toBe(DEFAULT_THEME.color.error)
|
||||
|
||||
const okEl = StatusRule({
|
||||
...baseProps,
|
||||
notice: { key: 'credits.restored', kind: 'ttl', level: 'success', text: '✓ restored', ttl_ms: 8000 }
|
||||
})
|
||||
|
||||
const okText = findElementWithText(okEl, '✓ restored')
|
||||
expect(okText?.props.color).toBe(DEFAULT_THEME.color.statusGood)
|
||||
})
|
||||
|
||||
it('does NOT add a glyph — the notice text is rendered verbatim', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
notice: { key: 'credits.90', kind: 'sticky', level: 'warn', text: '⚠ 90% used' }
|
||||
})
|
||||
|
||||
const noticeText = findElementWithText(element, '90% used')
|
||||
|
||||
// The leaf carries exactly the policy text — no extra prepended glyph.
|
||||
expect(noticeText?.props.children).toBe('⚠ 90% used')
|
||||
})
|
||||
|
||||
it('the notice text is the shrinkable element (flexShrink=1 + truncate-end) so a long notice ellipsizes', () => {
|
||||
const longText = '⚠ ' + 'x'.repeat(200)
|
||||
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
cols: 50,
|
||||
notice: { key: 'credits.90', kind: 'sticky', level: 'warn', text: longText }
|
||||
})
|
||||
|
||||
// The leaf <Text> truncates rather than wrapping/clipping the pinned tail.
|
||||
const noticeText = findElementWithText(element, 'xxxxx')
|
||||
expect(noticeText?.props.wrap).toBe('truncate-end')
|
||||
|
||||
// Its container box yields first (flexShrink=1) so model stays visible.
|
||||
const findShrinkBoxContaining = (node: ReactNodeLike): React.ReactElement | null => {
|
||||
if (!React.isValidElement(node)) {
|
||||
if (Array.isArray(node)) {
|
||||
for (const c of node) {
|
||||
const f = findShrinkBoxContaining(c)
|
||||
|
||||
if (f) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (node.props.flexShrink === 1 && textContent(node).includes('xxxxx') && node.type !== StatusRule) {
|
||||
// Prefer the closest shrink box that wraps the notice text.
|
||||
const deeper = findShrinkBoxContaining(node.props.children)
|
||||
|
||||
return deeper ?? node
|
||||
}
|
||||
|
||||
return findShrinkBoxContaining(node.props.children)
|
||||
}
|
||||
|
||||
const shrinkBox = findShrinkBoxContaining(element)
|
||||
expect(shrinkBox).not.toBeNull()
|
||||
|
||||
// Model survives on a narrow terminal because the notice yields.
|
||||
expect(textContent(element)).toContain('opus 4.8')
|
||||
})
|
||||
})
|
||||
|
||||
describe('StatusRule battery indicator', () => {
|
||||
it('renders the battery label with a battery glyph on AC-off', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
battery: { available: true, category: 'good', percent: 82, plugged: false }
|
||||
})
|
||||
|
||||
expect(textContent(element)).toContain('🔋 82%')
|
||||
})
|
||||
|
||||
it('uses a bolt glyph while charging', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
battery: { available: true, category: 'good', percent: 82, plugged: true }
|
||||
})
|
||||
|
||||
expect(textContent(element)).toContain('⚡ 82%')
|
||||
})
|
||||
|
||||
it('colours the read-out by category (critical → theme statusCritical)', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
battery: { available: true, category: 'critical', percent: 7, plugged: false }
|
||||
})
|
||||
|
||||
const leaf = findElementWithText(element, '7%')
|
||||
expect(leaf?.props.color).toBe(DEFAULT_THEME.color.statusCritical)
|
||||
})
|
||||
|
||||
it('omits the segment when battery is null', () => {
|
||||
const element = StatusRule({ ...baseProps, battery: null })
|
||||
|
||||
expect(textContent(element)).not.toContain('%🔋')
|
||||
expect(textContent(element)).not.toContain('🔋')
|
||||
})
|
||||
|
||||
it('omits the segment when no battery is available (desktop/server)', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
battery: { available: false, category: 'dim', percent: null, plugged: null }
|
||||
})
|
||||
|
||||
expect(textContent(element)).not.toContain('🔋')
|
||||
})
|
||||
})
|
||||
|
||||
describe('StatusRule idle-since read-out', () => {
|
||||
// The IdleSince component uses hooks, so it can't be invoked outside a
|
||||
// renderer — assert on the element tree instead (same reason the duration
|
||||
// tests don't check SessionDuration's text).
|
||||
const findComponentByName = (node: ReactNodeLike, name: string): React.ReactElement | null => {
|
||||
if (node === null || node === undefined || typeof node === 'boolean') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const child of node) {
|
||||
const found = findComponentByName(child, name)
|
||||
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (!React.isValidElement(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof node.type === 'function' && node.type.name === name) {
|
||||
return node
|
||||
}
|
||||
|
||||
return findComponentByName(node.props.children, name)
|
||||
}
|
||||
|
||||
it('shows time since the last final agent response when idle', () => {
|
||||
const endedAt = Date.now() - 42_000
|
||||
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
lastTurnEndedAt: endedAt,
|
||||
sessionStartedAt: Date.now() - 60_000
|
||||
})
|
||||
|
||||
const idle = findComponentByName(element, 'IdleSince')
|
||||
|
||||
expect(idle).not.toBeNull()
|
||||
expect(idle!.props.endedAt).toBe(endedAt)
|
||||
})
|
||||
|
||||
it('is hidden while a turn is busy', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
busy: true,
|
||||
lastTurnEndedAt: Date.now() - 42_000,
|
||||
turnStartedAt: Date.now()
|
||||
})
|
||||
|
||||
expect(findComponentByName(element, 'IdleSince')).toBeNull()
|
||||
})
|
||||
|
||||
it('is hidden before the first turn completes', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
lastTurnEndedAt: null,
|
||||
sessionStartedAt: Date.now() - 60_000
|
||||
})
|
||||
|
||||
expect(findComponentByName(element, 'IdleSince')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('StatusRule perf read-outs (cache hit / latency / tps)', () => {
|
||||
const perfUsage = {
|
||||
...baseProps.usage,
|
||||
avg_latency_s: 3.2,
|
||||
avg_tps: 50.4,
|
||||
cache_hit_pct: 87,
|
||||
calls: 4,
|
||||
input: 1000,
|
||||
output: 500
|
||||
}
|
||||
|
||||
it('renders all three segments on a wide terminal', () => {
|
||||
const element = StatusRule({ ...baseProps, cols: 160, usage: perfUsage })
|
||||
const rendered = textContent(element)
|
||||
|
||||
expect(rendered).toContain('◎ 87%')
|
||||
expect(rendered).toContain('◷ 3.2s')
|
||||
expect(rendered).toContain('↑ 50 t/s')
|
||||
})
|
||||
|
||||
it('self-hides when the server omits the keys', () => {
|
||||
const element = StatusRule({ ...baseProps, cols: 160 })
|
||||
const rendered = textContent(element)
|
||||
|
||||
expect(rendered).not.toContain('◎')
|
||||
expect(rendered).not.toContain('◷')
|
||||
expect(rendered).not.toContain('t/s')
|
||||
})
|
||||
|
||||
it('honors the display.status_bar.fields visibility filter', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
cols: 160,
|
||||
statusBarFields: new Set(['model', 'context_pct', 'cache_hit']),
|
||||
usage: perfUsage
|
||||
})
|
||||
|
||||
const rendered = textContent(element)
|
||||
|
||||
expect(rendered).toContain('◎ 87%')
|
||||
expect(rendered).not.toContain('◷')
|
||||
expect(rendered).not.toContain('t/s')
|
||||
})
|
||||
|
||||
it('hides the session title badge when the fields filter omits title', () => {
|
||||
const element = StatusRule({
|
||||
...baseProps,
|
||||
cols: 160,
|
||||
sessionTitle: 'weekly-digest',
|
||||
statusBarFields: new Set(['model', 'context_pct'])
|
||||
})
|
||||
|
||||
expect(textContent(element)).not.toContain('weekly-digest')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user