switchroom 0.21.12 → 0.21.14

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.
@@ -28,6 +28,7 @@ from lib.directives import ( # noqa: E402
28
28
  fetch_active_directives,
29
29
  fetch_active_directives_cached,
30
30
  format_active_directives_block,
31
+ injected_directive_ids,
31
32
  invalidate_directives_cache,
32
33
  parse_active_directives_block,
33
34
  rule_already_captured,
@@ -260,6 +261,51 @@ class FormatActiveDirectivesBlockTests(unittest.TestCase):
260
261
  # Honours a custom cap the same way the formatter does.
261
262
  self.assertEqual(count_omitted_directives(directives, max_directives=5), total - 5)
262
263
 
264
+ def test_injected_directive_ids_matches_the_rendered_block(self):
265
+ """Memory-redesign step 1 (E-45 recommendation (b)): the id list
266
+ `recall.py` puts on the recall_log row must name exactly the
267
+ directives `format_active_directives_block` actually rendered —
268
+ not the full fetched set, and in the same priority order."""
269
+ total = MAX_DIRECTIVES + 4
270
+ directives = [
271
+ _directive(f"d{i}", f"c{i}", priority=total - i) for i in range(total)
272
+ ]
273
+ with patch("sys.stderr", new=StringIO()):
274
+ out = format_active_directives_block(directives)
275
+ ids = injected_directive_ids(directives)
276
+ self.assertEqual(len(ids), MAX_DIRECTIVES)
277
+ # Real, concrete values — the head-slice in priority order, not a
278
+ # placeholder or a count.
279
+ self.assertEqual(ids, [f"id-d{i}" for i in range(MAX_DIRECTIVES)])
280
+ # Every injected id is actually present in the rendered block...
281
+ for i in range(MAX_DIRECTIVES):
282
+ self.assertIn(f"d{i}: c{i}", out)
283
+ # ...and the omitted tail's ids are excluded.
284
+ for i in range(MAX_DIRECTIVES, total):
285
+ self.assertNotIn(f"id-d{i}", ids)
286
+ self.assertNotIn(f"d{i}: c{i}", out)
287
+
288
+ def test_injected_directive_ids_under_cap_returns_all(self):
289
+ directives = [_directive(f"d{i}", f"c{i}", priority=5 - i) for i in range(3)]
290
+ self.assertEqual(injected_directive_ids(directives), ["id-d0", "id-d1", "id-d2"])
291
+
292
+ def test_injected_directive_ids_empty_list(self):
293
+ self.assertEqual(injected_directive_ids([]), [])
294
+
295
+ def test_injected_directive_ids_skips_malformed_entries(self):
296
+ directives = [
297
+ _directive("good", "content", priority=9),
298
+ {"priority": 5}, # no id — must not crash or contribute a None
299
+ "not-a-dict",
300
+ ]
301
+ self.assertEqual(injected_directive_ids(directives), ["id-good"])
302
+
303
+ def test_injected_directive_ids_honours_custom_cap(self):
304
+ directives = [_directive(f"d{i}", f"c{i}", priority=10 - i) for i in range(5)]
305
+ self.assertEqual(
306
+ injected_directive_ids(directives, max_directives=2), ["id-d0", "id-d1"]
307
+ )
308
+
263
309
  def test_no_warning_when_nothing_is_truncated(self):
264
310
  directives = [_directive("only", "single", priority=5)]
265
311
  with patch("sys.stderr", new=StringIO()) as fake_err:
@@ -470,6 +470,59 @@ class RecallTelemetryLogTests(unittest.TestCase):
470
470
  self.assertIsNone(e["injected_score_median"])
471
471
  self.assertIsNone(e["injected_score_max"])
472
472
 
473
+ def test_logs_injected_directive_ids(self):
474
+ """Memory-redesign step 1 (E-45 recommendation (b)): the recall_log
475
+ row must name WHICH directives were injected, not just how many.
476
+ `directive_count` alone can't answer "which directives were never
477
+ once injected" — this makes that queryable.
478
+ """
479
+ directives = [
480
+ _directive("first", "content one", priority=10),
481
+ _directive("second", "content two", priority=5),
482
+ ]
483
+ client = _FakeClient(directives=directives, memories=[])
484
+ _run_main_with(client)
485
+ e = self._read_log()[0]
486
+ self.assertEqual(e["directive_count"], 2)
487
+ self.assertEqual(e["directive_ids"], ["id-first", "id-second"])
488
+
489
+ def test_directive_ids_preserve_priority_order_across_more_than_a_few(self):
490
+ """Real values across several directives, in the rendered priority
491
+ order — not just a two-item happy path."""
492
+ directives = [
493
+ _directive(f"d{i}", f"c{i}", priority=10 - i) for i in range(6)
494
+ ]
495
+ client = _FakeClient(directives=directives, memories=[])
496
+ _run_main_with(client)
497
+ e = self._read_log()[0]
498
+ self.assertEqual(e["directive_count"], 6)
499
+ self.assertEqual(
500
+ e["directive_ids"],
501
+ ["id-d0", "id-d1", "id-d2", "id-d3", "id-d4", "id-d5"],
502
+ )
503
+
504
+ def test_directive_ids_null_on_cache_hit(self):
505
+ """A cache hit replays a formatted context block, not a fetched
506
+ directive list — `directive_ids` must be null (schema-uniform with
507
+ `directive_count`/`directives_omitted`), never a stale prior value.
508
+
509
+ Forces the cache-HIT branch directly (rather than relying on a
510
+ second `main()` call actually persisting a cache entry — the
511
+ integration harness patches `write_state` to a no-op, so
512
+ `_cache_store` never lands between calls)."""
513
+ client = _FakeClient(directives=[_directive("only", "content", priority=5)], memories=[])
514
+ with patch.object(recall, "_cache_lookup", return_value="[Hindsight] cached context"), \
515
+ patch.dict(os.environ, {"HINDSIGHT_RECALL_CACHE_TTL_SECS": "300"}):
516
+ _run_main_with(client)
517
+ entries = self._read_log()
518
+ self.assertEqual(len(entries), 1)
519
+ e = entries[0]
520
+ self.assertTrue(e["cache_hit"])
521
+ self.assertIsNone(e["directive_ids"])
522
+ # Schema-uniform with the fields it mirrors.
523
+ self.assertIsNone(e["directive_count"])
524
+ self.assertIsNone(e["directives_omitted"])
525
+
473
526
  def test_no_log_when_plugin_data_unset(self):
474
527
  # If CLAUDE_PLUGIN_DATA isn't set, the writer no-ops silently —
475
528
  # we don't want a stray log file in the working directory.