Testing
August 24, 2026 · View on GitHub
Automated (npm test)
npm test (= node --test tests/*.test.mjs) — pure-Node tests, no network, no real dsh, no API key. The glob is left unquoted so the shell expands it: Node 20's test runner does not expand globs and would otherwise look for a literal tests/*.test.mjs file.
args.test.mjs— argv parsing and raw$ARGUMENTSsplitting.state.test.mjs— state dir resolution, job upsert/prune (incl. log-file cleanup), terminal-claim races (single winner), and the SessionEnd-vs-writer concurrency race (session-cleanup-writer.mjsfixture).dsh.test.mjs— headless argv composition, model overlay YAML, mode overlays (minimal disable list + shared bootstrap insert, anchored-standard bootstrap insert without tool disables), structured-output parsing, a fullrunHeadlessAgentround-trip against the fake dsh fixture, binary-resolution order (env → npm-pin / harness / config → PATH), and source-checkout inspection.request-snapshot.test.mjs/tool-bootstrap.test.mjs— assemble/request snapshot reducer and the bootstrap filter (promotion, per-session isolation, assemble-time phase freeze vs persist-then-execute — 0.1.1-rc.2 still this shape — outermost assemble post-transform, pre-step strip, hidden-tooldeny, filter-failure fallback, pre-step +request/headerJSONL recorder against the EpochHeader shape{ config, tools }, which 0.1.1-rc.2 still uses) against a fake Cordis ctx; no real dsh.setup.test.mjs—setupnpm-prefix install + registry SDK-server specs against a fake npm/dsh,--harnesslink of a built checkout (absolute-path SDK-server install), refusal of unbuilt / SDK-less checkouts, migration of pre-npm source configs, npm →--harnessand checkout A → B profile switches, externalDSH_BINARYprofile repair (no npm prefix, including an already-ready profile), stale npm-pin reinstall and failed-refresh retry (CLI +sdkProfileVersionidentitynpm:<pin>/harness:<realpath>), andcheck's source reporting plus stale pin/identity unreadiness (skipped on Node < 22.19, the harness floor).git.test.mjs— review-target resolution (incl. bad--baserefusal), context collection, and the empty-diff vs failed-diff distinction on throwaway git repos.process.test.mjs—terminateProcessTreedeath confirmation (SIGTERM-ignoring child, descendant trees).job-control.test.mjs—stoptarget resolution: terminal refusal, stale reconciliation.stop.test.mjs— bridge-level stop semantics: finished-run refusal (PID-reuse regression), kill+cancel, stale cleanup, and in-flight broker-turn abort.broker.test.mjs— broker session continuity, timeout freeing, concurrent-startup convergence, stale-lock reclaim, socket-ownership rules, and mode-overlay composition (minimal disable list, standard none, anchored-standard bootstrap insert) against the fake SDK runtime.resume.test.mjs— resume continuity plus explicit refusal after broker stop/restart (generation checks),--timeout-msvalidation/forwarding.docs.test.mjs— local Markdown link integrity, required community-health files (including LICENSE and NOTICE), NOTICE license labels, reciprocal English/Chinese entry links, and the public/private documentation ignore boundary.
Fixtures: fake-dsh-fixture.mjs (records argv/env, prints canned output — point DSH_BINARY at a wrapper for it), fake-sdk-runtime.mjs (speaks the SDK wire protocol; prompt directives hang and sleep:<ms> drive timeout tests), ensure-broker-child.mjs and session-cleanup-writer.mjs (child processes for real cross-process races), helpers.mjs (temp dirs, env isolation).
Tests set CLAUDE_PLUGIN_DATA to a per-test temp dir; never let a test touch the real state root.
What automation deliberately does not cover
Real model behavior, profile installation, and the broker's live SDK handshake need a real dsh + DEEPSEEK_API_KEY. Those are manual acceptance, not CI.
Manual acceptance checklist (run against the pinned dsh before release)
In a scratch git repo with the plugin installed:
/dsh:check→ ready (or accurate next steps when deliberately unconfigured)./dsh:reviewon a dirty tree → review text; repo files unmodified (reviews always run the read-only sandbox)./dsh:critique→ parsed findings render (or a graceful "unstructured output" fallback)./dsh:run --background "summarize this repo"→ run id immediately;/dsh:runsshows running → completed;/dsh:showreplays the result./dsh:setup→ cc profile composes; rerun is a no-op./dsh:run --session "create NOTES.md with 3 bullets" --write→ file created, session id in footer./dsh:run --resume "add a 4th bullet"→ same session continues (file grows)./dsh:import→ digest acknowledged;/dsh:run --resumecontinues with the imported context./dsh:stop --broker→ broker gone; a later--resumeerrors explicitly ("no live broker holds it"), and after a new--sessionrun the old session stays unreachable — never a silent fresh session reported as a resume.- Kill Claude Code mid-background-run → worker survives; a new session's
/dsh:runs --allstill finds it. /dsh:run "name every tool you can call"→ the answer names the full toolset (standard default: file/web search, skills, subagents). The same prompt with--mode minimalnames only bash andstr_replace_editor./dsh:run --session "hi"on a fresh workspace, then/dsh:run --session --mode minimal "hi"→ explicit mode-mismatch error naming/dsh:stop --broker; after stopping, the minimal--sessionrun works and/dsh:checkshows the broker's mode.- Wire snapshot for
--mode anchored-standardon a new session (setDSH_CC_SNAPSHOT_FILEand/or read session JSONLrequest/header— do not use--dump-configas a wire test). The JSONL recorder writes afteragent/pre-step(source: "pre-step") and once per step atstep/end(source: "request", carrying that step's wire header — 0.1.1-rc.2 still emitsrequest/headeronly when the header changes, so a per-event line would skip every steady-state step). Assemble-time emptycontextSourceKindsis not a pass — those injections happen at pre-step. 0.1.1-rc.2 still storesmodel/maxTokens/reasoningEffortonevent.data.header.config(EpochHeader), not on the header root;header.toolsis the wire catalog.- First
pre-step/requestline:systemTextsis the one RL sentence (You are a helpful software engineer assistant.),contextSourceKindsis empty,toolNamesisbash+str_replace_editor, and therequestline has non-nullmodel/maxTokens/reasoningEffort. - After the first tool call, the next
pre-stepandrequestlines show the full dsh-base catalog — not the previous header's two tools. - After a text-only first assistant reply (typical of
/dsh:import), the next user turn is already promoted. --resumeon that session stays promoted (no re-anchor).
- First
- Two concurrent
--sessionruns in the same broker with--mode anchored-standard(different sessions): promoting A must not widen B's first request. --mode minimalon the same "name every tool you can call" prompt must not promote (still two tools after a reply).--mode standardstill names the full set from request #1. WithDSH_CC_SNAPSHOT_FILEset, a multi-step minimal run writes onepre-stepand onerequestline per step, every one of them naming exactly the pair.- Boot smoke first, before anything about catalogs:
/dsh:run "reply OK"under each of--mode minimaland--mode anchored-standard, one-shot and--session. Both modes inserttool-bootstrap.mjs, so a mount-time mistake in it is a boot failure —dsh: plugin tree failed to load: … cc-tool-bootstrap …before request #1 — and it takes review, critique, and every session with it. Item 17's--dump-configprobe cannot see this (see below).
- Boot smoke first, before anything about catalogs:
- Read-only sandbox still blocks writes under
anchored-standardin both phases (bootstrap pair and post-promotion catalog). - Loader probe (composition, not wire, and not execution):
dsh --profile headless --patch <generated mode-anchored-standard.yml> --dump-configmust compose. If the insertnameabsolute path fails, the overlay already copiestool-bootstrap.mjsinto that run'soverlays/— retry that copied path before consideringdsh plugin add. This probe resolves the row; it never calls the plugin'sapply(). Verified: an inserted plugin whoseapply()throws on the first line still exits 0 here. Only item 15's boot smoke covers mount-time failures.
Record the dsh version used at the top of the release notes; it must match the dsh-compat.md pin.