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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
//! Mode-flag coverage: `--explain`, `--schema`, `--verbose`.
//!
//! Acceptance: docs/design.md §3 MODES, §4.6 (provenance / explain /
//! schema), §11.1 (verbose emits skip warnings + provenance).
mod common;
use common::drive;
mod explain {
use super::drive;
/// Acceptance: docs/design.md §4.6 — `--explain` dumps the full
/// reasoning broken into `input:`, `strategy:`, `output:` sections
/// without the JSON envelope.
#[test]
fn explain_prints_input_strategy_output_sections() {
let stdin = "{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
let (code, stdout, stderr) = drive(&["face", "--explain"], stdin);
assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
// Soft-match: the developer may write the explain output to
// stdout (default) or stderr; check both for the section names.
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("input"),
"explain has `input` section: {combined:?}",
);
assert!(
combined.contains("strategy"),
"explain has `strategy` section: {combined:?}",
);
assert!(
combined.contains("output"),
"explain has `output` section: {combined:?}",
);
// Explain mode does not emit the JSON envelope: stdout should
// not parse as a face envelope. (If the developer chooses to
// emit explain on stderr and still emit human format on stdout,
// this assertion still passes — human format starts with
// `face: …` not `{`.)
assert!(
!stdout.trim_start().starts_with("{\"result\""),
"explain mode does not emit envelope JSON: {stdout:?}",
);
}
/// Acceptance: docs/design.md §4.6 — explain output names the
/// detected input format, e.g. `jsonl`.
#[test]
fn explain_includes_format_jsonl() {
let stdin = "{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
let (code, stdout, stderr) = drive(&["face", "--explain"], stdin);
assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("jsonl"),
"explain reports the detected jsonl format: {combined:?}",
);
}
/// Acceptance: docs/design.md §4.6 — explain output reports the
/// detected items path, not a hard-coded `.`.
#[test]
fn explain_includes_detected_items_path() {
let stdin = "{\"items\":[{\"kind\":\"bug\"},{\"kind\":\"feat\"}],\"query\":\"TODO\"}";
let (code, stdout, stderr) = drive(&["face", "--explain"], stdin);
assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains(".items"),
"explain reports detected items path: {combined:?}",
);
}
}
mod schema {
use super::drive;
/// Acceptance: docs/design.md §4.6 — `--schema` describes input
/// shape (records, fields, items_path) without grouping.
#[test]
fn schema_lists_fields_and_records() {
let stdin = "{\"items\":[{\"kind\":\"bug\",\"score\":0.5}],\"query\":\"TODO\"}";
let (code, stdout, stderr) = drive(&["face", "--schema"], stdin);
assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
let combined = format!("{stdout}{stderr}");
// Soft-match: the §4.6 example uses these labels but the
// developer may format slightly differently. Check for the
// canonical key names that name the structural facts.
assert!(
combined.contains("records"),
"schema mentions `records`: {combined:?}",
);
assert!(
combined.contains("fields"),
"schema mentions `fields`: {combined:?}",
);
assert!(
combined.contains("items_path"),
"schema mentions `items_path`: {combined:?}",
);
assert!(
combined.contains(".items"),
"schema reports detected items path: {combined:?}",
);
// The two field names from the input must appear somewhere.
assert!(
combined.contains("kind"),
"schema lists field `kind`: {combined:?}",
);
assert!(
combined.contains("score"),
"schema lists field `score`: {combined:?}",
);
}
/// Acceptance: docs/design.md §4.6 — schema exposes nested JSON
/// leaf paths so users can copy the same path into `--by`.
#[test]
fn schema_lists_nested_leaf_paths() {
let stdin = "{\"type\":\"match\",\"data\":{\"path\":{\"text\":\"src/lib.rs\"}}}\n\
{\"type\":\"summary\",\"data\":{\"elapsed_total\":{\"secs\":0}}}\n";
let (code, stdout, stderr) = drive(&["face", "--schema"], stdin);
assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("data.path.text"),
"schema lists nested path field: {combined:?}",
);
}
}
mod interactive {
use super::drive;
/// Acceptance: docs/design.md §10.4 — when no TTY is available,
/// `--interactive` silently falls back to normal non-interactive
/// rendering so scripts can pass it conditionally.
#[test]
fn non_tty_falls_back_to_normal_output() {
let stdin = "{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
let (code, stdout, stderr) = drive(&["face", "--interactive"], stdin);
assert_eq!(code, 0, "stderr={stderr:?} stdout={stdout:?}");
assert!(
stdout.contains("bug"),
"fallback renders clusters: {stdout:?}"
);
assert!(
stdout.contains("feat"),
"fallback renders clusters: {stdout:?}"
);
}
}
mod stderr_skips {
//! §11.1 — every skipped record (parser-side or clusterer-side)
//! produces one stderr line. The clusterer-side path is exercised
//! by feeding a record with a value at the axis field that the
//! clusterer cannot coerce (an array at an `exact` axis).
use super::drive;
/// Acceptance: docs/design.md §11.1 — clusterer-side skips
/// (collected via the `Diagnostics` sink) are surfaced to stderr
/// alongside parser-side skips. A record whose `exact_key` field
/// is an array trips a `WrongType` skip in the exact clusterer.
#[test]
fn tree_skips_emit_warning_lines() {
// Two valid records (good for picking `kind` as the grouping
// axis) plus one with an array at `kind` that the exact
// clusterer cannot coerce. Pin the grouping axis explicitly
// so auto-detection's heuristics don't reject the dataset.
let stdin = "{\"kind\":\"bug\"}\n\
{\"kind\":[1,2]}\n\
{\"kind\":\"feat\"}\n";
let (code, stdout, stderr) = drive(&["face", "--by=kind", "--verbose"], stdin);
assert_eq!(
code, 0,
"tree skip is not fatal: stdout={stdout:?} stderr={stderr:?}"
);
assert!(
stderr.contains("face: skipped record"),
"tree-side skip must produce a `face: skipped record` line on stderr; \
stderr={stderr:?}",
);
}
}
mod verbose {
use super::drive;
/// Acceptance: docs/design.md §11.1 — malformed records are quiet
/// by default so command composition does not get stderr noise.
#[test]
fn default_silences_stderr_warnings() {
let stdin = "{\"kind\":\"bug\"}\n\
{malformed json line\n\
{\"kind\":\"feat\"}\n";
let (code, _stdout, stderr) = drive(&["face"], stdin);
assert_eq!(
code, 0,
"malformed record is skipped, not fatal: stderr={stderr:?}",
);
assert!(
stderr.is_empty(),
"stderr is quiet by default: stderr={stderr:?}",
);
}
/// Acceptance: docs/design.md §11.1 — `--verbose` emits skipped
/// record warnings on stderr.
#[test]
fn verbose_emits_stderr_warnings() {
let stdin = "{\"kind\":\"bug\"}\n\
{malformed json line\n\
{\"kind\":\"feat\"}\n";
let (code, _stdout, stderr) = drive(&["face", "--verbose"], stdin);
assert_eq!(code, 0, "verbose mode is still successful");
assert!(
stderr.contains("face: skipped record"),
"stderr should warn about the skip under --verbose: stderr={stderr:?}",
);
}
/// Acceptance: docs/design.md §4.6 — `--verbose` emits provenance
/// on stderr.
#[test]
fn verbose_emits_provenance() {
let stdin = "{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
let (code, _stdout, stderr) = drive(&["face", "--verbose"], stdin);
assert_eq!(code, 0, "verbose success: stderr={stderr:?}");
assert!(
stderr.contains("items by"),
"stderr contains provenance under --verbose: {stderr:?}",
);
}
}