115 lines
3.8 KiB
Python
115 lines
3.8 KiB
Python
|
|
"""Regression tests for #3433: chat composer path completion for ~/ tokens."""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import pathlib
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
import textwrap
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
|
||
|
|
REPO_ROOT = pathlib.Path(__file__).parent.parent
|
||
|
|
COMMANDS_JS = (REPO_ROOT / "static" / "commands.js").read_text(encoding="utf-8")
|
||
|
|
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||
|
|
STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||
|
|
NODE = shutil.which("node")
|
||
|
|
|
||
|
|
|
||
|
|
def _run_commands_js(script_body: str) -> dict:
|
||
|
|
script = textwrap.dedent(
|
||
|
|
f"""
|
||
|
|
const vm = require('vm');
|
||
|
|
const ctx = {{
|
||
|
|
console,
|
||
|
|
URL,
|
||
|
|
URLSearchParams,
|
||
|
|
localStorage: {{ getItem(){{return null;}}, setItem(){{}}, removeItem(){{}} }},
|
||
|
|
t: (key) => key,
|
||
|
|
api: async (path) => {{
|
||
|
|
const url = new URL('http://hermes.local' + path);
|
||
|
|
if (url.pathname !== '/api/workspaces/suggest') {{
|
||
|
|
throw new Error('unexpected api path: ' + path);
|
||
|
|
}}
|
||
|
|
const prefix = url.searchParams.get('prefix');
|
||
|
|
return {{
|
||
|
|
suggestions: prefix === '~/' ? ['~', '~/Documents', '~/Projects'] : []
|
||
|
|
}};
|
||
|
|
}}
|
||
|
|
}};
|
||
|
|
vm.createContext(ctx);
|
||
|
|
vm.runInContext({json.dumps(COMMANDS_JS)}, ctx);
|
||
|
|
(async () => {{
|
||
|
|
const result = await vm.runInContext(`(async () => {{ {script_body} }})()`, ctx);
|
||
|
|
process.stdout.write(JSON.stringify(result));
|
||
|
|
}})().catch(err => {{
|
||
|
|
console.error(err && err.stack || err);
|
||
|
|
process.exit(1);
|
||
|
|
}});
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
proc = subprocess.run(
|
||
|
|
[NODE, "-e", script],
|
||
|
|
check=True,
|
||
|
|
capture_output=True,
|
||
|
|
text=True,
|
||
|
|
)
|
||
|
|
return json.loads(proc.stdout)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||
|
|
def test_composer_path_token_matches_tilde_path_inside_message():
|
||
|
|
result = _run_commands_js(
|
||
|
|
"""
|
||
|
|
return {
|
||
|
|
token: _findComposerPathToken('please inspect ~/Doc', 20),
|
||
|
|
slash: _findComposerPathToken('/model gpt', 10),
|
||
|
|
bareTilde: _findComposerPathToken('please inspect ~', 16)
|
||
|
|
};
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
|
||
|
|
assert result["token"] == {"start": 15, "end": 20, "prefix": "~/Doc"}
|
||
|
|
assert result["slash"] is None
|
||
|
|
assert result["bareTilde"] is None
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||
|
|
def test_composer_path_autocomplete_uses_workspace_suggest_endpoint():
|
||
|
|
result = _run_commands_js(
|
||
|
|
"""
|
||
|
|
const matches = await getComposerPathAutocompleteMatches('please inspect ~/', 17);
|
||
|
|
return {
|
||
|
|
count: matches.length,
|
||
|
|
first: matches[0],
|
||
|
|
second: matches[1]
|
||
|
|
};
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
|
||
|
|
assert result["count"] == 2
|
||
|
|
assert result["first"]["source"] == "path"
|
||
|
|
assert result["first"]["value"] == "~/Documents"
|
||
|
|
assert result["first"]["tokenStart"] == 15
|
||
|
|
assert result["first"]["tokenEnd"] == 17
|
||
|
|
assert result["second"]["value"] == "~/Projects"
|
||
|
|
|
||
|
|
|
||
|
|
def test_composer_input_uses_path_autocomplete_after_slash_branch():
|
||
|
|
assert "const cursor=$('msg').selectionStart;" in BOOT_JS
|
||
|
|
assert "getComposerPathAutocompleteMatches(text,cursor).then(matches=>" in BOOT_JS
|
||
|
|
assert "ta.value!==text||ta.selectionStart!==cursor" in BOOT_JS
|
||
|
|
|
||
|
|
|
||
|
|
def test_dropdown_selection_replaces_only_path_token():
|
||
|
|
assert "const isPath=c.source==='path';" in COMMANDS_JS
|
||
|
|
assert "tokenStart:token.start" in COMMANDS_JS
|
||
|
|
assert "tokenEnd:token.end" in COMMANDS_JS
|
||
|
|
assert "current.slice(0,safeStart)+nextPath+current.slice(safeEnd)" in COMMANDS_JS
|
||
|
|
assert "ta.setSelectionRange(pos,pos);" in COMMANDS_JS
|
||
|
|
assert "ta.dispatchEvent(new Event('input',{bubbles:true}));" in COMMANDS_JS
|
||
|
|
|
||
|
|
|
||
|
|
def test_path_suggestions_have_distinct_dropdown_style():
|
||
|
|
assert ".cmd-item-path-value" in STYLE_CSS
|