@@ -325,3 +325,246 @@ def test_maybe_run_sweep_runs_after_cooldown(home: Path) -> None:
325325 out = decay .maybe_run_sweep (k , now = when_now )
326326 assert out is not None
327327 assert out .archived == 1
328+
329+
330+ # ── Defensive paths (rarely-hit error branches) ──────────────────────
331+
332+
333+ def test_run_sweep_uses_default_knowledge_dir_when_omitted (home : Path ) -> None :
334+ """Calling without a kwarg should resolve to the env-pointed home."""
335+ # ``home`` fixture sets AGENT_MEM_HOME and creates knowledge/.
336+ result = decay .run_sweep ()
337+ # Empty library → no archives, but the sweep should still write state.
338+ assert result .archived == 0
339+ assert decay .read_last_sweep_at () is not None
340+
341+
342+ def test_run_sweep_missing_knowledge_dir_returns_empty_result (
343+ tmp_path : Path , monkeypatch : pytest .MonkeyPatch
344+ ) -> None :
345+ """Knowledge dir doesn't exist — should be a no-op but still update state."""
346+ monkeypatch .setenv ("AGENT_MEM_HOME" , str (tmp_path ))
347+ # Note: no `knowledge/` dir created.
348+ result = decay .run_sweep ()
349+ assert result .archived == 0
350+ assert result .kept == 0
351+ # Sweep-state still written so the cooldown ticks.
352+ assert decay .read_last_sweep_at () is not None
353+
354+
355+ def test_archive_destination_returns_none_for_outside_path (home : Path ) -> None :
356+ k = home / "knowledge"
357+ outside = home / "outside" / "foo.md"
358+ outside .parent .mkdir (parents = True , exist_ok = True )
359+ outside .write_text ("---\n id: x\n ---\n \n body\n " )
360+ assert decay ._archive_destination (outside , k ) is None
361+
362+
363+ def test_run_sweep_continues_after_unreadable_entry (
364+ home : Path , monkeypatch : pytest .MonkeyPatch
365+ ) -> None :
366+ """An entry that can't be parsed shouldn't abort the sweep — it
367+ just counts toward `errored` and the loop continues to other
368+ entries."""
369+ k = home / "knowledge"
370+ _write_entry (k / "global" / "good.md" , id_ = "good" , title = "Good" , created = "2026-01-01" )
371+ bad = k / "global" / "bad.md"
372+ bad .parent .mkdir (parents = True , exist_ok = True )
373+ bad .write_text ("not even close to a markdown entry\n " ) # no frontmatter
374+
375+ when = datetime (2026 , 5 , 27 , tzinfo = timezone .utc )
376+ result = decay .run_sweep (knowledge_dir = k , now = when )
377+ # Good one archived, bad one counted as errored.
378+ assert result .archived == 1
379+ assert result .errored == 1
380+
381+
382+ def test_run_sweep_catches_unexpected_exception_per_entry (
383+ home : Path , monkeypatch : pytest .MonkeyPatch
384+ ) -> None :
385+ """A truly-unexpected exception (e.g. permissions) on one entry
386+ shouldn't crash the sweep — it goes into the errored bucket."""
387+ k = home / "knowledge"
388+ _write_entry (k / "global" / "good.md" , id_ = "good" , title = "Good" , created = "2026-01-01" )
389+
390+ # Patch the parser to raise mid-loop.
391+ original = decay ._read_and_parse_frontmatter
392+ calls : list [Path ] = []
393+
394+ def _flaky (path ):
395+ calls .append (path )
396+ if "good" in str (path ):
397+ raise RuntimeError ("simulated read failure" )
398+ return original (path )
399+
400+ monkeypatch .setattr (decay , "_read_and_parse_frontmatter" , _flaky )
401+ when = datetime (2026 , 5 , 27 , tzinfo = timezone .utc )
402+ result = decay .run_sweep (knowledge_dir = k , now = when )
403+ assert result .errored == 1
404+ assert result .archived == 0
405+
406+
407+ def test_maybe_run_sweep_short_circuits_when_lock_held (home : Path ) -> None :
408+ """If another sweep is in flight, the second caller bails out."""
409+ k = home / "knowledge"
410+ with decay ._SWEEP_LOCK :
411+ # Inside the lock, maybe_run_sweep should refuse to run.
412+ out = decay .maybe_run_sweep (k )
413+ assert out is None
414+
415+
416+ def test_parse_iso_date_handles_invalid_strings () -> None :
417+ assert decay ._parse_iso_date (None ) is None
418+ assert decay ._parse_iso_date ("" ) is None
419+ assert decay ._parse_iso_date ("not-a-date" ) is None
420+ assert decay ._parse_iso_date ("2026-13-99" ) is None # invalid month/day
421+ assert decay ._parse_iso_date ("2026-05-27" ) == date (2026 , 5 , 27 )
422+
423+
424+ def test_reinforced_count_handles_bad_values () -> None :
425+ assert decay ._reinforced_count ({}) == 0
426+ assert decay ._reinforced_count ({"reinforced" : None }) == 0
427+ assert decay ._reinforced_count ({"reinforced" : "not a number" }) == 0
428+ assert decay ._reinforced_count ({"reinforced" : - 3 }) == 0 # clamped to 0
429+ assert decay ._reinforced_count ({"reinforced" : 7 }) == 7
430+
431+
432+ def test_arousal_pinned_accepts_both_field_names () -> None :
433+ """``arousal_pin`` and ``arousal_pinned`` both work — the design
434+ doc switches between them so accept either."""
435+ assert decay ._is_arousal_pinned ({"arousal_pin" : True }) is True
436+ assert decay ._is_arousal_pinned ({"arousal_pinned" : True }) is True
437+ assert decay ._is_arousal_pinned ({}) is False
438+ assert decay ._is_arousal_pinned ({"arousal_pin" : False }) is False
439+
440+
441+ def test_write_last_sweep_at_swallows_oserror (home : Path , monkeypatch : pytest .MonkeyPatch ) -> None :
442+ """If the sweep-state file can't be written, the function returns
443+ silently — bookkeeping must never crash the sweep itself."""
444+ monkeypatch .setattr (Path , "mkdir" , _raise_oserror )
445+ # Should not raise.
446+ decay .write_last_sweep_at (datetime (2026 , 5 , 27 , tzinfo = timezone .utc ))
447+
448+
449+ def _raise_oserror (* args , ** kwargs ):
450+ raise OSError ("simulated" )
451+
452+
453+ def test_atomic_write_entry_swallows_oserror (home : Path , monkeypatch : pytest .MonkeyPatch ) -> None :
454+ """The atomic-write helper returns False on disk error (rather than
455+ propagating) so callers can degrade gracefully."""
456+ p = home / "knowledge" / "global" / "foo.md"
457+ _write_entry (p , id_ = "foo" , title = "Foo" )
458+ monkeypatch .setattr (Path , "write_text" , _raise_oserror )
459+ ok = decay ._atomic_write_entry (p , {"id" : "foo" }, "body" )
460+ assert ok is False
461+
462+
463+ def test_archive_entry_returns_false_on_destination_write_failure (
464+ home : Path , monkeypatch : pytest .MonkeyPatch
465+ ) -> None :
466+ """If the archive destination write fails, the source must stay
467+ in place (no partial archive)."""
468+ k = home / "knowledge"
469+ p = k / "global" / "old.md"
470+ _write_entry (p , id_ = "old" , title = "Old" , created = "2026-01-01" )
471+ parsed = decay ._read_and_parse_frontmatter (p )
472+ assert parsed is not None
473+ fm , _raw , body = parsed
474+ monkeypatch .setattr (Path , "write_text" , _raise_oserror )
475+ ok = decay ._archive_entry (p , k , fm , body , today_iso = "2026-05-27" )
476+ assert ok is False
477+ # Source untouched.
478+ assert p .exists ()
479+
480+
481+ def test_iter_entries_skips_top_level_admin_files (home : Path ) -> None :
482+ """index.md and log.md at the top level are catalog files, not
483+ user entries — the sweep doesn't churn them."""
484+ k = home / "knowledge"
485+ (k / "index.md" ).write_text ("---\n id: index\n ---\n \n # Index\n " )
486+ (k / "log.md" ).write_text ("---\n id: log\n ---\n \n # Log\n " )
487+ _write_entry (k / "global" / "real.md" , id_ = "real" , title = "Real" , created = "2026-01-01" )
488+ entries = decay ._iter_entries (k )
489+ names = {e .name for e in entries }
490+ assert "real.md" in names
491+ assert "index.md" not in names
492+ assert "log.md" not in names
493+
494+
495+ def test_read_last_sweep_at_returns_none_for_wrong_field_type (home : Path ) -> None :
496+ """JSON with last_sweep_at as a non-string should be rejected."""
497+ (home / "sweep-state.json" ).write_text ('{"last_sweep_at": 12345}' )
498+ assert decay .read_last_sweep_at () is None
499+
500+
501+ def test_read_last_sweep_at_returns_none_for_unparseable_iso (home : Path ) -> None :
502+ """A string that isn't ISO format should be rejected."""
503+ (home / "sweep-state.json" ).write_text ('{"last_sweep_at": "tuesday"}' )
504+ assert decay .read_last_sweep_at () is None
505+
506+
507+ def test_read_last_sweep_at_returns_none_when_top_level_not_dict (home : Path ) -> None :
508+ """JSON array at top level is not a sweep-state record."""
509+ (home / "sweep-state.json" ).write_text ('["not", "a", "dict"]' )
510+ assert decay .read_last_sweep_at () is None
511+
512+
513+ def test_archive_entry_returns_true_even_when_source_unlink_fails (
514+ home : Path , monkeypatch : pytest .MonkeyPatch
515+ ) -> None :
516+ """If the destination write succeeded but source removal failed,
517+ the archive is effectively done — return True and log a warning."""
518+ k = home / "knowledge"
519+ p = k / "global" / "old.md"
520+ _write_entry (p , id_ = "old" , title = "Old" , created = "2026-01-01" )
521+ parsed = decay ._read_and_parse_frontmatter (p )
522+ assert parsed is not None
523+ fm , _raw , body = parsed
524+
525+ real_unlink = Path .unlink
526+
527+ def _selective_unlink_failure (self , * args , ** kwargs ):
528+ # Only fail when unlinking the source entry — let temp-file
529+ # cleanup work normally.
530+ if str (self ) == str (p ):
531+ raise OSError ("simulated unlink failure" )
532+ return real_unlink (self , * args , ** kwargs )
533+
534+ monkeypatch .setattr (Path , "unlink" , _selective_unlink_failure )
535+ ok = decay ._archive_entry (p , k , fm , body , today_iso = "2026-05-27" )
536+ # Destination is in place, so the archival did happen.
537+ assert ok is True
538+ archived = k / "_archive" / "global" / "old.md"
539+ assert archived .exists ()
540+
541+
542+ def test_archive_entry_returns_false_when_destination_resolution_fails (
543+ home : Path , monkeypatch : pytest .MonkeyPatch
544+ ) -> None :
545+ """A path that doesn't resolve under the knowledge dir gets a
546+ None destination and skips archival."""
547+ k = home / "knowledge"
548+ # Create entry OUTSIDE the knowledge dir.
549+ outside = home / "loose" / "stray.md"
550+ outside .parent .mkdir (parents = True , exist_ok = True )
551+ outside .write_text ("---\n id: stray\n created: '2026-01-01'\n ---\n \n body\n " )
552+ parsed = decay ._read_and_parse_frontmatter (outside )
553+ assert parsed is not None
554+ fm , _raw , body = parsed
555+ ok = decay ._archive_entry (outside , k , fm , body , today_iso = "2026-05-27" )
556+ assert ok is False
557+
558+
559+ def test_archive_entry_returns_false_on_mkdir_failure (
560+ home : Path , monkeypatch : pytest .MonkeyPatch
561+ ) -> None :
562+ k = home / "knowledge"
563+ p = k / "global" / "old.md"
564+ _write_entry (p , id_ = "old" , title = "Old" , created = "2026-01-01" )
565+ parsed = decay ._read_and_parse_frontmatter (p )
566+ assert parsed is not None
567+ fm , _raw , body = parsed
568+ monkeypatch .setattr (Path , "mkdir" , _raise_oserror )
569+ ok = decay ._archive_entry (p , k , fm , body , today_iso = "2026-05-27" )
570+ assert ok is False
0 commit comments