Merge #5168 (pet-handoff) into stage-pq1
This commit is contained in:
commit
1f00e4de8c
4 files changed
+726
-1
No files matched your search
+133
-1
@@ -48,6 +48,127 @@ function parseCommand(text){
|
||||
return {name,args};
|
||||
}
|
||||
|
||||
const DESKTOP_COMPANION_EXTENSION_ID='desktop-companion';
|
||||
const DESKTOP_COMPANION_NAME='Desktop Companion';
|
||||
const DESKTOP_COMPANION_INSTALL_PATH='Settings -> Extensions -> Gallery -> Desktop Companion';
|
||||
const DESKTOP_COMPANION_SETUP_GUIDE_URL='https://github.com/franksong2702/hermes-webui-desktop-companion#after-gallery-install';
|
||||
const DESKTOP_COMPANION_LOCAL_APP_LABEL='Desktop Companion app';
|
||||
|
||||
function _getDesktopCompanionStatusGlobal(){
|
||||
if(typeof window==='undefined') return null;
|
||||
return window.__HERMES_WEBUI_DESKTOP_COMPANION_STATUS__||null;
|
||||
}
|
||||
|
||||
function _getDesktopCompanionExtensionStatus(status){
|
||||
return Array.isArray(status&&status.extensions)
|
||||
? status.extensions.find(ext=>String(ext&&ext.id||'')===DESKTOP_COMPANION_EXTENSION_ID)||null
|
||||
: null;
|
||||
}
|
||||
|
||||
function _petSlashCommandArgs(rawCommandText){
|
||||
const parsed=parseCommand(String(rawCommandText||'').trim());
|
||||
return parsed&&parsed.name==='pet' ? parsed.args : String(rawCommandText||'').trim().replace(/^\/pet\b\s*/i,'').trim();
|
||||
}
|
||||
|
||||
function _desktopCompanionMissingMessage(){
|
||||
return `${DESKTOP_COMPANION_NAME} is not installed yet.
|
||||
|
||||
Install it from ${DESKTOP_COMPANION_INSTALL_PATH}, then follow the setup guide: ${DESKTOP_COMPANION_SETUP_GUIDE_URL}.
|
||||
|
||||
The guide also shows how to start the ${DESKTOP_COMPANION_LOCAL_APP_LABEL}.`;
|
||||
}
|
||||
|
||||
function _desktopCompanionDisabledMessage(){
|
||||
return `${DESKTOP_COMPANION_NAME} is installed but disabled.
|
||||
|
||||
Enable it in Settings -> Extensions, then reload WebUI if you just changed that and start the ${DESKTOP_COMPANION_LOCAL_APP_LABEL}.
|
||||
|
||||
Setup guide: ${DESKTOP_COMPANION_SETUP_GUIDE_URL}`;
|
||||
}
|
||||
|
||||
function _desktopCompanionReloadMessage(){
|
||||
return `${DESKTOP_COMPANION_NAME} is enabled, but the adapter status is not loaded yet.
|
||||
|
||||
Reload WebUI if you just enabled it, then start the ${DESKTOP_COMPANION_LOCAL_APP_LABEL}.`;
|
||||
}
|
||||
|
||||
function _desktopCompanionConnectMessage(){
|
||||
return `${DESKTOP_COMPANION_NAME} is enabled, but the local app is not connected yet.
|
||||
|
||||
Start or connect the ${DESKTOP_COMPANION_LOCAL_APP_LABEL}, then retry /pet.
|
||||
|
||||
Setup guide: ${DESKTOP_COMPANION_SETUP_GUIDE_URL}`;
|
||||
}
|
||||
|
||||
function _desktopCompanionStatusUnavailableMessage(){
|
||||
return `${DESKTOP_COMPANION_NAME} status is unavailable right now.
|
||||
|
||||
Reload WebUI or check your connection, then retry /pet.`;
|
||||
}
|
||||
|
||||
function _desktopCompanionUnavailableMessage(){
|
||||
return `${DESKTOP_COMPANION_NAME} is installed and connected, but /pet is not available yet in this Desktop Companion version.
|
||||
|
||||
Update the ${DESKTOP_COMPANION_LOCAL_APP_LABEL}, then follow the setup guide: ${DESKTOP_COMPANION_SETUP_GUIDE_URL}.`;
|
||||
}
|
||||
|
||||
function _desktopCompanionHookErrorMessage(){
|
||||
return `${DESKTOP_COMPANION_NAME} is installed and connected, but it hit an error while handling /pet.
|
||||
|
||||
Check the browser console and the ${DESKTOP_COMPANION_LOCAL_APP_LABEL}, then retry /pet.`;
|
||||
}
|
||||
|
||||
async function handlePetSlashCommand(rawCommandText,meta){
|
||||
const command=String(rawCommandText||'');
|
||||
const commandName=String((meta&&meta.name)||'pet').trim()||'pet';
|
||||
const args=_petSlashCommandArgs(command);
|
||||
let status;
|
||||
try{
|
||||
status=await api('/api/extensions/status');
|
||||
}catch(_e){
|
||||
return {handled:false,message:_desktopCompanionStatusUnavailableMessage()};
|
||||
}
|
||||
const companion=_getDesktopCompanionExtensionStatus(status);
|
||||
if(!companion){
|
||||
return {handled:false,message:_desktopCompanionMissingMessage()};
|
||||
}
|
||||
if(companion.effective_enabled!==true){
|
||||
return {handled:false,message:_desktopCompanionDisabledMessage()};
|
||||
}
|
||||
const companionStatus=_getDesktopCompanionStatusGlobal();
|
||||
if(!companionStatus){
|
||||
return {handled:false,message:_desktopCompanionReloadMessage()};
|
||||
}
|
||||
if(companionStatus.connected!==true){
|
||||
return {handled:false,message:_desktopCompanionConnectMessage()};
|
||||
}
|
||||
const hook=typeof window!=='undefined'&&window.__hermesHandlePetSlashCommand;
|
||||
if(typeof hook==='function'){
|
||||
try{
|
||||
const result=await hook({
|
||||
command,
|
||||
args,
|
||||
source:'webui-slash-command',
|
||||
metadata:{name:commandName},
|
||||
});
|
||||
if(result){
|
||||
return {
|
||||
handled:true,
|
||||
message:result&&typeof result==='object'&&'message' in result
|
||||
? String(result.message??'')
|
||||
: '',
|
||||
};
|
||||
}
|
||||
}catch(_e){
|
||||
if(typeof console!=='undefined'&&console.error){
|
||||
console.error('[hermes] Desktop Companion /pet hook error:',_e);
|
||||
}
|
||||
return {handled:false,message:_desktopCompanionHookErrorMessage()};
|
||||
}
|
||||
}
|
||||
return {handled:false,message:_desktopCompanionUnavailableMessage()};
|
||||
}
|
||||
|
||||
function executeCommand(text){
|
||||
const parsed=parseCommand(text);
|
||||
if(!parsed)return null;
|
||||
@@ -76,11 +197,22 @@ function getMatchingCommands(prefix){
|
||||
});
|
||||
seen.add(name);
|
||||
}
|
||||
if('pet'.startsWith(q)&&!seen.has('pet')){
|
||||
const petMeta=Array.isArray(_agentCommandCache)
|
||||
? _agentCommandCache.find(cmd=>String(cmd&&cmd.name||'').toLowerCase()==='pet')
|
||||
: null;
|
||||
matches.push({
|
||||
name:'pet',
|
||||
desc:String((petMeta&&petMeta.description)||'Desktop Companion command').trim()||'Desktop Companion command',
|
||||
source:'agent',
|
||||
});
|
||||
seen.add('pet');
|
||||
}
|
||||
// Include agent/plugin commands from /api/commands metadata
|
||||
for(const cmd of (_agentCommandCache||[])){
|
||||
const name=String(cmd&&cmd.name||'').toLowerCase();
|
||||
if(!name.startsWith(q)||seen.has(name))continue;
|
||||
if(cmd.cli_only)continue;
|
||||
if(cmd.cli_only&&name!=='pet')continue;
|
||||
matches.push({
|
||||
name,
|
||||
desc:String(cmd&&cmd.description||'').trim()||'Agent command',
|
||||
|
||||
@@ -1351,6 +1351,23 @@ async function send(){
|
||||
}
|
||||
}
|
||||
if(_parsedCmd&&!_cmd){
|
||||
if(_parsedCmd.name==='pet'){
|
||||
if(!S.session){await newSession();await renderSessionList();}
|
||||
S.messages.push({role:'user',content:text,_ts:Date.now()/1000});
|
||||
let _petOutput=null;
|
||||
try{
|
||||
_petOutput=typeof handlePetSlashCommand==='function'
|
||||
? await handlePetSlashCommand(text,{name:'pet'})
|
||||
: {handled:false,message:'Desktop Companion is unavailable in WebUI.'};
|
||||
}catch(e){
|
||||
_petOutput={handled:false,message:`Desktop Companion command error: ${e&&e.message||e}`};
|
||||
}
|
||||
if(_petOutput&&_petOutput.message){
|
||||
S.messages.push({role:'assistant',content:String(_petOutput.message),_ts:Date.now()/1000});
|
||||
}
|
||||
renderMessages();
|
||||
$('msg').value='';autoResize();hideCmdDropdown();return;
|
||||
}
|
||||
const _agentCmd=typeof getAgentCommandMetadata==='function'
|
||||
? await getAgentCommandMetadata(_parsedCmd.name)
|
||||
: null;
|
||||
|
||||
@@ -100,6 +100,14 @@ def _run_commands_js(script_body: str) -> dict:
|
||||
api: async (path) => {{
|
||||
if (path === '/api/commands') return {{
|
||||
commands: [
|
||||
{{
|
||||
name: 'pet',
|
||||
description: 'Desktop Companion command',
|
||||
category: 'Tools',
|
||||
aliases: [],
|
||||
cli_only: true,
|
||||
gateway_only: false
|
||||
}},
|
||||
{{
|
||||
name: 'browser',
|
||||
description: 'Attach browser tools',
|
||||
@@ -300,6 +308,8 @@ def test_cli_only_slugs_reserve_skill_autocomplete_namespace():
|
||||
await loadAgentCommandMetadata(true);
|
||||
await loadBundleCommands(true);
|
||||
await loadSkillCommands(true);
|
||||
const pet = await getSlashAutocompleteMatches('/pet');
|
||||
const browser = await getSlashAutocompleteMatches('/bro');
|
||||
const handoff = await getSlashAutocompleteMatches('/handoff');
|
||||
const delegate = await getSlashAutocompleteMatches('/delegate');
|
||||
const incident = await getSlashAutocompleteMatches('/incident');
|
||||
@@ -308,6 +318,10 @@ def test_cli_only_slugs_reserve_skill_autocomplete_namespace():
|
||||
const skills = await getSlashAutocompleteMatches('/skills');
|
||||
const use = await getSlashAutocompleteMatches('/use');
|
||||
return {
|
||||
pet_names: pet.map(item => item.name),
|
||||
pet_sources: pet.map(item => item.source),
|
||||
pet_descs: pet.map(item => item.desc),
|
||||
browser_names: browser.map(item => item.name),
|
||||
handoff_names: handoff.map(item => item.name),
|
||||
delegate_names: delegate.map(item => item.name),
|
||||
incident_names: incident.map(item => item.name),
|
||||
@@ -322,6 +336,10 @@ def test_cli_only_slugs_reserve_skill_autocomplete_namespace():
|
||||
"""
|
||||
)
|
||||
|
||||
assert result["pet_names"] == ["pet"]
|
||||
assert result["pet_sources"] == ["agent"]
|
||||
assert result["pet_descs"] == ["Desktop Companion command"]
|
||||
assert result["browser_names"] == []
|
||||
assert result["handoff_names"] == []
|
||||
assert result["delegate_names"] == []
|
||||
assert result["incident_names"] == ["incident-review"]
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
"""Regression tests for the WebUI /pet handoff."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import textwrap
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
COMMANDS_JS = (REPO_ROOT / "static" / "commands.js").read_text(encoding="utf-8")
|
||||
MESSAGES_JS = (REPO_ROOT / "static" / "messages.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _run_pet_js(
|
||||
*,
|
||||
status,
|
||||
adapter_status=None,
|
||||
status_throws=False,
|
||||
hook_result=None,
|
||||
hook_throws=False,
|
||||
command="/pet feed tuna",
|
||||
):
|
||||
hook_setup = ""
|
||||
if hook_throws:
|
||||
hook_setup += textwrap.dedent(
|
||||
"""
|
||||
ctx.window.__hermesHandlePetSlashCommand = async payload => {
|
||||
hookCalls.push(payload);
|
||||
throw new Error('hook failed');
|
||||
};
|
||||
"""
|
||||
)
|
||||
elif hook_result is not None:
|
||||
hook_setup += textwrap.dedent(
|
||||
f"""
|
||||
ctx.window.__hermesHandlePetSlashCommand = async payload => {{
|
||||
hookCalls.push(payload);
|
||||
return {json.dumps(hook_result)};
|
||||
}};
|
||||
"""
|
||||
)
|
||||
|
||||
script = textwrap.dedent(
|
||||
f"""
|
||||
const vm = require('vm');
|
||||
const hookCalls = [];
|
||||
const consoleErrors = [];
|
||||
const captureConsole = {{
|
||||
...console,
|
||||
error: (...args) => {{
|
||||
consoleErrors.push(args.map(arg => String(arg && arg.message ? arg.message : arg)).join(' '));
|
||||
}},
|
||||
}};
|
||||
const window = {{
|
||||
__HERMES_WEBUI_DESKTOP_COMPANION_STATUS__: {json.dumps(adapter_status)},
|
||||
}};
|
||||
window.window = window;
|
||||
const ctx = {{
|
||||
console: captureConsole,
|
||||
window,
|
||||
localStorage: {{ getItem(){{return null;}}, setItem(){{}}, removeItem(){{}} }},
|
||||
t: key => key,
|
||||
api: async path => {{
|
||||
if (path === '/api/extensions/status') {{
|
||||
if ({json.dumps(status_throws)}) throw new Error('extensions api failed');
|
||||
return {json.dumps(status)};
|
||||
}}
|
||||
throw new Error('unexpected api path: ' + path);
|
||||
}},
|
||||
}};
|
||||
{hook_setup}
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext({json.dumps(COMMANDS_JS)}, ctx);
|
||||
(async () => {{
|
||||
const result = await vm.runInContext(`(async () => {{ return await handlePetSlashCommand({json.dumps(command)}, {{name:'pet'}}); }})()`, ctx);
|
||||
process.stdout.write(JSON.stringify({{result, hookCalls, consoleErrors}}));
|
||||
}})().catch(err => {{
|
||||
console.error(err && err.stack || err);
|
||||
process.exit(1);
|
||||
}});
|
||||
"""
|
||||
)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".js", encoding="utf-8", delete=False) as handle:
|
||||
handle.write(script)
|
||||
script_path = Path(handle.name)
|
||||
try:
|
||||
proc = subprocess.run(["node", str(script_path)], check=True, capture_output=True, text=True)
|
||||
finally:
|
||||
script_path.unlink(missing_ok=True)
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _run_send_js(*, command, status, adapter_status=None, hook_result=None, hook_throws=False):
|
||||
hook_setup = ""
|
||||
if hook_throws:
|
||||
hook_setup += textwrap.dedent(
|
||||
"""
|
||||
ctx.window.__hermesHandlePetSlashCommand = async payload => {
|
||||
throw new Error('hook failed');
|
||||
};
|
||||
"""
|
||||
)
|
||||
elif hook_result is not None:
|
||||
hook_setup += textwrap.dedent(
|
||||
f"""
|
||||
ctx.window.__hermesHandlePetSlashCommand = async payload => {{
|
||||
return {json.dumps(hook_result)};
|
||||
}};
|
||||
"""
|
||||
)
|
||||
script = textwrap.dedent(
|
||||
f"""
|
||||
const vm = require('vm');
|
||||
const msgInput = {{
|
||||
value: {json.dumps(command)},
|
||||
style: {{}},
|
||||
scrollHeight: 0,
|
||||
addEventListener(){{}},
|
||||
removeEventListener(){{}},
|
||||
focus(){{}},
|
||||
blur(){{}},
|
||||
setSelectionRange(){{}},
|
||||
}};
|
||||
const commandExecCalls = [];
|
||||
const genericEl = {{
|
||||
addEventListener(){{}},
|
||||
removeEventListener(){{}},
|
||||
classList: {{ add(){{}}, remove(){{}} }},
|
||||
style: {{}},
|
||||
dataset: {{}},
|
||||
value: '',
|
||||
textContent: '',
|
||||
innerHTML: '',
|
||||
}};
|
||||
const ctx = {{
|
||||
console,
|
||||
window: {{
|
||||
__HERMES_WEBUI_DESKTOP_COMPANION_STATUS__: {json.dumps(adapter_status)},
|
||||
addEventListener(){{}},
|
||||
requestAnimationFrame(cb){{ return 1; }},
|
||||
}},
|
||||
document: {{
|
||||
addEventListener(){{}},
|
||||
getElementById(id){{ return id === 'msg' ? msgInput : genericEl; }},
|
||||
querySelector(){{ return null; }},
|
||||
}},
|
||||
localStorage: {{ getItem(){{return null;}}, setItem(){{}}, removeItem(){{}} }},
|
||||
t: key => key,
|
||||
S: {{
|
||||
busy: false,
|
||||
session: null,
|
||||
pendingFiles: [],
|
||||
messages: [],
|
||||
activeProfile: 'default',
|
||||
activeStreamId: null,
|
||||
}},
|
||||
INFLIGHT: {{}},
|
||||
_pendingSelections: [],
|
||||
_sendInProgress: false,
|
||||
_sendInProgressSid: null,
|
||||
_composerTextWithPendingSelections(){{ return msgInput.value; }},
|
||||
_flushSelectionBlocksToComposer(){{}},
|
||||
_dismissHandoffHint(){{}},
|
||||
_clearStaleBusyStateBeforeSend(){{ return false; }},
|
||||
_clearComposerAfterQueuedSelectionSend(){{}},
|
||||
_chatPayloadModelState(){{ return {{ model: '', model_provider: '' }}; }},
|
||||
queueSessionMessage(){{}},
|
||||
updateQueueBadge(){{}},
|
||||
renderTray(){{}},
|
||||
showToast(){{}},
|
||||
renderMessages(){{}},
|
||||
renderSessionList(){{}},
|
||||
autoResize(){{}},
|
||||
hideCmdDropdown(){{}},
|
||||
syncTopbar(){{}},
|
||||
setBusy(){{}},
|
||||
setComposerStatus(){{}},
|
||||
setStatus(){{}},
|
||||
updateSendBtn(){{}},
|
||||
clearOptimisticSessionStreaming(){{}},
|
||||
newSession: async () => {{
|
||||
ctx.S.session = {{ session_id: 'sid-1', title: 'New Chat' }};
|
||||
}},
|
||||
$: id => {{
|
||||
if (id === 'msg') return msgInput;
|
||||
return genericEl;
|
||||
}},
|
||||
api: async (path, options) => {{
|
||||
if (path === '/api/commands') return {{
|
||||
commands: [
|
||||
{{
|
||||
name: 'pet',
|
||||
description: 'Desktop Companion command',
|
||||
category: 'Tools',
|
||||
aliases: [],
|
||||
cli_only: true,
|
||||
gateway_only: false
|
||||
}},
|
||||
{{
|
||||
name: 'browser',
|
||||
description: 'Attach browser tools',
|
||||
category: 'Tools',
|
||||
aliases: ['browse'],
|
||||
cli_only: true,
|
||||
gateway_only: false
|
||||
}}
|
||||
]
|
||||
}};
|
||||
if (path === '/api/extensions/status') return {json.dumps(status)};
|
||||
if (path === '/api/commands/exec') {{
|
||||
commandExecCalls.push(JSON.parse(options.body).command);
|
||||
return {{ output: 'unexpected exec' }};
|
||||
}}
|
||||
throw new Error('unexpected api path: ' + path);
|
||||
}},
|
||||
}};
|
||||
ctx.window.window = ctx.window;
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext({json.dumps(COMMANDS_JS)}, ctx);
|
||||
{hook_setup}
|
||||
vm.runInContext({json.dumps(MESSAGES_JS)}, ctx);
|
||||
(async () => {{
|
||||
await vm.runInContext('send()', ctx);
|
||||
process.stdout.write(JSON.stringify({{
|
||||
messages: ctx.S.messages,
|
||||
commandExecCalls,
|
||||
remainingInput: msgInput.value,
|
||||
}}));
|
||||
}})().catch(err => {{
|
||||
console.error(err && err.stack || err);
|
||||
process.exit(1);
|
||||
}});
|
||||
"""
|
||||
)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".js", encoding="utf-8", delete=False) as handle:
|
||||
handle.write(script)
|
||||
script_path = Path(handle.name)
|
||||
try:
|
||||
proc = subprocess.run(["node", str(script_path)], check=True, capture_output=True, text=True)
|
||||
finally:
|
||||
script_path.unlink(missing_ok=True)
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_pet_help_routes_to_install_guidance_when_companion_is_missing():
|
||||
result = _run_pet_js(
|
||||
status={"enabled": False, "extensions": [], "gallery_installed": {}},
|
||||
adapter_status=None,
|
||||
)
|
||||
|
||||
assert result["result"]["handled"] is False
|
||||
message = result["result"]["message"]
|
||||
assert "Desktop Companion is not installed yet." in message
|
||||
assert "Settings -> Extensions -> Gallery -> Desktop Companion" in message
|
||||
assert "https://github.com/franksong2702/hermes-webui-desktop-companion#after-gallery-install" in message
|
||||
assert "Desktop Companion app" in message
|
||||
|
||||
|
||||
def test_pet_help_routes_to_enable_guidance_when_companion_is_disabled():
|
||||
result = _run_pet_js(
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": False,
|
||||
"user_disabled": True,
|
||||
"status": "user_disabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status=None,
|
||||
)
|
||||
|
||||
assert result["result"]["handled"] is False
|
||||
message = result["result"]["message"]
|
||||
assert "Desktop Companion is installed but disabled." in message
|
||||
assert "Enable it in Settings -> Extensions" in message
|
||||
assert "Desktop Companion app" in message
|
||||
assert "https://github.com/franksong2702/hermes-webui-desktop-companion#after-gallery-install" in message
|
||||
|
||||
|
||||
def test_pet_help_routes_to_reload_and_start_guidance_when_adapter_status_is_missing():
|
||||
result = _run_pet_js(
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": True,
|
||||
"user_disabled": False,
|
||||
"status": "enabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status=None,
|
||||
)
|
||||
|
||||
assert result["result"]["handled"] is False
|
||||
message = result["result"]["message"]
|
||||
assert "adapter status is not loaded yet" in message
|
||||
assert "Reload WebUI if you just enabled it" in message
|
||||
assert "Desktop Companion app" in message
|
||||
|
||||
|
||||
def test_pet_help_routes_to_connect_guidance_when_adapter_is_not_connected():
|
||||
result = _run_pet_js(
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": True,
|
||||
"user_disabled": False,
|
||||
"status": "enabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status={"connected": False},
|
||||
)
|
||||
|
||||
assert result["result"]["handled"] is False
|
||||
message = result["result"]["message"]
|
||||
assert "local app is not connected yet" in message
|
||||
assert "Start or connect the Desktop Companion app" in message
|
||||
assert "Setup guide:" in message
|
||||
|
||||
|
||||
def test_pet_help_hands_off_to_desktop_companion_hook_when_connected():
|
||||
result = _run_pet_js(
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": True,
|
||||
"user_disabled": False,
|
||||
"status": "enabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status={"connected": True},
|
||||
hook_result={"handled": True, "message": "mascot handled"},
|
||||
command="/pet feed tuna ",
|
||||
)
|
||||
|
||||
assert result["result"] == {"handled": True, "message": "mascot handled"}
|
||||
assert result["hookCalls"] == [
|
||||
{
|
||||
"command": "/pet feed tuna ",
|
||||
"args": "feed tuna",
|
||||
"source": "webui-slash-command",
|
||||
"metadata": {"name": "pet"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_pet_help_preserves_explicit_falsy_hook_message_values():
|
||||
result = _run_pet_js(
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": True,
|
||||
"user_disabled": False,
|
||||
"status": "enabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status={"connected": True},
|
||||
hook_result={"handled": True, "message": 0},
|
||||
command="/pet count snacks",
|
||||
)
|
||||
|
||||
assert result["result"] == {"handled": True, "message": "0"}
|
||||
assert result["hookCalls"] == [
|
||||
{
|
||||
"command": "/pet count snacks",
|
||||
"args": "count snacks",
|
||||
"source": "webui-slash-command",
|
||||
"metadata": {"name": "pet"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_pet_help_treats_truthy_hook_result_as_handled():
|
||||
result = _run_pet_js(
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": True,
|
||||
"user_disabled": False,
|
||||
"status": "enabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status={"connected": True},
|
||||
hook_result="mascot handled",
|
||||
command="/pet wave",
|
||||
)
|
||||
|
||||
assert result["result"] == {"handled": True, "message": ""}
|
||||
assert result["hookCalls"] == [
|
||||
{
|
||||
"command": "/pet wave",
|
||||
"args": "wave",
|
||||
"source": "webui-slash-command",
|
||||
"metadata": {"name": "pet"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_pet_help_routes_to_status_error_when_extension_status_api_fails():
|
||||
result = _run_pet_js(
|
||||
status={"enabled": False, "extensions": []},
|
||||
status_throws=True,
|
||||
adapter_status=None,
|
||||
)
|
||||
|
||||
assert result["result"]["handled"] is False
|
||||
assert result["result"]["message"] == (
|
||||
"Desktop Companion status is unavailable right now.\n\n"
|
||||
"Reload WebUI or check your connection, then retry /pet."
|
||||
)
|
||||
|
||||
|
||||
def test_pet_help_falls_back_to_unavailable_guidance_when_hook_is_missing():
|
||||
result = _run_pet_js(
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": True,
|
||||
"user_disabled": False,
|
||||
"status": "enabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status={"connected": True},
|
||||
)
|
||||
|
||||
message = result["result"]["message"]
|
||||
assert result["result"]["handled"] is False
|
||||
assert "Desktop Companion is installed and connected" in message
|
||||
assert "/pet is not available yet in this Desktop Companion version" in message
|
||||
assert "Update the Desktop Companion app" in message
|
||||
assert "https://github.com/franksong2702/hermes-webui-desktop-companion#after-gallery-install" in message
|
||||
|
||||
|
||||
def test_pet_help_routes_to_hook_error_guidance_when_hook_throws():
|
||||
result = _run_pet_js(
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": True,
|
||||
"user_disabled": False,
|
||||
"status": "enabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status={"connected": True},
|
||||
hook_throws=True,
|
||||
)
|
||||
|
||||
assert result["result"]["handled"] is False
|
||||
assert result["result"]["message"] == (
|
||||
"Desktop Companion is installed and connected, but it hit an error while handling /pet.\n\n"
|
||||
"Check the browser console and the Desktop Companion app, then retry /pet."
|
||||
)
|
||||
assert any("Desktop Companion /pet hook error:" in entry for entry in result["consoleErrors"])
|
||||
assert any("hook failed" in entry for entry in result["consoleErrors"])
|
||||
|
||||
|
||||
def test_pet_slash_intercept_bypasses_generic_agent_execution():
|
||||
pet = _run_send_js(
|
||||
command="/pet feed tuna",
|
||||
status={"enabled": False, "extensions": []},
|
||||
adapter_status=None,
|
||||
)
|
||||
browser = _run_send_js(
|
||||
command="/browser open",
|
||||
status={"enabled": False, "extensions": []},
|
||||
adapter_status=None,
|
||||
)
|
||||
|
||||
assert [item["role"] for item in pet["messages"]] == ["user", "assistant"]
|
||||
assert "Desktop Companion is not installed yet." in pet["messages"][1]["content"]
|
||||
assert "Hermes CLI-only command" not in pet["messages"][1]["content"]
|
||||
assert pet["commandExecCalls"] == []
|
||||
assert pet["remainingInput"] == ""
|
||||
|
||||
assert [item["role"] for item in browser["messages"]] == ["user", "assistant"]
|
||||
assert "`/browser` is a Hermes CLI-only command" in browser["messages"][1]["content"]
|
||||
assert browser["commandExecCalls"] == []
|
||||
|
||||
|
||||
def test_pet_send_uses_extension_message_when_hook_returns_one():
|
||||
result = _run_send_js(
|
||||
command="/pet feed tuna",
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": True,
|
||||
"user_disabled": False,
|
||||
"status": "enabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status={"connected": True},
|
||||
hook_result={"handled": True, "message": "mascot handled"},
|
||||
)
|
||||
|
||||
assert [item["role"] for item in result["messages"]] == ["user", "assistant"]
|
||||
assert result["messages"][1]["content"] == "mascot handled"
|
||||
assert result["commandExecCalls"] == []
|
||||
assert result["remainingInput"] == ""
|
||||
|
||||
|
||||
def test_pet_send_leaves_chat_reply_to_extension_when_hook_handles_silently():
|
||||
result = _run_send_js(
|
||||
command="/pet wave",
|
||||
status={
|
||||
"enabled": True,
|
||||
"extensions": [
|
||||
{
|
||||
"id": "desktop-companion",
|
||||
"name": "Desktop Companion",
|
||||
"effective_enabled": True,
|
||||
"user_disabled": False,
|
||||
"status": "enabled",
|
||||
}
|
||||
],
|
||||
},
|
||||
adapter_status={"connected": True},
|
||||
hook_result=True,
|
||||
)
|
||||
|
||||
assert [item["role"] for item in result["messages"]] == ["user"]
|
||||
assert result["commandExecCalls"] == []
|
||||
assert result["remainingInput"] == ""
|
||||
Reference in new issue
Block a user