KeiSeiKit-1.0/_primitives/_rust/kei-registry/src/secrets_tests.rs
Parfii-bot af46684330 feat(secrets+catalog): orphan-detector for env vars + image/video/voice models
Two parallel agents (both Sonnet 4.6 via the just-activated tier system)
extended the substrate-unified-registry. First end-to-end proof that the
Phase 4 router refactor saves money: no Opus spawns this round.

PART 1 — `kei-registry secrets` subcommand (Agent A — code-implementer)

Reads env-var NAMES from `~/.claude/secrets/.env` (RULE 0.8 SSoT) and
per-project `secrets/*.env`, greps the kit tree for usages, reports
orphans (defined but unreferenced). Live run on this kit found 26 keys,
11 ORPHAN — actionable cleanup candidates incl. GitHub OAuth client
creds, Godaddy keys, KeiGit admin creds, KEI_MEMORY_TOKEN.

Files:
- `_primitives/_rust/kei-registry/src/secrets.rs` (152 LOC) — pure
  read-side cube. SecretsReport + KeyRow types, env-file parser
  (KEY=value lines, validates `^[A-Z][A-Z0-9_]*$`), walkdir-based
  scanner with skips (target/ node_modules/ .git/ _generated/),
  word-boundary regex per key. ASCII + JSON render.
- `_primitives/_rust/kei-registry/src/secrets_tests.rs` (125 LOC) —
  5 unit tests covering env parse, scan correctness, word-boundary
  regression (`MY_KEY` ≠ `MY_KEY_EXTRA`), JSON roundtrip, ORPHAN marker.
- `_primitives/_rust/kei-registry/src/secrets_handler.rs` (58 LOC) —
  CLI dispatch handler.
- `cli.rs`, `handlers.rs`, `lib.rs` extended with Secrets variant.

Resolves the asymmetry called out in the design discussion: paths got
atomization (commit 3422bdc), keys get a query-layer instead. Reason:
env-var NAMES are already public and stable; opaque atom-DNA over them
adds zero security and full overhead. Orphan detection is the unique
value, and a 30-LOC subcommand delivers it without a per-key atom file.

PART 2 — kei-model catalog extension (Agent B — fal-ai-runner)

Adds 10 generation-model entries with VERIFIED pricing per RULE 0.4:
- google: gemini-3-1-flash-image, gemini-3-pro-image
- fal.ai: flux-2-pro, flux-pro-1-1, kling-o3, veo-3, ideogram-v3, recraft-v3
- elevenlabs: elevenlabs-v3, elevenlabs-multilingual-v2

Pricing sourced from each provider's public pricing page (URLs cited
per row in `notes` + `source_url` fields); 8/10 verified, 2 marked
needs-verification (gemini-3-pro-image price not found on public page).

Schema additions to `_primitives/_rust/kei-model/src/model.rs` to
support the new entries without `provider = "local"` placeholder:
- Provider enum + 3 variants: Google, Fal, Elevenlabs (with as_str
  + parse impls).
- Capability enum + 9 variants: image-gen, text-to-image, image-edit,
  video-gen, text-to-video, image-to-video, voice-gen, text-to-speech,
  voice-clone (with serde rename + as_str + parse).

Pricing struct unchanged: per-image / per-second / per-1k-chars unit
costs ride existing `output_per_mtok_micro` field with the unit
documented in `notes` (e.g. "Per-image cost. 1 unit = 1 image."). A
proper Pricing.unit field is a follow-up.

Files:
- `_primitives/_rust/kei-model/src/model.rs` (+24 LOC enum extensions)
- `_primitives/_rust/kei-model/data/models.toml` (+216 LOC, 471 total)

`kei-model list` returns the full 21-model catalog incl. new providers.

Tests:
- kei-registry: 25 passed (existing + 5 secrets tests + 10 status)
- kei-model: 0 (no unit tests in crate, parser smoke via list)
- agent-assembler: 29 passed (no regressions)

Verification (cited):
- `./target/release/kei-registry secrets --env-file ~/.claude/secrets/.env`
  emits real report 26/11 orphan.
- `./target/release/kei-model list` parses all 21 entries cleanly.
- `cargo build --release --workspace` clean.

What this does NOT do (deferred):
- Pricing.unit field (per-mtok / per-image / per-second / per-1k-chars
  discriminator) — needs Rust struct refactor + cost-estimator update.
- `secrets` skip-list extension (worktrees, _ts_packages/node_modules
  duplicate counts) — minor noise.
- gemini-3-pro-image pricing (no public page; vendor-specific quote
  needed).

=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
behaviour-verified: yes
follow-up-required:
  - Pricing.unit field for cost-estimator correctness on gen models
  - secrets scan: skip .claude/worktrees/ to avoid duplicate counts
  - gemini-3-pro-image price verification

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:06:16 +08:00

125 lines
4.1 KiB
Rust

//! Tests for secrets.rs — orphan-detection, env-parse, word-boundary, JSON roundtrip.
use std::io::Write;
use std::path::Path;
use tempfile::TempDir;
use super::*;
fn write_file(dir: &Path, name: &str, content: &str) {
let p = dir.join(name);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(content.as_bytes()).unwrap();
}
fn make_env(dir: &Path, name: &str, content: &str) -> std::path::PathBuf {
let p = dir.join(name);
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(content.as_bytes()).unwrap();
p
}
#[test]
fn test_parse_env_file_filters_correctly() {
let tmp = TempDir::new().unwrap();
let env_path = make_env(
tmp.path(),
".env",
"# comment\n\nANTHROPIC_API_KEY=sk-ant-xxx\nlower_key=ignored\nNO_EQUALS\nQUOTED_KEY=\"value\"\n",
);
let keys = parse_env_file(&env_path).unwrap();
assert!(keys.contains(&"ANTHROPIC_API_KEY".to_string()));
assert!(keys.contains(&"QUOTED_KEY".to_string()));
assert!(!keys.contains(&"lower_key".to_string()));
assert!(!keys.contains(&"NO_EQUALS".to_string()));
}
#[test]
fn test_scan_counts_usages_correctly() {
let src_tmp = TempDir::new().unwrap();
write_file(src_tmp.path(), "main.rs", "let x = std::env::var(\"MY_KEY\").unwrap();");
write_file(src_tmp.path(), "config.toml", "key = \"$MY_KEY\"");
write_file(src_tmp.path(), "other.rs", "// no secret here");
let env_tmp = TempDir::new().unwrap();
let env_path = make_env(env_tmp.path(), ".env", "MY_KEY=secret\nORPHAN_KEY=unused\n");
let report = compute_secrets_report(&[env_path], src_tmp.path()).unwrap();
let my_key = report.keys.iter().find(|k| k.name == "MY_KEY").unwrap();
let orphan_key = report.keys.iter().find(|k| k.name == "ORPHAN_KEY").unwrap();
assert_eq!(my_key.usage_count, 2);
assert!(!my_key.orphan);
assert_eq!(orphan_key.usage_count, 0);
assert!(orphan_key.orphan);
}
#[test]
fn test_word_boundary_no_false_positive() {
let src_tmp = TempDir::new().unwrap();
// MY_KEY_EXTRA must NOT match MY_KEY due to word boundary.
write_file(src_tmp.path(), "a.rs", "let _ = std::env::var(\"MY_KEY_EXTRA\");");
let env_tmp = TempDir::new().unwrap();
let env_path = make_env(env_tmp.path(), ".env", "MY_KEY=val\n");
let report = compute_secrets_report(&[env_path], src_tmp.path()).unwrap();
let row = report.keys.iter().find(|k| k.name == "MY_KEY").unwrap();
assert_eq!(
row.usage_count, 0,
"word boundary regression: MY_KEY matched inside MY_KEY_EXTRA"
);
}
#[test]
fn test_json_roundtrip() {
let report = SecretsReport {
keys: vec![KeyRow {
name: "TEST_KEY".into(),
source_env_file: "/tmp/.env".into(),
usage_count: 3,
usage_files: vec!["src/main.rs".into()],
orphan: false,
}],
scanned_files: 10,
env_files: vec!["/tmp/.env".into()],
};
let json = serde_json::to_string(&report).unwrap();
let decoded: SecretsReport = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.keys[0].name, "TEST_KEY");
assert_eq!(decoded.scanned_files, 10);
assert!(!decoded.keys[0].orphan);
}
#[test]
fn test_render_ascii_shows_orphan_marker() {
let report = SecretsReport {
keys: vec![
KeyRow {
name: "ACTIVE_KEY".into(),
source_env_file: "~/.env".into(),
usage_count: 5,
usage_files: vec!["src/a.rs".into()],
orphan: false,
},
KeyRow {
name: "LEGACY_TOKEN".into(),
source_env_file: "~/.env".into(),
usage_count: 0,
usage_files: vec![],
orphan: true,
},
],
scanned_files: 20,
env_files: vec!["~/.env".into()],
};
let ascii = render_ascii(&report);
assert!(ascii.contains("*ORPHAN*"));
assert!(ascii.contains("LEGACY_TOKEN"));
assert!(ascii.contains("ACTIVE_KEY"));
assert!(ascii.contains("Total: 2 keys"));
assert!(ascii.contains("1 orphan"));
}