drydock-cli 3.0.70__tar.gz → 3.0.72__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 (95) hide show
  1. {drydock_cli-3.0.70/drydock_cli.egg-info → drydock_cli-3.0.72}/PKG-INFO +1 -1
  2. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/extract.py +29 -3
  3. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/app.py +53 -4
  4. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/widgets.py +1 -1
  5. {drydock_cli-3.0.70 → drydock_cli-3.0.72/drydock_cli.egg-info}/PKG-INFO +1 -1
  6. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/pyproject.toml +1 -1
  7. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_extract.py +24 -0
  8. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_tui.py +37 -0
  9. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/LICENSE +0 -0
  10. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/NOTICE +0 -0
  11. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/README.md +0 -0
  12. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/__init__.py +0 -0
  13. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/__main__.py +0 -0
  14. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/agent.py +0 -0
  15. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/bash_safety.py +0 -0
  16. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/__init__.py +0 -0
  17. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/rmf-categorize.md +0 -0
  18. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/rmf-control.md +0 -0
  19. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/rmf-poam.md +0 -0
  20. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/rmf-review.md +0 -0
  21. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/stig-assess.md +0 -0
  22. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/stig-remediate.md +0 -0
  23. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/cli.py +0 -0
  24. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/compaction.py +0 -0
  25. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/config.py +0 -0
  26. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/detect.py +0 -0
  27. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/gittools.py +0 -0
  28. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/graphrag.py +0 -0
  29. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/guards.py +0 -0
  30. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/loop_detect.py +0 -0
  31. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/mcp.py +0 -0
  32. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/providers.py +0 -0
  33. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/rmf.py +0 -0
  34. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/rmf_graph.py +0 -0
  35. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/skills.py +0 -0
  36. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/stig.py +0 -0
  37. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tool_registry.py +0 -0
  38. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tools/__init__.py +0 -0
  39. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/__init__.py +0 -0
  40. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/approval.py +0 -0
  41. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/messages.py +0 -0
  42. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tuning.py +0 -0
  43. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/web.py +0 -0
  44. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/SOURCES.txt +0 -0
  45. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/dependency_links.txt +0 -0
  46. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/entry_points.txt +0 -0
  47. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/requires.txt +0 -0
  48. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/top_level.txt +0 -0
  49. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/setup.cfg +0 -0
  50. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_approval.py +0 -0
  51. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_back_command.py +0 -0
  52. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_bash_output_bounding.py +0 -0
  53. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_bash_process_group.py +0 -0
  54. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_bash_safety.py +0 -0
  55. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_bash_timeout_network.py +0 -0
  56. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_cli_agents.py +0 -0
  57. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_compact_command.py +0 -0
  58. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_compaction.py +0 -0
  59. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_config.py +0 -0
  60. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_config_migration.py +0 -0
  61. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_context_limit_config.py +0 -0
  62. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_detect.py +0 -0
  63. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_dispatch.py +0 -0
  64. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_e2e_connected.py +0 -0
  65. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_empty_response.py +0 -0
  66. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_failure_loop.py +0 -0
  67. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_first_run_setup.py +0 -0
  68. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_gittools.py +0 -0
  69. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_graphrag.py +0 -0
  70. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_guards_and_tools.py +0 -0
  71. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_hallucinated_tools.py +0 -0
  72. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_leaked_tool_call.py +0 -0
  73. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_loop_detect.py +0 -0
  74. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_mcp.py +0 -0
  75. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_oneshot_unreachable.py +0 -0
  76. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_plan_autocontinue.py +0 -0
  77. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_providers_unreachable.py +0 -0
  78. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_read_index.py +0 -0
  79. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_rmf.py +0 -0
  80. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_rmf_graph.py +0 -0
  81. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_rmf_stig_graph.py +0 -0
  82. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_runaway_repetition.py +0 -0
  83. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_skills.py +0 -0
  84. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_stig.py +0 -0
  85. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_stop.py +0 -0
  86. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_streaming_newlines.py +0 -0
  87. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_subagent.py +0 -0
  88. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_system_prompt_help.py +0 -0
  89. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_todo.py +0 -0
  90. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_tool_arg_parsing.py +0 -0
  91. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_tools_undo.py +0 -0
  92. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_tuning.py +0 -0
  93. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_vision_input.py +0 -0
  94. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_web_tools.py +0 -0
  95. {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_xccdf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.70
3
+ Version: 3.0.72
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -19,8 +19,9 @@ import zipfile
19
19
  from pathlib import Path
20
20
 
21
21
  # Extensions we can pull text out of (beyond the plain-text formats GraphRAG
22
- # already reads directly).
23
- EXTRACTABLE_EXT = {".pdf", ".docx"}
22
+ # already reads directly). .ckl/.cklb are STIG checklists — we extract the
23
+ # meaningful per-rule text (id/severity/status/finding), not the raw XML/JSON.
24
+ EXTRACTABLE_EXT = {".pdf", ".docx", ".ckl", ".cklb"}
24
25
 
25
26
  _W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
26
27
 
@@ -70,13 +71,38 @@ def _extract_pdf(path: str | Path) -> str | None:
70
71
  return None
71
72
 
72
73
 
74
+ def _extract_checklist(path: str | Path) -> str | None:
75
+ """Flatten a STIG .ckl/.cklb to readable, queryable text (per-rule id,
76
+ severity, status, title, finding details) — so the agent can answer 'which
77
+ findings are open?' over an ingested checklist."""
78
+ try:
79
+ from drydock import stig
80
+
81
+ cl = stig.load(path)
82
+ except Exception: # noqa: BLE001 — unreadable/unsupported → skip cleanly
83
+ return None
84
+ host = cl.asset.get("HOST_NAME") or cl.asset.get("host_name") or "?"
85
+ lines = [f"STIG Checklist: {cl.stig_name or 'STIG'} (host: {host})",
86
+ "Counts: " + ", ".join(f"{k}={v}" for k, v in cl.counts().items())]
87
+ for r in cl.rules:
88
+ lines.append(f"\n{r.group_id} ({r.rule_id}) severity={r.severity} "
89
+ f"STATUS={r.status} — {r.title}")
90
+ if r.finding_details:
91
+ lines.append(f" Finding: {r.finding_details}")
92
+ if r.check_content:
93
+ lines.append(f" Check: {r.check_content[:300]}")
94
+ return "\n".join(lines) or None
95
+
96
+
73
97
  def extract_document(path: str | Path) -> str | None:
74
- """Extract text from a supported document (.pdf/.docx), or None."""
98
+ """Extract text from a supported document (.pdf/.docx/.ckl/.cklb), or None."""
75
99
  ext = Path(path).suffix.lower()
76
100
  if ext == ".docx":
77
101
  return _extract_docx(path)
78
102
  if ext == ".pdf":
79
103
  return _extract_pdf(path)
104
+ if ext in (".ckl", ".cklb"):
105
+ return _extract_checklist(path)
80
106
  return None
81
107
 
82
108
 
@@ -12,9 +12,10 @@ import threading
12
12
  import time
13
13
 
14
14
  from textual import work
15
- from textual.app import App, ComposeResult
15
+ from textual.app import App, ComposeResult, ScreenStackError
16
16
  from textual.binding import Binding
17
17
  from textual.containers import Vertical, VerticalScroll
18
+ from textual.css.query import NoMatches
18
19
  from textual.widgets import Static
19
20
 
20
21
  from drydock.agent import (
@@ -351,8 +352,15 @@ class DrydockApp(App):
351
352
  return f"{spin} {self._work_word}… ({elapsed} · ↓ {toks} tokens{eff}{queued})"
352
353
 
353
354
  def _refresh_status(self) -> None:
354
- self.query_one("#status", Static).update(self._status_text())
355
- self.query_one("#working", Static).update(self._working_text())
355
+ # The 0.18s _tick_work timer can fire one last time DURING app teardown,
356
+ # after the footer widgets have been removed — query_one would then raise
357
+ # NoMatches and crash the app (or fail a test). Be defensive: if the
358
+ # widgets aren't there (shutting down), there's nothing to refresh.
359
+ try:
360
+ self.query_one("#status", Static).update(self._status_text())
361
+ self.query_one("#working", Static).update(self._working_text())
362
+ except (NoMatches, ScreenStackError):
363
+ pass
356
364
 
357
365
  @property
358
366
  def _scroll(self) -> VerticalScroll:
@@ -469,6 +477,8 @@ class DrydockApp(App):
469
477
  self._info("Nothing to go back to.")
470
478
  elif cmd == "/compact":
471
479
  self._cmd_compact()
480
+ elif cmd == "/context":
481
+ self._cmd_context(arg)
472
482
  elif cmd == "/graphrag":
473
483
  self._cmd_graphrag(arg)
474
484
  elif cmd == "/status":
@@ -490,6 +500,7 @@ class DrydockApp(App):
490
500
  " /stop stop the running turn (or press Esc)\n"
491
501
  " /status session model, cwd, turns, tokens\n"
492
502
  " /compact shrink old context to free up the window\n"
503
+ " /context view/set the context-window budget (e.g. /context 65536)\n"
493
504
  " /graphrag ingest docs into a knowledge base the agent can use\n"
494
505
  " build <path> · add <path> · query <q> · status · clear\n"
495
506
  " /skills list skills · /skills new <name> <prompt> to create one\n"
@@ -735,6 +746,39 @@ class DrydockApp(App):
735
746
  else:
736
747
  self._begin(prompt)
737
748
 
749
+ def _cmd_context(self, arg: str) -> None:
750
+ """View or change the context-window budget (tokens) — the cap that drives
751
+ the ctx gauge + auto-compaction. `/context` shows it + its source; `/context
752
+ <n>` sets and PERSISTS it to ~/.drydock/config.toml. This is the lever for
753
+ 'stuck at 32k': an old config.toml value (drydock never rewrites an existing
754
+ one) or a smaller model-server -c silently caps you."""
755
+ limit = self.config.get("context_limit", 65536) or 65536
756
+ arg = (arg or "").strip()
757
+ if not arg:
758
+ self._info(
759
+ f"context_limit: {limit:,} tokens (the '/{limit // 1024}k' in the ctx gauge).\n"
760
+ "Source order: built-in default 65536 < ~/.drydock/config.toml < --context-limit.\n"
761
+ "If you're stuck below this, your MODEL SERVER's context is smaller — raise its\n"
762
+ "`-c`/`--ctx-size`/`max_model_len` to match. To change drydock's budget:\n"
763
+ " /context <tokens> e.g. /context 65536 (saved to config.toml)"
764
+ )
765
+ return
766
+ try:
767
+ n = int(arg.replace(",", "").replace("k", "000").replace("K", "000"))
768
+ if n < 4096 or n > 2_000_000:
769
+ raise ValueError
770
+ except ValueError:
771
+ self._mount(ErrorMessage("usage: /context <tokens> (4096–2000000, e.g. /context 65536)"))
772
+ return
773
+ self.config["context_limit"] = n
774
+ self._persist_config()
775
+ self._refresh_status() # repaint the gauge against the new budget
776
+ self._info(
777
+ f"✓ context_limit set to {n:,} tokens and saved to ~/.drydock/config.toml.\n"
778
+ "Make sure your model server's context (-c / --ctx-size / max_model_len) is at\n"
779
+ "least this, or the server will still cap you below it."
780
+ )
781
+
738
782
  def _cmd_compact(self) -> None:
739
783
  """Manually compact the conversation to reclaim context NOW, without
740
784
  waiting for the automatic 60%-of-window threshold (agent.maybe_compact).
@@ -823,7 +867,12 @@ class DrydockApp(App):
823
867
  self._info("No knowledge base yet. Build one: /graphrag build <path>")
824
868
  return
825
869
  res = graphrag.query_index(index, rest, k=3)
826
- self._info(graphrag.format_results(res, rest))
870
+ self._info(
871
+ graphrag.format_results(res, rest)
872
+ + "\n\n— This is a PREVIEW for you; it does not go to the model. "
873
+ "The agent retrieves from this knowledge base automatically (via "
874
+ "its Knowledge tool) when you just ASK a question — no slash command."
875
+ )
827
876
  elif sub in ("", "status"):
828
877
  index = graphrag.load_index(store)
829
878
  if index is None:
@@ -153,7 +153,7 @@ class PromptHistory:
153
153
  # completes the prefix.
154
154
  SLASH_COMMANDS = [
155
155
  "/help", "/model", "/cwd", "/undo", "/back", "/stop", "/status",
156
- "/compact", "/graphrag", "/skills", "/loop", "/mcp", "/rmf", "/stig",
156
+ "/compact", "/context", "/graphrag", "/skills", "/loop", "/mcp", "/rmf", "/stig",
157
157
  "/clear", "/quit",
158
158
  ]
159
159
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.70
3
+ Version: 3.0.72
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
  # PyPI distribution name is drydock-cli (the established install name, continued
8
8
  # from the retired v2 fork); the import package + CLI command stay `drydock`.
9
9
  name = "drydock-cli"
10
- version = "3.0.70"
10
+ version = "3.0.72"
11
11
  description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -69,3 +69,27 @@ def test_graphrag_ingests_docx_and_pdf(tmp_path):
69
69
  if extract.pdf_backend_available():
70
70
  assert "manual.pdf" in srcs
71
71
  assert graphrag.query_index(idx, "refunds Zephyr gateway")["chunks"][0]["source"] == "manual.pdf"
72
+
73
+
74
+ def test_extract_ckl_checklist(tmp_path):
75
+ p = tmp_path / "sys.ckl"
76
+ p.write_text('<?xml version="1.0"?><CHECKLIST><ASSET><HOST_NAME>web01</HOST_NAME></ASSET>'
77
+ '<STIGS><iSTIG><STIG_INFO></STIG_INFO><VULN>'
78
+ '<STIG_DATA><VULN_ATTRIBUTE>Vuln_Num</VULN_ATTRIBUTE><ATTRIBUTE_DATA>V-1</ATTRIBUTE_DATA></STIG_DATA>'
79
+ '<STIG_DATA><VULN_ATTRIBUTE>Rule_Title</VULN_ATTRIBUTE><ATTRIBUTE_DATA>disable debug</ATTRIBUTE_DATA></STIG_DATA>'
80
+ '<STATUS>Open</STATUS><FINDING_DETAILS>debug=true found</FINDING_DETAILS></VULN></iSTIG></STIGS></CHECKLIST>')
81
+ out = extract.extract_document(p)
82
+ assert out and "V-1" in out and "STATUS=open" in out and "debug=true found" in out
83
+
84
+
85
+ def test_graphrag_ingests_ckl(tmp_path):
86
+ (tmp_path / "sys.ckl").write_text('<?xml version="1.0"?><CHECKLIST><STIGS><iSTIG><STIG_INFO></STIG_INFO>'
87
+ '<VULN><STIG_DATA><VULN_ATTRIBUTE>Vuln_Num</VULN_ATTRIBUTE><ATTRIBUTE_DATA>V-42</ATTRIBUTE_DATA></STIG_DATA>'
88
+ '<STIG_DATA><VULN_ATTRIBUTE>Rule_Title</VULN_ATTRIBUTE><ATTRIBUTE_DATA>SSH root login disabled</ATTRIBUTE_DATA></STIG_DATA>'
89
+ '<STATUS>NotAFinding</STATUS></VULN></iSTIG></STIGS></CHECKLIST>')
90
+ store = graphrag.default_store_path(str(tmp_path))
91
+ stats = graphrag.build_index(["."], store, cwd=str(tmp_path))
92
+ assert stats["files"] == 1
93
+ idx = graphrag.load_index(store)
94
+ assert "sys.ckl" in graphrag.sources(idx)
95
+ assert graphrag.query_index(idx, "SSH root login")["chunks"]
@@ -385,3 +385,40 @@ def test_loop_command_iterates_and_stops():
385
385
  assert app._repeat is None
386
386
 
387
387
  asyncio.run(main())
388
+
389
+
390
+ def test_refresh_status_survives_missing_widgets():
391
+ """The 0.18s _tick_work timer can fire one last time during app teardown,
392
+ after the footer widgets (#status/#working) are gone. _refresh_status must
393
+ swallow the resulting NoMatches, not crash the app. Regression for a flaky
394
+ NoMatches('#status') that surfaced under full-suite load."""
395
+ app = DrydockApp({"model": "gemma4", "provider": "vllm", "cwd": "/tmp"})
396
+ app._busy = True
397
+ # No widgets are mounted (no run_test), so query_one('#status') would raise
398
+ # NoMatches — the guard must make this a safe no-op.
399
+ app._refresh_status() # must not raise
400
+ app._tick_work() # the timer path that triggered the crash; must not raise
401
+
402
+
403
+ def test_context_command_sets_and_persists(tmp_path, monkeypatch):
404
+ """/context <n> updates the live budget AND writes it to config.toml — the
405
+ fix for a stale context_limit (e.g. 32768 left from an old install) that
406
+ silently caps the window."""
407
+ monkeypatch.setenv("HOME", str(tmp_path))
408
+
409
+ async def main():
410
+ app = DrydockApp({"model": "gemma4", "provider": "vllm", "cwd": "/tmp",
411
+ "context_limit": 32768})
412
+ async with app.run_test() as pilot:
413
+ await pilot.pause()
414
+ app._cmd_context("65536") # set + persist
415
+ await pilot.pause()
416
+ assert app.config["context_limit"] == 65536
417
+ app._cmd_context("not-a-number") # invalid → unchanged
418
+ await pilot.pause()
419
+ assert app.config["context_limit"] == 65536
420
+
421
+ asyncio.run(main())
422
+ import tomllib
423
+ saved = tomllib.loads((tmp_path / ".drydock" / "config.toml").read_text())
424
+ assert saved["context_limit"] == 65536 # survived to disk
File without changes
File without changes
File without changes
File without changes