hermes-agent/apps/desktop/electron/profile-delete-respawn.test.cjs
liuhao1024 c5e8a60b0a fix(desktop): skip ensureBackend after profile-delete teardown to prevent respawn loop
When the renderer sends a DELETE /api/profiles/{name} request, the IPC
handler tears down the profile's pool backend (or primary backend) via
prepareProfileDeleteRequest.  However, the very next line calls
ensureBackend(profile), which spawns a fresh pool backend for the just-
deleted profile.  The new backend's startup path calls ensure_hermes_home(),
which recreates the profile directory — defeating the deletion and leaving
the process as a zombie.

On the next Desktop restart the cycle repeats: the profile directory exists,
the Desktop spawns a backend, the backend recreates the directory after
deletion, and PIDs accumulate indefinitely.

Fix: make prepareProfileDeleteRequest return the torn-down profile name.
The IPC handler uses this to route the DELETE to the primary backend
instead of spawning a new pool backend for the deleted profile.

Fixes #52279
2026-07-02 15:18:49 -05:00

66 lines
2.4 KiB
JavaScript

'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const ELECTRON_DIR = __dirname
function readElectronFile(name) {
return fs.readFileSync(path.join(ELECTRON_DIR, name), 'utf8').replace(/\r\n/g, '\n')
}
// ---------------------------------------------------------------------------
// prepareProfileDeleteRequest must return the torn-down profile name so the
// caller can skip ensureBackend for that profile (issue #52279).
// ---------------------------------------------------------------------------
test('prepareProfileDeleteRequest returns the torn-down profile name', () => {
const source = readElectronFile('main.cjs')
// Locate the function definition and its closing brace.
const fnStart = source.indexOf('async function prepareProfileDeleteRequest(')
assert.notEqual(fnStart, -1, 'prepareProfileDeleteRequest function not found')
// The function must contain "return profile" (pool and primary paths).
const fnBody = source.slice(fnStart, fnStart + 800)
const returnProfileCount = (fnBody.match(/return profile/g) || []).length
assert.ok(
returnProfileCount >= 2,
`expected at least 2 "return profile" statements (primary + pool paths), found ${returnProfileCount}`
)
// The early-exit guard must return null (not void/undefined).
assert.match(
fnBody,
/return null/,
'early-exit guard should return null, not undefined'
)
})
test('hermes:api handler routes profile-delete requests to the primary backend', () => {
const source = readElectronFile('main.cjs')
// The handler must capture prepareProfileDeleteRequest's return value.
assert.match(
source,
/const tornDownProfile = await prepareProfileDeleteRequest\(request\)/,
'handler should capture the return value of prepareProfileDeleteRequest'
)
// The handler must use the return value to skip ensureBackend for the
// torn-down profile, routing to the primary (null) instead.
assert.match(
source,
/const routeProfile = tornDownProfile \? null : profile/,
'handler should route to primary backend when a profile was just torn down'
)
// ensureBackend must be called with the conditional route profile.
assert.match(
source,
/const connection = await ensureBackend\(routeProfile\)/,
'handler should pass routeProfile (not raw profile) to ensureBackend'
)
})