mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-21 10:22:18 +00:00
refactor(tui): reuse DASHBOARD_TUI_MODE for hosted /exit guard
Follow-up to the salvaged hosted /exit fix. Instead of a separate 4-env-var fingerprint (HERMES_TUI_INLINE + /opt/data HERMES_HOME + HERMES_WRITE_SAFE_ROOT + HERMES_DISABLE_LAZY_INSTALLS), gate /exit and /quit on the existing DASHBOARD_TUI_MODE flag (HERMES_TUI_DASHBOARD) that the keyboard idle-exit (useInputHandlers) and SIGINT-ignore (entry.tsx) paths already use. One hosted detection mechanism instead of two divergent ones. Extract the refusal text to an exported DASHBOARD_EXIT_DISABLED_MESSAGE so the test asserts the same source of truth as production (no change-detector on the literal). Test mocks only the DASHBOARD_TUI_MODE export via importActual so the other env exports stay real.
This commit is contained in:
parent
15e3b64b75
commit
3f0e9849e7
2 changed files with 34 additions and 31 deletions
|
|
@ -2,17 +2,30 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||
|
||||
import { createSlashHandler } from '../app/createSlashHandler.js'
|
||||
import { getOverlayState, resetOverlayState } from '../app/overlayStore.js'
|
||||
import { DASHBOARD_EXIT_DISABLED_MESSAGE } from '../app/slash/commands/core.js'
|
||||
import { getUiState, patchUiState, resetUiState } from '../app/uiStore.js'
|
||||
import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js'
|
||||
|
||||
// DASHBOARD_TUI_MODE resolves once at module load from HERMES_TUI_DASHBOARD,
|
||||
// so toggling process.env in a test body can't move it. Mock just that one
|
||||
// export (everything else stays real) and flip the holder per test.
|
||||
const envState = { dashboardTuiMode: false }
|
||||
vi.mock('../config/env.js', async importActual => {
|
||||
const actual = await importActual<typeof import('../config/env.js')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
get DASHBOARD_TUI_MODE() {
|
||||
return envState.dashboardTuiMode
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('createSlashHandler', () => {
|
||||
beforeEach(() => {
|
||||
resetOverlayState()
|
||||
resetUiState()
|
||||
delete process.env.HERMES_TUI_INLINE
|
||||
delete process.env.HERMES_HOME
|
||||
delete process.env.HERMES_WRITE_SAFE_ROOT
|
||||
delete process.env.HERMES_DISABLE_LAZY_INSTALLS
|
||||
envState.dashboardTuiMode = false
|
||||
})
|
||||
|
||||
it('opens the unified sessions overlay for /resume', () => {
|
||||
|
|
@ -73,25 +86,17 @@ describe('createSlashHandler', () => {
|
|||
})
|
||||
|
||||
it('keeps hosted dashboard chat alive for /exit', () => {
|
||||
process.env.HERMES_TUI_INLINE = '1'
|
||||
process.env.HERMES_HOME = '/opt/data/profiles/worker'
|
||||
process.env.HERMES_WRITE_SAFE_ROOT = '/opt/data'
|
||||
process.env.HERMES_DISABLE_LAZY_INSTALLS = '1'
|
||||
envState.dashboardTuiMode = true
|
||||
const ctx = buildCtx()
|
||||
|
||||
expect(createSlashHandler(ctx)('/exit')).toBe(true)
|
||||
expect(ctx.session.die).not.toHaveBeenCalled()
|
||||
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
||||
expect(ctx.transcript.sys).toHaveBeenCalledWith(
|
||||
'exit is disabled in hosted dashboard chat — use /new to start a fresh session'
|
||||
)
|
||||
expect(ctx.transcript.sys).toHaveBeenCalledWith(DASHBOARD_EXIT_DISABLED_MESSAGE)
|
||||
})
|
||||
|
||||
it('keeps /quit available outside hosted dashboard chat', () => {
|
||||
process.env.HERMES_TUI_INLINE = '1'
|
||||
process.env.HERMES_HOME = '/Users/example/.hermes'
|
||||
process.env.HERMES_WRITE_SAFE_ROOT = '/Users/example/.hermes'
|
||||
process.env.HERMES_DISABLE_LAZY_INSTALLS = '1'
|
||||
envState.dashboardTuiMode = false
|
||||
const ctx = buildCtx()
|
||||
|
||||
expect(createSlashHandler(ctx)('/quit')).toBe(true)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { forceRedraw, type MouseTrackingMode } from '@hermes/ink'
|
||||
|
||||
import { NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js'
|
||||
import { DASHBOARD_TUI_MODE, NO_CONFIRM_DESTRUCTIVE } from '../../../config/env.js'
|
||||
import { dailyFortune, randomFortune } from '../../../content/fortunes.js'
|
||||
import { HOTKEYS } from '../../../content/hotkeys.js'
|
||||
import { isSectionName, nextDetailsMode, parseDetailsMode, SECTION_NAMES } from '../../../domain/details.js'
|
||||
|
|
@ -76,19 +76,10 @@ const DETAILS_USAGE =
|
|||
|
||||
const DETAILS_SECTION_USAGE = 'usage: /details <section> [hidden|collapsed|expanded|reset]'
|
||||
|
||||
const truthyEnv = (v?: string) => /^(?:1|true|yes|on)$/i.test((v ?? '').trim())
|
||||
|
||||
const hostedInlineDashboardChat = () => {
|
||||
const hermesHome = (process.env.HERMES_HOME ?? '').trim()
|
||||
const hostedHome = hermesHome === '/opt/data' || hermesHome.startsWith('/opt/data/')
|
||||
|
||||
return (
|
||||
process.env.HERMES_TUI_INLINE === '1' &&
|
||||
hostedHome &&
|
||||
process.env.HERMES_WRITE_SAFE_ROOT === '/opt/data' &&
|
||||
truthyEnv(process.env.HERMES_DISABLE_LAZY_INSTALLS)
|
||||
)
|
||||
}
|
||||
// Shown when /exit or /quit is refused in the hosted dashboard chat. Kept as a
|
||||
// constant so the test asserts against the same source of truth as production.
|
||||
export const DASHBOARD_EXIT_DISABLED_MESSAGE =
|
||||
'exit is disabled in hosted dashboard chat — use /new to start a fresh session'
|
||||
|
||||
export const coreCommands: SlashCommand[] = [
|
||||
{
|
||||
|
|
@ -128,8 +119,15 @@ export const coreCommands: SlashCommand[] = [
|
|||
help: 'exit hermes',
|
||||
name: 'quit',
|
||||
run: (_arg, ctx) => {
|
||||
if (hostedInlineDashboardChat()) {
|
||||
ctx.transcript.sys('exit is disabled in hosted dashboard chat — use /new to start a fresh session')
|
||||
// In the hosted dashboard chat there is no in-page restart path after
|
||||
// the PTY child exits, so quitting bricks the tab until a refresh. The
|
||||
// keyboard idle-exit (Ctrl+C / Ctrl+D) and SIGINT handling already refuse
|
||||
// to die in this mode (see useInputHandlers + entry.tsx); gate /exit and
|
||||
// /quit on the same DASHBOARD_TUI_MODE flag. Unlike the keyboard path
|
||||
// (which auto-starts a fresh chat), the explicit quit command refuses and
|
||||
// instructs the user to run /new themselves.
|
||||
if (DASHBOARD_TUI_MODE) {
|
||||
ctx.transcript.sys(DASHBOARD_EXIT_DISABLED_MESSAGE)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue