switchroom 0.15.2 → 0.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * UAT — `/model` Telegram command (PR #2259, shipped v0.15.2).
3
+ *
4
+ * Serves: `reference/vision.md` outcome 2 (you hold the leash) — the
5
+ * operator can see and switch the agent's Claude model from Telegram
6
+ * without SSH. Session-scoped switch via claude's own `/model <name>`
7
+ * REPL verb injected into the tmux pane.
8
+ *
9
+ * Three assertions against a real agent over real Telegram:
10
+ *
11
+ * 1. Bare `/model` → shows the configured model (never opens claude's
12
+ * interactive picker — the reply must come from the gateway, fast,
13
+ * containing "Configured:").
14
+ * 2. `/model <valid-name>` → switch is injected; reply relays claude's
15
+ * response and carries the session-only persistence note.
16
+ * 3. `/model bogus-name` → still a reply (claude's inline error is
17
+ * relayed, or the empty-capture explanation) — never silence.
18
+ *
19
+ * The switch test sets the model to the SAME value the agent already
20
+ * runs (sonnet) so the canary doesn't leave the harness agent on a
21
+ * different model afterwards.
22
+ */
23
+
24
+ import { describe, it, expect } from "vitest";
25
+ import { spinUp } from "../harness.js";
26
+
27
+ const AGENT = "test-harness";
28
+ const REPLY_TIMEOUT_MS = 30_000;
29
+
30
+ describe("uat: /model command — show, switch, bad-name", () => {
31
+ it(
32
+ "bare /model shows the model dashboard (menu v2) or static fallback (v1)",
33
+ async () => {
34
+ const sc = await spinUp({ agent: AGENT });
35
+ try {
36
+ await sc.sendDM("/model");
37
+ // v2 (picker-driven menu): "Now: <model>"; v1 / fallback path:
38
+ // "Configured: <model>". Either proves the gateway handled the
39
+ // command rather than forwarding it to claude as plain text.
40
+ const shape = /Now:|Configured:/i;
41
+ const reply = await sc.expectMessage(shape, {
42
+ from: "bot",
43
+ timeout: REPLY_TIMEOUT_MS,
44
+ });
45
+ expect(reply.text).toMatch(shape);
46
+ // Persistence caveat present on both shapes
47
+ expect(reply.text).toMatch(/switchroom\.yaml/i);
48
+ } finally {
49
+ await sc.tearDown();
50
+ }
51
+ },
52
+ 60_000,
53
+ );
54
+
55
+ it(
56
+ "/model sonnet switches the live session (same-value, no net change)",
57
+ async () => {
58
+ const sc = await spinUp({ agent: AGENT });
59
+ try {
60
+ await sc.sendDM("/model sonnet");
61
+ // Accept either a relayed claude response or the explicit
62
+ // empty-capture explanation — both prove the command routed
63
+ // through the gateway handler (and neither is silence).
64
+ const reply = await sc.expectMessage(
65
+ /\/model sonnet|no response captured|Session-only/i,
66
+ { from: "bot", timeout: REPLY_TIMEOUT_MS },
67
+ );
68
+ expect(reply.text.length).toBeGreaterThan(0);
69
+ } finally {
70
+ await sc.tearDown();
71
+ }
72
+ },
73
+ 60_000,
74
+ );
75
+
76
+ it(
77
+ "/model bogus-name still gets a reply (error relayed, never silence)",
78
+ async () => {
79
+ const sc = await spinUp({ agent: AGENT });
80
+ try {
81
+ await sc.sendDM("/model bogus-model-name-xyz");
82
+ const reply = await sc.expectMessage(/\S/, {
83
+ from: "bot",
84
+ timeout: REPLY_TIMEOUT_MS,
85
+ });
86
+ expect(reply.text.length).toBeGreaterThan(0);
87
+ } finally {
88
+ await sc.tearDown();
89
+ }
90
+ },
91
+ 60_000,
92
+ );
93
+ });
@@ -66,6 +66,14 @@ export type StatusProbeRow = {
66
66
  export type AgentMetadata = {
67
67
  agentName: string;
68
68
  model: string | null;
69
+ /**
70
+ * Live session-model override set via the `/model` picker (session-only,
71
+ * resets on restart). When present it's what the agent is ACTUALLY running
72
+ * right now, distinct from `model` (the persistent configured model). Null
73
+ * when no session switch is active — then `/status` just shows `model`.
74
+ * Surfaced so `/status` and `/model` never silently disagree.
75
+ */
76
+ sessionModel?: string | null;
69
77
  extendsProfile: string | null;
70
78
  topicName: string | null;
71
79
  topicEmoji: string | null;
@@ -122,7 +130,14 @@ export function formatAgentLine(meta: AgentMetadata): string {
122
130
  const topic = meta.topicName
123
131
  ? ` · topic: ${escapeHtml([meta.topicEmoji, meta.topicName].filter(Boolean).join(" "))}`
124
132
  : "";
125
- return `<b>${escapeHtml(meta.agentName)}</b> · model: <code>${escapeHtml(m)}</code>${topic}`;
133
+ // A live `/model` session switch overrides what's running. Show it next to
134
+ // the configured model so the two surfaces agree (the override resets on
135
+ // restart, when the session reverts to the configured model).
136
+ const session =
137
+ meta.sessionModel && meta.sessionModel.length > 0
138
+ ? ` · live session: <code>${escapeHtml(meta.sessionModel)}</code>`
139
+ : "";
140
+ return `<b>${escapeHtml(meta.agentName)}</b> · model: <code>${escapeHtml(m)}</code>${session}${topic}`;
126
141
  }
127
142
 
128
143
  /**
@@ -1,40 +0,0 @@
1
- # HEARTBEAT.md — Proactive Check-Ins
2
-
3
- This file is read on every turn (it's a dynamic workspace file). Edit it
4
- to tell yourself what to look for when someone (or something) prompts you
5
- with a bare "heartbeat" — a cron firing, a quiet-period nudge, or a
6
- scheduled check-in.
7
-
8
- ## When this fires
9
-
10
- A heartbeat arrives as a user-role message with no real payload — often
11
- just "HEARTBEAT" or "heartbeat check". When that happens:
12
-
13
- 1. Run through the bullets below in order.
14
- 2. If anything needs action, respond normally (and take the action).
15
- 3. If nothing needs action, respond with exactly `HEARTBEAT_OK` on its
16
- own line. The plugin suppresses that as a silent reply — no Telegram
17
- message gets sent, and the user isn't notified.
18
-
19
- ## Things to check (customize per-agent)
20
-
21
- - **New emails / messages:** is there anything in the inbox or
22
- connected channels that looks actionable?
23
- - **Upcoming calendar events:** anything in the next ~2 hours the user
24
- should be reminded of?
25
- - **Long-running tasks:** any background work you kicked off earlier
26
- that might have completed?
27
- - **Today's plan:** anything in `memory/YYYY-MM-DD.md` (today's
28
- daily note, auto-loaded into context by the dynamic workspace hook)
29
- that hasn't been touched?
30
-
31
- ## Guidelines
32
-
33
- - **Respect quiet hours.** If it's late (local time 22:00–08:00),
34
- default to `HEARTBEAT_OK` unless something is genuinely urgent.
35
- - **Don't spam.** If you messaged the user in the last 30 minutes,
36
- `HEARTBEAT_OK` unless there's something new to add.
37
- - **Stay terse.** A heartbeat-initiated message should be one or two
38
- lines, not a paragraph.
39
-
40
- Edit this file to narrow or broaden the check set for this agent.