fix(fb-34): address PR #125 round 1 review
- error_wire: StructuredError wrapper preserves ErrorV1 through anyhow → classify pipeline. Adds downcast short-circuit so cursor::decode's typed code = "stale_cursor" reaches the wire instead of being string-formatted to code = "generic". - app: search_with_opts now wraps cursor::decode error in StructuredError instead of anyhow! string format. - test: error_wire pins both negative (bare anyhow → not stale_cursor) AND positive (StructuredError → stale_cursor) invariants. CLI integration test runs end-to-end and asserts error.v1.code on stderr. - app: next_cursor only emitted on full-page (k-pop) path; drop speculative emit on snippet-only truncation that would point at a different page than the agent expected. - cursor: differentiate malformed-base64 / malformed-payload / revision-mismatch error messages; all keep code = stale_cursor. - test: cursor_rejected fixture uses .expect() to fail loud on cursor non-emission instead of silent skip. - test: max_tokens=0 → 1-hit floor + truncated=true. - docs: SKILL.md + schema description distinguish snippet-shrink (widen) vs k-pop (paginate) truncated cases. HOTFIXES notes --no-cache semantic shift (cached path + clear vs uncached path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,24 +98,58 @@ fn cursor_rejected_after_corpus_revision_bump() {
|
||||
let page1 = app
|
||||
.search_with_opts(lex("apples", 1), SearchOpts::default())
|
||||
.unwrap();
|
||||
let cursor = page1.next_cursor;
|
||||
// p9-fb-34 round-1 review: replaced silent `if let Some(c) = ...`
|
||||
// with `.expect(...)` so a fixture regression that breaks the
|
||||
// cursor-emission contract fails loudly instead of passing vacuously.
|
||||
let c = page1
|
||||
.next_cursor
|
||||
.expect("k=1 page must emit next_cursor — fixture too small if this fails");
|
||||
|
||||
if let Some(c) = cursor {
|
||||
common::ingest_md(&env, "b.md", "# B\n\nbananas\n");
|
||||
let app2 = env.app();
|
||||
common::ingest_md(&env, "b.md", "# B\n\nbananas\n");
|
||||
let app2 = env.app();
|
||||
|
||||
let result = app2.search_with_opts(
|
||||
lex("apples", 1),
|
||||
SearchOpts {
|
||||
max_tokens: None,
|
||||
snippet_chars: None,
|
||||
cursor: Some(c),
|
||||
},
|
||||
);
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("stale_cursor"),
|
||||
"must surface stale_cursor: {err}"
|
||||
let result = app2.search_with_opts(
|
||||
lex("apples", 1),
|
||||
SearchOpts {
|
||||
max_tokens: None,
|
||||
snippet_chars: None,
|
||||
cursor: Some(c),
|
||||
},
|
||||
);
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("stale_cursor"),
|
||||
"must surface stale_cursor: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_tokens_zero_returns_one_hit_truncated() {
|
||||
// p9-fb-34 round-1 review: pin the documented "≥1 hit floor"
|
||||
// contract — even with `max_tokens=0` (an absurdly tight budget)
|
||||
// the budget loop must keep one hit and flip `truncated: true`.
|
||||
// Fixture intentionally seeds multiple matches so step 2 of the
|
||||
// budget loop (pop hits to 1) actually fires.
|
||||
let env = common::TestEnv::new();
|
||||
for i in 0..3 {
|
||||
common::ingest_md(
|
||||
&env,
|
||||
&format!("d{i}.md"),
|
||||
&format!("# T{i}\n\napples are red {i}\n"),
|
||||
);
|
||||
}
|
||||
let app = env.app();
|
||||
|
||||
let resp = app
|
||||
.search_with_opts(
|
||||
lex("apples", 5),
|
||||
SearchOpts {
|
||||
max_tokens: Some(0),
|
||||
snippet_chars: None,
|
||||
cursor: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resp.hits.len(), 1, "max_tokens=0 collapses to 1-hit floor");
|
||||
assert!(resp.truncated);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user