switchroom 0.21.18 → 0.21.19

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.
@@ -189,5 +189,75 @@ class KillSwitchOffTests(BufferJoinBase):
189
189
  self.assertNotIn("a prefetched memory", ctx)
190
190
 
191
191
 
192
+ class StaleBufferTokenTests(BufferJoinBase):
193
+ """F3 — a buffer consumed on turn N must NOT be re-served as fresh on
194
+ turns N+1..N+k when no strictly-newer sentinel has been produced."""
195
+
196
+ def test_consumed_buffer_is_not_reserved_as_fresh_next_turn(self):
197
+ recall_buffer.write_buffer(SESSION, "- a prefetched memory", {})
198
+ recall_buffer.write_sentinel(SESSION)
199
+ cfg = self._config(prefetch_enabled=True)
200
+
201
+ # Turn N+1: fresh hit — the buffer is consumed and its token recorded.
202
+ out1 = self._run(cfg, _DirectiveClient())
203
+ ctx1 = json.loads(out1)["hookSpecificOutput"]["additionalContext"]
204
+ self.assertIn("a prefetched memory", ctx1)
205
+
206
+ # Turn N+2: the producer did NOT run again (no new sentinel). The same
207
+ # on-disk buffer must now read as STALE (already-consumed token), so its
208
+ # memory must NOT be injected again. On the pre-fix code — which hard-
209
+ # wired last_consumed_token=None — read_if_fresh reported the old
210
+ # sentinel "fresh" and re-injected the turn-N memory here.
211
+ out2 = self._run(cfg, _DirectiveClient())
212
+ ctx2 = json.loads(out2)["hookSpecificOutput"]["additionalContext"]
213
+ self.assertNotIn(
214
+ "a prefetched memory", ctx2,
215
+ "a buffer consumed on a prior turn must not be re-served as fresh",
216
+ )
217
+
218
+ def test_a_newer_sentinel_is_served_after_consumption(self):
219
+ # Positive control: once the producer writes a STRICTLY-NEWER sentinel,
220
+ # the fresh path serves again — the token gate rejects only re-reads of
221
+ # an ALREADY-consumed sentinel, never a genuinely new one.
222
+ recall_buffer.write_buffer(SESSION, "- memory one", {})
223
+ recall_buffer.write_sentinel(SESSION)
224
+ cfg = self._config(prefetch_enabled=True)
225
+
226
+ out1 = self._run(cfg, _DirectiveClient())
227
+ self.assertIn("memory one", json.loads(out1)["hookSpecificOutput"]["additionalContext"])
228
+
229
+ # New turn's producer output.
230
+ recall_buffer.write_buffer(SESSION, "- memory two", {})
231
+ recall_buffer.write_sentinel(SESSION)
232
+ out2 = self._run(cfg, _DirectiveClient())
233
+ ctx2 = json.loads(out2)["hookSpecificOutput"]["additionalContext"]
234
+ self.assertIn("memory two", ctx2)
235
+
236
+
237
+ class ColdSessionSyncRecallTests(BufferJoinBase):
238
+ """F4 — a flag-on cold session's first turn (no sentinel ever, no prior
239
+ recall, no directives) must fall through to SYNCHRONOUS recall, not emit a
240
+ degraded banner and short-circuit it."""
241
+
242
+ class _MemClient:
243
+ def list_directives(self, bank_id, active_only=True, timeout=2):
244
+ return {"items": []}
245
+
246
+ def recall(self, bank_id, query, **kwargs):
247
+ return {"results": [{
248
+ "text": "sync recalled memory", "type": "fact",
249
+ "mentioned_at": "2026-01-01", "id": "s1", "scores": {"final": 0.9},
250
+ }]}
251
+
252
+ def test_cold_session_with_no_directives_runs_synchronous_recall(self):
253
+ out = self._run(self._config(prefetch_enabled=True), self._MemClient())
254
+ self.assertTrue(out)
255
+ ctx = json.loads(out)["hookSpecificOutput"]["additionalContext"]
256
+ self.assertIn(
257
+ "sync recalled memory", ctx,
258
+ "cold session's first turn must run synchronous recall, not a degraded no-op",
259
+ )
260
+
261
+
192
262
  if __name__ == "__main__":
193
263
  unittest.main()