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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased — WebUI V2 unified operator surface

- Fixed `wallbreaker corpus verify [--update]` (and the `wallbreaker parsel verify`
alias) becoming unreachable when the PR #21 and PR #24 lines merged: the dispatch
and subparser were dropped from `cli.py` while `_run_corpus_verify` survived as
dead code. Load-time pin checks were unaffected; the interactive command works
again. Routing is now regression-tested (`tests/test_corpus_cli_routing.py`).
- Added a shared typed capability catalog so TUI behavior is the canonical contract and
every registered operation remains discoverable from V2.
- Added server-owned queued executions with pause, resume, steering, attacker switching,
Expand Down
77 changes: 77 additions & 0 deletions tests/test_corpus_cli_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Regression tests: the `corpus verify` CLI routing must stay reachable.

The dispatch was born in 53c9ca2 (roadmap-implementation TG3) and silently
lost when the PR #21 and PR #24 lines merged in da21689 — _run_corpus_verify
survived as dead code while nothing failed. These tests pin the routing so a
future merge cannot strand the command again.
"""
from __future__ import annotations

from pathlib import Path

import pytest

import wallbreaker.cli as cli
from wallbreaker.cli import SUBCOMMANDS, _run_corpus_verify, build_sub_parser


def test_corpus_in_subcommands():
assert "corpus" in SUBCOMMANDS


def test_corpus_verify_parses():
args = build_sub_parser().parse_args(["corpus", "verify"])
assert args.command == "corpus"
assert args.corpus_action == "verify"
assert args.update is False


def test_corpus_verify_parses_update_and_lock():
args = build_sub_parser().parse_args(
["corpus", "verify", "--update", "--lock", "/tmp/other.lock.toml"]
)
assert args.update is True
assert args.lock == "/tmp/other.lock.toml"


def test_parsel_verify_alias_parses():
args = build_sub_parser().parse_args(["parsel", "verify"])
assert args.command == "parsel"
assert args.parsel_action == "verify"


def test_main_routes_corpus_verify(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
lock = tmp_path / "library.lock.toml"
lock.write_text("", encoding="utf-8")
called = {}

def fake_verify(args):
called["lock"] = args.lock
return 7

monkeypatch.setattr(cli, "_run_corpus_verify", fake_verify)
rc = cli.main(["corpus", "verify", "--lock", str(lock)])
assert rc == 7
assert called["lock"] == str(lock)


def test_main_routes_parsel_verify_alias(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
called = {}

def fake_verify(args):
called["via"] = "parsel"
return 0

monkeypatch.setattr(cli, "_run_corpus_verify", fake_verify)
rc = cli.main(["parsel", "verify"])
assert rc == 0
assert called["via"] == "parsel"


def test_main_routes_parsel_update_to_parsel_lib(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""The verify alias must not swallow the real parsel actions."""
monkeypatch.setattr(
"wallbreaker.tools.parsel_lib.run_parsel_cli", lambda args: 0
)
rc = cli.main(["parsel", "list"])
assert rc == 0
23 changes: 21 additions & 2 deletions wallbreaker/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import argparse
import asyncio
import subprocess
import sys
from pathlib import Path

from dotenv import load_dotenv

Expand Down Expand Up @@ -168,7 +170,7 @@ def _add_endpoint_flags(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--api-key", help="API key literal (prefer --api-key-env)")


SUBCOMMANDS = ("lib", "parsel", "eni", "transform", "findings", "report", "export", "check", "regrade", "baseline", "dashboard")
SUBCOMMANDS = ("lib", "parsel", "eni", "transform", "findings", "report", "export", "check", "regrade", "baseline", "dashboard", "corpus")


def build_main_parser() -> argparse.ArgumentParser:
Expand Down Expand Up @@ -244,7 +246,7 @@ def build_sub_parser() -> argparse.ArgumentParser:
parsel = sub.add_parser(
"parsel", help="Manage the P4RS3LT0NGV3 transform library (MCP server backend)"
)
parsel.add_argument("parsel_action", choices=["update", "list", "path"])
parsel.add_argument("parsel_action", choices=["update", "list", "path", "verify"])

eni = sub.add_parser("eni", help="Browse the ENI persona-jailbreak collection")
eni.add_argument("eni_action", choices=["list", "update", "path"])
Expand Down Expand Up @@ -305,6 +307,19 @@ def build_sub_parser() -> argparse.ArgumentParser:
help="Acknowledge the risk of exposing this unauthenticated single-operator dashboard",
)

corpus = sub.add_parser("corpus", help="Manage corpus integrity pins (library.lock.toml)")
corpus_sub = corpus.add_subparsers(dest="corpus_action", required=True)
cv = corpus_sub.add_parser(
"verify",
help="Check corpus SHAs against library.lock.toml; non-zero exit on UNRESOLVED or DRIFT",
)
cv.add_argument(
"--update",
action="store_true",
help="Attempt to resolve actual HEAD SHAs via git ls-remote and update the lock file",
)
cv.add_argument("--lock", default=None, help="Path to library.lock.toml (default: repo root)")

return parser


Expand Down Expand Up @@ -412,7 +427,11 @@ def main(argv: list[str] | None = None) -> int:
from .tools.eni import run_eni_cli

return run_eni_cli(args)
if args.command == "corpus":
return _run_corpus_verify(args)
if args.command == "parsel":
if getattr(args, "parsel_action", None) == "verify":
return _run_corpus_verify(args)
from .tools.parsel_lib import run_parsel_cli

return run_parsel_cli(args)
Expand Down
Loading