Testing the plugins¶
Four hosts, a native dsh plugin and a web panel, and none of it is exercised by importing the library. This page is how to check each piece — what runs with no credentials, what needs a host installed, and what only a human can confirm.
The short version:
pip install -e ".[dev,mcp]"
pytest tests/test_evolvespec.py tests/test_runstore_cli.py tests/test_mcp.py \
tests/test_plugin_runner.py tests/test_integrations.py \
tests/test_run_panel.py tests/test_stop_when.py tests/test_demo.py
That is the whole plugin surface, offline, in about a minute. Everything below is what those tests cannot reach.
1. What the test suite already covers¶
No credentials, no host installed, no network.
| File | What it pins |
|---|---|
test_evolvespec.py |
every kind composes the evolve() call the quickstarts write; refs resolve inside the allowlist; a spec runs end to end with a stub agent |
test_runstore_cli.py |
a detached run really detaches, reports, resumes and cancels (with its workers); every CLI verb, including relative paths surviving the process boundary |
test_mcp.py |
the tool bodies, the nested-run stub, and — when mcp is installed — the real server's tool and resource registration |
test_plugin_runner.py |
kind: plugin against a stub host CLI: isolated host home, the validate gate, frozen hooks surviving a candidate that rewrites them, env_passthrough |
test_integrations.py |
every host manifest, the checked-in plugin packages matching their renderers, and the skill naming only tools/kinds/verbs that exist |
test_run_panel.py |
the HTTP panel, and that it refuses path traversal and non-loopback CORS |
test_demo.py |
agentdescent demo — the skill really is wrong, the offline agent really obeys the one on disk, and the run fixes it without touching your files |
Two of them reach further when the tools are present, and skip cleanly when not:
# drives the dsh plugin through dsh's own isSkillName validator
which dsh && pytest tests/test_integrations.py -k real_registry -q
# loads the browser bundle the way dsh's module loader does and renders it
mkdir -p /tmp/react-probe && cd /tmp/react-probe && npm init -y && npm i react react-dom
AGENTDESCENT_NODE_PROBE=/tmp/react-probe pytest tests/test_integrations.py -k client_bundle -q
2. Check your own machine¶
Lists which agent CLIs are on PATH, which provider keys are set, whether a
container engine and the mcp package are present, and where the run store is.
Every "problem" line it prints is something that will make a real run fail.
3. The offline end-to-end run¶
The fastest way to see the whole loop without spending anything, and the first thing to try when something is wrong — if this fails, the problem is the install, not the host.
It writes a skill whose references/rules.md names the wrong column, twelve
CSVs with known totals, and a spec pointing at
agentdescent.demo:offline_agent — a subprocess that reads the skill off disk,
so staging, the workspace, the ledger, the merge and the held-out gate all run
for real. Only the model is replaced.
Expected: round 0 reward=1.000 +1/-0, held-out reward: 1.000, and
what it learned: rules.md -> 'COLUMN: amount'. Anything less and the exit
status is 1 and the run directory is named for you to open.
Then the verbs an agent would call, against that same run:
agentdescent status
agentdescent show <run_id> # the diff: COLUMN: id -> COLUMN: amount
agentdescent apply <run_id> --dry-run
apply without --dry-run writes it back and leaves a backup at
csv-total.bak-0. To rehearse your own spec against the same free pair, point
its agent and reflect at agentdescent.demo:offline_agent and
{"ref": "agentdescent.demo:offline_reflector", "call": false} — both are
public and inside the ref allowlist.
4. Each host¶
install is idempotent and --dry-run prints what it would write, so it is
safe to inspect first. Use --home to keep a probe out of your real setup.
Claude Code¶
agentdescent install claude-code
claude plugin validate ~/.agentdescent/plugins/claude-code # structural check
claude --plugin-dir ~/.agentdescent/plugins/claude-code # interactive
Headless, which is what a CI check would run — note the tool names a plugin
gets (mcp__plugin_<plugin>_<server>__<tool>), and that --allowedTools is
variadic so the prompt goes on stdin:
echo "Call the agentdescent doctor tool and print its run_store field." | \
claude -p --plugin-dir ~/.agentdescent/plugins/claude-code \
--permission-mode acceptEdits \
--allowedTools mcp__plugin_agentdescent_agentdescent__doctor
Expected: the run-store path. If it says the server failed to connect, the
usual cause is agentdescent not being on PATH for the process that started
Claude Code, or the [mcp] extra missing — agentdescent mcp then exits 3 and
says so. Claude Code caches a failed connection for ~15 minutes; a different
--plugin-dir path is the quickest way to retry.
DeepSeek Harness¶
Either the files or the native plugin, not both.
dsh plugin --profile web add link:$PWD/integrations/dsh-agentdescent
dsh --profile web --dump-config | grep -n agentdescent
Expected: rows mcp-agentdescent (and dsh-agentdescent for the plugin) under
a # == ... heading. patch: entry "..." not found means the rows were
written without - insert: — a dsh patch file overrides by id, it does not
append. A declares no dsh.bundle warning on install means the package is
inert; it needs "dsh": {"bundle": {"patch": "./cordis.patch.yml"}}.
Codex¶
Two routes. The plugin is one command and brings the MCP server with it; Codex reads the same plugin format and marketplace as Claude Code:
codex plugin marketplace add /path/to/agentdescent # or Birfy/agentdescent
codex plugin add agentdescent@agentdescent
codex plugin list # agentdescent@agentdescent installed, enabled
codex mcp list # agentdescent ... enabled
Or the file route, which needs no marketplace:
codex doctor reports "no MCP servers configured" even when a plugin-provided
server is live — it counts only mcp_servers in config.toml. Trust
codex mcp list.
OpenCode¶
connected here really did start the server; Executable not found in $PATH
means agentdescent is not on the PATH OpenCode inherited.
5. The MCP server on its own¶
Independent of any host, using the protocol SDK as a client:
import asyncio, os, sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command="agentdescent", args=["mcp"], env={**os.environ})
async with stdio_client(params) as (r, w):
async with ClientSession(r, w) as s:
await s.initialize()
print(sorted(t.name for t in (await s.list_tools()).tools))
print((await s.call_tool("doctor", {})).content[0].text[:200])
asyncio.run(main())
Expected: the eight tools (apply cancel doctor plan resume show start status)
and a doctor report.
6. The web panel¶
The page lists runs and refreshes itself. In dsh's web UI the plugin adds an
evolve action to the session header with the live count; clicking it shows the
same list. If the panel says "No run panel", agentdescent serve is not
running.
7. What no test here can tell you¶
Be aware of these rather than assume they are covered.
- Whether the dsh panel looks right. The bundle is checked for loading,
registering into
conversation.session.header.actionsand rendering HTML through React. Whether it is legible in the actual UI needs a browser and a dsh session. - A full Codex or DSH session. Both were verified at the config layer
(
codex mcp list,dsh --dump-config) because neither had credentials here. Claude Code and OpenCode were driven to the point of tools returning data. kind: pluginagainst a real dsh plugin repo. The host table is exercised with a stub host CLI; thepnpm install && pnpm buildchain in the dsh row has not been run against a real Cordis package.- Cost. Every number
planreports is an upper bound on calls; the dollar figure only appears when you give it a per-call price. Run a real evolution with--budgetand a smallroundsthe first time.