use serde_json::json;
use super::*;
use crate::{
answer::TypedResult,
inline::InlineRun,
math::MathView,
render::{AnswerStep, CellAlign, ChecklistItem, ContentUnit},
session::{CardTier, Cell},
};
fn pin<T: serde::Serialize>(anchor: &str, dto: &T, expected: serde_json::Value) {
let actual = serde_json::to_value(dto).unwrap();
assert_eq!(
actual, expected,
"wire shape drifted from docs/API.md#{anchor} — update the doc's \
field table + example AND add a CHANGELOG entry"
);
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/contracts");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join(format!("{anchor}.json")),
serde_json::to_string_pretty(&expected).unwrap() + "\n",
)
.unwrap();
}
#[test]
fn statedto_select_phase_wire_shape() {
let dto = StateDto {
kind: "review",
topology: None,
region: None,
study_revision: 7,
phase: "select",
card: None,
choices: None,
choices_multiple: None,
choice_runs: None,
keypoints: None,
keypoint_runs: None,
introducing: false,
mode: "flip",
depth: "recall",
input: "type",
remaining: 0,
initial: 0,
reviews: 0,
passed: 0,
failed: 0,
introduced: 0,
partial: 0,
exam_due: Vec::new(),
can_restart: false,
next_due_ms: None,
due_left: 0,
new_left: 0,
met_total: 4,
deck_total: 9,
recognize_gap: None,
label: "select decks".to_string(),
save_error: None,
load_warnings: Vec::new(),
};
pin(
"StateDto.select",
&dto,
json!({
"kind": "review",
"phase": "select",
"card": null,
"choices": null,
"choices_multiple": null,
"choice_runs": null,
"keypoints": null,
"keypoint_runs": null,
"introducing": false,
"mode": "flip",
"depth": "recall",
"input": "type",
"remaining": 0,
"initial": 0,
"reviews": 0,
"study_revision": 7,
"passed": 0,
"failed": 0,
"introduced": 0,
"partial": 0,
"exam_due": [],
"can_restart": false,
"due_left": 0,
"new_left": 0,
"met_total": 4,
"deck_total": 9,
"label": "select decks"
}),
);
}
#[test]
fn statedto_review_phase_wire_shape() {
let dto = StateDto {
kind: "review",
topology: Some("order".to_string()),
region: Some("intro".to_string()),
study_revision: 7,
phase: "review",
card: Some(CardDto {
id: Some("card-9w2c7xkq4m".to_string()),
front: "What is ownership?".to_string(),
front_runs: crate::inline::parse_inline("What is **ownership**?"),
front_units: None,
section_context: Vec::new(),
section_context_runs: Vec::new(),
section_context_units: Vec::new(),
context: vec!["Chapter 4".to_string()],
context_leads: true,
context_runs: vec![crate::inline::parse_inline("Chapter 4")],
context_units: Vec::new(),
back: vec!["every value has one owner".to_string()],
back_runs: vec![crate::inline::parse_inline("every value has one owner")],
back_units: vec![ContentUnit::Sentence {
text: "every value has one owner".to_string(),
runs: crate::inline::parse_inline("every value has one owner"),
}],
answer_steps: vec![
AnswerStep::Line {
back_from: 0,
back_to: 1,
},
],
reshaped: true,
note: vec![NoteDto {
badge: Some(crate::card::Badge::Note),
units: vec![
ContentUnit::Sentence {
text: "Ownership frees memory deterministically.".to_string(),
runs: crate::inline::parse_inline("Ownership frees memory deterministically."),
},
ContentUnit::Code {
lines: vec!["let s = String::new();".to_string()],
},
ContentUnit::Checklist {
items: vec![
ChecklistItem {
checked: true,
text: "Own the value".to_string(),
runs: crate::inline::parse_inline("Own the **value**"),
},
ChecklistItem {
checked: false,
text: "Borrow forever".to_string(),
runs: crate::inline::parse_inline("Borrow forever"),
},
],
},
],
}],
images: vec![ImageDto {
src: "/img/0123456789abcdef".to_string(),
alt: Some("a heap-allocated String".to_string()),
regions: vec![
crate::review::RegionView {
role: crate::review::RegionRole::Asked,
reveal_on_answer: true,
x: 240.0,
y: 160.0,
width: 600.0,
height: 400.0,
unit: "px".to_string(),
},
crate::review::RegionView {
role: crate::review::RegionRole::Mask,
reveal_on_answer: false,
x: 10.0,
y: 20.0,
width: 30.0,
height: 40.0,
unit: "px".to_string(),
},
crate::review::RegionView {
role: crate::review::RegionRole::Cover,
reveal_on_answer: false,
x: 700.0,
y: 0.0,
width: 100.0,
height: 60.0,
unit: "px".to_string(),
},
],
crop: Some(crate::review::CropView {
x: 0.0,
y: 0.0,
width: 800.0,
height: 600.0,
unit: "px".to_string(),
}),
}],
images_back: vec![
ImageDto {
src: "/img/0123456789abcdef".to_string(),
alt: Some("the stack pointer".to_string()),
regions: Vec::new(),
crop: None,
},
ImageDto {
src: "/img/fedcba9876543210".to_string(),
alt: None,
regions: Vec::new(),
crop: None,
},
],
citations: vec![CitationDto {
locator: "string.rs:120-128".to_string(),
excerpt: Some(ExcerptDto {
path: "src/string.rs".to_string(),
lines: vec![LineDto {
n: 120,
text: "let s = String::new();".to_string(),
}],
truncated: false,
}),
error: None,
}],
crumb: Some(CrumbDto {
regions: vec!["intro".to_string(), "body".to_string()],
current: 1,
cells: vec![vec![Cell { tier: CardTier::Seen, locked: false }], vec![Cell { tier: CardTier::LearnedStrong, locked: false }]],
}),
}),
choices: Some(vec!["owner".to_string(), "borrower".to_string()]),
choices_multiple: None,
choice_runs: Some(vec![
crate::inline::parse_inline("owner"),
crate::inline::parse_inline("borrower"),
]),
keypoints: Some(vec!["one owner per value".to_string()]),
keypoint_runs: Some(vec![crate::inline::parse_inline("one owner per value")]),
introducing: false,
mode: "flip",
depth: "recall",
input: "type",
remaining: 3,
initial: 5,
reviews: 2,
passed: 1,
failed: 1,
introduced: 4,
partial: 0,
exam_due: vec!["rust.md".to_string()],
can_restart: true,
next_due_ms: None,
due_left: 0,
new_left: 0,
met_total: 4,
deck_total: 9,
recognize_gap: None,
label: "rust.md".to_string(),
save_error: Some(
"progress/deck-rust1.json: stale progress revision 3; disk is at 4".to_string(),
),
load_warnings: vec!["2 frozen diagram(s) did not resolve and fall back to source; run `alix doctor` for details".to_string()],
};
pin(
"StateDto.review",
&dto,
json!({
"kind": "review",
"phase": "review",
"card": {
"id": "card-9w2c7xkq4m",
"front": "What is ownership?",
"front_runs": [
{"text": "What is "},
{"text": "ownership", "bold": true},
{"text": "?"}
],
"context": ["Chapter 4"],
"context_leads": true,
"context_runs": [[{"text": "Chapter 4"}]],
"context_units": [],
"back": ["every value has one owner"],
"back_runs": [[{"text": "every value has one owner"}]],
"back_units": [{
"kind": "sentence",
"text": "every value has one owner",
"runs": [{"text": "every value has one owner"}]
}],
"answer_steps": [
{"kind": "line", "back_from": 0, "back_to": 1}
],
"reshaped": true,
"note": [{
"badge": "note",
"units": [
{
"kind": "sentence",
"text": "Ownership frees memory deterministically.",
"runs": [{"text": "Ownership frees memory deterministically."}]
},
{"kind": "code", "lines": ["let s = String::new();"]},
{
"kind": "checklist",
"items": [
{
"checked": true,
"text": "Own the value",
"runs": [{"text": "Own the "}, {"text": "value", "bold": true}]
},
{
"checked": false,
"text": "Borrow forever",
"runs": [{"text": "Borrow forever"}]
}
]
}
]
}],
"images": [
{"src": "/img/0123456789abcdef", "alt": "a heap-allocated String",
"regions": [
{"role": "asked", "reveal_on_answer": true, "x": 240.0, "y": 160.0, "width": 600.0, "height": 400.0, "unit": "px"},
{"role": "mask", "reveal_on_answer": false, "x": 10.0, "y": 20.0, "width": 30.0, "height": 40.0, "unit": "px"},
{"role": "cover", "reveal_on_answer": false, "x": 700.0, "y": 0.0, "width": 100.0, "height": 60.0, "unit": "px"}
],
"crop": {"x": 0.0, "y": 0.0, "width": 800.0, "height": 600.0, "unit": "px"}}
],
"images_back": [
{"src": "/img/0123456789abcdef", "alt": "the stack pointer"},
{"src": "/img/fedcba9876543210", "alt": null}
],
"citations": [{
"locator": "string.rs:120-128",
"excerpt": {
"path": "src/string.rs",
"lines": [{"n": 120, "text": "let s = String::new();"}],
"truncated": false
},
"error": null
}],
"crumb": {
"regions": ["intro", "body"],
"current": 1,
"cells": [
[{"tier": "seen", "locked": false}],
[{"tier": "learned-strong", "locked": false}]
]
}
},
"choices": ["owner", "borrower"],
"choices_multiple": null,
"choice_runs": [[{"text": "owner"}], [{"text": "borrower"}]],
"keypoints": ["one owner per value"],
"keypoint_runs": [[{"text": "one owner per value"}]],
"introducing": false,
"mode": "flip",
"depth": "recall",
"input": "type",
"remaining": 3,
"initial": 5,
"reviews": 2,
"study_revision": 7,
"passed": 1,
"failed": 1,
"introduced": 4,
"partial": 0,
"exam_due": ["rust.md"],
"can_restart": true,
"due_left": 0,
"new_left": 0,
"met_total": 4,
"deck_total": 9,
"label": "rust.md",
"topology": "order",
"region": "intro",
"save_error": "progress/deck-rust1.json: stale progress revision 3; disk is at 4",
"load_warnings": ["2 frozen diagram(s) did not resolve and fall back to source; run `alix doctor` for details"]
}),
);
}
#[test]
fn statedto_done_phase_carries_the_next_due_instant() {
let dto = StateDto {
kind: "review",
topology: None,
region: None,
study_revision: 7,
phase: "done",
card: None,
choices: None,
choices_multiple: None,
choice_runs: None,
keypoints: None,
keypoint_runs: None,
introducing: false,
mode: "flip",
depth: "recall",
input: "type",
remaining: 0,
initial: 0,
reviews: 0,
passed: 0,
failed: 0,
introduced: 0,
partial: 0,
exam_due: Vec::new(),
can_restart: true,
next_due_ms: Some(1_700_000_100_000),
due_left: 12,
new_left: 3,
met_total: 4,
deck_total: 9,
recognize_gap: None,
label: "rust.md".to_string(),
save_error: None,
load_warnings: Vec::new(),
};
pin(
"StateDto.done",
&dto,
json!({
"kind": "review",
"phase": "done",
"card": null,
"choices": null,
"choices_multiple": null,
"choice_runs": null,
"keypoints": null,
"keypoint_runs": null,
"introducing": false,
"mode": "flip",
"depth": "recall",
"input": "type",
"remaining": 0,
"initial": 0,
"reviews": 0,
"study_revision": 7,
"passed": 0,
"failed": 0,
"introduced": 0,
"partial": 0,
"exam_due": [],
"can_restart": true,
"next_due_ms": 1_700_000_100_000_u64,
"due_left": 12,
"new_left": 3,
"met_total": 4,
"deck_total": 9,
"label": "rust.md"
}),
);
}
#[test]
fn statedto_done_phase_carries_the_recognize_gap() {
let dto = StateDto {
kind: "review",
topology: None,
region: None,
study_revision: 7,
phase: "done",
card: None,
choices: None,
choices_multiple: None,
choice_runs: None,
keypoints: None,
keypoint_runs: None,
introducing: false,
mode: "flip",
depth: "recognize",
input: "type",
remaining: 0,
initial: 0,
reviews: 0,
passed: 0,
failed: 0,
introduced: 0,
partial: 1,
exam_due: Vec::new(),
can_restart: false,
next_due_ms: None,
due_left: 0,
new_left: 0,
met_total: 4,
deck_total: 9,
recognize_gap: Some(crate::session::RecognizeGap {
recall: 13,
unaugmented: 13,
}),
label: "status-codes.md".to_string(),
save_error: None,
load_warnings: Vec::new(),
};
pin(
"StateDto.recognize-gap",
&dto,
json!({
"kind": "review",
"phase": "done",
"card": null,
"choices": null,
"choices_multiple": null,
"choice_runs": null,
"keypoints": null,
"keypoint_runs": null,
"introducing": false,
"mode": "flip",
"depth": "recognize",
"input": "type",
"remaining": 0,
"initial": 0,
"reviews": 0,
"study_revision": 7,
"passed": 0,
"failed": 0,
"introduced": 0,
"partial": 1,
"exam_due": [],
"can_restart": false,
"due_left": 0,
"new_left": 0,
"met_total": 4,
"deck_total": 9,
"recognize_gap": {"recall": 13, "unaugmented": 13},
"label": "status-codes.md"
}),
);
}
#[test]
fn walkdto_predict_phase_wire_shape() {
let dto = WalkDto {
kind: "walk",
phase: "predict",
description: "how a String grows".to_string(),
description_runs: crate::inline::parse_inline("how a String grows"),
source: Some("src/lib.rs".to_string()),
total: 2,
current: 2,
path: vec![
HopDto {
prompt: "push begins".to_string(),
delta: Some("passed"),
current: false,
},
HopDto {
prompt: "capacity doubles".to_string(),
delta: None,
current: true,
},
],
prompt: Some("what does push do when full?".to_string()),
prompt_runs: Some(crate::inline::parse_inline("what does push do when full?")),
givens: vec!["a String at capacity".to_string()],
given_runs: vec![crate::inline::parse_inline("a String at capacity")],
locator: Some("lib.rs:40-52".to_string()),
prediction: None,
excerpt: None,
excerpt_error: None,
points: Vec::new(),
point_runs: Vec::new(),
note: None,
note_runs: None,
summary: None,
};
pin(
"WalkDto.predict",
&dto,
json!({
"kind": "walk",
"phase": "predict",
"description": "how a String grows",
"description_runs": [{"text": "how a String grows"}],
"source": "src/lib.rs",
"total": 2,
"current": 2,
"path": [
{"prompt": "push begins", "delta": "passed", "current": false},
{"prompt": "capacity doubles", "delta": null, "current": true}
],
"prompt": "what does push do when full?",
"prompt_runs": [{"text": "what does push do when full?"}],
"givens": ["a String at capacity"],
"given_runs": [[{"text": "a String at capacity"}]],
"locator": "lib.rs:40-52",
"prediction": null,
"excerpt": null,
"excerpt_error": null,
"points": [],
"point_runs": [],
"note": null,
"note_runs": null,
"summary": null
}),
);
}
#[test]
fn walkdto_done_phase_wire_shape() {
let dto = WalkDto {
kind: "walk",
phase: "done",
description: "how a String grows".to_string(),
description_runs: crate::inline::parse_inline("how a String grows"),
source: None,
total: 3,
current: 3,
path: vec![HopDto {
prompt: "capacity doubles".to_string(),
delta: Some("partly"),
current: false,
}],
prompt: None,
prompt_runs: None,
givens: Vec::new(),
given_runs: Vec::new(),
locator: None,
prediction: Some("it reallocates".to_string()),
excerpt: Some(ExcerptDto {
path: "src/lib.rs".to_string(),
lines: vec![LineDto {
n: 40,
text: "self.grow();".to_string(),
}],
truncated: true,
}),
excerpt_error: None,
points: vec!["amortized doubling".to_string()],
point_runs: vec![crate::inline::parse_inline("amortized doubling")],
note: Some("see also Vec".to_string()),
note_runs: Some(crate::inline::parse_inline("see also Vec")),
summary: Some(SummaryDto {
passed: 1,
partly: 1,
failed: 1,
weak: vec![2, 3],
total: 3,
}),
};
pin(
"WalkDto.done",
&dto,
json!({
"kind": "walk",
"phase": "done",
"description": "how a String grows",
"description_runs": [{"text": "how a String grows"}],
"source": null,
"total": 3,
"current": 3,
"path": [
{"prompt": "capacity doubles", "delta": "partly", "current": false}
],
"prompt": null,
"prompt_runs": null,
"givens": [],
"given_runs": [],
"locator": null,
"prediction": "it reallocates",
"excerpt": {
"path": "src/lib.rs",
"lines": [{"n": 40, "text": "self.grow();"}],
"truncated": true
},
"excerpt_error": null,
"points": ["amortized doubling"],
"point_runs": [[{"text": "amortized doubling"}]],
"note": "see also Vec",
"note_runs": [{"text": "see also Vec"}],
"summary": {"passed": 1, "partly": 1, "failed": 1, "weak": [2, 3], "total": 3}
}),
);
}
#[test]
fn examdto_results_phase_wire_shape() {
let dto = ExamDto {
phase: "results",
deck: "rust.md".to_string(),
strictness: "balanced",
total: 1,
current: 1,
question: None,
answer: String::new(),
on_last: true,
grades: vec![ExamGradeDto {
question: "Why does Rust use ownership?".to_string(),
points: vec!["memory safety without a GC".to_string()],
answer: "it frees memory deterministically".to_string(),
verdict: "PASS",
feedback: "solid".to_string(),
missed: Vec::new(),
}],
passed: Some(true),
gaps: Vec::new(),
can_remediate: false,
remediated_count: None,
is_trace: false,
unlocks: vec!["next.md".to_string()],
thinking: false,
error: None,
elapsed: None,
cooldown_ms: None,
};
pin(
"ExamDto.results",
&dto,
json!({
"phase": "results",
"deck": "rust.md",
"strictness": "balanced",
"total": 1,
"current": 1,
"question": null,
"answer": "",
"on_last": true,
"grades": [{
"question": "Why does Rust use ownership?",
"points": ["memory safety without a GC"],
"answer": "it frees memory deterministically",
"verdict": "PASS",
"feedback": "solid",
"missed": []
}],
"passed": true,
"gaps": [],
"can_remediate": false,
"remediated_count": null,
"is_trace": false,
"unlocks": ["next.md"],
"thinking": false,
"error": null,
"elapsed": null,
"cooldown_ms": null
}),
);
}
#[test]
fn examdto_cooldown_phase_wire_shape() {
let dto = cooldown_dto("deck.md", 90000);
pin(
"ExamDto.cooldown",
&dto,
json!({
"phase": "cooldown",
"deck": "deck.md",
"strictness": "balanced",
"total": 0,
"current": 0,
"question": null,
"answer": "",
"on_last": false,
"grades": [],
"passed": null,
"gaps": [],
"can_remediate": false,
"remediated_count": null,
"is_trace": true,
"unlocks": [],
"thinking": false,
"error": null,
"elapsed": null,
"cooldown_ms": 90000
}),
);
}
#[test]
fn no_route_serves_section_context() {
let source = include_str!("mod.rs");
let routes: Vec<&str> = source
.match_indices("\"/api/")
.filter_map(|(at, _)| {
let rest = &source[at + 1..];
rest.find('"').map(|end| &rest[..end])
})
.collect();
assert!(
routes.len() > 20,
"the scrape found {} routes, so it stopped matching the source",
routes.len()
);
let offenders: Vec<&&str> = routes
.iter()
.filter(|route| route.contains("context") || route.contains("section"))
.collect();
assert!(
offenders.is_empty(),
"a section is delivered on its card, never fetched: {offenders:?}"
);
}
#[test]
fn carddto_sectioned_wire_shape() {
let text =
"# *Ownership* rules\n\nBorrowing has rules.\n\n```rust\nlet x = 1;\n```\n\n## q\na\n";
let cards = crate::parser::parse_str("t", text).expect("the fixture parses");
let dto = card_dto(
crate::review::CardView::from(&cards[0]),
Some("card-9w2c7xkq4m".to_string()),
);
pin(
"CardDto.sectioned",
&dto,
json!({
"back": [
"a"
],
"back_runs": [
[
{
"text": "a"
}
]
],
"back_units": [
{
"kind": "sentence",
"runs": [
{
"text": "a"
}
],
"text": "a"
}
],
"citations": [],
"context": [],
"context_leads": false,
"context_runs": [],
"context_units": [],
"crumb": null,
"front": "q",
"front_runs": [
{
"text": "q"
}
],
"id": "card-9w2c7xkq4m",
"images": [],
"images_back": [],
"note": [],
"answer_steps": [
{"kind": "line", "back_from": 0, "back_to": 1}
],
"reshaped": false,
"section_context": [
"*Ownership* rules",
"Borrowing has rules.",
"```rust",
"let x = 1;",
"```"
],
"section_context_runs": [
[
{
"italic": true,
"text": "Ownership"
},
{
"text": " rules"
}
],
[
{
"text": "Borrowing has rules."
}
],
[
{
"text": "```rust"
}
],
[
{
"text": "let x = 1;"
}
],
[
{
"text": "```"
}
]
],
"section_context_units": [
{
"kind": "code",
"lines": [
"let x = 1;"
]
}
]
}),
);
}
#[test]
fn carddto_section_bare_math_is_one_display_sentence() {
let text = "# Formula\n\n$$\nx^2 + y^2\n$$\n\n## q\na\n";
let cards = crate::parser::parse_str("t", text).expect("the fixture parses");
let dto = card_dto(crate::review::CardView::from(&cards[0]), None);
let [crate::render::ContentUnit::Sentence { text, runs }] =
dto.section_context_units.as_slice()
else {
panic!(
"the closed section block must be one display sentence: {:?}",
dto.section_context_units
);
};
assert_eq!("x^2 + y^2", text);
assert!(runs[0].math.as_ref().is_some_and(|math| math.display));
}
#[test]
fn carddto_wire_shape() {
let dto = card_dto(
crate::review::CardView {
front: "Pick a city:\n- [x] Paris\n- [ ] Rome".to_string(),
front_runs: crate::inline::parse_inline("Pick a city:\n- [x] **Paris**\n- [ ] Rome"),
front_units: crate::render::front_units("Pick a city:\n- [x] **Paris**\n- [ ] Rome"),
section_context: Vec::new(),
section_context_runs: Vec::new(),
section_context_units: Vec::new(),
context: Vec::new(),
context_leads: false,
context_runs: Vec::new(),
context_units: Vec::new(),
back: vec!["Use **x**".to_string(), "France".to_string()],
back_runs: vec![
crate::inline::parse_inline("Use `**x**`"),
crate::inline::parse_inline("*France*"),
],
back_units: vec![ContentUnit::Sentence {
text: "Use **x** France".to_string(),
runs: crate::inline::parse_inline("Use `**x**` *France*"),
}],
answer_steps: vec![
AnswerStep::Line {
back_from: 0,
back_to: 1,
},
AnswerStep::Line {
back_from: 1,
back_to: 2,
},
],
reshaped: false,
note: vec![crate::review::NoteView {
badge: Some(crate::card::Badge::Note),
units: vec![ContentUnit::Sentence {
text: "A **city**.".to_string(),
runs: crate::inline::parse_inline("A **city**."),
}],
}],
images: Vec::new(),
images_back: Vec::new(),
citations: Vec::new(),
},
Some("card-9w2c7xkq4m".to_string()),
);
pin(
"CardDto",
&dto,
json!({
"id": "card-9w2c7xkq4m",
"front": "Pick a city:\n- [x] Paris\n- [ ] Rome",
"front_runs": [
{"text": "Pick a city:\n- [x] "},
{"text": "Paris", "bold": true},
{"text": "\n- [ ] Rome"}
],
"front_units": [
{
"kind": "sentence",
"text": "Pick a city:",
"runs": [{"text": "Pick a city:"}]
},
{
"kind": "checklist",
"items": [
{
"checked": true,
"text": "Paris",
"runs": [{"text": "Paris", "bold": true}]
},
{
"checked": false,
"text": "Rome",
"runs": [{"text": "Rome"}]
}
]
}
],
"context": [],
"context_leads": false,
"context_runs": [],
"context_units": [],
"back": ["Use **x**", "France"],
"back_runs": [
[{"text": "Use "}, {"text": "**x**", "code": true}],
[{"text": "France", "italic": true}]
],
"back_units": [{
"kind": "sentence",
"text": "Use **x** France",
"runs": [
{"text": "Use "},
{"text": "**x**", "code": true},
{"text": " "},
{"text": "France", "italic": true}
]
}],
"answer_steps": [
{"kind": "line", "back_from": 0, "back_to": 1},
{"kind": "line", "back_from": 1, "back_to": 2}
],
"reshaped": false,
"note": [{
"badge": "note",
"units": [{
"kind": "sentence",
"text": "A **city**.",
"runs": [{"text": "A "}, {"text": "city", "bold": true}, {"text": "."}]
}]
}],
"images": [],
"images_back": [],
"citations": [],
"crumb": null
}),
);
}
#[test]
fn carddto_math_wire_shape() {
let inline = InlineRun {
text: "x^2".to_string(),
math: Some(MathView {
display: false,
svg: Some("<svg><path d=\"M0 0\"/></svg>".to_string()),
error: None,
}),
..InlineRun::default()
};
let display = InlineRun {
text: "y^2".to_string(),
math: Some(MathView {
display: true,
svg: Some("<svg><path d=\"M1 1\"/></svg>".to_string()),
error: None,
}),
..InlineRun::default()
};
let error = InlineRun {
text: r"\frac{1".to_string(),
math: Some(MathView {
display: false,
svg: None,
error: Some("ParseError: unexpected end".to_string()),
}),
..InlineRun::default()
};
let dto = CardDto {
id: Some("card-9w2c7xkq4m".to_string()),
front: "x^2".to_string(),
front_runs: vec![inline],
front_units: None,
section_context: Vec::new(),
section_context_runs: Vec::new(),
section_context_units: Vec::new(),
context: vec!["$$y^2$$".to_string()],
context_leads: true,
context_runs: vec![vec![display]],
context_units: Vec::new(),
back: vec![r"\frac{1".to_string()],
back_runs: vec![vec![error]],
back_units: vec![ContentUnit::Sentence {
text: r"\frac{1".to_string(),
runs: vec![InlineRun {
text: r"\frac{1".to_string(),
math: Some(MathView {
display: false,
svg: None,
error: Some("ParseError: unexpected end".to_string()),
}),
..InlineRun::default()
}],
}],
answer_steps: vec![AnswerStep::Line {
back_from: 0,
back_to: 1,
}],
reshaped: false,
note: Vec::new(),
images: Vec::new(),
images_back: Vec::new(),
citations: Vec::new(),
crumb: None,
};
pin(
"CardDto.math",
&dto,
json!({
"id": "card-9w2c7xkq4m",
"front": "x^2",
"front_runs": [{
"text": "x^2",
"math": {
"display": false,
"svg": "<svg><path d=\"M0 0\"/></svg>"
}
}],
"context": ["$$y^2$$"],
"context_leads": true,
"context_runs": [[{
"text": "y^2",
"math": {
"display": true,
"svg": "<svg><path d=\"M1 1\"/></svg>"
}
}]],
"context_units": [],
"back": ["\\frac{1"],
"back_runs": [[{
"text": "\\frac{1",
"math": {
"display": false,
"error": "ParseError: unexpected end"
}
}]],
"back_units": [{
"kind": "sentence",
"text": "\\frac{1",
"runs": [{
"text": "\\frac{1",
"math": {
"display": false,
"error": "ParseError: unexpected end"
}
}]
}],
"answer_steps": [
{"kind": "line", "back_from": 0, "back_to": 1}
],
"reshaped": false,
"note": [],
"images": [],
"images_back": [],
"citations": [],
"crumb": null
}),
);
}
#[test]
fn carddto_quote_wire_shape() {
let dto = CardDto {
id: Some("card-8k3p1vzr5n".to_string()),
front: "What did Dijkstra say about testing?".to_string(),
front_runs: crate::inline::parse_inline("What did Dijkstra say about testing?"),
front_units: None,
section_context: Vec::new(),
section_context_runs: Vec::new(),
section_context_units: Vec::new(),
context: Vec::new(),
context_leads: false,
context_runs: Vec::new(),
context_units: Vec::new(),
back: vec![
"That it shows the presence of bugs.".to_string(),
"> Program testing can be used to show the presence of bugs,".to_string(),
"> but never to show their absence.".to_string(),
],
back_runs: vec![
crate::inline::parse_inline("That it shows the presence of bugs."),
crate::inline::parse_inline("> Program testing can be used to show the presence of bugs,"),
crate::inline::parse_inline("> but never to show their absence."),
],
back_units: vec![
ContentUnit::Sentence {
text: "That it shows the presence of bugs.".to_string(),
runs: crate::inline::parse_inline("That it shows the presence of bugs."),
},
ContentUnit::Quote {
units: vec![ContentUnit::Sentence {
text: "Program testing can be used to show the presence of bugs, but never to show their absence.".to_string(),
runs: crate::inline::parse_inline("Program testing can be used to show the presence of bugs, but never to show their absence."),
}],
},
],
answer_steps: vec![
AnswerStep::Line {
back_from: 0,
back_to: 1,
},
AnswerStep::Quote {
back_from: 1,
back_to: 3,
units: vec![ContentUnit::Sentence {
text: "Program testing can be used to show the presence of bugs, but never to show their absence.".to_string(),
runs: crate::inline::parse_inline("Program testing can be used to show the presence of bugs, but never to show their absence."),
}],
},
],
reshaped: false,
note: Vec::new(),
images: Vec::new(),
images_back: Vec::new(),
citations: Vec::new(),
crumb: None,
};
pin(
"CardDto.quote",
&dto,
json!({
"id": "card-8k3p1vzr5n",
"front": "What did Dijkstra say about testing?",
"front_runs": [{"text": "What did Dijkstra say about testing?"}],
"context": [],
"context_leads": false,
"context_runs": [],
"context_units": [],
"back": [
"That it shows the presence of bugs.",
"> Program testing can be used to show the presence of bugs,",
"> but never to show their absence."
],
"back_runs": [
[{"text": "That it shows the presence of bugs."}],
[{"text": "> Program testing can be used to show the presence of bugs,"}],
[{"text": "> but never to show their absence."}]
],
"back_units": [
{
"kind": "sentence",
"text": "That it shows the presence of bugs.",
"runs": [{"text": "That it shows the presence of bugs."}]
},
{
"kind": "quote",
"units": [{
"kind": "sentence",
"text": "Program testing can be used to show the presence of bugs, but never to show their absence.",
"runs": [{"text": "Program testing can be used to show the presence of bugs, but never to show their absence."}]
}]
}
],
"answer_steps": [
{"kind": "line", "back_from": 0, "back_to": 1},
{
"kind": "quote",
"back_from": 1,
"back_to": 3,
"units": [{
"kind": "sentence",
"text": "Program testing can be used to show the presence of bugs, but never to show their absence.",
"runs": [{"text": "Program testing can be used to show the presence of bugs, but never to show their absence."}]
}]
}
],
"reshaped": false,
"note": [],
"images": [],
"images_back": [],
"citations": [],
"crumb": null
}),
);
}
#[test]
fn carddto_table_wire_shape() {
let table = ContentUnit::Table {
aligns: vec![
CellAlign::None,
CellAlign::Left,
CellAlign::Center,
CellAlign::Right,
],
header: vec![
crate::inline::parse_inline("a"),
crate::inline::parse_inline("b"),
crate::inline::parse_inline("c"),
crate::inline::parse_inline("d"),
],
rows: vec![vec![
crate::inline::parse_inline("1"),
crate::inline::parse_inline("**2**"),
crate::inline::parse_inline("3"),
Vec::new(),
]],
};
let dto = CardDto {
id: Some("card-9w2c7xkq4m".to_string()),
front: "Name the columns".to_string(),
front_runs: crate::inline::parse_inline("Name the columns"),
front_units: None,
section_context: Vec::new(),
section_context_runs: Vec::new(),
section_context_units: Vec::new(),
context: Vec::new(),
context_leads: false,
context_runs: Vec::new(),
context_units: Vec::new(),
back: vec![
"| a | b | c | d |".to_string(),
"| --- | :-- | :-: | --: |".to_string(),
"| 1 | **2** | 3 |".to_string(),
],
back_runs: vec![
crate::inline::parse_inline("| a | b | c | d |"),
crate::inline::parse_inline("| --- | :-- | :-: | --: |"),
crate::inline::parse_inline("| 1 | **2** | 3 |"),
],
back_units: vec![table.clone()],
answer_steps: vec![AnswerStep::Table {
back_from: 0,
back_to: 3,
units: vec![table],
}],
reshaped: true,
note: Vec::new(),
images: Vec::new(),
images_back: Vec::new(),
citations: Vec::new(),
crumb: None,
};
pin(
"CardDto.table",
&dto,
json!({
"id": "card-9w2c7xkq4m",
"front": "Name the columns",
"front_runs": [{"text": "Name the columns"}],
"context": [],
"context_leads": false,
"context_runs": [],
"context_units": [],
"back": [
"| a | b | c | d |",
"| --- | :-- | :-: | --: |",
"| 1 | **2** | 3 |"
],
"back_runs": [
[{"text": "| a | b | c | d |"}],
[{"text": "| --- | :-- | :-: | --: |"}],
[{"text": "| 1 | "}, {"text": "2", "bold": true}, {"text": " | 3 |"}]
],
"back_units": [{
"kind": "table",
"aligns": ["none", "left", "center", "right"],
"header": [
[{"text": "a"}],
[{"text": "b"}],
[{"text": "c"}],
[{"text": "d"}]
],
"rows": [[
[{"text": "1"}],
[{"text": "2", "bold": true}],
[{"text": "3"}],
[]
]]
}],
"answer_steps": [{
"kind": "table",
"back_from": 0,
"back_to": 3,
"units": [{
"kind": "table",
"aligns": ["none", "left", "center", "right"],
"header": [
[{"text": "a"}],
[{"text": "b"}],
[{"text": "c"}],
[{"text": "d"}]
],
"rows": [[
[{"text": "1"}],
[{"text": "2", "bold": true}],
[{"text": "3"}],
[]
]]
}]
}],
"reshaped": true,
"note": [],
"images": [],
"images_back": [],
"citations": [],
"crumb": null
}),
);
}
#[test]
fn decklistdto_wire_shape() {
let dto = DeckListDto {
workspaces: vec![DeckItemDto {
name: "rustws".to_string(),
selectable: false,
label: "Rust workspace".to_string(),
meta: Some("3/10".to_string()),
state: "workspace",
locked: false,
reviewable: true,
reviewable_recognize: false,
can_recognize: false,
reviewable_recall: true,
reviewable_reconstruct: true,
mastered: false,
is_trace: false,
examable: false,
has_exam: false,
recent: true,
is_workspace: true,
description: Some("learn Rust ownership".to_string()),
members: vec![MemberDto {
name: "rustws/intro.md".to_string(),
selectable: true,
label: "Intro".to_string(),
meta: None,
state: "started",
locked: false,
reviewable: true,
reviewable_recognize: true,
can_recognize: true,
reviewable_recall: true,
reviewable_reconstruct: false,
mastered: false,
is_trace: false,
examable: true,
has_exam: true,
indent: 1,
tree: "└─ ".to_string(),
has_topology: false,
badge_depth: None,
badge_dotted: false,
new_cards: false,
crammable: true,
last_depth: "recall",
}],
path: Some("~/decks".to_string()),
icon: Some("/img/0123456789abcdef".to_string()),
icon_svg: true,
has_topology: true,
badge_depth: Some("recall"),
badge_dotted: true,
new_cards: true,
crammable: true,
last_depth: "recall",
deadline: Some(DeadlineDto {
date: "2026-09-01".to_string(),
days_left: 23,
ready: 2,
total: 5,
}),
}],
recent: vec![DeckItemDto {
name: "vocab.md".to_string(),
selectable: true,
label: "Vocab".to_string(),
meta: None,
state: "new",
locked: false,
reviewable: true,
reviewable_recognize: true,
can_recognize: true,
reviewable_recall: true,
reviewable_reconstruct: false,
mastered: false,
is_trace: false,
examable: false,
has_exam: false,
recent: true,
is_workspace: false,
description: None,
members: Vec::new(),
path: None,
icon: None,
icon_svg: false,
has_topology: false,
badge_depth: None,
badge_dotted: false,
new_cards: true,
crammable: true,
last_depth: "recall",
deadline: None,
}],
folders: Vec::new(),
};
pin(
"DeckListDto",
&dto,
json!({
"workspaces": [{
"name": "rustws",
"selectable": false,
"label": "Rust workspace",
"meta": "3/10",
"state": "workspace",
"locked": false,
"reviewable": true,
"reviewable_recognize": false,
"can_recognize": false,
"reviewable_recall": true,
"reviewable_reconstruct": true,
"mastered": false,
"is_trace": false,
"examable": false,
"has_exam": false,
"recent": true,
"is_workspace": true,
"description": "learn Rust ownership",
"members": [{
"name": "rustws/intro.md",
"selectable": true,
"label": "Intro",
"meta": null,
"state": "started",
"locked": false,
"reviewable": true,
"reviewable_recognize": true,
"can_recognize": true,
"reviewable_recall": true,
"reviewable_reconstruct": false,
"mastered": false,
"is_trace": false,
"examable": true,
"has_exam": true,
"indent": 1,
"tree": "└─ ",
"has_topology": false,
"badge_depth": null,
"badge_dotted": false,
"new_cards": false,
"crammable": true,
"last_depth": "recall"
}],
"path": "~/decks",
"icon": "/img/0123456789abcdef",
"icon_svg": true,
"has_topology": true,
"badge_depth": "recall",
"badge_dotted": true,
"new_cards": true,
"crammable": true,
"last_depth": "recall",
"deadline": {
"date": "2026-09-01",
"days_left": 23,
"ready": 2,
"total": 5
}
}],
"recent": [{
"name": "vocab.md",
"selectable": true,
"label": "Vocab",
"meta": null,
"state": "new",
"locked": false,
"reviewable": true,
"reviewable_recognize": true,
"can_recognize": true,
"reviewable_recall": true,
"reviewable_reconstruct": false,
"mastered": false,
"is_trace": false,
"examable": false,
"has_exam": false,
"recent": true,
"is_workspace": false,
"description": null,
"members": [],
"path": null,
"icon": null,
"icon_svg": false,
"has_topology": false,
"badge_depth": null,
"badge_dotted": false,
"new_cards": true,
"crammable": true,
"last_depth": "recall",
"deadline": null
}],
"folders": []
}),
);
}
#[test]
fn deckdrawerdto_wire_shape() {
let dto = DeckDrawerDto {
description: Some("A short intro from the frontmatter.".to_string()),
heatmap: vec![
Cell {
tier: CardTier::Unseen,
locked: false,
},
Cell {
tier: CardTier::Seen,
locked: false,
},
Cell {
tier: CardTier::Learning,
locked: false,
},
Cell {
tier: CardTier::LearnedStrong,
locked: false,
},
Cell {
tier: CardTier::LearnedFading,
locked: false,
},
Cell {
tier: CardTier::LearnedWeak,
locked: false,
},
Cell {
tier: CardTier::Retired,
locked: false,
},
],
topologies: vec![TopologyInfoDto {
name: "north-south".to_string(),
principle: "north to south".to_string(),
regions: vec![RegionInfoDto {
name: "north".to_string(),
cells: vec![
Cell {
tier: CardTier::Seen,
locked: false,
},
Cell {
tier: CardTier::LearnedFading,
locked: false,
},
],
}],
}],
total: 7,
seen: 6,
graduated: 4,
retired: 1,
};
pin(
"DeckDrawerDto",
&dto,
json!({
"description": "A short intro from the frontmatter.",
"heatmap": [
{"tier": "unseen", "locked": false},
{"tier": "seen", "locked": false},
{"tier": "learning", "locked": false},
{"tier": "learned-strong", "locked": false},
{"tier": "learned-fading", "locked": false},
{"tier": "learned-weak", "locked": false},
{"tier": "retired", "locked": false}
],
"topologies": [{
"name": "north-south",
"principle": "north to south",
"regions": [{"name": "north", "cells": [
{"tier": "seen", "locked": false},
{"tier": "learned-fading", "locked": false}
]}]
}],
"total": 7,
"seen": 6,
"graduated": 4,
"retired": 1
}),
);
}
#[test]
fn browsedto_wire_shape() {
let dto = BrowseDto {
phase: "browse",
label: "rust.md".to_string(),
cards: vec![CardDto {
id: Some("card-9w2c7xkq4m".to_string()),
front: "q".to_string(),
front_runs: crate::inline::parse_inline("q"),
front_units: None,
section_context: Vec::new(),
section_context_runs: Vec::new(),
section_context_units: Vec::new(),
context: Vec::new(),
context_leads: false,
context_runs: Vec::new(),
context_units: Vec::new(),
back: vec!["a".to_string()],
back_runs: vec![crate::inline::parse_inline("a")],
back_units: vec![ContentUnit::Sentence {
text: "a".to_string(),
runs: crate::inline::parse_inline("a"),
}],
answer_steps: vec![AnswerStep::Line {
back_from: 0,
back_to: 1,
}],
reshaped: false,
note: Vec::new(),
images: Vec::new(),
images_back: Vec::new(),
citations: Vec::new(),
crumb: None,
}],
};
pin(
"BrowseDto",
&dto,
json!({
"phase": "browse",
"label": "rust.md",
"cards": [{
"id": "card-9w2c7xkq4m",
"front": "q",
"front_runs": [{"text": "q"}],
"context": [],
"context_leads": false,
"context_runs": [],
"context_units": [],
"back": ["a"],
"back_runs": [[{"text": "a"}]],
"back_units": [{
"kind": "sentence",
"text": "a",
"runs": [{"text": "a"}]
}],
"answer_steps": [
{"kind": "line", "back_from": 0, "back_to": 1}
],
"reshaped": false,
"note": [],
"images": [],
"images_back": [],
"citations": [],
"crumb": null
}]
}),
);
}
#[test]
fn multichoosefeedbackdto_wire_shape() {
let feedback = crate::review::MultiChoiceFeedback {
chosen: vec![0, 2],
correct: vec![0, 1],
passed: false,
};
pin(
"MultiChooseFeedbackDto",
&feedback,
json!({"chosen": [0, 2], "correct": [0, 1], "passed": false}),
);
}
#[test]
fn choosefeedbackdto_wire_shape() {
let feedback = crate::review::ChoiceFeedback {
chosen: 2,
correct: 1,
passed: false,
};
pin(
"ChooseFeedbackDto",
&feedback,
json!({"chosen": 2, "correct": 1, "passed": false}),
);
}
#[test]
fn checkfeedbackdto_wire_shape() {
let feedback = crate::review::CheckFeedback {
results: vec![TypedResult {
input: "pars".to_string(),
expected: "Paris".to_string(),
passed: false,
}],
passed: false,
};
pin(
"CheckFeedbackDto",
&feedback,
json!({
"results": [{"input": "pars", "expected": "Paris", "passed": false}],
"passed": false
}),
);
}
#[test]
fn askdto_populated_wire_shape() {
let dto = AskDto {
transcript: vec![ExchangeDto {
q: "why one owner?".to_string(),
a: "so drops are deterministic".to_string(),
}],
thinking: true,
status: Some("asking claude".to_string()),
error: None,
draft: None,
};
pin(
"AskDto.populated",
&dto,
json!({
"transcript": [{"q": "why one owner?", "a": "so drops are deterministic"}],
"thinking": true,
"status": "asking claude",
"error": null,
"draft": null
}),
);
}
#[test]
fn askdto_empty_wire_shape() {
let dto = AskDto {
transcript: Vec::new(),
thinking: false,
status: None,
error: None,
draft: None,
};
pin(
"AskDto.empty",
&dto,
json!({
"transcript": [],
"thinking": false,
"status": null,
"error": null,
"draft": null
}),
);
}
#[test]
fn askdto_with_draft_wire_shape() {
let dto = AskDto {
transcript: vec![ExchangeDto {
q: "why one owner?".to_string(),
a: "so drops are deterministic".to_string(),
}],
thinking: false,
status: None,
error: None,
draft: Some(DraftCardDto {
front: "Why does Rust use one owner per value?".to_string(),
back: vec!["so drops are deterministic".to_string()],
}),
};
pin(
"AskDto.with_draft",
&dto,
json!({
"transcript": [{"q": "why one owner?", "a": "so drops are deterministic"}],
"thinking": false,
"status": null,
"error": null,
"draft": {
"front": "Why does Rust use one owner per value?",
"back": ["so drops are deterministic"]
}
}),
);
}
#[test]
fn createcardresp_wire_shape() {
let dto = CreateCardResp {
id: "card-9w2c7xkq4m".to_string(),
};
pin("CreateCardResp", &dto, json!({"id": "card-9w2c7xkq4m"}));
}
#[test]
fn askinfodto_and_versiondto_wire_shape() {
let info = AskInfoDto {
backend: "claude",
model: "default".to_string(),
effort: "default".to_string(),
};
pin(
"AskInfoDto",
&info,
json!({"backend": "claude", "model": "default", "effort": "default"}),
);
let version = VersionDto {
version: env!("CARGO_PKG_VERSION"),
};
pin(
"VersionDto",
&version,
json!({"version": env!("CARGO_PKG_VERSION")}),
);
}
#[test]
fn doctordto_wire_shape() {
let dto = DoctorDto {
rows: vec![
DoctorRowDto {
name: "config",
status: "ok",
detail: "~/.config/alix/config.toml parses".to_string(),
remedy: None,
},
DoctorRowDto {
name: "share",
status: "warn",
detail: "`wormhole` not found on PATH".to_string(),
remedy: Some("pipx install magic-wormhole".to_string()),
},
],
};
pin(
"DoctorDto",
&dto,
json!({
"rows": [
{"name": "config", "status": "ok",
"detail": "~/.config/alix/config.toml parses", "remedy": null},
{"name": "share", "status": "warn",
"detail": "`wormhole` not found on PATH",
"remedy": "pipx install magic-wormhole"}
]
}),
);
}
#[test]
fn pairdto_wire_shape() {
let dto = PairDto {
url: "http://127.0.0.1:7777/".to_string(),
svg: None,
lan: false,
};
pin(
"PairDto",
&dto,
json!({"url": "http://127.0.0.1:7777/", "svg": null, "lan": false}),
);
}
#[test]
fn resetdto_wire_shape() {
let dto = ResetDto {
deck: "rust.md".to_string(),
cards_cleared: 17,
};
pin(
"ResetDto",
&dto,
json!({"deck": "rust.md", "cards_cleared": 17}),
);
}
#[test]
fn removalpreviewdto_wire_shape() {
let dto = RemovalPreviewDto {
target: "rust".to_string(),
kind: "workspace",
decks: 2,
cards_with_progress: 17,
earliest_review_ms: Some(1_700_000_000_000),
files: vec![
"decks/basics.md".to_string(),
"progress/deck-basics.json".to_string(),
"alix.toml".to_string(),
],
directories: vec!["assets".to_string()],
dependents: vec!["advanced.md".to_string()],
};
pin(
"RemovalPreviewDto",
&dto,
json!({
"target": "rust",
"kind": "workspace",
"decks": 2,
"cards_with_progress": 17,
"earliest_review_ms": 1_700_000_000_000_u64,
"files": ["decks/basics.md", "progress/deck-basics.json", "alix.toml"],
"directories": ["assets"],
"dependents": ["advanced.md"]
}),
);
}
#[test]
fn removaldto_wire_shape() {
let dto = RemovalDto {
target: "rust".to_string(),
kind: "workspace",
removed: vec!["decks/basics.md".to_string(), "alix.toml".to_string()],
decks_removed: 2,
directory_removed: false,
dependents: vec!["advanced.md".to_string()],
};
pin(
"RemovalDto",
&dto,
json!({
"target": "rust",
"kind": "workspace",
"removed": ["decks/basics.md", "alix.toml"],
"decks_removed": 2,
"directory_removed": false,
"dependents": ["advanced.md"]
}),
);
}
#[test]
fn removalfailuredto_wire_shape() {
let dto = RemovalFailureDto {
target: "rust".to_string(),
error: "removal incomplete",
completed: vec!["decks/basics.md".to_string()],
failed: "progress/deck-basics.json".to_string(),
recovery: "Run alix doctor to inspect and repair the remaining artifacts.",
};
pin(
"RemovalFailureDto",
&dto,
json!({
"target": "rust",
"error": "removal incomplete",
"completed": ["decks/basics.md"],
"failed": "progress/deck-basics.json",
"recovery": "Run alix doctor to inspect and repair the remaining artifacts."
}),
);
}
#[test]
fn importdto_wire_shape() {
let dto = ImportDto {
deck: "kanji.md".to_string(),
cards: 40,
};
pin("ImportDto", &dto, json!({"deck": "kanji.md", "cards": 40}));
}
#[test]
fn augmentdto_wire_shape() {
let dto = AugmentDto {
deck: "rust.md".to_string(),
cards: 12,
rows: vec![AugmentRowDto {
kind: "choices",
label: "choice distractors",
covered: 4,
eligible: 12,
items: vec!["north-south".to_string()],
busy: true,
}],
busy: Some("choices"),
elapsed: Some(3),
error: None,
queued: vec!["notes"],
done: vec!["format"],
failed: vec![FailedTargetDto {
target: "topology",
error: "the model did not return valid JSON".to_string(),
}],
};
pin(
"AugmentDto",
&dto,
json!({
"deck": "rust.md",
"cards": 12,
"rows": [{
"kind": "choices",
"label": "choice distractors",
"covered": 4,
"eligible": 12,
"items": ["north-south"],
"busy": true
}],
"busy": "choices",
"elapsed": 3,
"error": null,
"queued": ["notes"],
"done": ["format"],
"failed": [{
"target": "topology",
"error": "the model did not return valid JSON"
}]
}),
);
}
#[test]
fn generatedto_done_wire_shape() {
let dto = GenerateDto {
phase: "done",
deck: Some("rust-ownership.md".to_string()),
cards: Some(12),
elapsed: Some(41),
error: None,
};
pin(
"GenerateDto",
&dto,
json!({"phase": "done", "deck": "rust-ownership.md", "cards": 12,
"elapsed": 41, "error": null}),
);
}
#[test]
fn sharedto_code_phase_wire_shape() {
let dto = ShareDto {
phase: "code",
code: Some("7-alpha-bravo".to_string()),
elapsed: Some(3),
error: None,
};
pin(
"ShareDto",
&dto,
json!({"phase": "code", "code": "7-alpha-bravo", "elapsed": 3, "error": null}),
);
}
#[test]
fn receivedto_done_wire_shape() {
let dto = ReceiveDto {
phase: "done",
landed: Some("rust-decks".to_string()),
stripped: vec!["progress".to_string()],
elapsed: Some(9),
error: None,
};
pin(
"ReceiveDto",
&dto,
json!({"phase": "done", "landed": "rust-decks",
"stripped": ["progress"], "elapsed": 9, "error": null}),
);
}
#[test]
fn remoteaskdto_thinking_wire_shape() {
let dto = RemoteAskDto {
thinking: true,
answer: None,
draft: None,
note: None,
error: None,
elapsed: Some(3),
};
pin(
"RemoteAskDto.thinking",
&dto,
json!({
"thinking": true,
"answer": null,
"draft": null,
"note": null,
"error": null,
"elapsed": 3
}),
);
}
#[test]
fn remoteaskdto_done_wire_shape() {
let dto = RemoteAskDto {
thinking: false,
answer: Some("so drops are deterministic".to_string()),
draft: Some(DraftCardDto {
front: "Why does Rust use one owner per value?".to_string(),
back: vec![
"so drops are deterministic".to_string(),
"no GC needed".to_string(),
],
}),
note: None,
error: None,
elapsed: None,
};
pin(
"RemoteAskDto.done",
&dto,
json!({
"thinking": false,
"answer": "so drops are deterministic",
"draft": {
"front": "Why does Rust use one owner per value?",
"back": ["so drops are deterministic", "no GC needed"]
},
"note": null,
"error": null,
"elapsed": null
}),
);
}
#[test]
fn remoteaskdto_note_wire_shape() {
let dto = RemoteAskDto {
thinking: false,
answer: None,
draft: None,
note: Some(vec![
"ownership drops values deterministically".to_string(),
"no GC needed".to_string(),
]),
error: None,
elapsed: None,
};
pin(
"RemoteAskDto.note",
&dto,
json!({
"thinking": false,
"answer": null,
"draft": null,
"note": ["ownership drops values deterministically", "no GC needed"],
"error": null,
"elapsed": null
}),
);
}
#[test]
fn remoteexamdto_idle_wire_shape() {
let dto = RemoteExamDto {
phase: "idle",
deck: String::new(),
strictness: "balanced",
questions: Vec::new(),
passed: None,
grades: Vec::new(),
gaps: Vec::new(),
can_remediate: false,
cards: None,
is_trace: false,
thinking: false,
elapsed: None,
error: None,
};
pin(
"RemoteExamDto.idle",
&dto,
json!({
"phase": "idle",
"deck": "",
"strictness": "balanced",
"questions": [],
"passed": null,
"grades": [],
"gaps": [],
"can_remediate": false,
"cards": null,
"is_trace": false,
"thinking": false,
"elapsed": null,
"error": null
}),
);
}
#[test]
fn remoteexamdto_answering_wire_shape() {
let dto = RemoteExamDto {
phase: "answering",
deck: "rust.md".to_string(),
strictness: "balanced",
questions: vec![
"Why does Rust use ownership?".to_string(),
"What is borrowing?".to_string(),
],
passed: None,
grades: Vec::new(),
gaps: Vec::new(),
can_remediate: false,
cards: None,
is_trace: false,
thinking: false,
elapsed: None,
error: None,
};
pin(
"RemoteExamDto.answering",
&dto,
json!({
"phase": "answering",
"deck": "rust.md",
"strictness": "balanced",
"questions": ["Why does Rust use ownership?", "What is borrowing?"],
"passed": null,
"grades": [],
"gaps": [],
"can_remediate": false,
"cards": null,
"is_trace": false,
"thinking": false,
"elapsed": null,
"error": null
}),
);
}
#[test]
fn remoteexamdto_results_wire_shape() {
let dto = RemoteExamDto {
phase: "results",
deck: "rust.md".to_string(),
strictness: "balanced",
questions: vec!["Why does Rust use ownership?".to_string()],
passed: Some(false),
grades: vec![ExamGradeDto {
question: "Why does Rust use ownership?".to_string(),
points: vec!["memory safety without a GC".to_string()],
answer: "it has a garbage collector".to_string(),
verdict: "FAIL",
feedback: "Rust has no GC".to_string(),
missed: vec!["memory safety without a GC".to_string()],
}],
gaps: vec!["ownership and the GC-free memory model".to_string()],
can_remediate: true,
cards: None,
is_trace: false,
thinking: false,
elapsed: None,
error: None,
};
pin(
"RemoteExamDto.results",
&dto,
json!({
"phase": "results",
"deck": "rust.md",
"strictness": "balanced",
"questions": ["Why does Rust use ownership?"],
"passed": false,
"grades": [{
"question": "Why does Rust use ownership?",
"points": ["memory safety without a GC"],
"answer": "it has a garbage collector",
"verdict": "FAIL",
"feedback": "Rust has no GC",
"missed": ["memory safety without a GC"]
}],
"gaps": ["ownership and the GC-free memory model"],
"can_remediate": true,
"cards": null,
"is_trace": false,
"thinking": false,
"elapsed": null,
"error": null
}),
);
}
#[test]
fn remoteexamdto_remediated_wire_shape() {
let dto = RemoteExamDto {
phase: "remediated",
deck: "rust.md".to_string(),
strictness: "balanced",
questions: vec!["Why does Rust use ownership?".to_string()],
passed: Some(false),
grades: vec![ExamGradeDto {
question: "Why does Rust use ownership?".to_string(),
points: vec!["memory safety without a GC".to_string()],
answer: "it has a garbage collector".to_string(),
verdict: "FAIL",
feedback: "Rust has no GC".to_string(),
missed: vec!["memory safety without a GC".to_string()],
}],
gaps: vec!["ownership and the GC-free memory model".to_string()],
can_remediate: false,
cards: Some(
"## Why does Rust use ownership?\nso drops are deterministic, no GC needed".to_string(),
),
is_trace: false,
thinking: false,
elapsed: None,
error: None,
};
pin(
"RemoteExamDto.remediated",
&dto,
json!({
"phase": "remediated",
"deck": "rust.md",
"strictness": "balanced",
"questions": ["Why does Rust use ownership?"],
"passed": false,
"grades": [{
"question": "Why does Rust use ownership?",
"points": ["memory safety without a GC"],
"answer": "it has a garbage collector",
"verdict": "FAIL",
"feedback": "Rust has no GC",
"missed": ["memory safety without a GC"]
}],
"gaps": ["ownership and the GC-free memory model"],
"can_remediate": false,
"cards": "## Why does Rust use ownership?\nso drops are deterministic, no GC needed",
"is_trace": false,
"thinking": false,
"elapsed": null,
"error": null
}),
);
}
#[test]
fn remoteexamdto_trace_results_wire_shape() {
let dto = RemoteExamDto {
phase: "results",
deck: "trace.md".to_string(),
strictness: "balanced",
questions: vec!["how it works".to_string()],
passed: Some(false),
grades: vec![ExamGradeDto {
question: "how it works".to_string(),
points: vec!["it reads the first line".to_string()],
answer: "it reads the file".to_string(),
verdict: "FAIL",
feedback: "missed the second hop".to_string(),
missed: vec!["it reads the second line".to_string()],
}],
gaps: vec!["it reads the second line".to_string()],
can_remediate: false,
cards: None,
is_trace: true,
thinking: false,
elapsed: None,
error: None,
};
pin(
"RemoteExamDto.trace_results",
&dto,
json!({
"phase": "results",
"deck": "trace.md",
"strictness": "balanced",
"questions": ["how it works"],
"passed": false,
"grades": [{
"question": "how it works",
"points": ["it reads the first line"],
"answer": "it reads the file",
"verdict": "FAIL",
"feedback": "missed the second hop",
"missed": ["it reads the second line"]
}],
"gaps": ["it reads the second line"],
"can_remediate": false,
"cards": null,
"is_trace": true,
"thinking": false,
"elapsed": null,
"error": null
}),
);
}
#[test]
fn remotegeneratedto_generating_wire_shape() {
let dto = RemoteGenerateDto {
phase: "generating",
deck: None,
filename: None,
cards: None,
elapsed: Some(4),
error: None,
};
pin(
"RemoteGenerateDto.generating",
&dto,
json!({
"phase": "generating",
"deck": null,
"filename": null,
"cards": null,
"elapsed": 4,
"error": null
}),
);
}
#[test]
fn remotegeneratedto_done_wire_shape() {
let dto = RemoteGenerateDto {
phase: "done",
deck: Some("---\nlink: https://example.org\n---\n## Q\nA\n".to_string()),
filename: Some("example-org.md".to_string()),
cards: Some(1),
elapsed: None,
error: None,
};
pin(
"RemoteGenerateDto.done",
&dto,
json!({
"phase": "done",
"deck": "---\nlink: https://example.org\n---\n## Q\nA\n",
"filename": "example-org.md",
"cards": 1,
"elapsed": null,
"error": null
}),
);
}
#[test]
fn remotegeneratedto_error_wire_shape() {
let dto = RemoteGenerateDto {
phase: "error",
deck: None,
filename: None,
cards: None,
elapsed: None,
error: Some("the model returned no deck content".to_string()),
};
pin(
"RemoteGenerateDto.error",
&dto,
json!({
"phase": "error",
"deck": null,
"filename": null,
"cards": null,
"elapsed": null,
"error": "the model returned no deck content"
}),
);
}
#[test]
fn carddto_diagram_wire_shape() {
let view = crate::review::CardView::project(
&diagram_card(),
&mut crate::inline::DisplayProjector::default(),
);
let dto = super::dto::card_dto(view, Some("card-9w2c7xkq4m".to_string()));
let ContentUnit::Diagram { src, .. } = &dto.back_units[0] else {
panic!(
"the fence slot holds the diagram unit: {:?}",
dto.back_units
);
};
let src = src.clone();
assert!(src.starts_with("/img/"), "{src}");
pin(
"CardDto.diagram",
&dto.back_units,
serde_json::json!([
{
"kind": "diagram",
"src": src,
"width": 188,
"height": 114,
"alt": "flowchart LR\n A-->B"
}
]),
);
}
#[test]
fn carddto_context_diagram_wire_shape() {
let source = "flowchart LR\n A-->B";
let mut card = diagram_card();
card.context = vec![
"```mermaid".to_string(),
"flowchart LR".to_string(),
" A-->B".to_string(),
"```".to_string(),
];
card.answer_fences.push(crate::card::AnswerFence {
fingerprint: crate::diagram::fingerprint(source),
interior: std::sync::Arc::from(source),
spans: Vec::new(),
});
let view =
crate::review::CardView::project(&card, &mut crate::inline::DisplayProjector::default());
let dto = super::dto::card_dto(view, Some("card-9w2c7xkq4m".to_string()));
let ContentUnit::Diagram { src, .. } = &dto.context_units[0] else {
panic!(
"the context fence slot holds the diagram unit: {:?}",
dto.context_units
);
};
let src = src.clone();
assert!(src.starts_with("/img/"), "{src}");
pin(
"CardDto.context_units",
&dto.context_units,
serde_json::json!([
{
"kind": "diagram",
"src": src,
"width": 188,
"height": 114,
"alt": "flowchart LR\n A-->B"
}
]),
);
}
#[test]
fn carddto_masked_context_diagram_wire_shape() {
let text = concat!(
"## the request path\n",
"```mermaid\n",
"flowchart LR\n",
" Cache[store] --> B[Cache]\n",
"```\n",
"<!-- blank: span hidden=\"Cache\" occurrence=2 -->\n",
);
let mut cards = crate::parser::parse_str("deck.md", text).unwrap();
let card = &mut cards[0];
let interior = "flowchart LR\n Cache[store] --> B[Cache]";
let store = interior.find("store").unwrap() as u32;
let cache = interior.rfind("Cache").unwrap() as u32;
let label =
|id: &str, text: &str, start: u32, end: u32, y: u32| crate::diagram::GeometryLabel {
id: id.into(),
text: text.into(),
source: crate::diagram::LabelSource::Range { start, end },
bounds: crate::diagram::PixelBox {
x: 10,
y,
width: 100,
height: 40,
},
};
card.resolved_diagrams.push(crate::card::ResolvedDiagram {
fingerprint: crate::diagram::fingerprint(interior),
png: std::path::PathBuf::from("/ws/assets/deck-x/sha256-aa.png"),
geometry: crate::diagram::DiagramGeometry {
image: "sha256-aa.png".to_string(),
image_width: 376,
image_height: 228,
logical_width: 188,
logical_height: 114,
labels: vec![
label("Cache", "store", store, store + 5, 10),
label("B", "Cache", cache, cache + 5, 50),
],
},
});
let view =
crate::review::CardView::project(card, &mut crate::inline::DisplayProjector::default());
let dto = super::dto::card_dto(view, Some("card-9w2c7xkq4m".to_string()));
let ContentUnit::Diagram { src, .. } = &dto.context_units[0] else {
panic!("the masked fence projects: {:?}", dto.context_units);
};
let src = src.clone();
assert!(src.starts_with("/img/"), "{src}");
pin(
"CardDto.context_units.masked",
&dto.context_units,
serde_json::json!([
{
"kind": "diagram",
"src": src,
"width": 188,
"height": 114,
"alt": "diagram labels: store, …",
"regions": [
{
"role": "asked",
"reveal_on_answer": true,
"x": 10.0,
"y": 50.0,
"width": 100.0,
"height": 40.0,
"unit": "px"
}
],
"revealed_alt": "diagram labels: store, Cache"
}
]),
);
}
fn diagram_card() -> crate::card::Card {
let source = "flowchart LR\n A-->B";
let mut card = crate::card::Card::plain(
std::sync::Arc::from("deck.md"),
"q".to_string(),
vec![
"```mermaid".to_string(),
source.split('\n').next().unwrap().to_string(),
source.split('\n').nth(1).unwrap().to_string(),
"```".to_string(),
],
Vec::new(),
1,
);
card.resolved_diagrams.push(crate::card::ResolvedDiagram {
fingerprint: crate::diagram::fingerprint(source),
png: std::path::PathBuf::from("/ws/assets/deck-x/sha256-aa.png"),
geometry: crate::diagram::DiagramGeometry {
image: "sha256-aa.png".to_string(),
image_width: 376,
image_height: 228,
logical_width: 188,
logical_height: 114,
labels: Vec::new(),
},
});
card
}
#[test]
fn every_carddto_carries_answer_steps_covering_its_back_exactly_once() {
let backs = [
"plain",
"a\n> q",
"> q\n> r\nb",
"a\n> q\n> r\nb",
"a\n> q\nb\n> r",
"| a | b |\n| --- | --- |\n| 1 | 2 |",
"lead\n| a | b |\n| --- | --- |\n| 1 | 2 |\ntrail",
"```text\n> q\n```",
"left > right\n<!-- blank: span hidden=\">\" boundary=char b:a1b2c3 -->",
];
for back in backs {
let cards = crate::parser::parse_str("t.md", &format!("## q\n{back}\n"))
.unwrap_or_else(|e| panic!("{back:?} parses: {e}"));
let dto = card_dto(crate::review::CardView::from(&cards[0]), None);
let value = serde_json::to_value(&dto).unwrap();
let steps = value["answer_steps"]
.as_array()
.unwrap_or_else(|| panic!("{back:?}: answer_steps is always present, never elided"))
.clone();
assert!(
!steps.is_empty(),
"{back:?}: an answer with lines has at least one step"
);
let mut next = 0;
for step in &steps {
let kind = step["kind"].as_str().expect("a step names its kind");
assert!(
matches!(kind, "line" | "quote" | "table"),
"{back:?}: unknown step kind {kind:?}"
);
let from = step["back_from"].as_u64().expect("a step spans the back");
let to = step["back_to"].as_u64().expect("a step spans the back");
assert_eq!(next, from, "{back:?}: {kind} starts where the last ended");
assert!(from < to, "{back:?}: {kind} spans at least one line");
assert_eq!(
kind != "line",
step.get("units").is_some(),
"{back:?}: a block step carries its units and a line never does"
);
next = to;
}
assert_eq!(
dto.back.len() as u64,
next,
"{back:?}: the steps reach the end of the back, leaving no line unwalked"
);
}
}
#[test]
fn every_badge_reaches_the_wire_as_its_own_lowercase_name() {
let rows = [
(crate::card::Badge::Note, "note"),
(crate::card::Badge::Tip, "tip"),
(crate::card::Badge::Important, "important"),
(crate::card::Badge::Warning, "warning"),
(crate::card::Badge::Caution, "caution"),
];
for (badge, spelling) in rows {
let deck = format!("## q\na\n\n> [!{}]\n> body\n", spelling.to_uppercase());
let cards = crate::parser::parse_str("t.md", &deck)
.unwrap_or_else(|e| panic!("{spelling}: the deck parses: {e}"));
let dto = card_dto(crate::review::CardView::from(&cards[0]), None);
let value = serde_json::to_value(&dto).unwrap();
assert_eq!(
value["note"][0]["badge"],
serde_json::json!(spelling),
"{badge:?} travels as {spelling:?}; the adult and kids stylesheets \
select on that exact string"
);
}
}