Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: 🧪 Tests

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true

jobs:
pytest:
name: 🐍 ${{ matrix.os }} · py${{ matrix.python }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# Ubuntu only while this branch is based on a main that predates the
# Windows support in #57: the Windows lanes there exercise platform
# code this branch does not carry, and fail on main's known
# POSIX-only layers (os.killpg and friends). #57 brings the full
# ubuntu + windows matrix; on merge, its version of this file wins.
os: [ubuntu-latest]
python: ["3.10", "3.14"]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}

- name: 📦 Install with test dependencies
run: python -m pip install -e ".[test]"

- name: 🧪 Run the suite
# The Windows symlink fixtures skip themselves when the runner lacks
# SeCreateSymbolicLinkPrivilege; those skips are expected and green.
run: python -m pytest -q
13 changes: 11 additions & 2 deletions src/lh_harness/role_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,12 +480,21 @@ def format_audit_findings(

def parse_role_manager_next_step(text: str) -> RoleNextStep:
for line in str(text or "").splitlines():
normalized = line.strip().strip("*").replace(" ", "").replace(" ", "").lower()
# `*` covers bold; the backtick covers code spans. The prompt itself
# displays every route inside backticks ("exactly one route:
# `Next: gui`, ..."), and models -- reliably the smaller ones -- copy
# that formatting into their answer, so a parser that accepts
# `**Next: cli**` but not "`Next: cli`" burns a whole round on
# formatting the harness's own instruction taught the model.
normalized = line.strip().strip("*`").replace(" ", "").replace(" ", "").lower()
# Models commonly append a short rationale after the required route,
# e.g. `Next: done — all constraints passed`. Treat only an explicitly
# delimited suffix as commentary so prose such as `Next: done later`
# remains invalid.
normalized = re.split(r"(?:—|–|--|//|#|[((])", normalized, maxsplit=1)[0]
# Strip wrappers again after cutting the rationale: in
# "`Next: cli` — reason" the closing backtick sits before the dash and
# survives the first strip.
normalized = re.split(r"(?:—|–|--|//|#|[((])", normalized, maxsplit=1)[0].strip("*`")
if normalized in {"下一步:gui任务", "下一步:gui任务", "next:gui"}:
return MANAGER_NEXT_GUI
if normalized in {"下一步:cli任务", "下一步:cli任务", "next:cli"}:
Expand Down
15 changes: 15 additions & 0 deletions tests/test_role_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ def test_manager_route_rejects_undelimited_suffix() -> None:
assert parse_role_manager_next_step("Next: done later") != MANAGER_NEXT_DONE


@pytest.mark.parametrize(
"line",
[
# The prompt shows every route inside backticks, and models copy that
# formatting literally; bold was already accepted, so a code span must
# not be the one markdown wrapper that voids an otherwise valid route.
"`Next: cli`",
"**`Next: cli`**",
"`Next: cli` — routed to the CLI executor",
],
)
def test_manager_route_accepts_code_span_wrapping(line: str) -> None:
assert parse_role_manager_next_step(line) == MANAGER_NEXT_CLI


@pytest.mark.parametrize("language", ["en", "zh"])
def test_manager_prompt_exposes_total_and_remaining_round_budget(language: str) -> None:
prompt = build_role_manager_prompt(
Expand Down
Loading