feat(supervision): add choice watcher daemon, HTTPS spec docs, and test suites

- bin/muse_choice_watcher.py + systemd/muse-choices-reconcile.*: automatic choice answering and timer reconciliation
- bin/digest.py: fleet log and health summarization
- docs/BOX-*-HTTPS.md: comprehensive HTTPS execution contracts and API documentation
- docs/MUSE-CHOICES-POLICY.md & docs/SUPERVISION-SPEC.md: autonomous execution specs
- tests/test_*.py: unit test suites for HTTPS API, choice watcher, fleet heal, and swarm pruning
This commit is contained in:
operator
2026-10-07 00:25:46 +00:00
parent 04339bad14
commit 0065d11e97
29 changed files with 6419 additions and 7 deletions
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""
Side-chat work digest — INTEGRATOR minimal version (agent 4 of 5 never
delivered its digest/throttle design within the window).
Purpose: stop the per-message ✅ COMPLETED relay flood. COMPLETED hits are
batched here and emitted as ONE periodic digest instead of N main-chat
messages. ALERT / BLOCKER / DECISION still relay individually via
siphon() — urgency is never batched.
Interface (what agent 4's full design should remain compatible with):
- buffer = DigestBuffer(max_items=20, max_age_s=3600)
- buffer.add(hit) -> None
- buffer.flush() -> Optional[str] (formatted digest, clears buffer)
- flush_digest() -> Optional[str] (module-level singleton convenience)
Reversible: to restore per-message COMPLETED relays, route COMPLETED back
through siphon() in monitor.py and ignore this module.
"""
import time
from typing import List, Optional
try:
from detect import SiphonHit
except ImportError: # pragma: no cover
SiphonHit = object
class DigestBuffer:
"""Batch COMPLETED hits; flush() renders one digest message."""
def __init__(self, max_items: int = 20, max_age_s: int = 3600):
self.max_items = max_items
self.max_age_s = max_age_s
self._items: List[tuple] = [] # (ts, SiphonHit)
def add(self, hit) -> None:
now = time.time()
# Prune items older than max_age_s on every add (bounded memory).
self._items = [(ts, h) for ts, h in self._items
if now - ts < self.max_age_s]
self._items.append((now, hit))
# Bound the buffer; oldest evicted first.
self._items = self._items[-self.max_items:]
def __len__(self) -> int:
return len(self._items)
def flush(self) -> Optional[str]:
"""Render and clear. Returns None when there's nothing to digest."""
if not self._items:
return None
lines = ["📦 [DIGEST] completions from side chats "
f"({len(self._items)} item(s))"]
for _, hit in self._items:
author = getattr(hit, "author", "") or "?"
url = f"https://muse.ai/thread/{hit.thread_id}"
lines.append(f"• {hit.summary} — {author} ({url})")
self._items = []
return "\n".join(lines)
# Module-level singleton: the monitor loop shares one buffer per process.
_default_buffer = DigestBuffer()
def get_buffer() -> DigestBuffer:
return _default_buffer
def flush_digest() -> Optional[str]:
"""Flush the process-wide digest buffer. None if empty."""
return _default_buffer.flush()
+1456
View File
File diff suppressed because it is too large Load Diff
+152
View File
@@ -0,0 +1,152 @@
# Box Read-Only Lookups over HTTPS (Agent Access, No SSH)
> **Box is the main surface.** All operator work goes through Box (box.muse-dev.online). The web UI, `box` CLI, and agents share the same API endpoints. No UI-only powers.
**Date:** 2026-10-06
**Status:** bl side implemented; VM board REST below is specified, not yet implemented
**Scope:** read-only lookups only (fleet, threads, unread, dm log). Mutations stay on existing paths.
## 1. Problem
Agents in containers reach box over a 2-hop SSH chain (container → VM → bl).
SSH toggles lapse and every agent needs the full chain configured. Agents need
the daily read lookups — "latest from each agent" — over HTTPS with no secrets
on the wire.
## 2. What exists now (bl side, implemented)
Two agent HTTPS paths already serve reads; both use the same signature auth
(`ssh-keygen -Y sign`, namespace per server, ±300s clock skew, nonce replay
cache) and per-agent principals from `dm-signers/allowed_signers`:
| Path | Server | Client | Auth namespace |
|---|---|---|---|
| Named ops (works today) | `bin/exec-constrained.py` via `https://exec.muse-dev.online/exec` | `bin/exec-sign.sh <op> '<args>'` or `bin/box-relay.sh` (served at `GET /box`) | `exec-constrained` |
| Typed REST (specified below) | VM board `/srv/board/server.py` | any HTTPS client | `box-api` |
New named ops (this change, all `side_effecting: false`, all in `DEFAULT_PERMS`
so any valid fleet signer may call them):
- `fleet.unread` `{"agent"?}` → `box-ctl.py unread [--agent X]`
- `dm.log` `{"limit"?, "agent"?}` (limit 1..100, default 20) → `box-ctl.py dm-log [limit] [--agent X]`
Already present and unchanged: `health.check` (fleet status), `thread.list`,
`thread.view`, `dm.read`, `chat.messages`.
New `box-relay.sh` client commands (this change):
```bash
box unread [<agent>] # fleet unread/activity counts
box dm log [<limit=20>] [--agent <agent>]
```
New `box-ctl.py` backend verbs (this change; also callable over the board's
existing SSH bridge until the board speaks REST):
```bash
box-ctl.py unread [--agent <agent>]
box-ctl.py dm-log [limit] [--agent <agent>] # back-compat: bare [limit] unchanged
```
Also fixed: `box lookup unread` / `muse unread` previously always failed with
"Unknown lookup target 'unread'" (`_lookup_unreads` was never wired into
`cmd_lookup`); it now works and supports `--json`.
## 3. VM board REST (to implement on the VM)
Base: `https://box.muse-dev.online/api/box`. All endpoints require
agent-signature auth (§4) or the existing `ops_session` cookie (humans).
```text
GET /api/box/fleet exists today; keep behavior
GET /api/box/threads?agent=X backend: box-ctl.py thread-list --agent X (pass JSON through)
GET /api/box/unread?agent=X backend: box-ctl.py unread [--agent X]
GET /api/box/dm/log?limit=N&agent=X
backend: box-ctl.py dm-log [N] [--agent X]
```
### Agent scoping (server-enforced)
- Verified identity `operator-X` or `X` (X in `muse,pip,646,opm,def,dev`)
may only read slices for X. The board MUST pass `--agent X` to box-ctl and
MUST NOT accept a different `agent=` query value from that identity.
- `ops_session` (human PIN login) may omit `agent=` and read the full fleet.
- Unknown/expired signatures → `401`. Authenticated but out-of-scope → `403`.
### Response schemas (bl verbs pass through unchanged)
`GET /api/box/unread`:
```json
{"ok": true, "nodes": [
{"node": "muse", "unread": 2, "approval_pending": false,
"title": "muse (2)", "thread": "abc123-uuid-or-null"}
]}
```
`GET /api/box/dm/log` (agent filter matches entries from OR to the agent):
```json
{"ok": true,
"entries": [{"type": "sent", "id": "bdf7beb6", "agent": "opm",
"to": "pip", "target": "pip tasks",
"ts": "2026-10-06T05:56:01.328629+00:00"}],
"dms": ["... same array, legacy key ..."]}
```
`GET /api/box/fleet`: existing `{"ok": true, "fleet": [...]}` shape, unchanged.
Errors follow `docs/BOX-API-DESIGN-DMS.md` §3.1 (`{"ok": false, "code", "error"}`).
## 4. Agent-signature auth for the REST endpoints
Same identity primitive as signed DMs and `exec-constrained.py`; a signature
is not a secret, so agents can sign without handling credentials.
1. Client builds the canonical string (LF-separated, no trailing newline):
```text
{METHOD}\n{PATH}\n{SORTED_QUERY}\n{TS}\n{NONCE}
```
- `METHOD`: `GET`; `PATH`: e.g. `/api/box/dm/log`; `SORTED_QUERY`: raw
query string sorted by key (`agent=opm&limit=5`), empty string when none.
- `TS`: unix epoch seconds; `NONCE`: 16–128 hex chars, single use.
2. Client signs it: `ssh-keygen -Y sign -f <key> -n box-api`.
3. Client sends headers (armor is base64-encoded to stay header-safe):
```text
X-Box-Identity: operator-646
X-Box-Timestamp: 1728...
X-Box-Nonce: <hex>
X-Box-Signature: <base64 of the -----BEGIN SSH SIGNATURE----- armor>
```
4. Server recomputes the canonical string from the received request, base64-
decodes the signature, and runs `ssh-keygen -Y verify -f allowed_signers
-I <identity> -n box-api -s <sigfile>` with the canonical string on stdin.
Accept only if: verify exit 0, `|now-TS| ≤ 300`, nonce unseen (cache ≥600s).
Signers file is synced from bl `dm-signers/allowed_signers`.
Example:
```bash
TS=$(date +%s); NONCE=$(python3 -c "import secrets; print(secrets.token_hex(16))")
CANON=$(printf 'GET\n/api/box/dm/log\nagent=opm&limit=5\n%s\n%s' "$TS" "$NONCE")
SIG=$(printf '%s' "$CANON" | ssh-keygen -Y sign -f ~/.ssh/id_frontdoor -n box-api \
| base64 -w0)
curl -s 'https://box.muse-dev.online/api/box/dm/log?agent=opm&limit=5' \
-H "X-Box-Identity: operator-646" -H "X-Box-Timestamp: $TS" \
-H "X-Box-Nonce: $NONCE" -H "X-Box-Signature: $SIG"
```
## 5. Rollout notes
- `exec-constrained.py` reads `OPS` at startup: restart the service after
deploying for `fleet.unread` / `dm.log` to appear in `GET /ops`.
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to get
`unread` / `dm log`.
- Until the VM board implements §3, agents use the named-ops path (§2),
which needs no SSH today.
- Non-goals: write endpoints (`dm.send` etc. stay on the ops path for now),
PIN/human flows (unchanged), secret handling (no secrets cross the wire).
+92
View File
@@ -0,0 +1,92 @@
# Box Approvals over HTTPS (No SSH)
> **Box is the main surface.** All operator work goes through Box (box.muse-dev.online). The web UI, `box` CLI, and agents share the same API endpoints. No UI-only powers.
**Date:** 2026-10-06
**Status:** implemented on bl (`exec-constrained.py` + `box-relay.sh`;
`box-ctl.py` verbs pre-existed, plus fast node validation and
`quality-validate` branches; `approvals.py` untouched)
**Scope:** approval visibility (check) + governed decisions (deny, auto,
one-shot allow). Persistent/forced allow (`--always`/`--force`) stays
SSH-only.
## 1. Why
Agents blocked on browser approvals needed SSH to see fleet approval
state, deny a bad prompt, auto-resolve trusted prompts, or allow a
known-good one. All of this now rides the agent HTTPS path
(`https://exec.muse-dev.online/exec`, signature or Bearer [REDACTED], named-op
allowlist, audit log).
## 2. New ops
| Op | Args | Backend | Access |
|---|---|---|---|
| `approval.check` | `{node?}` (default fleet) | `box-ctl.py approval-check` | read-only, in `DEFAULT_PERMS` |
| `approval.deny` | `{node!, message!, allow_main_chat?}` | `box-ctl.py approval-deny` | known-identities-only |
| `approval.auto` | `{node?}` (default fleet) | `box-ctl.py approval-auto` | known-identities-only |
| `approval.allow` | `{node!, message!, allow_main_chat?}` | `box-ctl.py approval-allow` (one-shot) | known-identities-only |
New `box-relay.sh` client commands:
```bash
box approvals check [node]
box approvals allow <node> --message <text> [--allow-main-chat]
box approvals deny <node> --message <text> [--allow-main-chat]
box approvals auto [node]
```
Raw op calls (signature auth, no token):
```bash
exec-sign.sh approval.check '{}'
exec-sign.sh approval.check '{"node": "646"}'
exec-sign.sh approval.allow '{"node": "646", "message": "trusted deploy script"}'
exec-sign.sh approval.auto '{"node": "opm"}'
```
## 3. What the decisions do
- `approval.allow` clicks Allow **once** on the node's active prompt.
There is deliberately no remote `--always` (persistent site allow)
or `--force`.
- `approval.deny` clicks Deny on the node's active prompt.
- `approval.auto` scans (fleet or one node) and allows only TRUSTED
non-key prompts. Key/passkey approvals are never auto-approved;
they need an explicit allow/deny, which notifies the waiting agent.
- All three are audited with identity + op + node.
## 4. Safety notes (same posture as existing ops)
- **Attribution is mandatory.** The allow/deny flows DM the waiting
agent, historically with an `[operator]` prefix. A remote caller is
an agent, not the operator — so the exec layer **requires** a
non-empty `message` (≤2000 chars, no controls) on both allow and
deny. (`box-ctl.py` still permits omitting `--message` for SSH
callers; the HTTPS layer is the narrower gate.)
- **One-shot only.** The allow argv never carries `--always` or
`--force`; the validators reject those keys. Persistence stays an
SSH-side decision.
- **Sidechat-first.** `allow_main_chat` defaults to false; Main Chat
delivery needs the explicit flag, same as `notify`/`dm.ack`.
- **Fixed argv, validated values.** Nodes must be fleet members (fast
`BAD_NODE` before any CDP probe — `approval-check`/`approval-auto`
gained the same node check allow/deny already had); unknown arg
keys rejected.
- **Timeouts.** Check 180s (fleet CDP scan), auto 300s (scan plus one
click per trusted prompt), allow/deny 120s.
- **Retry-safe reads.** `approval-check` / `approval-list` joined
`IDEMPOTENT_ACTIONS`; all six approval verbs have
`quality-validate` dry-run branches.
## 5. Rollout notes
- Restart `exec-constrained.py` after deploy for the 4 new ops to
appear in `GET /ops` (repo total becomes 83).
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to
get the `approvals` group.
- While fleet browsers crash-loop, decision ops fail honestly
(`APPROVAL_FAILED` / CDP errors) instead of hanging; check still
reports per-node state including `UNREACHABLE`.
- Non-goals: deletes, `main-loop` enable/disable, policy writes,
swarm kill/prune — future expansions, same pattern.
+80
View File
@@ -0,0 +1,80 @@
# Box Dev + Comms over HTTPS (No SSH)
> **Box is the main surface.** All operator work goes through Box (box.muse-dev.online). The web UI, `box` CLI, and agents share the same API endpoints. No UI-only powers.
**Date:** 2026-10-06
**Status:** implemented on bl (`exec-constrained.py` + `box-ctl.py` + `box-relay.sh`)
**Scope:** git visibility, test runs, notify, work-order acks. Read-only lookups
live in `docs/BOX-API-READ-HTTPS.md`.
## 1. Why
Agents developing the box must inspect the tree, run the suite, nudge peers,
and acknowledge work orders without SSH. All of this now rides the existing
agent HTTPS path (`https://exec.muse-dev.online/exec`, signature or bearer
auth, named-op allowlist, audit log) — no new trust model.
## 2. New ops
| Op | Args | Backend | Write? | Who |
|---|---|---|---|---|
| `git.status` | `{}` | `box-ctl.py git-status` | no | any valid signer |
| `git.diff` | `{path?, stat?}` | `box-ctl.py git-diff [--stat] [--path p]` | no | any valid signer |
| `git.log` | `{limit?, path?}` (1..50, default 10) | `box-ctl.py git-log` | no | any valid signer |
| `tests.run` | `{test?, filter?}` (`tests.<module>` or full suite; `filter` is unittest `-k`) | `box-ctl.py tests-run [module] [--filter p]` | yes (executes) | known identities only |
| `notify.send` | `{agent, message≤1000, sidechat?, sender?}` | `box-ctl.py notify ...` | yes (sends DM) | known identities only |
| `dm.ack` | `{id, to, sender!, sidechat?, allow_main_chat?}` | `box-ctl.py ack ...` | yes (sends DM) | known identities only |
New `box-ctl.py` verbs: `git-status`, `git-diff`, `git-log`, `tests-run`,
`ack` (all in `USAGE`, `quality-validate`, and — for the git reads —
`IDEMPOTENT_ACTIONS`).
New `box-relay.sh` client commands:
```bash
box git status
box git diff [--stat] [--path <path>] # path also accepted positionally
box git log [<limit=10>] [--path <path>]
box tests run [tests.<module>] [--filter <pattern>] # full suite (~2-3 min) when omitted; filter is -k
box notify <agent> [--sidechat <n>] [--sender <a>] <message...>
box dm ack <id> --to <agent> --sender <agent> [--sidechat <name>]
```
Raw op call (signature auth, no token):
```bash
exec-sign.sh git.log '{"limit": 5, "path": "bin/dm.py"}'
exec-sign.sh tests.run '{"test": "tests.test_box_dev_https"}'
exec-sign.sh dm.ack '{"id": "bdf7beb6", "to": "pip", "sender": "opm"}'
```
## 3. Safety notes (same posture as existing ops)
- **Fixed argv, validated values.** Clients influence only whitelisted argument
values. Git paths must be repo-relative without `..` (plus symlink-escape
check in box-ctl); test modules must match `^tests\.[a-z0-9_]+$` and exist;
ack ids must be 6–64 hex; notify messages ≤1000 chars.
- **Caps.** `git diff` output capped at 64KB, status at 200 entries, test
output at 32KB tail; every capped response carries `truncated: true`.
- **Sidechat-first.** `notify.send` and `dm.ack` default to the recipient's
sidechat and never touch Main Chat unless `allow_main_chat` is set —
mirroring `box-ctl.py notify` and the WO dispatcher.
- **Attribution.** `sender` is caller-asserted (validated ∈ fleet agents),
same as the existing `dm.send` op; the HTTPS identity is recorded
separately in the exec audit log. `dm.ack` requires an explicit sender —
no silent default.
- **tests.run executes repo code** (whatever is in `tests/`), so it is
`side_effecting`, excluded from the read-only default permission subset,
and capped at a 600s timeout. Test failures report as
`{"ok": false, "returncode", "output"}` — the op itself succeeded.
- New `box-ctl.py` fail codes `GIT_ERROR` / `TESTS_ERROR` are registered in
`KNOWN_ERROR_CODES`, so `quality-check` stays at its baseline.
## 4. Rollout notes
- Restart `exec-constrained.py` after deploy for the new ops to appear in
`GET /ops`.
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to get
`git` / `tests` / `notify` / `dm ack`.
- Non-goals: git commit/push, service restarts for box itself, live
streaming tails — future expansions, same pattern.
+88
View File
@@ -0,0 +1,88 @@
# Box Job Lifecycle over HTTPS (No SSH)
> **Box is the main surface.** All operator work goes through Box (box.muse-dev.online). The web UI, `box` CLI, and agents share the same API endpoints. No UI-only powers.
**Date:** 2026-10-06
**Status:** implemented on bl (`exec-constrained.py` + `box-relay.sh`;
`box-ctl.py` verbs pre-existed)
**Scope:** safe job-lifecycle mutations. Deletes are deliberately NOT
exposed (`job-delete`, `timer-delete` stay SSH/operator-only).
## 1. Why
Agents own scheduled automation but could only run jobs (`cron.run`) or read
them (`box.exec`). Creating, updating, triggering, chaining, previewing, and
pausing jobs needed SSH. All of this now rides the agent HTTPS path
(`https://exec.muse-dev.online/exec`, signature or bearer auth, named-op
allowlist, audit log).
## 2. New ops
| Op | Args | Backend | Write? | Who |
|---|---|---|---|---|
| `job.put` | `{name, definition}` | `box-ctl.py job-put` (definition on stdin) | yes (writes + commits) | known identities only |
| `job.trigger` | `{name}` | `box-ctl.py job-trigger` | yes (dispatches now) | known identities only |
| `job.chain` | `{from, to, on_failure?}` | `box-ctl.py job-chain` | yes (writes + commits) | known identities only |
| `job.next` | `{job_id, success?}` | `box-ctl.py job-next` | no (dry-run) | any valid signer |
| `cron.timer_stop` | `{name}` | `box-ctl.py timer-stop` | yes (systemd) | known identities only |
| `cron.timer_disable` | `{name}` | `box-ctl.py timer-disable` | yes (systemd) | known identities only |
New `box-relay.sh` client commands:
```bash
box cron put <name> '<json-definition>' # create or update (see §3)
box cron trigger <name> # dispatch now (audited JSON)
box cron chain <from> <to> [--on-failure] # wire chain_next
box cron next <job-id> [--success|--fail] # dry-run: what dispatches next
box timer stop <name> # pause schedule (keeps unit)
box timer disable <name> # pause schedule (disables unit)
```
Raw op call (signature auth, no token):
```bash
exec-sign.sh job.next '{"job_id": "heartbeat-20200101-000000-deadbeef"}'
exec-sign.sh job.chain '{"from": "ops-audit-step2", "to": "ops-audit-step3"}'
```
Notes:
- `job.trigger` vs existing `job.run`: `job.run` shells straight to
`job-dispatch.py` and relays raw output; `job.trigger` goes through
`box-ctl.py` (existence check, 300s bound, audit trail, JSON contract).
Prefer `job.trigger` for agent-driven dispatches.
- `job.next` job ids look like `<name>-YYYYMMDD-HHMMSS-<8hex>`; with no
`success` flag the box infers it from the last recorded result.
## 3. Job definition shape (`job.put`)
The full schema is enforced by `box-ctl.py validate_job` (single copy);
required fields: `name` (must match the argv name), `schedule` (`manual`
or convertible cron), `agent` (fleet member), `prompt_template` (1–4000
chars, no protocol literals, known `{placeholders}` only). `timeout`
60–3600s, `on_failure` policy, optional `chain_next` (must exist) and
`sidechat` / `dm_target` routing. `box-ctl.py` writes `jobs/<name>.json`
and commits (`Add/Update job <name> via box-ctl`).
## 4. Safety notes (same posture as existing ops)
- **Fixed argv, validated values.** Job names match
`^[a-z0-9][a-z0-9-]{0,63}$`; trigger/chain/timer ops require the job
file to exist; chain rejects self-links and cycles; timer ops require
the unit to exist. All checks run before any side effect.
- **Stdin plumbing.** `job.put` is the first op to pipe a request body to
`box-ctl.py` stdin (the raw definition, not the envelope); the routing
lives in one helper (`_stdin_body`) covered by unit tests.
- **No deletes, no kills.** `job-delete` / `timer-delete` are reachable
only over the operator SSH path, by explicit scope decision.
- **Audited.** Every execution records identity + op + args hash;
`box-ctl.py` additionally audits each mutation with its target.
## 5. Rollout notes
- Restart `exec-constrained.py` after deploy for the new ops to appear in
`GET /ops`.
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to get
the `cron put|trigger|chain|next` and `timer stop|disable` commands.
- Non-goals: deletes, `job-result` ingestion, `strat`/`loop` writes,
approvals — future expansions, same pattern.
+85
View File
@@ -0,0 +1,85 @@
# Box Loop + Strategy Writes over HTTPS (No SSH)
> **Box is the main surface.** All operator work goes through Box (box.muse-dev.online). The web UI, `box` CLI, and agents share the same API endpoints. No UI-only powers.
**Date:** 2026-10-06
**Status:** implemented on bl (`exec-constrained.py` + `box-relay.sh`;
`box-ctl.py` verbs pre-existed, plus a vars-name validator fix)
**Scope:** safe loop/strategy/variable mutations. Reads (`loop-status`,
`loop-health`, `loop-breaks`, `strat-get`, `vars-get`, ...) already ride
`box.exec` or typed read ops and are unchanged here.
## 1. Why
Agents watching loop health could see breaks but needed SSH to remediate
them, resolve stale followups, tune strategy overrides, or undo variable
changes. All of this now rides the agent HTTPS path
(`https://exec.muse-dev.online/exec`, signature or bearer auth, named-op
allowlist, audit log).
## 2. New ops (all known-identities-only, none in the read-only subset)
| Op | Args | Backend |
|---|---|---|
| `loop.remediate` | `{dry_run?}` (default false) | `box-ctl.py loop-remediate [--dry-run]` |
| `loop.resolve` | `{dm_id, note?}` | `box-ctl.py loop-resolve` |
| `strat.set` | `{type!, subtype?, agent?, track?, priority?, timeout_s?, nudges?, escalate?}` | `box-ctl.py strat-set` (payload as argv JSON) |
| `strat.reset` | `{type!, subtype?, agent?}` | `box-ctl.py strat-reset` |
| `vars.reset` | `{name}` | `box-ctl.py vars-reset` (restore default) |
| `vars.rollback` | `{name, revision?}` (int step or timestamp) | `box-ctl.py vars-rollback` |
New `box-relay.sh` client commands:
```bash
box loop remediate [--dry-run]
box loop resolve <dm_id> [note...]
box strat set <type> [--subtype S] [--agent A] [--track true|false] [--priority p] [--timeout N] [--nudges N] [--escalate E]
box strat reset <type> [subtype] [--agent <agent>]
box vars reset <name>
box vars rollback <name> [revision]
```
Raw op call (signature auth, no token):
```bash
exec-sign.sh loop.remediate '{"dry_run": true}'
exec-sign.sh strat.set '{"type": "job", "priority": "important", "nudges": 3}'
```
## 3. What remediate does (non-dry)
`loop.remediate` delegates to `gravity.remediate_breaks`: resolves
followups already answered in logs, re-arms expired followups with nudges
remaining (runs the followup sweeper once), auto-allows TRUSTED (non-key)
browser approvals, and on hard breaks appends a `hard_break_alert` to
job-log plus one DM to opm. Use `{"dry_run": true}` first to preview the
`remediated` / `escalated` lists with zero side effects.
## 4. Safety notes (same posture as existing ops)
- **Fixed argv, validated values.** Strategy types are a strict enum
(`wake|job|siphon|manual|health|heartbeat`) — the backend silently maps
typos to MANUAL, so the op rejects them instead. Priorities are a
strict enum; timeouts/nudges must be integers (backend clamps);
dm ids must be 6–64 hex; variable names mirror the engine identifier
rule. All checks run before any side effect.
- **Stdin-free.** Unlike `job.put`, `strat.set` passes its JSON payload
as an argv token (`strat-set <type> [JSON]`), so no new stdin plumbing
was needed.
- **Vars-name validator fix.** `quality-validate` for all five vars
verbs used the job-name regex (`^[a-z0-9-]{1,64}$`), rejecting every
real variable name (`max_nudge_count`, ...). They now share
`qv_var_name` (`^[A-Za-z0-9_.-]{1,64}$`), mirroring the
`exec-constrained.py` rule.
- **Audited.** Every execution records identity + op + args hash;
`box-ctl.py` additionally audits each mutation with its target.
## 5. Rollout notes
- Restart `exec-constrained.py` after deploy for the new ops to appear in
`GET /ops`.
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to get
the `loop` / `strat` groups and `vars reset|rollback`.
- Non-goals: approvals, deletes, `main-loop` enable/disable — future
expansions, same pattern. (md drive files shipped separately; see
BOX-MD-HTTPS.md.)
+109
View File
@@ -0,0 +1,109 @@
# Box Md Drive Files over HTTPS (No SSH)
> **Box is the main surface.** All operator work goes through Box (box.muse-dev.online). The web UI, `box` CLI, and agents share the same API endpoints. No UI-only powers.
**Date:** 2026-10-06
**Status:** implemented on bl (`exec-constrained.py` + `box-relay.sh`;
`box-ctl.py` verbs pre-existed, plus traversal hardening, output caps,
`--stdin` content plumbing, and hyphenated amend/append/pull aliases)
**Scope:** md reads (audit/list/read/diff) + governed writes
(amend/append/pull/inject-drive/sync-all). Raw container writes
(`md-write`) stay SSH-only by design.
## 1. Why
Agents shaping fleet behavior could see drive scores but needed SSH to
read an agent's `SOUL.md`, diff it against the canonical template, or
push updated operator files. All of this now rides the agent HTTPS path
(`https://exec.muse-dev.online/exec`, signature or Bearer [REDACTED], named-op
allowlist, audit log).
## 2. New ops
Reads (all `side_effecting: false`, all in `DEFAULT_PERMS`):
| Op | Args | Backend |
|---|---|---|
| `md.audit` | `{accounts?}` (default all) | `box-ctl.py md-audit [accounts...]` |
| `md.list` | `{account!, path?}` | `box-ctl.py md-list` (capped, see §4) |
| `md.read` | `{account!, filename!}` | `box-ctl.py md-read` (capped, see §4) |
| `md.diff` | `{account!, filename!}` (shared template only) | `box-ctl.py md-diff` (capped, see §4) |
Governed writes (all known-identities-only, none in the read-only subset):
| Op | Args | Backend |
|---|---|---|
| `md.pull` | `{account!, filename!}` (shared template only) | `box-ctl.py md-pull` |
| `md.inject_drive` | `{account!}` | `box-ctl.py md-inject-drive` |
| `md.sync_all` | `{}` | `box-ctl.py md-sync-all` |
| `md.amend` | `{filename!, content!, author?, reason?}` | `box-ctl.py md-amend --stdin` (content on stdin) |
| `md.append` | `{filename!, text!, author?, section?}` | `box-ctl.py md-append --stdin` (text on stdin) |
New `box-relay.sh` client commands:
```bash
box md audit [accounts...]
box md list <account> [path]
box md read <account> <filename>
box md diff <account> <filename>
box md pull <account> <filename>
box md inject-drive <account>
box md sync-all
box md amend <filename> (--content <text>|--file <path>) [--author <name>] [--reason <why>]
box md append <filename> (--content <text>|--file <path>) [--author <name>] [--section <header>]
```
Raw op calls (signature auth, no token):
```bash
exec-sign.sh md.audit '{"accounts": ["646", "opm"]}'
exec-sign.sh md.read '{"account": "646", "filename": "SOUL.md"}'
exec-sign.sh md.diff '{"account": "pip", "filename": "HEARTBEAT.md"}'
exec-sign.sh md.append '{"filename": "AGENTS.md", "text": "lesson ...", "author": "646"}'
```
## 3. What the governed writes do
- `md.amend` rewrites a `shared/operators/` template after the
drive-safety checks (HEARTBEAT checklist not gutted,
PROACTIVE_PREFERENCES not blanked, SOUL not reverted to stock),
then git-commits it. Full-file content rides stdin (up to 256KB).
- `md.append` appends a timestamped, attributed note (optional section)
via the same validated + committed path (up to 64KB).
- `md.pull` / `md.inject_drive` / `md.sync_all` push canonical
templates *out* to containers; no agent-supplied content crosses.
Injection always overwrites (AGENTS.md preserves remote `## Lessons`).
## 4. Safety notes (same posture as existing ops)
- **Traversal hardening (single-copy in `agent_md.py`).** Account,
filename, and list-path validation now lives in `agent_md.py`
(`MDValidationError`, raised before any gateway call or write);
`box-ctl.py` maps it to `BAD_NAME`, and exec ops + quality-validate
mirror the same shapes. Previously `md-read 646 ../x` reached the
gateway and `md amend ../../x` could escape `shared/operators/`.
Template flows (diff/amend/append/pull) additionally require one of
the 8 known template names.
- **Fixed argv, validated values.** Unknown arg keys rejected; author /
reason / section are control-char-free with length caps; amend
content must be non-empty.
- **Caps with `truncated` flags.** Reads cap at 64KB, diffs at 64KB,
listings at 200 entries — same convention as git/tests verbs.
- **No raw `md-write` op.** Arbitrary content-to-container stays
SSH-only; remote writes go through the validated template flows.
- **Timeouts.** Audit 300s, sync-all 600s, single-file ops 120s.
- **Audited.** Every execution records identity + op; `box-ctl.py`
additionally audits each verb with its target.
- **Retry-safe reads.** `md-audit` / `md-list` / `md-read` / `md-diff`
joined `IDEMPOTENT_ACTIONS`; all ten md verbs have
`quality-validate` dry-run branches.
## 5. Rollout notes
- Restart `exec-constrained.py` after deploy for the 9 new ops to
appear in `GET /ops` (repo total becomes 79).
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to
get the `md` group.
- Non-goals: deletes, `main-loop` enable/disable, policy writes,
swarm kill/prune — future expansions, same pattern. (Approvals
shipped separately; see BOX-APPROVALS-HTTPS.md.)
+103
View File
@@ -0,0 +1,103 @@
# Muse-Choices Deny/Escalate Policy — DECISION RECORD (Final)
Topic: add deny/escalate decisions to the `muse-choices` auto-approve daemon
(`bin/muse_choice_watcher.py`), which today only approves (top choice per
prompt kind). Interviewed 2026-10-06/07 per grill contract; accepted
verbatim below, which flipped this record from Draft to Final.
## Standing constraints (settled by user)
- All prompts must resolve: no stuck states are acceptable in any outcome.
- The full-auto top-choice flow must always exist as a path.
- Model review of choices is a FUTURE layer. Deferred out of this interview.
## Settled decisions
- D0 (helper form): a checked-in repo rules file informs decisions.
Source: user's structured answerquared 2026-10-06 ("Repo rules file
(Recommended)" for "which helper should inform approve/deny/hold
decisions"). Rationale recorded at selection time: deterministic,
versioned, sub-second, unit-testable; no agent round-trip latency.
## Settled during interview
- D0b (approve path needs no helper): straightforward prompts resolve
locally with top-choice keys (the five matcher kinds, already
implemented and live). Helpers (D0 rules file) govern deny/hold
judgments only. Source: user direction 2026-10-06 ("the watcher
itself should be able to input 1"; "always have the flow for full
auto just top choice").
- D1 (rule match dimensions): command pattern first, plus kind and
text pattern. Source: user selected option 1, 2026-10-06.
Rationale: the observed risk lives in the `$ command` of approval
dialogs; kind/text add precision around it.
- D2 (deny mechanics): deny exists only for permission kinds --
`muse-approval` dialogs receive `2` + Enter, `y/n` prompts receive
`n` + Enter. Question kinds (interview, letter, numbered) always
resolve top-choice and are never denied. Source: user selected
option 1, 2026-10-06. Rationale: deny is only meaningful where a
permission is refused; questions stay total.
- D3 (hold mechanics): hold leaves the dialog untouched, suppresses
auto-answer, raises a HELD entry in `box muse-choices status` plus
an audit record; the operator resolves via a box command, otherwise
a SHORT window expires back to top-choice approve. Source: user
selected option 1 with "short window", 2026-10-06. Exact duration
proposed below (2 minutes, tunable); accepted or amended with the
scope text in D5.
- D4 (unmatched default): approve top-choice, exactly today's
behavior. Source: user selected option 1, 2026-10-06. Rationale:
follows from the standing constraints; rules carve out only
deny/hold exceptions, so an empty rules file changes nothing.
## Open questions (unresolved)
None. All interview questions resolved and the scope accepted.
## Scope contract (ACCEPTED)
Artifact boundary, IN:
- `docs/MUSE-CHOICES-POLICY.md`: this record (Draft -> Final on acceptance).
- New `muse-choices-rules.json` at repo root (beside
`keepalive-config.json`): the checked-in deny/hold rules.
- `bin/muse_choice_watcher.py`: rule evaluation, deny/hold paths, HELD
state with short-window expiry, resolve plumbing.
- `bin/super-cli.py`: `box muse-choices resolve` command + HELD display
in status.
- `tests/test_muse_choice_watcher.py`: rule eval, per-kind deny keys,
hold/suppress/expiry, resolve flow.
Artifact boundary, OUT (rejected or deferred, each needs its own
interview to re-enter):
- Model review of choices (deferred future stage).
- Peer-agent consultation (rejected in D0).
- New matcher shapes (matcher suite's lane).
- `box runtime` work (adjacent lane, untouched).
- Timer cadence / daemon supervision changes.
Done means (all observable):
- [ ] This record marked Final with the acceptance quoted.
- [ ] Rules file loads; empty rules == today's behavior exactly.
- [ ] Deny sends `2`+Enter / `n`+Enter per D2: unit tests + one live
scratch proof per permission kind.
- [ ] Hold suppresses + shows HELD + resolves via box + expires to
approve: unit tests + one live scratch proof of hold and one of
expiry.
- [ ] Audit records for deny/hold/resolve/expire in `box-ctl.jsonl`.
- [ ] Full suite green; fleet reloaded; desired state left as found.
Acceptance (quoted verbatim, chat, 2026-10-07T00:19:57Z): "ACCEPT".
Accepted as written, including the 2-minute tunable hold window. Per the
grill scope contract, later work outside the IN boundary needs explicit
owner approval or its own follow-up interview; "go" authorizes only this
boundary. No owning issue exists in this workflow, so this record is the
lane-coordination evidence.
## Non-goals (accepted with the scope)
- Model-based review of choices (deferred future layer).
- Peer-agent consultation over sidechat (rejected in favor of D0).
- Changes to approval matching shapes (covered by the matcher test suite).
+81
View File
@@ -0,0 +1,81 @@
# Supervision Scope Contract
Status: **Draft** — decisions below are unsettled until marked otherwise.
Only explicit user acceptance moves this document (or any decision) to Final.
## Goal
Every fleet node stays alive and truthfully reported: browsers supervised,
relays supervised, dead nodes recovered or loudly paged, and `box` status
honest from any shell (including PID/net-blind sandboxed shells).
## Non-goals (proposed)
- Agent lifecycle/onboarding stages (provision, auth, OTP, invite redeem).
- Work completion (job dispatch, followups, harvester, completion auditor).
- Loop-health scoring and drive repair.
## Supervisors (observed, all installed 2026-10-06)
| Supervisor | Cadence | Coverage | Decides |
|---|---|---|---|
| chromebox-watchdog@\<node\>.timer ×6 | 2 min | all registry nodes (def/dev timers installed 18:32Z) | browser+egress per node; tunnel restart, chrome relaunch |
| cdp-relay-watchdog.timer | 5 min | registry-driven (`watched_nodes()`) | relay veth IP + connectivity; relay restart |
| agent-health.timer (user) | 5 min | registry-driven | API check per node; kill+restart with 2-strike rule + circuit breaker (3 futile → open 30 min) |
| ensure-node-supervision.sh | on node-up / `--all` | new + drifted nodes | NODES.md row + chromebox timer install |
| host_evidence fallback | on `box` read | registry (relay) + installed timers (browser) | effective status when live probes are blind |
## Decisions
(D1..D7 below — all UNRESOLVED unless marked.)
### D1. Contract boundary: which supervisors are in scope — SETTLED (recommended accepted)
IN: chromebox-watchdog ×6, cdp-relay-watchdog, agent-health + circuit
breaker, ensure-node-supervision feed, host_evidence fallback.
OUT: onboarding pipeline lifecycle, completion auditor, loop-health
(each keeps its own owner and interviews separately).
### D2. Kill-path precedence (chromebox-watchdog vs agent-health) — UNRESOLVED
Both can kill a browser today; only time guards (<2 min) de-conflict them.
### D3. Egress-down fall-through (relaunch chrome after failed tunnel restart?) — UNRESOLVED
Observed 19:12Z: tunnel restart failed, watchdog relaunched chrome 3×
anyway (one FAILED page). Browser was never the problem.
### D4. Circuit-breaker thresholds (3 futile / 30 min cooldown) — UNRESOLVED
Current values unvalidated against real recurrence intervals.
### D5. Coverage source of truth — UNRESOLVED
Registry-only vs registry+installed-timers for browser verdicts.
### D6. Concurrent-edit protocol for shared supervision files — UNRESOLVED
Two agents editing super-cli.py / watchdogs / runbook; one clobber
(18:03Z) and one unattributed commit (c9143a5) already occurred.
### D7. Done means — UNRESOLVED
Proposed checklist: timers on all 6 firing silent; relay/agent-health
loops registry-driven with tests; ensure hook live; this doc Final.
## Risks
- Egress-down pages read as browser failures (D3).
- Uncommitted supervisor work can be clobbered by a concurrent editor (D6).
- c9143a5 contains unattributed peer hunks (host_evidence `_covered_nodes`,
fleet-status test updates) — needs an amend-or-leave decision.
## Validation
- `box fleet status` truthful from blind shells (live-verified 6/6 ACTIVE).
- Focused suites green (supervision, fleet, agent-health, watchdog coverage).
- Timer firing proven via journal, not config presence.
## Unresolved items
D2–D7 unresolved. D1 settled.
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=NetVM Muse Choice Watcher Reconcile
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /home/super/Projects/NetVM/bin/muse_choice_watcher.py reconcile
WorkingDirectory=/home/super/Projects/NetVM
StandardOutput=journal
StandardError=journal
# Reconcile spawns long-lived per-pane daemons. Default KillMode=
# control-group would SIGTERM/SIGKILL them (setsid cannot escape a
# cgroup) the moment this oneshot service exits -- leaving every
# timer-started pane uncovered (observed live). Only signal the main
# process so spawned watchers survive service exit.
KillMode=process
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Run Muse Choice Watcher reconcile every minute
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
Persistent=true
[Install]
WantedBy=timers.target
+107 -1
View File
@@ -12,8 +12,10 @@ import json
import os import os
import subprocess import subprocess
import sys import sys
import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest import mock
REPO_ROOT = Path("/home/super/Projects/NetVM") REPO_ROOT = Path("/home/super/Projects/NetVM")
BIN_DIR = REPO_ROOT / "bin" BIN_DIR = REPO_ROOT / "bin"
@@ -138,8 +140,12 @@ class TestMuseChatApiConnection(unittest.TestCase):
def test_muse_chat_api_approvals_command(self): def test_muse_chat_api_approvals_command(self):
cmd = [sys.executable, str(BIN_DIR / "muse-chat-api.py"), "--account", "pip", "approvals"] cmd = [sys.executable, str(BIN_DIR / "muse-chat-api.py"), "--account", "pip", "approvals"]
r = subprocess.run(cmd, capture_output=True, text=True) r = subprocess.run(cmd, capture_output=True, text=True)
self.assertEqual(r.returncode, 0) self.assertIn(r.returncode, (0, 2))
if r.returncode == 0:
self.assertIn("No pending approvals", r.stdout) self.assertIn("No pending approvals", r.stdout)
else:
self.assertIn("APPROVAL_NEEDED", r.stdout)
class TestApprovalsReplySafety(unittest.TestCase): class TestApprovalsReplySafety(unittest.TestCase):
@@ -242,6 +248,106 @@ class TestKeyApprovalsAndPasskey(unittest.TestCase):
self.assertEqual(deny_data.get("decision"), "deny") self.assertEqual(deny_data.get("decision"), "deny")
class _FakeWS:
"""Scripted stand-in for a CDP websocket (no network)."""
def __init__(self, recvs):
self._recvs = list(recvs)
self.sent_ids = []
def send(self, msg):
self.sent_ids.append(json.loads(msg)["id"])
def recv(self):
if not self._recvs:
raise Exception("recv queue exhausted")
item = self._recvs.pop(0)
if callable(item):
return item(self)
return item
def close(self):
pass
def _echo_last_value(value):
def _recv(ws):
return json.dumps({"id": ws.sent_ids[-1],
"result": {"result": {"value": value}}})
return _recv
class TestInspectRobustness(unittest.TestCase):
"""Regression tests for intermittent approval failures."""
def test_cdp_request_ids_unique(self):
# Millisecond-clock ids collide for rapid successive evaluates;
# a stale buffered response can then be misattributed to the
# wrong call (e.g. verify-after-click reads the click result).
# Frozen clock makes the old collision deterministic.
with mock.patch("approvals.time.time", return_value=1728000000.123):
ws = _FakeWS([_echo_last_value("a"), _echo_last_value("b")])
self.assertEqual(approvals.cdp_evaluate(ws, "1+1"), "a")
self.assertEqual(approvals.cdp_evaluate(ws, "2+2"), "b")
self.assertNotEqual(ws.sent_ids[0], ws.sent_ids[1])
def test_cdp_skips_stale_ids(self):
stale = json.dumps({"id": 999999999,
"result": {"result": {"value": "stale"}}})
ws = _FakeWS([stale, _echo_last_value("fresh")])
self.assertEqual(approvals.cdp_evaluate(ws, "1+1"), "fresh")
def test_unreachable_returns_full_shape(self):
with mock.patch.object(approvals, "get_node_pages",
side_effect=ConnectionError("nope")), \
mock.patch.object(approvals, "check_node_key_request",
return_value=None):
res = approvals.inspect_node_approvals("pip")
self.assertEqual(res["status"], "UNREACHABLE")
self.assertFalse(res["has_pending"])
for key in ("node", "title", "buttons", "is_trusted",
"input_waits", "error"):
self.assertIn(key, res)
def test_all_pages_failed_reports_error(self):
pages = [{"title": "t", "url": "u", "type": "page",
"webSocketDebuggerUrl": "ws://127.0.0.1:9/none"}]
class _DeadWSModule:
@staticmethod
def create_connection(*a, **k):
raise ConnectionError("refused")
with mock.patch.object(approvals, "get_node_pages",
return_value=pages), \
mock.patch.object(approvals, "websocket", _DeadWSModule()), \
mock.patch.object(approvals, "check_node_key_request",
return_value=None):
res = approvals.inspect_node_approvals("pip")
# Per-page CDP failures must surface as ERROR, never as a
# false CLEAR that hides pending approvals.
self.assertEqual(res["status"], "ERROR")
self.assertFalse(res["has_pending"])
self.assertIn("error", res)
def test_state_saves_roundtrip_without_leftovers(self):
# Guards the atomic-save refactor (tmp + replace): correct
# content and no stray temp files left behind.
with tempfile.TemporaryDirectory() as td:
rp = Path(td) / "resp.json"
with mock.patch.object(approvals, "RESPONDED_WAITS_FILE", rp):
approvals.save_responded_waits({"pip": {"t": "x"}})
self.assertEqual(json.loads(rp.read_text()),
{"pip": {"t": "x"}})
fp = Path(td) / "seen.json"
with mock.patch.object(approvals, "FIRST_SEEN_WAITS_FILE", fp):
approvals.save_first_seen_waits({"pip": {"t": "x"}})
self.assertEqual(json.loads(fp.read_text()),
{"pip": {"t": "x"}})
self.assertEqual(sorted(p.name for p in Path(td).iterdir()),
["resp.json", "seen.json"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+79
View File
@@ -0,0 +1,79 @@
"""The bl-side #lobby relay must stay opt-in.
fleet-alert-check.sh invokes bin/fleet-alert-relay.sh only when
FLEET_BL_RELAY=1: the container-side hook is the live pager, and running
both double-posts every alert (2026-10-06). These tests run a copy of the
checker with stubbed-out fleet commands and assert the relay stub is (not)
invoked.
"""
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CHECKER = REPO_ROOT / "bin" / "fleet-alert-check.sh"
class BlRelayGate(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
root = Path(self.tmp.name)
self.bindir = root / "bin"
self.bindir.mkdir()
# Copy the real checker so BIN-relative lookups hit our stubs.
shutil.copy(CHECKER, self.bindir / "fleet-alert-check.sh")
(self.bindir / "fleet-alert-relay.sh").write_text(
"#!/bin/bash\necho RELAY-RAN >> \"$CALLS\"\n")
(self.bindir / "fleet-alert-relay.sh").chmod(0o755)
(self.bindir / "netvm-registry.py").write_text(
"#!/usr/bin/env python3\n") # no nodes: all fleet loops skip
(self.bindir / "netvm-registry.py").chmod(0o755)
(self.bindir / "box-ctl.py").write_text("#!/usr/bin/env python3\n")
(self.bindir / "box-ctl.py").chmod(0o755)
fakebin = root / "fakebin"
fakebin.mkdir()
(fakebin / "sudo").write_text("#!/bin/bash\necho sudo-stub >&2\nexit 1\n")
(fakebin / "sudo").chmod(0o755)
self.state = root / "state"
self.state.mkdir()
self.calls = root / "calls.log"
self.env = dict(os.environ)
self.env["PATH"] = str(fakebin) + ":/usr/bin:/bin"
self.env["FLEET_ALERT_STATE_DIR"] = str(self.state)
self.env["CALLS"] = str(self.calls)
self.env.pop("FLEET_BL_RELAY", None)
def run_checker(self, **extra):
env = dict(self.env)
env.update(extra)
# NOTE: appends 1-2 lines to the shared /tmp/fleet-alert-check.log
# (LOG path is hardcoded); same as any production timer run.
return subprocess.run(
["bash", str(self.bindir / "fleet-alert-check.sh")],
capture_output=True, text=True, env=env, timeout=120)
def relay_ran(self):
return self.calls.exists() and "RELAY-RAN" in self.calls.read_text()
def test_relay_not_invoked_by_default(self):
r = self.run_checker()
self.assertEqual(r.returncode, 0, r.stderr[-2000:])
self.assertFalse(self.relay_ran())
def test_relay_invoked_when_opted_in(self):
r = self.run_checker(FLEET_BL_RELAY="1")
self.assertEqual(r.returncode, 0, r.stderr[-2000:])
self.assertTrue(self.relay_ran())
def test_dry_run_never_invokes_relay(self):
r = self.run_checker(FLEET_BL_RELAY="1", FLEET_ALERT_DRY_RUN="1")
self.assertEqual(r.returncode, 0, r.stderr[-2000:])
self.assertFalse(self.relay_ran())
if __name__ == "__main__":
unittest.main()
+218
View File
@@ -0,0 +1,218 @@
"""Tests for approvals over HTTPS (no SSH).
Covers the approvals expansion:
box-relay.sh (agent client) -> exec-constrained.py named ops
-> box-ctl.py backend verbs -> approvals.py (fleet CDP).
Live execution is limited to validation-failure paths (BAD_NODE/BAD_ARGS,
which fail before any CDP probe) plus quality-validate dry-runs. No live
browser traffic and no live-socket round-trips here; instead we assert the
exact argv each op builds. In particular the allow build must never carry
--always/--force: remote allow is one-shot only.
"""
import importlib.util
import json
import subprocess
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
BOX_CTL = REPO_ROOT / "bin" / "box-ctl.py"
RELAY = REPO_ROOT / "bin" / "box-relay.sh"
def _load(name, relpath):
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
exec_constrained = _load("exec_constrained_approvals",
"bin/exec-constrained.py")
def _box_ctl(*args):
return subprocess.run(
[sys.executable, str(BOX_CTL), *args],
capture_output=True, text=True, timeout=180)
class ExecApprovalOpsTests(unittest.TestCase):
def test_ops_registered_and_side_effecting(self):
spec = exec_constrained.OPS
self.assertIn("approval.check", spec)
self.assertFalse(spec["approval.check"]["side_effecting"])
for op in ("approval.deny", "approval.auto", "approval.allow"):
self.assertIn(op, spec)
self.assertTrue(spec[op]["side_effecting"], op)
def test_known_identities_only(self):
p = exec_constrained.permitted
self.assertTrue(p("some-unknown-identity", "approval.check"))
for op in ("approval.deny", "approval.auto", "approval.allow"):
self.assertFalse(p("some-unknown-identity", op), op)
self.assertTrue(p("operator-646", op), op)
self.assertFalse(p("exec-canary", "approval.check"))
def test_check_validate(self):
v = exec_constrained.OPS["approval.check"]["validate"]
self.assertEqual(v({}), {"node": None})
self.assertEqual(v({"node": None}), {"node": None})
self.assertEqual(v({"node": "646"}), {"node": "646"})
for bad in ({"node": "nope"}, {"node": "../x"},
{"node": "646", "bogus": 1}):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v(bad)
def test_deny_validate(self):
v = exec_constrained.OPS["approval.deny"]["validate"]
good = v({"node": "646", "message": "not trusted",
"allow_main_chat": True})
self.assertEqual(good, {"node": "646", "message": "not trusted",
"allow_main_chat": True})
self.assertFalse(v({"node": "646",
"message": "m"})["allow_main_chat"])
# Multiline explanations are fine; other controls are not.
v({"node": "646", "message": "line1\nline2"})
over = "x" * (exec_constrained.MAX_MESSAGE + 1)
for bad in ({"node": "646"},
{"node": "646", "message": " "},
{"node": "646", "message": over},
{"node": "646", "message": "a\x07b"},
{"node": "nope", "message": "m"},
{"node": "646", "message": "m",
"allow_main_chat": "yes"},
{"node": "646", "message": "m", "force": True}):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v(bad)
def test_auto_validate(self):
v = exec_constrained.OPS["approval.auto"]["validate"]
self.assertEqual(v({}), {"node": None})
self.assertEqual(v({"node": "opm"}), {"node": "opm"})
with self.assertRaises(exec_constrained.OpError):
v({"node": "nope"})
with self.assertRaises(exec_constrained.OpError):
v({"node": "646", "always": True})
def test_allow_validate(self):
v = exec_constrained.OPS["approval.allow"]["validate"]
good = v({"node": "646", "message": "trusted deploy script"})
self.assertEqual(good["message"], "trusted deploy script")
self.assertFalse(good["allow_main_chat"])
# Attribution is mandatory: the flow notifies the waiting agent,
# so a remote allow must carry its reason.
with self.assertRaises(exec_constrained.OpError):
v({"node": "646"})
with self.assertRaises(exec_constrained.OpError):
v({"node": "646", "message": " "})
with self.assertRaises(exec_constrained.OpError):
v({"node": "nope", "message": "m"})
# No persistence/force remotely, not even as rejected keys.
with self.assertRaises(exec_constrained.OpError):
v({"node": "646", "message": "m", "always": True})
with self.assertRaises(exec_constrained.OpError):
v({"node": "646", "message": "m", "force": True})
def test_build_argv_shapes(self):
ops = exec_constrained.OPS
ck = ops["approval.check"]
self.assertEqual(ck["build"]({"node": None})[-1],
"approval-check")
self.assertEqual(ck["build"]({"node": "646"})[-2:],
["approval-check", "646"])
de = ops["approval.deny"]
argv = de["build"]({"node": "646", "message": "m",
"allow_main_chat": False})
self.assertEqual(argv[-4:],
["approval-deny", "646", "--message", "m"])
argv = de["build"]({"node": "646", "message": "m",
"allow_main_chat": True})
self.assertEqual(argv[-1], "--allow-main-chat")
au = ops["approval.auto"]
self.assertEqual(au["build"]({"node": None})[-1], "approval-auto")
self.assertEqual(au["build"]({"node": "opm"})[-2:],
["approval-auto", "opm"])
al = ops["approval.allow"]
argv = al["build"]({"node": "646", "message": "m",
"allow_main_chat": False})
self.assertEqual(argv[-4:],
["approval-allow", "646", "--message", "m"])
self.assertNotIn("--always", argv)
self.assertNotIn("--force", argv)
argv = al["build"]({"node": "646", "message": "m",
"allow_main_chat": True})
self.assertEqual(argv[-1], "--allow-main-chat")
self.assertIsInstance(argv, list)
class BoxCtlApprovalTests(unittest.TestCase):
def test_rejects_unknown_node_before_cdp(self):
for args in (["approval-check", "badnode"],
["approval-list", "badnode"],
["approval-allow", "badnode", "--message", "m"],
["approval-deny", "badnode", "--message", "m"],
["approval-auto", "badnode"]):
r = _box_ctl(*args)
self.assertNotEqual(r.returncode, 0, args)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NODE",
args)
def test_rejects_missing_node(self):
for args in (["approval-allow"], ["approval-deny"]):
r = _box_ctl(*args)
self.assertNotEqual(r.returncode, 0, args)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS",
args)
def test_quality_validate_approval_verbs(self):
# Note: box-ctl leaves --message optional (SSH callers may rely on
# the flow default); the exec layer is the narrower gate and
# requires it. quality-validate mirrors box-ctl.
cases = [
(["approval-check"], True),
(["approval-check", "646"], True),
(["approval-check", "nope"], False),
(["approval-check", "646", "opm"], False),
(["approval-list", "646"], True),
(["approval-allow", "646", "--message", "hi"], True),
(["approval-allow", "646"], True),
(["approval-allow"], False),
(["approval-allow", "nope", "--message", "x"], False),
(["approval-allow", "646", "--always", "--force",
"--message", "x"], True),
(["approval-approve", "646", "--message", "x"], True),
(["approval-deny", "646", "--message", "x"], True),
(["approval-deny", "646", "--message", "x",
"--allow-main-chat"], True),
(["approval-deny"], False),
(["approval-auto"], True),
(["approval-auto", "646"], True),
(["approval-auto", "nope"], False),
(["approval-auto", "a", "b"], False),
]
for args, valid in cases:
r = _box_ctl("quality-validate", *args)
self.assertEqual(json.loads(r.stdout)["valid"], valid, args)
class BoxRelayApprovalTests(unittest.TestCase):
def test_relay_help_lists_approval_commands(self):
r = subprocess.run(["bash", str(RELAY), "help"],
capture_output=True, text=True, timeout=30)
self.assertEqual(r.returncode, 0, r.stderr)
for line in ("box approvals check", "box approvals allow",
"box approvals deny", "box approvals auto"):
self.assertIn(line, r.stdout)
def test_relay_maps_approval_commands_to_ops(self):
text = RELAY.read_text()
for op in ('"approval.check"', '"approval.deny"',
'"approval.auto"', '"approval.allow"'):
self.assertIn(op, text)
if __name__ == "__main__":
unittest.main()
+345
View File
@@ -0,0 +1,345 @@
"""Tests for no-SSH agent development and communication streams.
Covers the second HTTPS expansion:
box-relay.sh (agent client) -> exec-constrained.py named ops
-> box-ctl.py backend verbs -> git / unittest / dm.py.
Live-socket round-trips are intentionally NOT covered here (loopback TCP is
unavailable in some sandboxes); instead we assert the exact argv each op
builds and execute the fast, side-effect-free argv directly. Live sends
(notify/ack) are NEVER executed here: only their validation-failure paths.
"""
import importlib.util
import json
import os
import subprocess
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
BOX_CTL = REPO_ROOT / "bin" / "box-ctl.py"
RELAY = REPO_ROOT / "bin" / "box-relay.sh"
def _load(name, relpath):
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
exec_constrained = _load("exec_constrained_dev", "bin/exec-constrained.py")
def _box_ctl(*args):
return subprocess.run(
[sys.executable, str(BOX_CTL), *args],
capture_output=True, text=True, timeout=120)
class ExecGitOpsTests(unittest.TestCase):
def test_ops_registered_and_read_only(self):
for op in ("git.status", "git.diff", "git.log"):
self.assertIn(op, exec_constrained.OPS)
self.assertFalse(exec_constrained.OPS[op]["side_effecting"])
def test_default_perms_include_git_ops(self):
self.assertTrue(exec_constrained.permitted("some-unknown-identity", "git.status"))
self.assertTrue(exec_constrained.permitted("some-unknown-identity", "git.diff"))
self.assertTrue(exec_constrained.permitted("some-unknown-identity", "git.log"))
self.assertFalse(exec_constrained.permitted("exec-canary", "git.status"))
def test_git_status_validate(self):
v = exec_constrained.OPS["git.status"]["validate"]
self.assertEqual(v({}), {})
with self.assertRaises(exec_constrained.OpError):
v({"bogus": 1})
def test_git_diff_validate(self):
v = exec_constrained.OPS["git.diff"]["validate"]
self.assertEqual(v({}), {"path": None, "stat": False})
self.assertEqual(v({"path": "bin/dm.py", "stat": True}),
{"path": "bin/dm.py", "stat": True})
for bad in ("../x", "/abs/path", "a\x00b", ""):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v({"path": bad})
def test_git_log_validate(self):
v = exec_constrained.OPS["git.log"]["validate"]
self.assertEqual(v({}), {"limit": 10, "path": None})
self.assertEqual(v({"limit": 3})["limit"], 3)
with self.assertRaises(exec_constrained.OpError):
v({"limit": 0})
with self.assertRaises(exec_constrained.OpError):
v({"limit": 51})
with self.assertRaises(exec_constrained.OpError):
v({"path": "../x"})
def test_build_argv_shapes(self):
status = exec_constrained.OPS["git.status"]
self.assertEqual(status["build"]({})[-1], "git-status")
diff = exec_constrained.OPS["git.diff"]
self.assertEqual(diff["build"]({"path": None, "stat": False})[-1], "git-diff")
argv = diff["build"]({"path": "bin/dm.py", "stat": True})
self.assertEqual(argv[-4:], ["git-diff", "--stat", "--path", "bin/dm.py"])
log = exec_constrained.OPS["git.log"]
argv = log["build"]({"limit": 3, "path": None})
self.assertEqual(argv[-3:], ["git-log", "--limit", "3"])
self.assertIsInstance(argv, list)
def test_git_status_built_argv_executes(self):
spec = exec_constrained.OPS["git.status"]
argv = spec["build"](spec["validate"]({}))
argv[0] = sys.executable # hermetic interpreter, same script + args
r = subprocess.run(argv, capture_output=True, text=True, timeout=60)
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertIn("branch", data)
self.assertIsInstance(data["changes"], list)
class ExecTestsRunTests(unittest.TestCase):
def test_registered_and_side_effecting(self):
self.assertIn("tests.run", exec_constrained.OPS)
self.assertTrue(exec_constrained.OPS["tests.run"]["side_effecting"])
def test_known_identities_only(self):
# Executes repo code: excluded from the read-only default subset.
self.assertFalse(exec_constrained.permitted("some-unknown-identity", "tests.run"))
self.assertTrue(exec_constrained.permitted("operator-646", "tests.run"))
def test_validate(self):
v = exec_constrained.OPS["tests.run"]["validate"]
self.assertEqual(v({}), {"test": None, "filter": None})
self.assertEqual(v({"test": "tests.test_box_read_https"}),
{"test": "tests.test_box_read_https",
"filter": None})
self.assertEqual(v({"filter": "safepath"})["filter"], "safepath")
for bad in ("os", "tests..x", "tests/x", "tests.test-x", ""):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v({"test": bad})
for bad in ("", "x" * 201, "a\nb"):
with self.assertRaises(exec_constrained.OpError, msg=repr(bad)):
v({"filter": bad})
def test_build_argv_shape(self):
b = exec_constrained.OPS["tests.run"]["build"]
self.assertEqual(b({"test": None, "filter": None})[-1], "tests-run")
argv = b({"test": "tests.test_box_read_https", "filter": None})
self.assertEqual(argv[-2:], ["tests-run", "tests.test_box_read_https"])
argv = b({"test": None, "filter": "safepath"})
self.assertEqual(argv[-3:], ["tests-run", "--filter", "safepath"])
class ExecCommsOpsTests(unittest.TestCase):
def test_registered_and_side_effecting(self):
for op in ("notify.send", "dm.ack"):
self.assertIn(op, exec_constrained.OPS)
self.assertTrue(exec_constrained.OPS[op]["side_effecting"])
def test_known_identities_only(self):
self.assertFalse(exec_constrained.permitted("some-unknown-identity", "notify.send"))
self.assertFalse(exec_constrained.permitted("some-unknown-identity", "dm.ack"))
self.assertTrue(exec_constrained.permitted("operator-646", "notify.send"))
self.assertTrue(exec_constrained.permitted("muse", "dm.ack"))
def test_notify_send_validate(self):
v = exec_constrained.OPS["notify.send"]["validate"]
self.assertEqual(v({"agent": "pip", "message": "hi"}),
{"agent": "pip", "message": "hi",
"sidechat": None, "sender": None})
with self.assertRaises(exec_constrained.OpError):
v({"agent": "nope", "message": "hi"})
with self.assertRaises(exec_constrained.OpError):
v({"agent": "pip", "message": "x" * 1001})
with self.assertRaises(exec_constrained.OpError):
v({"agent": "pip", "message": " "})
with self.assertRaises(exec_constrained.OpError):
v({"agent": "pip", "message": "hi", "sidechat": "a" * 65})
def test_dm_ack_validate(self):
v = exec_constrained.OPS["dm.ack"]["validate"]
good = v({"id": "bdf7beb6", "to": "pip", "sender": "opm"})
self.assertEqual(good["id"], "bdf7beb6")
self.assertFalse(good["allow_main_chat"])
with self.assertRaises(exec_constrained.OpError):
v({"id": "xyz!", "to": "pip", "sender": "opm"})
with self.assertRaises(exec_constrained.OpError):
v({"id": "bdf7beb6", "to": "nope", "sender": "opm"})
with self.assertRaises(exec_constrained.OpError):
v({"id": "bdf7beb6", "to": "pip"}) # sender required
def test_build_argv_shapes(self):
n = exec_constrained.OPS["notify.send"]
argv = n["build"]({"agent": "pip", "message": "hi",
"sidechat": None, "sender": None})
self.assertEqual(argv[-3:], ["notify", "pip", "hi"])
argv = n["build"]({"agent": "pip", "message": "hi",
"sidechat": "pip tasks", "sender": "opm"})
self.assertIn("--sidechat", argv)
self.assertIn("--sender", argv)
a = exec_constrained.OPS["dm.ack"]
argv = a["build"]({"id": "bdf7beb6", "to": "pip", "sender": "opm",
"sidechat": None, "allow_main_chat": False})
self.assertEqual(argv[-6:],
["ack", "bdf7beb6", "--to", "pip", "--sender", "opm"])
class BoxCtlGitTests(unittest.TestCase):
def test_git_status_live(self):
r = _box_ctl("git-status")
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertTrue(data["branch"])
self.assertIsInstance(data["changes"], list)
def test_git_log_live(self):
r = _box_ctl("git-log", "--limit", "2")
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertEqual(len(data["commits"]), 2)
self.assertIn("sha", data["commits"][0])
self.assertIn("subject", data["commits"][0])
def test_git_diff_stat_live(self):
r = _box_ctl("git-diff", "--stat")
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertIn("diff", data)
def test_rejects_bad_path_and_limit(self):
for bad in ("../x", "/abs/path"):
r = _box_ctl("git-diff", "--path", bad)
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
r = _box_ctl("git-log", "--limit", "0")
self.assertNotEqual(r.returncode, 0)
r = _box_ctl("git-log", "--limit", "51")
self.assertNotEqual(r.returncode, 0)
def test_quality_validate_git_verbs(self):
for args in (["git-status"], ["git-diff", "--stat"],
["git-diff", "--path", "bin/dm.py"],
["git-log", "--limit", "5"]):
r = _box_ctl("quality-validate", *args)
self.assertTrue(json.loads(r.stdout)["valid"], args)
r = _box_ctl("quality-validate", "git-diff", "--path", "../x")
self.assertFalse(json.loads(r.stdout)["valid"])
class BoxCtlTestsRunTests(unittest.TestCase):
def test_tests_run_single_module_live(self):
r = _box_ctl("tests-run", "tests.test_box_read_https")
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"], data.get("output", "")[-2000:])
self.assertEqual(data["returncode"], 0)
def test_tests_run_discovery_importable(self):
# Full discover must import every test module. An impossible -k
# filter runs zero tests in seconds while still importing all of
# them, deterministically guarding the discover argv (a `-t .`
# regresses to ImportError here). Never asserts suite success:
# outage-sensitive tests may be red independently.
r = _box_ctl("tests-run", "--filter", "zzz_no_match_zzz")
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertIn("Ran 0 tests", data.get("output", ""))
self.assertNotIn("Traceback", data.get("output", ""))
def test_tests_run_survives_safepath_invoker(self):
# `python -m` drops CWD from sys.path under PYTHONSAFEPATH/-P;
# tests-run pins PYTHONPATH so it still resolves the tests package.
env = dict(os.environ)
env["PYTHONSAFEPATH"] = "1"
r = subprocess.run(
[sys.executable, str(BOX_CTL), "tests-run",
"tests.test_box_read_https"],
capture_output=True, text=True, timeout=120, env=env)
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"], data.get("output", "")[-2000:])
def test_rejects_bad_module(self):
r = _box_ctl("tests-run", "os")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
r = _box_ctl("tests-run", "tests.nonexistent_xyz")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND")
def test_quality_validate_tests_run(self):
r = _box_ctl("quality-validate", "tests-run")
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "tests-run", "tests.test_box_read_https")
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "tests-run", "--filter", "safepath")
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "tests-run", "os")
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "tests-run", "--filter")
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
class BoxCtlAckTests(unittest.TestCase):
# Validation-failure paths only: a live ack would send a real DM.
def test_rejects_bad_id_and_agents(self):
r = _box_ctl("ack", "xyz!", "--to", "pip", "--sender", "opm")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
r = _box_ctl("ack", "bdf7beb6", "--to", "nope", "--sender", "opm")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
def test_requires_sender(self):
r = _box_ctl("ack", "bdf7beb6", "--to", "pip")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS")
def test_quality_validate_ack(self):
r = _box_ctl("quality-validate", "ack", "bdf7beb6",
"--to", "pip", "--sender", "opm")
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "ack", "xyz!",
"--to", "pip", "--sender", "opm")
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
class BoxCtlNotifyValidationTests(unittest.TestCase):
# Failure paths only: act_notify validates before sending.
def test_rejects_unknown_agent(self):
r = _box_ctl("notify", "nope", "hi")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
def test_rejects_oversize_message(self):
r = _box_ctl("notify", "pip", "x" * 1001)
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB")
class BoxRelayDevTests(unittest.TestCase):
def test_relay_help_lists_dev_commands(self):
r = subprocess.run(["bash", str(RELAY), "help"],
capture_output=True, text=True, timeout=30)
self.assertEqual(r.returncode, 0, r.stderr)
for line in ("box git status", "box git diff", "box git log",
"box tests run", "box notify", "box dm ack"):
self.assertIn(line, r.stdout)
def test_relay_maps_dev_commands_to_ops(self):
text = RELAY.read_text()
for op in ("git.status", "git.diff", "git.log",
'"tests.run"', '"notify.send"', '"dm.ack"'):
self.assertIn(op, text)
if __name__ == "__main__":
unittest.main()
+246
View File
@@ -0,0 +1,246 @@
"""Tests for job lifecycle over HTTPS (no SSH).
Covers the job-lifecycle expansion:
box-relay.sh (agent client) -> exec-constrained.py named ops
-> box-ctl.py backend verbs -> jobs/*.json / systemd / job-dispatch.
Safe mutations only: put/trigger/chain/stop/disable. Deletes are
deliberately NOT exposed. Live writes, triggers, and timer control are
NEVER executed here: only validation-failure paths (which fail before any
side effect) plus the read-only job-next dry-run. Live-socket round-trips
are intentionally NOT covered here; instead we assert the exact argv each
op builds and execute the fast, side-effect-free argv directly.
"""
import importlib.util
import json
import subprocess
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
BOX_CTL = REPO_ROOT / "bin" / "box-ctl.py"
RELAY = REPO_ROOT / "bin" / "box-relay.sh"
# An existing job used for existence-gated validation (read-only).
EXISTING_JOB = "heartbeat"
# Well-formed names that must not exist (validation-failure paths only).
MISSING_JOB = "definitely-no-such-job-xyz"
MISSING_ID = "definitely-no-such-job-xyz-20200101-000000-deadbeef"
def _load(name, relpath):
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
exec_constrained = _load("exec_constrained_jobs", "bin/exec-constrained.py")
def _box_ctl(*args, stdin=None):
return subprocess.run(
[sys.executable, str(BOX_CTL), *args],
input=stdin, capture_output=True, text=True, timeout=120)
def _job_def(name, **over):
d = {"name": name, "description": "unit test job",
"schedule": "manual", "agent": "opm",
"prompt_template": "test prompt {job_id}", "timeout": 300}
d.update(over)
return d
class ExecJobOpsTests(unittest.TestCase):
def test_ops_registered_with_side_effect_flags(self):
spec = exec_constrained.OPS
for op in ("job.put", "job.trigger", "job.chain",
"cron.timer_stop", "cron.timer_disable"):
self.assertIn(op, spec)
self.assertTrue(spec[op]["side_effecting"])
self.assertIn("job.next", spec)
self.assertFalse(spec["job.next"]["side_effecting"])
def test_no_delete_ops_exposed(self):
names = set(exec_constrained.OPS)
self.assertNotIn("job.delete", names)
self.assertNotIn("cron.timer_delete", names)
def test_permissions(self):
p = exec_constrained.permitted
self.assertTrue(p("some-unknown-identity", "job.next"))
for op in ("job.put", "job.trigger", "job.chain",
"cron.timer_stop", "cron.timer_disable"):
self.assertFalse(p("some-unknown-identity", op))
self.assertTrue(p("operator-646", op))
self.assertFalse(p("exec-canary", "job.next"))
def test_job_put_validate(self):
v = exec_constrained.OPS["job.put"]["validate"]
good = v({"name": "my-job", "definition": _job_def("my-job")})
self.assertEqual(good["name"], "my-job")
with self.assertRaises(exec_constrained.OpError):
v({"name": "Bad_Name!", "definition": _job_def("x")})
with self.assertRaises(exec_constrained.OpError):
v({"name": "my-job", "definition": ["not", "a", "dict"]})
with self.assertRaises(exec_constrained.OpError):
v({"name": "my-job", "definition": _job_def("other")})
with self.assertRaises(exec_constrained.OpError):
v({"name": "my-job"})
def test_job_trigger_validate(self):
v = exec_constrained.OPS["job.trigger"]["validate"]
self.assertEqual(v({"name": EXISTING_JOB})["name"], EXISTING_JOB)
with self.assertRaises(exec_constrained.OpError):
v({"name": MISSING_JOB})
with self.assertRaises(exec_constrained.OpError):
v({"name": "Bad_Name!"})
def test_job_chain_validate(self):
v = exec_constrained.OPS["job.chain"]["validate"]
good = v({"from": EXISTING_JOB, "to": EXISTING_JOB})
self.assertFalse(good["on_failure"])
self.assertTrue(v({"from": EXISTING_JOB, "to": EXISTING_JOB,
"on_failure": True})["on_failure"])
with self.assertRaises(exec_constrained.OpError):
v({"from": MISSING_JOB, "to": EXISTING_JOB})
with self.assertRaises(exec_constrained.OpError):
v({"from": EXISTING_JOB})
def test_job_next_validate(self):
v = exec_constrained.OPS["job.next"]["validate"]
good = v({"job_id": MISSING_ID})
self.assertEqual(good["job_id"], MISSING_ID)
self.assertIsNone(good["success"])
self.assertTrue(v({"job_id": MISSING_ID, "success": True})["success"])
for bad in ("plainname", "a-20200101-000000-xyz!",
"UPPER-20200101-000000-deadbeef", ""):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v({"job_id": bad})
def test_timer_validate(self):
for op in ("cron.timer_stop", "cron.timer_disable"):
v = exec_constrained.OPS[op]["validate"]
self.assertEqual(v({"name": EXISTING_JOB})["name"], EXISTING_JOB)
with self.assertRaises(exec_constrained.OpError):
v({"name": MISSING_JOB})
def test_build_argv_shapes(self):
put = exec_constrained.OPS["job.put"]
argv = put["build"]({"name": "my-job",
"definition": _job_def("my-job")})
self.assertEqual(argv[-2:], ["job-put", "my-job"])
trig = exec_constrained.OPS["job.trigger"]
self.assertEqual(trig["build"]({"name": EXISTING_JOB})[-2:],
["job-trigger", EXISTING_JOB])
chain = exec_constrained.OPS["job.chain"]
argv = chain["build"]({"from": "a", "to": "b", "on_failure": False})
self.assertEqual(argv[-3:], ["job-chain", "a", "b"])
argv = chain["build"]({"from": "a", "to": "b", "on_failure": True})
self.assertEqual(argv[-4:], ["job-chain", "a", "b", "--on-failure"])
nxt = exec_constrained.OPS["job.next"]
self.assertEqual(nxt["build"]({"job_id": "i", "success": None})[-2:],
["job-next", "i"])
argv = nxt["build"]({"job_id": "i", "success": False})
self.assertEqual(argv[-3:], ["job-next", "i", "--fail"])
stop = exec_constrained.OPS["cron.timer_stop"]
self.assertEqual(stop["build"]({"name": EXISTING_JOB})[-2:],
["timer-stop", EXISTING_JOB])
dis = exec_constrained.OPS["cron.timer_disable"]
self.assertEqual(dis["build"]({"name": EXISTING_JOB})[-2:],
["timer-disable", EXISTING_JOB])
self.assertIsInstance(argv, list)
def test_stdin_body_routing(self):
body = exec_constrained._stdin_body(
"job.put", {"name": "my-job", "definition": _job_def("my-job")})
# box-ctl job-put reads the raw definition (not the envelope).
self.assertEqual(json.loads(body)["name"], "my-job")
self.assertNotIn("definition", json.loads(body))
self.assertIsNone(exec_constrained._stdin_body("job.trigger", {}))
env = exec_constrained._stdin_body("files.read", {"path": "x"})
self.assertEqual(json.loads(env), {"path": "x"})
class BoxCtlJobsTests(unittest.TestCase):
def test_job_next_dry_run_live(self):
r = _box_ctl("job-next", MISSING_ID)
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertEqual(data["job_id"], MISSING_ID)
self.assertFalse(data["would_dispatch"])
def test_job_put_rejects_before_write(self):
r = _box_ctl("job-put", "Bad_Name!", stdin="{}")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
r = _box_ctl("job-put", "my-job", stdin="not json")
self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB")
r = _box_ctl("job-put", "my-job",
stdin=json.dumps(_job_def("other")))
self.assertEqual(json.loads(r.stdout)["code"], "NAME_MISMATCH")
bad = _job_def("my-job")
del bad["agent"]
r = _box_ctl("job-put", "my-job", stdin=json.dumps(bad))
self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB")
def test_job_trigger_rejects_missing(self):
r = _box_ctl("job-trigger", "Bad_Name!")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
r = _box_ctl("job-trigger", MISSING_JOB)
self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND")
def test_job_chain_rejects_before_write(self):
r = _box_ctl("job-chain", "Bad_Name!", EXISTING_JOB)
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
r = _box_ctl("job-chain", EXISTING_JOB, EXISTING_JOB)
self.assertEqual(json.loads(r.stdout)["code"], "INVALID_JOB")
r = _box_ctl("job-chain", MISSING_JOB, EXISTING_JOB)
self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND")
def test_timer_control_rejects_before_action(self):
for verb in ("timer-stop", "timer-disable"):
r = _box_ctl(verb, "Bad_Name!")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
r = _box_ctl(verb, MISSING_JOB)
self.assertEqual(json.loads(r.stdout)["code"], "NOT_FOUND")
def test_quality_validate_job_verbs(self):
r = _box_ctl("quality-validate", "job-put", "my-job")
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "job-trigger", EXISTING_JOB)
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "job-chain", "a", "b")
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "job-next", MISSING_ID)
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "timer-stop", EXISTING_JOB)
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "job-put", "Bad_Name!")
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
class BoxRelayJobsTests(unittest.TestCase):
def test_relay_help_lists_job_commands(self):
r = subprocess.run(["bash", str(RELAY), "help"],
capture_output=True, text=True, timeout=30)
self.assertEqual(r.returncode, 0, r.stderr)
for line in ("box cron put", "box cron trigger", "box cron chain",
"box cron next", "box timer stop"):
self.assertIn(line, r.stdout)
def test_relay_maps_job_commands_to_ops(self):
text = RELAY.read_text()
for op in ('"job.put"', '"job.trigger"', '"job.chain"', '"job.next"',
'"cron.timer_stop"', '"cron.timer_disable"'):
self.assertIn(op, text)
if __name__ == "__main__":
unittest.main()
+233
View File
@@ -0,0 +1,233 @@
"""Tests for loop + strategy writes over HTTPS (no SSH).
Covers the loop/strategy expansion:
box-relay.sh (agent client) -> exec-constrained.py named ops
-> box-ctl.py backend verbs -> followups/variables/strategy state.
Safe mutations only. Live execution is limited to side-effect-free paths:
loop-remediate --dry-run (all writes guarded), strat-reset on a probe key
that can never exist (returns False, no write), and validation-failure
paths (which fail before any side effect). loop-resolve always appends to
job-log, and vars-reset/rollback/strat-set mutate live fleet state, so
those success paths are covered by quality-validate (dry-run) plus unit
tests — never executed here. Live-socket round-trips are intentionally
NOT covered here; instead we assert the exact argv each op builds.
"""
import importlib.util
import json
import subprocess
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
BOX_CTL = REPO_ROOT / "bin" / "box-ctl.py"
RELAY = REPO_ROOT / "bin" / "box-relay.sh"
MISSING_VAR = "definitely-no-such-var-xyz"
# A strategy key no agent will ever set: reset returns False, no write.
PROBE_TYPE = "heartbeat"
PROBE_SUBTYPE = "ZZZ_PROBE_NO_SUCH_SUBTYPE"
def _load(name, relpath):
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
exec_constrained = _load("exec_constrained_loop", "bin/exec-constrained.py")
def _box_ctl(*args):
return subprocess.run(
[sys.executable, str(BOX_CTL), *args],
capture_output=True, text=True, timeout=180)
class ExecLoopOpsTests(unittest.TestCase):
def test_ops_registered_and_side_effecting(self):
spec = exec_constrained.OPS
for op in ("loop.remediate", "loop.resolve", "strat.set",
"strat.reset", "vars.reset", "vars.rollback"):
self.assertIn(op, spec)
self.assertTrue(spec[op]["side_effecting"])
def test_known_identities_only(self):
p = exec_constrained.permitted
for op in ("loop.remediate", "loop.resolve", "strat.set",
"strat.reset", "vars.reset", "vars.rollback"):
self.assertFalse(p("some-unknown-identity", op))
self.assertTrue(p("operator-646", op))
self.assertFalse(p("exec-canary", "loop.remediate"))
def test_remediate_validate(self):
v = exec_constrained.OPS["loop.remediate"]["validate"]
self.assertEqual(v({}), {"dry_run": False})
self.assertTrue(v({"dry_run": True})["dry_run"])
with self.assertRaises(exec_constrained.OpError):
v({"bogus": 1})
def test_resolve_validate(self):
v = exec_constrained.OPS["loop.resolve"]["validate"]
good = v({"dm_id": "bdf7beb6", "note": "looks good"})
self.assertEqual(good["dm_id"], "bdf7beb6")
self.assertEqual(good["note"], "looks good")
self.assertIsNone(v({"dm_id": "bdf7beb6"})["note"])
with self.assertRaises(exec_constrained.OpError):
v({"dm_id": "xyz!"})
with self.assertRaises(exec_constrained.OpError):
v({"dm_id": "bdf7beb6", "note": " "})
def test_strat_set_validate(self):
v = exec_constrained.OPS["strat.set"]["validate"]
good = v({"type": "job", "priority": "important", "nudges": 3})
self.assertEqual(good["type"], "job")
self.assertEqual(good["priority"], "important")
# Typos must fail: the backend silently maps unknown types to MANUAL.
with self.assertRaises(exec_constrained.OpError, msg="typo type"):
v({"type": "wkae"})
with self.assertRaises(exec_constrained.OpError):
v({"type": "job", "priority": "urgent"})
with self.assertRaises(exec_constrained.OpError):
v({"type": "job", "timeout_s": "soon"})
with self.assertRaises(exec_constrained.OpError):
v({"type": "job", "agent": "nope"})
with self.assertRaises(exec_constrained.OpError):
v({"priority": "normal"})
def test_strat_reset_validate(self):
v = exec_constrained.OPS["strat.reset"]["validate"]
good = v({"type": "heartbeat", "subtype": "DM", "agent": "opm"})
self.assertEqual(good, {"type": "heartbeat", "subtype": "DM",
"agent": "opm"})
with self.assertRaises(exec_constrained.OpError):
v({"type": "wkae"})
with self.assertRaises(exec_constrained.OpError):
v({"type": "job", "subtype": "has space"})
def test_vars_validate(self):
vr = exec_constrained.OPS["vars.reset"]["validate"]
self.assertEqual(vr({"name": "max_nudge_count"})["name"],
"max_nudge_count")
with self.assertRaises(exec_constrained.OpError):
vr({"name": "has space"})
vb = exec_constrained.OPS["vars.rollback"]["validate"]
self.assertIsNone(vb({"name": "max_nudge_count"})["revision"])
self.assertEqual(vb({"name": "x", "revision": 2})["revision"], 2)
with self.assertRaises(exec_constrained.OpError):
vb({"name": "x", "revision": 0})
with self.assertRaises(exec_constrained.OpError):
vb({"name": "x", "revision": "a\nb"})
def test_build_argv_shapes(self):
rem = exec_constrained.OPS["loop.remediate"]
self.assertEqual(rem["build"]({"dry_run": False})[-1],
"loop-remediate")
argv = rem["build"]({"dry_run": True})
self.assertEqual(argv[-2:], ["loop-remediate", "--dry-run"])
res = exec_constrained.OPS["loop.resolve"]
argv = res["build"]({"dm_id": "abc123", "note": None})
self.assertEqual(argv[-2:], ["loop-resolve", "abc123"])
argv = res["build"]({"dm_id": "abc123", "note": "n"})
self.assertEqual(argv[-3:], ["loop-resolve", "abc123", "n"])
st = exec_constrained.OPS["strat.set"]
argv = st["build"]({"type": "job", "subtype": None, "agent": None,
"track": None, "priority": "normal",
"timeout_s": None, "nudges": 2, "escalate": None})
self.assertEqual(argv[-3], "strat-set")
payload = json.loads(argv[-1])
self.assertEqual(payload["priority"], "normal")
self.assertEqual(payload["nudges"], 2)
sr = exec_constrained.OPS["strat.reset"]
argv = sr["build"]({"type": "job", "subtype": "DM",
"agent": "opm"})
self.assertEqual(argv[-4:],
["strat-reset", "job", "DM", "--agent", "opm"][-4:])
vrt = exec_constrained.OPS["vars.reset"]
self.assertEqual(vrt["build"]({"name": "x"})[-2:],
["vars-reset", "x"])
vrb = exec_constrained.OPS["vars.rollback"]
argv = vrb["build"]({"name": "x", "revision": 2})
self.assertEqual(argv[-3:], ["vars-rollback", "x", "2"])
self.assertIsInstance(argv, list)
class BoxCtlLoopTests(unittest.TestCase):
def test_remediate_dry_run_live(self):
r = _box_ctl("loop-remediate", "--dry-run")
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertTrue(data["dry_run"])
self.assertIn("remediated", data)
self.assertIn("escalated", data)
def test_strat_reset_probe_key_live(self):
r = _box_ctl("strat-reset", PROBE_TYPE, PROBE_SUBTYPE)
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertFalse(data["reset"])
def test_strat_set_rejects_before_write(self):
r = _box_ctl("strat-set")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
r = _box_ctl("strat-set", "job", "not json")
self.assertEqual(json.loads(r.stdout)["code"], "STRAT_ERROR")
bad = json.dumps({"priority": "urgent"})
r = _box_ctl("strat-set", "job", bad)
self.assertEqual(json.loads(r.stdout)["code"], "STRAT_ERROR")
def test_vars_rejects_unknown_before_write(self):
r = _box_ctl("vars-reset", MISSING_VAR)
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "VARS_ERROR")
r = _box_ctl("vars-rollback", MISSING_VAR)
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "VARS_ERROR")
def test_quality_validate_loop_verbs(self):
cases = [
(["loop-remediate"], True),
(["loop-remediate", "--dry-run"], True),
(["loop-resolve", "bdf7beb6"], True),
(["loop-resolve", "bdf7beb6", "note"], True),
(["loop-resolve", "xyz!"], False),
(["strat-set", "job"], True),
(["strat-set"], False),
(["strat-reset", "job", "DM", "--agent", "opm"], True),
(["strat-reset", "--agent", "nope"], False),
(["vars-reset", "max_nudge_count"], True),
(["vars-rollback", "max_nudge_count", "2"], True),
(["vars-get", "loop_health_threshold"], True),
(["vars-set", "max_nudge_count", "5"], True),
(["vars-reset"], False),
(["vars-get", "has space"], False),
]
for args, valid in cases:
r = _box_ctl("quality-validate", *args)
self.assertEqual(json.loads(r.stdout)["valid"], valid, args)
class BoxRelayLoopTests(unittest.TestCase):
def test_relay_help_lists_loop_commands(self):
r = subprocess.run(["bash", str(RELAY), "help"],
capture_output=True, text=True, timeout=30)
self.assertEqual(r.returncode, 0, r.stderr)
for line in ("box loop remediate", "box loop resolve",
"box strat set", "box strat reset",
"box vars reset", "box vars rollback"):
self.assertIn(line, r.stdout)
def test_relay_maps_loop_commands_to_ops(self):
text = RELAY.read_text()
for op in ('"loop.remediate"', '"loop.resolve"', '"strat.set"',
'"strat.reset"', '"vars.reset"', '"vars.rollback"'):
self.assertIn(op, text)
if __name__ == "__main__":
unittest.main()
+412
View File
@@ -0,0 +1,412 @@
"""Tests for md-file reads + governed writes over HTTPS (no SSH).
Covers the md expansion:
box-relay.sh (agent client) -> exec-constrained.py named ops
-> box-ctl.py backend verbs -> agent_md.py (Hatch gateway / shared templates).
Raw container writes (md-write) are deliberately NOT exposed over HTTPS;
the governed flows are amend/append (validated shared templates with git
commit) and pull/inject-drive/sync-all (push canonical templates).
Live execution is limited to validation-failure paths (which fail before
any gateway call or write), one stdin-plumbing path that the backend
safety gate rejects before writing, quality-validate dry-runs, and
agent_md validator unit tests. No live gateway traffic, no template
writes, and no live-socket round-trips here; instead we assert the exact
argv each op builds.
"""
import hashlib
import importlib.util
import json
import subprocess
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
BOX_CTL = REPO_ROOT / "bin" / "box-ctl.py"
RELAY = REPO_ROOT / "bin" / "box-relay.sh"
HEARTBEAT = REPO_ROOT / "shared" / "operators" / "HEARTBEAT.md"
MD_READ_OPS = ("md.audit", "md.list", "md.read", "md.diff")
MD_WRITE_OPS = ("md.pull", "md.inject_drive", "md.sync_all",
"md.amend", "md.append")
def _load(name, relpath):
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
exec_constrained = _load("exec_constrained_md", "bin/exec-constrained.py")
agent_md = _load("agent_md_mdtest", "bin/agent_md.py")
def _box_ctl(*args, stdin=None):
return subprocess.run(
[sys.executable, str(BOX_CTL), *args],
input=stdin, capture_output=True, text=True, timeout=180)
class ExecMdOpsTests(unittest.TestCase):
def test_ops_registered_and_side_effecting(self):
spec = exec_constrained.OPS
for op in MD_READ_OPS:
self.assertIn(op, spec)
self.assertFalse(spec[op]["side_effecting"], op)
for op in MD_WRITE_OPS:
self.assertIn(op, spec)
self.assertTrue(spec[op]["side_effecting"], op)
def test_raw_write_not_exposed(self):
self.assertNotIn("md.write", exec_constrained.OPS)
def test_known_identities_only(self):
p = exec_constrained.permitted
for op in MD_READ_OPS:
self.assertTrue(p("some-unknown-identity", op), op)
self.assertTrue(p("operator-646", op), op)
for op in MD_WRITE_OPS:
self.assertFalse(p("some-unknown-identity", op), op)
self.assertTrue(p("operator-646", op), op)
self.assertFalse(p("exec-canary", "md.read"))
self.assertFalse(p("exec-canary", "md.amend"))
def test_audit_validate(self):
v = exec_constrained.OPS["md.audit"]["validate"]
self.assertEqual(v({}), {"accounts": None})
# Explicit null means "all accounts", same as omitted (optional-arg
# convention shared with strat.set / loop.resolve).
self.assertEqual(v({"accounts": None}), {"accounts": None})
self.assertEqual(v({"accounts": ["646", "muse-main"]})["accounts"],
["646", "muse-main"])
for bad in ({"accounts": "646"}, {"accounts": []},
{"accounts": ["../x"]}, {"accounts": ["a b"]},
{"accounts": ["ok", ""]}, {"bogus": 1}):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v(bad)
def test_list_validate(self):
v = exec_constrained.OPS["md.list"]["validate"]
self.assertEqual(v({"account": "646"}),
{"account": "646", "path": ""})
self.assertEqual(v({"account": "646", "path": "sub/dir"})["path"],
"sub/dir")
for bad in ({"account": "../x"}, {"account": "a b"},
{"account": "646", "path": ".."},
{"account": "646", "path": "/abs"},
{"account": "646", "path": "a/../../x"},
{"account": "646", "bogus": 1},
{"path": "sub"}):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v(bad)
def test_read_validate(self):
v = exec_constrained.OPS["md.read"]["validate"]
good = v({"account": "646", "filename": "SOUL.md"})
self.assertEqual(good, {"account": "646", "filename": "SOUL.md"})
# Reads accept any container basename, not just shared templates.
self.assertEqual(
v({"account": "646", "filename": "NOTES.md"})["filename"],
"NOTES.md")
for bad in ({"account": "646", "filename": "../x"},
{"account": "646", "filename": "/abs"},
{"account": "646", "filename": ".."},
{"account": "646", "filename": "a/b"},
{"account": "646", "filename": ""},
{"account": "a b", "filename": "SOUL.md"},
{"account": "646", "filename": "SOUL.md", "x": 1}):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v(bad)
def test_diff_validate(self):
v = exec_constrained.OPS["md.diff"]["validate"]
good = v({"account": "646", "filename": "SOUL.md"})
self.assertEqual(good, {"account": "646", "filename": "SOUL.md"})
# Template flows reject non-templates: the backend indexes
# shared/operators/ by filename.
for bad in ({"account": "646", "filename": "NOPE.md"},
{"account": "646", "filename": "../x"},
{"account": "646", "filename": "NOTES.md"}):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v(bad)
def test_pull_validate(self):
v = exec_constrained.OPS["md.pull"]["validate"]
self.assertEqual(v({"account": "opm", "filename": "TOOLS.md"}),
{"account": "opm", "filename": "TOOLS.md"})
with self.assertRaises(exec_constrained.OpError):
v({"account": "opm", "filename": "NOPE.md"})
with self.assertRaises(exec_constrained.OpError):
v({"account": "../x", "filename": "TOOLS.md"})
def test_inject_drive_validate(self):
v = exec_constrained.OPS["md.inject_drive"]["validate"]
self.assertEqual(v({"account": "646"}), {"account": "646"})
with self.assertRaises(exec_constrained.OpError):
v({"account": "../x"})
with self.assertRaises(exec_constrained.OpError):
v({"account": "646", "force": True})
def test_sync_all_validate(self):
v = exec_constrained.OPS["md.sync_all"]["validate"]
self.assertEqual(v({}), {})
with self.assertRaises(exec_constrained.OpError):
v({"accounts": ["646"]})
def test_amend_validate(self):
v = exec_constrained.OPS["md.amend"]["validate"]
good = v({"filename": "SOUL.md", "content": "body",
"author": "646", "reason": "tune"})
self.assertEqual(good, {"filename": "SOUL.md", "content": "body",
"author": "646", "reason": "tune"})
defaults = v({"filename": "SOUL.md", "content": "body"})
self.assertEqual(defaults["author"], "operator")
self.assertEqual(defaults["reason"], "")
over = "x" * (exec_constrained.MD_MAX_AMEND + 1)
for bad in ({"filename": "NOPE.md", "content": "body"},
{"filename": "SOUL.md", "content": " "},
{"filename": "SOUL.md", "content": over},
{"filename": "SOUL.md", "content": "x", "author": ""},
{"filename": "SOUL.md", "content": "x",
"author": "a\nb"},
{"filename": "SOUL.md", "content": "x",
"author": "a" * 65},
{"filename": "SOUL.md", "content": "x",
"reason": "r" * 257},
{"filename": "SOUL.md", "content": "x",
"reason": "a\nb"},
{"filename": "SOUL.md", "content": "x", "bogus": 1},
{"filename": "SOUL.md"}):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v(bad)
def test_append_validate(self):
v = exec_constrained.OPS["md.append"]["validate"]
good = v({"filename": "AGENTS.md", "text": "lesson",
"author": "opm", "section": "Wins"})
self.assertEqual(good["section"], "Wins")
self.assertIsNone(v({"filename": "AGENTS.md",
"text": "lesson"})["section"])
over = "x" * (exec_constrained.MD_MAX_APPEND + 1)
for bad in ({"filename": "NOPE.md", "text": "lesson"},
{"filename": "AGENTS.md", "text": " "},
{"filename": "AGENTS.md", "text": over},
{"filename": "AGENTS.md", "text": "t",
"section": " "},
{"filename": "AGENTS.md", "text": "t",
"section": "s" * 129}):
with self.assertRaises(exec_constrained.OpError, msg=bad):
v(bad)
def test_build_argv_shapes(self):
ops = exec_constrained.OPS
au = ops["md.audit"]
self.assertEqual(au["build"]({"accounts": None})[-1], "md-audit")
self.assertEqual(
au["build"]({"accounts": ["646", "opm"]})[-3:],
["md-audit", "646", "opm"])
li = ops["md.list"]
self.assertEqual(li["build"]({"account": "646", "path": ""})[-2:],
["md-list", "646"])
self.assertEqual(
li["build"]({"account": "646", "path": "sub"})[-3:],
["md-list", "646", "sub"])
rd = ops["md.read"]
self.assertEqual(
rd["build"]({"account": "646", "filename": "SOUL.md"})[-3:],
["md-read", "646", "SOUL.md"])
df = ops["md.diff"]
self.assertEqual(
df["build"]({"account": "646", "filename": "SOUL.md"})[-3:],
["md-diff", "646", "SOUL.md"])
pu = ops["md.pull"]
self.assertEqual(
pu["build"]({"account": "646", "filename": "SOUL.md"})[-3:],
["md-pull", "646", "SOUL.md"])
inj = ops["md.inject_drive"]
self.assertEqual(inj["build"]({"account": "646"})[-2:],
["md-inject-drive", "646"])
self.assertEqual(ops["md.sync_all"]["build"]({})[-1], "md-sync-all")
am = ops["md.amend"]
argv = am["build"]({"filename": "SOUL.md", "content": "x",
"author": "646", "reason": "why"})
self.assertEqual(argv[-7:],
["md-amend", "SOUL.md", "--stdin",
"--author", "646", "--reason", "why"])
argv = am["build"]({"filename": "SOUL.md", "content": "x",
"author": "646", "reason": ""})
self.assertEqual(argv[-5:],
["md-amend", "SOUL.md", "--stdin",
"--author", "646"])
ap = ops["md.append"]
argv = ap["build"]({"filename": "AGENTS.md", "text": "t",
"author": "opm", "section": "Wins"})
self.assertEqual(argv[-7:],
["md-append", "AGENTS.md", "--stdin",
"--author", "opm", "--section", "Wins"])
argv = ap["build"]({"filename": "AGENTS.md", "text": "t",
"author": "opm", "section": None})
self.assertEqual(argv[-5:],
["md-append", "AGENTS.md", "--stdin",
"--author", "opm"])
self.assertIsInstance(argv, list)
def test_stdin_body(self):
sb = exec_constrained._stdin_body
self.assertEqual(sb("md.amend", {"content": "C"}), "C")
self.assertEqual(sb("md.append", {"text": "T"}), "T")
self.assertIsNone(sb("md.read", {"account": "646"}))
class AgentMdValidationTests(unittest.TestCase):
def test_account(self):
for good in ("646", "muse", "muse-main", "opm", "dev", "def"):
self.assertEqual(agent_md.validate_account(good), good)
for bad in ("../x", "a b", "", "a/b", "x" * 33, "-lead"):
with self.assertRaises(agent_md.MDValidationError, msg=bad):
agent_md.validate_account(bad)
def test_filename(self):
for good in ("SOUL.md", "NOTES.md", "a"):
self.assertEqual(agent_md.validate_filename(good), good)
self.assertEqual(
agent_md.validate_filename("SOUL.md", template_only=True),
"SOUL.md")
for bad in ("../x", "/abs", "..", ".", "a/b", ""):
with self.assertRaises(agent_md.MDValidationError, msg=bad):
agent_md.validate_filename(bad)
for bad in ("NOPE.md", "../SOUL.md", "NOTES.md"):
with self.assertRaises(agent_md.MDValidationError, msg=bad):
agent_md.validate_filename(bad, template_only=True)
def test_subpath(self):
self.assertEqual(agent_md.validate_subpath(""), "")
self.assertEqual(agent_md.validate_subpath("a/b"), "a/b")
for bad in ("..", "/abs", "a/../../x", "a b"):
with self.assertRaises(agent_md.MDValidationError, msg=bad):
agent_md.validate_subpath(bad)
def test_rejects_before_gateway(self):
# Validation failures raise MDValidationError; a call that
# reached the gateway would raise RuntimeError (no gateway
# module here) or FileNotFoundError (no cookies) instead.
with self.assertRaises(agent_md.MDValidationError):
agent_md.read_md("646", "../x")
with self.assertRaises(agent_md.MDValidationError):
agent_md.list_files("../x", "")
with self.assertRaises(agent_md.MDValidationError):
agent_md.audit_agents(["ok", "../x"])
with self.assertRaises(agent_md.MDValidationError):
agent_md.diff_md("646", "NOPE.md")
def test_amend_rejects_before_write(self):
with self.assertRaises(agent_md.MDValidationError):
agent_md.amend_md("../x", "body")
with self.assertRaises(agent_md.MDValidationError):
agent_md.append_md("NOPE.md", "note")
class BoxCtlMdTests(unittest.TestCase):
def test_rejects_traversal_before_gateway(self):
cases = [
["md-read", "646", "../x"],
["md-read", "646", ".."],
["md-list", "bad!", "x"],
["md-list", "646", "../.."],
["md-write", "646", "/abs", "hi"],
["md-audit", "../x"],
["md", "read", "646", "../x"],
]
for args in cases:
r = _box_ctl(*args)
self.assertNotEqual(r.returncode, 0, args)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME", args)
def test_rejects_non_template_before_write(self):
cases = [
["md-diff", "646", "NOPE.md"],
["md-pull", "646", "NOPE.md"],
["md", "diff", "646", "NOPE.md"],
["md", "amend", "NOPE.md", "content here"],
["md", "append", "NOPE.md", "note here"],
]
for args in cases:
r = _box_ctl(*args)
self.assertNotEqual(r.returncode, 0, args)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME", args)
r = _box_ctl("md-amend", "../x", "--stdin", stdin="hi")
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
r = _box_ctl("md-append", "../x", "--stdin", stdin="hi")
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NAME")
def test_amend_stdin_safety_rejection_writes_nothing(self):
before = hashlib.sha256(HEARTBEAT.read_bytes()).hexdigest()
# Gutted HEARTBEAT content via --stdin: proves stdin plumbing
# reaches the backend, and the safety gate rejects it before
# any write or git commit.
r = _box_ctl("md-amend", "HEARTBEAT.md", "--stdin", stdin="gutted")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "AMEND_FAILED")
after = hashlib.sha256(HEARTBEAT.read_bytes()).hexdigest()
self.assertEqual(before, after)
def test_quality_validate_md_verbs(self):
cases = [
(["md-audit"], True),
(["md-audit", "646", "opm"], True),
(["md-audit", "../x"], False),
(["md-list", "646"], True),
(["md-list", "646", "sub/dir"], True),
(["md-list", "646", ".."], False),
(["md-list"], False),
(["md-read", "646", "SOUL.md"], True),
(["md-read", "646", "../x"], False),
(["md-read", "646"], False),
(["md-diff", "646", "SOUL.md"], True),
(["md-diff", "646", "NOPE.md"], False),
(["md-pull", "646", "SOUL.md"], True),
(["md-pull", "646"], False),
(["md-inject-drive", "646"], True),
(["md-inject-drive", "646", "--force"], True),
(["md-inject-drive"], False),
(["md-sync-all"], True),
(["md-sync-all", "--force"], True),
(["md-sync-all", "646"], False),
(["md-amend", "SOUL.md", "--stdin"], True),
(["md-amend", "SOUL.md", "--stdin", "--author", "646"], True),
(["md-amend", "NOPE.md", "--stdin"], False),
(["md-amend"], False),
(["md-append", "SOUL.md", "--stdin"], True),
(["md-append", "SOUL.md", "note", "--section", "s"], True),
(["md-append", "x", "y", "z"], False),
(["md-write", "646", "SOUL.md", "x"], True),
(["md-write", "646", "SOUL.md"], False),
]
for args, valid in cases:
r = _box_ctl("quality-validate", *args)
self.assertEqual(json.loads(r.stdout)["valid"], valid, args)
class BoxRelayMdTests(unittest.TestCase):
def test_relay_help_lists_md_commands(self):
r = subprocess.run(["bash", str(RELAY), "help"],
capture_output=True, text=True, timeout=30)
self.assertEqual(r.returncode, 0, r.stderr)
for line in ("box md audit", "box md list", "box md read",
"box md diff", "box md pull", "box md inject-drive",
"box md sync-all", "box md amend", "box md append"):
self.assertIn(line, r.stdout)
def test_relay_maps_md_commands_to_ops(self):
text = RELAY.read_text()
for op in ('"md.audit"', '"md.list"', '"md.read"', '"md.diff"',
'"md.pull"', '"md.inject_drive"', '"md.sync_all"',
'"md.amend"', '"md.append"'):
self.assertIn(op, text)
if __name__ == "__main__":
unittest.main()
+202
View File
@@ -0,0 +1,202 @@
"""Tests for read-only box lookups over HTTPS (no SSH).
Covers the agent-facing read path:
box-relay.sh (agent client) -> exec-constrained.py named ops
-> box-ctl.py backend verbs -> super-cli.py lookups.
Live-socket round-trips are intentionally NOT covered here (loopback TCP is
unavailable in some sandboxes); instead we assert the exact argv each op
builds and execute the fast argv directly.
"""
import argparse
import importlib.util
import io
import json
import subprocess
import sys
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parent.parent
BOX_CTL = REPO_ROOT / "bin" / "box-ctl.py"
RELAY = REPO_ROOT / "bin" / "box-relay.sh"
def _load(name, relpath):
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / relpath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
exec_constrained = _load("exec_constrained_read", "bin/exec-constrained.py")
super_cli = _load("super_cli_read", "bin/super-cli.py")
def _box_ctl(*args):
return subprocess.run(
[sys.executable, str(BOX_CTL), *args],
capture_output=True, text=True, timeout=60)
class ExecReadOpsTests(unittest.TestCase):
def test_ops_registered_and_read_only(self):
self.assertIn("fleet.unread", exec_constrained.OPS)
self.assertIn("dm.log", exec_constrained.OPS)
self.assertFalse(exec_constrained.OPS["fleet.unread"]["side_effecting"])
self.assertFalse(exec_constrained.OPS["dm.log"]["side_effecting"])
def test_default_perms_include_read_ops(self):
# Any valid fleet signer can read; canary stays ping-only.
self.assertTrue(exec_constrained.permitted("some-unknown-identity", "fleet.unread"))
self.assertTrue(exec_constrained.permitted("some-unknown-identity", "dm.log"))
self.assertFalse(exec_constrained.permitted("exec-canary", "fleet.unread"))
self.assertFalse(exec_constrained.permitted("exec-canary", "dm.log"))
def test_fleet_unread_validate(self):
v = exec_constrained.OPS["fleet.unread"]["validate"]
self.assertEqual(v({}), {"agent": None})
self.assertEqual(v({"agent": "pip"}), {"agent": "pip"})
with self.assertRaises(exec_constrained.OpError):
v({"agent": "nope"})
with self.assertRaises(exec_constrained.OpError):
v({"bogus": 1})
def test_dm_log_validate(self):
v = exec_constrained.OPS["dm.log"]["validate"]
self.assertEqual(v({}), {"limit": 20, "agent": None})
self.assertEqual(v({"limit": 5, "agent": "opm"}), {"limit": 5, "agent": "opm"})
with self.assertRaises(exec_constrained.OpError):
v({"limit": 0})
with self.assertRaises(exec_constrained.OpError):
v({"limit": 101})
with self.assertRaises(exec_constrained.OpError):
v({"agent": "nope"})
with self.assertRaises(exec_constrained.OpError):
v({"bogus": 1})
def test_build_argv_shapes(self):
unread = exec_constrained.OPS["fleet.unread"]
argv = unread["build"]({"agent": None})
self.assertEqual(argv[-1], "unread")
self.assertNotIn("--agent", argv)
argv = unread["build"]({"agent": "muse"})
self.assertEqual(argv[-3:], ["unread", "--agent", "muse"])
dmlog = exec_constrained.OPS["dm.log"]
argv = dmlog["build"]({"limit": 5, "agent": "opm"})
self.assertEqual(argv[-4:], ["dm-log", "5", "--agent", "opm"])
argv = dmlog["build"]({"limit": 20, "agent": None})
self.assertEqual(argv[-2:], ["dm-log", "20"])
# argv only, never a shell string.
self.assertIsInstance(argv, list)
def test_dm_log_built_argv_executes(self):
spec = exec_constrained.OPS["dm.log"]
clean = spec["validate"]({"limit": 2})
argv = spec["build"](clean)
argv[0] = sys.executable # hermetic interpreter, same script + args
r = subprocess.run(argv, capture_output=True, text=True, timeout=60)
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertEqual(len(data["entries"]), 2)
class BoxCtlReadVerbsTests(unittest.TestCase):
def test_unread_rejects_unknown_agent(self):
r = _box_ctl("unread", "--agent", "nope")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NODE")
def test_unread_rejects_positional_and_missing_value(self):
r = _box_ctl("unread", "pip")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS")
r = _box_ctl("unread", "--agent")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS")
def test_dm_log_rejects_bad_limit_and_agent(self):
r = _box_ctl("dm-log", "abc")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_LIMIT")
r = _box_ctl("dm-log", "5", "--agent", "nope")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_NODE")
r = _box_ctl("dm-log", "1", "2")
self.assertNotEqual(r.returncode, 0)
self.assertEqual(json.loads(r.stdout)["code"], "BAD_ARGS")
def test_dm_log_back_compat_limit_only(self):
r = _box_ctl("dm-log", "2")
self.assertEqual(r.returncode, 0, r.stderr)
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertEqual(len(data["entries"]), 2)
def test_quality_validate_new_verbs(self):
r = _box_ctl("quality-validate", "unread", "--agent", "pip")
data = json.loads(r.stdout)
self.assertTrue(data["valid"], r.stdout)
r = _box_ctl("quality-validate", "dm-log", "5", "--agent", "opm")
self.assertTrue(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "unread", "--agent", "nope")
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
r = _box_ctl("quality-validate", "unread", "extra-positional")
self.assertFalse(json.loads(r.stdout)["valid"], r.stdout)
class SuperCliUnreadTests(unittest.TestCase):
def test_lookup_dispatches_unread(self):
args = argparse.Namespace(target="unread", lookup_args=[], json=False)
with mock.patch.object(super_cli, "_lookup_unreads") as m:
super_cli.cmd_lookup(args)
m.assert_called_once_with(args)
def test_lookup_unreads_json_shape(self):
fleet = [
{"node": "muse", "title": "muse (2)", "url": "https://muse.ai/thread/abc123",
"approval_pending": False},
{"node": "pip", "title": "muse", "url": "https://muse.ai/",
"approval_pending": True},
]
args = argparse.Namespace(json=True)
buf = io.StringIO()
with mock.patch.object(super_cli, "collect_fleet_data", return_value=fleet):
with redirect_stdout(buf):
super_cli._lookup_unreads(args)
data = json.loads(buf.getvalue())
self.assertTrue(data["ok"])
by_node = {n["node"]: n for n in data["nodes"]}
self.assertEqual(by_node["muse"]["unread"], 2)
self.assertEqual(by_node["muse"]["thread"], "abc123")
self.assertFalse(by_node["muse"]["approval_pending"])
self.assertEqual(by_node["pip"]["unread"], 0)
self.assertTrue(by_node["pip"]["approval_pending"])
self.assertEqual(by_node["pip"]["thread"], "home")
class BoxRelayClientTests(unittest.TestCase):
def test_relay_syntax_valid(self):
r = subprocess.run(["bash", "-n", str(RELAY)],
capture_output=True, text=True, timeout=30)
self.assertEqual(r.returncode, 0, r.stderr)
def test_relay_help_lists_read_commands(self):
r = subprocess.run(["bash", str(RELAY), "help"],
capture_output=True, text=True, timeout=30)
self.assertEqual(r.returncode, 0, r.stderr)
self.assertIn("box unread", r.stdout)
self.assertIn("box dm log", r.stdout)
def test_relay_maps_read_commands_to_ops(self):
text = RELAY.read_text()
self.assertIn("fleet.unread", text)
self.assertIn('"dm.log"', text)
if __name__ == "__main__":
unittest.main()
+192
View File
@@ -0,0 +1,192 @@
"""Tests for `box run` and `box watch` (headless muse-code tmux runs) in super-cli.py."""
import argparse
import importlib.util
import io
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parent.parent
SPEC = importlib.util.spec_from_file_location(
"super_cli_box_run", REPO_ROOT / "bin" / "super-cli.py")
super_cli = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(super_cli)
def _ns(**over):
kw = dict(prompt="hello from unit test", prompt_file=None, session="ut-boxrun",
log=None, model=None, effort=None, provider="echo",
permission_profile=None, approval_mode="never",
trust_workspace=False, auto_approve=False)
kw.update(over)
return argparse.Namespace(**kw)
class FakeCompleted:
def __init__(self, returncode=0, stderr=""):
self.returncode = returncode
self.stderr = stderr
class BoxRunTests(unittest.TestCase):
def setUp(self):
for f in ("/tmp/ut-boxrun.prompt", "/tmp/ut-boxrun.sh", "/tmp/ut-boxrun.exit",
"/tmp/ut-boxrun-watch.sh", "/tmp/utw-watch.sh"):
try:
Path(f).unlink()
except FileNotFoundError:
pass
self.addCleanup(self._cleanup)
def _cleanup(self):
for f in ("/tmp/ut-boxrun.prompt", "/tmp/ut-boxrun.sh", "/tmp/ut-boxrun.exit",
"/tmp/ut-boxrun-watch.sh", "/tmp/utw-watch.sh"):
try:
Path(f).unlink()
except FileNotFoundError:
pass
for log in ("ut-boxrun.log", "ut-boxrun-watch.log", "utw-watch.log"):
try:
Path(super_cli.MUSE_TMUX_LOG_DIR / log).unlink()
except FileNotFoundError:
pass
def _which(self, name):
if name == "tmux":
return "/usr/bin/tmux"
if "muse-code" in name:
return "/home/super/.local/bin/muse-code"
return None
def test_spawn_writes_prompt_wrapper_and_tmux_argv(self):
calls = []
def fake_run(argv, **kw):
calls.append(argv)
return FakeCompleted(0)
with mock.patch.object(super_cli.shutil, "which", side_effect=self._which), \
mock.patch.object(super_cli.subprocess, "run", side_effect=fake_run):
buf = io.StringIO()
with redirect_stdout(buf):
super_cli.cmd_run(_ns())
out = buf.getvalue()
self.assertIn("session: ut-boxrun", out)
self.assertIn("attach: tmux -S /tmp/tmux-muse.sock attach -t ut-boxrun", out)
self.assertEqual(Path("/tmp/ut-boxrun.prompt").read_text(encoding="utf-8"),
"hello from unit test\n")
wrapper = Path("/tmp/ut-boxrun.sh").read_text(encoding="utf-8")
self.assertIn("muse-code", wrapper)
self.assertIn("--provider echo", wrapper)
self.assertIn("--prompt-file", wrapper)
self.assertIn("cd /home/super/Projects/NetVM", wrapper)
new_session = [c for c in calls if "new-session" in c]
self.assertEqual(len(new_session), 1)
self.assertIn("/tmp/tmux-muse.sock", new_session[0])
self.assertIn("ut-boxrun", new_session[0])
def test_optional_flags_passed_through(self):
calls = []
def fake_run(argv, **kw):
calls.append(argv)
return FakeCompleted(0)
ns = _ns(model="m1", effort="low", permission_profile="prof",
trust_workspace=True, approval_mode="on-request")
with mock.patch.object(super_cli.shutil, "which", side_effect=self._which), \
mock.patch.object(super_cli.subprocess, "run", side_effect=fake_run):
with redirect_stdout(io.StringIO()):
super_cli.cmd_run(ns)
wrapper = Path("/tmp/ut-boxrun.sh").read_text(encoding="utf-8")
for flag in ("--model m1", "--reasoning-effort low", "--permission-profile prof",
"--trust-workspace", "--approval-mode on-request"):
self.assertIn(flag, wrapper)
def test_no_prompt_exits_2(self):
with mock.patch.object(super_cli.shutil, "which", side_effect=self._which), \
mock.patch.object(super_cli.sys.stdin, "isatty", return_value=True):
with self.assertRaises(SystemExit) as cm:
super_cli.cmd_run(_ns(prompt=None))
self.assertEqual(cm.exception.code, 2)
def test_missing_muse_code_exits_2(self):
with mock.patch.object(super_cli.shutil, "which", return_value=None):
with self.assertRaises(SystemExit) as cm:
super_cli.cmd_run(_ns())
self.assertEqual(cm.exception.code, 2)
def test_sanitize_session_name(self):
self.assertEqual(super_cli._sanitize_tmux_name("run:2026/10/06 05.00"), "run-2026-10-06-05-00")
self.assertEqual(super_cli._sanitize_tmux_name("!!!"), "run")
def test_run_auto_approve_spawns_watcher(self):
calls = []
def fake_run(argv, **kw):
calls.append(argv)
return FakeCompleted(0)
with mock.patch.object(super_cli.shutil, "which", side_effect=self._which), \
mock.patch.object(super_cli.subprocess, "run", side_effect=fake_run):
buf = io.StringIO()
with redirect_stdout(buf):
super_cli.cmd_run(_ns(auto_approve=True))
out = buf.getvalue()
self.assertIn("watcher: ut-boxrun-watch", out)
self.assertIn("watcher-log:", out)
new_session = [c for c in calls if "new-session" in c]
self.assertEqual(len(new_session), 2)
watch_argv = [c for c in new_session if "ut-boxrun-watch" in c]
self.assertEqual(len(watch_argv), 1)
self.assertTrue(Path("/tmp/ut-boxrun-watch.sh").exists())
def test_write_watch_script_content(self):
watch_session, script_file = super_cli._write_watch_script("utw")
self.assertEqual(watch_session, "utw-watch")
text = Path(script_file).read_text(encoding="utf-8")
self.assertIn('target="utw"', text)
self.assertIn('exit_file="/tmp/utw.exit"', text)
self.assertIn("capture-pane", text)
self.assertIn('send-keys -t "$target" "1" Enter', text)
self.assertIn("max=200", text)
self.assertIn("has-session", text)
def test_watch_missing_session_exits_2(self):
def fake_run(argv, **kw):
if "has-session" in argv:
return FakeCompleted(1)
return FakeCompleted(0)
ns = argparse.Namespace(session="nope-missing")
with mock.patch.object(super_cli.shutil, "which", side_effect=self._which), \
mock.patch.object(super_cli.subprocess, "run", side_effect=fake_run):
with self.assertRaises(SystemExit) as cm:
super_cli.cmd_watch(ns)
self.assertEqual(cm.exception.code, 2)
def test_watch_spawns_watcher_session(self):
calls = []
def fake_run(argv, **kw):
calls.append(argv)
return FakeCompleted(0)
ns = argparse.Namespace(session="utw")
with mock.patch.object(super_cli.shutil, "which", side_effect=self._which), \
mock.patch.object(super_cli.subprocess, "run", side_effect=fake_run):
buf = io.StringIO()
with redirect_stdout(buf):
super_cli.cmd_watch(ns)
out = buf.getvalue()
self.assertIn("watching: utw", out)
self.assertIn("watcher: utw-watch", out)
new_session = [c for c in calls if "new-session" in c]
self.assertEqual(len(new_session), 1)
self.assertIn("utw-watch", new_session[0])
if __name__ == "__main__":
unittest.main()
+342
View File
@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""test_box_runtime.py — Runtime state sensing + `box runtime` management.
Covers: runtime_state classification (approval-pending/working/open-prompt),
muse argv approval-posture parsing, runtime_rows assembly (mocked tmux),
and the `box runtime` CLI surface.
"""
import json
import sys
import unittest
from pathlib import Path
from unittest import mock
REPO_ROOT = Path("/home/super/Projects/NetVM")
BIN_DIR = REPO_ROOT / "bin"
sys.path.insert(0, str(BIN_DIR))
import muse_choice_watcher as w
PROMPT = "❯" # Muse TUI input glyph (U+276F)
STATE_OPEN = (
"Some completed agent output here.\n"
"\n"
"───────────────────────────────────\n"
+ PROMPT + "\n"
"───────────────────────────────────\n"
" muse-spark-1.3-con… · Auto-review\n"
)
STATE_WORKING = (
"Partial agent output...\n"
"\n"
"… — running (10s · esc to interrupt)\n"
"\n"
"───────────────────────────────────\n"
+ PROMPT + "\n"
"───────────────────────────────────\n"
" muse-spark-1.3-con… · Auto-review\n"
)
STATE_WORKING_CUT = (
"Partial agent output...\n"
"◆ Calling tools (5m 53s · esc to in\n"
"\n"
"───────────────────────────────────\n"
+ PROMPT + "\n"
)
STATE_APPROVAL = (
"───────────────────────────────────\n"
"Would you like to run the following\n"
"\n"
" $ tmux capture-pane -p\n"
"\n"
"› 1. Yes, proceed (y)\n"
" 2. No, and tell Muse Code what to do instead\n"
)
STATE_SHELL = "[super@bl NetVM]$ printf 'hi'\nhi\n[super@bl NetVM]$ "
class TestRuntimeState(unittest.TestCase):
def test_open_prompt(self):
st = w.runtime_state(STATE_OPEN)
self.assertEqual(st["state"], "open-prompt")
self.assertIsNone(st["match"])
def test_working(self):
st = w.runtime_state(STATE_WORKING)
self.assertEqual(st["state"], "working")
def test_working_edge_cut_indicator(self):
st = w.runtime_state(STATE_WORKING_CUT)
self.assertEqual(st["state"], "working")
def test_approval_pending(self):
st = w.runtime_state(STATE_APPROVAL)
self.assertEqual(st["state"], "approval-pending")
self.assertEqual(st["match"]["kind"], "muse-approval")
self.assertEqual(st["match"]["key"], "1")
def test_approval_beats_working(self):
st = w.runtime_state(STATE_WORKING + STATE_APPROVAL)
self.assertEqual(st["state"], "approval-pending")
def test_working_beats_open_prompt(self):
# A working pane still renders its prompt footer.
st = w.runtime_state(STATE_WORKING)
self.assertEqual(st["state"], "working")
def test_shell_is_unknown(self):
self.assertEqual(w.runtime_state(STATE_SHELL)["state"], "unknown")
def test_empty_is_unknown(self):
self.assertEqual(w.runtime_state("")["state"], "unknown")
self.assertEqual(w.runtime_state(None)["state"], "unknown")
class TestApprovalFlags(unittest.TestCase):
def test_bare_is_not_auto(self):
p = w.muse_approval_flags(["/home/super/.local/bin/muse-bin-1.4.3"])
self.assertFalse(p["auto_approve"])
self.assertEqual(p["flags"], [])
def test_yolo(self):
p = w.muse_approval_flags(["muse", "--yolo"])
self.assertTrue(p["auto_approve"])
self.assertIn("yolo", p["flags"])
def test_disable_approval(self):
p = w.muse_approval_flags(["muse", "--disable-approval"])
self.assertTrue(p["auto_approve"])
def test_approval_mode_never(self):
p = w.muse_approval_flags(["muse", "--approval-mode", "never"])
self.assertTrue(p["auto_approve"])
self.assertIn("approval-mode=never", p["flags"])
def test_approval_mode_equals(self):
p = w.muse_approval_flags(["muse", "--approval-mode=never"])
self.assertTrue(p["auto_approve"])
def test_approval_mode_on_request_is_not_auto(self):
p = w.muse_approval_flags(["muse", "--approval-mode", "on-request"])
self.assertFalse(p["auto_approve"])
self.assertIn("approval-mode=on-request", p["flags"])
def test_empty_argv(self):
p = w.muse_approval_flags([])
self.assertFalse(p["auto_approve"])
def _tmux_result(returncode=0, stdout="", stderr=""):
r = mock.Mock()
r.returncode = returncode
r.stdout = stdout
r.stderr = stderr
return r
class TestRuntimeRows(unittest.TestCase):
LISTING = ("muse\t1\t%37\tmuse-bin-1.4.3-R5018.1\t2880158\t71\t27\n"
"muse\t1\t%38\tbash\t2880200\t100\t30\n")
def _patched(self, tmux_stdout=LISTING, tmux_rc=0, captures=None,
children=None, cmdlines=None, watcher=None):
captures = captures or {}
cmdlines = cmdlines or {}
children = children or {}
return (mock.patch.object(w, "_tmux", return_value=_tmux_result(
tmux_rc, tmux_stdout)),
mock.patch.object(w, "capture_pane",
side_effect=lambda s, p: captures.get(p)),
mock.patch.object(w, "_child_pids",
side_effect=lambda p: children.get(p, [])),
mock.patch.object(w, "_cmdline",
side_effect=lambda p: cmdlines.get(p, [])),
mock.patch.object(w, "is_running", return_value=watcher))
def test_rows_shape(self):
patches = self._patched(
captures={"%37": STATE_OPEN, "%38": STATE_SHELL},
children={2880158: [2881158]},
cmdlines={2881158: ["/home/super/.local/bin/muse-bin-1.4.3",
"--disable-approval"]},
watcher=1234)
with patches[0], patches[1], patches[2], patches[3], patches[4]:
rows = w.runtime_rows("/tmp/sock")
self.assertEqual(len(rows), 2)
muse = rows[0]
self.assertEqual(muse["pane"], "%37")
self.assertTrue(muse["is_muse"])
self.assertTrue(muse["auto_approve"])
self.assertEqual(muse["approval_flags"], ["disable-approval"])
self.assertEqual(muse["state"], "open-prompt")
self.assertTrue(muse["watcher_alive"])
self.assertEqual(muse["watcher_pid"], 1234)
self.assertEqual(muse["width"], 71)
self.assertEqual(muse["height"], 27)
self.assertFalse(muse["squeezed"])
shell = rows[1]
self.assertFalse(shell["is_muse"])
self.assertIsNone(shell["auto_approve"])
self.assertEqual(shell["state"], "unknown")
def test_bare_muse_reports_not_auto(self):
patches = self._patched(
captures={"%37": STATE_WORKING, "%38": STATE_SHELL},
children={2880158: [2881158]},
cmdlines={2881158: ["/home/super/.local/bin/muse-bin-1.4.3"]})
with patches[0], patches[1], patches[2], patches[3], patches[4]:
rows = w.runtime_rows("/tmp/sock")
self.assertFalse(rows[0]["auto_approve"])
self.assertEqual(rows[0]["state"], "working")
self.assertFalse(rows[0]["watcher_alive"])
def test_approval_pending_row_carries_kind(self):
patches = self._patched(captures={"%37": STATE_APPROVAL,
"%38": STATE_SHELL})
with patches[0], patches[1], patches[2], patches[3], patches[4]:
rows = w.runtime_rows("/tmp/sock")
self.assertEqual(rows[0]["state"], "approval-pending")
self.assertEqual(rows[0]["prompt_kind"], "muse-approval")
self.assertEqual(rows[0]["prompt_key"], "1")
def test_tmux_failure_returns_empty(self):
patches = self._patched(tmux_rc=1, tmux_stdout="")
with patches[0], patches[1], patches[2], patches[3], patches[4]:
self.assertEqual(w.runtime_rows("/tmp/sock"), [])
def test_vanished_pane_skipped(self):
patches = self._patched(captures={"%37": None, "%38": STATE_SHELL})
with patches[0], patches[1], patches[2], patches[3], patches[4]:
rows = w.runtime_rows("/tmp/sock")
self.assertEqual([r["pane"] for r in rows], ["%38"])
def test_pane_state_found_and_missing(self):
patches = self._patched(captures={"%37": STATE_OPEN,
"%38": STATE_SHELL})
with patches[0], patches[1], patches[2], patches[3], patches[4]:
hit = w.pane_state("/tmp/sock", "%37")
miss = w.pane_state("/tmp/sock", "%99")
self.assertEqual(hit["pane"], "%37")
self.assertEqual(miss["error"], "no_such_pane")
def test_rows_flag_squeezed(self):
listing = ("muse\t1\t%37\tmuse-bin-1.4\t2880158\t35\t7\n"
"muse\t1\t%38\tbash\t2880200\t35\t7\n")
patches = self._patched(
tmux_stdout=listing,
captures={"%37": STATE_OPEN, "%38": STATE_SHELL})
with patches[0], patches[1], patches[2], patches[3], patches[4]:
rows = w.runtime_rows("/tmp/sock")
self.assertTrue(rows[0]["squeezed"])
self.assertEqual(rows[0]["width"], 35)
self.assertEqual(rows[0]["height"], 7)
self.assertTrue(rows[1]["squeezed"])
class TestSpreadTargets(unittest.TestCase):
def _row(self, pane, is_muse, squeezed):
return {"socket": "/tmp/s", "session": "muse", "window": "1",
"pane": pane, "is_muse": is_muse, "squeezed": squeezed}
def test_selects_squeezed_muse_only(self):
rows = [self._row("%22", True, False),
self._row("%23", True, True),
self._row("%38", False, True)]
targets = w.spread_targets(rows)
self.assertEqual([t["pane"] for t in targets], ["%23"])
def test_empty_when_nothing_squeezed(self):
rows = [self._row("%22", True, False)]
self.assertEqual(w.spread_targets(rows), [])
def test_tolerates_missing_keys(self):
self.assertEqual(w.spread_targets([{"pane": "%1"}]), [])
class TestBoxRuntimeCLI(unittest.TestCase):
def _box(self, *argv, timeout=60):
import subprocess
cmd = [sys.executable, str(BIN_DIR / "super-cli.py"),
"runtime"] + list(argv)
return subprocess.run(cmd, capture_output=True, text=True,
timeout=timeout)
def test_list_empty_socket_json(self):
r = self._box("list", "--socket", "/nonexistent.sock", "--json")
self.assertEqual(r.returncode, 0, r.stderr[:500])
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertEqual(data["runtimes"], [])
def test_list_muse_only_flag_accepted(self):
r = self._box("list", "--socket", "/nonexistent.sock", "--json",
"--muse-only")
self.assertEqual(r.returncode, 0, r.stderr[:500])
self.assertTrue(json.loads(r.stdout)["ok"])
def test_subcommand_help(self):
for sub in ("list", "send", "launch", "layout", "spread"):
r = self._box(sub, "--help")
self.assertEqual(r.returncode, 0, sub)
def test_launch_dry_run_injects_approve(self):
r = self._box("launch", "--session", "probe-x",
"--dry-run", "--json")
self.assertEqual(r.returncode, 0, r.stderr[:500])
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertTrue(data["dry_run"])
self.assertEqual(data["injected"], ["--disable-approval"])
self.assertIn("--disable-approval", data["cmdline"])
self.assertIn("muse-code", data["cmdline"])
def test_launch_dry_run_respects_caller_flags(self):
r = self._box("launch", "--session", "probe-x",
"--dry-run", "--json", "--", "--yolo")
self.assertEqual(r.returncode, 0, r.stderr[:500])
data = json.loads(r.stdout)
self.assertEqual(data["injected"], [])
self.assertIn("--yolo", data["cmdline"])
self.assertNotIn("--disable-approval", data["cmdline"])
def test_send_missing_pane_json(self):
r = self._box("send", "--socket", "/nonexistent.sock",
"%99", "hi", "--json")
self.assertEqual(r.returncode, 0, r.stderr[:500])
data = json.loads(r.stdout)
self.assertFalse(data["ok"])
self.assertEqual(data["error"], "no_such_pane")
def test_layout_empty_socket_json(self):
r = self._box("layout", "--socket", "/nonexistent.sock", "--json")
self.assertEqual(r.returncode, 0, r.stderr[:500])
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertEqual(data["runtimes"], [])
self.assertIn("width", data["minimum"])
self.assertIn("height", data["minimum"])
def test_spread_empty_socket_json(self):
r = self._box("spread", "--socket", "/nonexistent.sock", "--json")
self.assertEqual(r.returncode, 0, r.stderr[:500])
data = json.loads(r.stdout)
self.assertTrue(data["ok"])
self.assertEqual(data["spread"], [])
def test_spread_dry_run_empty_socket_json(self):
r = self._box("spread", "--socket", "/nonexistent.sock",
"--dry-run", "--json")
self.assertEqual(r.returncode, 0, r.stderr[:500])
data = json.loads(r.stdout)
self.assertTrue(data["dry_run"])
self.assertEqual(data["targets"], [])
if __name__ == "__main__":
unittest.main()
+309
View File
@@ -0,0 +1,309 @@
"""Tests for `box fleet heal` and `box watchdog` in super-cli.py.
Heal drives lock cleanup, watchdog-timer install, tunnel restart, and
egress/relay verification; watchdog status/run expose the systemd
watchdog layer. Shell-outs are faked; no sudo/systemctl/netns touch
the host.
"""
import argparse
import importlib.util
import io
import json
import sys
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest import mock
REPO_ROOT = Path(__file__).resolve().parent.parent
SPEC = importlib.util.spec_from_file_location(
"super_cli_heal", REPO_ROOT / "bin" / "super-cli.py")
super_cli = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(super_cli)
def _ns(**over):
kw = dict(node=None, target=None, json=True)
kw.update(over)
return argparse.Namespace(**kw)
class FakeSh:
"""Programmable stand-in for super_cli._sh; records calls."""
def __init__(self):
self.calls = []
self.handlers = []
def on(self, *needles, rc=0, out=""):
self.handlers.append((needles, rc, out))
return self
def __call__(self, cmd, timeout=120, input_text=None):
self.calls.append((list(cmd), timeout, input_text))
blob = " ".join(cmd)
for needles, rc, out in self.handlers:
if all(n in blob for n in needles):
return rc, out() if callable(out) else out
raise AssertionError("unexpected command: %r" % (cmd,))
class HealBase(unittest.TestCase):
def setUp(self):
import tempfile
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.systemd = self.root / "systemd"
self.systemd.mkdir()
self.locks = self.root / "locks"
self.locks.mkdir()
self.patchers = [
mock.patch.object(super_cli, "SYSTEMD_SYSTEM_DIR", self.systemd),
mock.patch.object(super_cli, "WATCHDOG_LOCK_TMPL",
str(self.locks / "chromebox-watchdog-{node}.lock")),
]
for p in self.patchers:
p.start()
self.addCleanup(self._unpatch)
self.sh = FakeSh()
self.sh_mock = mock.patch.object(super_cli, "_sh", self.sh)
self.sh_mock.start()
self.addCleanup(self.sh_mock.stop)
def _unpatch(self):
for p in self.patchers:
p.stop()
def run_heal(self, node="dev", as_json=True):
buf = io.StringIO()
with redirect_stdout(buf):
with self.assertRaises(SystemExit) as cm:
super_cli.cmd_fleet_heal(_ns(node=node, json=as_json))
return cm.exception.code, buf.getvalue()
class FleetHealTests(HealBase):
def _healthy_sh(self):
(self.sh
.on("systemctl", "enable", "--now", rc=0, out="")
.on("netvm-node-up.sh", "dev", rc=0,
out="tunnel already up (egress=1.2.3.4), skipping handshake wait\n"
"node=dev netns=warp-dev egress=1.2.3.4")
.on("curl", rc=0, out="ip=1.2.3.4\nfoo=bar"))
def test_heal_recovered(self):
(self.systemd / "chromebox-watchdog-dev.timer").write_text("x")
self._healthy_sh()
with mock.patch.object(super_cli, "probe_cdp_status",
return_value={"ok": True, "latency_ms": 12}):
code, out = self.run_heal()
self.assertEqual(code, 0)
data = json.loads(out)
self.assertEqual(data["verdict"], "RECOVERED")
self.assertTrue(data["ok"])
self.assertEqual([s["step"] for s in data["steps"]],
["lock", "timer", "tunnel", "egress", "relay"])
self.assertTrue(all(s["ok"] for s in data["steps"]))
def test_heal_down_when_egress_fails(self):
(self.systemd / "chromebox-watchdog-dev.timer").write_text("x")
(self.sh
.on("systemctl", "enable", "--now", rc=0, out="")
.on("netvm-node-up.sh", "dev", rc=0,
out="no handshake yet (endpoint=162.159.192.1)\n"
"node=dev netns=warp-dev egress=unknown")
.on("curl", rc=7, out="curl: (7) couldn't connect"))
with mock.patch.object(super_cli, "probe_cdp_status",
return_value={"ok": False, "error": "refused",
"latency_ms": None}):
code, out = self.run_heal()
self.assertEqual(code, 1)
data = json.loads(out)
self.assertEqual(data["verdict"], "DOWN")
self.assertFalse(data["ok"])
by_step = {s["step"]: s for s in data["steps"]}
self.assertFalse(by_step["tunnel"]["ok"])
self.assertFalse(by_step["egress"]["ok"])
def test_heal_down_prints_identity_guidance(self):
(self.systemd / "chromebox-watchdog-dev.timer").write_text("x")
(self.sh
.on("systemctl", rc=0, out="")
.on("netvm-node-up.sh", rc=0, out="node=dev egress=unknown")
.on("curl", rc=7, out="fail"))
with mock.patch.object(super_cli, "probe_cdp_status",
return_value={"ok": False, "error": "x",
"latency_ms": None}):
code, out = self.run_heal(as_json=False)
self.assertEqual(code, 1)
self.assertIn("netvm-new-identity.sh dev", out)
def test_heal_installs_missing_timer(self):
seen = {}
def fake(cmd, timeout=120, input_text=None):
blob = " ".join(cmd)
self.sh.calls.append((list(cmd), timeout, input_text))
if "tee" in blob:
seen["tee_target"] = cmd[-1]
seen["tee_input"] = input_text
return 0, ""
if "daemon-reload" in blob:
seen["reload"] = True
return 0, ""
if "enable" in blob:
return 0, ""
if "netvm-node-up.sh" in blob:
return 0, "node=dev egress=1.2.3.4"
if "curl" in blob:
return 0, "ip=1.2.3.4"
raise AssertionError("unexpected: %r" % (cmd,))
with mock.patch.object(super_cli, "_sh", fake):
with mock.patch.object(
super_cli, "probe_cdp_status",
return_value={"ok": True, "latency_ms": 3}):
code, _ = self.run_heal()
self.assertEqual(code, 0)
self.assertEqual(seen["tee_target"],
str(self.systemd / "chromebox-watchdog-dev.timer"))
self.assertIn("Unit=chromebox-watchdog@dev.service", seen["tee_input"])
self.assertTrue(seen["reload"])
def test_heal_clears_unwritable_lock(self):
(self.systemd / "chromebox-watchdog-dev.timer").write_text("x")
lock = self.locks / "chromebox-watchdog-dev.lock"
lock.write_text("")
lock.chmod(0o444)
self._healthy_sh()
self.sh.on("rm", str(lock), rc=0, out="")
with mock.patch.object(super_cli, "probe_cdp_status",
return_value={"ok": True, "latency_ms": 3}):
code, out = self.run_heal()
self.assertEqual(code, 0)
data = json.loads(out)
by_step = {s["step"]: s for s in data["steps"]}
self.assertTrue(by_step["lock"]["ok"])
self.assertIn("removed stale lock", by_step["lock"]["detail"])
self.assertTrue(any("rm" in " ".join(c[0]) for c in self.sh.calls))
def test_heal_rejects_unknown_node(self):
buf = io.StringIO()
with redirect_stdout(buf):
with self.assertRaises(SystemExit) as cm:
super_cli.cmd_fleet_heal(_ns(node="ghost"))
self.assertEqual(cm.exception.code, 1)
class ShHelperTests(unittest.TestCase):
def test_timeout_and_oserror(self):
import subprocess as real_subprocess
with mock.patch.object(super_cli.subprocess, "run",
side_effect=real_subprocess.TimeoutExpired("x", 1)):
self.assertEqual(super_cli._sh(["x"], timeout=1)[0], 124)
with mock.patch.object(super_cli.subprocess, "run",
side_effect=OSError("nope")):
self.assertEqual(super_cli._sh(["x"])[0], 127)
class WatchdogTests(unittest.TestCase):
def setUp(self):
self.sh = FakeSh()
self.sh_mock = mock.patch.object(super_cli, "_sh", self.sh)
self.sh_mock.start()
self.addCleanup(self.sh_mock.stop)
def _unit_states(self, missing=()):
def fake(cmd, timeout=120, input_text=None):
self.sh.calls.append((list(cmd), timeout, input_text))
unit = cmd[-1]
if "is-enabled" in cmd or "is-active" in cmd:
return (1, "") if unit in missing else (0, "")
if cmd[:3] == ["sudo", "systemctl", "start"]:
return 0, ""
raise AssertionError("unexpected: %r" % (cmd,))
return fake
def test_status_json(self):
stub = mock.Mock()
stub.collect.return_value = {
n: {"browser": "healthy", "browser_detail": "silent",
"cdp": "healthy", "cdp_detail": "silent"}
for n in super_cli.VALID_NODES}
with mock.patch.object(super_cli, "_sh", self._unit_states(
missing=("chromebox-watchdog-def.timer",))):
with mock.patch.dict(sys.modules, {"host_evidence": stub}):
buf = io.StringIO()
with redirect_stdout(buf):
super_cli.cmd_watchdog_status(_ns(json=True))
data = json.loads(buf.getvalue())
self.assertTrue(data["ok"])
self.assertEqual(len(data["nodes"]), 6)
by_node = {n["node"]: n for n in data["nodes"]}
self.assertFalse(by_node["def"]["enabled"])
self.assertFalse(by_node["def"]["active"])
self.assertTrue(by_node["dev"]["active"])
self.assertEqual(by_node["dev"]["browser"], "healthy")
self.assertIn("timer", data["relay"])
def test_status_survives_missing_evidence(self):
with mock.patch.object(super_cli, "_sh", self._unit_states()):
with mock.patch.dict(sys.modules, {"host_evidence": None}):
buf = io.StringIO()
with redirect_stdout(buf):
super_cli.cmd_watchdog_status(_ns(json=True))
data = json.loads(buf.getvalue())
self.assertTrue(data["ok"])
self.assertEqual(data["nodes"][0]["browser"], "unknown")
def test_run_node_and_relay(self):
for target, unit in (("dev", "chromebox-watchdog@dev.service"),
("relay", "cdp-relay-watchdog.service")):
with self.subTest(target=target):
with mock.patch.object(super_cli, "_sh",
self._unit_states()) as _:
buf = io.StringIO()
with redirect_stdout(buf):
with self.assertRaises(SystemExit) as cm:
super_cli.cmd_watchdog_run(
_ns(target=target, json=True))
self.assertEqual(cm.exception.code, 0)
data = json.loads(buf.getvalue())
self.assertTrue(data["ok"])
self.assertEqual(data["unit"], unit)
def test_run_rejects_bad_target(self):
with self.assertRaises(SystemExit) as cm:
super_cli.cmd_watchdog_run(_ns(target="ghost"))
self.assertEqual(cm.exception.code, 1)
def test_run_reports_start_failure(self):
def fake(cmd, timeout=120, input_text=None):
return 1, "Failed to start"
with mock.patch.object(super_cli, "_sh", fake):
buf = io.StringIO()
with redirect_stdout(buf):
with self.assertRaises(SystemExit) as cm:
super_cli.cmd_watchdog_run(_ns(target="dev", json=True))
self.assertEqual(cm.exception.code, 1)
self.assertFalse(json.loads(buf.getvalue())["ok"])
class ParserTests(unittest.TestCase):
def test_fleet_heal_parses(self):
args = super_cli.build_parser().parse_args(["fleet", "heal", "dev"])
self.assertEqual((args.domain, args.action, args.node),
("fleet", "heal", "dev"))
def test_watchdog_parses(self):
args = super_cli.build_parser().parse_args(["watchdog", "run", "relay"])
self.assertEqual((args.domain, args.action, args.target),
("watchdog", "run", "relay"))
args = super_cli.build_parser().parse_args(["watchdog"])
self.assertEqual((args.domain, args.action), ("watchdog", "status"))
if __name__ == "__main__":
unittest.main()
+134 -3
View File
@@ -129,6 +129,17 @@ class DialogTests(unittest.TestCase):
side_effect=RuntimeError("boom")): side_effect=RuntimeError("boom")):
self.assertIsNone(dialog.dialog_text(mock.Mock())) self.assertIsNone(dialog.dialog_text(mock.Mock()))
def test_dialog_text_rejects_non_string(self):
with mock.patch.object(dialog, "cdp_evaluate", return_value=123):
self.assertIsNone(dialog.dialog_text(mock.Mock()))
def test_describe_rows_rejects_non_list(self):
with mock.patch.object(dialog, "goto_tab", return_value=True), \
mock.patch.object(dialog, "cdp_evaluate",
return_value="error"):
self.assertEqual(dialog.describe_rows(mock.Mock(),
"Permissions"), [])
def test_click_row_success_and_no_match(self): def test_click_row_success_and_no_match(self):
with mock.patch.object(dialog, "cdp_evaluate", with mock.patch.object(dialog, "cdp_evaluate",
side_effect=["CLICKED", "CLICKED", side_effect=["CLICKED", "CLICKED",
@@ -147,6 +158,11 @@ class DialogTests(unittest.TestCase):
"Permissions")) "Permissions"))
self.assertEqual(cdp.call_count, 1) self.assertEqual(cdp.call_count, 1)
def test_row_js_excludes_tab_rail(self):
blob = json.dumps(dialog.TAB_NAMES)
self.assertIn(blob, dialog.JS_CLICK_ROW_TMPL)
self.assertIn(blob, dialog.JS_DESCRIBE_ROWS)
def test_describe_rows_maps(self): def test_describe_rows_maps(self):
rows = [{"name": "Websites", "help": "1 site", "extra": ""}] rows = [{"name": "Websites", "help": "1 site", "extra": ""}]
with mock.patch.object(dialog, "goto_tab", return_value=True), \ with mock.patch.object(dialog, "goto_tab", return_value=True), \
@@ -183,8 +199,8 @@ class ControlsTests(unittest.TestCase):
on = [{"heading": "Connector defaults", "value": "auto_allow", on = [{"heading": "Connector defaults", "value": "auto_allow",
"checked": True}] "checked": True}]
with mock.patch.object(controls, "cdp_evaluate", with mock.patch.object(controls, "cdp_evaluate",
side_effect=["CLICKED", off, side_effect=["CLICKED"] + [off] * 10
{"x": 5, "y": 6}, on]), \ + [{"x": 5, "y": 6}, on]), \
mock.patch.object(controls, "real_click") as click: mock.patch.object(controls, "real_click") as click:
self.assertTrue(controls.set_radio_by_heading( self.assertTrue(controls.set_radio_by_heading(
mock.Mock(), "Connector defaults", "auto_allow")) mock.Mock(), "Connector defaults", "auto_allow"))
@@ -194,7 +210,8 @@ class ControlsTests(unittest.TestCase):
off = [{"heading": "Connector defaults", "value": "auto_allow", off = [{"heading": "Connector defaults", "value": "auto_allow",
"checked": False}] "checked": False}]
with mock.patch.object(controls, "cdp_evaluate", with mock.patch.object(controls, "cdp_evaluate",
side_effect=["CLICKED", off, None]), \ side_effect=["CLICKED"] + [off] * 10
+ [None]), \
mock.patch.object(controls, "real_click"): mock.patch.object(controls, "real_click"):
self.assertFalse(controls.set_radio_by_heading( self.assertFalse(controls.set_radio_by_heading(
mock.Mock(), "Connector defaults", "auto_allow")) mock.Mock(), "Connector defaults", "auto_allow"))
@@ -234,6 +251,22 @@ class ControlsTests(unittest.TestCase):
self.assertTrue(controls.set_switch(mock.Mock(), self.assertTrue(controls.set_switch(mock.Mock(),
"Transparent proxy", True)) "Transparent proxy", True))
def test_set_switch_polls_then_lands(self):
off = [{"label": "Transparent proxy row", "aria": "",
"checked": False}]
on = [{"label": "Transparent proxy row", "aria": "",
"checked": True}]
with mock.patch.object(controls, "cdp_evaluate",
side_effect=[off, "CLICKED", off, off, on]):
self.assertTrue(controls.set_switch(mock.Mock(),
"Transparent proxy", True))
def test_listers_reject_wrong_types(self):
with mock.patch.object(controls, "cdp_evaluate",
return_value="error"):
self.assertEqual(controls.list_radios(mock.Mock()), [])
self.assertEqual(controls.list_switches(mock.Mock()), [])
class TogglesTests(unittest.TestCase): class TogglesTests(unittest.TestCase):
def test_resolve_static_website_protocol(self): def test_resolve_static_website_protocol(self):
@@ -248,6 +281,14 @@ class TogglesTests(unittest.TestCase):
"permissions.protocols:Agent Skills endpoints") "permissions.protocols:Agent Skills endpoints")
self.assertEqual(spec["label"], "Agent Skills endpoints") self.assertEqual(spec["label"], "Agent Skills endpoints")
def test_resolve_protocol_network_and_substring(self):
spec = toggles.resolve_toggle("permissions.protocols:outbound-ssh")
self.assertEqual(spec["label"], "Outbound SSH")
spec = toggles.resolve_toggle("permissions.protocols:ssh")
self.assertEqual(spec["label"], "Outbound SSH")
with self.assertRaises(toggles.MenuError):
toggles.resolve_toggle("permissions.protocols:mail")
def test_resolve_unknowns_raise_before_cdp(self): def test_resolve_unknowns_raise_before_cdp(self):
for bad in ("nope", "", "permissions.websites:", for bad in ("nope", "", "permissions.websites:",
"permissions.protocols:nope"): "permissions.protocols:nope"):
@@ -315,6 +356,23 @@ class TogglesTests(unittest.TestCase):
res = toggles.set_toggle("pip", "permissions.web_access", res = toggles.set_toggle("pip", "permissions.web_access",
"always_ask") "always_ask")
self.assertEqual((res["ok"], res["value"]), (True, "always_ask")) self.assertEqual((res["ok"], res["value"]), (True, "always_ask"))
self.assertNotIn("readback_only", res)
def test_set_toggle_readback_only_success(self):
with mock.patch.object(toggles, "get_cdp_ws",
return_value=(mock.Mock(), {})), \
mock.patch.object(dialog, "open_settings",
return_value=True), \
mock.patch.object(dialog, "goto_tab", return_value=True), \
mock.patch.object(controls, "set_radio_by_heading",
return_value=False), \
mock.patch.object(toggles, "get_toggle",
return_value={"ok": True,
"value": "always_ask"}):
res = toggles.set_toggle("pip", "permissions.web_access",
"always_ask")
self.assertTrue(res["ok"])
self.assertTrue(res["readback_only"])
def test_set_toggle_readback_mismatch_fails(self): def test_set_toggle_readback_mismatch_fails(self):
with mock.patch.object(toggles, "get_cdp_ws", with mock.patch.object(toggles, "get_cdp_ws",
@@ -405,6 +463,15 @@ class TabContractTests(unittest.TestCase):
"Model Context Protocol servers (SSE)") "Model Context Protocol servers (SSE)")
self.assertEqual(permissions.resolve_protocol( self.assertEqual(permissions.resolve_protocol(
"Agent Skills endpoints"), "Agent Skills endpoints") "Agent Skills endpoints"), "Agent Skills endpoints")
self.assertEqual(permissions.resolve_protocol("outbound-ssh"),
"Outbound SSH")
self.assertEqual(permissions.resolve_protocol("Outbound SSH"),
"Outbound SSH")
self.assertEqual(permissions.resolve_protocol("ssh"),
"Outbound SSH")
self.assertEqual(permissions.resolve_protocol("tcp"),
"Other TCP connections")
self.assertIsNone(permissions.resolve_protocol("mail"))
self.assertIsNone(permissions.resolve_protocol("nope")) self.assertIsNone(permissions.resolve_protocol("nope"))
self.assertIsNone(permissions.resolve_protocol("")) self.assertIsNone(permissions.resolve_protocol(""))
self.assertIsNone(permissions.resolve_protocol(None)) self.assertIsNone(permissions.resolve_protocol(None))
@@ -415,12 +482,76 @@ class TabContractTests(unittest.TestCase):
mock.Mock(), "x.com", "Sometimes")) mock.Mock(), "x.com", "Sometimes"))
ev.assert_not_called() ev.assert_not_called()
def test_set_website_noop_without_click(self):
rows = [{"host": "x.com", "mode": "Allow", "x": 1, "y": 2}]
with mock.patch.object(permissions, "_websites_raw",
return_value=rows), \
mock.patch.object(permissions, "_back_to_root",
return_value=True), \
mock.patch.object(permissions, "real_click") as click, \
mock.patch("time.sleep"):
self.assertTrue(permissions.set_website_mode(
mock.Mock(), "x.com", "Allow"))
click.assert_not_called()
def test_set_website_absent_host_fails(self):
with mock.patch.object(permissions, "_websites_raw",
return_value=[]), \
mock.patch.object(permissions, "_back_to_root",
return_value=True), \
mock.patch("time.sleep"):
self.assertFalse(permissions.set_website_mode(
mock.Mock(), "y.com", "Ask"))
def test_set_website_ask_verifies_by_absence(self):
rows = [{"host": "x.com", "mode": "Allow", "x": 1, "y": 2}]
items = [{"text": "Allow"}, {"text": "Ask"}, {"text": "Deny"}]
with mock.patch.object(permissions, "_websites_raw",
return_value=rows), \
mock.patch.object(permissions, "_back_to_root",
return_value=True), \
mock.patch.object(permissions, "real_click"), \
mock.patch.object(permissions, "_eval",
side_effect=[items, "CLICKED", []]), \
mock.patch("time.sleep"):
self.assertTrue(permissions.set_website_mode(
mock.Mock(), "x.com", "Ask"))
def test_stable_rows_waits_for_agreement(self):
rows = [{"host": "x.com"}]
with mock.patch.object(permissions, "_eval",
side_effect=[None, rows, rows]), \
mock.patch("time.sleep"):
self.assertEqual(
permissions._stable_rows(mock.Mock(), "js"), rows)
def test_stable_rows_gives_up(self):
with mock.patch.object(permissions, "_eval", return_value=None), \
mock.patch("time.sleep"):
self.assertIsNone(
permissions._stable_rows(mock.Mock(), "js"))
def test_back_to_root_prefers_back_affordance(self): def test_back_to_root_prefers_back_affordance(self):
with mock.patch.object(dialog, "go_back", return_value=True), \ with mock.patch.object(dialog, "go_back", return_value=True), \
mock.patch.object(dialog, "dialog_text", mock.patch.object(dialog, "dialog_text",
return_value="Manage permissions\nrows"): return_value="Manage permissions\nrows"):
self.assertTrue(permissions._back_to_root(mock.Mock())) self.assertTrue(permissions._back_to_root(mock.Mock()))
def test_theme_values_match_live(self):
self.assertIn("avatar", general.THEME_VALUES)
self.assertNotIn("match", general.THEME_VALUES)
def test_data_controls_pinned_label(self):
self.assertEqual(data_controls.SWITCH_LABEL,
"Help improve our AI models")
sws = [{"label": "Something else", "aria": "", "checked": False},
{"label": "Help improve our AI models", "aria": "",
"checked": True}]
with mock.patch.object(dialog, "goto_tab", return_value=True), \
mock.patch.object(controls, "list_switches",
return_value=sws):
self.assertTrue(data_controls.ai_improvement(mock.Mock()))
def test_data_controls_single_switch(self): def test_data_controls_single_switch(self):
one = [{"label": "Help improve the model", "aria": "", one = [{"label": "Help improve the model", "aria": "",
"checked": True}] "checked": True}]
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -272,8 +272,8 @@ class TestPromptTUIIntegration(unittest.TestCase):
target_text = sends[0]["text"] target_text = sends[0]["text"]
h, w = self.tui.stdscr.getmaxyx() h, w = self.tui.stdscr.getmaxyx()
modal_w = min(74, w - 6) modal_w = min(84 if self.tui.modal in ("prompts", "history_search") else 74, w - 6)
modal_h = min(20, h - 4) modal_h = min(22 if self.tui.modal in ("prompts", "history_search") else 20, h - 4)
top_y = (h - modal_h) // 2 top_y = (h - modal_h) // 2
left_x = (w - modal_w) // 2 left_x = (w - modal_w) // 2
+11
View File
@@ -198,6 +198,17 @@ class TestRateLimitUserInteraction(unittest.TestCase):
self.assertEqual(updated[0]["text"], "Earlier reply") self.assertEqual(updated[0]["text"], "Earlier reply")
self.assertEqual(updated[1]["text"], "New uncommitted instruction") self.assertEqual(updated[1]["text"], "New uncommitted instruction")
def test_history_payload_mentioning_rate_limits_not_falsely_flagged(self):
"""Chat history discussing rate limits or HTTP 429 does NOT place node into cooldown when rc == 0."""
msgs_about_rate_limits = [
{"role": "user", "text": "Are we hitting any rate limits or 429 errors?"},
{"role": "assistant", "text": "No rate limit reached, all queues are unthrottled and healthy."}
]
with patch.object(muse_tui_rl, "run_command_isolated", return_value=(0, json.dumps(msgs_about_rate_limits), "")):
self.tui.data._fetch_history("pip", "thread-chat")
self.assertFalse(self.tui.data.is_node_rate_limited("pip"))
self.assertEqual(len(self.tui.data.history_cache.get(("pip", "thread-chat"), [])), 2)
if __name__ == "__main__": if __name__ == "__main__":
import json import json
+50
View File
@@ -0,0 +1,50 @@
"""Regression tests for the hyphenated swarm-prune branch.
`swarm-prune` was dispatched but had no handler branch, so it exited 0
with no output. It now mirrors `swarm prune` (preview counts without
--confirm, archive only with --confirm). These tests never pass
--confirm, so they never write.
"""
import json
import subprocess
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
BOX_CTL = REPO_ROOT / "bin" / "box-ctl.py"
def _box_ctl(*args):
return subprocess.run(
[sys.executable, str(BOX_CTL), *args],
capture_output=True, text=True, timeout=120)
class SwarmPruneTests(unittest.TestCase):
def test_hyphenated_prune_previews_without_confirm(self):
r = _box_ctl("swarm-prune")
self.assertNotEqual(r.returncode, 0)
self.assertTrue(r.stdout.strip(), "hyphenated prune must emit JSON")
payload = json.loads(r.stdout)
self.assertEqual(payload["code"], "CONFIRM_REQUIRED")
self.assertIn("matching_count", payload["detail"])
self.assertEqual(payload["detail"]["stale_hours"], 6)
def test_hyphenated_matches_space_form(self):
hyphen = json.loads(_box_ctl("swarm-prune").stdout)
space = json.loads(_box_ctl("swarm", "prune").stdout)
self.assertEqual(hyphen["code"], "CONFIRM_REQUIRED")
self.assertEqual(space["code"], "CONFIRM_REQUIRED")
self.assertEqual(sorted(hyphen["detail"]),
sorted(space["detail"]))
def test_bad_stale_hours_falls_back_to_default(self):
r = _box_ctl("swarm-prune", "--stale-hours", "bogus")
payload = json.loads(r.stdout)
self.assertEqual(payload["code"], "CONFIRM_REQUIRED")
self.assertEqual(payload["detail"]["stale_hours"], 6)
if __name__ == "__main__":
unittest.main()
+100
View File
@@ -0,0 +1,100 @@
"""Watchdog coverage: every registry node is supervised.
Regression test for the dev/def outage (2026-10-06): both watchdogs
hardcoded the original four nodes (muse/pip/646/opm), so dev/def had no
browser supervision, no Warp-tunnel auto-recovery, and no relay
supervision. A dead tunnel paged CRITICAL partition alerts forever with
nothing acting on it.
Both scripts now resolve nodes/ports from the fleet registry
(bin/netvm-registry.py). These tests drive the real shell code sourced
with a LIB_ONLY guard (same pattern as test_agent_health.py).
"""
import importlib.util
import subprocess
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CHROMEBOX_WD = REPO_ROOT / "bin" / "chromebox-watchdog.sh"
RELAY_WD = REPO_ROOT / "bin" / "cdp-relay-watchdog.sh"
# The fleet's pinned contract (also pinned in bin/netvm-names.sh).
EXPECTED_PORTS = {
"muse": 9410, "pip": 9420, "646": 9430,
"opm": 9440, "def": 9450, "dev": 9455,
}
EXPECTED_PEERS = {
"muse": "10.201.35.2", "pip": "10.201.87.2", "646": "10.201.202.2",
"opm": "10.201.157.2", "def": "10.201.66.2", "dev": "10.201.36.2",
}
def _load(mod_name, rel_path):
spec = importlib.util.spec_from_file_location(mod_name, REPO_ROOT / rel_path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _chromebox(profile, snippet):
prog = "set -- '%s'\nsource '%s'\n%s\n" % (profile, CHROMEBOX_WD, snippet)
env = {"PATH": "/usr/bin:/bin", "CHROMEBOX_WATCHDOG_LIB_ONLY": "1"}
return subprocess.run(["bash", "-c", prog], capture_output=True,
text=True, env=env, timeout=30)
def _relay(snippet):
prog = "source '%s'\n%s\n" % (RELAY_WD, snippet)
env = {"PATH": "/usr/bin:/bin", "CDP_RELAY_WATCHDOG_LIB_ONLY": "1"}
return subprocess.run(["bash", "-c", prog], capture_output=True,
text=True, env=env, timeout=30)
class RegistryContract(unittest.TestCase):
def test_registry_matches_pinned_ports(self):
reg = _load("netvm_registry_cov", "bin/netvm-registry.py")
self.assertEqual(reg.active_nodes(), EXPECTED_PORTS)
for node, peer in EXPECTED_PEERS.items():
self.assertEqual(reg.peer_ip_for(node), peer)
class ChromeboxWatchdogPorts(unittest.TestCase):
def test_all_registry_nodes_resolve_to_pinned_ports(self):
for node, port in sorted(EXPECTED_PORTS.items()):
with self.subTest(node=node):
r = _chromebox(node, "echo \"PORT=$CDP_PORT\"")
self.assertEqual(r.returncode, 0, r.stderr)
line = next((ln for ln in r.stdout.splitlines()
if ln.startswith("PORT=")), None)
self.assertIsNotNone(
line, "no PORT line: stdout=%r stderr=%r"
% (r.stdout, r.stderr))
self.assertEqual(int(line.split("=", 1)[1]), port)
def test_unknown_profile_rejected(self):
r = _chromebox("ghost", "echo UNREACHABLE")
self.assertNotEqual(r.returncode, 0)
self.assertIn("unknown profile", r.stderr)
self.assertNotIn("UNREACHABLE", r.stdout)
class RelayWatchdogCoverage(unittest.TestCase):
def test_watched_nodes_covers_registry(self):
r = _relay("watched_nodes")
self.assertEqual(r.returncode, 0, r.stderr)
nodes = set(r.stdout.split())
for node in EXPECTED_PORTS:
self.assertIn(node, nodes)
def test_relay_targets_use_pinned_ports(self):
for node, port in sorted(EXPECTED_PORTS.items()):
with self.subTest(node=node):
r = _relay("relay_target %s" % node)
self.assertEqual(r.returncode, 0, r.stderr)
self.assertEqual(r.stdout.strip(),
"%s:%d" % (EXPECTED_PEERS[node], port))
if __name__ == "__main__":
unittest.main()