switchroom 0.12.28 → 0.13.0

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,175 @@
1
+ /**
2
+ * Tests for the prefix-cache warmup module.
3
+ *
4
+ * Per cold-start TTFO RFC Option A: fire a synthetic inbound on
5
+ * bridge-up so Anthropic's prefix cache is warm by the user's next
6
+ * real message. These tests pin: env gate, cooldown, missing-target
7
+ * skip, message shape (meta.source="warmup", correct text), and
8
+ * cooldown debounce across multiple bridge reconnects.
9
+ */
10
+
11
+ import { describe, expect, it, beforeEach, vi } from 'vitest'
12
+ import {
13
+ __resetForTests,
14
+ maybeFireWarmup,
15
+ WARMUP_TEXT,
16
+ } from '../gateway/prefix-warmup'
17
+ import type { WarmupCtx } from '../gateway/prefix-warmup'
18
+
19
+ beforeEach(() => {
20
+ __resetForTests()
21
+ delete process.env.SWITCHROOM_PREFIX_WARMUP
22
+ })
23
+
24
+ function makeCtx(overrides?: Partial<WarmupCtx>): {
25
+ ctx: WarmupCtx
26
+ send: ReturnType<typeof vi.fn>
27
+ logs: string[]
28
+ } {
29
+ const send = vi.fn()
30
+ const logs: string[] = []
31
+ const ctx: WarmupCtx = {
32
+ selfAgent: 'test-agent',
33
+ client: { send, agentName: 'test-agent', id: 'c1', isAlive: () => true, lastHeartbeat: 0, close: () => {} } as never,
34
+ resolveBootTarget: () => ({ chatId: '12345', threadId: undefined }),
35
+ log: (l: string) => logs.push(l),
36
+ now: () => 1_000_000,
37
+ ...overrides,
38
+ }
39
+ return { ctx, send, logs }
40
+ }
41
+
42
+ describe('maybeFireWarmup — env gate', () => {
43
+ it('skipped when SWITCHROOM_PREFIX_WARMUP is unset', () => {
44
+ const { ctx, send } = makeCtx()
45
+ expect(maybeFireWarmup(ctx)).toBe(false)
46
+ expect(send).not.toHaveBeenCalled()
47
+ })
48
+
49
+ it('skipped when SWITCHROOM_PREFIX_WARMUP is 0', () => {
50
+ process.env.SWITCHROOM_PREFIX_WARMUP = '0'
51
+ const { ctx, send } = makeCtx()
52
+ expect(maybeFireWarmup(ctx)).toBe(false)
53
+ expect(send).not.toHaveBeenCalled()
54
+ })
55
+
56
+ it('fires when SWITCHROOM_PREFIX_WARMUP=1', () => {
57
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
58
+ const { ctx, send } = makeCtx()
59
+ expect(maybeFireWarmup(ctx)).toBe(true)
60
+ expect(send).toHaveBeenCalledTimes(1)
61
+ })
62
+ })
63
+
64
+ describe('maybeFireWarmup — message shape', () => {
65
+ beforeEach(() => {
66
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
67
+ })
68
+
69
+ it('tags meta.source="warmup"', () => {
70
+ const { ctx, send } = makeCtx()
71
+ maybeFireWarmup(ctx)
72
+ const msg = send.mock.calls[0][0]
73
+ expect(msg.meta.source).toBe('warmup')
74
+ })
75
+
76
+ it('uses synthetic messageId=0 and userId=0', () => {
77
+ const { ctx, send } = makeCtx()
78
+ maybeFireWarmup(ctx)
79
+ const msg = send.mock.calls[0][0]
80
+ expect(msg.messageId).toBe(0)
81
+ expect(msg.userId).toBe(0)
82
+ expect(msg.user).toBe('switchroom-warmup')
83
+ })
84
+
85
+ it('text carries the NO_REPLY instruction', () => {
86
+ const { ctx, send } = makeCtx()
87
+ maybeFireWarmup(ctx)
88
+ const msg = send.mock.calls[0][0]
89
+ expect(msg.text).toBe(WARMUP_TEXT)
90
+ expect(msg.text).toContain('__WARMUP_PING__')
91
+ expect(msg.text).toContain('NO_REPLY')
92
+ })
93
+
94
+ it('routes to the resolved boot chat', () => {
95
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
96
+ const { ctx, send } = makeCtx({
97
+ resolveBootTarget: () => ({ chatId: 'CHAT-9', threadId: 42 }),
98
+ })
99
+ maybeFireWarmup(ctx)
100
+ const msg = send.mock.calls[0][0]
101
+ expect(msg.chatId).toBe('CHAT-9')
102
+ expect(msg.threadId).toBe(42)
103
+ })
104
+
105
+ it('omits threadId when boot target has none', () => {
106
+ const { ctx, send } = makeCtx()
107
+ maybeFireWarmup(ctx)
108
+ const msg = send.mock.calls[0][0]
109
+ expect(msg.threadId).toBeUndefined()
110
+ })
111
+ })
112
+
113
+ describe('maybeFireWarmup — cooldown', () => {
114
+ beforeEach(() => {
115
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
116
+ })
117
+
118
+ it('second call within cooldown is skipped', () => {
119
+ const { ctx, send, logs } = makeCtx()
120
+ expect(maybeFireWarmup(ctx)).toBe(true)
121
+ // Same now() — well within cooldown.
122
+ expect(maybeFireWarmup(ctx)).toBe(false)
123
+ expect(send).toHaveBeenCalledTimes(1)
124
+ expect(logs.some((l) => l.includes('reason=cooldown'))).toBe(true)
125
+ })
126
+
127
+ it('call after cooldown elapses fires again', () => {
128
+ let t = 1_000_000
129
+ const ctx = makeCtx({ now: () => t }).ctx
130
+ expect(maybeFireWarmup(ctx)).toBe(true)
131
+ t += 5 * 60_000 + 1 // just past cooldown
132
+ expect(maybeFireWarmup(ctx)).toBe(true)
133
+ })
134
+
135
+ it('cooldown is per-agent', () => {
136
+ const a = makeCtx({ selfAgent: 'agent-a' })
137
+ const b = makeCtx({ selfAgent: 'agent-b' })
138
+ expect(maybeFireWarmup(a.ctx)).toBe(true)
139
+ expect(maybeFireWarmup(b.ctx)).toBe(true)
140
+ expect(maybeFireWarmup(a.ctx)).toBe(false) // a in cooldown
141
+ expect(maybeFireWarmup(b.ctx)).toBe(false) // b in cooldown
142
+ })
143
+ })
144
+
145
+ describe('maybeFireWarmup — missing boot target', () => {
146
+ beforeEach(() => {
147
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
148
+ })
149
+
150
+ it('skipped when no boot chat resolves', () => {
151
+ const { ctx, send, logs } = makeCtx({ resolveBootTarget: () => null })
152
+ expect(maybeFireWarmup(ctx)).toBe(false)
153
+ expect(send).not.toHaveBeenCalled()
154
+ expect(logs.some((l) => l.includes('reason=no-boot-chat-target'))).toBe(true)
155
+ })
156
+ })
157
+
158
+ describe('maybeFireWarmup — send error handling', () => {
159
+ beforeEach(() => {
160
+ process.env.SWITCHROOM_PREFIX_WARMUP = '1'
161
+ })
162
+
163
+ it('caught send throw returns false and does not mark cooldown', () => {
164
+ const send = vi.fn(() => {
165
+ throw new Error('boom')
166
+ })
167
+ const ctx = makeCtx({
168
+ client: { send } as never,
169
+ }).ctx
170
+ expect(maybeFireWarmup(ctx)).toBe(false)
171
+ // Cooldown NOT recorded — next call should attempt again.
172
+ expect(maybeFireWarmup(ctx)).toBe(false) // still throws
173
+ expect(send).toHaveBeenCalledTimes(2)
174
+ })
175
+ })