open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-runtime/tests/chat.rs
Transaction Science 29dff259e4 jouleclaw Phase 3: backends (reference + apple) + runtime
Three crates ported. All 440 tests pass across Phase 1+2+3.

jouleclaw-backend-reference
  Ported from pattern-lang/joule-backend-reference (~3556 LOC, 17 files).
  Pure-Rust scalar kernels — the determinism oracle every other backend
  validates against. Activation, conv1d, elementwise, lookup (incl. bit
  and ternary), matmul (incl. bit, q8_0, stq1_0, ternary), norm, rope,
  sample, shape, softmax.

jouleclaw-backend-apple
  Ported from pattern-lang/joule-backend-apple (~1116 LOC, 4 files).
  Accelerate (cblas_sgemm via direct FFI; build.rs links the
  framework), Metal compute kernels, MPSMatrixMultiplication. On M5,
  MPS matmul auto-routes through the GPU's Neural Accelerators
  (Tensor APIs). Two Rust-2024 extern-block fixes (unsafe extern "C")
  in accelerate.rs and mps_matmul.rs.

jouleclaw-runtime
  Ported from pattern-lang/joule-runtime (~10218 LOC, 10 files).
  The runtime kernel: chat, embed, generate, streaming, compile,
  execute, validate, plus the speculative-decoding primitives
  (drafter, PLD = prompt lookup decoding). Wires the cascade
  (jouleclaw-cascade), the topology (jouleclaw-topology), the format
  loader (jouleclaw-loader-gguf), and both backends together.

  One Rust-2024 binding-mode fix in compile.rs (|(_, &d)| d == 0 →
  |(_, d)| **d == 0). Three types in jouleclaw-loader-gguf
  (KvQuant, KvSnapshot, ShortConvStateSnapshot) gained
  serde::{Serialize, Deserialize} derives so the runtime's
  ConversationCheckpoint can be serialized; serde added to the
  loader-gguf Cargo.toml.

  Day-one model coverage inherits everything mistral.rs's parent
  loaders support — Llama (incl. Llama 4), Mistral (incl. Small 4),
  Qwen (3, 3-VL, 3 MoE), Gemma (incl. Gemma 4 with MTP drafter), Phi
  (incl. Phi-4 MM), DeepSeek V3, Granite 4 — through this runtime's
  PLD + drafter paths.

  31 integration test files cover: alias-energy, blocks, boot
  validation, causal, chat, chunked prefill, concat-and-offset, embed,
  end-to-end model, generation (incl. inplace), GQA, inplace-alias
  proof, integration, KV cache (incl. inplace), multihead, prefix
  cache, real Bonsai (incl. streaming), real Liquid LFM2, real Tencent
  HYMT, real TinyLlama, repeat, rope-and-scaling, sampling features,
  scatter (incl. inplace), shape, slice (incl. slice-attention),
  streaming, text-to-logits, validation.

Phase 4 next: the model tiers — gemma4, liquid (CfC SSM), prism (1-bit
ternary BitNet), mrl (Matryoshka embeddings), lmm (vision/text/audio),
ebm (energy-based constraint satisfaction). All depend on the runtime
+ backends shipped in this commit.

Deferred to later phases: HAL from efficient-genai + the discrete-GPU
backends (CUDA, ROCm, Vulkan) — these are needed for Phase 5
omni-modal (diffusion, stable-diffusion, gaussian3d, video), not for
Phase 4 text/code/SSM model tiers which run on Apple + reference.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 13:09:59 -04:00

203 lines
7.2 KiB
Rust

//! Tests for chat templates: unit tests for each template's format, plus
//! a GGUF round-trip showing detect_from_model() works.
use jouleclaw_loader_gguf::read_gguf;
use jouleclaw_loader_gguf::synthetic::{synthesize_llama_gguf, SyntheticConfig};
use jouleclaw_runtime::{ChatMessage, ChatRole, ChatTemplate};
use std::io::Cursor;
fn msgs() -> Vec<ChatMessage> {
vec![
ChatMessage::system("You are a helpful assistant."),
ChatMessage::user("What's 2+2?"),
]
}
#[test]
fn chatml_format_basic() {
let out = ChatTemplate::ChatML.format(&msgs());
assert!(out.contains("<|im_start|>system\nYou are a helpful assistant.<|im_end|>"));
assert!(out.contains("<|im_start|>user\nWhat's 2+2?<|im_end|>"));
assert!(out.ends_with("<|im_start|>assistant\n"),
"ChatML should end with the open assistant turn; got:\n{:?}", out);
}
#[test]
fn llama3_format_has_correct_markers() {
let out = ChatTemplate::Llama3.format(&msgs());
assert!(out.starts_with("<|begin_of_text|>"));
assert!(out.contains("<|start_header_id|>system<|end_header_id|>\n\nYou are a helpful assistant.<|eot_id|>"));
assert!(out.contains("<|start_header_id|>user<|end_header_id|>\n\nWhat's 2+2?<|eot_id|>"));
assert!(out.ends_with("<|start_header_id|>assistant<|end_header_id|>\n\n"));
}
#[test]
fn llama2_format_has_sys_block() {
let out = ChatTemplate::Llama2.format(&msgs());
assert!(out.contains("<<SYS>>\nYou are a helpful assistant.\n<</SYS>>"));
assert!(out.contains("[INST]"));
assert!(out.contains("[/INST]"));
assert!(out.contains("What's 2+2?"));
}
#[test]
fn mistral_format_inlines_system_into_first_user() {
let out = ChatTemplate::Mistral.format(&msgs());
assert!(out.starts_with("<s>[INST] "));
assert!(out.contains("You are a helpful assistant.\n\nWhat's 2+2?"),
"Mistral inlines system into the first user message; got:\n{:?}", out);
assert!(out.contains("[/INST]"));
}
#[test]
fn phi3_format_uses_role_tags() {
let out = ChatTemplate::Phi3.format(&msgs());
assert!(out.contains("<|system|>\nYou are a helpful assistant.<|end|>"));
assert!(out.contains("<|user|>\nWhat's 2+2?<|end|>"));
assert!(out.ends_with("<|assistant|>\n"));
}
#[test]
fn plain_format_is_readable() {
let out = ChatTemplate::Plain.format(&msgs());
assert!(out.contains("System: You are a helpful assistant."));
assert!(out.contains("User: What's 2+2?"));
assert!(out.ends_with("Assistant: "));
}
#[test]
fn multi_turn_chatml_round_trip() {
let conv = vec![
ChatMessage::system("Be brief."),
ChatMessage::user("Hi."),
ChatMessage::assistant("Hello!"),
ChatMessage::user("Who are you?"),
];
let out = ChatTemplate::ChatML.format(&conv);
// All four turns present, plus open assistant.
assert!(out.contains("<|im_start|>system\nBe brief.<|im_end|>"));
assert!(out.contains("<|im_start|>user\nHi.<|im_end|>"));
assert!(out.contains("<|im_start|>assistant\nHello!<|im_end|>"));
assert!(out.contains("<|im_start|>user\nWho are you?<|im_end|>"));
assert!(out.ends_with("<|im_start|>assistant\n"));
}
fn build_model_with_template(template: Option<&str>) -> jouleclaw_loader_gguf::GgufModel {
let mut vocab = vec![
("<unk>".to_string(), 0.0),
("<s>".to_string(), 0.0),
("</s>".to_string(), 0.0),
];
for c in 'a'..='z' { vocab.push((c.to_string(), -1.0)); }
let cfg = SyntheticConfig {
vocab_size: vocab.len(),
embedding_length: 8,
block_count: 1,
feed_forward_length: 16,
head_count: 1,
head_count_kv: 1,
rms_eps: 1e-6,
seed: 1,
vocab: Some(vocab),
merges: None,
bos_id: Some(1), eos_id: Some(2), unk_id: Some(0),
chat_template: template.map(|s| s.to_string()),
};
let bytes = synthesize_llama_gguf(&cfg);
read_gguf(Cursor::new(bytes)).unwrap()
}
/// detect_from_model returns None when no chat_template metadata is set.
#[test]
fn detect_returns_none_when_no_template() {
let model = build_model_with_template(None);
assert_eq!(ChatTemplate::detect_from_model(&model), None);
}
/// Detect Llama 3 from its signature markers.
#[test]
fn detect_llama3() {
let template = "{% for message in messages %}<|start_header_id|>{{ message.role }}<|end_header_id|>\n\n{{ message.content }}<|eot_id|>{% endfor %}";
let model = build_model_with_template(Some(template));
assert_eq!(ChatTemplate::detect_from_model(&model), Some(ChatTemplate::Llama3));
}
/// Detect Llama 2 from <<SYS>> marker.
#[test]
fn detect_llama2() {
let template = "<s>[INST] <<SYS>>\n{{ system }}\n<</SYS>>\n\n{{ user }} [/INST]";
let model = build_model_with_template(Some(template));
assert_eq!(ChatTemplate::detect_from_model(&model), Some(ChatTemplate::Llama2));
}
/// Detect Mistral from [INST] + <s> without <<SYS>>.
#[test]
fn detect_mistral() {
let template = "<s>[INST] {{ message.content }} [/INST]";
let model = build_model_with_template(Some(template));
assert_eq!(ChatTemplate::detect_from_model(&model), Some(ChatTemplate::Mistral));
}
/// Detect ChatML from <|im_start|>/<|im_end|>.
#[test]
fn detect_chatml() {
let template = "{% for m in messages %}<|im_start|>{{ m.role }}\n{{ m.content }}<|im_end|>\n{% endfor %}<|im_start|>assistant\n";
let model = build_model_with_template(Some(template));
assert_eq!(ChatTemplate::detect_from_model(&model), Some(ChatTemplate::ChatML));
}
/// Detect Phi-3 from <|user|>/<|assistant|> tags.
#[test]
fn detect_phi3() {
let template = "<|user|>\n{{ user }}<|end|>\n<|assistant|>\n";
let model = build_model_with_template(Some(template));
assert_eq!(ChatTemplate::detect_from_model(&model), Some(ChatTemplate::Phi3));
}
/// Unknown template strings produce None — caller handles fallback.
#[test]
fn detect_unknown_template_returns_none() {
let template = "completely custom format with no known markers";
let model = build_model_with_template(Some(template));
assert_eq!(ChatTemplate::detect_from_model(&model), None);
}
/// Demo: format the same conversation in all templates.
#[test]
fn chat_template_demo() {
let conv = vec![
ChatMessage::system("Be brief."),
ChatMessage::user("What is 2+2?"),
];
println!("\n=== Chat template comparison ===");
println!("Input messages:");
for m in &conv {
let role = match m.role {
ChatRole::System => "system",
ChatRole::User => "user",
ChatRole::Assistant => "assistant",
};
println!(" [{}]: {:?}", role, m.content);
}
println!();
for tmpl in &[
ChatTemplate::Llama3,
ChatTemplate::Llama2,
ChatTemplate::Mistral,
ChatTemplate::ChatML,
ChatTemplate::Phi3,
ChatTemplate::Plain,
] {
let formatted = tmpl.format(&conv);
println!("--- {} ---", tmpl.name());
// Render escaped so newlines and special tokens are visible.
let escaped = formatted
.replace('\n', "\\n")
.replace('\r', "\\r");
println!(" {}", escaped);
}
println!("\nEach template ends with the model's \"open assistant turn\"");
println!("marker — generate() continues from there.");
}