hermes-agent / apps /desktop /src /contrib /runtime-loader.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
a1b6014 verified
Raw History Blame Contribute Delete
15 kB
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { HermesReadDirResult } from '@/global'
import type * as HermesModule from '@/hermes'
import { $pluginRecords, publishPlugin, setPluginEnabled } from './plugins-store'
import { discoverRuntimePlugins, loadRuntimePlugin, watchRuntimePlugins } from './runtime-loader'
// getStatus would supply the connected backend's hermes_home — a REMOTE path in
// remote mode. The disk scanner must NOT derive the plugin root from it (#66899).
const getStatus = vi.fn(async () => ({ hermes_home: '/remote/box/.hermes' }))
vi.mock('@/hermes', async importActual => ({
...(await importActual<typeof HermesModule>()),
getStatus: () => getStatus()
}))
const desktopPluginsRoot = vi.fn<() => Promise<string>>()
const readDir = vi.fn<(path: string) => Promise<HermesReadDirResult>>()
const readFileText = vi.fn<(path: string) => Promise<{ text: string; truncated?: boolean }>>()
const readPluginSource = vi.fn<(path: string) => Promise<{ text: string; truncated?: boolean }>>()
const watchDirectory = vi.fn<(path: string) => Promise<{ id: string }>>()
const watchPreviewFile = vi.fn<(path: string) => Promise<{ id: string }>>()
const stopPreviewFileWatch = vi.fn<(id: string) => Promise<boolean>>()
const onPreviewFileChanged = vi.fn()
beforeEach(() => {
desktopPluginsRoot.mockReset()
readDir.mockReset()
readFileText.mockReset()
readPluginSource.mockReset()
watchDirectory.mockReset()
watchPreviewFile.mockReset()
stopPreviewFileWatch.mockReset()
stopPreviewFileWatch.mockResolvedValue(true)
onPreviewFileChanged.mockReset()
getStatus.mockClear()
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
desktopPluginsRoot,
onPreviewFileChanged,
readDir,
readFileText,
stopPreviewFileWatch,
watchDirectory,
watchPreviewFile
}
})
afterEach(() => {
delete (window as unknown as { hermesDesktop?: unknown }).hermesDesktop
})
describe('scanDiskPlugins (#66899)', () => {
it('scans the Electron-resolved local roots, never the backend hermes_home', async () => {
desktopPluginsRoot.mockResolvedValue('/local/.hermes/desktop-plugins')
readDir.mockResolvedValue({ entries: [] })
await discoverRuntimePlugins()
expect(desktopPluginsRoot).toHaveBeenCalled()
expect(readDir).toHaveBeenCalledWith('/local/.hermes/desktop-plugins')
// Unified halves are COPIED into the app root by Electron; the renderer
// never scans the (profile-shaped) agent-plugins root itself.
expect(readDir).not.toHaveBeenCalledWith('/local/.hermes/plugins')
// The remote backend's hermes_home must never feed the local plugin scan.
expect(getStatus).not.toHaveBeenCalled()
expect(readDir).not.toHaveBeenCalledWith('/remote/box/.hermes/desktop-plugins')
})
it('no-ops when the resolvers yield no local root', async () => {
desktopPluginsRoot.mockResolvedValue('')
await discoverRuntimePlugins()
expect(readDir).not.toHaveBeenCalled()
})
it('treats a folder without plugin.js as metadata, not a throwing file read', async () => {
desktopPluginsRoot.mockResolvedValue('/local/.hermes/desktop-plugins')
readDir.mockImplementation(async dir => {
if (dir === '/local/.hermes/desktop-plugins') {
return {
entries: [{ isDirectory: true, name: 'my-feature', path: '/local/.hermes/desktop-plugins/my-feature' }]
}
}
if (dir === '/local/.hermes/desktop-plugins/my-feature') {
return {
entries: [
{ isDirectory: false, name: 'README.md', path: '/local/.hermes/desktop-plugins/my-feature/README.md' }
]
}
}
return { entries: [] }
})
await discoverRuntimePlugins()
expect(readDir).toHaveBeenCalledWith('/local/.hermes/desktop-plugins/my-feature')
expect(readFileText).not.toHaveBeenCalled()
})
it('a DIRECTORY named plugin.js is not a plugin entry (metadata walk rejects it)', async () => {
desktopPluginsRoot.mockResolvedValue('/local/.hermes/desktop-plugins')
readDir.mockImplementation(async dir => {
if (dir === '/local/.hermes/desktop-plugins') {
return { entries: [{ isDirectory: true, name: 'odd', path: '/local/.hermes/desktop-plugins/odd' }] }
}
if (dir === '/local/.hermes/desktop-plugins/odd') {
// A folder literally named plugin.js — must resolve to "no entry".
return {
entries: [{ isDirectory: true, name: 'plugin.js', path: '/local/.hermes/desktop-plugins/odd/plugin.js' }]
}
}
return { entries: [] }
})
await discoverRuntimePlugins()
expect(readFileText).not.toHaveBeenCalled()
expect($pluginRecords.get().odd).toBeUndefined()
})
it('loads a unified desktop half (app-root copy + package marker) OPT-IN and tags it with its package', async () => {
desktopPluginsRoot.mockResolvedValue('/local/.hermes/desktop-plugins')
let desktopEntryPresent = true
const root = '/local/.hermes/desktop-plugins'
readDir.mockImplementation(async dir => {
if (dir === root) {
return { entries: desktopEntryPresent ? [{ isDirectory: true, name: 'uni', path: `${root}/uni` }] : [] }
}
if (dir === `${root}/uni`) {
return {
entries: [
{ isDirectory: false, name: '.hermes-package.json', path: `${root}/uni/.hermes-package.json` },
{ isDirectory: false, name: 'plugin.js', path: `${root}/uni/plugin.js` }
]
}
}
return { entries: [] }
})
const register = vi.fn()
;(globalThis as unknown as { __uniRegister: unknown }).__uniRegister = register
readFileText.mockImplementation(async file =>
file.endsWith('.hermes-package.json')
? { text: JSON.stringify({ package: 'uni-pkg', source: '/x/plugins/uni-pkg/desktop', sourceMtimeMs: 1 }) }
: { text: 'export default { id: "uni", register: globalThis.__uniRegister }' }
)
watchPreviewFile.mockResolvedValue({ id: 'w-uni' })
// The loader evaluates plugins via blob-URL import(), which vite's module
// runner can't resolve in tests — reroute to a data: URL, which node's
// native ESM loader handles.
const createObjectURL = vi
.spyOn(URL, 'createObjectURL')
.mockImplementation(
blob =>
`data:text/javascript;base64,${Buffer.from((blob as unknown as { parts: string[] }).parts.join('')).toString('base64')}`
)
const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined)
const RealBlob = globalThis.Blob
vi.stubGlobal(
'Blob',
class {
parts: string[]
constructor(parts: string[]) {
this.parts = parts
}
}
)
try {
await discoverRuntimePlugins()
// Inventoried for Capabilities → Plugins with its package identity, but
// the unified posture wins: installed-but-inert until the user toggles.
expect($pluginRecords.get().uni).toMatchObject({ kind: 'disk', status: 'disabled', packageName: 'uni-pkg' })
expect(register).not.toHaveBeenCalled()
// The user's explicit enable still activates it.
await setPluginEnabled('uni', true)
expect(register).toHaveBeenCalledTimes(1)
expect($pluginRecords.get().uni.status).toBe('loaded')
// Electron removing the copy (package uninstalled) unloads the previous
// Desktop registration instead of leaving a live ghost behind.
desktopEntryPresent = false
await discoverRuntimePlugins()
expect($pluginRecords.get().uni).toBeUndefined()
expect(stopPreviewFileWatch).toHaveBeenCalledWith('w-uni')
} finally {
createObjectURL.mockRestore()
revokeObjectURL.mockRestore()
vi.stubGlobal('Blob', RealBlob)
delete (globalThis as unknown as { __uniRegister?: unknown }).__uniRegister
}
})
})
describe('watchRuntimePlugins dir watch (#66899)', () => {
it('watches the Electron-resolved app root, never the backend hermes_home', async () => {
desktopPluginsRoot.mockResolvedValue('/local/.hermes/desktop-plugins')
readDir.mockResolvedValue({ entries: [] })
watchDirectory.mockResolvedValue({ id: 'watch-1' })
watchRuntimePlugins()
// Drain the async scan + startDirWatches chains.
await vi.waitFor(() => expect(watchDirectory).toHaveBeenCalledTimes(1))
expect(watchDirectory).toHaveBeenCalledWith('/local/.hermes/desktop-plugins')
expect(watchDirectory).not.toHaveBeenCalledWith('/remote/box/.hermes/desktop-plugins')
expect(getStatus).not.toHaveBeenCalled()
})
})
describe('plugin source reads (512 KiB preview-cap bug)', () => {
const blobToDataUrl = () => {
const createObjectURL = vi
.spyOn(URL, 'createObjectURL')
.mockImplementation(
blob =>
`data:text/javascript;base64,${Buffer.from((blob as unknown as { parts: string[] }).parts.join('')).toString('base64')}`
)
const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined)
const RealBlob = globalThis.Blob
vi.stubGlobal(
'Blob',
class {
parts: string[]
constructor(parts: string[]) {
this.parts = parts
}
}
)
return () => {
createObjectURL.mockRestore()
revokeObjectURL.mockRestore()
vi.stubGlobal('Blob', RealBlob)
}
}
/** Two-level standalone-root listing the metadata-walk probe needs:
* the root lists the package folder, the folder lists plugin.js. */
const standaloneRootWith = (name: string) => {
const folder = `/local/.hermes/desktop-plugins/${name}`
readDir.mockImplementation(async dir => {
if (dir === '/local/.hermes/desktop-plugins') {
return { entries: [{ isDirectory: true, name, path: folder }] }
}
if (dir === folder) {
return { entries: [{ isDirectory: false, name: 'plugin.js', path: `${folder}/plugin.js` }] }
}
return { entries: [] }
})
}
it('loads the full source via readPluginSource when the shell offers it', async () => {
;(window.hermesDesktop as unknown as { readPluginSource: unknown }).readPluginSource = readPluginSource
desktopPluginsRoot.mockResolvedValue('/local/.hermes/desktop-plugins')
standaloneRootWith('big')
// The preview read would truncate this source — it must never be used.
readFileText.mockResolvedValue({ text: '// first 512 KiB only', truncated: true })
const register = vi.fn()
;(globalThis as unknown as { __bigRegister: unknown }).__bigRegister = register
readPluginSource.mockResolvedValue({
text: 'export default { id: "big", register: globalThis.__bigRegister }'
})
watchPreviewFile.mockResolvedValue({ id: 'w-big' })
const restore = blobToDataUrl()
try {
await discoverRuntimePlugins()
// The EVALUATED source came from the full read, not the truncated preview.
expect(readPluginSource).toHaveBeenCalledWith('/local/.hermes/desktop-plugins/big/plugin.js')
expect(register).toHaveBeenCalledTimes(1)
expect($pluginRecords.get().big).toMatchObject({ kind: 'disk', status: 'loaded' })
} finally {
restore()
delete (globalThis as unknown as { __bigRegister?: unknown }).__bigRegister
}
})
it('older shell without readPluginSource: a truncated preview read fails LOUDLY, never evaluates', async () => {
desktopPluginsRoot.mockResolvedValue('/local/.hermes/desktop-plugins')
standaloneRootWith('huge')
// 512 KiB window of a larger file — parses fine, but is NOT the plugin.
readFileText.mockResolvedValue({
text: 'export default { id: "huge", register: () => { throw new Error("must never evaluate") } }',
truncated: true
})
watchPreviewFile.mockResolvedValue({ id: 'w-huge' })
const restore = blobToDataUrl()
try {
await discoverRuntimePlugins()
// No live plugin — an error inventory row names the folder instead.
expect($pluginRecords.get().huge).toMatchObject({
kind: 'disk',
status: 'error',
file: '/local/.hermes/desktop-plugins/huge/plugin.js'
})
expect($pluginRecords.get().huge.error).toMatch(/512 KiB/)
} finally {
restore()
}
})
it('older shell, small plugin (not truncated): still loads through readFileText', async () => {
desktopPluginsRoot.mockResolvedValue('/local/.hermes/desktop-plugins')
standaloneRootWith('small')
const register = vi.fn()
;(globalThis as unknown as { __smallRegister: unknown }).__smallRegister = register
readFileText.mockResolvedValue({
text: 'export default { id: "small", register: globalThis.__smallRegister }'
})
watchPreviewFile.mockResolvedValue({ id: 'w-small' })
const restore = blobToDataUrl()
try {
await discoverRuntimePlugins()
expect(register).toHaveBeenCalledTimes(1)
expect($pluginRecords.get().small).toMatchObject({ kind: 'disk', status: 'loaded' })
} finally {
restore()
delete (globalThis as unknown as { __smallRegister?: unknown }).__smallRegister
}
})
})
describe('bundled-shadowed disk copies', () => {
it('skips a disk copy of a bundled plugin but publishes a visible inventory row', async () => {
// The bundled twin is already registered (build-time glob).
publishPlugin({ id: 'hermes-bots', name: 'Bot Mode', kind: 'bundled', status: 'loaded' })
// Same blob→data: URL reroute as the opt-in test above.
const createObjectURL = vi
.spyOn(URL, 'createObjectURL')
.mockImplementation(
blob =>
`data:text/javascript;base64,${Buffer.from((blob as unknown as { parts: string[] }).parts.join('')).toString('base64')}`
)
const revokeObjectURL = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined)
const RealBlob = globalThis.Blob
vi.stubGlobal(
'Blob',
class {
parts: string[]
constructor(parts: string[]) {
this.parts = parts
}
}
)
try {
const id = await loadRuntimePlugin(
'export default { id: "hermes-bots", name: "Bot Mode", register() {} }',
'hermes-bots',
{ file: '/local/.hermes/desktop-plugins/hermes-bots/plugin.js' }
)
// Skipped — the bundled copy stays the only live registration...
expect(id).toBeNull()
expect($pluginRecords.get()['hermes-bots']).toMatchObject({ kind: 'bundled', status: 'loaded' })
// ...but the stale folder is DISCOVERABLE: an inventory row names it,
// carries its path (reveal/delete affordance), and can never activate.
expect($pluginRecords.get()['hermes-bots:disk-shadowed']).toMatchObject({
kind: 'disk',
status: 'disabled',
file: '/local/.hermes/desktop-plugins/hermes-bots/plugin.js'
})
} finally {
createObjectURL.mockRestore()
revokeObjectURL.mockRestore()
vi.stubGlobal('Blob', RealBlob)
}
})
})