System S-057 assigned to real star GJ 35 (Van Maanen's Star, DG white dwarf at 13.9 ly). Renamed across all content, server code, docs, decisions, wiki lore, and config files. 224 files updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
323 lines
11 KiB
Rust
323 lines
11 KiB
Rust
//! Voice pipeline integration test (D-138, Spike 2).
|
|
//!
|
|
//! Exercises the full pipeline: queue → worker → sr-voice child (mock or real)
|
|
//! → cache → disk. Outputs voiced results to `.tmp/voice-test/`.
|
|
//!
|
|
//! Run with mock: cargo test --test voice_pipeline -- --nocapture
|
|
//! Run with real: SR_VOICE_BIN=sr-voice/target/release/sr-voice \
|
|
//! SR_VOICE_MODEL=models/gemma2.gguf \
|
|
//! cargo test --test voice_pipeline -- --nocapture
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
|
|
use settled_reach_server::npc::blueprint::{
|
|
CulturalValues, CultureProfile, NamingConventions, OccasionalInjection, SpeechPatterns,
|
|
VoiceExample,
|
|
};
|
|
use settled_reach_server::npc::PersonalityTrait;
|
|
use settled_reach_server::npc::tell_state::TellCategory;
|
|
use settled_reach_server::voice::cache::VoiceCacheStore;
|
|
use settled_reach_server::voice::prompt_builder::ContentType;
|
|
use settled_reach_server::voice::queue::{Priority, VoiceQueue, VoiceRequest};
|
|
use settled_reach_server::voice::worker::{VoiceProcessConfig, WorkerPool};
|
|
|
|
/// Output directory for test results (relative to repo root).
|
|
const OUTPUT_DIR: &str = ".tmp/voice-test";
|
|
|
|
fn output_dir() -> PathBuf {
|
|
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
|
|
PathBuf::from(manifest).join("..").join(OUTPUT_DIR)
|
|
}
|
|
|
|
/// Known paths relative to CARGO_MANIFEST_DIR (server/).
|
|
const SR_VOICE_BIN: &str = "sr-voice/target/release/sr-voice";
|
|
const SR_VOICE_MODEL: &str = "models/gemma2.gguf";
|
|
|
|
fn voice_config() -> VoiceProcessConfig {
|
|
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
|
|
let manifest = PathBuf::from(manifest);
|
|
|
|
let bin_path = manifest.join(SR_VOICE_BIN);
|
|
let model_path = manifest.join(SR_VOICE_MODEL);
|
|
|
|
// SR_VOICE_MOCK=1 forces mock mode (used by `make test-voice-mock`)
|
|
let force_mock = std::env::var("SR_VOICE_MOCK").is_ok();
|
|
|
|
// Use real sr-voice if both binary and model exist on disk
|
|
if !force_mock && bin_path.exists() && model_path.exists() {
|
|
eprintln!("Using real sr-voice: {}", bin_path.display());
|
|
eprintln!("Model: {}", model_path.display());
|
|
VoiceProcessConfig {
|
|
binary_path: bin_path.to_string_lossy().into(),
|
|
model_path: model_path.to_string_lossy().into(),
|
|
threads: 2,
|
|
ctx_size: 512,
|
|
}
|
|
} else {
|
|
// Fall back to mock script — no model needed
|
|
let mock = manifest.join("sr-voice/mock-stdio.sh");
|
|
assert!(
|
|
mock.exists(),
|
|
"Mock script not found: {}",
|
|
mock.display()
|
|
);
|
|
eprintln!("Using mock sr-voice: {}", mock.display());
|
|
if !bin_path.exists() {
|
|
eprintln!(" (real binary not found: {})", bin_path.display());
|
|
}
|
|
if !model_path.exists() {
|
|
eprintln!(" (model not found: {})", model_path.display());
|
|
}
|
|
VoiceProcessConfig {
|
|
binary_path: mock.to_string_lossy().into(),
|
|
model_path: "unused".into(),
|
|
threads: 1,
|
|
ctx_size: 512,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn van_maanens_star_culture() -> CultureProfile {
|
|
CultureProfile {
|
|
id: "van-maanens-star".into(),
|
|
name: "Van Maanen's Star Culture".into(),
|
|
description: "Working-class pragmatic".into(),
|
|
naming: NamingConventions {
|
|
style: "compact".into(),
|
|
given_names: vec!["Kael".into()],
|
|
family_names: vec!["Davan".into()],
|
|
family_name_used_socially: false,
|
|
},
|
|
speech: SpeechPatterns {
|
|
register: "direct".into(),
|
|
filler_words: vec!["look".into()],
|
|
greetings: vec!["hey".into()],
|
|
farewells: vec!["shift's calling".into()],
|
|
exclamations: vec!["void take it".into()],
|
|
},
|
|
values: CulturalValues {
|
|
description: "Pragmatic".into(),
|
|
favored_traits: vec![PersonalityTrait::Bold],
|
|
disfavored_traits: vec![PersonalityTrait::Reclusive],
|
|
},
|
|
voice_persona: Some(
|
|
"PERSONA: You are a Van Maanen's Star station worker.\n\
|
|
1. Be direct. No pleasantries.\n\
|
|
2. You're working-class and pragmatic."
|
|
.into(),
|
|
),
|
|
voice_examples: vec![VoiceExample {
|
|
input: "declines to answer a question".into(),
|
|
output: "Look, that's not mine to say.".into(),
|
|
}],
|
|
occasional_injections: vec![OccasionalInjection {
|
|
kind: "oath".into(),
|
|
clause: "Use an oath like \"void take it.\"".into(),
|
|
example: Some(VoiceExample {
|
|
input: "discovers a critical part is missing".into(),
|
|
output: "Void take it. The coupling's not here.".into(),
|
|
}),
|
|
frequency: 0.25,
|
|
suppress_on_tells: vec![
|
|
TellCategory::Guarded,
|
|
TellCategory::RoutineDeviation,
|
|
TellCategory::Friendly,
|
|
],
|
|
}],
|
|
behavior_modifiers: vec![],
|
|
}
|
|
}
|
|
|
|
fn test_requests() -> Vec<VoiceRequest> {
|
|
vec![
|
|
VoiceRequest {
|
|
priority: Priority::High,
|
|
npc_stable_id: 1,
|
|
zone_id: 100,
|
|
culture_id: "van-maanens-star".into(),
|
|
base_text: "The coupling is faulty.".into(),
|
|
content_type: ContentType::Dialogue,
|
|
content_index: 0,
|
|
tell_state: None,
|
|
seed: 42,
|
|
},
|
|
VoiceRequest {
|
|
priority: Priority::High,
|
|
npc_stable_id: 1,
|
|
zone_id: 100,
|
|
culture_id: "van-maanens-star".into(),
|
|
base_text: "I heard the night crew had to stop the line twice because the coupling was faulty and nobody had flagged it in the log.".into(),
|
|
content_type: ContentType::Dialogue,
|
|
content_index: 1,
|
|
tell_state: Some(TellCategory::Nervous),
|
|
seed: 43,
|
|
},
|
|
VoiceRequest {
|
|
priority: Priority::Standard,
|
|
npc_stable_id: 2,
|
|
zone_id: 100,
|
|
culture_id: "van-maanens-star".into(),
|
|
base_text: "checks the pressure gauge and writes in a logbook".into(),
|
|
content_type: ContentType::Behavior,
|
|
content_index: 0,
|
|
tell_state: None,
|
|
seed: 44,
|
|
},
|
|
VoiceRequest {
|
|
priority: Priority::High,
|
|
npc_stable_id: 3,
|
|
zone_id: 100,
|
|
culture_id: "unknown_culture".into(),
|
|
base_text: "Unknown culture fallback test.".into(),
|
|
content_type: ContentType::Dialogue,
|
|
content_index: 0,
|
|
tell_state: None,
|
|
seed: 45,
|
|
},
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn voice_pipeline_end_to_end() {
|
|
// Setup output dir
|
|
let out = output_dir();
|
|
let cache_dir = out.join("cache");
|
|
let _ = std::fs::remove_dir_all(&cache_dir);
|
|
std::fs::create_dir_all(&cache_dir).expect("failed to create cache dir");
|
|
|
|
let config = voice_config();
|
|
|
|
// Build cultures map
|
|
let mut cultures = BTreeMap::new();
|
|
cultures.insert("van-maanens-star".to_string(), van_maanens_star_culture());
|
|
let cultures = Arc::new(cultures);
|
|
|
|
// Create cache
|
|
let cache = Arc::new(Mutex::new(VoiceCacheStore::new(
|
|
cache_dir.clone(),
|
|
12345,
|
|
"test-v1".into(),
|
|
"test-i1".into(),
|
|
)));
|
|
|
|
// Create queue and worker pool
|
|
let queue = VoiceQueue::new();
|
|
let paused = queue.paused_flag();
|
|
let mut pool = WorkerPool::spawn(
|
|
2, // 2 workers
|
|
&config,
|
|
queue.receiver(),
|
|
Arc::clone(&cache),
|
|
Arc::clone(&cultures),
|
|
paused,
|
|
);
|
|
|
|
eprintln!("Worker pool started: {} workers", pool.worker_count());
|
|
|
|
// Submit test requests
|
|
let requests = test_requests();
|
|
let request_count = requests.len();
|
|
for req in requests {
|
|
let submitted = queue.submit(req);
|
|
assert!(submitted, "queue should accept request");
|
|
}
|
|
eprintln!("Submitted {} requests", request_count);
|
|
|
|
// Wait for processing (mock is instant, real LLM takes seconds per request)
|
|
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
|
|
let is_real = std::env::var("SR_VOICE_MOCK").is_err()
|
|
&& PathBuf::from(&manifest).join(SR_VOICE_BIN).exists()
|
|
&& PathBuf::from(&manifest).join(SR_VOICE_MODEL).exists();
|
|
let max_wait = if is_real {
|
|
Duration::from_secs(120)
|
|
} else {
|
|
Duration::from_secs(10)
|
|
};
|
|
let start = std::time::Instant::now();
|
|
|
|
loop {
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
|
|
let pending = queue.pending_count();
|
|
let active = pool.active_workers();
|
|
|
|
if pending == 0 && active == 0 {
|
|
break;
|
|
}
|
|
|
|
if start.elapsed() > max_wait {
|
|
eprintln!(
|
|
"Timeout waiting for pipeline (pending={}, active={})",
|
|
pending, active
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
let elapsed = start.elapsed();
|
|
eprintln!("Pipeline drained in {:.1}s", elapsed.as_secs_f64());
|
|
|
|
// Shutdown workers
|
|
pool.shutdown();
|
|
eprintln!("Workers shut down");
|
|
|
|
// Inspect cache
|
|
let mut cache_guard = cache.lock().unwrap();
|
|
let zone_cache = cache_guard.zone_cache(100);
|
|
let entry_count = zone_cache.len();
|
|
eprintln!("Cache entries for zone 100: {}", entry_count);
|
|
|
|
// Write results to a human-readable file
|
|
let results_path = out.join("results.txt");
|
|
let mut results = String::new();
|
|
results.push_str(&format!(
|
|
"Voice Pipeline Test Results\n\
|
|
==========================\n\
|
|
Workers: {}\n\
|
|
Requests: {}\n\
|
|
Cache entries: {}\n\
|
|
Time: {:.1}s\n\
|
|
Mode: {}\n\n",
|
|
2,
|
|
request_count,
|
|
entry_count,
|
|
elapsed.as_secs_f64(),
|
|
if is_real { "real sr-voice" } else { "mock" },
|
|
));
|
|
|
|
for (key, text) in &zone_cache.entries {
|
|
results.push_str(&format!(
|
|
"--- npc={} type={:?} idx={} tell={:?} ---\n{}\n\n",
|
|
key.npc_stable_id, key.content_type, key.content_index, key.tell_state, text
|
|
));
|
|
}
|
|
|
|
// Save cache to disk
|
|
drop(cache_guard);
|
|
// Drop triggers save_all via the Drop impl — but the cache is behind
|
|
// Arc<Mutex<>>, so we can't drop it here. Explicitly save instead.
|
|
cache.lock().unwrap().save_all().expect("failed to save cache");
|
|
|
|
std::fs::write(&results_path, &results).expect("failed to write results");
|
|
eprintln!("Results written to {}", results_path.display());
|
|
eprintln!("\n{}", results);
|
|
|
|
// Assertions
|
|
assert!(
|
|
entry_count >= 3,
|
|
"Expected at least 3 cache entries (3 valid culture requests), got {}",
|
|
entry_count
|
|
);
|
|
|
|
// The unknown culture request should still cache (with base text as fallback)
|
|
// Total should be 4 (3 van-maanens-star + 1 unknown fallback)
|
|
assert!(
|
|
entry_count == 4,
|
|
"Expected 4 cache entries (3 van-maanens-star + 1 unknown fallback), got {}",
|
|
entry_count
|
|
);
|
|
}
|