1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! Regression tests for scatter's handling of single (non-array) JSON `.data`.
//!
//! Bug: `extract_items` ignored non-array JSON structured data and fell through
//! to newline-splitting the pretty-printed text, producing N bogus items instead
//! of 1. Example: `echo '{"k":1}' | jq . | scatter` produced 3 items (the 3
//! lines of pretty-printed JSON) instead of 1 (the object itself).
//!
//! Standalone scatter is validation-gated (requires a `gather`), so all tests
//! use `KernelConfig::repl().with_skip_validation(true)` to reach the scatter
//! path directly via the kernel dispatch chain (not via `Scatter::execute`
//! directly — per the no-direct-builtin-call convention).
#![cfg(feature = "localfs")]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use kaish_kernel::{Kernel, KernelConfig};
fn kernel() -> Kernel {
Kernel::new(KernelConfig::repl().with_skip_validation(true)).expect("kernel")
}
// ---------------------------------------------------------------------------
// Single JSON object must produce exactly 1 item (the core regression case)
// ---------------------------------------------------------------------------
/// `echo '{"k":1}' | jq . | scatter` — a single JSON object is almost always
/// the unselected envelope around the array the caller meant, so it's a LOUD
/// error with a select-the-array hint (GH #73; supersedes the earlier
/// one-item behavior). The original regression this file guards — a
/// pretty-printed object newline-splitting into N bogus items — stays
/// impossible: the error fires before any text splitting.
#[tokio::test]
async fn single_json_object_is_a_loud_error_with_hint() {
let kernel = kernel();
let r = kernel
.execute(r#"echo '{"k":1}' | jq . | scatter"#)
.await
.expect("execute");
assert_ne!(r.code, 0, "object input must be loud: {:?}", r.text_out());
assert!(
r.err.contains("single object"),
"should name the problem, got: {:?}",
r.err
);
}
// ---------------------------------------------------------------------------
// Single JSON scalar (number) must produce exactly 1 item
// ---------------------------------------------------------------------------
/// `echo '42' | jq . | scatter` — jq emits a single JSON number.
#[tokio::test]
async fn single_json_number_is_one_item() {
let kernel = kernel();
let r = kernel
.execute(r#"echo '42' | jq . | scatter"#)
.await
.expect("execute");
assert_eq!(r.code, 0, "should succeed; err={:?}", r.err);
assert!(
r.text_out().contains("1 items"),
"expected 1 item for a single JSON number, got: {:?}",
r.text_out()
);
}
// ---------------------------------------------------------------------------
// Single JSON string must produce exactly 1 item
// ---------------------------------------------------------------------------
/// `echo '"hello"' | jq . | scatter` — jq emits a single JSON string.
#[tokio::test]
async fn single_json_string_is_one_item() {
let kernel = kernel();
let r = kernel
.execute(r#"echo '"hello"' | jq . | scatter"#)
.await
.expect("execute");
assert_eq!(r.code, 0, "should succeed; err={:?}", r.err);
assert!(
r.text_out().contains("1 items"),
"expected 1 item for a single JSON string, got: {:?}",
r.text_out()
);
}
// ---------------------------------------------------------------------------
// JSON array must STILL fan out per element (no regression)
// ---------------------------------------------------------------------------
/// `seq 1 3 | scatter` — seq emits a JSON array; scatter must still produce
/// 3 items (one per element). This is the correct behavior that must not
/// regress.
#[tokio::test]
async fn json_array_fans_out_per_element() {
let kernel = kernel();
let r = kernel
.execute("seq 1 3 | scatter")
.await
.expect("execute");
assert_eq!(r.code, 0, "should succeed; err={:?}", r.err);
assert!(
r.text_out().contains("3 items"),
"expected 3 items for seq 1..3, got: {:?}",
r.text_out()
);
}
// ---------------------------------------------------------------------------
// Plain-text (no structured data) still newline-splits
// ---------------------------------------------------------------------------
/// Without structured data, scatter must still newline-split the text (no
/// regression on the plain-text path).
#[tokio::test]
async fn plain_text_pipe_still_newline_splits() {
let kernel = kernel();
// printf emits plain text — no structured .data sideband
let r = kernel
.execute(r#"printf "a\nb\nc" | scatter"#)
.await
.expect("execute");
assert_eq!(r.code, 0, "should succeed; err={:?}", r.err);
assert!(
r.text_out().contains("3 items"),
"expected 3 items from 3-line plain text, got: {:?}",
r.text_out()
);
}