Kin / Maintainers
Testing
The kin test surface is split into two homes:
Read as MarkdownThe kin test surface is split into two homes:
tests/— the pytest suite. Headless, no model needed, no real network (most files). Runs undertask verify. This is where you add tests for harness internals, headless flows, and the like.scripts/— live / smoke / probe scripts. Real model, remote service, network, or subprocess. Runs undertask live-drive*,task live-git-tools,task smoke*, andtask probe-mcp*. Not pytest; they stay as CLI scripts because their assertion surface doesn’t fit a unit-test frame.
The split matches the project character: most of what we test is harness internals (pytest is the right tool); a small but important slice is real-wire behavior (CLI scripts are the right tool).
The structured Git/GitHub boundary has one deliberately separate publication gate:
KIN_LIVE_GIT_PUBLISH=1 task live-git-tools
It refuses to start without that exact environment opt-in or unless local
HEAD matches the remote base. It then creates a disposable worktree/branch,
drives the structured local Git operations, and fetches/pushes through Kin’s
real gh-backed HTTPS credential route even when the saved remote uses SSH. An
independent temporary repository advances the disposable branch while the
first worktree has one unpublished commit; git.sync must replay it. The gate
then publishes that result, makes a test-only local amend, proves an ordinary
push rejects the non-fast-forward, and proves the structured exact
force-with-lease succeeds. Finally it opens/retries/reads a draft PR and closes
the PR plus exact remote/local branch. It is not part of task ship:
remote publication and cleanup require an explicit operator decision, while
the promotion gate remains hermetic.
Pytest layout
tests/
├── __init__.py # env-before-import gate (sets KIN_HOME etc.)
├── conftest.py # shared fixtures + helpers
├── test_settings.py # headless settings
├── test_banner.py # headless banner
├── test_integration.py # end-to-end with FakeBackend + KinApp
├── test_workflow.py # the 17 closed-namespace / AST-filter tests
├── test_search.py # FTS5 workspace search
├── test_memory.py # the memory_20250818 command set
├── test_provider_presets.py # the first-party preset system
├── test_evals.py # the regression sentinel (--update-baseline)
├── test_web.py # web_fetch SSRF + Brave formatting
├── test_office_security.py # docx/xlsx read_file: zip-bomb cap + entity-expansion guard
├── test_cli.py # `kin --version` + `kin doctor` (install/config/network chunks)
├── test_ship_gate.py # parallel gate + agent-output contract
├── test_docs_audit.py # static guide-screenshot ownership graph
├── test_harness/ # the split harness subpackage
├── test_app/ # the split UI subpackage
├── test_outpost/ # the outpost dashboard (requires --extra outpost)
├── test_pty.py # the out-of-process PTY smoke
├── test_snapshot.py # the SVG baseline guard
├── test_worktrees.py # worktree isolation
├── test_perf.py # synthetic journal perf benchmark
└── test_live_drive.py # thin wrapper over scripts/_live_drive_scenarios
Most top-level test_<area>.py files correspond 1:1 to a historical
verify_<area>.py script they migrated from (Phases 1-5, 2026-07-02) —
see the per-task table below. test_office_security.py and test_cli.py
are newer, added directly as pytest with no verify_*.py ancestor. The
split subpackages (test_harness/, test_app/, test_outpost/)
replaced the monolithic verify_harness.py / verify_app.py /
verify_outpost.py files (~12K + ~4K + ~9K lines each) with focused
submodules.
Adding a tool, a permission kind, or a subagent profile all need matching
coverage under tests/test_harness/ — see
Extending kin for where each of those lands.
The local promotion gate
task ship is the contract before a dev → main merge — see
AGENTS.md’s
Commands table for the day-to-day command surface and
Multi-machine dev setup for running it on a
second machine (task kloud:verify). No CI server, no GitHub Actions —
Blake + AI peers run it as the explicit verification step. The shape:
task ship # exhaustive promotion proof; compact on green, failure-first on red
The gate has two waves:
check,verify,docs-build,docs-audit,memory-check, andcode-auditrun concurrently. Verification itself has two concurrent lanes: every non-Outpost file is grouped across at most eight xdist workers, whiletests/test_outpost/remains serial because its process-lifecycle and journal fixtures are not yet safe under blanket distribution.docs-audithard-fails decision conventions, boundedSTATUS.mdsections, and the deterministic ownership graph joining both guide capture manifests, committed SVG/PNG assets, and non-empty Markdown references; broaderSOURCE:and deleted-document findings remain warnings.live-drive-dryruns only after the independent first wave is green.
On the 2026-07-17 reference machine, the pre-change serial pytest selection took 102.9s and the complete gate took 111.3s. The safe split runs the same selection in about 22–23s; complete-gate wall time is the longest first-wave stage plus the roughly 5s dry drive, not the sum of every row. Reference runs of the new topology are green in about 28s.
If any sub-command fails, the merge is blocked. All independent first-wave stages finish so one peer turn reports every failure family; ordered stages are shown as not run. The contract remains “I ran it, here’s the output” — not “looks right.”
task ship is one Taskfile target rather than a separate workflow, so the
promotion path and the ad-hoc local command cannot drift apart.
Agent-facing output
Passing stage output is captured rather than streamed. A green run reports the commit, aggregate pytest counts, wall time, per-stage timings, and the slowest test:
SHIP PASS 27.8s | sha abc1234+dirty | 2509 passed, 5 skipped
stages: check 0.4s · verify 22.9s · docs-build 1.7s · ... · live-drive-dry 4.9s
verify lanes: verify-main 15.2s (1736 passed, 1 skipped) · verify-outpost 22.9s (773 passed, 4 skipped)
slowest: 7.12s tests/test_outpost/test_transcript.py::test_name
A red run reports every failed stage together, retains useful head-and-tail
evidence within Kin’s shell-output cap, prints exact pytest node reruns when
available, and preserves the complete logs under /tmp/kin-ship-*. Successful
runs remove their temporary logs. The SHA carries +dirty when the proof ran
against uncommitted or untracked changes, so an agent cannot mistake the base
commit for the complete verified source state. The per-lane counts also make a
base install’s clean Outpost package skip visible instead of hiding it inside
the aggregate green result.
Pytest’s global configuration deliberately keeps test-case progress quiet while retaining high assertion verbosity and short tracebacks. The hermetic lanes also report slow tests and apply a 60-second per-test backstop; slow/live tests keep their specialized bounds outside this gate.
AI-peer repair loop
Use the narrowest relevant node while changing code:
task verify-one -- 'tests/test_harness/test_tools.py::test_name' -vv
uv run pytest --lf -x
task check
Then run task ship exactly once as the exhaustive completion proof. Last-
failed, stepwise, and affected-test techniques are repair-loop accelerators,
not substitutes for the promotion gate. Set KIN_TEST_WORKERS to reduce the
default eight-worker cap on a constrained machine:
KIN_TEST_WORKERS=4 task verify
Markers
Two markers route tests out of the default task verify pass:
| Marker | Meaning | Run with |
|---|---|---|
slow |
machine / perf / Textual-version sensitive | task test-slow |
live |
needs real network / real endpoint | task test-live |
The default task verify runs everything that ISN’T marked — i.e. the
hermetic headless suite. The slow and live suites run on demand via their
dedicated task test-* entry points (or by directly passing -m slow /
-m live to pytest).
The fifteen rendered TUI comparisons in tests/test_snapshot.py are slow
because Rich/Textual SVG bytes depend on the locked rendering environment and
machine layout. task verify-snapshot compares all fifteen baselines;
task docs-screenshots-tui regenerates the ten test_docs_ scenes published
directly under docs/assets/screenshots/. task docs-screenshots-web
regenerates the five local-only Outpost PNGs at their fixed desktop/phone
viewports, and task docs-screenshots runs both capture paths. The static
dual-manifest ownership checks in tests/test_docs_audit.py are ordinary
hermetic tests and remain in task verify and task ship; rendering and
Playwright do not.
Outpost tests are hermetic, not a separate marker class. They are collected by
task verify when the outpost extra is installed and skipped as a package by
tests/test_outpost/conftest.py (via its aiohttp check) on a base install. To
require that full package rather than permit the skip, run:
uv run --extra outpost pytest tests/test_outpost/
Conftest surface — fixtures + helpers
tests/conftest.py is the shared surface pytest applies across the collection.
The headline wins:
Registered CLI options
--update-baseline→update_baselinefor the deterministic eval fingerprint.--update→update_snapshotsfor SVG regeneration. Registering this in conftest is required because pytest finalizes options before it importstests/test_snapshot.py;task verify-snapshot -- --updatewould otherwise fail during argument parsing.
Fixtures (function-scoped unless noted)
temp_kin_home(tmp_path, monkeypatch)— per-test KIN_HOME override withsettings.reset_cache()so cached lookups from prior tests don’t bleed through. Use this in any test that mutates KIN_HOME.temp_session_dir(tmp_path, monkeypatch)— per-test KIN_SESSION_DIR override. Rare; mainly for tests that exercise persistence paths.fake_backend— a freshFakeBackendwith an empty responder. Pair with themake_responder(...)helper to script turns.session— a realSessionwired with a captured-events list + the FakeBackend. Tests assert on the event stream viasession._captured_events.pilot_app— freshKinAppper scenario, wired with the FakeBackend viaapp.run_test(). Yields(app, pilot, sess, captured_events). CRITICAL gotcha: if you domonkeypatch.delenv("KIN_HOME", raising=False)to clear env keys, it UNDOEStemp_kin_home’smonkeypatch.setenv("KIN_HOME", tmp_path)and the test falls back to~/.kin(the user’s real home). Skip KIN_HOME in any env-clear loop. This bug bit 3 of the original 8 migrations in Phase 2.env_snapshot(monkeypatch)— capture-and-restore helper for tests that mutate env keys._reset_env_between_tests(autouse) — snapshotsos.environat the start of every test, restores it on teardown, AND callskin.harness.settings.reset_cache()so cachedkin_home()lookups from earlier tests don’t bleed through. No opt-out by design — the cost of a surprise env leak is much higher than the cost of an opt-out. Tests that legitimately need to mutate env usemonkeypatch(auto-restoring) orenv_snapshot(manual restore). Cost: ~1ms per test.
Helpers (module-level functions, used inside test bodies)
import_tool_module(name)—importlib.import_module(...)alias. Used to bypasskin.harness.tools.__init__’s tool-instance shadow when monkeypatching tool internals. Modules using it:web_fetch,web_context,_media,workflow, etc.async settle(pilot, predicate, *, tries=80, sleep=0.05)— poll a predicate until truthy. Consolidates the historical integration and live-drive settle helpers.async wait_for_event(events, predicate, *, timeout=2.0)— poll a captured-events list for an event matching the predicate.clean_terminal(text)/clean_interrupted(text)— strip ANSI / control sequences from a Textual screen dump._git_short_sha(cwd=None)— current git short SHA, or “unknown”. Consolidates the historical eval and live-drive helpers.assert_eventually(condition, *, timeout, interval, message)— sync version ofsettle.make_responder(*scripted_turns)— build a FakeBackend responder from a list of(text | tool_call | (text, [tool_calls...]))tuples.
Env-before-import architecture
tests/__init__.py (NOT conftest.py) sets the KIN_HOME / KIN_SESSION_DIR
/ KIN_SPANS env vars at MODULE TOP. This is critical because:
- Several verify scripts (now test modules) do top-level
from kin.harness import presets, settings. settings.kin_home()is read at first import, not lazily.- Pytest imports
tests/__init__.pybefore any test module — guaranteeing the env is correct for every test module’s top-level kin imports.
setdefault (not =) is used so a developer’s explicit env wins for
local debugging. The conftest session fixture does the teardown
(rmtree of the tmpdir).
Do NOT add from kin.harness... at the top of tests/__init__.py
itself; that would defeat the ordering guarantee.
The importlib shadow-bypass idiom
kin.harness.tools.__init__.py re-exports tool INSTANCES, which shadow
the submodule names. from kin.harness.tools import web_fetch gives you
the tool INSTANCE, not the module. Tests that monkeypatch (e.g.
wf._validate_ip, wc.brave_get) need the REAL module object:
# DON'T:
from kin.harness.tools import web_fetch # returns the Tool INSTANCE
wf = web_fetch
wf._validate_ip = lambda ip: None # mutates the tool instance, not the module
# DO:
wf = import_tool_module("kin.harness.tools.web_fetch")
wf._validate_ip = lambda ip: None # mutates the real module
The import_tool_module(name) helper consolidates this idiom in one
place. It also covers kin.harness.tools._media (the media submodule
that tests/test_web.py needs) and kin.harness.tools.workflow (the
workflow executor module that tests/test_workflow.py needs).
Coverage interpretation
Lazy SDKs (openai, anthropic, mcp, playwright, tomlkit, etc.)
are imported inside the code that needs them. Headless tests using
FakeBackend never trigger OpenAIBackend / AnthropicBackend imports,
so the SDK modules show up as “no statements” in the coverage report —
NOT “missing” or “untested”.
Don’t interpret low coverage on a lazy-imported module as a regression.
Use pytest --cov-context=test for per-test context if a specific module
needs diagnosing.
Run task test-cov to see the coverage report:
task test-cov # = uv run pytest tests/ --cov=src --cov-report=term-missing \
# -m "not slow and not live"
There’s NO --cov-fail-under threshold — coverage is visibility, not
enforcement. The lazy-SDK interaction makes a meaningful threshold
premature (most of kin.harness.backends.* would show 0% under headless
tests using FakeBackend).
Adding a new test
- Pick the right home. Headless? →
tests/. Live / smoke / network? →scripts/. - Use the shared fixtures (
temp_kin_home,fake_backend,session,pilot_app) instead of hand-rolled env mutation. - Use
assertinstead ofcheck()— pytest captures failures natively and the test function name becomes the test ID. - Preserve the assertion semantics. Same checks, same expected values — just pytest-shaped.
- Add an
if __name__ == "__main__":guard at the bottom if you want ad-hocpython tests/test_<area>.pydebugging. - If your test is slow or live, mark it so the default
task verifyskips it. Outpost tests belong undertests/test_outpost/; that package’s conftest owns the optional-dependency skip. - Don’t edit
tests/conftest.pyif you’re working in a parallel branch / migration; add the fixture locally in your test file. The shared conftest is the only one that needs the KIN_HOME / settings reset dance, so adding fixtures there is rarely necessary.
Isolation guarantees
The autouse _reset_env_between_tests fixture (see Conftest surface
above) closes the 4 known isolation failures from the migration — they
now pass in the full suite, not just in isolation. The fixture is the
contract: every test starts with a clean os.environ and a fresh
kin.harness.settings._cache. No opt-out by design — the cost of
a surprise env leak is much higher than the cost of an opt-out.
Tests that need to mutate env use monkeypatch (auto-restoring) or
env_snapshot (manual restore). Module-top os.environ[...] = ...
mutations are the original sin: pytest collects modules before
running anything, so module-top mutations persist for the entire
session. The pytest migration removed the ones it found in
test_evals.py; the autouse fixture is the backstop for any that slip
in later.
Per-task pytest invocations
Old verify_*.py |
New pytest task |
|---|---|
verify_settings.py |
task verify-settings (uv run pytest tests/test_settings.py) |
verify_banner.py |
task verify-banner (uv run pytest tests/test_banner.py) |
verify_integration.py |
task verify-integration (uv run pytest tests/test_integration.py) |
verify_workflow.py |
task verify-workflow (uv run pytest tests/test_workflow.py) |
verify_search.py |
task verify-search (uv run pytest tests/test_search.py) |
verify_memory.py |
task verify-memory (uv run pytest tests/test_memory.py) |
verify_provider_presets.py |
task verify-provider-presets (uv run pytest tests/test_provider_presets.py) |
verify_evals.py |
task verify-evals (uv run pytest tests/test_evals.py; add -- --update-baseline only when intentionally re-pinning) |
verify_harness.py |
task verify-harness (uv run pytest tests/test_harness/) |
verify_app.py |
task verify-app (uv run pytest tests/test_app/) |
verify_outpost.py |
uv run --extra outpost pytest tests/test_outpost/ (task verify also collects it when the extra is already installed) |
verify_outpost_live.py |
retired — tests/test_outpost_live.py + task verify-outpost-live tested the ttyd/tmux browser-terminal surface, deleted wholesale in the middle-way plan’s Step 3 ttyd cut (2026-07-06, DR 0033) |
verify_pty.py |
task verify-pty (uv run --group pty pytest tests/test_pty.py -m slow) |
verify_snapshot.py |
task verify-snapshot (uv run pytest tests/test_snapshot.py -m slow); task docs-screenshots-tui regenerates the ten guide-owned test_docs_ targets |
verify_worktrees.py |
task verify-worktrees (uv run pytest tests/test_worktrees.py -m live) |
verify_perf.py |
task verify-perf (uv run pytest tests/test_perf.py -m slow) |
verify_web.py |
task verify-web (uv run pytest tests/test_web.py) |
live_drive_vllm.py |
stays a CLI script; thin pytest wrapper at tests/test_live_drive.py |
live_git_tools.py |
stays a CLI script; KIN_LIVE_GIT_PUBLISH=1 task live-git-tools is an explicitly armed remote publication proof, never part of task ship |
tests/test_docs_audit.py has no historical verify_*.py ancestor. It pins
the bounded STATUS.md contract and combined TUI/browser screenshot graph used
by task docs-audit; unlike rendering either asset type, those read-only checks
are deterministic and belong in the file-grouped hermetic lane. task status-trim removes whole recently-landed records by age and count; it never
leaves continuation lines behind.
verify_harness.py was kept as a backward-compat shell through the
migration (Phases 1-5) so existing cron / muscle-memory invocations kept
working while the pytest files landed; it’s now removed (the pytest
collection is the canonical entry point).