mirror of
https://github.com/instructkr/claw-code.git
synced 2026-10-18 00:00:11 +08:00
test(api): lock the README model-alias table to the resolver
The `## Model Aliases` table and `MODEL_REGISTRY` had drifted: the table listed only the three Anthropic aliases while the resolver had grown the xAI and Kimi entries, so an alias could resolve without being documented. Assert the two agree in both directions — every registry alias appears in the table with the canonical id and auth env var it actually resolves to, and the table introduces no alias the resolver does not know. Each row is also checked case-insensitively, since the README claims `OPUS` behaves the same as `opus`. Also pin that a provider-prefixed form such as `anthropic/opus` is not an alias: it is passed through as-is rather than expanded. The README now says so, which is what made the distinction worth a test rather than a comment.
This commit is contained in:
1 parent
b89ddda387
commit
072594dfee
2 files changed
+133
-1
No files matched your search
+4
-1
@@ -113,7 +113,10 @@ Primary artifacts:
|
||||
Short model aliases resolve to canonical model IDs. Because the router selects a
|
||||
provider from the resolved model, an alias also determines which backend and
|
||||
credentials are used — choose the alias for the provider you have an API key for.
|
||||
Alias resolution is case-insensitive (`OPUS` behaves the same as `opus`).
|
||||
Alias resolution is case-insensitive (`OPUS` behaves the same as `opus`). Aliases
|
||||
are the bare short names listed below: a prefixed form such as `anthropic/opus`
|
||||
is not an alias, and is passed through as-is rather than expanded to
|
||||
`claude-opus-4-7`.
|
||||
|
||||
| Alias | Resolves To | Provider | Auth env var |
|
||||
|-------|-------------|----------|--------------|
|
||||
|
||||
@@ -902,6 +902,135 @@ mod tests {
|
||||
assert_eq!(resolve_model_alias("grok-2"), "grok-2");
|
||||
}
|
||||
|
||||
/// Collects every non-empty backtick-delimited span in a markdown cell.
|
||||
fn backticked(cell: &str) -> Vec<&str> {
|
||||
let mut spans = Vec::new();
|
||||
let mut rest = cell;
|
||||
while let Some(start) = rest.find('`') {
|
||||
let after_open = &rest[start + 1..];
|
||||
let Some(end) = after_open.find('`') else {
|
||||
break;
|
||||
};
|
||||
spans.push(&after_open[..end]);
|
||||
rest = &after_open[end + 1..];
|
||||
}
|
||||
spans
|
||||
}
|
||||
|
||||
// Locks the `## Model Aliases` table in `rust/README.md` to the resolver.
|
||||
//
|
||||
// The table and the alias set must agree in *both* directions: no alias may
|
||||
// resolve without being documented, and none may be documented without
|
||||
// resolving. The table previously listed only the three Anthropic aliases
|
||||
// while the resolver had grown xAI and Kimi entries, so an alias could work
|
||||
// undocumented — exactly the drift a parity test catches and a hand-written
|
||||
// `assert_eq!` per alias does not.
|
||||
#[test]
|
||||
fn readme_alias_table_matches_resolver() {
|
||||
// `rust/crates/api/src/providers/mod.rs` -> `rust/README.md`.
|
||||
const README: &str = include_str!("../../../../README.md");
|
||||
|
||||
let section = README
|
||||
.split("## Model Aliases")
|
||||
.nth(1)
|
||||
.expect("rust/README.md must keep a `## Model Aliases` section");
|
||||
let section = section.split("\n## ").next().unwrap_or(section);
|
||||
|
||||
// (alias, documented canonical id, documented auth env var)
|
||||
let mut documented: Vec<(String, String, String)> = Vec::new();
|
||||
for line in section.lines() {
|
||||
let line = line.trim();
|
||||
let Some(row) = line.strip_prefix('|') else {
|
||||
continue;
|
||||
};
|
||||
let cells: Vec<&str> = row
|
||||
.trim_end_matches('|')
|
||||
.split('|')
|
||||
.map(str::trim)
|
||||
.collect();
|
||||
if cells.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
let aliases = backticked(cells[0]);
|
||||
if aliases.is_empty() {
|
||||
// Header and `|---|---|` separator rows carry no aliases.
|
||||
continue;
|
||||
}
|
||||
let canonicals = backticked(cells[1]);
|
||||
assert_eq!(
|
||||
canonicals.len(),
|
||||
1,
|
||||
"`{line}` must document exactly one canonical model id"
|
||||
);
|
||||
let auth_envs = backticked(cells[3]);
|
||||
assert_eq!(
|
||||
auth_envs.len(),
|
||||
1,
|
||||
"`{line}` must document exactly one auth env var"
|
||||
);
|
||||
for alias in aliases {
|
||||
documented.push((
|
||||
alias.to_string(),
|
||||
canonicals[0].to_string(),
|
||||
auth_envs[0].to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
!documented.is_empty(),
|
||||
"parsed no alias rows out of the `## Model Aliases` table in rust/README.md"
|
||||
);
|
||||
|
||||
for (alias, canonical, auth_env) in &documented {
|
||||
let resolved = resolve_model_alias(alias);
|
||||
assert_eq!(
|
||||
resolved.as_str(),
|
||||
canonical.as_str(),
|
||||
"rust/README.md documents `{alias}` -> `{canonical}`, but resolve_model_alias returns `{resolved}`"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_model_alias(&alias.to_ascii_uppercase()).as_str(),
|
||||
resolved.as_str(),
|
||||
"rust/README.md claims alias resolution is case-insensitive, but `{}` disagrees with `{alias}`",
|
||||
alias.to_ascii_uppercase()
|
||||
);
|
||||
let metadata = super::metadata_for_model(&resolved).unwrap_or_else(|| {
|
||||
panic!("no provider metadata for documented model `{canonical}`")
|
||||
});
|
||||
assert_eq!(
|
||||
metadata.auth_env,
|
||||
auth_env.as_str(),
|
||||
"rust/README.md documents `{alias}` as needing `{auth_env}`, but `{canonical}` resolves to provider metadata with auth env `{}`",
|
||||
metadata.auth_env
|
||||
);
|
||||
}
|
||||
|
||||
let documented_aliases: std::collections::BTreeSet<&str> = documented
|
||||
.iter()
|
||||
.map(|(alias, _, _)| alias.as_str())
|
||||
.collect();
|
||||
let resolver_aliases: std::collections::BTreeSet<&str> = super::MODEL_REGISTRY
|
||||
.iter()
|
||||
.map(|(alias, _)| *alias)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
documented_aliases, resolver_aliases,
|
||||
"rust/README.md's `## Model Aliases` table and MODEL_REGISTRY have drifted apart"
|
||||
);
|
||||
}
|
||||
|
||||
// The table documents bare short names only, so the provider-prefixed form
|
||||
// is deliberately *not* an alias. This pins that distinction, since the
|
||||
// prefix is handled later (at request-build time) and a future refactor
|
||||
// could easily fold it into alias resolution.
|
||||
#[test]
|
||||
fn provider_prefixed_alias_is_not_an_alias() {
|
||||
assert_eq!(resolve_model_alias("opus"), "claude-opus-4-7");
|
||||
assert_eq!(resolve_model_alias("anthropic/opus"), "anthropic/opus");
|
||||
assert_eq!(resolve_model_alias("xai/grok"), "xai/grok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_provider_from_model_name_first() {
|
||||
assert_eq!(detect_provider_kind("grok"), ProviderKind::Xai);
|
||||
|
||||
Reference in new issue
Block a user