tests.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. use super::parse::bench_complete;
  2. use super::quality::{Medals, award_medals, incomplete};
  3. use super::render::fmt_pct;
  4. use super::serve::load_report;
  5. use super::{ReportRow, collect, detect_quant, extract_time, html_page, parse_score, slash_pair};
  6. use crate::config::Config;
  7. use std::fs;
  8. use std::path::PathBuf;
  9. #[test]
  10. fn slash_pair_tolerates_bold_and_spaces() {
  11. assert_eq!(slash_pair("**81** / 100"), Some((81, 100)));
  12. assert_eq!(
  13. slash_pair("Score: 159/176 (90.4) — 74 pass"),
  14. Some((159, 176))
  15. );
  16. assert_eq!(slash_pair("no pair here"), None);
  17. }
  18. #[test]
  19. fn parse_score_handles_both_header_styles() {
  20. let bench = "- **Final Score**: **81** / 100\n- more\n";
  21. assert_eq!(parse_score(bench), Some((81, 100, Some(81.0))));
  22. let campaign = "intro\n## Score: 159/176 (90.4) — 74 pass / 11 partial\n";
  23. assert_eq!(parse_score(campaign), Some((159, 176, Some(90.4))));
  24. assert_eq!(parse_score("# no score in here\n"), None);
  25. }
  26. #[test]
  27. fn parse_score_reads_log_summary_box() {
  28. // raw tool-eval-bench log: the score box sits deep in the file
  29. let log = "│ Quality: 88/100 │\n\
  30. │ Score: 88 / 100 │\n\
  31. │ • Final score: (total points / max points) × 100 │\n";
  32. assert_eq!(parse_score(log), Some((88, 100, Some(88.0))));
  33. }
  34. #[test]
  35. fn parse_score_ignores_transcript_noise() {
  36. // embedded tool transcripts contain "score" but no leading score header
  37. let noise = "tool_result={\"relevance_score\": 0.95, \"url\": \"x/1\"}\n\
  38. Note: my initial search returned no matches, would you like more?\n\
  39. │ Deployability: 73/100 (α=0.7) │\n\
  40. │ Responsiveness: 39/100 (median turn: 4.1s) │\n";
  41. assert_eq!(parse_score(noise), None);
  42. }
  43. #[test]
  44. fn fmt_pct_whole_and_fractional() {
  45. assert_eq!(fmt_pct(81.0), "81");
  46. assert_eq!(fmt_pct(90.4), "90.4");
  47. }
  48. #[test]
  49. fn extract_time_reads_both_timestamp_shapes() {
  50. let md = "- **Run ID**: `2026-09-06T01-10-29.307431Z_edbd57fa`\n\
  51. - **Date**: `2026-09-06T01:58:31.440817+00:00`\n";
  52. assert_eq!(extract_time(md), Some("01:10".into()));
  53. assert_eq!(
  54. extract_time("x 2026-09-07T23:05:00 y\n"),
  55. Some("23:05".into())
  56. );
  57. assert_eq!(extract_time("# no time here\n"), None);
  58. assert_eq!(extract_time("2026-09-07 at 12:30 o'clock\n"), None);
  59. assert_eq!(extract_time("2026-09-07T12:3 short\n"), None);
  60. }
  61. #[test]
  62. fn detect_quant_prefers_longest_token() {
  63. assert_eq!(detect_quant("glm52-hybrid-fp8-mxfp4", "x"), "MXFP4");
  64. assert_eq!(detect_quant("x", "unsloth/Qwen3.8-27B-NVFP4"), "NVFP4");
  65. assert_eq!(detect_quant("glm53-flash-r18-4x-nvfp4kv", ""), "NVFP4KV");
  66. assert_eq!(detect_quant("qwen38-27b-int4", ""), "INT4");
  67. assert_eq!(detect_quant("deepseek-v4-flash-dspark38", "m"), "—");
  68. }
  69. pub(crate) fn fixture() -> PathBuf {
  70. use std::sync::atomic::{AtomicU32, Ordering};
  71. static N: AtomicU32 = AtomicU32::new(0);
  72. // unique per call — tests run in parallel and clean up after themselves
  73. let dir = std::env::temp_dir().join(format!(
  74. "sm121-report-{}-{}",
  75. std::process::id(),
  76. N.fetch_add(1, Ordering::Relaxed)
  77. ));
  78. let r = dir.join("test-recipe-nvfp4");
  79. fs::create_dir_all(r.join("reports")).unwrap();
  80. fs::write(
  81. r.join("recipe.toml"),
  82. "engine = \"vllm\"\nname = \"Test Recipe\"\nmodel = \"org/Test-Model-NVFP4\"\nrequired_nodes = 2\n",
  83. )
  84. .unwrap();
  85. fs::write(
  86. r.join("reports/2026-01-02_demo-run.md"),
  87. "# demo\n## Score: 159/176 (90.4)\n",
  88. )
  89. .unwrap();
  90. fs::write(
  91. r.join("reports/2026-01-01_raw.log"),
  92. "noise\nrun 2026-01-01T08-30-00Z\n│ Score: 88 / 100 │\n",
  93. )
  94. .unwrap();
  95. fs::write(
  96. r.join("reports/2026-01-03_hardmode.md"),
  97. "# hm\n## Score: 159/176 (90.4) — 74 pass / 11 partial\n",
  98. )
  99. .unwrap();
  100. // a full `sm121 bench` run triplet: one bench run must yield ONE entry
  101. fs::write(
  102. r.join("reports/2026-01-04_roce-run.json"),
  103. r#"{"scores": {"final_score": 92, "total_points": 162, "max_points": 176}}"#,
  104. )
  105. .unwrap();
  106. fs::write(
  107. r.join("reports/2026-01-04_roce-run.md"),
  108. "- **Run ID**: `2026-01-04T11:08:03.183685Z_ef01052a`\n- **Final Score**: **92** / 100\n",
  109. )
  110. .unwrap();
  111. fs::write(
  112. r.join("reports/2026-01-04_roce-run.log"),
  113. "noise\n│ Score: 92 / 100 │\n\n\
  114. │ pp2048 tg128 @ d0 │ c1 │ 1,500 │ 27.4 │ 1,398 │ 5,959 │ 2048+128\n\
  115. │ pp2048 tg128 @ d0 │ c2 │ 1,346 │ 33.3 │ 1,999 │ 8,653 │ 2048+128\n",
  116. )
  117. .unwrap();
  118. dir
  119. }
  120. #[test]
  121. fn collect_carries_file_and_time() {
  122. let dir = fixture();
  123. let config: Config =
  124. toml::from_str(&format!("recipe_sources = [\"{}\"]", dir.to_str().unwrap())).unwrap();
  125. let rows = collect(&config, false);
  126. fs::remove_dir_all(&dir).ok();
  127. let row = rows.iter().find(|r| r.name == "test-recipe-nvfp4").unwrap();
  128. assert_eq!(row.reports[2].file, "2026-01-03_hardmode.md");
  129. assert_eq!(row.reports[2].time, None, "fixture .md has no timestamp");
  130. assert_eq!(
  131. row.reports[0].time,
  132. Some("08:30".into()),
  133. "from Run ID in .log"
  134. );
  135. }
  136. fn entry(pct: f64, done: bool) -> super::ReportEntry {
  137. serde_json::from_value(serde_json::json!({
  138. "date": "2026-01-02", "time": null, "label": "l", "file": "f.md",
  139. "files": ["f.md"], "pct": pct, "points": null, "ratio": null,
  140. "done": done, "perf": [],
  141. }))
  142. .unwrap()
  143. }
  144. fn qrow(name: &str, parent: &str, model: &str, tp: u32, pct: Option<f64>) -> ReportRow {
  145. let reports = match pct {
  146. Some(p) => serde_json::json!([
  147. {"file": "f.md", "files": ["f.md"], "date": "2026-01-02", "label": "l", "pct": p, "done": true}
  148. ]),
  149. None => serde_json::json!([]),
  150. };
  151. serde_json::from_value(serde_json::json!({
  152. "name": name, "title": name, "model": model, "parent": parent,
  153. "quant": "NVFP4", "engine": "vllm", "image": "img", "nodes": tp,
  154. "tp": tp, "deploy": "docker", "port": 30000,
  155. "reports": reports,
  156. }))
  157. .unwrap()
  158. }
  159. #[test]
  160. fn bench_complete_requires_full_pool() {
  161. // Legacy dev-era file (176-point pool, no status marker).
  162. assert!(bench_complete(
  163. r#"{"scores": {"final_score": 92, "total_points": 162, "max_points": 176, "scenario_results": {}}}"#
  164. ));
  165. // v2.7.0 pool (184 pts) — completed by coverage, no hardcode.
  166. let mut results = serde_json::Map::new();
  167. for i in 0..92 {
  168. results.insert(format!("TC-{i}"), serde_json::json!({"pass": true}));
  169. }
  170. assert!(bench_complete(
  171. &serde_json::json!({"status": "completed", "total_scenarios": 92,
  172. "scores": {"final_score": 91, "total_points": 168, "max_points": 184,
  173. "scenario_results": results}})
  174. .to_string()
  175. ));
  176. // Interrupted early: fewer scenarios ran than the tool declares.
  177. let partial =
  178. serde_json::Map::from_iter((0..44).map(|i| (format!("TC-{i}"), serde_json::json!({}))));
  179. assert!(!bench_complete(
  180. &serde_json::json!({"status": "completed", "total_scenarios": 92,
  181. "scores": {"final_score": 45, "total_points": 88, "max_points": 184,
  182. "scenario_results": partial}})
  183. .to_string()
  184. ));
  185. // Explicit non-completed status always fails, coverage or not.
  186. assert!(!bench_complete(
  187. r#"{"status": "interrupted", "total_scenarios": 92, "scores": {"final_score": 90, "scenario_results": {}}}"#
  188. ));
  189. assert!(!bench_complete("not json"));
  190. }
  191. #[test]
  192. fn incomplete_flags_pool_not_finished() {
  193. assert!(!incomplete(&[]));
  194. assert!(incomplete(&[entry(0.0, false)]));
  195. assert!(!incomplete(&[entry(0.0, false), entry(50.0, true)]));
  196. }
  197. #[test]
  198. fn medals_rank_family_model_and_tp_tiers() {
  199. let mut rows = vec![
  200. qrow("a", "M", "m1", 2, Some(90.0)),
  201. qrow("b", "M", "m1", 2, Some(94.0)), // family gold, model gold, tp(2) gold
  202. qrow("c", "M", "m1", 2, Some(88.0)), // family bronze, model bronze, tp(2) bronze
  203. qrow("d", "M", "m2", 4, Some(91.0)), // family silver, model-m2 gold, tp(4) gold
  204. qrow("e", "M", "m1", 2, None), // no score — no medal anywhere
  205. qrow("f", "N", "n1", 1, Some(50.0)), // own family/model/tp golds
  206. ];
  207. award_medals(&mut rows);
  208. let got = |n: &str| rows.iter().find(|r| r.name == n).unwrap().medals;
  209. assert_eq!(
  210. got("b"),
  211. Medals {
  212. p: Some(1),
  213. m: Some(1),
  214. t: Some(1)
  215. }
  216. );
  217. assert_eq!(
  218. got("a"),
  219. Medals {
  220. p: Some(3),
  221. m: Some(2),
  222. t: Some(2)
  223. }
  224. );
  225. // family rank 4 (d=91 outranks) — no family medal, but model/tp medals hold
  226. assert_eq!(
  227. got("c"),
  228. Medals {
  229. p: None,
  230. m: Some(3),
  231. t: Some(3)
  232. }
  233. );
  234. assert_eq!(
  235. got("d"),
  236. Medals {
  237. p: Some(2),
  238. m: Some(1),
  239. t: Some(1)
  240. },
  241. "family silver; tp(4) own group gold"
  242. );
  243. assert_eq!(got("e"), Medals::default());
  244. assert_eq!(
  245. got("f"),
  246. Medals {
  247. p: Some(1),
  248. m: Some(1),
  249. t: Some(1)
  250. }
  251. );
  252. }
  253. #[test]
  254. fn load_report_resolves_and_rejects_traversal() {
  255. let dir = fixture();
  256. let config: Config =
  257. toml::from_str(&format!("recipe_sources = [\"{}\"]", dir.to_str().unwrap())).unwrap();
  258. let ok = load_report(&config, "test-recipe-nvfp4", "2026-01-03_hardmode.md");
  259. fs::remove_dir_all(&dir).ok();
  260. assert!(ok.unwrap().contains("159/176"));
  261. let dir = fixture();
  262. let config: Config =
  263. toml::from_str(&format!("recipe_sources = [\"{}\"]", dir.to_str().unwrap())).unwrap();
  264. let toml = load_report(&config, "test-recipe-nvfp4", "recipe.toml");
  265. fs::remove_dir_all(&dir).ok();
  266. assert!(
  267. toml.unwrap().contains("engine = \"vllm\""),
  268. "recipe.toml served"
  269. );
  270. let dir = fixture();
  271. let config: Config =
  272. toml::from_str(&format!("recipe_sources = [\"{}\"]", dir.to_str().unwrap())).unwrap();
  273. fs::remove_dir_all(&dir).ok();
  274. assert_eq!(
  275. load_report(&config, "test-recipe-nvfp4", "../../etc/passwd"),
  276. None
  277. );
  278. assert_eq!(load_report(&config, "..", "x.md"), None);
  279. assert_eq!(
  280. load_report(&config, "test-recipe-nvfp4", "missing.md"),
  281. None
  282. );
  283. }
  284. #[test]
  285. fn collect_reads_recipe_reports_and_dedups() {
  286. let dir = fixture();
  287. let config: Config =
  288. toml::from_str(&format!("recipe_sources = [\"{}\"]", dir.to_str().unwrap())).unwrap();
  289. let rows = collect(&config, false);
  290. fs::remove_dir_all(&dir).ok();
  291. let row = rows.iter().find(|r| r.name == "test-recipe-nvfp4").unwrap();
  292. assert_eq!(row.title, "Test Recipe");
  293. assert_eq!(row.quant, "NVFP4");
  294. assert_eq!(row.nodes, Some(2));
  295. assert_eq!(row.reports.len(), 4);
  296. assert_eq!(row.reports[0].label, "raw");
  297. assert_eq!(row.reports[0].pct, Some(88.0));
  298. assert_eq!(row.reports[0].points, None);
  299. assert_eq!(row.reports[1].label, "demo-run");
  300. assert_eq!(row.reports[1].pct, Some(90.4));
  301. assert_eq!(row.reports[1].points.as_deref(), Some("159/176"));
  302. assert_eq!(row.reports[1].ratio, Some(159.0 / 176.0));
  303. assert_eq!(row.reports[2].label, "hardmode");
  304. assert_eq!(row.reports[2].points.as_deref(), Some("159/176"));
  305. assert_eq!(row.reports[2].pct, Some(90.4));
  306. assert_eq!(row.reports[2].ratio, Some(159.0 / 176.0));
  307. }
  308. #[test]
  309. fn bench_triplet_groups_into_one_entry_with_json_authority() {
  310. let dir = fixture();
  311. let config: Config =
  312. toml::from_str(&format!("recipe_sources = [\"{}\"]", dir.to_str().unwrap())).unwrap();
  313. let rows = collect(&config, false);
  314. fs::remove_dir_all(&dir).ok();
  315. let row = rows.iter().find(|r| r.name == "test-recipe-nvfp4").unwrap();
  316. let e = &row.reports[3];
  317. assert_eq!(e.label, "roce-run");
  318. // the JSON score wins over the md/log text (same value here — 92)
  319. assert_eq!(e.pct, Some(92.0));
  320. assert_eq!(e.points.as_deref(), Some("162/176"));
  321. assert_eq!(e.ratio, Some(162.0 / 176.0));
  322. // primary file = the human .md; all three files are listed
  323. assert_eq!(e.file, "2026-01-04_roce-run.md");
  324. assert_eq!(
  325. e.files,
  326. vec![
  327. "2026-01-04_roce-run.json",
  328. "2026-01-04_roce-run.log",
  329. "2026-01-04_roce-run.md"
  330. ]
  331. );
  332. // time comes from the md Run ID (11:08)
  333. assert_eq!(e.time, Some("11:08".into()));
  334. // decode speeds parsed from the log perf box
  335. assert_eq!(
  336. e.perf,
  337. vec![
  338. super::PerfCell {
  339. d: 0,
  340. c: 1,
  341. tg: 27.4
  342. },
  343. super::PerfCell {
  344. d: 0,
  345. c: 2,
  346. tg: 33.3
  347. },
  348. ]
  349. );
  350. }
  351. #[test]
  352. fn parse_bench_json_reads_scores_block() {
  353. let v = r#"{"scores": {"final_score": 88, "total_points": 155, "max_points": 176}}"#;
  354. assert_eq!(
  355. super::parse_bench_json(v),
  356. Some((88.0, Some("155/176".into()), Some(155.0 / 176.0)))
  357. );
  358. // max_points 100 → points suppressed (already normalized), pct kept
  359. let v = r#"{"scores": {"final_score": 81.0, "total_points": 81, "max_points": 100}}"#;
  360. assert_eq!(super::parse_bench_json(v), Some((81.0, None, None)));
  361. // missing block / garbage
  362. assert_eq!(super::parse_bench_json("{}"), None);
  363. assert_eq!(super::parse_bench_json("not json"), None);
  364. assert_eq!(super::parse_bench_json(r#"{"scores": {}}"#), None);
  365. }
  366. #[test]
  367. fn parse_perf_box_reads_both_table_shapes() {
  368. // modern box rows (concurrency as its own column)
  369. let box_log = "\u{2502} pp2048 tg128 @ d0 \u{2502} c1 \u{2502} 1,500 \u{2502} 27.4 \u{2502} 1,398 \u{2502} 5,959 \u{2502} 2048+128\n\
  370. \u{2502} pp2048 tg128 @ d4096 \u{2502} c2 \u{2502} 1,642 \u{2502} 30.6 \u{2502} 6,040 \u{2502} 12,822 \u{2502} 2048+128\n";
  371. assert_eq!(
  372. super::parse::parse_perf_box(box_log),
  373. vec![
  374. super::PerfCell {
  375. d: 0,
  376. c: 1,
  377. tg: 27.4
  378. },
  379. super::PerfCell {
  380. d: 4096,
  381. c: 2,
  382. tg: 30.6
  383. },
  384. ]
  385. );
  386. // legacy markdown rows (concurrency in the label, absent = c1)
  387. let md = "| pp2048 tg128 @ d0 | 1,861 | 24.5 | 1,236 | 6,228 | 2048+128 |\n\
  388. | pp2048 tg128 @ d0 c2 | 1,728 | 36.0 | 2,097 | 8,802 | 2048+128 |\n\
  389. | Test | pp t/s | tg t/s | TTFT (ms) | Total (ms) | Tokens |\n";
  390. assert_eq!(
  391. super::parse::parse_perf_box(md),
  392. vec![
  393. super::PerfCell {
  394. d: 0,
  395. c: 1,
  396. tg: 24.5
  397. },
  398. super::PerfCell {
  399. d: 0,
  400. c: 2,
  401. tg: 36.0
  402. },
  403. ]
  404. );
  405. // header chrome and prose must not produce cells
  406. assert!(super::parse::parse_perf_box("pp=[2048] tg=[128] depth=[0, 4096]").is_empty());
  407. assert!(super::parse::parse_perf_box("no perf here").is_empty());
  408. }
  409. #[test]
  410. fn parse_perf_box_ignores_progress_spinner_soup() {
  411. // llama-benchy redraws its progress bar on one \r-overwritten line that
  412. // embeds the same `pp2048 tg128 @ d0` labels with no table columns —
  413. // it used to parse as a bogus {d:0, c:1, tg:0.0} cell that shadowed the
  414. // real one in first-match lookups (glm53-flash-eugr-2x).
  415. let soup = "⠋ Initializing… ━━━━━━━━━━━━━━━━━━━━━━ 0/18 0:00:00⠙ Warming… pp2048 tg128 @ d0 0/18 0:00:00⠹ ━━━━ 0/18\n\
  416. │ pp2048 tg128 @ d0 │ c1 │ 1,198 │ 29.0 │ 2,113 │ 6,437 │ 2048+128\n\
  417. │ pp2048 tg128 @ d0 │ c2 │ 1,287 │ 35.1 │ 2,148 │ 8,054 │ 2048+128\n";
  418. let cells = super::parse::parse_perf_box(soup);
  419. assert_eq!(
  420. cells,
  421. vec![
  422. super::PerfCell {
  423. d: 0,
  424. c: 1,
  425. tg: 29.0
  426. },
  427. super::PerfCell {
  428. d: 0,
  429. c: 2,
  430. tg: 35.1
  431. },
  432. ]
  433. );
  434. // first-match lookup must see the real speed, not 0
  435. assert_eq!(
  436. cells.iter().find(|p| p.d == 0 && p.c == 1).map(|p| p.tg),
  437. Some(29.0)
  438. );
  439. }
  440. #[test]
  441. fn html_page_keeps_view_state_in_the_url() {
  442. // the full view (filter, grouping, sort, columns, paging) must be
  443. // deep-link-backed: parse from the query string on load, and
  444. // replaceState it on every render
  445. let page = html_page(&[]);
  446. assert!(page.contains("new URLSearchParams(location.search)"));
  447. assert!(page.contains("history.replaceState"));
  448. assert!(page.contains("syncUrl();"));
  449. }
  450. #[test]
  451. fn report_keys_fall_back_outside_en() {
  452. let _ = fluent_i18n::set_locale(Some("ru"));
  453. let s = fluent_i18n::t!("report-title", { "count" => 3 });
  454. let _ = fluent_i18n::set_locale(None);
  455. assert!(s.contains('3'), "fallback broken, got: {s}");
  456. assert_ne!(s, "report-title", "missing fluent fallback, got: {s}");
  457. }
  458. #[tokio::test]
  459. async fn refresher_publishes_on_change_only() {
  460. use super::serve::refresh_loop;
  461. use std::time::Duration;
  462. let dir = fixture();
  463. let config: Config =
  464. toml::from_str(&format!("recipe_sources = [\"{}\"]", dir.to_str().unwrap())).unwrap();
  465. let cfg = std::sync::Arc::new(config);
  466. let (tx, mut rx) = tokio::sync::watch::channel(String::new());
  467. // keepalive 100 ms: if the file watcher fires, the update lands via the
  468. // event path; if not, the keepalive picks it up — either way within 3 s
  469. let h = tokio::spawn(refresh_loop(cfg, tx, Duration::from_millis(100), false));
  470. // the first collect runs immediately: the fixture's report is visible
  471. tokio::time::timeout(
  472. Duration::from_secs(3),
  473. rx.wait_for(|j| j.contains("demo-run")),
  474. )
  475. .await
  476. .expect("initial collect not published")
  477. .expect("channel closed");
  478. // a new report file appears → the next tick publishes the fresh JSON
  479. fs::write(
  480. dir.join("test-recipe-nvfp4/reports/2026-01-05_live-update.md"),
  481. "# lu\n## Score: 159/176 (90.4)\n",
  482. )
  483. .unwrap();
  484. tokio::time::timeout(
  485. Duration::from_secs(3),
  486. rx.wait_for(|j| j.contains("live-update")),
  487. )
  488. .await
  489. .expect("change not published within 3 s")
  490. .expect("channel closed");
  491. // stability: with no further changes the JSON stays put (no re-publish
  492. // storm on a quiet tree — the SSE side only wakes on watch changes)
  493. let stable = rx.borrow().clone();
  494. tokio::time::sleep(Duration::from_millis(150)).await;
  495. assert_eq!(*rx.borrow(), stable, "unchanged tree must not re-publish");
  496. h.abort();
  497. fs::remove_dir_all(&dir).ok();
  498. }