Metadata-Version: 2.4
Name: sel2pw
Version: 0.2.0
Summary: Selenium Java -> Python Playwright migration agent (CLI, MCP server, LLM-agnostic agent)
Author: Shan Konduru
License-Expression: MIT
Keywords: selenium,playwright,migration,test-automation,pytest,java,llm,mcp
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: anthropic
Requires-Dist: anthropic>=1.0; extra == "anthropic"
Provides-Extra: openai
Requires-Dist: openai>=1.40; extra == "openai"
Provides-Extra: mcp
Requires-Dist: mcp>=2.0; extra == "mcp"
Provides-Extra: all
Requires-Dist: anthropic>=1.0; extra == "all"
Requires-Dist: openai>=1.40; extra == "all"
Requires-Dist: mcp>=2.0; extra == "all"
Provides-Extra: dev
Requires-Dist: anthropic>=1.0; extra == "dev"
Requires-Dist: openai>=1.40; extra == "dev"
Requires-Dist: mcp>=2.0; extra == "dev"
Requires-Dist: pytest>=8; extra == "dev"
Dynamic: license-file

# sel2pw — Selenium Java → Python Playwright migration agent

Implements the agent described in
`selenium_java_to_playwright_migration_agent_system_prompt.md` (bundled as `sel2pw/prompts/system_prompt.md`)
three ways, all on one shared core:

| Surface | Command | Who does the reasoning |
|---|---|---|
| CLI | `sel2pw inventory / migrate / validate` | Built-in agent + any LLM provider |
| MCP server | `sel2pw mcp` (or `sel2pw-mcp`) | The MCP host's LLM (Claude Code/Desktop, Copilot, Cursor…), or the built-in agent via `run_migration_agent` |
| Library | `MigrationAgent(get_provider(...), Workspace.create(...)).run()` | Any `LLMProvider` |

## Architecture

```
src/sel2pw/
  workspace.py   sandbox: named roots (source=ro, target=rw, --reference=ro), path-escape checks, secret redaction
  inventory.py   deterministic Java scan: tests (IDs "Class#method"), lifecycle hooks, page objects,
                 dependencies, locators, data-provider rows, constructs to flag (waits, frames, alerts, JS, ...)
  validate.py    compile (in-process), pytest --collect-only, pytest run (opt-in only)
  ledger.py      structured mappings + issues recorded by the agent
  gates.py       deterministic acceptance gates
  reports.py     code-generated migration_map.md / migration_validation.md
  manifest.py    run fingerprint + manifest under <target>/.sel2pw/runs/
  tools.py       ONE tool registry + input validation, used by both the agent and the MCP server
  agent.py       provider-agnostic loop (append-only history, gate-driven repair rounds, finalize)
  llm/           base.py (neutral messages) + anthropic/openai adapters + registry + cache.py (record/replay)
  prompts/       system_prompt.md (extracted from the spec) + static TOOL LAYER appendix
  cli.py, mcp_server.py
```

The spec says *"prompt text alone is not a security boundary"*, so the tool layer enforces it:
the source root is never writable; no path can leave its root; secrets are redacted before reaching any LLM;
overwriting a target file that existed before the run is recorded and reported; browser execution
(`run_check kind=run`) is refused unless the operator passes `--allow-browser-tests`.
There is no generic shell tool.

## Install

```bash
pip install -e ".[all]"        # or .[anthropic] / .[openai] / .[mcp]; .[dev] adds pytest
```

## CLI

```bash
# Deterministic inventory (no LLM, free)
sel2pw inventory path/to/selenium-project --out inventory.md

# Full migration with the built-in agent
sel2pw migrate --source path/to/selenium-project --target path/to/innoharmony-repo \
    --task "Migrate LoginTest and its page objects" --scope "LoginTest#*"

# Static checks on migrated code (add --collect for pytest --collect-only)
sel2pw validate path/to/innoharmony-repo --collect --python path/to/repo/.venv/Scripts/python
```

Useful `migrate` flags: `--reference innoharmony=path/to/framework` (extra read-only root),
`--allow-browser-tests` (approved non-prod env only), `--python` (target repo's interpreter for pytest),
`--scope`, `--max-repairs`, `--gate-collect`, `--gate-run`, `--min-migrated N|N%`, `--allow-exclude PATTERN`, `--report-dir`, `--cache`, `--max-turns`, `--max-total-tokens N` (stop cleanly with `stop=budget`), `--system-prompt FILE`.
Exit code is 0 only when the acceptance gates pass.

Large suites: `--batch-by class` (or `package`) runs one agent session per unit, each scoped to exactly that
unit's in-scope tests, with one shared ledger. Progress is saved to `<target>/.sel2pw/batch/state.json` after
every unit, and `--resume` continues a batch, skipping units already accepted. When all units have run, the
gates run across the whole scope and the combined reports are written.

## Determinism

LLM output can't be made bit-for-bit deterministic. Claude Opus 5.5 rejects `temperature`, and OpenAI's `seed`
is only best effort. sel2pw makes every *outcome* deterministic and every *run* reproducible:

| Layer | Mechanism |
|---|---|
| **What counts as "done"** | Deterministic acceptance gates: `scope_coverage` (every in-scope test mapped), `mapping_integrity` (target file + symbol exist; blocked/excluded have reasons; a partial test has a MEDIUM-or-higher issue and a blocked test a BLOCKER/HIGH one; a test is excluded only if it matches `--allow-exclude`), `migration_yield` (at least `--min-migrated` in-scope tests, default 1, are migrated or partial), `assertion_parity`, `parametrize_parity` (data-provider rows = `parametrize` cases), `disabled_preserved` (stays skipped), `compile`, `lint` (no unjustified sleeps/`networkidle`, no secrets or redacted placeholders, no assertions that always pass, no `expect()` without a matcher), optional `collect`, and optional `run` (`--gate-run` with `--allow-browser-tests`: the mapped tests are executed once and each test's passed / failed / skipped result goes into both reports). Failures are sent back to the model for up to `--max-repairs` rounds, and the run fails if they persist. |
| **Reports** | The agent records structured data (`record_mapping`, `record_issue`). It cannot write `migration_map.md` or `migration_validation.md`. Code renders both, with no timestamps, so identical inputs give byte-identical reports. Text the model wrote is shown in labelled "agent" columns or sections. |
| **Reproducing a run** | Record/replay cache (`--cache auto` by default) in `<target>/.sel2pw/cache`. It is keyed by a SHA-256 of the provider settings, system prompt, tool schemas and the whole conversation, including tool results. Re-running with the same inputs and the same starting target replays the run exactly without calling the model. `--cache replay` makes any deviation an error, which suits CI. `--cache off` forces a fresh attempt. Tool output is normalized (sorted listings, pytest timings stripped) so keys stay stable. |
| **Traceability** | Every run writes `<target>/.sel2pw/runs/<run_id>/`: `manifest.json` (model + settings, prompt/tools/task/source-tree/inventory hashes, target tree hash before/after, cache hits, usage, SDK versions), `gates.json`, `ledger.json`, `events.jsonl`. The stable part of the fingerprint is also in the validation report. |

Escape hatches the gates recognise (all appear in reports for reviewers): a `justification` on a mapping
(intentional assertion, case or skip difference), and line comments `# sel2pw: allow-sleep: <reason>`,
`# sel2pw: allow-networkidle: <reason>`, `# sel2pw: allow-secret` (test data, not credentials).

Add `.sel2pw/` to the target's `.gitignore`, or commit `.sel2pw/cache` if you want others to be able to replay your runs.

### LLM providers (`sel2pw providers`)

| `--provider` | Notes |
|---|---|
| `anthropic` (default) | Default model `claude-opus-5-5`. Auth: `ANTHROPIC_API_KEY` or `ant auth login`. |
| `openai` | `OPENAI_API_KEY`, `--model` required |
| `azure-openai` | `AZURE_OPENAI_BASE_URL` (v1 endpoint) + `AZURE_OPENAI_API_KEY`; `--model` = deployment |
| `gemini` | `GEMINI_API_KEY`, OpenAI-compatible endpoint, `--model` required |
| `ollama` | Local, `OLLAMA_BASE_URL` (default `http://localhost:11434/v1`); use a tool-capable model |
| `openai-compatible` | vLLM, LM Studio, OpenRouter, LiteLLM… via `SEL2PW_BASE_URL` / `SEL2PW_API_KEY` |
| `my_pkg.module:MyProvider` | Plugin: any `sel2pw.llm.base.LLMProvider` subclass |

Defaults can come from env: `SEL2PW_PROVIDER`, `SEL2PW_MODEL`, `SEL2PW_EFFORT`.

Anthropic adapter specifics: streaming with `eager_input_streaming` tools (whole-file writes stream as they are
generated; inputs are re-validated before running), adaptive thinking with `--effort` (default `high`),
automatic prompt caching, and **server-side refusal fallbacks enabled by default** (`fallbacks="default"`).
Pass `--no-fallbacks` to turn them off; you must do that on Bedrock/Vertex/Foundry gateways, where the parameter is rejected.
Long runs use **server-side compaction** (`compact-2026-01-12`, at 150K input tokens, with a summary prompt that keeps
the migration state); the history stays append-only, so thinking blocks remain valid. `--no-compaction` turns it off.

OpenAI-compatible providers get client-side clearing instead: once tool output in the history passes 400K
characters, older large results are sent as a short stub (the three newest tool-result messages always go in full).

### Adding a provider

```python
from sel2pw.llm.base import LLMProvider, AssistantTurn, ToolCall

class MyProvider(LLMProvider):
    name = "mine"
    def __init__(self, model=None, **kw):
        super().__init__(model or "my-model")
    def complete(self, system, messages, tools) -> AssistantTurn:
        ...  # convert UserMessage / AssistantTurn(raw) / ToolResultsMessage to your API, call it,
             # return AssistantTurn(text, [ToolCall(id, name, args)], stop_reason, raw=<native turn>)
```

```bash
sel2pw migrate --provider my_pkg.providers:MyProvider ...
```

## MCP server

Tools: `java_inventory`, `list_files`, `read_file`, `search`, `write_file`, `edit_file`, `run_check`,
`record_mapping`, `record_issue`, `check_acceptance`, `finalize_migration` (gates + generated reports + run manifest),
`reset_session` (start a new migration on the same server), `run_migration_agent` (hide with `--no-agent-tool`). Prompt: `migrate_selenium_to_playwright`.
Resource: `sel2pw://system-prompt`. The roots, `--scope` and gate options are fixed at server start, so a host
can't widen the scope. Host-driven runs get the same gates and generated reports through `finalize_migration`.

**Claude Code**

```bash
claude mcp add sel2pw -- sel2pw mcp --source C:/work/selenium-tests --target C:/work/innoharmony-tests
```

**VS Code / Cursor / Claude Desktop** (`mcp.json` / `claude_desktop_config.json`)

```json
{
  "mcpServers": {
    "sel2pw": {
      "command": "C:/myprojects/selenium2playwright/.venv/Scripts/sel2pw.exe",
      "args": ["mcp", "--source", "C:/work/selenium-tests", "--target", "C:/work/innoharmony-tests"]
    }
  }
}
```

HTTP transport: `sel2pw mcp ... --transport streamable-http [--host 127.0.0.1] [--port 8000]`. The server exposes
write tools, so it binds to 127.0.0.1 by default and refuses any other address unless `SEL2PW_MCP_TOKEN` is set;
with a token, every request needs `Authorization: Bearer <token>`. One server instance serves one migration at
a time: its recorded mappings and written files are shared by every client that connects, so run a separate
server per concurrent migration, and call `reset_session` before starting a new one. `run_migration_agent`
keeps its own record of written files.

## Security audit

```bash
security-audit.bat            # Windows
./security-audit.sh           # Linux, macOS, Git Bash
```

Creates and activates `.venv` with the project and its dev extras, installs the pinned tools in
`requirements-security.txt` into a separate `.venv-audit` (Semgrep pins an older `mcp` than this project needs),
downloads checksum-verified gitleaks, Trivy and CodeQL into `.tools/` on first use
(CodeQL is a one-time ~665 MB download), then runs gitleaks, Trivy, Bandit, Semgrep, pip-audit and CodeQL
(Python and GitHub Actions). Open `security-reports/security-report.html` for all results, with a page per tool.
Options: `-SkipCodeQL` / `--skip-codeql`, `-SkipInstall` / `--skip-install`, `-Out DIR` / `--out DIR`.

## Tests

```bash
pytest -q
```

No test calls a real LLM. The provider adapters are tested against fake clients, and the MCP tests run the
server in-process.
