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.
- {drydock_cli-3.0.70/drydock_cli.egg-info → drydock_cli-3.0.72}/PKG-INFO +1 -1
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/extract.py +29 -3
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/app.py +53 -4
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/widgets.py +1 -1
- {drydock_cli-3.0.70 → drydock_cli-3.0.72/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/pyproject.toml +1 -1
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_extract.py +24 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_tui.py +37 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/LICENSE +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/NOTICE +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/README.md +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/agent.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/cli.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/config.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/detect.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/guards.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/providers.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/skills.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/stig.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock/web.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/setup.cfg +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_config.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.70 → drydock_cli-3.0.72}/tests/test_xccdf.py +0 -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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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(
|
|
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
|
|
|
@@ -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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|