feat: PowerShell 7 preflight and launcher - preflight.ps1, wikitool.ps1, doctor policy and Mark of the Web checks, pwsh CI job (#151, B)
CI / verify (push) Successful in 2m16s
CI / pwsh (push) Successful in 1m24s
Release / release (push) Successful in 37s

Files changed:
- .gitea/workflows/ci.yml
- CHANGES.md
- INSTALL.md
- README.md
- VERSION
- docs/why-gates-are-code.md
- instructions/bootstrap.md
- instructions/bug-report.md
- instructions/preflight.md
- instructions/setup-instance.md
- instructions/upgrade-instance.md
- tools/CONTRACT.md
- tools/README.md
- tools/bugreport.py
- tools/chemenu/cli.py
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/prerequisites.py
- tools/chemenu/tests/conftest.py
- tools/chemenu/tests/test_bugreport.py
- tools/chemenu/tests/test_doctor.py
- tools/chemenu/tests/test_preflight.py
- tools/chemenu/tests/test_preflight_pwsh.py
- tools/chemenu/toolpaths.py
- tools/preflight.ps1
- tools/wikitool
- tools/wikitool.ps1
This commit is contained in:
torben committed 2026-10-01 09:52:05 +02:00
1 parent 4035b1ba12
commit 210e0c8286
27 files changed
+1513 -32

No files matched your search

+2
View File
@@ -63,6 +63,8 @@ except ModuleNotFoundError as exc:
"This is not optional - schema validation depends on it. Run the preflight,\n"
"which (re)installs tools/.venv from tools/requirements.txt:\n"
" tools/preflight.sh\n"
"or, from PowerShell 7:\n"
f" {toolpaths.PREFLIGHT_PWSH}\n"
)
sys.exit(1)
+1 -1
View File
@@ -1516,7 +1516,7 @@ def run_upgrade(
summary += (
" Nothing was committed and nothing is verified yet."
" `instructions/upgrade-instance.md` carries the order for everything that follows"
" and resumes with the preflight (`tools/preflight.sh`), which `tools/wikitool` now"
" and resumes with the preflight (`tools/preflight.sh`, or `tools/preflight.ps1` under PowerShell 7), which `tools/wikitool` now"
" refuses to run without."
)
success(summary)
+57 -2
View File
@@ -49,7 +49,7 @@ def _git(args: list[str]) -> Optional[subprocess.CompletedProcess]:
return None
PREFLIGHT_FIX = "Run tools/preflight.sh"
PREFLIGHT_FIX = f"Run tools/preflight.sh (PowerShell 7: {toolpaths.PREFLIGHT_PWSH})"
def check_python() -> Check:
@@ -130,6 +130,53 @@ def check_install_dir() -> Check:
)
def check_execution_policy() -> Check:
"""Whether an ordinary PowerShell 7 may run `tools/wikitool.ps1` - the policy
the preflight checks before it stops, so a harness that starts `pwsh`
without a bypass is not turned away by a setting nobody looked at."""
if prerequisites.platform() != "windows":
return Check("execution-policy", "OK", "only PowerShell on Windows has one - not applicable here")
policy = prerequisites.execution_policy()
if policy is None:
return Check(
"execution-policy", "WARN", "the PowerShell execution policy could not be read",
f"{PREFLIGHT_FIX}; the preflight checks it",
)
if not policy.blocks:
return Check("execution-policy", "OK", policy.describe())
if policy.group_policy:
return Check(
"execution-policy", "FAIL",
f"a group policy sets the PowerShell execution policy to {policy.policy} - "
"tools/wikitool.ps1 does not start",
"A group policy cannot be overridden from this computer: ask whoever looks after it to allow "
"locally written scripts (RemoteSigned) for PowerShell 7, or run `tools/wikitool` from Git Bash",
)
return Check(
"execution-policy", "FAIL",
f"the PowerShell execution policy is {policy.describe()} - tools/wikitool.ps1 does not start",
"In a PowerShell 7 window: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned",
)
def check_script_marks() -> Check:
"""Mark of the Web on the stack's PowerShell scripts: a wiki downloaded with a
browser and unpacked in Explorer carries one, and PowerShell refuses to run
a marked script under its usual policy."""
if prerequisites.platform() != "windows":
return Check("script-marks", "OK", "no Mark of the Web outside Windows")
marked = prerequisites.marked_scripts()
if not marked:
return Check("script-marks", "OK", "no script under tools/ is marked as downloaded")
shown = ", ".join(marked[:5]) + (", ..." if len(marked) > 5 else "")
return Check(
"script-marks", "FAIL",
f"Windows marks scripts under tools/ as downloaded from the internet: {shown}",
"In a PowerShell 7 window, from the folder holding tools/: "
"Get-ChildItem -Recurse -File | Unblock-File",
)
def check_author() -> Check:
try:
author = config.default_author()
@@ -669,6 +716,8 @@ def run_doctor() -> list[Check]:
check_tool_paths(),
check_ripgrep(),
check_install_dir(),
check_execution_policy(),
check_script_marks(),
check_author(),
check_stack_version(),
check_kb_version(),
@@ -706,10 +755,16 @@ def run_doctor() -> list[Check]:
"Tool paths (`tool-paths`): `.wikitool-tools.json` written by a preflight that "
"finished, every tool `tools/prerequisites.txt` names for this platform recorded, and "
"every recorded path still there - a `FAIL` otherwise, fixed by running "
"`tools/preflight.sh` again.",
"`tools/preflight.sh` again (PowerShell 7: `tools/preflight.ps1`).",
"Install folder (`install-dir`): on Windows with long paths off, a `FAIL` when the "
"folder holding `tools/` is longer than `tools/prerequisites.txt` allows (95 "
"characters) - the limit the preflight enforces before it sets anything up.",
"PowerShell (`execution-policy`, `script-marks`; Windows only, `OK` elsewhere): a `FAIL` "
"when the effective execution policy is `Restricted` or `AllSigned` - the line then says whether "
"a group policy sets it, which only whoever administers the computer can change - and a "
"`FAIL` when a script under `tools/` carries a Mark of the Web from the internet zone, "
"which a browser download unpacked in Explorer leaves behind and `Invoke-WebRequest` plus "
"`tar` do not. `tools/preflight.ps1` checks the same two things before it stops.",
"Personalization: `USER.md`/`SOUL.md` present **and** filled - a file still carrying "
"the template's sentinel is a `FAIL`.",
"KB conventions: `kb/CONVENTIONS.md` present, unsentinelled, and naming all three "
+99 -3
View File
@@ -8,21 +8,34 @@ preflight enforced up front - a tool whose recorded path has gone, a checkout
moved into a folder too long for Windows.
`CHEMENU_PREFLIGHT_PLATFORM` and `CHEMENU_PREFLIGHT_LONGPATHS` stand in for the
real platform and registry, exactly as they do for the preflight scripts; the
test suite is their only user.
real platform and registry, exactly as they do for the preflight scripts;
`CHEMENU_PREFLIGHT_POLICY` and `CHEMENU_PREFLIGHT_MARKED` stand in for the
PowerShell execution policy and the Mark of the Web on the stack's scripts the
same way (what `tools/preflight.ps1` documents). The test suite is their only
user.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from chemenu import config, titles
from chemenu import config, titles, toolpaths
ENV_PLATFORM = "CHEMENU_PREFLIGHT_PLATFORM"
ENV_LONGPATHS = "CHEMENU_PREFLIGHT_LONGPATHS"
ENV_POLICY = "CHEMENU_PREFLIGHT_POLICY"
ENV_MARKED = "CHEMENU_PREFLIGHT_MARKED"
# Which scopes decide whether an ordinary pwsh starts tools/wikitool.ps1, strongest first.
# `Process` is left out: it holds a bypass the caller started with, not the machine's setting.
POLICY_SCOPES = ("MachinePolicy", "UserPolicy", "CurrentUser", "LocalMachine")
GROUP_POLICY_SCOPES = ("MachinePolicy", "UserPolicy")
BLOCKING_POLICIES = ("Restricted", "AllSigned")
@dataclass(frozen=True)
@@ -115,3 +128,86 @@ def install_dir_problem(folder: Optional[str] = None) -> Optional[str]:
f"the install folder is {length} characters long and Windows allows at most "
f"{limit} here (long paths are off): {folder}"
)
@dataclass(frozen=True)
class Policy:
"""The execution policy an ordinary pwsh applies. `scope` is None when no
scope sets one, which on Windows means the default, RemoteSigned."""
scope: Optional[str]
policy: str
@property
def blocks(self) -> bool:
return self.policy in BLOCKING_POLICIES
@property
def group_policy(self) -> bool:
return self.scope in GROUP_POLICY_SCOPES
def describe(self) -> str:
if self.scope is None:
return f"{self.policy} (the default)"
return f"{self.policy} (set for {self.scope})"
def _parse_policy_list(text: str) -> Optional[Policy]:
"""`Scope=Policy` pairs, separated by commas or lines, to the effective policy."""
pairs = {}
for item in re.split(r"[,\r\n]+", text):
scope, sep, policy = item.partition("=")
if sep:
pairs[scope.strip()] = policy.strip()
if not pairs:
return None
for scope in POLICY_SCOPES:
policy = pairs.get(scope, "Undefined")
if policy and policy != "Undefined":
return Policy(scope, policy)
return Policy(None, "RemoteSigned")
def execution_policy() -> Optional[Policy]:
"""The effective PowerShell execution policy, or None when it cannot be
read here (not Windows, or no pwsh to ask)."""
forced = os.environ.get(ENV_POLICY, "").strip()
if forced:
return _parse_policy_list(forced)
if platform() != "windows" or sys.platform != "win32":
return None
try: # pragma: no cover - Windows only
pwsh = toolpaths.resolve("pwsh")
done = subprocess.run(
[pwsh, "-NoProfile", "-NonInteractive", "-Command",
"Get-ExecutionPolicy -List | ForEach-Object { '{0}={1}' -f $_.Scope, $_.ExecutionPolicy }"],
capture_output=True, text=True, timeout=30,
)
except (toolpaths.ToolPathError, OSError, subprocess.SubprocessError): # pragma: no cover
return None
if done.returncode != 0: # pragma: no cover - Windows only
return None
return _parse_policy_list(done.stdout) # pragma: no cover - Windows only
def marked_scripts() -> list[str]:
"""PowerShell scripts below `tools/` that carry a Mark of the Web from the
internet zone (3 or 4), as names relative to `tools/`. A browser download
unpacked in Explorer has them; `Invoke-WebRequest` and `tar` do not."""
forced = os.environ.get(ENV_MARKED, "").strip()
if forced:
return [name.strip() for name in forced.split(",") if name.strip()]
if sys.platform != "win32":
return []
tools_dir = config._PACKAGE_ROOT / "tools" # pragma: no cover - Windows only
marked = [] # pragma: no cover - Windows only
for script in sorted(tools_dir.rglob("*.ps1")): # pragma: no cover - Windows only
if ".venv" in script.relative_to(tools_dir).parts:
continue
try:
stream = Path(str(script) + ":Zone.Identifier").read_text(encoding="utf-8", errors="replace")
except OSError:
continue
if re.search(r"ZoneId=[3-9]", stream):
marked.append(script.relative_to(tools_dir).as_posix())
return marked # pragma: no cover - Windows only
+2
View File
@@ -35,6 +35,8 @@ _WIKITOOL_ENV = (
"WIKITOOL_TASKS_CONFIG",
"CHEMENU_PREFLIGHT_PLATFORM",
"CHEMENU_PREFLIGHT_LONGPATHS",
"CHEMENU_PREFLIGHT_POLICY",
"CHEMENU_PREFLIGHT_MARKED",
) + tuple(var for var, _harness in HARNESS_ENV_VARS)
# Environment git reads for identity or for where its repo lives. A stray
+3 -2
View File
@@ -61,7 +61,7 @@ def checkout(tmp_path: Path, monkeypatch) -> Path:
json.dumps({"schema": 1, "api_token": TOKEN, "enabled": True})
)
(root / ".wikitool-tools.json").write_text(
json.dumps({"schema": 1, "tools": {"python": {"path": "/definitely/not/here/python"}}})
json.dumps({"schema": 1, "complete": True, "tools": {"python": "/definitely/not/here/python"}})
)
launcher = root / "tools" / "wikitool"
launcher.write_text(LAUNCHER.format(python=sys.executable, title=TITLE))
@@ -271,8 +271,9 @@ def test_the_tools_config_path_check_reports_what_is_missing(checkout, tmp_path)
bundle = collect(checkout, tmp_path, "--no-trace")
config = json.loads((bundle / "environment.json").read_text())["tools_config"]
assert config["status"] == "present"
assert config["content"]["complete"] is True
assert config["path_checks"] == [
{"at": "tools.python.path", "path": "/definitely/not/here/python", "exists": False}
{"at": "tools.python", "path": "/definitely/not/here/python", "exists": False}
]
+71
View File
@@ -103,6 +103,10 @@ def _detail(checks, name) -> str:
return next(c.detail for c in checks if c.name == name)
def _fix(checks, name) -> str:
return next(c.fix or "" for c in checks if c.name == name)
def test_healthy_instance_has_no_fail(instance):
checks = doctor.run_doctor()
assert not any(c.status == "FAIL" for c in checks)
@@ -640,3 +644,70 @@ def test_install_dir_is_not_limited_off_windows(instance, monkeypatch):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "linux")
monkeypatch.setattr(prerequisites, "install_dir", lambda: "/" + "x" * 300)
assert _status(doctor.run_doctor(), "install-dir") == "OK"
# --- PowerShell: execution policy and Mark of the Web (Gitea #151) -----------------
OPEN_POLICY = "MachinePolicy=Undefined,UserPolicy=Undefined,Process=Undefined,CurrentUser=Undefined,LocalMachine=RemoteSigned"
def _windows(monkeypatch, policy=None, marked=None):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "windows")
if policy is not None:
monkeypatch.setenv(prerequisites.ENV_POLICY, policy)
if marked is not None:
monkeypatch.setenv(prerequisites.ENV_MARKED, marked)
@pytest.mark.parametrize("check", ["execution-policy", "script-marks"])
def test_powershell_checks_do_not_apply_off_windows(instance, monkeypatch, check):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "linux")
monkeypatch.setenv(prerequisites.ENV_POLICY, "CurrentUser=AllSigned")
monkeypatch.setenv(prerequisites.ENV_MARKED, "wikitool.ps1")
assert _status(doctor.run_doctor(), check) == "OK"
@pytest.mark.parametrize("policy, expected", [
(OPEN_POLICY, "OK"),
("CurrentUser=Unrestricted,LocalMachine=Restricted", "OK"),
("Process=Restricted,LocalMachine=RemoteSigned", "OK"),
("CurrentUser=Undefined,LocalMachine=Undefined", "OK"),
("CurrentUser=Restricted,LocalMachine=RemoteSigned", "FAIL"),
("LocalMachine=AllSigned", "FAIL"),
])
def test_execution_policy_blocks_only_restricted_and_allsigned(instance, monkeypatch, policy, expected):
_windows(monkeypatch, policy=policy)
assert _status(doctor.run_doctor(), "execution-policy") == expected
def test_execution_policy_fix_names_the_one_line_command(instance, monkeypatch):
_windows(monkeypatch, policy="CurrentUser=Restricted")
fix = _fix(doctor.run_doctor(), "execution-policy")
assert "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned" in fix
@pytest.mark.parametrize("scope", ["MachinePolicy", "UserPolicy"])
def test_a_group_policy_gets_no_command_it_could_not_obey(instance, monkeypatch, scope):
_windows(monkeypatch, policy=f"{scope}=AllSigned,LocalMachine=RemoteSigned")
checks = doctor.run_doctor()
assert _status(checks, "execution-policy") == "FAIL"
assert "Set-ExecutionPolicy" not in _fix(checks, "execution-policy")
assert "Git Bash" in _fix(checks, "execution-policy")
def test_execution_policy_that_cannot_be_read_is_a_warning(instance, monkeypatch):
monkeypatch.setenv(prerequisites.ENV_PLATFORM, "windows")
assert _status(doctor.run_doctor(), "execution-policy") == "WARN"
def test_marked_scripts_fail_with_the_unblock_command(instance, monkeypatch):
_windows(monkeypatch, marked="wikitool.ps1,preflight.ps1")
checks = doctor.run_doctor()
assert _status(checks, "script-marks") == "FAIL"
assert "wikitool.ps1" in _detail(checks, "script-marks")
assert "Unblock-File" in _fix(checks, "script-marks")
def test_unmarked_scripts_are_ok(instance, monkeypatch):
_windows(monkeypatch)
assert _status(doctor.run_doctor(), "script-marks") == "OK"
+6 -2
View File
@@ -94,7 +94,8 @@ class Machine:
self.root = base / root_name
self.tools = self.root / "tools"
self.tools.mkdir(parents=True)
for name in ("preflight.sh", "prerequisites.txt", "wikitool", "run_wikitool.py", "trace-hook"):
for name in ("preflight.sh", "preflight.ps1", "prerequisites.txt", "wikitool", "wikitool.ps1",
"run_wikitool.py", "trace-hook"):
shutil.copy2(TOOLS / name, self.tools / name)
(self.tools / "requirements.txt").write_text("PyYAML\n", encoding="utf-8")
self.sysbin = base / "sysbin"
@@ -392,8 +393,11 @@ def test_launcher_runs_the_entry_script_with_either_venv_layout(machine, layout)
assert result.stdout.splitlines() == [str(machine.tools / "run_wikitool.py"), "doctor", "--json"]
def test_there_is_a_powershell_launcher_slot_but_no_cmd():
def test_there_is_a_powershell_launcher_and_no_cmd():
"""pwsh resolves `tools/wikitool` to `wikitool.ps1` before the sh launcher, and
cmd.exe is not a supported shell, so there is no `.cmd`."""
assert (TOOLS / "wikitool").is_file()
assert (TOOLS / "wikitool.ps1").is_file()
assert not (TOOLS / "wikitool.cmd").exists()
+304
View File
@@ -0,0 +1,304 @@
"""tools/preflight.ps1 and tools/wikitool.ps1 (Gitea #151, section B).
The PowerShell twin of `test_preflight.py`, run against the same stub machine: a
`PATH` the test builds, stub tools in it, and a fake Python that answers the probe
and fakes `-m venv`/`-m pip`. The scripts need PowerShell 7, so these tests skip
where there is none and run in CI's `pwsh` job, whose image carries it; everything
Windows-specific (policy, Mark of the Web, path limit, Store alias) is driven by the
same kind of environment hook the shell script has.
What these add to the shell tests is the one thing only the twin can break: the two
scripts must record the same file. `test_both_preflights_record_the_same_file`
compares them byte for byte.
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
from pathlib import Path
import pytest
from chemenu import prerequisites
from chemenu.tests.test_preflight import (
ECHO_PYTHON, SHELLS, TOOLS, Machine, _machine_with_root_of, assert_guidance,
)
PWSH = shutil.which("pwsh")
pytestmark = pytest.mark.skipif(PWSH is None, reason="PowerShell 7 (pwsh) is not installed")
WINDOWS = {"CHEMENU_PREFLIGHT_PLATFORM": "windows", "CHEMENU_PREFLIGHT_LONGPATHS": "1"}
def _pwsh(machine: Machine, script: str, *args: str) -> subprocess.CompletedProcess:
env = {
"PATH": os.pathsep.join(str(d) for d in machine.path_dirs),
"HOME": str(machine.base),
"PIP_LOG": str(machine.pip_log),
"DOTNET_CLI_TELEMETRY_OPTOUT": "1",
"POWERSHELL_TELEMETRY_OPTOUT": "1",
**machine.extra_env,
}
return subprocess.run(
[PWSH, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(machine.tools / script), *args],
capture_output=True, text=True, env=env, timeout=120,
)
def _preflight(machine: Machine, *args: str) -> subprocess.CompletedProcess:
return _pwsh(machine, "preflight.ps1", *args)
@pytest.fixture
def machine(tmp_path: Path) -> Machine:
return Machine(tmp_path.resolve()).standard()
# --- the happy path ---------------------------------------------------------------
def test_complete_path_records_absolute_paths_and_sets_up_the_venv(machine):
result = _preflight(machine)
assert result.returncode == 0, result.stdout + result.stderr
data = machine.recorded()
assert data["schema"] == 1 and data["complete"] is True
assert set(data["tools"]) == {"python", "git", "rg"}
assert data["tools"]["rg"] == str(machine.stubs / "rg")
assert (machine.tools / ".venv" / "bin" / "python").is_file()
assert machine.pip_log.read_text(encoding="utf-8").count("install") == 1
assert "Preflight passed" in result.stdout
def test_second_run_changes_nothing(machine):
assert _preflight(machine).returncode == 0
before = machine.tools_file.read_text(encoding="utf-8")
again = _preflight(machine)
assert again.returncode == 0, again.stdout
assert machine.tools_file.read_text(encoding="utf-8") == before
assert machine.pip_log.read_text(encoding="utf-8").count("install") == 1
@pytest.mark.skipif(not SHELLS, reason="no POSIX shell on this machine")
def test_both_preflights_record_the_same_file(machine):
assert _preflight(machine).returncode == 0
from_pwsh = machine.tools_file.read_bytes()
machine.tools_file.unlink()
assert machine.run(shell=SHELLS[0]).returncode == 0
assert machine.tools_file.read_bytes() == from_pwsh
def test_help_prints_the_usage_and_exits_0(machine):
result = _preflight(machine, "--help")
assert result.returncode == 0
assert "--set" in result.stdout
assert not machine.tools_file.exists()
# --- stop cases -------------------------------------------------------------------
def test_missing_rg_stops_with_42_and_a_missing_line(tmp_path):
machine = Machine(tmp_path.resolve()).standard(rg=False)
result = _preflight(machine)
assert result.returncode == 42
assert any(line.split()[:2] == ["MISSING", "rg"] for line in result.stdout.splitlines())
assert_guidance(result.stdout, "ripgrep (rg) was not found", "--set rg=")
data = machine.recorded()
assert data["complete"] is False and "rg" not in data["tools"]
assert not (machine.tools / ".venv").exists()
def test_python_too_old_stops(machine):
machine.extra_env["FAKE_PY_VERSION"] = "3.9"
result = _preflight(machine)
assert result.returncode == 42
assert "TOO_OLD" in result.stdout
assert_guidance(result.stdout, "Python 3.9 is too old")
def test_invalid_set_path_writes_nothing(machine):
result = _preflight(machine, "--set", f"rg={machine.base / 'nowhere' / 'rg'}")
assert result.returncode == 42
assert_guidance(result.stdout, "The path given for ripgrep (rg) does not work")
assert not machine.tools_file.exists()
def test_valid_set_path_is_recorded_and_kept(machine):
elsewhere = machine.stub("rg", "#!/bin/sh\necho 'ripgrep 14.1.1'\n", machine.base / "opt")
machine.stub("rg", "#!/bin/sh\nexit 1\n")
result = _preflight(machine, f"--set=rg={elsewhere}")
assert result.returncode == 0, result.stdout
assert machine.recorded()["tools"]["rg"] == str(elsewhere)
assert _preflight(machine).returncode == 0
assert machine.recorded()["tools"]["rg"] == str(elsewhere)
def test_set_for_an_unknown_tool_is_a_usage_error(machine):
result = _preflight(machine, "--set", "foo=/bin/sh")
assert result.returncode == 1
assert not machine.tools_file.exists()
def test_venv_that_cannot_be_created_stops(machine):
machine.extra_env["FAKE_VENV_FAIL"] = "1"
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "ensurepip is not available")
def test_pip_failure_stops_and_is_retried_next_time(machine):
machine.extra_env["FAKE_PIP_FAIL"] = "1"
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "network unreachable")
assert machine.recorded()["complete"] is False
del machine.extra_env["FAKE_PIP_FAIL"]
assert _preflight(machine).returncode == 0
assert machine.recorded()["complete"] is True
@pytest.mark.parametrize("platform, alias", [("linux", "python3"), ("windows", "python")])
def test_store_alias_is_never_run_and_never_recorded(machine, platform, alias):
marker = machine.base / "alias-was-run"
apps = machine.base / "Users" / "u" / "AppData" / "Local" / "Microsoft" / "WindowsApps"
machine.stub(alias, f"#!/bin/sh\necho ran > '{marker}'\nexit 9009\n", apps)
machine.path_dirs.insert(0, apps)
machine.extra_env.update({"CHEMENU_PREFLIGHT_PLATFORM": platform, "CHEMENU_PREFLIGHT_LONGPATHS": "1"})
if platform == "windows":
machine.python("python")
result = _preflight(machine)
assert result.returncode == 0, result.stdout
recorded = machine.recorded()["tools"]["python"]
assert "WindowsApps" not in recorded
assert recorded.startswith(str(machine.stubs))
assert not marker.exists()
# --- install folder length (D32) --------------------------------------------------
@pytest.mark.parametrize("length, longpaths, expected", [
(95, "0", 0),
(96, "0", 42),
(96, "1", 0),
])
def test_install_folder_limit_at_the_boundary(tmp_path, length, longpaths, expected):
machine = _machine_with_root_of(tmp_path, length)
machine.extra_env["CHEMENU_PREFLIGHT_LONGPATHS"] = longpaths
result = _preflight(machine)
assert result.returncode == expected, result.stdout
if expected == 42:
assert_guidance(result.stdout, f"too long ({length} characters, at most 95)", "C:\\Chemenu")
assert not (machine.tools / ".venv").exists()
# --- execution policy and Mark of the Web (D16, T6) --------------------------------
OPEN = "MachinePolicy=Undefined,UserPolicy=Undefined,Process=Undefined,CurrentUser=Undefined,LocalMachine=RemoteSigned"
@pytest.mark.parametrize("policy", [
OPEN,
"CurrentUser=Unrestricted,LocalMachine=Restricted",
"Process=Restricted,LocalMachine=RemoteSigned",
"CurrentUser=Undefined,LocalMachine=Undefined",
])
def test_a_policy_that_lets_scripts_run_passes(machine, policy):
machine.extra_env.update({**WINDOWS, "CHEMENU_PREFLIGHT_POLICY": policy})
result = _preflight(machine)
assert result.returncode == 0, result.stdout
@pytest.mark.parametrize("policy", [
"CurrentUser=Restricted,LocalMachine=RemoteSigned",
"CurrentUser=AllSigned",
"CurrentUser=Undefined,LocalMachine=Restricted",
"Process=Bypass,LocalMachine=AllSigned",
])
def test_a_policy_that_blocks_scripts_stops_with_the_one_line_fix(machine, policy):
machine.extra_env.update({**WINDOWS, "CHEMENU_PREFLIGHT_POLICY": policy})
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned")
assert not (machine.tools / ".venv").exists()
@pytest.mark.parametrize("scope", ["MachinePolicy", "UserPolicy"])
def test_a_group_policy_is_a_stop_that_names_the_administrator(machine, scope):
machine.extra_env.update({**WINDOWS, "CHEMENU_PREFLIGHT_POLICY": f"{scope}=AllSigned,LocalMachine=RemoteSigned"})
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "group policy", "Git Bash")
assert "Set-ExecutionPolicy" not in result.stdout
def test_policy_and_marks_are_not_checked_off_windows(machine):
machine.extra_env.update({
"CHEMENU_PREFLIGHT_PLATFORM": "linux",
"CHEMENU_PREFLIGHT_POLICY": "CurrentUser=AllSigned",
"CHEMENU_PREFLIGHT_MARKED": "preflight.ps1",
})
assert _preflight(machine).returncode == 0
def test_a_marked_script_stops_with_the_unblock_command(machine):
machine.extra_env.update({**WINDOWS, "CHEMENU_PREFLIGHT_MARKED": "wikitool.ps1,preflight.ps1"})
result = _preflight(machine)
assert result.returncode == 42
assert_guidance(result.stdout, "wikitool.ps1", "Unblock-File")
assert not (machine.tools / ".venv").exists()
# --- the launcher -----------------------------------------------------------------
def _launch(machine: Machine, *args: str) -> subprocess.CompletedProcess:
return _pwsh(machine, "wikitool.ps1", *args)
def _complete_file(machine: Machine, complete: bool = True) -> None:
machine.tools_file.write_text(json.dumps(
{"schema": 1, "complete": complete, "tools": {"git": "/usr/bin/git"}}, indent=2),
encoding="utf-8")
@pytest.mark.parametrize("state", ["no file", "no venv", "incomplete"])
def test_launcher_stops_with_42_until_the_preflight_has_passed(machine, state):
if state != "no file":
_complete_file(machine, complete=(state != "incomplete"))
if state != "no venv":
machine.stub("python", ECHO_PYTHON, machine.tools / ".venv" / "bin")
result = _launch(machine, "doctor")
assert result.returncode == 42
assert "tools/preflight.ps1" in result.stderr
assert "ExecutionPolicy Bypass" in result.stderr
@pytest.mark.parametrize("layout", [("bin", "python"), ("Scripts", "python.exe")])
def test_launcher_runs_the_entry_script_with_either_venv_layout(machine, layout):
_complete_file(machine)
machine.stub(layout[1], ECHO_PYTHON, machine.tools / ".venv" / layout[0])
result = _launch(machine, "doctor", "--json")
assert result.returncode == 0, result.stderr
assert result.stdout.splitlines() == [str(machine.tools / "run_wikitool.py"), "doctor", "--json"]
def test_launcher_passes_the_exit_code_of_the_cli_through(machine):
_complete_file(machine)
machine.stub("python", "#!/bin/sh\nexit 7\n", machine.tools / ".venv" / "bin")
assert _launch(machine, "lint").returncode == 7
def test_both_launchers_exist_for_pwsh_to_resolve():
assert (TOOLS / "wikitool.ps1").is_file() and (TOOLS / "wikitool").is_file()
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."""
monkeypatch.setenv(prerequisites.ENV_POLICY, "MachinePolicy=AllSigned,LocalMachine=RemoteSigned")
policy = prerequisites.execution_policy()
assert policy is not None and policy.blocks and policy.group_policy
+2 -1
View File
@@ -31,7 +31,8 @@ from chemenu.errors import ChemenuError
FILE_NAME = ".wikitool-tools.json"
SCHEMA = 1
PREFLIGHT = "run the preflight again: tools/preflight.sh"
PREFLIGHT_PWSH = "pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1"
PREFLIGHT = f"run the preflight again: tools/preflight.sh (PowerShell 7: {PREFLIGHT_PWSH})"
class ToolPathError(ChemenuError):