test/live/) drives Comis end-to-end through the
real channel adapter — the production Telegram adapter, unmodified — against an isolated,
$0, fully offline daemon. It exists to exercise the surfaces the chat-API test path cannot
reach: the delivery layer, inbound media routing, callbacks and edits, the adapter resilience
fallbacks, group/forum addressing, and reactions as the Verified-Learning outcome signal.
The integration relies on a single seam already present in the product — the channel adapter’s
apiRoot / baseUrl redirect — so the harness points the real adapter at an in-process
emulator instead of the live Bot API. No production code changes are needed to run it; the
framework, the emulator, the rig, and the chan/tg CLI all live under the test tree.
This is a developer / contributor tool, not an operator feature. Nothing here ships in the
published
comisai package — the harness is forbidden from bundledDependencies and is never a
comis CLI subcommand (see the harness never ships, below). Everything it
touches in @comis/* is consumed from each package’s built dist/, so run pnpm build
first — a stale dist/ silently masks src/ changes.Anatomy
startRig / buildRig path (a vitest
scenario boots the emulator and daemon inside the test process — the certified default), and the
chan/tg CLI (a thin client that talks to a running rig over its recorded endpoints). The CLI
also drives a detached rig shell-only and unattended across separate processes — see
Driving from a cold shell.
The in-process path: startRig / buildRig
The strongest, most deterministic path. A scenario boots the emulator + an isolated daemon
in-process, drives it, and asserts against ground truth — no separate shell, no human step.
Stage-B vs Stage-C
Every scenario splits onconst isLive = !!process.env["COMIS_LIVE"]:
- Stage-B (always runs, in-process, no model) — the deterministic wiring proof. Drives the real exported product function or the bare adapter against the emulator-built shape: group/forum mapping, the General-Topic thread asymmetry, the four outbound fallbacks under fault injection, error classification, Tier-3 platform actions, the forum-service negative. CI-able, offline, needs no provider keys.
- Stage-C (
describe.skipIf(!isLive), operator-gated) — the agent-content legs against a real keyless model onlocalhost:11434(ollama). A full-daemon reply, the Verified-Learning A-then-B reaction loop, a DAG pipeline, the injection-gauntlet sweep. An operator runs these withCOMIS_LIVE=1; offline they skip, never fail.
The chan / tg CLI
chan is the standalone shell/agent driver for the channel surface; tg is the alias for
chan --channel telegram. It is a thin tsx client over the three surfaces a running rig
records in its handle file: the emulator /control/* API (the drive verbs), the gateway
/rpc over WebSocket (RPC passthrough + the curated explain / fleet + trigger), and the
rig lifecycle. Invoke it through tsx (it is not a comis subcommand):
--json and exits non-zero, reason-coded on a no-reply timeout, an RPC error, a dead
handle, or malformed JSON. A no-reply is an honest empty body, never a fabricated success.
Verbs
Pass
--agent <id> to target a non-default agent (for tg trigger), --endpoint <url> to
target a specific rig, and --json for a machine-readable body.
The dual-oracle workflow
The harness corroborates every assertion against two independent oracles, so a passing test cannot be a false green:- The channel oracle — the emulator’s recorded wire bytes (what the adapter actually
sent:
tg last/emu.outbound(chat)). - The Comis oracle — the daemon’s own record of what it delivered: the
delivery_mirrorrow, the session trajectory, and the isolatedmemory.db(tg mirror/tg traj/tg db).
waitForReply in-process, or tg wait from the shell), the HARD
cross-check asserts the channel oracle’s text equals delivery_mirror.text
(assertChannelTrace). A disagreement is a real bug, not flake.
The webhook flag
The emulator supports a webhook-POST mode: instead of queuing an inboundUpdate for the
adapter’s getUpdates long-poll, it POSTs the grammy Update to a configured webhookUrl
carrying the X-Telegram-Bot-Api-Secret-Token header. The scenario asserts the harness-side
secret-token gate (a wrong or absent token is rejected) plus the product’s boot-time secret
format validator and the polling-off switch (shouldUseRunner returns false when a
webhookUrl is set).
Driving from a cold shell (shell-only, unattended)
Two autonomy paths, both certified: The in-process (vitest) path is the certified default — a scenario boots, drives, and
tears down the rig with zero human steps, exactly as the acceptance suites run. The chan/tg
CLI’s drive verbs (send, react, rpc, trigger, wait, last, db, …) work against
a running rig from any shell.
The cold-shell (detached) path drives the rig shell-only, unattended, across separate
processes — start it with tg up --detached:
tg up --detached spawns a detached-subprocess rig (harness/rig-daemon.ts) that survives
the launching process. It runs its own emulator + a real Comis daemon (booted as a grandchild
from packages/daemon/dist/daemon.js, the production entrypoint) + a loopback-only,
token-owner-checked rig-control HTTP surface, and records a handle carrying a real pid
and a dedicated rigControlEndpoint (≠ the gateway). A separate-shell tg send / tg rpc
reaches the surviving rig via that handle. The cold-shell lifecycle verbs drive it for real:
tg down SIGTERMs the rig’s process group (the rig-daemon and its daemon grandchild) and
confirms the process is gone before returning; tg restart / tg reset --deep /
tg reconfigure POST the owner-checked rig-control endpoint. Orphan reaping: the rig
self-terminates (reaping its daemon) when its handle file disappears, so a tg down or a
stray rm of the handle never leaves a zombie daemon.
The “shell-only, unattended” claim is backed by a green cross-process acceptance test
(
scenarios/channels/telegram-cold-shell.test.ts, Stage-C): a real tg up --detached (process 1)
→ tg status / tg rpc / tg send (process 2) → tg down (process 3) sequence runs across
separate OS processes, asserting the rig OUTLIVES tg up, a fresh process reaches it, and
tg down leaves no leaked daemon/port (the pid is gone, the gateway port is free). A false
success is a hard fail. Without --detached, tg up boots an in-process rig (the certified
default) that the lifecycle verbs honestly reason-code as lifecycle_in_process_only /
down_not_owned_in_process from a cold shell — never a faked cross-process success.The harness never ships
The harness is a contributor tool and is forbidden from the published package:- It must never enter
bundledDependencies, and adds no@comis/*dependency edge. chan/tgis a standalonetsxentry undertest/live/bin/, never acomisCLI subcommand — there is no.command("chan")/.command("tg")registration to forward-protect those names.- There is no
package.jsonundertest/live/**.
Related
Custom Adapters
Build a channel adapter from the ChannelPort interface
Data Directory
The
~/.comis-chanlive/<channel>.json handle file the rig writesArchitecture
Port interfaces and the hexagonal pattern
CLI Reference
The operator
comis CLI (distinct from the harness chan/tg)