Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
138 lines
3.9 KiB
Rust
138 lines
3.9 KiB
Rust
//! Selector resolution: role match, fallback to defaults, budget filtering.
|
||
//!
|
||
//! The seed catalog has all pricing=0, so budget filtering can't be tested
|
||
//! against it (zero is below any cap). For budget tests we use a synthetic
|
||
//! fixture catalog with non-zero rates.
|
||
|
||
use std::path::PathBuf;
|
||
|
||
use kei_model::model::Capability;
|
||
use kei_model::registry::Registry;
|
||
use kei_model::selector::resolve;
|
||
|
||
fn seed_catalog() -> PathBuf {
|
||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data/models.toml")
|
||
}
|
||
|
||
fn seed_selectors() -> PathBuf {
|
||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data/selectors.toml")
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_code_implementer_returns_opus() {
|
||
let reg = Registry::load(&seed_catalog()).unwrap();
|
||
let r = resolve(
|
||
"code-implementer",
|
||
None,
|
||
&[Capability::Code],
|
||
®,
|
||
Some(&seed_selectors()),
|
||
)
|
||
.expect("code-implementer must resolve");
|
||
assert_eq!(r.model.id, "claude-opus-4-7");
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_kei_critic_returns_haiku() {
|
||
let reg = Registry::load(&seed_catalog()).unwrap();
|
||
let r = resolve(
|
||
"kei-critic",
|
||
None,
|
||
&[Capability::Code],
|
||
®,
|
||
Some(&seed_selectors()),
|
||
)
|
||
.expect("kei-critic must resolve");
|
||
assert_eq!(r.model.id, "claude-haiku-4-5");
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_no_caps_no_role_match_falls_back_to_defaults() {
|
||
// "edit-shared" is in selectors.toml [defaults], no model carries that
|
||
// tag in the seed catalog, so the resolver falls through to the default.
|
||
let reg = Registry::load(&seed_catalog()).unwrap();
|
||
let r = resolve(
|
||
"edit-shared",
|
||
None,
|
||
&[],
|
||
®,
|
||
Some(&seed_selectors()),
|
||
)
|
||
.expect("edit-shared must resolve via defaults table");
|
||
assert_eq!(r.model.id, "claude-opus-4-7");
|
||
}
|
||
|
||
#[test]
|
||
fn resolve_unknown_role_errors() {
|
||
let reg = Registry::load(&seed_catalog()).unwrap();
|
||
let err = resolve(
|
||
"no-such-role-xyz",
|
||
None,
|
||
&[],
|
||
®,
|
||
Some(&seed_selectors()),
|
||
);
|
||
assert!(err.is_err(), "unknown role must produce a NoMatch error");
|
||
}
|
||
|
||
#[test]
|
||
fn budget_filter_rejects_overpriced_with_synthetic_fixture() {
|
||
// Synthetic fixture: two models, only the cheap one fits the budget.
|
||
let fixture = synth_two_model_fixture();
|
||
let cat_path = fixture.path().join("models.toml");
|
||
std::fs::write(&cat_path, FIXTURE_CATALOG).unwrap();
|
||
let sel_path = fixture.path().join("selectors.toml");
|
||
std::fs::write(&sel_path, FIXTURE_SELECTORS).unwrap();
|
||
|
||
let reg = Registry::load(&cat_path).unwrap();
|
||
// Budget = 1000 micro on a 1k+1k baseline only fits the cheap model
|
||
// (input rate 200 micro/Mtok → 1k input = 0.2 micro; ok). The
|
||
// expensive model has input rate 1_000_000 micro/Mtok → 1k input = 1
|
||
// micro and 1k output × 2_000_000/Mtok = 2 micro, total 3 micro per
|
||
// 1k+1k baseline; we cap below that. We use 1000 to clearly admit cheap.
|
||
let r = resolve("worker", Some(1_000), &[], ®, Some(&sel_path)).unwrap();
|
||
assert_eq!(r.model.id, "synth-cheap");
|
||
}
|
||
|
||
fn synth_two_model_fixture() -> tempfile::TempDir {
|
||
tempfile::tempdir().unwrap()
|
||
}
|
||
|
||
const FIXTURE_CATALOG: &str = r#"
|
||
[[models]]
|
||
id = "synth-cheap"
|
||
provider = "local"
|
||
display_name = "Synthetic Cheap"
|
||
context_tokens = 8000
|
||
capabilities = ["code"]
|
||
status = "active"
|
||
role_tags = ["worker"]
|
||
fallback = ""
|
||
|
||
[models.pricing]
|
||
input_per_mtok_micro = 200
|
||
output_per_mtok_micro = 400
|
||
status = "needs-verification"
|
||
source_url = "https://example.test/cheap"
|
||
|
||
[[models]]
|
||
id = "synth-expensive"
|
||
provider = "local"
|
||
display_name = "Synthetic Expensive"
|
||
context_tokens = 8000
|
||
capabilities = ["code"]
|
||
status = "active"
|
||
role_tags = ["worker"]
|
||
fallback = ""
|
||
|
||
[models.pricing]
|
||
input_per_mtok_micro = 100000000
|
||
output_per_mtok_micro = 100000000
|
||
status = "needs-verification"
|
||
source_url = "https://example.test/expensive"
|
||
"#;
|
||
|
||
const FIXTURE_SELECTORS: &str = r#"
|
||
[defaults]
|
||
worker = "synth-cheap"
|
||
"#;
|