- 2001
assert_eq!(decision.unwrap().recipe_id, "coding.diff_inspector"); - 2002
})); - 2003
- 2004
// Empty signals returns answer.basic - 2005
scenarios.push(tc("empty_signals_default", || { - 2006
let catalog = built_in_recipes(); - 2007
let decision = catalog.choose(&[], "terminal"); - 2008
assert_eq!(decision.unwrap().recipe_id, "answer.basic"); - 2009
})); - 2010
- 2011
run("recipe_selection", scenarios); - 2012
} - 2013
- 2014
// ════════════════════════════════════════════════════════════════════ - 2015
// 11. OUTBOX LIFECYCLE - 2016
// ════════════════════════════════════════════════════════════════════ - 2017
- 2018
#[test] - 2019
fn audit_outbox() { - 2020
let mut scenarios: Vec<Scenario> = vec![]; - 2021
- 2022
let make_root = || { - 2023
std::env::temp_dir().join(format!( - 2024
"vak-outbox-{}-{}", - 2025
std::process::id(), - 2026
std::time::SystemTime::now() - 2027
.duration_since(std::time::UNIX_EPOCH) - 2028
.unwrap() - 2029
.as_nanos() - 2030
)) - 2031
}; - 2032
- 2033
macro_rules! job { - 2034
() => { - 2035
DeliveryJob { - 2036
job_id: "job-1".into(), - 2037
target: "test:surface".into(), - 2038
kind: DeliveryKind::Assistant, - 2039
content: DeliveryContent::Answer(answer_draft("# Title\n\nContent.")), - 2040
profile: plain_profile("test"), - 2041
skill_registry: None, - 2042
} - 2043
}; - 2044
} - 2045
- 2046
scenarios.push(tc("enqueue_creates_pending", move || { - 2047
let root = make_root(); - 2048
let job = job!(); - 2049
let outbox = Outbox::new(&root); - 2050
let record = outbox.enqueue(job).expect("enqueue"); - 2051
assert_eq!(record.state, OutboxState::Pending); - 2052
assert_eq!(record.attempts, 0); - 2053
assert!(record.created_at_ms > 0); - 2054
assert_eq!(record.job.job_id, "job-1"); - 2055
assert!(record.packet.is_none()); - 2056
assert!(record.last_error.is_none()); - 2057
let _ = std::fs::remove_dir_all(&root); - 2058
})); - 2059
- 2060
scenarios.push(tc("pending_returns_enqueued", move || { - 2061
let root = make_root(); - 2062
let job = job!(); - 2063
let outbox = Outbox::new(&root); - 2064
let _ = outbox.enqueue(job).expect("enqueue"); - 2065
let records = outbox.pending().expect("pending"); - 2066
assert!(records.iter().any(|r| r.job.job_id == "job-1")); - 2067
let _ = std::fs::remove_dir_all(&root); - 2068
})); - 2069
- 2070
scenarios.push(tc("get_returns_record", move || { - 2071
let root = make_root(); - 2072
let job = job!(); - 2073
let outbox = Outbox::new(&root); - 2074
let _ = outbox.enqueue(job).expect("enqueue"); - 2075
let record = outbox.get("job-1").expect("get"); - 2076
assert_eq!(record.job.job_id, "job-1"); - 2077
let _ = std::fs::remove_dir_all(&root); - 2078
})); - 2079
- 2080
scenarios.push(tc("get_missing_fails", move || { - 2081
let root = make_root(); - 2082
let outbox = Outbox::new(&root); - 2083
assert!(outbox.get("nonexistent").is_err()); - 2084
let _ = std::fs::remove_dir_all(&root); - 2085
})); - 2086
- 2087
scenarios.push(tc("mark_delivered", move || { - 2088
let root = make_root(); - 2089
let job = job!(); - 2090
let outbox = Outbox::new(&root); - 2091
let _ = outbox.enqueue(job.clone()).expect("enqueue"); - 2092
let packet = render(&job).expect("render"); - 2093
let record = outbox.mark_delivered("job-1", packet).expect("deliver"); - 2094
assert_eq!(record.state, OutboxState::Delivered); - 2095
assert_eq!(record.attempts, 1); - 2096
assert!(record.packet.is_some()); - 2097
assert!(record.last_error.is_none()); - 2098
let _ = std::fs::remove_dir_all(&root); - 2099
})); - 2100
- 2101
scenarios.push(tc("pending_empty_after_delivery", move || { - 2102
let root = make_root(); - 2103
let job = job!(); - 2104
let outbox = Outbox::new(&root); - 2105
let _ = outbox.enqueue(job.clone()).expect("enqueue"); - 2106
let packet = render(&job).expect("render"); - 2107
let _ = outbox.mark_delivered("job-1", packet).expect("deliver"); - 2108
assert!(outbox.pending().expect("pending").is_empty()); - 2109
let _ = std::fs::remove_dir_all(&root); - 2110
})); - 2111
- 2112
scenarios.push(tc("mark_failed_increments", move || { - 2113
let root = make_root(); - 2114
let job = job!(); - 2115
let outbox = Outbox::new(&root); - 2116
let _ = outbox.enqueue(job).expect("enqueue"); - 2117
let _ = outbox.mark_failed("job-1", "first error").expect("fail"); - 2118
let record = outbox.mark_failed("job-1", "network error").expect("fail"); - 2119
assert_eq!(record.attempts, 2); - 2120
assert_eq!(record.last_error.as_deref(), Some("network error")); - 2121
let _ = std::fs::remove_dir_all(&root); - 2122
})); - 2123
- 2124
scenarios.push(tc("mark_dead_letter", move || { - 2125
let root = make_root(); - 2126
let job = job!(); - 2127
let outbox = Outbox::new(&root); - 2128
let _ = outbox.enqueue(job).expect("enqueue"); - 2129
let record = outbox - 2130
.mark_dead_letter("job-1", "permanent error") - 2131
.expect("dlq"); - 2132
assert_eq!(record.state, OutboxState::DeadLetter); - 2133
let _ = std::fs::remove_dir_all(&root); - 2134
})); - 2135
- 2136
scenarios.push(tc("list_returns_all", move || { - 2137
let root = make_root(); - 2138
let job = job!(); - 2139
let outbox = Outbox::new(&root); - 2140
let _ = outbox.enqueue(job).expect("enqueue"); - 2141
let records = outbox.list().expect("list"); - 2142
assert!(records.iter().any(|r| r.job.job_id == "job-1")); - 2143
let _ = std::fs::remove_dir_all(&root); - 2144
})); - 2145
- 2146
scenarios.push(tc("conflict_on_different_content_same_id", move || { - 2147
let root = make_root(); - 2148
let job = job!(); - 2149
let outbox = Outbox::new(&root); - 2150
let _ = outbox.enqueue(job.clone()).expect("enqueue"); - 2151
let mut conflicting = job.clone(); - 2152
conflicting.content = DeliveryContent::Answer(answer_draft("Different content.")); - 2153
let result = outbox.enqueue(conflicting); - 2154
assert!(matches!(result, Err(OutboxError::Conflict(_)))); - 2155
let _ = std::fs::remove_dir_all(&root); - 2156
})); - 2157
- 2158
scenarios.push(tc("idempotent_enqueue_same_job", move || { - 2159
let root = make_root(); - 2160
let job = job!(); - 2161
let outbox = Outbox::new(&root); - 2162
let _ = outbox.enqueue(job.clone()).expect("enqueue"); - 2163
let result = Outbox::new(&root).enqueue(job); - 2164
assert!(result.is_ok()); - 2165
let _ = std::fs::remove_dir_all(&root); - 2166
})); - 2167
- 2168
scenarios.push(tc("empty_root_pending", move || { - 2169
let root = make_root(); - 2170
let outbox = Outbox::new(&root); - 2171
assert!(outbox.pending().expect("pending").is_empty()); - 2172
assert!(outbox.list().expect("list").is_empty()); - 2173
let _ = std::fs::remove_dir_all(&root); - 2174
})); - 2175
- 2176
scenarios.push(tc("delivered_record_in_list", move || { - 2177
let root = make_root(); - 2178
let job = job!(); - 2179
let outbox = Outbox::new(&root); - 2180
let _ = outbox.enqueue(job.clone()).expect("enqueue"); - 2181
let packet = render(&job).expect("render"); - 2182
let _ = outbox.mark_delivered("job-1", packet).expect("deliver"); - 2183
let records = outbox.list().expect("list"); - 2184
assert!(records.iter().any(|r| r.state == OutboxState::Delivered)); - 2185
let _ = std::fs::remove_dir_all(&root); - 2186
})); - 2187
- 2188
scenarios.push(tc("dead_letter_in_list", move || { - 2189
let root = make_root(); - 2190
let job = job!(); - 2191
let outbox = Outbox::new(&root); - 2192
let _ = outbox.enqueue(job).expect("enqueue"); - 2193
let _ = outbox - 2194
.mark_dead_letter("job-1", "permanent error") - 2195
.expect("dlq"); - 2196
let records = outbox.list().expect("list"); - 2197
assert!(records.iter().any(|r| r.state == OutboxState::DeadLetter)); - 2198
let _ = std::fs::remove_dir_all(&root); - 2199
})); - 2200
- 2201
scenarios.push(tc("list_empty_on_fresh_root", move || { - 2202
let root = make_root(); - 2203
let outbox = Outbox::new(&root); - 2204
assert_eq!(outbox.list().expect("list").len(), 0); - 2205
let _ = std::fs::remove_dir_all(&root); - 2206
})); - 2207
- 2208
scenarios.push(tc("pending_only_returns_pending", move || { - 2209
let root = make_root(); - 2210
let job = job!(); - 2211
let outbox = Outbox::new(&root); - 2212
let _ = outbox.enqueue(job.clone()).expect("enqueue"); - 2213
let mut job2 = job.clone(); - 2214
job2.job_id = "job-2".into(); - 2215
job2.content = DeliveryContent::Answer(answer_draft("second content")); - 2216
let _ = outbox.enqueue(job2).expect("enqueue2"); - 2217
let packet = render(&job).expect("render"); - 2218
let _ = outbox.mark_delivered("job-2", packet).expect("deliver"); - 2219
let pending = outbox.pending().expect("pending"); - 2220
assert_eq!(pending.len(), 1); - 2221
assert_eq!(pending[0].job.job_id, "job-1"); - 2222
let _ = std::fs::remove_dir_all(&root); - 2223
})); - 2224
- 2225
run("outbox", scenarios); - 2226
} - 2227
- 2228
// ════════════════════════════════════════════════════════════════════ - 2229
// 12. WORKER SUBPROCESS PROTOCOL - 2230
// ════════════════════════════════════════════════════════════════════ - 2231
- 2232
fn make_req(job: &DeliveryJob) -> String { - 2233
let request = WorkerRequest { - 2234
protocol_version: WORKER_PROTOCOL_VERSION, - 2235
job: job.clone(), - 2236
}; - 2237
serde_json::to_string(&request).expect("serialize") - 2238
} - 2239
- 2240
#[test] - 2241
fn audit_worker_protocol() { - 2242
let mut scenarios: Vec<Scenario> = vec![]; - 2243
- 2244
scenarios.push(tc("process_line_valid_render", || { - 2245
let job = answer_job( - 2246
"# Title\n\nBody.", - 2247
Markup::Markdown, - 2248
None, - 2249
DeliveryKind::Assistant, - 2250
"test", - 2251
); - 2252
let response_str = process_line(&make_req(&job)); - 2253
let response: WorkerResponse = serde_json::from_str(&response_str).expect("deserialize"); - 2254
assert_eq!(response.protocol_version, WORKER_PROTOCOL_VERSION); - 2255
assert!(response.packet.is_some()); - 2256
assert!(response.error.is_none()); - 2257
assert_eq!(response.job_id.as_deref(), Some("test-job")); - 2258
})); - 2259
- 2260
scenarios.push(tc("process_line_bad_version", || { - 2261
let job = answer_job( - 2262
"hi", - 2263
Markup::Markdown, - 2264
None, - 2265
DeliveryKind::Assistant, - 2266
"test", - 2267
); - 2268
let request = serde_json::json!({ - 2269
"protocol_version": 999, - 2270
"job": serde_json::to_value(&job).unwrap() - 2271
}); - 2272
let response_str = process_line(&serde_json::to_string(&request).unwrap()); - 2273
let response: WorkerResponse = serde_json::from_str(&response_str).expect("deserialize"); - 2274
assert!(response.error.is_some()); - 2275
assert!(response.error.unwrap().contains("unsupported")); - 2276
})); - 2277
- 2278
scenarios.push(tc("process_line_invalid_json", || { - 2279
let response_str = process_line("not json"); - 2280
let response: WorkerResponse = serde_json::from_str(&response_str).expect("deserialize"); - 2281
assert!(response.error.is_some()); - 2282
assert!(response.error.unwrap().contains("invalid")); - 2283
})); - 2284
- 2285
scenarios.push(tc("process_line_system_kind_fails", || { - 2286
let job = DeliveryJob { - 2287
job_id: "sys".into(), - 2288
target: "t".into(), - 2289
kind: DeliveryKind::System, - 2290
content: DeliveryContent::Text { - 2291
markdown: "sys".into(), - 2292
}, - 2293
profile: plain_profile("test"), - 2294
skill_registry: None, - 2295
}; - 2296
let response_str = process_line(&make_req(&job)); - 2297
let response: WorkerResponse = serde_json::from_str(&response_str).expect("deserialize"); - 2298
assert!(response.error.is_some()); - 2299
assert!(response.packet.is_none()); - 2300
})); - 2301
- 2302
scenarios.push(tc("process_line_empty", || { - 2303
let response_str = process_line(""); - 2304
let response: WorkerResponse = serde_json::from_str(&response_str).expect("deserialize"); - 2305
assert!(response.error.is_some()); - 2306
})); - 2307
- 2308
for (i, kind_name) in ["approval", "progress", "toolresult"].iter().enumerate() { - 2309
let name = kind_name.to_string(); - 2310
scenarios.push(tc(&format!("process_line_{name}_{i}"), move || { - 2311
let job = match name.as_str() { - 2312
"approval" => approval_job(Markup::Markdown, "test"), - 2313
"progress" => progress_job(Markup::Markdown, "test"), - 2314
"toolresult" => tool_result_job(Markup::Markdown, "test", false), - 2315
_ => unreachable!(), - 2316
}; - 2317
let response_str = process_line(&make_req(&job)); - 2318
let response: WorkerResponse = - 2319
serde_json::from_str(&response_str).expect("deserialize"); - 2320
assert!(response.packet.is_some()); - 2321
})); - 2322
} - 2323
- 2324
scenarios.push(tc("process_line_text_job", || { - 2325
let job = text_job("Some **text**", Markup::Markdown, "test"); - 2326
let response_str = process_line(&make_req(&job)); - 2327
let response: WorkerResponse = serde_json::from_str(&response_str).expect("deserialize"); - 2328
assert!(response.packet.is_some()); - 2329
})); - 2330
- 2331
scenarios.push(tc("process_line_response_roundtrip", || { - 2332
let job = answer_job( - 2333
"# T\n\nBody", - 2334
Markup::Plain, - 2335
None, - 2336
DeliveryKind::Assistant, - 2337
"test", - 2338
); - 2339
let response_str = process_line(&make_req(&job)); - 2340
let response: WorkerResponse = serde_json::from_str(&response_str).expect("deserialize"); - 2341
let packet = response.packet.expect("packet"); - 2342
assert_eq!(packet.job_id, "test-job"); - 2343
match &job.content { - 2344
DeliveryContent::Answer(answer) => { - 2345
assert_eq!(packet.fallback_markdown, answer.source_markdown); - 2346
} - 2347
_ => panic!("expected answer"), - 2348
} - 2349
})); - 2350
- 2351
scenarios.push(tc("process_line_json_markup", || { - 2352
let job = answer_job( - 2353
"# T\n\nBody", - 2354
Markup::Json, - 2355
None, - 2356
DeliveryKind::Assistant, - 2357
"test", - 2358
); - 2359
let response_str = process_line(&make_req(&job)); - 2360
let response: WorkerResponse = serde_json::from_str(&response_str).expect("deserialize"); - 2361
let packet = response.packet.expect("packet"); - 2362
assert!(matches!(packet.payload, DeliveryPayload::Structured(_))); - 2363
})); - 2364
- 2365
scenarios.push(tc("process_line_with_template", || { - 2366
let mut profile = plain_profile("test"); - 2367
profile.template = Some(TemplateSpec { - 2368
id: "t".into(), - 2369
revision: 1, - 2370
origin: TemplateOrigin::User, - 2371
activation: TemplateActivation::Active, - 2372
nodes: vec![ - 2373
TemplateNode::Literal { text: "Hi ".into() }, - 2374
TemplateNode::Slot { - 2375
slot: TemplateSlot::Body, - 2376
}, - 2377
], - 2378
}); - 2379
let job = DeliveryJob { - 2380
job_id: "tmpl".into(), - 2381
target: "t".into(), - 2382
kind: DeliveryKind::Assistant, - 2383
content: DeliveryContent::Answer(answer_draft("source")), - 2384
profile, - 2385
skill_registry: None, - 2386
}; - 2387
let response_str = process_line(&make_req(&job)); - 2388
let response: WorkerResponse = serde_json::from_str(&response_str).expect("deserialize"); - 2389
assert!(response.packet.is_some()); - 2390
})); - 2391
- 2392
run("worker_protocol", scenarios); - 2393
} - 2394
- 2395
// ════════════════════════════════════════════════════════════════════ - 2396
// 13. MARKUP CONVERTERS - 2397
// ════════════════════════════════════════════════════════════════════ - 2398
- 2399
#[test] - 2400
fn audit_markup_converters() { - 2401
let mut scenarios: Vec<Scenario> = vec![]; - 2402
- 2403
let converter_cases: &[(&str, &str)] = &[ - 2404
("plain text", "Hello world"), - 2405
("bold", "**bold**"), - 2406
("italic", "*italic*"), - 2407
("strikethrough", "~~struck~~"), - 2408
("inline code", "use `code`"), - 2409
("link", "[label](https://example.com)"), - 2410
("heading", "# Title"), - 2411
("unordered list", "- a\n- b"), - 2412
("ordered list", "1. first\n2. second"), - 2413
("blockquote", "> quote"), - 2414
("code fence", "```rust\nlet x = 1;\n```"), - 2415
("table", "| A | B |\n|---|---|\n| 1 | 2 |"), - 2416
("horizontal rule", "---"), - 2417
( - 2418
"mixed", - 2419
"# T\n\n**b** *i* ~~s~~ `c`\n\n- a\n- b\n\n[link](https://x.com)", - 2420
), - 2421
("empty", ""), - 2422
("emoji", "🎉🚀✅"), - 2423
("unicode", "世界の人々"), - 2424
("special chars", "< > & \" '"), - 2425
("long paragraph", &"word ".repeat(200)), - 2426
("nested quote", "> > deep"), - 2427
("task list", "- [ ] todo\n- [x] done"), - 2428
("multiple headings", "# A\n## B\n### C"), - 2429
("link with title", "[text](https://example.com \"Title\")"), - 2430
("image with title", ""), - 2431
("raw html", "<b>bold</b>"), - 2432
("escaped chars", r"\*not bold\*"), - 2433
("code with backticks", "```\n```\n```"), - 2434
("empty table", "| |\n|---|\n| |"), - 2435
("single row table", "| A |\n|---|\n| 1 |"), - 2436
]; - 2437
- 2438
for (label, md) in converter_cases { - 2439
let l = label.to_string(); - 2440
- 2441
let m_tg = md.to_string(); - 2442
scenarios.push(tc(&format!("telegram_{l}"), move || { - 2443
let result = telegram::markdown_to_html(&m_tg); - 2444
if m_tg == "| |\n|---|\n| |" { - 2445
assert!(result.is_empty()); - 2446
} else { - 2447
assert!(!result.is_empty() || m_tg.is_empty()); - 2448
} - 2449
})); - 2450
- 2451
let m_sl = md.to_string(); - 2452
scenarios.push(tc(&format!("slack_{l}"), move || { - 2453
let result = slack::markdown_to_mrkdwn(&m_sl); - 2454
if m_sl == "| |\n|---|\n| |" { - 2455
assert!(result.is_empty()); - 2456
} else { - 2457
assert!(!result.is_empty() || m_sl.is_empty()); - 2458
} - 2459
})); - 2460
- 2461
let m_ds = md.to_string(); - 2462
scenarios.push(tc(&format!("discord_{l}"), move || { - 2463
let result = discord::markdown_to_discord(&m_ds); - 2464
if m_ds == "| |\n|---|\n| |" { - 2465
assert!(result.is_empty()); - 2466
} else { - 2467
assert!(!result.is_empty() || m_ds.is_empty()); - 2468
} - 2469
})); - 2470
} - 2471
- 2472
// Telegram-specific assertions - 2473
let tg_cases: &[(&str, &str, &str)] = &[ - 2474
("bold_becomes_strong", "**bold**", "<b>"), - 2475
("heading_becomes_bold", "# Title", "<b>"), - 2476
("code_becomes_pre", "```rust\nfn main() {}\n```", "<pre>"), - 2477
("link_becomes_a", "[text](https://example.com)", "<a href"), - 2478
( - 2479
"table_becomes_pre", - 2480
"| A | B |\n|---|---|\n| 1 | 2 |", - 2481
"<pre>", - 2482
), - 2483
("quote_becomes_blockquote", "> quoted", "<blockquote>"), - 2484
("hr_becomes_separator", "---", "─"), - 2485
("strike_becomes_s", "~~struck~~", "<s>"), - 2486
("inline_code_becomes_code", "use `code`", "<code>"), - 2487
]; - 2488
for (label, input, expected) in tg_cases { - 2489
let l = label.to_string(); - 2490
let inp = input.to_string(); - 2491
let exp = expected.to_string(); - 2492
scenarios.push(tc(&format!("tg_check_{l}"), move || { - 2493
let html = telegram::markdown_to_html(&inp); - 2494
assert!( - 2495
html.contains(&exp), - 2496
"expected {exp:?} in {html:?} for input {inp:?}" - 2497
); - 2498
})); - 2499
} - 2500
- 2501
scenarios.push(tc("tg_escape_html_script", || { - 2502
let html = telegram::markdown_to_html("<script>alert(1)</script>"); - 2503
assert!(!html.contains("<script>")); - 2504
})); - 2505
- 2506
scenarios.push(tc("tg_strip_html_removes_tags", || { - 2507
let stripped = telegram::strip_html("<b>hello</b> <i>world</i>"); - 2508
assert!(!stripped.contains("<b>")); - 2509
assert!(stripped.contains("hello")); - 2510
})); - 2511
- 2512
scenarios.push(tc("tg_strip_html_empty", || { - 2513
assert_eq!(telegram::strip_html(""), ""); - 2514
})); - 2515
- 2516
scenarios.push(tc("tg_split_html_chunks_no_limit", || { - 2517
let chunks = telegram::split_html_chunks("<p>Hello</p>", None); - 2518
assert_eq!(chunks.len(), 1); - 2519
})); - 2520
- 2521
scenarios.push(tc("tg_split_html_chunks_small_limit", || { - 2522
let long = "<p>".to_string() + &"word ".repeat(20) + "</p>"; - 2523
let chunks = telegram::split_html_chunks(&long, Some(50)); - 2524
assert!(chunks.len() > 1); - 2525
})); - 2526
- 2527
scenarios.push(tc("tg_split_html_chunks_below_32", || { - 2528
let chunks = telegram::split_html_chunks("hello", Some(10)); - 2529
assert!(chunks.len() >= 1); - 2530
})); - 2531
- 2532
// Task list handling - 2533
scenarios.push(tc("tg_task_list_unchecked", || { - 2534
let out = telegram::markdown_to_html("- [ ] todo"); - 2535
assert!(out.contains("☐")); - 2536
assert!(out.contains("todo")); - 2537
})); - 2538
- 2539
scenarios.push(tc("tg_task_list_checked", || { - 2540
let out = telegram::markdown_to_html("- [x] done"); - 2541
assert!(out.contains("☑")); - 2542
assert!(out.contains("done")); - 2543
})); - 2544
- 2545
scenarios.push(tc("tg_inline_spoiler", || { - 2546
let out = telegram::markdown_to_html("here ||spoiler|| text"); - 2547
assert!(out.contains("<tg-spoiler>")); - 2548
assert!(out.contains("spoiler")); - 2549
})); - 2550
- 2551
scenarios.push(tc("tg_line_spoiler", || { - 2552
let out = telegram::markdown_to_html("||hidden content||"); - 2553
assert!(out.contains("<tg-spoiler>")); - 2554
assert!(out.contains("hidden")); - 2555
})); - 2556
- 2557
// Slack-specific assertions - 2558
let slack_cases: &[(&str, &str, bool)] = &[ - 2559
("bold_single_star", "**bold**", true), - 2560
("italic_single_star", "*italic*", true), - 2561
]; - 2562
for (label, input, should_contain) in slack_cases { - 2563
let l = label.to_string(); - 2564
let inp = input.to_string(); - 2565
let sc = *should_contain; - 2566
scenarios.push(tc(&format!("slack_check_{l}"), move || { - 2567
let out = slack::markdown_to_mrkdwn(&inp); - 2568
if sc { - 2569
assert!(!out.is_empty()); - 2570
} - 2571
})); - 2572
} - 2573
- 2574
scenarios.push(tc("slack_table_preserved", || { - 2575
let out = slack::markdown_to_mrkdwn("| A | B |\n|---|---|\n| 1 | 2 |"); - 2576
assert!(out.contains("A") && out.contains("1")); - 2577
})); - 2578
- 2579
scenarios.push(tc("slack_code_fence_preserved", || { - 2580
let out = slack::markdown_to_mrkdwn("```rust\nfn main() {}\n```"); - 2581
assert!(out.contains("```")); - 2582
})); - 2583
- 2584
scenarios.push(tc("slack_link_not_link_syntax", || { - 2585
let out = slack::markdown_to_mrkdwn("[text](https://example.com)"); - 2586
assert!(!out.contains("](https://example.com)")); - 2587
})); - 2588
- 2589
scenarios.push(tc("slack_task_list_unchecked", || { - 2590
let out = slack::markdown_to_mrkdwn("- [ ] todo"); - 2591
assert!(out.contains("☐")); - 2592
assert!(out.contains("todo")); - 2593
})); - 2594
- 2595
scenarios.push(tc("slack_task_list_checked", || { - 2596
let out = slack::markdown_to_mrkdwn("- [x] done"); - 2597
assert!(out.contains("☑")); - 2598
assert!(out.contains("done")); - 2599
})); - 2600
- 2601
// Discord-specific assertions - 2602
scenarios.push(tc("discord_link_rewritten_exactly", || { - 2603
let out = discord::markdown_to_discord("[text](https://example.com)"); - 2604
assert_eq!(out, "text (<https://example.com>)"); - 2605
})); - 2606
- 2607
let discord_cases: &[(&str, &str, &str)] = &[ - 2608
("bold", "**bold**", "**bold**"), - 2609
("italic", "*italic*", "*italic*"), - 2610
("strike", "~~strikethrough~~", "~~strikethrough~~"), - 2611
("inline_code", "`code`", "`code`"), - 2612
("heading", "# Title", "<b>"), - 2613
("blockquote", "> quote", "> quote"), - 2614
("list", "- a\n- b", "- a"), - 2615
("code_fence", "```rust\nfn() {}\n```", "```rust"), - 2616
("spoiler", "||spoiler||", "||spoiler||"), - 2617
("mixed", "**b** *i* ~~s~~", "**b**"), - 2618
]; - 2619
for (label, input, expected) in discord_cases { - 2620
let l = label.to_string(); - 2621
let inp = input.to_string(); - 2622
let exp = expected.to_string(); - 2623
scenarios.push(tc(&format!("discord_check_{l}"), move || { - 2624
let out = discord::markdown_to_discord(&inp); - 2625
assert!( - 2626
out.contains(&exp), - 2627
"expected {exp:?} in {out:?} for {inp:?}" - 2628
); - 2629
})); - 2630
} - 2631
- 2632
scenarios.push(tc("discord_task_list_unchecked", || { - 2633
let out = discord::markdown_to_discord("- [ ] todo"); - 2634
assert!(out.contains("☐")); - 2635
assert!(out.contains("todo")); - 2636
})); - 2637
- 2638
scenarios.push(tc("discord_task_list_checked", || { - 2639
let out = discord::markdown_to_discord("- [x] done"); - 2640
assert!(out.contains("☑")); - 2641
assert!(out.contains("done")); - 2642
})); - 2643
- 2644
scenarios.push(tc("discord_heading_bold", || { - 2645
let out = discord::markdown_to_discord("# Title"); - 2646
assert!(out.contains("<b>")); - 2647
assert!(out.contains("Title")); - 2648
})); - 2649
- 2650
scenarios.push(tc("discord_table_to_code_block", || { - 2651
let out = discord::markdown_to_discord("| A | B |\n|---|---|\n| 1 | 2 |"); - 2652
assert!(out.contains("```")); - 2653
assert!(!out.contains("| A | B |")); - 2654
})); - 2655
- 2656
scenarios.push(tc("discord_horizontal_rule", || { - 2657
let out = discord::markdown_to_discord("---"); - 2658
assert!(out.contains("─")); - 2659
})); - 2660
- 2661
scenarios.push(tc("discord_empty_input", || { - 2662
let out = discord::markdown_to_discord(""); - 2663
assert!(out.is_empty() || out.trim().is_empty()); - 2664
})); - 2665
- 2666
scenarios.push(tc("discord_unclosed_link", || { - 2667
let out = discord::markdown_to_discord("[text](not_a_url)"); - 2668
assert!(out.contains("text")); - 2669
})); - 2670
- 2671
run("markup_converters", scenarios); - 2672
} - 2673
- 2674
// ════════════════════════════════════════════════════════════════════ - 2675
// 14. STRUCTURED OUTPUTS & ADAPTERS - 2676
// ════════════════════════════════════════════════════════════════════ - 2677
- 2678
#[test] - 2679
fn audit_structured_outputs() { - 2680
let mut scenarios: Vec<Scenario> = vec![]; - 2681
- 2682
let valid_fragments: &[&str] = &[ - 2683
r#"{"semantic_type":"metric","payload":{"label":"X","value":42}}"#, - 2684
r#"{"semantic_type":"link.preview","payload":{"url":"https://x.com","title":"T"}}"#, - 2685
r#"{"semantic_type":"detail","payload":{"title":"T"}}"#, - 2686
r#"{"semantic_type":"collection","payload":{"items":["a","b"]}}"#, - 2687
r#"{"semantic_type":"comparison","payload":{"left":"a","right":"b"}}"#, - 2688
]; - 2689
- 2690
for (i, frag) in valid_fragments.iter().enumerate() { - 2691
let f = frag.to_string(); - 2692
scenarios.push(tc(&format!("structured_bare_{i}"), move || { - 2693
let outputs = structured_outputs_from_text(&f); - 2694
assert_eq!(outputs.len(), 1, "fragment {i}: {outputs:?}"); - 2695
})); - 2696
} - 2697
- 2698
let invalid_fragments: &[&str] = &[ - 2699
r#"{"semantic_type":"unknown_type","payload":{}}"#, - 2700
r#"{"semantic_type":"metric","payload":{"bad":"shape"}}"#, - 2701
r#"not json"#, - 2702
r#""{"missing": "quotes"}"#, - 2703
]; - 2704
for (i, frag) in invalid_fragments.iter().enumerate() { - 2705
let f = frag.to_string(); - 2706
scenarios.push(tc(&format!("structured_invalid_{i}"), move || { - 2707
let outputs = structured_outputs_from_text(&f); - 2708
assert!(outputs.is_empty(), "expected empty for {f}"); - 2709
})); - 2710
} - 2711
- 2712
let link_texts: &[&str] = &[ - 2713
"Check https://example.com for details", - 2714
"Visit http://test.org\nAnd https://another.com", - 2715
"No links here", - 2716
"Multiple https://a.com https://b.com https://c.com", - 2717
"", - 2718
]; - 2719
for (i, text) in link_texts.iter().enumerate() { - 2720
let t = text.to_string(); - 2721
scenarios.push(tc(&format!("link_preview_{i}"), move || { - 2722
let previews = link_previews_from_text(&t); - 2723
assert!(previews.len() <= 12); - 2724
})); - 2725
} - 2726
- 2727
scenarios.push(tc("link_preview_dedup", || { - 2728
let previews = link_previews_from_text("https://a.com https://a.com"); - 2729
assert_eq!(previews.len(), 1); - 2730
})); - 2731
- 2732
scenarios.push(tc("link_preview_max_12", || { - 2733
let urls: Vec<String> = (0..15).map(|i| format!("https://example{i}.com")).collect(); - 2734
let text = urls.join(" "); - 2735
assert_eq!(link_previews_from_text(&text).len(), 12); - 2736
})); - 2737
- 2738
// project_structured_fences - 2739
let fence_cases: &[&str] = &[ - 2740
"# Title\n\n```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"X\",\"value\":42}}\n```\n\nBody.", - 2741
"```vak\n{\"semantic_type\":\"collection\",\"payload\":{\"items\":[\"a\",\"b\"]}}\n```", - 2742
"No fences here", - 2743
"```rust\nfn main() {}\n```\n\nText.", - 2744
"```vak\n{bad json}\n```", - 2745
"```vak\n{\"semantic_type\":\"unknown_type\",\"payload\":{}}\n```", - 2746
"```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"A\",\"value\":1}}\n```\n```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"B\",\"value\":2}}\n```", - 2747
]; - 2748
for (i, source) in fence_cases.iter().enumerate() { - 2749
let s = source.to_string(); - 2750
scenarios.push(tc(&format!("project_fences_{i}"), move || { - 2751
let projected = project_structured_fences(&s); - 2752
assert!(!projected.is_empty() || s.is_empty()); - 2753
})); - 2754
} - 2755
- 2756
// Adapters - 2757
scenarios.push(tc("adapter_self_declared", || { - 2758
let adapters = built_in_adapters(); - 2759
let outputs = structured_outputs_from_tool_result( - 2760
r#"{"semantic_type":"metric","payload":{"label":"X","value":42}}"#, - 2761
"desktop", - 2762
&adapters, - 2763
); - 2764
assert_eq!(outputs.len(), 1); - 2765
assert_eq!(outputs[0].semantic_type, "metric"); - 2766
})); - 2767
- 2768
scenarios.push(tc("adapter_unrecognized_empty", || { - 2769
let adapters = built_in_adapters(); - 2770
let outputs = structured_outputs_from_tool_result( - 2771
r#"{"random":"data","count":42}"#, - 2772
"desktop", - 2773
&adapters, - 2774
); - 2775
assert!(outputs.is_empty()); - 2776
})); - 2777
- 2778
scenarios.push(tc("adapter_invalid_json_empty", || { - 2779
let adapters = built_in_adapters(); - 2780
let outputs = structured_outputs_from_tool_result("not json at all", "desktop", &adapters); - 2781
assert!(outputs.is_empty()); - 2782
})); - 2783
- 2784
run("structured_outputs", scenarios); - 2785
} - 2786
- 2787
// ════════════════════════════════════════════════════════════════════ - 2788
// 15. RENDER ERROR HANDLING - 2789
// ════════════════════════════════════════════════════════════════════ - 2790
- 2791
#[test] - 2792
fn audit_render_errors() { - 2793
let mut scenarios: Vec<Scenario> = vec![]; - 2794
- 2795
for kind in [ - 2796
DeliveryKind::System, - 2797
DeliveryKind::Developer, - 2798
DeliveryKind::Internal, - 2799
] { - 2800
scenarios.push(tc(&format!("control_rejected_{kind:?}"), move || { - 2801
let job = DeliveryJob { - 2802
job_id: "sys".into(), - 2803
target: "t".into(), - 2804
kind: DeliveryKind::System, - 2805
content: DeliveryContent::Text { - 2806
markdown: "x".into(), - 2807
}, - 2808
profile: plain_profile("test"), - 2809
skill_registry: None, - 2810
}; - 2811
assert!(render(&job).is_err()); - 2812
})); - 2813
} - 2814
- 2815
scenarios.push(tc("mismatched_approval_for_alert", || { - 2816
let job = DeliveryJob { - 2817
job_id: "m".into(), - 2818
target: "t".into(), - 2819
kind: DeliveryKind::Alert, - 2820
content: DeliveryContent::Approval(ApprovalPayload { - 2821
request_id: "r".into(), - 2822
title: "t".into(), - 2823
detail: "d".into(), - 2824
expires_at: None, - 2825
actions: vec![], - 2826
}), - 2827
profile: plain_profile("test"), - 2828
skill_registry: None, - 2829
}; - 2830
assert!(render(&job).is_err()); - 2831
})); - 2832
- 2833
scenarios.push(tc("wrong_schema_version", || { - 2834
let mut answer = answer_draft("content"); - 2835
answer.schema_version = 999; - 2836
let job = DeliveryJob { - 2837
job_id: "bad".into(), - 2838
target: "t".into(), - 2839
kind: DeliveryKind::Assistant, - 2840
content: DeliveryContent::Answer(answer), - 2841
profile: plain_profile("test"), - 2842
skill_registry: None, - 2843
}; - 2844
assert!(render(&job).is_err()); - 2845
})); - 2846
- 2847
scenarios.push(tc("template_on_non_template_kind", || { - 2848
let mut profile = plain_profile("test"); - 2849
profile.template = Some(TemplateSpec { - 2850
id: "t".into(), - 2851
revision: 1, - 2852
origin: TemplateOrigin::User, - 2853
activation: TemplateActivation::Active, - 2854
nodes: vec![TemplateNode::Literal { text: "x".into() }], - 2855
}); - 2856
let job = DeliveryJob { - 2857
job_id: "tmpl".into(), - 2858
target: "t".into(), - 2859
kind: DeliveryKind::Approval, - 2860
content: DeliveryContent::Approval(ApprovalPayload { - 2861
request_id: "r".into(), - 2862
title: "t".into(), - 2863
detail: "d".into(), - 2864
expires_at: None, - 2865
actions: vec![], - 2866
}), - 2867
profile, - 2868
skill_registry: None, - 2869
}; - 2870
assert!(render(&job).is_err()); - 2871
})); - 2872
- 2873
scenarios.push(tc("proposed_template_rejected", || { - 2874
let mut profile = plain_profile("test"); - 2875
profile.template = Some(TemplateSpec { - 2876
id: "t".into(), - 2877
revision: 1, - 2878
origin: TemplateOrigin::AgentProposal, - 2879
activation: TemplateActivation::Proposed, - 2880
nodes: vec![TemplateNode::Literal { text: "x".into() }], - 2881
}); - 2882
let job = DeliveryJob { - 2883
job_id: "pt".into(), - 2884
target: "t".into(), - 2885
kind: DeliveryKind::Assistant, - 2886
content: DeliveryContent::Answer(answer_draft("source")), - 2887
profile, - 2888
skill_registry: None, - 2889
}; - 2890
assert!(render(&job).is_err()); - 2891
})); - 2892
- 2893
scenarios.push(tc("empty_surface_rejected", || { - 2894
let job = answer_job("source", Markup::Plain, None, DeliveryKind::Assistant, ""); - 2895
assert!(render(&job).is_err()); - 2896
})); - 2897
- 2898
scenarios.push(tc("max_chars_zero_rejected", || { - 2899
let job = answer_job( - 2900
"source", - 2901
Markup::Plain, - 2902
Some(0), - 2903
DeliveryKind::Assistant, - 2904
"test", - 2905
); - 2906
assert!(render(&job).is_err()); - 2907
})); - 2908
- 2909
run("render_errors", scenarios); - 2910
} - 2911
- 2912
// ════════════════════════════════════════════════════════════════════ - 2913
// 16. PACKET STRUCTURE & INVARIANTS - 2914
// ════════════════════════════════════════════════════════════════════ - 2915
- 2916
#[test] - 2917
fn audit_packet_structure() { - 2918
let mut scenarios: Vec<Scenario> = vec![]; - 2919
- 2920
let source = "# Title\n\nBody text.\n\n```rust\nlet x = 1;\n```"; - 2921
- 2922
for (i, markup) in [ - 2923
Markup::Plain, - 2924
Markup::Markdown, - 2925
Markup::TelegramHtml, - 2926
Markup::SlackMrkdwn, - 2927
Markup::DiscordMarkdown, - 2928
Markup::Json, - 2929
] - 2930
.iter() - 2931
.enumerate() - 2932
{ - 2933
let m = *markup; - 2934
scenarios.push(tc(&format!("packet_schema_{i}"), move || { - 2935
let job = answer_job(source, m, Some(1000), DeliveryKind::Assistant, "test"); - 2936
let packet = render(&job).expect("render"); - 2937
assert_eq!(packet.schema_version, DELIVERY_SCHEMA_VERSION); - 2938
})); - 2939
} - 2940
- 2941
scenarios.push(tc("fallback_exact_source", move || { - 2942
let job = answer_job( - 2943
source, - 2944
Markup::TelegramHtml, - 2945
None, - 2946
DeliveryKind::Assistant, - 2947
"test", - 2948
); - 2949
let packet = render(&job).expect("render"); - 2950
assert_eq!(packet.fallback_markdown, source); - 2951
})); - 2952
- 2953
let chunk_sources: &[&str] = &["Hello", "# Title\n\nBody", "```\ncode\n```", ""]; - 2954
for (i, src) in chunk_sources.iter().enumerate() { - 2955
let s = src.to_string(); - 2956
scenarios.push(tc(&format!("chunks_nonempty_{i}"), move || { - 2957
let job = answer_job( - 2958
&s, - 2959
Markup::Markdown, - 2960
Some(10), - 2961
DeliveryKind::Assistant, - 2962
"test", - 2963
); - 2964
let packet = render(&job).expect("render"); - 2965
for chunk in &packet.chunks { - 2966
assert!(!chunk.is_empty() || s.is_empty()); - 2967
} - 2968
})); - 2969
} - 2970
- 2971
scenarios.push(tc("coverage_matches_blocks", || { - 2972
let src = "# A\n\nB\n\n- 1\n- 2\n\n```\ncode\n```"; - 2973
let answer = answer_draft(src); - 2974
let expected = answer.blocks.len(); - 2975
let job = answer_job(src, Markup::Plain, None, DeliveryKind::Assistant, "test"); - 2976
let packet = render(&job).expect("render"); - 2977
assert_eq!(packet.coverage.len(), expected); - 2978
})); - 2979
- 2980
scenarios.push(tc("actions_diagnostic_no_support", || { - 2981
let job = DeliveryJob { - 2982
job_id: "action-ns".into(), - 2983
target: "test:one".into(), - 2984
kind: DeliveryKind::Approval, - 2985
content: DeliveryContent::Approval(ApprovalPayload { - 2986
request_id: "r".into(), - 2987
title: "t".into(), - 2988
detail: "d".into(), - 2989
expires_at: None, - 2990
actions: vec![DeliveryAction { - 2991
id: "approve".into(), - 2992
label: "Yes".into(), - 2993
verb: "approve".into(), - 2994
data: BTreeMap::new(), - 2995
}], - 2996
}), - 2997
profile: DeliveryProfile { - 2998
surface: "test".into(), - 2999
markup: Markup::Plain, - 3000
max_chars: None,
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.