@hraness/wordcell 0.24.0 → 0.25.0
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.
- package/README.md +14 -12
- package/dist/authoring.js +2 -1
- package/dist/capture.js +2 -2
- package/dist/cli.js +12 -11
- package/dist/clip/cli.js +2 -2
- package/dist/clip/defuddle-worker.js +87 -2
- package/dist/clip/jobs.js +5 -1
- package/dist/evaluation-builder.js +5 -4
- package/dist/evaluation-kb.js +5 -4
- package/dist/graph-authority.js +2 -1
- package/dist/graph-percolation.js +3 -2
- package/dist/{index-44kw9tr8.js → index-1wccy765.js} +18 -12
- package/dist/{index-1er88ckw.js → index-3gc2yk4k.js} +10 -5
- package/dist/{index-0eacgvpv.js → index-47893rgw.js} +22 -8
- package/dist/{index-mvs6f7ne.js → index-59bcxqee.js} +1 -1
- package/dist/{index-1w0sp1q7.js → index-7drercgk.js} +1 -1
- package/dist/{index-tcaq1c7f.js → index-e26mdwxz.js} +1 -1
- package/dist/{index-6jcz0m1c.js → index-hxyjer3x.js} +52 -1
- package/dist/{index-f184qyh1.js → index-hz2wyggk.js} +1 -1
- package/dist/{index-5d1rd5v9.js → index-mcpppwbq.js} +2 -2
- package/dist/index-qkesh4c6.js +7 -0
- package/dist/{index-b0khj0h5.js → index-qt2zwza8.js} +6 -1
- package/dist/{index-2fmnttca.js → index-t3abkwk4.js} +1 -1
- package/dist/index.js +6 -5
- package/dist/portfolio.js +5 -4
- package/dist/sdk.js +4 -3
- package/dist/{support-1yhv6vm6.js → support-adj7bbge.js} +11 -10
- package/dist/url-intelligence.js +3 -2
- package/dist/workflows/decision-context.js +5 -4
- package/dist/workflows/index.js +5 -4
- package/package.json +13 -8
- package/skills/wordcell/SKILL.md +1 -1
- package/skills/wordcell/references/publish.md +2 -1
- package/src/authoring-platform.ts +3 -0
- package/src/cli-program.ts +3 -3
- package/src/clip/jobs.ts +2 -1
- package/src/clip/metadata-search-tool/Cargo.lock +12 -0
- package/src/clip/metadata-search-tool/Cargo.toml +3 -0
- package/src/clip/metadata-search-tool/runner.ts +4 -1
- package/src/clip/metadata-search-tool/src/bing.rs +539 -0
- package/src/clip/metadata-search-tool/src/duckduckgo.rs +562 -0
- package/src/clip/metadata-search-tool/src/main.rs +263 -24
- package/src/clip/metadata-search-tool/src/openalex.rs +557 -0
- package/src/clip/metadata-search.ts +22 -4
- package/src/clip/url-intelligence.ts +54 -1
- package/src/clip/url-metadata.ts +2 -1
- package/src/directory-sync.ts +9 -0
- package/src/graph-authority.ts +5 -2
- package/src/url-intelligence.ts +2 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
mod bing;
|
|
2
|
+
mod openalex;
|
|
3
|
+
mod duckduckgo;
|
|
4
|
+
|
|
1
5
|
use std::alloc::{GlobalAlloc, Layout, System};
|
|
2
6
|
use std::io::{self, Read};
|
|
3
7
|
use std::net::{IpAddr, SocketAddr};
|
|
@@ -8,9 +12,7 @@ use std::time::Duration;
|
|
|
8
12
|
|
|
9
13
|
use metadata_search_engine_rs::{
|
|
10
14
|
aggregator::aggregate,
|
|
11
|
-
engines::{
|
|
12
|
-
BraveEngine, DuckDuckGoEngine, SearchEngine, StartpageEngine, YahooEngine,
|
|
13
|
-
},
|
|
15
|
+
engines::{BraveEngine, SearchEngine, StartpageEngine, YahooEngine},
|
|
14
16
|
models::SearchResponse,
|
|
15
17
|
};
|
|
16
18
|
use reqwest::{
|
|
@@ -18,7 +20,66 @@ use reqwest::{
|
|
|
18
20
|
header::{self, HeaderMap, HeaderValue},
|
|
19
21
|
redirect::Policy,
|
|
20
22
|
};
|
|
21
|
-
use serde::Deserialize;
|
|
23
|
+
use serde::{Deserialize, Serialize};
|
|
24
|
+
|
|
25
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
|
26
|
+
#[serde(rename_all = "kebab-case")]
|
|
27
|
+
enum FailureCode {
|
|
28
|
+
Timeout,
|
|
29
|
+
Http,
|
|
30
|
+
HttpBody,
|
|
31
|
+
HttpStatus,
|
|
32
|
+
HttpForbidden,
|
|
33
|
+
HttpRateLimited,
|
|
34
|
+
HttpRedirect,
|
|
35
|
+
Parse,
|
|
36
|
+
UnverifiedEmpty,
|
|
37
|
+
Challenge,
|
|
38
|
+
ContentType,
|
|
39
|
+
ContentEncoding,
|
|
40
|
+
BodyLimit,
|
|
41
|
+
BodyEncoding,
|
|
42
|
+
ResultLimit,
|
|
43
|
+
UnrecognizedHtml,
|
|
44
|
+
QueryMismatch,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Only bounded categorical diagnostics cross the protocol boundary, never error text.
|
|
48
|
+
#[derive(Serialize)]
|
|
49
|
+
struct EngineFailure {
|
|
50
|
+
engine: String,
|
|
51
|
+
code: FailureCode,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[derive(Serialize)]
|
|
55
|
+
struct EngineUsage { engine: String, reported_cost_usd: f64, billing_status: &'static str }
|
|
56
|
+
|
|
57
|
+
#[derive(Serialize)]
|
|
58
|
+
struct DiagnosticResponse {
|
|
59
|
+
#[serde(flatten)]
|
|
60
|
+
response: SearchResponse,
|
|
61
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
62
|
+
engine_failures: Option<Vec<EngineFailure>>,
|
|
63
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
64
|
+
engine_usage: Option<Vec<EngineUsage>>,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn transport_code(error: &reqwest::Error, otherwise: FailureCode) -> FailureCode {
|
|
68
|
+
if error.is_timeout() {
|
|
69
|
+
FailureCode::Timeout
|
|
70
|
+
} else {
|
|
71
|
+
otherwise
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn status_code(status: u16) -> FailureCode {
|
|
76
|
+
match status {
|
|
77
|
+
403 => FailureCode::HttpForbidden,
|
|
78
|
+
429 => FailureCode::HttpRateLimited,
|
|
79
|
+
300..=399 => FailureCode::HttpRedirect,
|
|
80
|
+
_ => FailureCode::HttpStatus,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
22
83
|
|
|
23
84
|
const MAX_INPUT_BYTES: u64 = 16 * 1024;
|
|
24
85
|
const MAX_QUERY_BYTES: usize = 4 * 1024;
|
|
@@ -137,12 +198,27 @@ struct EngineHost {
|
|
|
137
198
|
#[serde(deny_unknown_fields)]
|
|
138
199
|
struct SearchRequest {
|
|
139
200
|
schema_version: u8,
|
|
201
|
+
#[serde(default)]
|
|
202
|
+
diagnostics: bool,
|
|
203
|
+
#[serde(default)]
|
|
204
|
+
bing_rss: bool,
|
|
205
|
+
#[serde(default)]
|
|
206
|
+
openalex_discovery: bool,
|
|
140
207
|
query: String,
|
|
141
208
|
max_results: usize,
|
|
142
209
|
timeout_ms: u64,
|
|
143
210
|
engine_hosts: Vec<EngineHost>,
|
|
144
211
|
}
|
|
145
212
|
|
|
213
|
+
fn expected_hosts(bing_rss: bool, openalex_discovery: bool) -> Vec<&'static str> {
|
|
214
|
+
let mut hosts = ENGINE_HOSTS.to_vec();
|
|
215
|
+
if bing_rss {
|
|
216
|
+
hosts.push("www.bing.com");
|
|
217
|
+
}
|
|
218
|
+
if openalex_discovery { hosts.push("api.openalex.org"); }
|
|
219
|
+
hosts
|
|
220
|
+
}
|
|
221
|
+
|
|
146
222
|
fn read_request() -> Result<SearchRequest, &'static str> {
|
|
147
223
|
let mut bytes = Vec::new();
|
|
148
224
|
io::stdin()
|
|
@@ -165,10 +241,16 @@ fn read_request() -> Result<SearchRequest, &'static str> {
|
|
|
165
241
|
if !(MIN_TIMEOUT_MS..=MAX_TIMEOUT_MS).contains(&request.timeout_ms) {
|
|
166
242
|
return Err("invalid-timeout");
|
|
167
243
|
}
|
|
168
|
-
|
|
244
|
+
validate_engine_hosts(&request)?;
|
|
245
|
+
Ok(request)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn validate_engine_hosts(request: &SearchRequest) -> Result<(), &'static str> {
|
|
249
|
+
let expected_hosts = expected_hosts(request.bing_rss, request.openalex_discovery);
|
|
250
|
+
if request.engine_hosts.len() != expected_hosts.len() {
|
|
169
251
|
return Err("invalid-engine-hosts");
|
|
170
252
|
}
|
|
171
|
-
for (host, expected) in request.engine_hosts.iter().zip(
|
|
253
|
+
for (host, expected) in request.engine_hosts.iter().zip(expected_hosts) {
|
|
172
254
|
if host.hostname != expected
|
|
173
255
|
|| host.addresses.is_empty()
|
|
174
256
|
|| host.addresses.len() > MAX_ENGINE_ADDRESSES
|
|
@@ -176,7 +258,7 @@ fn read_request() -> Result<SearchRequest, &'static str> {
|
|
|
176
258
|
return Err("invalid-engine-hosts");
|
|
177
259
|
}
|
|
178
260
|
}
|
|
179
|
-
Ok(
|
|
261
|
+
Ok(())
|
|
180
262
|
}
|
|
181
263
|
|
|
182
264
|
fn fail(code: &'static str) -> ! {
|
|
@@ -225,8 +307,7 @@ fn is_private_or_reserved(address: IpAddr) -> bool {
|
|
|
225
307
|
IpAddr::V6(address) => {
|
|
226
308
|
let groups = address.segments();
|
|
227
309
|
let ipv4_compatible = groups[..6].iter().all(|group| *group == 0);
|
|
228
|
-
let ipv4_mapped = groups[..5].iter().all(|group| *group == 0)
|
|
229
|
-
&& groups[5] == 0xffff;
|
|
310
|
+
let ipv4_mapped = groups[..5].iter().all(|group| *group == 0) && groups[5] == 0xffff;
|
|
230
311
|
if ipv4_compatible || ipv4_mapped {
|
|
231
312
|
let high = groups[6];
|
|
232
313
|
let low = groups[7];
|
|
@@ -291,18 +372,12 @@ fn build_pinned_http_client(
|
|
|
291
372
|
);
|
|
292
373
|
headers.insert(
|
|
293
374
|
header::ACCEPT,
|
|
294
|
-
HeaderValue::from_static(
|
|
295
|
-
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
296
|
-
),
|
|
375
|
+
HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
|
|
297
376
|
);
|
|
298
377
|
headers.insert(
|
|
299
378
|
header::ACCEPT_LANGUAGE,
|
|
300
379
|
HeaderValue::from_static("en-US,en;q=0.9"),
|
|
301
380
|
);
|
|
302
|
-
headers.insert(
|
|
303
|
-
header::ACCEPT_ENCODING,
|
|
304
|
-
HeaderValue::from_static("gzip, deflate, br"),
|
|
305
|
-
);
|
|
306
381
|
headers.insert("DNT", HeaderValue::from_static("1"));
|
|
307
382
|
headers.insert("Sec-Fetch-Dest", HeaderValue::from_static("document"));
|
|
308
383
|
headers.insert("Sec-Fetch-Mode", HeaderValue::from_static("navigate"));
|
|
@@ -339,23 +414,60 @@ async fn run() {
|
|
|
339
414
|
Ok(request) => request,
|
|
340
415
|
Err(code) => fail(code),
|
|
341
416
|
};
|
|
342
|
-
let timeout =
|
|
417
|
+
let timeout =
|
|
418
|
+
Duration::from_millis(request.timeout_ms / expected_hosts(request.bing_rss, request.openalex_discovery).len() as u64);
|
|
343
419
|
let client = Arc::new(
|
|
344
|
-
build_pinned_http_client(&request.engine_hosts, timeout)
|
|
345
|
-
.unwrap_or_else(|code| fail(code)),
|
|
420
|
+
build_pinned_http_client(&request.engine_hosts, timeout).unwrap_or_else(|code| fail(code)),
|
|
346
421
|
);
|
|
347
422
|
let engines: Vec<Arc<dyn SearchEngine>> = vec![
|
|
348
|
-
Arc::new(DuckDuckGoEngine::with_timeout(Arc::clone(&client), timeout)),
|
|
349
423
|
Arc::new(BraveEngine::with_timeout(Arc::clone(&client), timeout)),
|
|
350
424
|
Arc::new(StartpageEngine::with_timeout(Arc::clone(&client), timeout)),
|
|
351
425
|
Arc::new(YahooEngine::with_timeout(Arc::clone(&client), timeout)),
|
|
352
426
|
];
|
|
353
|
-
let engines_queried = engines
|
|
427
|
+
let mut engines_queried = engines
|
|
354
428
|
.iter()
|
|
355
429
|
.map(|engine| engine.name().to_string())
|
|
356
430
|
.collect::<Vec<_>>();
|
|
431
|
+
engines_queried.insert(0, "duckduckgo".to_string());
|
|
357
432
|
let mut successes = Vec::new();
|
|
358
433
|
let mut engines_failed = Vec::new();
|
|
434
|
+
let mut engine_failures = Vec::new();
|
|
435
|
+
let mut engine_usage = Vec::new();
|
|
436
|
+
// Optional independent backend runs first within the existing total time budget.
|
|
437
|
+
if request.openalex_discovery {
|
|
438
|
+
engines_queried.insert(0, "openalex".to_string());
|
|
439
|
+
match openalex::search(&client, request.query.trim(), request.max_results, timeout).await {
|
|
440
|
+
Ok(discovery) => {
|
|
441
|
+
if let Some(cost)=discovery.reported_cost_usd { engine_usage.push(EngineUsage {engine:"openalex".into(),reported_cost_usd:cost,billing_status:"not-established"}); }
|
|
442
|
+
successes.push(("openalex".to_string(), discovery.results));
|
|
443
|
+
},
|
|
444
|
+
Err(code) => { engines_failed.push("openalex".to_string()); engine_failures.push(EngineFailure {engine:"openalex".to_string(),code}); }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if request.bing_rss {
|
|
448
|
+
engines_queried.insert(0, "bing".to_string());
|
|
449
|
+
match bing::search(&client, request.query.trim(), request.max_results, timeout).await {
|
|
450
|
+
Ok(results) => successes.push(("bing".to_string(), results)),
|
|
451
|
+
Err(code) => {
|
|
452
|
+
engines_failed.push("bing".to_string());
|
|
453
|
+
engine_failures.push(EngineFailure {
|
|
454
|
+
engine: "bing".to_string(),
|
|
455
|
+
code,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
match duckduckgo::search(&client, request.query.trim(), request.max_results, timeout).await {
|
|
461
|
+
Ok(results) => successes.push(("duckduckgo".to_string(), results)),
|
|
462
|
+
Err(code) => {
|
|
463
|
+
eprintln!("metadata search engine duckduckgo: {code:?}");
|
|
464
|
+
engines_failed.push("duckduckgo".to_string());
|
|
465
|
+
engine_failures.push(EngineFailure {
|
|
466
|
+
engine: "duckduckgo".to_string(),
|
|
467
|
+
code,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
359
471
|
// Run one upstream engine at a time so its unbounded internal `text()`
|
|
360
472
|
// allocation is contained by the process memory ceiling rather than
|
|
361
473
|
// multiplied across four concurrent responses.
|
|
@@ -365,8 +477,30 @@ async fn run() {
|
|
|
365
477
|
.search(request.query.trim(), request.max_results)
|
|
366
478
|
.await
|
|
367
479
|
{
|
|
368
|
-
Ok(results) => successes.push((name, results)),
|
|
369
|
-
|
|
480
|
+
Ok(results) if !results.is_empty() => successes.push((name, results)),
|
|
481
|
+
Ok(_) => {
|
|
482
|
+
eprintln!("metadata search engine {name}: unverified-empty");
|
|
483
|
+
engine_failures.push(EngineFailure {
|
|
484
|
+
engine: name.clone(),
|
|
485
|
+
code: FailureCode::UnverifiedEmpty,
|
|
486
|
+
});
|
|
487
|
+
engines_failed.push(name);
|
|
488
|
+
}
|
|
489
|
+
Err(error) => {
|
|
490
|
+
use metadata_search_engine_rs::error::EngineError;
|
|
491
|
+
let code = match error {
|
|
492
|
+
EngineError::Timeout { .. } => FailureCode::Timeout,
|
|
493
|
+
EngineError::Http { source, .. } => transport_code(&source, FailureCode::Http),
|
|
494
|
+
EngineError::BadStatus { status, .. } => status_code(status),
|
|
495
|
+
EngineError::ParseFailed { .. } => FailureCode::Parse,
|
|
496
|
+
};
|
|
497
|
+
eprintln!("metadata search engine {name}: {code:?}");
|
|
498
|
+
engine_failures.push(EngineFailure {
|
|
499
|
+
engine: name.clone(),
|
|
500
|
+
code,
|
|
501
|
+
});
|
|
502
|
+
engines_failed.push(name);
|
|
503
|
+
}
|
|
370
504
|
}
|
|
371
505
|
}
|
|
372
506
|
let mut results = aggregate(successes, request.max_results);
|
|
@@ -383,7 +517,112 @@ async fn run() {
|
|
|
383
517
|
engines_queried,
|
|
384
518
|
engines_failed,
|
|
385
519
|
};
|
|
386
|
-
if serde_json::to_writer(
|
|
520
|
+
if serde_json::to_writer(
|
|
521
|
+
io::stdout().lock(),
|
|
522
|
+
&DiagnosticResponse {
|
|
523
|
+
response,
|
|
524
|
+
engine_failures: request.diagnostics.then_some(engine_failures),
|
|
525
|
+
engine_usage: (request.diagnostics && !engine_usage.is_empty()).then_some(engine_usage),
|
|
526
|
+
},
|
|
527
|
+
)
|
|
528
|
+
.is_err()
|
|
529
|
+
{
|
|
387
530
|
fail("stdout-write");
|
|
388
531
|
}
|
|
389
532
|
}
|
|
533
|
+
|
|
534
|
+
#[cfg(test)]
|
|
535
|
+
mod diagnostic_tests {
|
|
536
|
+
use super::*;
|
|
537
|
+
#[test]
|
|
538
|
+
fn emits_categorical_diagnostics_inside_search_protocol() {
|
|
539
|
+
let response = DiagnosticResponse {
|
|
540
|
+
response: SearchResponse {
|
|
541
|
+
query: "query".into(),
|
|
542
|
+
results: vec![],
|
|
543
|
+
engines_queried: vec!["brave".into()],
|
|
544
|
+
engines_failed: vec!["brave".into()],
|
|
545
|
+
},
|
|
546
|
+
engine_usage: None,
|
|
547
|
+
engine_failures: Some(vec![EngineFailure {
|
|
548
|
+
engine: "brave".into(),
|
|
549
|
+
code: FailureCode::HttpForbidden,
|
|
550
|
+
}]),
|
|
551
|
+
};
|
|
552
|
+
let value = serde_json::to_value(response).unwrap();
|
|
553
|
+
assert_eq!(
|
|
554
|
+
value["engine_failures"],
|
|
555
|
+
serde_json::json!([{ "engine": "brave", "code": "http-forbidden" }])
|
|
556
|
+
);
|
|
557
|
+
assert_eq!(value.as_object().unwrap().len(), 5);
|
|
558
|
+
assert_eq!(status_code(429), FailureCode::HttpRateLimited);
|
|
559
|
+
assert_eq!(status_code(302), FailureCode::HttpRedirect);
|
|
560
|
+
assert_eq!(status_code(503), FailureCode::HttpStatus);
|
|
561
|
+
}
|
|
562
|
+
#[test]
|
|
563
|
+
fn diagnostics_are_an_explicit_boolean_opt_in() {
|
|
564
|
+
let legacy = serde_json::json!({"schema_version":1,"query":"query","max_results":3,"timeout_ms":15000,"engine_hosts":[]});
|
|
565
|
+
let old: SearchRequest = serde_json::from_value(legacy.clone()).unwrap();
|
|
566
|
+
assert!(!old.diagnostics);
|
|
567
|
+
for requested in [false, true] {
|
|
568
|
+
let mut input = legacy.clone();
|
|
569
|
+
input["diagnostics"] = serde_json::json!(requested);
|
|
570
|
+
let parsed: SearchRequest = serde_json::from_value(input).unwrap();
|
|
571
|
+
let output = DiagnosticResponse {
|
|
572
|
+
response: SearchResponse {
|
|
573
|
+
query: "query".into(),
|
|
574
|
+
results: vec![],
|
|
575
|
+
engines_queried: vec![],
|
|
576
|
+
engines_failed: vec![],
|
|
577
|
+
},
|
|
578
|
+
engine_failures: parsed.diagnostics.then_some(vec![]),
|
|
579
|
+
engine_usage: None,
|
|
580
|
+
};
|
|
581
|
+
let encoded = serde_json::to_value(output).unwrap();
|
|
582
|
+
assert_eq!(
|
|
583
|
+
encoded.as_object().unwrap().len(),
|
|
584
|
+
if requested { 5 } else { 4 }
|
|
585
|
+
);
|
|
586
|
+
assert_eq!(encoded.get("engine_failures").is_some(), requested);
|
|
587
|
+
}
|
|
588
|
+
for invalid in [
|
|
589
|
+
serde_json::json!(null),
|
|
590
|
+
serde_json::json!(1),
|
|
591
|
+
serde_json::json!("true"),
|
|
592
|
+
] {
|
|
593
|
+
let mut input = legacy.clone();
|
|
594
|
+
input["diagnostics"] = invalid;
|
|
595
|
+
assert!(serde_json::from_value::<SearchRequest>(input).is_err());
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
#[test]
|
|
599
|
+
fn optional_bing_preserves_exact_legacy_host_contract() {
|
|
600
|
+
let mut value = serde_json::json!({"schema_version":1,"query":"query","max_results":3,"timeout_ms":15000,
|
|
601
|
+
"engine_hosts": expected_hosts(false, false).into_iter().map(|hostname| serde_json::json!({"hostname":hostname,"addresses":[{"address":"93.184.216.34","family":4}]})).collect::<Vec<_>>() });
|
|
602
|
+
let legacy: SearchRequest = serde_json::from_value(value.clone()).unwrap();
|
|
603
|
+
assert!(!legacy.bing_rss);
|
|
604
|
+
assert!(validate_engine_hosts(&legacy).is_ok());
|
|
605
|
+
value["bing_rss"] = serde_json::json!(true);
|
|
606
|
+
assert!(validate_engine_hosts(&serde_json::from_value(value.clone()).unwrap()).is_err());
|
|
607
|
+
value["engine_hosts"].as_array_mut().unwrap().push(serde_json::json!({"hostname":"www.bing.com","addresses":[{"address":"93.184.216.34","family":4}]}));
|
|
608
|
+
assert!(validate_engine_hosts(&serde_json::from_value(value.clone()).unwrap()).is_ok());
|
|
609
|
+
value["bing_rss"] = serde_json::json!(false);
|
|
610
|
+
assert!(validate_engine_hosts(&serde_json::from_value(value.clone()).unwrap()).is_err());
|
|
611
|
+
value["bing_rss"] = serde_json::json!("true");
|
|
612
|
+
assert!(serde_json::from_value::<SearchRequest>(value).is_err());
|
|
613
|
+
}
|
|
614
|
+
#[test]
|
|
615
|
+
fn optional_openalex_preserves_legacy_hosts_and_boolean_contract() {
|
|
616
|
+
let mut value=serde_json::json!({"schema_version":1,"query":"query","max_results":3,"timeout_ms":15000,
|
|
617
|
+
"engine_hosts":expected_hosts(false,false).into_iter().map(|hostname|serde_json::json!({"hostname":hostname,"addresses":[{"address":"93.184.216.34","family":4}]})).collect::<Vec<_>>()});
|
|
618
|
+
let legacy:SearchRequest=serde_json::from_value(value.clone()).unwrap();assert!(!legacy.openalex_discovery);
|
|
619
|
+
value["openalex_discovery"]=serde_json::json!(true);
|
|
620
|
+
assert!(validate_engine_hosts(&serde_json::from_value(value.clone()).unwrap()).is_err());
|
|
621
|
+
value["engine_hosts"].as_array_mut().unwrap().push(serde_json::json!({"hostname":"api.openalex.org","addresses":[{"address":"93.184.216.34","family":4}]}));
|
|
622
|
+
assert!(validate_engine_hosts(&serde_json::from_value(value.clone()).unwrap()).is_ok());
|
|
623
|
+
value["openalex_discovery"]=serde_json::json!(false);
|
|
624
|
+
assert!(validate_engine_hosts(&serde_json::from_value(value.clone()).unwrap()).is_err());
|
|
625
|
+
value["openalex_discovery"]=serde_json::json!("true");assert!(serde_json::from_value::<SearchRequest>(value).is_err());
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
}
|