squackit 0.8.2__tar.gz → 0.8.4__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.
- {squackit-0.8.2 → squackit-0.8.4}/CHANGELOG.md +99 -0
- {squackit-0.8.2 → squackit-0.8.4}/PKG-INFO +1 -1
- {squackit-0.8.2 → squackit-0.8.4}/pyproject.toml +1 -1
- squackit-0.8.4/squackit/__init__.py +17 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/tool_config.py +32 -1
- {squackit-0.8.2 → squackit-0.8.4}/squackit/tools.py +17 -4
- {squackit-0.8.2 → squackit-0.8.4}/squackit/workflows.py +72 -8
- squackit-0.8.4/tests/test_error_visibility.py +253 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_sandbox_anchoring.py +95 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_smoke.py +17 -1
- squackit-0.8.2/squackit/__init__.py +0 -3
- {squackit-0.8.2 → squackit-0.8.4}/.github/workflows/release.yml +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/.gitignore +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/.mcp.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/.readthedocs.yaml +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/.readthedocs.yml +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/CLAUDE.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/LICENSE +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/README.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/agent-system-prompt.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/architecture.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/configuration.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/index.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/prompts.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/proposals/2026-06-04-response-type-improvements.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/quickstart.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/resources.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/plans/2026-04-10-phase1-handoff.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/plans/2026-04-10-squawkit-extraction.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/plans/2026-04-12-cli-tool-exposure.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/plans/2026-04-12-session-handoff.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/plans/2026-04-16-session-handoff.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/plans/2026-04-26-named-fts-collections.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/skills/pluck-chains/SKILL.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/skills/using-squackit/SKILL.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/specs/2026-04-10-squawkit-design.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/specs/2026-04-12-pluckit-tool-integration-design.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/docs/tools.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/mkdocs.yml +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/EXPERIMENT_LOG.md +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/benchmark_models.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/cascade_prototype.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/combo_eval.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/generate_examples.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/grid_generate_examples.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/hard_task_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/hard_task_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/hard_task_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/hard_task_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/hard_task_results/qwen3.5_2b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/hard_task_results/qwen3_8b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/hard_task_results/scoring_input.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/hard_task_results/summary.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/hard_tasks.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/harvest_grid.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/iterative_eval.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/iterative_results/summary.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/iterative_results/summary_deepseek_plain.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/iterative_results/summary_distilled_xml.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/iterative_results/summary_editor_3b_7b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/iterative_results/summary_fulldocs.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/iterative_results/summary_plaintext_3b7b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/codegemma_2b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/codegemma_7b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/deepseek-llm_7b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/gemma_latest.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/glm4_9b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/granite-code_3b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/granite3.1-dense_2b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/granite3.3_2b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/llama2-uncensored_7b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/llama3.2-vision_11b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/llama3.2_1b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/llama3.2_latest.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/phi3_latest.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/phi4-mini_latest.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen2.5-coder_1.5b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen2.5_1.5b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen2.5_3b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen2.5_7b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen2_latest.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen3.5_0.8b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen3.5_2b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen3.5_4b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen3.5_9b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen3_0.6b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/qwen3_8b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/smollm2_latest.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_card_results/summary.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/model_cards.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/multiline_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/multiline_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/multiline_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/multiline_results/qwen3.5_2b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/multiline_results/qwen3_8b.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/multiline_results/summary.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/multiline_tasks.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/qm_retrieval_eval.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/quartermaster_eval.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/quartermaster_results/retrieval_summary.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/quartermaster_results/summary.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/selector_eval.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/scripts/selector_results/summary.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/__main__.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/cli.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/data/examples.json +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/db.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/defaults.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/formatting.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/lackpy_integration.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/prompts.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/runtime.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/server.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/squackit/session.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/conftest.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/fixtures/has_call_filter.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_cli_tools.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_config_consumption.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_defaults.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_formatting_json.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_fts_root.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_prompts.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_resources.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_runtime_config.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_session.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_tool_config.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_tools.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_truncation.py +0 -0
- {squackit-0.8.2 → squackit-0.8.4}/tests/test_workflows.py +0 -0
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.4
|
|
4
|
+
|
|
5
|
+
A review of 0.8.1–0.8.3 found both fixes had reached the macro-tool layer and
|
|
6
|
+
stopped there. The compound tools have their own parameters and never pass
|
|
7
|
+
through `apply_defaults`, so each gap was reachable through a different door.
|
|
8
|
+
|
|
9
|
+
### Fixed — the compound tools did not anchor explicit patterns
|
|
10
|
+
|
|
11
|
+
0.8.1 anchored caller-supplied relative patterns, but only in the macro-tool
|
|
12
|
+
wrapper. `investigate`, `review` and `search` take `file_pattern` themselves and
|
|
13
|
+
handed it to the macro unchanged, so a relative glob still resolved against the
|
|
14
|
+
process working directory. On a server whose cwd is not the corpus — the exact
|
|
15
|
+
deployment 0.8.1 was written for — this reproduced the original failure:
|
|
16
|
+
|
|
17
|
+
investigate(name="parse_config", file_pattern="src/**/*.py")
|
|
18
|
+
-> IOException: Failed to initialize file processing ... Permission
|
|
19
|
+
|
|
20
|
+
Verified against the published 0.8.3 before fixing. The 0.8.1 note that
|
|
21
|
+
"caller-supplied relative patterns are anchored too" was true of the macro path
|
|
22
|
+
only; it is now true of both. Absolute patterns are still passed through
|
|
23
|
+
untouched, and an explicit pattern still works without a defaults object.
|
|
24
|
+
|
|
25
|
+
### Fixed — briefing sections named no cause
|
|
26
|
+
|
|
27
|
+
`_section` wraps every part of `explore`, `review` and `search`, and
|
|
28
|
+
`investigate`'s Source/Called-by/Calls. It caught everything, logged at DEBUG —
|
|
29
|
+
invisible at normal levels — and rendered a bare `(could not load)`. A
|
|
30
|
+
permission error, a missing git revision and an unloaded macro were
|
|
31
|
+
indistinguishable from each other and from a section that legitimately had
|
|
32
|
+
nothing.
|
|
33
|
+
|
|
34
|
+
That is the same defect 0.8.3 fixed in `investigate`'s primary lookup and did
|
|
35
|
+
not carry to the siblings. Sections now report the exception type and message,
|
|
36
|
+
capped at 200 characters with the truncation marked, and log at WARNING. One
|
|
37
|
+
failing section still cannot take down a briefing.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## 0.8.3
|
|
41
|
+
|
|
42
|
+
Three fixes with one theme: a failure must not be representable as an ordinary
|
|
43
|
+
result. Nothing here changes what a working call returns.
|
|
44
|
+
|
|
45
|
+
### Fixed — `investigate` reported failed lookups as confirmed absences (#14)
|
|
46
|
+
|
|
47
|
+
Every exception from the definition query was caught and rendered with the
|
|
48
|
+
sentence used for a genuine miss:
|
|
49
|
+
|
|
50
|
+
No definition found for 'X'. Try a broader pattern or check spelling.
|
|
51
|
+
|
|
52
|
+
So a permission error, a binder error and "that symbol isn't here" were
|
|
53
|
+
indistinguishable, and the advice actively misdirected — the pattern was never
|
|
54
|
+
the problem. This is how the 0.8.1 sandbox bug presented: every symbol in a
|
|
55
|
+
healthy project reported missing, for hours, while the server called all its
|
|
56
|
+
tools healthy.
|
|
57
|
+
|
|
58
|
+
A failed lookup now says so, names the underlying error, and reports the
|
|
59
|
+
pattern it searched. The friendly wording is kept for the genuine empty case,
|
|
60
|
+
where it is good advice.
|
|
61
|
+
|
|
62
|
+
### Fixed — a raising pluckin lost its tools silently (#14)
|
|
63
|
+
|
|
64
|
+
`collect_pluckin_tools` swallowed per-pluckin exceptions with a bare `pass`,
|
|
65
|
+
under a comment claiming the error would be "surfaced contextually". Nothing
|
|
66
|
+
surfaced it. The guard is right — one broken pluckin must not take down the
|
|
67
|
+
registry — but it now logs a warning naming the pluckin and the error.
|
|
68
|
+
|
|
69
|
+
Not reachable today (no shipped pluckin defines `squackit_tools`), fixed as
|
|
70
|
+
latent. The symptom it would produce — a silently shorter tool surface — was
|
|
71
|
+
independently hit through another mechanism and cost a four-combination version
|
|
72
|
+
bisection, because a shorter tool list doesn't read as a failure. It reads as a
|
|
73
|
+
different, reasonable version of the product.
|
|
74
|
+
|
|
75
|
+
### Fixed — `ast_replace` / `ast_patch` read as mutating and are not (#16)
|
|
76
|
+
|
|
77
|
+
Connection macros are published with a description generated from the
|
|
78
|
+
signature, so these carried no semantics at all:
|
|
79
|
+
|
|
80
|
+
Query: ast_replace(source, selector, new_text, language)
|
|
81
|
+
|
|
82
|
+
These are sitting_duck SQL macros. A SQL macro cannot write files — it computes
|
|
83
|
+
the patched source and returns it, which is coherent, and sitting_duck is not
|
|
84
|
+
misbehaving. But over MCP the caller reads a verb. One did: it called
|
|
85
|
+
`ast_replace` to repair a bug, got a successful-looking table back, and changed
|
|
86
|
+
nothing on disk.
|
|
87
|
+
|
|
88
|
+
Generated descriptions for macros whose *name* implies an effect now state that
|
|
89
|
+
they return the patched result and modify nothing. Read-only macros are
|
|
90
|
+
unchanged — a disclaimer on all ninety is context every agent pays for.
|
|
91
|
+
|
|
92
|
+
For an actually-mutating primitive, see #17.
|
|
93
|
+
|
|
94
|
+
### Fixed — `__version__` was four releases stale (#15, #2)
|
|
95
|
+
|
|
96
|
+
`squackit.__version__` was a hand-written `"0.4.1"` against a distribution
|
|
97
|
+
version of `0.8.2`, and survived `--force-reinstall` because it derived from
|
|
98
|
+
nothing. It is now read from `importlib.metadata`, and the smoke test asserts
|
|
99
|
+
that correspondence rather than a literal — which is what let both drift in
|
|
100
|
+
agreement with each other and with nothing else.
|
|
101
|
+
|
|
3
102
|
## 0.8.2
|
|
4
103
|
|
|
5
104
|
### Fixed — AST tools could not read a source outside the working directory
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: squackit
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.4
|
|
4
4
|
Summary: Semi-QUalified Agent Companion Kit — the stateful intelligence + MCP server layer for fledgling-equipped agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/teaguesterling/squackit
|
|
6
6
|
Project-URL: Documentation, https://squackit.readthedocs.io
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""squackit: Semi-QUalified Agent Companion Kit — the stateful intelligence + MCP server layer for fledgling-equipped agents."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _dist_version
|
|
4
|
+
|
|
5
|
+
# Derived, never written by hand. The literal that used to live here read
|
|
6
|
+
# "0.4.1" while the installed distribution was 0.8.2 — four releases stale, and
|
|
7
|
+
# immune to --force-reinstall because it was not derived from anything. Someone
|
|
8
|
+
# debugging an unrelated problem read it, concluded they had a mixed install,
|
|
9
|
+
# and went looking for a packaging fault that did not exist. A version string
|
|
10
|
+
# that lies makes a healthy install look broken, which is the most expensive
|
|
11
|
+
# direction to send a reader.
|
|
12
|
+
try:
|
|
13
|
+
__version__ = _dist_version("squackit")
|
|
14
|
+
except PackageNotFoundError: # running from a source tree with no install
|
|
15
|
+
__version__ = "0.0.0.dev0"
|
|
16
|
+
|
|
17
|
+
__all__ = ["__version__"]
|
|
@@ -37,6 +37,21 @@ _FALLBACK_NUMERIC = {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
#: Name tokens that read as an effect. A macro whose name contains one of these
|
|
41
|
+
#: gets an explicit "does not modify any file" note in its generated
|
|
42
|
+
#: description. Missing a verb costs nothing beyond the status quo; a false
|
|
43
|
+
#: positive adds a sentence that is true of every SQL macro anyway.
|
|
44
|
+
_MUTATION_VERBS = frozenset({
|
|
45
|
+
"replace", "patch", "apply", "write", "insert", "update", "delete",
|
|
46
|
+
"remove", "rename", "edit", "set", "modify", "save", "commit",
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _name_implies_mutation(name: str) -> bool:
|
|
51
|
+
"""True when a tool's NAME reads as an effect it does not perform."""
|
|
52
|
+
return any(token in _MUTATION_VERBS for token in name.split("_"))
|
|
53
|
+
|
|
54
|
+
|
|
40
55
|
@dataclass
|
|
41
56
|
class ToolPresentation:
|
|
42
57
|
"""Wraps fledgling ToolInfo with squackit's presentation/UX config."""
|
|
@@ -100,7 +115,23 @@ class ToolPresentation:
|
|
|
100
115
|
def description(self) -> str:
|
|
101
116
|
if self.description_override is not None:
|
|
102
117
|
return self.description_override
|
|
103
|
-
|
|
118
|
+
if self.info.description:
|
|
119
|
+
return self.info.description
|
|
120
|
+
signature = f"{self.name}({', '.join(self.params)})"
|
|
121
|
+
if _name_implies_mutation(self.name):
|
|
122
|
+
# SQL macros cannot write files — they compute and return a result
|
|
123
|
+
# set. But a caller reads a verb: `ast_replace` was called to repair
|
|
124
|
+
# a bug, returned a successful-looking table, and changed nothing on
|
|
125
|
+
# disk. The bare signature said nothing either way, and the only
|
|
126
|
+
# hint was the "Query:" prefix — a category label competing against
|
|
127
|
+
# an imperative. Spell it out for the names where it misleads, and
|
|
128
|
+
# only those: a disclaimer on all ~90 macros is context every agent
|
|
129
|
+
# pays for on every call.
|
|
130
|
+
return (
|
|
131
|
+
f"Query: {signature} — computes and RETURNS the patched result "
|
|
132
|
+
f"as text. Does not modify any file."
|
|
133
|
+
)
|
|
134
|
+
return f"Query: {signature}"
|
|
104
135
|
|
|
105
136
|
@property
|
|
106
137
|
def parameters_schema(self) -> dict | None:
|
|
@@ -9,6 +9,7 @@ fledgling macro tools. They take priority over fledgling equivalents
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import hashlib
|
|
12
|
+
import logging
|
|
12
13
|
import os
|
|
13
14
|
import re
|
|
14
15
|
import tempfile
|
|
@@ -16,6 +17,8 @@ import tempfile
|
|
|
16
17
|
from fledgling.tools import ToolInfo
|
|
17
18
|
from squackit.tool_config import ToolPresentation
|
|
18
19
|
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
#: How much source text ``read_ast`` puts in the ``peek`` column when squackit
|
|
21
24
|
#: materializes its own table. ``'smart'`` matches what pluckit's cache uses by
|
|
@@ -305,10 +308,20 @@ def collect_pluckin_tools(plucker) -> list:
|
|
|
305
308
|
if callable(fn):
|
|
306
309
|
try:
|
|
307
310
|
tools.extend(fn())
|
|
308
|
-
except Exception:
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
|
|
311
|
+
except Exception as exc:
|
|
312
|
+
# Keep the guard — one broken pluckin must not take down the
|
|
313
|
+
# whole registry — but SAY SO. Dropping its tools silently
|
|
314
|
+
# yields a shorter tool surface, and a shorter tool list does
|
|
315
|
+
# not read as a failure: it reads as a different, reasonable
|
|
316
|
+
# version of the product, so callers reason forward from it
|
|
317
|
+
# instead of doubting it. (An earlier comment here claimed the
|
|
318
|
+
# server/CLI would "surface the error contextually". Nothing
|
|
319
|
+
# did.)
|
|
320
|
+
log.warning(
|
|
321
|
+
"pluckin %s failed to contribute squackit tools (%s); its "
|
|
322
|
+
"tools are missing from this session's registry",
|
|
323
|
+
type(pluckin).__name__, exc, exc_info=True,
|
|
324
|
+
)
|
|
312
325
|
return tools
|
|
313
326
|
|
|
314
327
|
|
|
@@ -32,20 +32,41 @@ def _format_briefing(title: str, sections: list[tuple[str, str]]) -> str:
|
|
|
32
32
|
return "\n".join(parts)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
#: How much of a failing section's error text reaches the briefing. DuckDB
|
|
36
|
+
#: errors run to hundreds of characters and a briefing is prose, but a cause
|
|
37
|
+
#: the reader cannot see is the thing this whole clause exists to avoid.
|
|
38
|
+
_SECTION_ERROR_CHARS = 200
|
|
39
|
+
|
|
40
|
+
|
|
35
41
|
def _section(heading: str, fn):
|
|
36
42
|
"""Run fn() with error handling, return (heading, content) tuple.
|
|
37
43
|
|
|
38
|
-
Returns "(no data)" for empty/None results,
|
|
39
|
-
|
|
44
|
+
Returns "(no data)" for empty/None results, and "(could not load — <type>:
|
|
45
|
+
<message>)" on exceptions.
|
|
46
|
+
|
|
47
|
+
THE CAUSE IS IN THE TEXT ON PURPOSE. This wrapper covers every section of
|
|
48
|
+
explore/review/search and investigate's Source/Called-by/Calls. It used to
|
|
49
|
+
return a bare "(could not load)" and log at DEBUG, which is invisible at
|
|
50
|
+
normal levels — so a permission error, a missing git revision and a macro
|
|
51
|
+
that simply is not loaded all rendered identically, and none of them said
|
|
52
|
+
why. That is the same "a failure must not be representable as an ordinary
|
|
53
|
+
result" defect fixed in investigate's primary lookup in 0.8.3; it was not
|
|
54
|
+
extended to the sibling sections until 0.8.4.
|
|
55
|
+
|
|
56
|
+
Still non-fatal: one broken section must not take down the briefing, which
|
|
57
|
+
is why the guard stays.
|
|
40
58
|
"""
|
|
41
59
|
try:
|
|
42
60
|
content = fn()
|
|
43
61
|
if not content:
|
|
44
62
|
return (heading, "(no data)")
|
|
45
63
|
return (heading, content)
|
|
46
|
-
except Exception:
|
|
47
|
-
log.
|
|
48
|
-
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
log.warning("section %r failed: %s", heading, exc, exc_info=True)
|
|
66
|
+
detail = str(exc).strip().replace("\n", " ")
|
|
67
|
+
if len(detail) > _SECTION_ERROR_CHARS:
|
|
68
|
+
detail = detail[:_SECTION_ERROR_CHARS] + "…"
|
|
69
|
+
return (heading, f"(could not load — {type(exc).__name__}: {detail})")
|
|
49
70
|
|
|
50
71
|
|
|
51
72
|
def _has_module(con, module_name: str) -> bool:
|
|
@@ -92,6 +113,31 @@ def _sorted_table(con, macro_name, kwargs, sort_col, max_rows, descending=True):
|
|
|
92
113
|
# ── Compound tools ─────────────────────────────────────────────────
|
|
93
114
|
|
|
94
115
|
|
|
116
|
+
def _anchored(defaults, file_pattern):
|
|
117
|
+
"""Root a caller-supplied file pattern, the way apply_defaults does.
|
|
118
|
+
|
|
119
|
+
The compound tools take `file_pattern` as their own parameter and never
|
|
120
|
+
pass through `apply_defaults`, so 0.8.1's anchoring of explicit relative
|
|
121
|
+
patterns reached the macro tools only. A relative glob handed to
|
|
122
|
+
`investigate`/`review`/`search` still resolved against the process cwd and,
|
|
123
|
+
under fledgling's sandbox, failed outright — the same deployment shape
|
|
124
|
+
0.8.1 was written for, reached through a different door.
|
|
125
|
+
|
|
126
|
+
`anchor()` passes absolute patterns through untouched, so a caller who
|
|
127
|
+
named a path in full still gets it.
|
|
128
|
+
|
|
129
|
+
Tolerates ``defaults=None``. An explicit ``file_pattern`` previously needed
|
|
130
|
+
no defaults object at all — the caller had already said where to look — and
|
|
131
|
+
callers rely on that: several tests drive ``investigate`` with a fake
|
|
132
|
+
connection and no defaults to isolate the name-matching logic. There is
|
|
133
|
+
also nothing to anchor *against* without a root, which is the same reason
|
|
134
|
+
``anchor()`` itself passes patterns through when ``root`` is None.
|
|
135
|
+
"""
|
|
136
|
+
if file_pattern is None or defaults is None:
|
|
137
|
+
return file_pattern
|
|
138
|
+
return defaults.anchor(file_pattern)
|
|
139
|
+
|
|
140
|
+
|
|
95
141
|
def explore(con, defaults, path=None):
|
|
96
142
|
"""First-contact codebase briefing."""
|
|
97
143
|
# Scope patterns to path if provided
|
|
@@ -144,6 +190,7 @@ def investigate(con, defaults, name, file_pattern=None, path=None):
|
|
|
144
190
|
path: Project root to scope the search to. Defaults to process cwd.
|
|
145
191
|
Only consulted when file_pattern is None.
|
|
146
192
|
"""
|
|
193
|
+
file_pattern = _anchored(defaults, file_pattern)
|
|
147
194
|
if file_pattern is None:
|
|
148
195
|
from squackit.runtime import resolve_scope_path
|
|
149
196
|
# Precedence: explicit path -> runtime.active_root -> process cwd.
|
|
@@ -160,13 +207,22 @@ def investigate(con, defaults, name, file_pattern=None, path=None):
|
|
|
160
207
|
# the whole briefing describe the wrong symbol. Fall back to the full
|
|
161
208
|
# substring set only when nothing matches exactly, preserving the
|
|
162
209
|
# forgiving lookup for partial names / typos.
|
|
210
|
+
# A FAILED LOOKUP IS NOT AN ABSENT SYMBOL. Both used to return the "no
|
|
211
|
+
# definition found" sentence below, which made a permission error, a binder
|
|
212
|
+
# error and a genuine miss indistinguishable — and told the reader to
|
|
213
|
+
# broaden a pattern that was never the problem. That is exactly how the
|
|
214
|
+
# 0.8.1 sandbox bug presented: every symbol in a healthy project reported
|
|
215
|
+
# missing, for hours, while the server called all its tools healthy.
|
|
216
|
+
lookup_error: Exception | None = None
|
|
163
217
|
try:
|
|
164
218
|
rel = con.find_definitions(
|
|
165
219
|
file_pattern=file_pattern, name_pattern=f"%{name}%",
|
|
166
220
|
)
|
|
167
221
|
defs = rel.fetchall()
|
|
168
222
|
def_cols = rel.columns
|
|
169
|
-
except Exception:
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
log.debug("find_definitions failed for %r", name, exc_info=True)
|
|
225
|
+
lookup_error = exc
|
|
170
226
|
defs = []
|
|
171
227
|
def_cols = []
|
|
172
228
|
|
|
@@ -176,6 +232,14 @@ def investigate(con, defaults, name, file_pattern=None, path=None):
|
|
|
176
232
|
if _exact:
|
|
177
233
|
defs = _exact
|
|
178
234
|
|
|
235
|
+
if lookup_error is not None:
|
|
236
|
+
return (
|
|
237
|
+
f"Could not look up '{name}' — the definition query failed, so "
|
|
238
|
+
f"whether this symbol exists is UNKNOWN.\n"
|
|
239
|
+
f"{type(lookup_error).__name__}: {lookup_error}\n"
|
|
240
|
+
f"Searched: {file_pattern}"
|
|
241
|
+
)
|
|
242
|
+
|
|
179
243
|
if not defs:
|
|
180
244
|
return f"No definition found for '{name}'. Try a broader pattern or check spelling."
|
|
181
245
|
|
|
@@ -271,7 +335,7 @@ def review(con, defaults, from_rev=None, to_rev=None, file_pattern=None):
|
|
|
271
335
|
"""Code review prep for a revision range."""
|
|
272
336
|
from_rev = from_rev or defaults.from_rev
|
|
273
337
|
to_rev = to_rev or defaults.to_rev
|
|
274
|
-
file_pattern = file_pattern or defaults.code_glob
|
|
338
|
+
file_pattern = _anchored(defaults, file_pattern) or defaults.code_glob
|
|
275
339
|
|
|
276
340
|
sections = []
|
|
277
341
|
|
|
@@ -351,7 +415,7 @@ def review(con, defaults, from_rev=None, to_rev=None, file_pattern=None):
|
|
|
351
415
|
|
|
352
416
|
def search(con, defaults, query, file_pattern=None):
|
|
353
417
|
"""Multi-source search across code, docs, and git."""
|
|
354
|
-
file_pattern = file_pattern or defaults.code_glob
|
|
418
|
+
file_pattern = _anchored(defaults, file_pattern) or defaults.code_glob
|
|
355
419
|
|
|
356
420
|
sections = []
|
|
357
421
|
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""A failure must not be representable as an ordinary result.
|
|
2
|
+
|
|
3
|
+
Three places used to violate that, each in a different way:
|
|
4
|
+
|
|
5
|
+
* ``investigate`` caught every query exception and rendered it with the same
|
|
6
|
+
sentence it uses for a genuine miss — so a permission error, a binder error
|
|
7
|
+
and "that symbol isn't here" were indistinguishable, and the advice offered
|
|
8
|
+
("try a broader pattern") actively misdirected. This is not hypothetical: it
|
|
9
|
+
is exactly how the 0.8.1 sandbox bug presented, for hours, while the server
|
|
10
|
+
reported every tool healthy.
|
|
11
|
+
* ``collect_pluckin_tools`` dropped a raising pluckin's tools with a bare
|
|
12
|
+
``pass`` under a comment claiming the error would be surfaced elsewhere.
|
|
13
|
+
Nothing surfaced it. A silently shorter tool surface is worse than a silently
|
|
14
|
+
empty result — an empty result prompts "did that work?", a shorter tool list
|
|
15
|
+
reads as a different, reasonable version of the product.
|
|
16
|
+
* Auto-generated macro descriptions were a bare signature, so a SQL macro named
|
|
17
|
+
``ast_replace`` — which computes patched source and writes nothing, because a
|
|
18
|
+
SQL macro *cannot* write — read as a mutation to any caller.
|
|
19
|
+
|
|
20
|
+
See issues #14 and #16.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
from squackit.defaults import ProjectDefaults
|
|
28
|
+
from squackit.tool_config import ToolPresentation
|
|
29
|
+
from squackit.tools import collect_pluckin_tools
|
|
30
|
+
from squackit.workflows import investigate
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── investigate: a failed query is not a miss ───────────────────────
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _Rel:
|
|
37
|
+
def __init__(self, rows, cols):
|
|
38
|
+
self._rows, self.columns = rows, cols
|
|
39
|
+
|
|
40
|
+
def fetchall(self):
|
|
41
|
+
return self._rows
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _ConRaises:
|
|
45
|
+
"""A connection whose lookup fails the way a sandboxed one does."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, exc):
|
|
48
|
+
self._exc = exc
|
|
49
|
+
|
|
50
|
+
def find_definitions(self, **kw):
|
|
51
|
+
raise self._exc
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _ConEmpty:
|
|
55
|
+
"""A connection that works and genuinely has no match."""
|
|
56
|
+
|
|
57
|
+
def find_definitions(self, **kw):
|
|
58
|
+
return _Rel([], ["file_path", "name", "kind",
|
|
59
|
+
"start_line", "end_line", "signature"])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.fixture
|
|
63
|
+
def defaults(tmp_path):
|
|
64
|
+
return ProjectDefaults(code_pattern="**/*.py", root=str(tmp_path))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestInvestigateDistinguishesFailureFromMiss:
|
|
68
|
+
|
|
69
|
+
def test_genuine_miss_keeps_the_helpful_message(self, defaults):
|
|
70
|
+
"""The friendly wording is right when it IS a miss — keep it."""
|
|
71
|
+
out = investigate(_ConEmpty(), defaults, "nonexistent_symbol")
|
|
72
|
+
assert "No definition found" in out
|
|
73
|
+
assert "nonexistent_symbol" in out
|
|
74
|
+
|
|
75
|
+
def test_query_failure_does_NOT_claim_the_symbol_is_absent(self, defaults):
|
|
76
|
+
out = investigate(_ConRaises(RuntimeError("boom")), defaults, "parse_config")
|
|
77
|
+
assert "No definition found" not in out, (
|
|
78
|
+
"a failed lookup was reported as a confirmed absence"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def test_query_failure_says_it_failed(self, defaults):
|
|
82
|
+
out = investigate(_ConRaises(RuntimeError("boom")), defaults, "parse_config")
|
|
83
|
+
assert "could not" in out.lower() or "failed" in out.lower()
|
|
84
|
+
|
|
85
|
+
def test_query_failure_surfaces_the_underlying_error(self, defaults):
|
|
86
|
+
"""The 0.8.1 bug was diagnosable only from this text."""
|
|
87
|
+
exc = RuntimeError(
|
|
88
|
+
'Permission Error: Cannot access file "**/*.py" '
|
|
89
|
+
"- file system operations are disabled by configuration"
|
|
90
|
+
)
|
|
91
|
+
out = investigate(_ConRaises(exc), defaults, "parse_config")
|
|
92
|
+
assert "Permission Error" in out, out
|
|
93
|
+
|
|
94
|
+
def test_query_failure_does_NOT_advise_broadening_the_pattern(self, defaults):
|
|
95
|
+
"""That advice sends the reader at the wrong thing entirely."""
|
|
96
|
+
out = investigate(_ConRaises(RuntimeError("boom")), defaults, "parse_config")
|
|
97
|
+
assert "broader pattern" not in out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── collect_pluckin_tools: a dropped pluckin is logged ───────────────
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _Pluckin:
|
|
104
|
+
def __init__(self, tools=None, exc=None):
|
|
105
|
+
self._tools, self._exc = tools or [], exc
|
|
106
|
+
|
|
107
|
+
def squackit_tools(self):
|
|
108
|
+
if self._exc:
|
|
109
|
+
raise self._exc
|
|
110
|
+
return self._tools
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _Plucker:
|
|
114
|
+
def __init__(self, pluckins):
|
|
115
|
+
self.pluckins = pluckins
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestPluckinFailuresAreVisible:
|
|
119
|
+
|
|
120
|
+
def test_a_raising_pluckin_does_not_break_the_registry(self):
|
|
121
|
+
"""The guard itself is correct and stays."""
|
|
122
|
+
good = _Pluckin(tools=["t1", "t2"])
|
|
123
|
+
bad = _Pluckin(exc=RuntimeError("plugin exploded"))
|
|
124
|
+
assert collect_pluckin_tools(_Plucker([bad, good])) == ["t1", "t2"]
|
|
125
|
+
|
|
126
|
+
def test_a_raising_pluckin_is_LOGGED(self, caplog):
|
|
127
|
+
bad = _Pluckin(exc=RuntimeError("plugin exploded"))
|
|
128
|
+
with caplog.at_level(logging.WARNING, logger="squackit.tools"):
|
|
129
|
+
collect_pluckin_tools(_Plucker([bad]))
|
|
130
|
+
assert caplog.records, "a pluckin's tools vanished with no log record"
|
|
131
|
+
|
|
132
|
+
def test_the_log_names_the_pluckin_and_the_error(self, caplog):
|
|
133
|
+
bad = _Pluckin(exc=RuntimeError("plugin exploded"))
|
|
134
|
+
with caplog.at_level(logging.WARNING, logger="squackit.tools"):
|
|
135
|
+
collect_pluckin_tools(_Plucker([bad]))
|
|
136
|
+
text = " ".join(r.getMessage() for r in caplog.records)
|
|
137
|
+
assert "_Pluckin" in text, text
|
|
138
|
+
assert "plugin exploded" in text, text
|
|
139
|
+
|
|
140
|
+
def test_healthy_pluckins_log_nothing(self, caplog):
|
|
141
|
+
with caplog.at_level(logging.WARNING, logger="squackit.tools"):
|
|
142
|
+
collect_pluckin_tools(_Plucker([_Pluckin(tools=["t1"])]))
|
|
143
|
+
assert not caplog.records
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ── generated descriptions state their semantics ─────────────────────
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class _Info:
|
|
150
|
+
"""Stand-in for fledgling's ToolInfo carrying no description of its own.
|
|
151
|
+
|
|
152
|
+
ToolPresentation derives `name` and `params` from the info object, so the
|
|
153
|
+
stub has to supply macro_name/params rather than the presentation taking
|
|
154
|
+
them directly.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, macro_name, params, description=None):
|
|
158
|
+
self.macro_name = macro_name
|
|
159
|
+
self.tool_name = None
|
|
160
|
+
self.params = list(params)
|
|
161
|
+
self.description = description
|
|
162
|
+
self.required = None
|
|
163
|
+
self.required_params = []
|
|
164
|
+
self.format = None
|
|
165
|
+
self.parameters_schema = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _presentation(name, params):
|
|
169
|
+
return ToolPresentation(info=_Info(name, params))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestMutationNamedMacrosSayTheyDoNotMutate:
|
|
173
|
+
|
|
174
|
+
def test_ast_replace_states_it_does_not_modify(self):
|
|
175
|
+
d = _presentation("ast_replace", ["source", "selector", "new_text"])
|
|
176
|
+
assert "does not modify" in d.description.lower(), d.description
|
|
177
|
+
|
|
178
|
+
def test_ast_patch_states_it_does_not_modify(self):
|
|
179
|
+
d = _presentation("ast_patch", ["edits", "files"])
|
|
180
|
+
assert "does not modify" in d.description.lower(), d.description
|
|
181
|
+
|
|
182
|
+
def test_the_signature_is_still_there(self):
|
|
183
|
+
d = _presentation("ast_replace", ["source", "selector", "new_text"])
|
|
184
|
+
assert "ast_replace(source, selector, new_text)" in d.description
|
|
185
|
+
|
|
186
|
+
def test_read_only_macros_are_NOT_cluttered(self):
|
|
187
|
+
"""A disclaimer on all 90 macros is context every agent pays for."""
|
|
188
|
+
d = _presentation("find_definitions", ["file_pattern", "name_pattern"])
|
|
189
|
+
assert "does not modify" not in d.description.lower()
|
|
190
|
+
|
|
191
|
+
def test_a_real_description_still_wins(self):
|
|
192
|
+
info = _Info("ast_replace", ["source"], description="The macro's own words.")
|
|
193
|
+
assert ToolPresentation(info=info).description == "The macro's own words."
|
|
194
|
+
|
|
195
|
+
def test_an_override_still_wins(self):
|
|
196
|
+
d = ToolPresentation(info=_Info("ast_replace", ["source"]),
|
|
197
|
+
description_override="Overridden.")
|
|
198
|
+
assert d.description == "Overridden."
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ── Briefing sections name their failure (#2 from the 0.8.3 review) ──
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TestSectionFailuresAreNamed:
|
|
205
|
+
"""`_section` wraps every part of explore/review/search/investigate.
|
|
206
|
+
|
|
207
|
+
It caught everything, logged at DEBUG (invisible at normal levels) and
|
|
208
|
+
rendered "(could not load)" — a string that cannot be told apart from a
|
|
209
|
+
section that legitimately has nothing, and which names no cause. 0.8.3
|
|
210
|
+
fixed exactly this shape in investigate's primary lookup but never reached
|
|
211
|
+
the sibling sections, so the same permission error that motivated 0.8.1
|
|
212
|
+
still arrived as undifferentiated text in three of the four compound tools.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def test_a_failing_section_still_reports_could_not_load(self):
|
|
216
|
+
"""Keep the recognisable phrase — callers and tests match on it."""
|
|
217
|
+
from squackit.workflows import _section
|
|
218
|
+
|
|
219
|
+
heading, content = _section("Diffs", lambda: (_ for _ in ()).throw(
|
|
220
|
+
RuntimeError("boom")))
|
|
221
|
+
assert heading == "Diffs"
|
|
222
|
+
assert "could not load" in content.lower()
|
|
223
|
+
|
|
224
|
+
def test_a_failing_section_names_the_error(self):
|
|
225
|
+
from squackit.workflows import _section
|
|
226
|
+
|
|
227
|
+
_heading, content = _section("Diffs", lambda: (_ for _ in ()).throw(
|
|
228
|
+
RuntimeError("permission denied on **/*.py")))
|
|
229
|
+
assert "RuntimeError" in content, content
|
|
230
|
+
assert "permission denied" in content, content
|
|
231
|
+
|
|
232
|
+
def test_an_empty_section_is_distinguishable_from_a_failed_one(self):
|
|
233
|
+
from squackit.workflows import _section
|
|
234
|
+
|
|
235
|
+
_h, empty = _section("Docs", lambda: "")
|
|
236
|
+
_h2, failed = _section("Docs", lambda: (_ for _ in ()).throw(
|
|
237
|
+
RuntimeError("boom")))
|
|
238
|
+
assert empty != failed
|
|
239
|
+
assert "no data" in empty.lower()
|
|
240
|
+
|
|
241
|
+
def test_a_very_long_error_is_capped_visibly(self):
|
|
242
|
+
"""DuckDB errors run to hundreds of characters; a briefing is prose."""
|
|
243
|
+
from squackit.workflows import _section
|
|
244
|
+
|
|
245
|
+
_h, content = _section("Diffs", lambda: (_ for _ in ()).throw(
|
|
246
|
+
RuntimeError("x" * 4000)))
|
|
247
|
+
assert len(content) < 400, len(content)
|
|
248
|
+
assert "…" in content or "..." in content, content
|
|
249
|
+
|
|
250
|
+
def test_a_working_section_is_untouched(self):
|
|
251
|
+
from squackit.workflows import _section
|
|
252
|
+
|
|
253
|
+
assert _section("Docs", lambda: "| a | b |") == ("Docs", "| a | b |")
|