boost-skill-cli 1.0.60__tar.gz → 1.0.62__tar.gz

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.
Files changed (117) hide show
  1. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/PKG-INFO +1 -1
  2. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/_version.py +2 -2
  3. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_skill_cli.egg-info/PKG-INFO +1 -1
  4. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/docs/roadmap.html +52 -1
  5. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_logs.py +97 -0
  6. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/.gitattributes +0 -0
  7. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/.gitignore +0 -0
  8. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/CLAUDE.md +0 -0
  9. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/CONTRIBUTING.md +0 -0
  10. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/LICENSE +0 -0
  11. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/MANIFEST.in +0 -0
  12. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/Makefile +0 -0
  13. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/README.md +0 -0
  14. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/SECURITY.md +0 -0
  15. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost +0 -0
  16. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/__init__.py +0 -0
  17. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/__main__.py +0 -0
  18. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/cli.py +0 -0
  19. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/cliparse.py +0 -0
  20. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/__init__.py +0 -0
  21. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/bmad.py +0 -0
  22. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/configuration.py +0 -0
  23. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/discovery.py +0 -0
  24. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/hooks.py +0 -0
  25. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/info.py +0 -0
  26. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/intelligence.py +0 -0
  27. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/pkg.py +0 -0
  28. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/quality.py +0 -0
  29. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/taps.py +0 -0
  30. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/commands/team.py +0 -0
  31. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/__init__.py +0 -0
  32. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/agents.py +0 -0
  33. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/ai.py +0 -0
  34. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/catalog.py +0 -0
  35. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/claude_settings.py +0 -0
  36. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/config.py +0 -0
  37. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/dense.py +0 -0
  38. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/embed.py +0 -0
  39. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/frontmatter.py +0 -0
  40. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/gitutil.py +0 -0
  41. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/journal.py +0 -0
  42. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/lockfile.py +0 -0
  43. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/logs.py +0 -0
  44. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/mcp.py +0 -0
  45. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/output.py +0 -0
  46. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/paths.py +0 -0
  47. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/policy.py +0 -0
  48. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/rag.py +0 -0
  49. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/registry.py +0 -0
  50. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/store.py +0 -0
  51. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/core/util.py +0 -0
  52. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/data/registries.json +0 -0
  53. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/errors.py +0 -0
  54. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_cli/spin.py +0 -0
  55. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_skill_cli.egg-info/SOURCES.txt +0 -0
  56. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
  57. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_skill_cli.egg-info/entry_points.txt +0 -0
  58. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_skill_cli.egg-info/requires.txt +0 -0
  59. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/boost_skill_cli.egg-info/top_level.txt +0 -0
  60. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/docs/.claude/settings.local.json +0 -0
  61. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/docs/DEBUGGING.md +0 -0
  62. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/docs/demo.gif +0 -0
  63. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/docs/demo.tape +0 -0
  64. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/docs/design-roadmap.html +0 -0
  65. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/docs/index.html +0 -0
  66. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/docs/mcp-hub.html +0 -0
  67. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/docs/rag-architecture.md +0 -0
  68. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/index.html +0 -0
  69. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/pyproject.toml +0 -0
  70. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/scripts/build_registries.py +0 -0
  71. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/scripts/mutation_gate.py +0 -0
  72. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/setup.cfg +0 -0
  73. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/style/README.md +0 -0
  74. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/style/boost.css +0 -0
  75. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/style/boost.js +0 -0
  76. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/style/demo.html +0 -0
  77. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/conftest.py +0 -0
  78. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_bmad.py +0 -0
  79. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_configuration.py +0 -0
  80. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_discovery.py +0 -0
  81. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_hooks.py +0 -0
  82. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_info.py +0 -0
  83. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_intelligence.py +0 -0
  84. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_pkg.py +0 -0
  85. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_quality.py +0 -0
  86. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_taps.py +0 -0
  87. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_cli_team.py +0 -0
  88. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/functional/test_everyday_loop.py +0 -0
  89. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/make_fixture.py +0 -0
  90. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/smoke.sh +0 -0
  91. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_agents.py +0 -0
  92. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_ai.py +0 -0
  93. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_catalog.py +0 -0
  94. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_claude_settings.py +0 -0
  95. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_cliparse.py +0 -0
  96. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_config.py +0 -0
  97. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_dense.py +0 -0
  98. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_dense_fallback.py +0 -0
  99. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_embed.py +0 -0
  100. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_errors_and_cli_table.py +0 -0
  101. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_frontmatter.py +0 -0
  102. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_gitutil.py +0 -0
  103. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_journal.py +0 -0
  104. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_lockfile.py +0 -0
  105. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_mcp.py +0 -0
  106. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_mutation_hardening.py +0 -0
  107. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_output.py +0 -0
  108. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_paths.py +0 -0
  109. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_policy.py +0 -0
  110. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_rag.py +0 -0
  111. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_registries_fresh.py +0 -0
  112. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_registry.py +0 -0
  113. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_spin.py +0 -0
  114. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_store.py +0 -0
  115. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_token_parity.py +0 -0
  116. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_util.py +0 -0
  117. {boost_skill_cli-1.0.60 → boost_skill_cli-1.0.62}/tests/unit/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.60
3
+ Version: 1.0.62
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.0.60'
22
- __version_tuple__ = version_tuple = (1, 0, 60)
21
+ __version__ = version = '1.0.62'
22
+ __version_tuple__ = version_tuple = (1, 0, 62)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.60
3
+ Version: 1.0.62
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -114,7 +114,7 @@
114
114
  <!-- ================= SNAPSHOT ================= -->
115
115
  <section class="block reveal">
116
116
  <div class="stats">
117
- <div class="stat"><b>5</b><span>Shipped</span></div>
117
+ <div class="stat"><b>6</b><span>Shipped</span></div>
118
118
  <div class="stat"><b>3</b><span>Next up</span></div>
119
119
  <div class="stat"><b>4</b><span>Planned</span></div>
120
120
  <div class="stat"><b>48</b><span>Loop finds</span></div>
@@ -193,6 +193,24 @@
193
193
  </div>
194
194
  </article>
195
195
 
196
+ <article class="cap rcard">
197
+ <div class="head"><span class="pill shipped">Shipped</span><span class="cat">Testing · Resilience</span></div>
198
+ <h3>Crash-recorder error paths — <code>core/logs.py</code></h3>
199
+ <p>The black-box diagnostic logger swallows every filesystem/handler
200
+ failure so a broken log can never break the CLI — but those
201
+ <code>except</code> arms (the parts that matter most <em>during</em> a
202
+ crash) were the least exercised. New tests force each failure —
203
+ handler <code>close()</code> raising, a read-only log dir, an
204
+ unresolvable version, a wedged logger, <code>unlink</code> denied —
205
+ taking the module to <strong>100%</strong> coverage.</p>
206
+ <div class="meta">
207
+ <span class="m">Complexity <b>S</b></span>
208
+ <span class="m">Impact <b>Med</b></span>
209
+ <span class="m">Wow <b class="wow">★★★</b></span>
210
+ <span class="m">7 branches · 88%→100%</span>
211
+ </div>
212
+ </article>
213
+
196
214
  <article class="cap rcard">
197
215
  <div class="head"><span class="pill shipped">Shipped</span><span class="cat">Infra · DX</span></div>
198
216
  <h3>Autonomous ship-workflow &amp; isolated worktree</h3>
@@ -856,6 +874,23 @@
856
874
  </div>
857
875
  <div class="rgrid">
858
876
 
877
+ <article class="cap rcard">
878
+ <div class="head"><span class="pill planned">Planned</span><span class="cat">DX · Design system</span></div>
879
+ <h3>Promote nav / footer into the shared style system</h3>
880
+ <p>Canonical <code>style/boost.css</code> ships tokens + primitives but
881
+ <strong>no nav, logo, or footer</strong>, so every page (the guide and
882
+ both roadmaps) redefines that chrome inline — and the Design Roadmap
883
+ currently renders its nav <strong>unstyled</strong> because of the gap.
884
+ Add <code>.site-nav</code>/<code>.logo</code>/<code>footer</code>
885
+ primitives to the one sheet so the chrome is defined once and can't drift.</p>
886
+ <div class="meta">
887
+ <span class="m">Complexity <b>S</b></span>
888
+ <span class="m">Impact <b>Med</b></span>
889
+ <span class="m">Wow <b class="wow">★★★</b></span>
890
+ <span class="m"><code>style/boost.css</code></span>
891
+ </div>
892
+ </article>
893
+
859
894
  <article class="cap rcard">
860
895
  <div class="head"><span class="pill planned">Planned</span><span class="cat">DX · Infra</span></div>
861
896
  <h3>Shift-left gate — <code>pre-commit</code> + pre-commit.ci</h3>
@@ -992,6 +1027,22 @@
992
1027
  </div>
993
1028
  <div class="rgrid">
994
1029
 
1030
+ <article class="cap rcard">
1031
+ <div class="head"><span class="pill next">Next</span><span class="cat">Docs · Discoverability</span></div>
1032
+ <h3>Make the roadmaps discoverable</h3>
1033
+ <p>Neither <code>roadmap.html</code> nor <code>design-roadmap.html</code> is
1034
+ linked from <code>docs/index.html</code> or the README — the living
1035
+ roadmaps are unreachable unless you already know the URL. Add nav entries
1036
+ on the Visual Guide and a "Roadmap" link in the README so the two boards
1037
+ are one click away and the work stays visible.</p>
1038
+ <div class="meta">
1039
+ <span class="m">Complexity <b>S</b></span>
1040
+ <span class="m">Impact <b>Med</b></span>
1041
+ <span class="m">Wow <b class="wow">★★★</b></span>
1042
+ <span class="m">2 nav links</span>
1043
+ </div>
1044
+ </article>
1045
+
995
1046
  <article class="cap rcard">
996
1047
  <div class="head"><span class="pill planned">Planned</span><span class="cat">Docs · Perf</span></div>
997
1048
  <h3>Lighthouse CI on the Pages site</h3>
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import logging
5
+ import pathlib
5
6
 
6
7
  import pytest
7
8
 
@@ -151,6 +152,102 @@ def test_console_level_resolution(env, monkeypatch):
151
152
  assert logs._console_level(False, False, False) is None
152
153
 
153
154
 
155
+ # --- defensive / best-effort error branches --------------------------------
156
+ # logs.py is the black-box recorder: a broken log must never break the CLI, so
157
+ # every filesystem/handler touchpoint swallows its own errors. Those `except`
158
+ # arms are the parts that matter most in a crash, yet are the least exercised —
159
+ # these tests force each failure so the swallow is real, not just declared.
160
+
161
+ def test_reset_swallows_a_handler_that_fails_to_close(env):
162
+ logs.configure()
163
+ logger = logs.get_logger()
164
+
165
+ class BadHandler(logging.Handler):
166
+ def close(self):
167
+ raise RuntimeError("cannot close")
168
+
169
+ def emit(self, record):
170
+ pass
171
+
172
+ bad = BadHandler()
173
+ logger.addHandler(bad)
174
+ logs.reset() # must not raise despite close() blowing up
175
+ assert bad not in logger.handlers # …and still detaches the handler
176
+ assert not logger.handlers
177
+
178
+
179
+ def test_configure_survives_file_handler_oserror(env, monkeypatch):
180
+ def _boom(*a, **k):
181
+ raise OSError("read-only filesystem")
182
+
183
+ monkeypatch.setattr(logs.logging.handlers, "RotatingFileHandler", _boom)
184
+ logs.reset()
185
+ logger = logs.configure() # the OSError is swallowed, CLI proceeds
186
+ assert logger is logs.get_logger()
187
+ # no file handler was attached, and logging still works without raising
188
+ assert not any(isinstance(h, logging.handlers.RotatingFileHandler)
189
+ for h in logger.handlers)
190
+ logger.info("still-alive")
191
+
192
+
193
+ def test_boost_version_falls_back_to_unknown(env, monkeypatch):
194
+ import boost_cli
195
+ # simulate a build with no resolvable version (the `from .. import` fails)
196
+ monkeypatch.delattr(boost_cli, "__version__", raising=False)
197
+ monkeypatch.setitem(__import__("sys").modules, "boost_cli._version", None)
198
+ assert logs._boost_version() == "unknown"
199
+
200
+
201
+ def test_crash_report_swallows_trail_write_failure(env, monkeypatch):
202
+ """A broken logger must not stop the on-disk crash file from being written."""
203
+ real = logs.get_logger()
204
+
205
+ class BadLogger:
206
+ def error(self, *a, **k):
207
+ raise RuntimeError("logger is wedged")
208
+
209
+ monkeypatch.setattr(logs, "get_logger", lambda: BadLogger())
210
+ report = logs.write_crash_report(ValueError("boom"), ["x"])
211
+ monkeypatch.setattr(logs, "get_logger", lambda: real) # restore for teardown
212
+ assert report is not None and report.exists()
213
+ assert "boom" in report.read_text()
214
+
215
+
216
+ def test_crash_report_returns_none_when_file_write_fails(env, monkeypatch):
217
+ class BadDir:
218
+ def mkdir(self, *a, **k):
219
+ raise OSError("no space left on device")
220
+
221
+ monkeypatch.setattr(logs.paths, "logs_dir", lambda: BadDir())
222
+ assert logs.write_crash_report(ValueError("boom"), ["x"]) is None
223
+
224
+
225
+ def test_prune_returns_early_when_glob_fails(env, monkeypatch):
226
+ class BadDir:
227
+ def glob(self, pattern):
228
+ raise OSError("cannot list directory")
229
+
230
+ monkeypatch.setattr(logs.paths, "logs_dir", lambda: BadDir())
231
+ logs._prune_crash_reports() # must return quietly, not raise
232
+
233
+
234
+ def test_prune_swallows_unlink_failure_and_keeps_files(env, monkeypatch):
235
+ logs.configure()
236
+ monkeypatch.setattr(logs, "KEEP_CRASH_REPORTS", 1)
237
+ d = paths.logs_dir()
238
+ d.mkdir(parents=True, exist_ok=True)
239
+ for i in range(4):
240
+ (d / ("crash-t%02d.log" % i)).write_text("x", encoding="utf-8")
241
+
242
+ def _locked(self, *a, **k):
243
+ raise OSError("file is locked")
244
+
245
+ monkeypatch.setattr(pathlib.Path, "unlink", _locked)
246
+ logs._prune_crash_reports() # unlink raises for each stale file…
247
+ # …swallowed, so all four files survive (nothing deleted)
248
+ assert len(sorted(d.glob("crash-*.log"))) == 4
249
+
250
+
154
251
  # --- CLI integration -------------------------------------------------------
155
252
 
156
253
  def test_cli_logs_invocation(boost):
File without changes