aion-cli 0.31.0

The `aion` command line: operate Aion durable workflows over gRPC and run the Aion server.
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
//! What `aion awl guide` must print, and what it must refuse.

use aion_awl::{Keyword, POSITIONAL_WORDS};

use super::{json, render_all, render_one};

#[test]
fn the_text_form_prints_every_word_of_the_language() {
    let rendered = render_all();
    for keyword in Keyword::ALL {
        assert!(
            rendered.contains(&format!("  {}  ", keyword.as_word()))
                || rendered.contains(&format!("  {} ", keyword.as_word())),
            "`{}` is a word of the language and must be listed",
            keyword.as_word()
        );
    }
    for word in POSITIONAL_WORDS {
        assert!(
            rendered.contains(word),
            "`{word}` is a word of the language and must be listed"
        );
    }
}

/// The command is the CLI's answer to "where do I read more". Every path it
/// names is gated for existence by `tests/doc_pointer_gate.rs`; this pins
/// that it names them at all.
#[test]
fn the_text_form_names_the_documents_an_author_needs() {
    let rendered = render_all();
    for path in [
        "docs/authoring/AWL.md",
        "docs/assistant/grounding/AWL-REFERENCE.md",
        "docs/assistant/grounding/AWL-AUTHORING.md",
        "docs/assistant/grounding/WORKERS.md",
        "docs/assistant/grounding/COMMANDS.md",
        "docs/workers/DECLARED-COMMANDS.md",
    ] {
        assert!(rendered.contains(path), "the guide must name {path}");
    }
}

/// Sections are printed once each, in reference order. A grouping that
/// emitted a heading every time the section changed would print §4 six times
/// over, and the reader would take each as a new section.
#[test]
fn each_reference_section_is_printed_once_in_order() {
    let rendered = render_all();
    let headings: Vec<u8> = rendered
        .lines()
        .filter_map(|line| line.strip_prefix('§'))
        .filter_map(|line| line.split_once(' ').map(|(number, _)| number.to_owned()))
        .filter_map(|number| number.parse::<u8>().ok())
        .collect();
    assert!(
        !headings.is_empty(),
        "the guide must print section headings"
    );
    let mut sorted = headings.clone();
    sorted.sort_unstable();
    sorted.dedup();
    assert_eq!(
        headings, sorted,
        "sections must appear once each, in reference order"
    );
}

#[test]
fn one_word_prints_its_sentence_examples_and_citation() {
    let entry = aion_awl::guide::for_word("collect");
    assert!(entry.is_some(), "`collect` is a word of the language");
    let rendered = entry.map(render_one).unwrap_or_default();
    assert!(rendered.starts_with("collect  (reserved word)\n"));
    assert!(rendered.contains("Closes the nearest region"));
    assert!(
        rendered.contains("AWL reference §8 — Parallel and sequential work"),
        "{rendered}"
    );
    assert!(
        !rendered.contains("AWL-REFERENCE.md"),
        "the citation must not name a repository path an installed binary cannot resolve: \
         {rendered}"
    );
    assert!(
        rendered.contains("\n      collect health -> results\n"),
        "the example document must be printed, indented as a block: {rendered}"
    );
    assert!(
        rendered.contains("\n    workflow check_services\n"),
        "the example must be a complete document, not a fragment: {rendered}"
    );
    assert!(
        rendered.contains("  Example 1:\n") && rendered.contains("  Example 2:\n"),
        "a word with several forms must number its examples: {rendered}"
    );
    assert!(
        rendered.contains("collect report? -> reports"),
        "the tolerant form must be among the examples: {rendered}"
    );
    assert!(
        rendered.contains("Full reference: aion awl guide --reference"),
        "the entry must say how to open the full reference from this binary: {rendered}"
    );
}

/// A word with exactly one honest form prints its single example without a
/// number — a lone "Example 1:" would promise a second that never comes.
#[test]
fn a_single_form_word_prints_one_unnumbered_example() {
    let entry = aion_awl::guide::for_word("spawn");
    assert!(entry.is_some(), "`spawn` is a word of the language");
    let rendered = entry.map(render_one).unwrap_or_default();
    assert!(
        !rendered.contains("Example 1:"),
        "a single example must not be numbered: {rendered}"
    );
    assert!(
        rendered.contains("\n      spawn notify_archived(report_id: report_id)\n"),
        "the single example must still be printed: {rendered}"
    );
}

/// The `--reference` output is the embedded reference itself — the same file
/// the repository's `guide_reference_gate` reads, so an installed binary
/// prints exactly what the gate verified.
#[test]
fn the_embedded_reference_is_the_whole_document() {
    let text = aion_awl::guide::reference_text();
    assert!(
        text.contains("## 1. Document structure"),
        "the embedded reference must open with its first numbered section"
    );
    assert!(
        text.contains("## 16. Worker documents"),
        "the embedded reference must reach its final numbered section"
    );
}

/// A positional word is labelled as one. An author who reads "reserved word"
/// over `run` would conclude they cannot name a step `run`, which is false.
#[test]
fn a_positional_word_is_labelled_as_positional() {
    let entry = aion_awl::guide::for_word("run");
    assert!(entry.is_some(), "`run` is a word of the language");
    let rendered = entry.map(render_one).unwrap_or_default();
    assert!(
        rendered.starts_with("run  (positional word)\n"),
        "{rendered}"
    );
}

#[test]
fn the_json_form_carries_the_whole_inventory_and_the_paths() {
    let rendered = json(None);
    assert!(rendered.is_ok(), "the glossary must render as JSON");
    let text = rendered.unwrap_or_default();
    assert!(text.ends_with('\n'), "the asset must end in a newline");
    let parsed: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
    assert_eq!(
        parsed["entries"].as_array().map(Vec::len),
        Some(Keyword::ALL.len() + POSITIONAL_WORDS.len())
    );
    assert_eq!(parsed["guide"].as_str(), Some("docs/authoring/AWL.md"));
    assert_eq!(
        parsed["reference"].as_str(),
        Some("docs/assistant/grounding/AWL-REFERENCE.md")
    );
    assert_eq!(parsed["entries"][0]["word"].as_str(), Some("workflow"));
    for entry in parsed["entries"].as_array().cloned().unwrap_or_default() {
        let examples = entry["examples"].as_array().cloned().unwrap_or_default();
        assert!(
            !examples.is_empty(),
            "every entry must carry at least one example document: {entry}"
        );
        for example in &examples {
            assert!(
                example
                    .as_str()
                    .is_some_and(|example| example.starts_with("//!")),
                "every example must be a complete narrated document: {entry}"
            );
        }
    }
}

/// The grammar axis on the wire: the form taxonomy and position inventory
/// ride the document, every entry names a form the taxonomy carries and
/// positions the inventory carries, and the full reference text ships so the
/// console can render the sections the citations point into.
#[test]
fn the_json_form_carries_the_grammar_axis_and_the_reference() {
    let text = json(None).unwrap_or_default();
    let parsed: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);

    let forms = parsed["forms"].as_array().cloned().unwrap_or_default();
    let form_ids: Vec<&str> = forms
        .iter()
        .filter_map(|form| form["id"].as_str())
        .collect();
    assert_eq!(
        form_ids.len(),
        aion_awl::guide::GrammarForm::ALL.len(),
        "every form of the taxonomy must ride the wire: {forms:?}"
    );
    for form in &forms {
        for field in ["id", "label", "description"] {
            assert!(
                form[field].as_str().is_some_and(|value| !value.is_empty()),
                "a form is missing `{field}`: {form}"
            );
        }
    }

    let positions = parsed["positions"].as_array().cloned().unwrap_or_default();
    let position_ids: Vec<&str> = positions
        .iter()
        .filter_map(|position| position["id"].as_str())
        .collect();
    assert_eq!(
        position_ids.len(),
        aion_awl::guide::GrammarPosition::ALL.len(),
        "every grammar position must ride the wire: {positions:?}"
    );

    for entry in parsed["entries"].as_array().cloned().unwrap_or_default() {
        let form = entry["form"].as_str().unwrap_or_default();
        assert!(
            form_ids.contains(&form),
            "an entry names a form the taxonomy does not carry: {entry}"
        );
        let stands = entry["positions"].as_array().cloned().unwrap_or_default();
        assert!(
            !stands.is_empty(),
            "an entry stands in no position: {entry}"
        );
        for position in &stands {
            assert!(
                position
                    .as_str()
                    .is_some_and(|position| position_ids.contains(&position)),
                "an entry stands in a position the inventory does not carry: {entry}"
            );
        }
    }

    let reference = parsed["reference_text"].as_str().unwrap_or_default();
    assert!(
        reference.contains("## 1. Document structure")
            && reference.contains("## 16. Worker documents"),
        "the wire must carry the whole reference, first section to last"
    );
    assert_eq!(
        reference,
        aion_awl::guide::reference_text(),
        "the wire's reference must be the embedded reference, verbatim"
    );
}

/// The console asset is gated by byte comparison, which is only a gate if the
/// rendering is deterministic.
#[test]
fn the_json_form_is_byte_stable() {
    assert_eq!(
        json(None).unwrap_or_default(),
        json(None).unwrap_or_default()
    );
}

#[test]
fn one_word_as_json_carries_only_that_word() {
    let text = json(Some("distribute")).unwrap_or_default();
    let parsed: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
    assert_eq!(parsed["entries"].as_array().map(Vec::len), Some(1));
    assert_eq!(parsed["entries"][0]["word"].as_str(), Some("distribute"));
    assert_eq!(parsed["entries"][0]["kind"].as_str(), Some("keyword"));
    assert_eq!(parsed["entries"][0]["section"]["number"].as_u64(), Some(8));
    assert_eq!(parsed["entries"][0]["form"].as_str(), Some("statement"));
}

/// A word the language does not have must produce no entry, not an empty
/// document that reads like an answer.
#[test]
fn a_word_the_language_does_not_have_resolves_to_nothing() {
    assert!(aion_awl::guide::for_word("select").is_none());
    let text = json(Some("select")).unwrap_or_default();
    let parsed: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
    assert_eq!(parsed["entries"].as_array().map(Vec::len), Some(0));
}

/// The three structural rows print under the sentence, in the taxonomy's
/// order, with the words the grammar table derives. Written out rather than
/// recomputed: a test that asked `entry.children()` for its expectation would
/// pass whatever the derivation said.
#[test]
fn one_word_prints_where_it_stands_in_the_language() {
    let entry = aion_awl::guide::for_word("workflow");
    assert!(entry.is_some(), "`workflow` is a word of the language");
    let rendered = entry.map(render_one).unwrap_or_default();
    assert!(
        rendered.contains(
            "  Inside it  input, signal, outcome, timeout, type, route, success, failure,\n         \
             \x20   query, send\n"
        ),
        "the words a workflow header admits must print under the sentence: {rendered}"
    );
    assert!(
        rendered.contains(
            "  Alongside  workloop, type, worker, child, detached, const, subflow, \
                           step,\n             retire, set, export, recipe, wall, artifact\n"
        ),
        "the words a workflow stands beside must print: {rendered}"
    );
    let sentence = "one `.awl` file declares exactly one workflow, named here.\n";
    let rows = rendered
        .split_once(sentence)
        .map(|(_, rest)| rest)
        .unwrap_or_default();
    assert!(
        rows.starts_with("  Inside it"),
        "the rows must sit directly under the sentence: {rendered}"
    );
}

/// A top-level declaration is enclosed by the DOCUMENT, which is no word of
/// the language. Printing an empty row there would read exactly like
/// `filter`'s empty row, which means the opposite thing.
#[test]
fn a_top_level_word_lives_in_the_document() {
    let entry = aion_awl::guide::for_word("workflow");
    let rendered = entry.map(render_one).unwrap_or_default();
    assert!(
        rendered.contains("  Lives in   the document\n"),
        "a top-level declaration must name the document as what encloses it: {rendered}"
    );
    let nested = aion_awl::guide::for_word("step")
        .map(render_one)
        .unwrap_or_default();
    assert!(
        nested.contains("  Lives in   the document, recipe\n"),
        "`step` stands at the top level AND inside a recipe: {nested}"
    );
}

/// A word that opens nothing and is reached by a cue on the line prints
/// neither row. An "Inside it" with nothing after it invites a reader to
/// wonder what went missing, when the answer is that nothing is there.
#[test]
fn a_word_with_no_such_relation_prints_no_empty_row() {
    let rendered = aion_awl::guide::for_word("filter")
        .map(render_one)
        .unwrap_or_default();
    assert!(
        rendered.contains("  Alongside  sort, map, count, any, all, route\n"),
        "`filter` stands beside the other pipeline stages: {rendered}"
    );
    assert!(
        !rendered.contains("Inside it"),
        "`filter` opens nothing, so it must print no containment row: {rendered}"
    );
    assert!(
        !rendered.contains("Lives in"),
        "`filter` is reached by a cue on the line, so nothing encloses it: {rendered}"
    );
}

/// A step body admits twenty-six words and the row carries all of them. The
/// whole truth is printed; it is the WRAPPING that keeps it readable, and the
/// continuation lines align under the first word rather than under the
/// heading.
#[test]
fn a_long_relation_row_wraps_under_its_heading() {
    let rendered = aion_awl::guide::for_word("step")
        .map(render_one)
        .unwrap_or_default();
    let row: Vec<&str> = rendered
        .lines()
        .skip_while(|line| !line.starts_with("  Inside it"))
        .take_while(|line| !line.trim_start().starts_with("Alongside"))
        .collect();
    assert!(
        row.len() > 1,
        "the step body's twenty-six words must wrap over several lines: {rendered}"
    );
    for line in &row {
        assert!(
            line.len() <= 78,
            "a wrapped row must not run past the measure: `{line}`"
        );
    }
    for line in row.iter().skip(1) {
        assert!(
            line.starts_with("             ") && !line.starts_with("              "),
            "a continuation must align under the first word, not under the heading: `{line}`"
        );
    }
    let words: String = row.join(" ");
    for word in aion_awl::guide::children_of("step") {
        assert!(
            words.contains(word),
            "the row must carry every word the step body admits, `{word}` included: {rendered}"
        );
    }
}

/// The wire carries the relation taxonomy and every entry's three lists, and
/// each id in them names another entry of the same document — which is what
/// lets the console render them as links without checking.
#[test]
fn the_json_form_carries_the_structural_relations() {
    let text = json(None).unwrap_or_default();
    let parsed: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);

    let relations = parsed["relations"].as_array().cloned().unwrap_or_default();
    let relation_ids: Vec<&str> = relations
        .iter()
        .filter_map(|relation| relation["id"].as_str())
        .collect();
    assert_eq!(
        relation_ids,
        vec!["children", "siblings", "parents"],
        "the relation taxonomy must ride the wire in its own order: {relations:?}"
    );
    for relation in &relations {
        for field in ["id", "label", "description"] {
            assert!(
                relation[field]
                    .as_str()
                    .is_some_and(|value| !value.is_empty()),
                "a relation is missing `{field}`: {relation}"
            );
        }
    }

    let entries = parsed["entries"].as_array().cloned().unwrap_or_default();
    let words: Vec<&str> = entries
        .iter()
        .filter_map(|entry| entry["word"].as_str())
        .collect();
    assert_eq!(
        words.len(),
        99,
        "the sweep below is only as wide as the entries it reads: {}",
        words.len()
    );
    let mut edges = 0usize;
    for entry in &entries {
        for relation in &relation_ids {
            let related = entry[*relation].as_array().cloned().unwrap_or_default();
            for id in &related {
                edges += 1;
                let id = id.as_str().unwrap_or_default();
                assert!(
                    words.contains(&id),
                    "an entry's `{relation}` names `{id}`, which is no entry of this document"
                );
            }
        }
        assert!(
            entry["at_document_level"].as_bool().is_some(),
            "every entry must say whether the document is what encloses it: {entry}"
        );
    }
    assert!(
        edges > 500,
        "the control failed: only {edges} relation ids were read, so the sweep measured an empty \
         harness"
    );
}