Metadata-Version: 2.4
Name: ancha
Version: 0.1.0
Summary: Ancha for Python: test your agent's own tools against production's recorded calls, and pytest worlds for agent tests
License-Expression: MIT
Classifier: Framework :: Pytest
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: pytest
Requires-Dist: pytest>=7; extra == "pytest"

# ancha

Ancha for Python. Two parts, one `import ancha`:

- **Tool wrappers** (no dependencies): wrap your agent's own tools once with `@ancha.tool` or `ancha.wrap_tools`, and
  run the agent through `ancha.entry`. In production nothing changes. In an [Ancha](https://ancha.io) test made from a
  production trace, each call answers as production did. See
  [Tool wrappers](https://docs.ancha.io/reference/tool-wrappers).
- **A pytest plugin** (`pip install "ancha[pytest]"`): a fresh simulated world for each test. See
  [Test with pytest](https://docs.ancha.io/guides/pytest).

```sh
pip install ancha
```

```python
import ancha

@ancha.tool
def lookup_order(order_id: str) -> dict: ...

ancha.entry(lambda input: run_agent(input["task"]))
```

## The pytest plugin

pytest for Ancha: a fresh simulated world for each test, your agent run against it, and checks on what people learned and what the tools hold. It drives the `ancha` CLI. This development version needs the matching CLI with `world capture` and `world record`; published CLI 0.1.4 does not support run recording. `ANCHA_CLI` names another CLI command.

```python
import ancha, pytest

@pytest.mark.ancha(test="ancha/tests/relay-export-bug.yaml")
@ancha.attempts(10, min_pass_rate=0.9)
def test_relay(world):
    world.start("npm start")                 # the agent gets world.env: the tools' URLs and fake credentials, ANCHA_TASK
    world.wait_until_quiet()
    assert world.people["ben"].knows("laura.beta_on")
    assert world.slack.messages(to="laura", in_thread_of="$laura_report", from_="$slack_bot")
    assert not world.people["laura"].knows("ben.root_cause")
```

- `@pytest.mark.ancha(test=…, scene=…, world=…, on="local"|"cloud", seed=…, ttl=…)` says what the `world` fixture starts (`ancha world up`). Without it, the one scene `ancha.yaml` names.
- `world.env`, `world.task`; `world.start(command)`, `world.wait_until_quiet(timeout)`, `world.log()`, `world.stop()`.
- `world.check()` applies the test's own checks; `world.check({...})` applies check objects (the kinds in `ancha docs scenes`). A report is truthy when every check passed; `report.failures` has expected and actual.
- `world.people[ref].knows(fact)`, `.decided(decision, option)`; `world.slack.messages(to=, in_=, in_thread_of=, from_=, text_contains=, fact=)`.
- `world.state()`, `world.trace()`; `world.fresh()` for another world; the fixture removes its world (`ancha world down`).
- `@ancha.attempts(n, min_pass_rate)` runs the test in `n` fresh worlds and passes when enough of them do.

An evaluated check that fails returns a false `CheckReport`; `report.failures` explains the expected and actual values. A check that cannot be evaluated raises `ancha.AnchaError` instead. For example, a `knows` check naming an unknown person raises an error with `code == "USAGE"` and a message naming the invalid reference. Correct the test definition; this error is not a verdict on the agent. The people and Slack helpers use the same check path and can raise the same error. CLI failures also raise `AnchaError`, retaining the CLI's `code`, `message` and optional `hint`.

Worlds start locally by default, including after `ancha login`. Run `pytest --on cloud` to put worlds in your hosted workspace; pytest and the agent still run on your machine. `--on local` forces local worlds. The pytest option overrides the marker's `on="local"` or `on="cloud"`; without either, the CLI's normal world selection applies. An explicit server URL can also select a world server. Never give the agent the test files or the checks.

## Recorded runs

While the plugin is enabled, each executed pytest item creates one local run under `.ancha/runs/`, with `source: pytest` and `target: local`. This includes tests without a world, parameterized cases and skipped tests. Collection alone creates no records. Disable the plugin with `-p no:ancha` for suites that should not record. Run files stay local even when worlds run in the cloud; uploading pytest records is not supported.

```sh
ancha --local runs list --json
ancha --local runs show <run-id> --json
ancha --local explain <run-id> --json
```

Pytest assertions decide the test outcome. A deliberately false `world.check()` remains evidence and does not fail a passing negative assertion. Records preserve setup, call and teardown outcomes separately. Skipped and expected-failure cases are ungraded; setup, capture and cleanup failures are invalid, not failed agent answers. `@ancha.attempts` records each invocation and its fresh world under the same run, including failed attempts when the overall pass-rate threshold is met. The requested repetition count is recorded before execution. Reports keep every requested attempt in the pass-rate denominator, including invalid, skipped and missing attempts; all-k requires all requested attempts to finish validly and pass. Thresholds use exact integer counts. An old baseline without verified requested counts must be regenerated before comparison.

The plugin stops its agent, captures a coherent world observation, then removes the world. This observation is not a sealed managed-trial capture. Capture failures remain visible and cleanup is still attempted. Recording failures are reported and make an otherwise successful pytest session fail.

Parameter values in test IDs are replaced by a hash. Records omit environment values, raw exception messages, captured pytest output and agent logs. Known credentials are redacted from evidence; keep `.ancha/` private and out of version control. Abrupt termination such as SIGKILL cannot finish a record or guarantee cleanup; world TTLs still apply.
