fix: trace-hook.ps1 - Copilot hooks under PowerShell on Windows no longer open the choose-an-app dialog (#164)
Files changed: - CHANGES.md - EVALS.md - VERSION - tools/README.md - tools/chemenu/tests/test_preflight.py - tools/chemenu/tests/test_preflight_pwsh.py - tools/trace-hook - tools/trace-hook.ps1
This commit is contained in:
1 parent
d6e973c3ce
commit
9d06050338
8 files changed
+119
-6
No files matched your search
@@ -98,7 +98,7 @@ class Machine:
|
||||
self.script = self.tools / "preflight.sh"
|
||||
self.tools.mkdir(parents=True)
|
||||
for name in ("preflight.sh", "preflight.ps1", "prerequisites.txt", "wikitool", "wikitool.ps1",
|
||||
"run_wikitool.py", "trace-hook"):
|
||||
"run_wikitool.py", "trace-hook", "trace-hook.ps1"):
|
||||
shutil.copy2(TOOLS / name, self.tools / name)
|
||||
(self.tools / "requirements.txt").write_text("PyYAML\n", encoding="utf-8")
|
||||
self.sysbin = base / "sysbin"
|
||||
@@ -743,6 +743,18 @@ def test_no_hook_relies_on_a_shebang_or_a_bare_python():
|
||||
assert command.startswith(("./tools/trace-hook ", ".\\tools\\.venv\\Scripts\\python.exe ")), command
|
||||
|
||||
|
||||
def test_every_extensionless_hook_target_has_a_powershell_twin():
|
||||
"""PowerShell on Windows resolves `./tools/trace-hook` to `trace-hook.ps1` first. Without
|
||||
one it hands the sh script to a file association, and Windows asks which app should open
|
||||
it, on every hook event (Gitea #164). Copilot CLI runs `.claude/settings.json`'s `command`
|
||||
there under PowerShell, so not even a file's `bash` field is safe from it."""
|
||||
targets = {command.split()[0] for command in _hook_commands()}
|
||||
extensionless = {t for t in targets if not Path(t.replace("\\", "/")).suffix}
|
||||
assert extensionless == {"./tools/trace-hook"}
|
||||
for target in extensionless:
|
||||
assert (config._PACKAGE_ROOT / f"{target}.ps1").is_file(), target
|
||||
|
||||
|
||||
# --- toolpaths --------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ pytestmark = pytest.mark.skipif(PWSH is None, reason="PowerShell 7 (pwsh) is not
|
||||
WINDOWS = {"CHEMENU_PREFLIGHT_PLATFORM": "windows", "CHEMENU_PREFLIGHT_LONGPATHS": "1"}
|
||||
|
||||
|
||||
def _pwsh(machine: Machine, script: str, *args: str) -> subprocess.CompletedProcess:
|
||||
def _pwsh(machine: Machine, script: str, *args: str, stdin: str | None = None) -> subprocess.CompletedProcess:
|
||||
env = {
|
||||
"PATH": os.pathsep.join(str(d) for d in machine.path_dirs),
|
||||
"HOME": str(machine.base),
|
||||
@@ -48,7 +48,7 @@ def _pwsh(machine: Machine, script: str, *args: str) -> subprocess.CompletedProc
|
||||
}
|
||||
return subprocess.run(
|
||||
[PWSH, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(machine.tools / script), *args],
|
||||
capture_output=True, text=True, env=env, timeout=120,
|
||||
input=stdin, capture_output=True, text=True, env=env, timeout=120,
|
||||
)
|
||||
|
||||
|
||||
@@ -466,6 +466,43 @@ def test_both_launchers_exist_for_pwsh_to_resolve():
|
||||
assert (TOOLS / "wikitool.ps1").is_file() and (TOOLS / "wikitool").is_file()
|
||||
|
||||
|
||||
# --- trace-hook.ps1 -----------------------------------------------------------------
|
||||
|
||||
# Prints its arguments, then whatever arrived on stdin - with shell built-ins only, since the
|
||||
# stub machine's PATH carries no `cat` of its own.
|
||||
ECHO_STDIN_PYTHON = ECHO_PYTHON + "while IFS= read -r line; do printf '%s\\n' \"$line\"; done\n"
|
||||
PAYLOAD = '{"sessionId": "s-1", "toolName": "bash"}'
|
||||
|
||||
|
||||
def _hook(machine: Machine, *args: str) -> subprocess.CompletedProcess:
|
||||
return _pwsh(machine, "trace-hook.ps1", *args, stdin=PAYLOAD + "\n")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("layout", [("Scripts", "python.exe"), ("bin", "python")])
|
||||
def test_trace_hook_ps1_hands_arguments_and_payload_to_the_venv_python(machine, layout):
|
||||
machine.stub(layout[1], ECHO_STDIN_PYTHON, machine.tools / ".venv" / layout[0])
|
||||
result = _hook(machine, "--source", "copilot-cli", "--event", "tool.pre")
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert result.stdout.splitlines() == [
|
||||
str(machine.tools / "trace_ingest.py"), "--source", "copilot-cli", "--event", "tool.pre", PAYLOAD,
|
||||
]
|
||||
|
||||
|
||||
def test_trace_hook_ps1_is_silent_without_a_venv(machine):
|
||||
result = _hook(machine, "--source", "claude-code", "--event", "prompt.submitted")
|
||||
assert result.returncode == 0
|
||||
assert result.stdout == "" and result.stderr == ""
|
||||
|
||||
|
||||
def test_trace_hook_ps1_never_fails_the_call_it_observes(machine):
|
||||
"""Copilot denies the tool call on a non-zero `preToolUse` hook - an observer that
|
||||
passed a failing Python through would turn into a blocker."""
|
||||
machine.stub("python", "#!/bin/sh\necho 'Traceback: broken' >&2\nexit 2\n", machine.tools / ".venv" / "bin")
|
||||
result = _hook(machine, "--source", "copilot-cli", "--event", "tool.pre")
|
||||
assert result.returncode == 0
|
||||
assert result.stderr == ""
|
||||
|
||||
|
||||
def test_the_python_side_reads_what_the_hooks_say(monkeypatch):
|
||||
"""The doctor checks use the same hook values the script does; the two
|
||||
parsers must agree on what a policy list means."""
|
||||
|
||||
Reference in new issue
Block a user