Skip to content

Commit b0838d6

Browse files
committed
test(tools): add real-LLM context tool coverage
1 parent 2f152ae commit b0838d6

5 files changed

Lines changed: 613 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Added an opt-in real-LLM integration suite for batch-read continuation,
13+
every grep output mode, guarded edit previews and writes, and stable glob
14+
pagination.
15+
16+
### Fixed
17+
18+
- Accepted structured providers' neutral pagination defaults (`limit = 200`
19+
and an empty cursor) in non-paginated grep modes while continuing to reject
20+
effective pagination controls there.
21+
1022
## [6.5.0] - 2026-07-28
1123

1224
### Added

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,15 @@ node scripts/sdk_api_alignment_check.mjs
467467
Real-provider, browser-runtime, and S3 tests are ignored unless their external
468468
prerequisites are configured. The normal test suite is hermetic.
469469

470+
Run the context-tool real-LLM suite through a local Codex login:
471+
472+
```bash
473+
A3S_CONTEXT_TOOLS_USE_CODEX_LOGIN=1 scripts/context_tools_real_llm.sh
474+
```
475+
476+
Alternatively, point `A3S_CONFIG_FILE` at an ACL provider configuration and
477+
run the same script without `A3S_CONTEXT_TOOLS_USE_CODEX_LOGIN`.
478+
470479
## License
471480

472481
[MIT](LICENSE)

core/src/tools/builtin/grep.rs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,19 @@ impl GrepOutputMode {
5353
}
5454
}
5555

56+
fn has_non_neutral_page_controls(args: &serde_json::Value) -> bool {
57+
let limit_is_non_neutral = match args.get("limit") {
58+
None | Some(serde_json::Value::Null) => false,
59+
Some(value) => value.as_u64() != Some(DEFAULT_PAGE_LIMIT as u64),
60+
};
61+
let cursor_is_non_neutral = match args.get("cursor") {
62+
None | Some(serde_json::Value::Null) => false,
63+
Some(serde_json::Value::String(value)) => !value.trim().is_empty(),
64+
Some(_) => true,
65+
};
66+
limit_is_non_neutral || cursor_is_non_neutral
67+
}
68+
5669
pub struct GrepTool;
5770

5871
#[async_trait]
@@ -150,9 +163,13 @@ impl Tool for GrepTool {
150163
Err(error) => return Ok(ToolOutput::error(error)),
151164
}
152165
} else {
153-
if args.get("limit").is_some() || args.get("cursor").is_some() {
166+
// Structured-output providers can materialize omitted optional
167+
// fields with their neutral schema defaults. Do not reject a
168+
// content/summary call merely because it carries limit=200 and an
169+
// empty cursor; neither value changes the non-paginated result.
170+
if has_non_neutral_page_controls(args) {
154171
return Ok(ToolOutput::error(
155-
"limit and cursor are supported only with output_mode='files_with_matches' or 'count'",
172+
"non-default limit and non-empty cursor are supported only with output_mode='files_with_matches' or 'count'",
156173
));
157174
}
158175
None
@@ -735,6 +752,49 @@ mod tests {
735752
assert_eq!(result.metadata.unwrap()["search"]["truncated"], false);
736753
}
737754

755+
#[tokio::test]
756+
async fn test_grep_non_paginated_mode_accepts_materialized_neutral_page_defaults() {
757+
let temp = tempfile::tempdir().unwrap();
758+
std::fs::write(temp.path().join("a.txt"), "needle\n").unwrap();
759+
let ctx = ToolContext::new(temp.path().to_path_buf());
760+
761+
let result = GrepTool
762+
.execute(
763+
&serde_json::json!({
764+
"pattern": "needle",
765+
"output_mode": "content",
766+
"limit": DEFAULT_PAGE_LIMIT,
767+
"cursor": ""
768+
}),
769+
&ctx,
770+
)
771+
.await
772+
.unwrap();
773+
774+
assert!(result.success, "{}", result.content);
775+
assert!(result.content.contains("needle"));
776+
}
777+
778+
#[tokio::test]
779+
async fn test_grep_non_paginated_mode_rejects_effective_page_controls() {
780+
let ctx = ToolContext::new(PathBuf::from("/tmp"));
781+
for controls in [
782+
serde_json::json!({"limit": 2}),
783+
serde_json::json!({"cursor": "2"}),
784+
] {
785+
let mut args = serde_json::json!({
786+
"pattern": "needle",
787+
"output_mode": "summary"
788+
});
789+
args.as_object_mut()
790+
.unwrap()
791+
.extend(controls.as_object().unwrap().clone());
792+
let result = GrepTool.execute(&args, &ctx).await.unwrap();
793+
assert!(!result.success);
794+
assert!(result.content.contains("supported only"));
795+
}
796+
}
797+
738798
#[cfg(unix)]
739799
#[tokio::test]
740800
async fn test_grep_source_anchor_preserves_newline_filename_without_injection() {

0 commit comments

Comments
 (0)