1372:mod tests {
  fn tmp_paths() -> Paths {
    let dir = std::env::temp_dir().join(format!(
      "ai-control-test-{}-{}",
      std::process::id(),
      N.fetch_add(1, Ordering::SeqCst)
    ));
    fs::create_dir_all(dir.join("claude-projects")).unwrap();
    Paths { home: dir }
  }

  fn make_oauth_pool(p: &Paths, name: &str) {
    create_oauth_pool_in(p, name).unwrap();
  }

  fn mode(path: &PathBuf) -> u32 {
    fs::metadata(path).unwrap().permissions().mode() & 0o777
  }

  // -- Pool anlegen --

  #[test]
  fn apikey_pool_anlegen() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-test-123").unwrap();
    let dir = p.pool_dir("kunde");

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

    let key_path = dir.join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-test-123\n");
    assert_eq!(mode(&key_path), 0o600);

    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      format!("cat '{}'", key_path.display())
    );
  }

  #[test]
  fn oauth_pool_anlegen() {
    let p = tmp_paths();
    make_oauth_pool(&p, "privat");
    let dir = p.pool_dir("privat");

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.credential_type, "oauth");

    // Kein Credentials-File — den Login macht claude selbst beim ersten Start.
    assert!(!dir.join(".credentials.json").exists());
    // Grundausstattung: settings.json mit aufgeräumten UI-Defaults + CLAUDE.md,
    // damit claude nicht ins Onboarding fällt und keine Vorschlagstabelle zeigt.
    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(settings["promptSuggestionEnabled"], serde_json::json!(false));
    assert_eq!(settings["awaySummaryEnabled"], serde_json::json!(false));
    assert!(dir.join("CLAUDE.md").is_file());
  }

  #[test]
  fn pool_init_erhaelt_bestehende_claude_md() {
    let p = tmp_paths();
    let dir = p.pool_dir("privat");
    fs::create_dir_all(&dir).unwrap();
17:const POOL_FILE: &str = "pool.json";
44:  fn pool_dir(&self, pool: &str) -> PathBuf {
214:  let cfg_path = paths.pool_dir(pool).join(POOL_FILE);
367:    if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
485:  if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
653:  let dir = paths.pool_dir(name);
660:fn write_pool_json(dir: &PathBuf, name: &str, credential_type: &str) -> Result<(), String> {
722:  let src = paths.pool_dir(pool);
746:/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json.
747:fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<(), String> {
759:  write_pool_json(&dir, name, "apikey")?;
764:/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
766:fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
769:  write_pool_json(&dir, name, "oauth")?;
777:  let dir = paths.pool_dir(name);
797:  write_secret_file(&paths.pool_dir(pool).join(APIKEY_FILE), &format!("{key}\n"))
1050:  create_oauth_pool_in(&Paths::real(), &name)
1055:  create_apikey_pool_in(&Paths::real(), &name, &key)
1105:  let hash = Sha256::digest(paths.pool_dir(pool).to_string_lossy().as_bytes());
1391:    create_oauth_pool_in(p, name).unwrap();
1403:    create_apikey_pool_in(&p, "kunde", "sk-test-123").unwrap();
1404:    let dir = p.pool_dir("kunde");
1428:    let dir = p.pool_dir("privat");
1449:    let dir = p.pool_dir("privat");
1464:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1465:    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
1472:    assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());
1478:    assert!(create_apikey_pool_in(&p, "a/b", "sk-1").is_err());
1486:    create_apikey_pool_in(&p, "kunde", "sk-alt").unwrap();
1488:    let key_path = p.pool_dir("kunde").join(APIKEY_FILE);
1503:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1514:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1516:    assert!(!p.pool_dir("kunde").exists());
1524:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1679:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1684:    let dir = p.pool_dir("kunde").join("projects").join(&encoded);
1717:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1742:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1750:    fs::write(p.pool_dir("kunde").join(APIKEY_FILE), "\n").unwrap();
1754:    fs::remove_file(p.pool_dir("kunde").join(APIKEY_FILE)).unwrap();
1762:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1766:    assert!(!p.pool_dir("kunde").exists());
1773:    create_oauth_pool_in(&p, "privat").unwrap();
1774:    let pooldir = p.pool_dir("privat");
1800:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1826:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1849:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1869:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();


Bash

grep -n "dependencies" -A15 /Users/marcus.hinz/projects/ai-control/src-tauri/Cargo.toml | head -25

Cargo.toml Dependencies ansehen


17:[build-dependencies]
18-tauri-build = { version = "2.6.3", features = [] }
19-
20:[dependencies]
21-serde_json = "1.0"
22-serde = { version = "1.0", features = ["derive"] }
23-log = "0.4"
24-tauri = { version = "2.11.3", features = ["tray-icon", "image-png"] }
25-tauri-plugin-log = "2"
26-tauri-plugin-autostart = "2"
27-tauri-plugin-dialog = "2"
28-base64 = "0.22"
29-sha2 = "0.10"
30-portable-pty = "0.9"
31-
32:[target.'cfg(target_os = "macos")'.dependencies]
33-objc2 = "0.6"
34-objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSImage", "NSResponder"] }
35-objc2-foundation = { version = "0.3", features = ["NSString"] }


Benenne Pool privateDefault in private um


Edit

sha2 = "0.10"
portable-pty = "0.9"
uuid = { version = "1", features = ["v4"] }


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


1	[package]
2	name = "ai-control"
3	version = "0.1.0"
4	description = "Pool- und Session-Verwaltung für Claude Code"
5	authors = ["marcus.hinz"]
6	license = ""
7	repository = ""
8	edition = "2021"
9	rust-version = "1.77.2"
10
11	# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12
13	[lib]
14	name = "app_lib"
15	crate-type = ["staticlib", "cdylib", "rlib"]
16
17	[build-dependencies]
18	tauri-build = { version = "2.6.3", features = [] }
19
20	[dependencies]
21	serde_json = "1.0"
22	serde = { version = "1.0", features = ["derive"] }
23	log = "0.4"
24	tauri = { version = "2.11.3", features = ["tray-icon", "image-png"] }
25	tauri-plugin-log = "2"
26	tauri-plugin-autostart = "2"
27	tauri-plugin-dialog = "2"
28	base64 = "0.22"
29	sha2 = "0.10"
30	portable-pty = "0.9"
31
32	[target.'cfg(target_os = "macos")'.dependencies]
33	objc2 = "0.6"
34	objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSImage", "NSResponder"] }
35	objc2-foundation = { version = "0.3", features = ["NSString"] }
36

[package]
name = "ai-control"
version = "0.1.0"
description = "Pool- und Session-Verwaltung für Claude Code"
authors = ["marcus.hinz"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2.6.3", features = [] }

[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.11.3", features = ["tray-icon", "image-png"] }
tauri-plugin-log = "2"
tauri-plugin-autostart = "2"
tauri-plugin-dialog = "2"
base64 = "0.22"
sha2 = "0.10"
portable-pty = "0.9"

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSImage", "NSResponder"] }
objc2-foundation = { version = "0.3", features = ["NSString"] }


sha2 = "0.10"
portable-pty = "0.9"
uuid = { version = "1", features = ["v4"] }


The file /Users/marcus.hinz/projects/ai-control/src-tauri/Cargo.toml has been updated successfully. (file state is current in your context — no need to Read it back)

sha2 = "0.10"
portable-pty = "0.9"
uuid = { version = "1", features = ["v4"] }

[package]
name = "ai-control"
version = "0.1.0"
description = "Pool- und Session-Verwaltung für Claude Code"
authors = ["marcus.hinz"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2.6.3", features = [] }

[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.11.3", features = ["tray-icon", "image-png"] }
tauri-plugin-log = "2"
tauri-plugin-autostart = "2"
tauri-plugin-dialog = "2"
base64 = "0.22"
sha2 = "0.10"
portable-pty = "0.9"

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSImage", "NSResponder"] }
objc2-foundation = { version = "0.3", features = ["NSString"] }

+uuid = { version = "1", features = ["v4"] }


#[derive(Serialize)]
struct PoolInfo {
  name: String,
  #[serde(rename = "credentialType")]
  credential_type: String,

#[derive(Serialize)]
struct PoolInfo {
  /// Ordnername unter pools/ (bei Neuanlagen eine UUID) — stabile ID, an der
  /// Keychain-Suffix, Symlinks und Projekt-Zuordnungen hängen.
  id: String,
  /// Anzeigename aus pool.json, frei umbenennbar.
  name: String,
  #[serde(rename = "credentialType")]
  credential_type: String,


The file /Users/marcus.hinz/projects/ai-control/src-tauri/src/lib.rs has been updated successfully. (file state is current in your context — no need to Read it back)

#[derive(Serialize)]
struct PoolInfo {
  name: String,
  #[serde(rename = "credentialType")]
  credential_type: String,

#[derive(Serialize)]
struct PoolInfo {
  /// Ordnername unter pools/ (bei Neuanlagen eine UUID) — stabile ID, an der
  /// Keychain-Suffix, Symlinks und Projekt-Zuordnungen hängen.
  id: String,
  /// Anzeigename aus pool.json, frei umbenennbar.
  name: String,
  #[serde(rename = "credentialType")]
  credential_type: String,

+  /// Ordnername unter pools/ (bei Neuanlagen eine UUID) — stabile ID, an der

+  /// Keychain-Suffix, Symlinks und Projekt-Zuordnungen hängen.

+  /// Anzeigename aus pool.json, frei umbenennbar.


    let projects = projects_using_pool(paths, &pool.name)?;
    let running = projects.iter().filter(|p| is_running(p)).cloned().collect();
    pools.push(PoolInfo {
      projects,
      running,
      name: pool.name,
      credential_type: pool.credential_type,
      has_credentials,
    });
  }
  pools.sort_by(|a, b| a.name.cmp(&b.name));
  Ok(pools)
}

fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  let dir = paths.pool_dir(name);
  if dir.exists() {
    return Err(format!("Pool existiert bereits: {name}"));
  }
  Ok(dir)
}

    let id = entry.file_name().to_string_lossy().into_owned();
    let projects = projects_using_pool(paths, &id)?;
    let running = projects.iter().filter(|p| is_running(p)).cloned().collect();
    pools.push(PoolInfo {
      id,
      projects,
      running,
      name: pool.name,
      credential_type: pool.credential_type,
      has_credentials,
    });
  }
  pools.sort_by(|a, b| a.name.cmp(&b.name));
  Ok(pools)
}

/// Prüft den Anzeigenamen (gültig + noch nicht vergeben) und liefert den
/// Ordner für einen neuen Pool: pools/<UUID v4>.
fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  for pool in list_pools_in(paths)? {
    if pool.name == name {
      return Err(format!("Pool existiert bereits: {name}"));
    }
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}


    let projects = projects_using_pool(paths, &pool.name)?;
    let running = projects.iter().filter(|p| is_running(p)).cloned().collect();
    pools.push(PoolInfo {
      projects,
      running,
      name: pool.name,
      credential_type: pool.credential_type,
      has_credentials,
    });
  }
  pools.sort_by(|a, b| a.name.cmp(&b.name));
  Ok(pools)
}

fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  let dir = paths.pool_dir(name);
  if dir.exists() {
    return Err(format!("Pool existiert bereits: {name}"));
  }
  Ok(dir)
}

    let id = entry.file_name().to_string_lossy().into_owned();
    let projects = projects_using_pool(paths, &id)?;
    let running = projects.iter().filter(|p| is_running(p)).cloned().collect();
    pools.push(PoolInfo {
      id,
      projects,
      running,
      name: pool.name,
      credential_type: pool.credential_type,
      has_credentials,
    });
  }
  pools.sort_by(|a, b| a.name.cmp(&b.name));
  Ok(pools)
}

/// Prüft den Anzeigenamen (gültig + noch nicht vergeben) und liefert den
/// Ordner für einen neuen Pool: pools/<UUID v4>.
fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  for pool in list_pools_in(paths)? {
    if pool.name == name {
      return Err(format!("Pool existiert bereits: {name}"));
    }
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}

-    let projects = projects_using_pool(paths, &pool.name)?;

+    let id = entry.file_name().to_string_lossy().into_owned();

+    let projects = projects_using_pool(paths, &id)?;

+/// Prüft den Anzeigenamen (gültig + noch nicht vergeben) und liefert den

+/// Ordner für einen neuen Pool: pools/<UUID v4>.

-    return Err(format!("Pool existiert bereits: {name}"));

+      return Err(format!("Pool existiert bereits: {name}"));

fn write_pool_json(dir: &PathBuf, name: &str, credential_type: &str) -> Result<(), String> {


/// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit
/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json.
fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<(), String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let key_path = dir.join(APIKEY_FILE);
  write_secret_file(&key_path, &format!("{key}\n"))?;
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  link_pool_runtime_in(paths, name)
}

/// Legt einen oauth-Pool an: Grundausstattung (leere settings.json + CLAUDE.md)
/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
/// (`/login`) und legt den Keychain-Eintrag an — die App speichert keine Tokens.
fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
  let dir = check_new_pool(paths, name)?;
  init_pool_config(&dir, serde_json::json!({}))?;
  write_pool_json(&dir, name, "oauth")?;
  link_pool_runtime_in(paths, name)
}

/// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit
/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json. Liefert die Pool-ID.
fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let key_path = dir.join(APIKEY_FILE);
  write_secret_file(&key_path, &format!("{key}\n"))?;
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

/// Legt einen oauth-Pool an: Grundausstattung (leere settings.json + CLAUDE.md)
/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
/// (`/login`) und legt den Keychain-Eintrag an — die App speichert keine Tokens.
/// Liefert die Pool-ID.
fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  init_pool_config(&dir, serde_json::json!({}))?;
  write_pool_json(&dir, name, "oauth")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

/// Setzt den Anzeigenamen eines Pools — reines pool.json-Update, ID/Ordner
/// (und damit Keychain-Suffix, Symlinks, Zuordnungen) bleiben unverändert.
fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
  check_name(name)?;
  let current = read_pool(paths, pool)?;
  for other in list_pools_in(paths)? {
    if other.id != pool && other.name == name {
      return Err(format!("Pool existiert bereits: {name}"));
    }
  }
  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)
}


/// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit
/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json.
fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<(), String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let key_path = dir.join(APIKEY_FILE);
  write_secret_file(&key_path, &format!("{key}\n"))?;
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  link_pool_runtime_in(paths, name)
}

/// Legt einen oauth-Pool an: Grundausstattung (leere settings.json + CLAUDE.md)
/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
/// (`/login`) und legt den Keychain-Eintrag an — die App speichert keine Tokens.
fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<(), String> {
  let dir = check_new_pool(paths, name)?;
  init_pool_config(&dir, serde_json::json!({}))?;
  write_pool_json(&dir, name, "oauth")?;
  link_pool_runtime_in(paths, name)
}

/// Legt einen apikey-Pool an: Key-Datei (0600), settings.json mit
/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json. Liefert die Pool-ID.
fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  let key = key.trim();
  if key.is_empty() {
    return Err("leerer API-Key".into());
  }
  let key_path = dir.join(APIKEY_FILE);
  write_secret_file(&key_path, &format!("{key}\n"))?;
  init_pool_config(
    &dir,
    serde_json::json!({ "apiKeyHelper": format!("cat '{}'", key_path.display()) }),
  )?;
  write_pool_json(&dir, name, "apikey")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

/// Legt einen oauth-Pool an: Grundausstattung (leere settings.json + CLAUDE.md)
/// + pool.json. Die Anmeldung macht claude selbst beim ersten Start des Pools
/// (`/login`) und legt den Keychain-Eintrag an — die App speichert keine Tokens.
/// Liefert die Pool-ID.
fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<String, String> {
  let dir = check_new_pool(paths, name)?;
  init_pool_config(&dir, serde_json::json!({}))?;
  write_pool_json(&dir, name, "oauth")?;
  let id = dir.file_name().unwrap().to_string_lossy().into_owned();
  link_pool_runtime_in(paths, &id)?;
  Ok(id)
}

/// Setzt den Anzeigenamen eines Pools — reines pool.json-Update, ID/Ordner
/// (und damit Keychain-Suffix, Symlinks, Zuordnungen) bleiben unverändert.
fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
  check_name(name)?;
  let current = read_pool(paths, pool)?;
  for other in list_pools_in(paths)? {
    if other.id != pool && other.name == name {
      return Err(format!("Pool existiert bereits: {name}"));
    }
  }
  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)
}

-/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json.

-fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<(), String> {

+/// apiKeyHelper auf diese Datei, CLAUDE.md, pool.json. Liefert die Pool-ID.

+fn create_apikey_pool_in(paths: &Paths, name: &str, key: &str) -> Result<String, String> {

+  let id = dir.file_name().unwrap().to_string_lossy().into_owned();

-fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<(), String> {

+fn create_oauth_pool_in(paths: &Paths, name: &str) -> Result<String, String> {

+/// Setzt den Anzeigenamen eines Pools — reines pool.json-Update, ID/Ordner

+/// (und damit Keychain-Suffix, Symlinks, Zuordnungen) bleiben unverändert.

+fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {

+  let current = read_pool(paths, pool)?;

+    if other.id != pool && other.name == name {

+      return Err(format!("Pool existiert bereits: {name}"));

+  write_pool_json(&paths.pool_dir(pool), name, &current.credential_type)

/// Löscht einen Pool samt Ordner (inkl. Credentials). Zugeordnete Projekte

/// verlieren die Zuordnung (Terminal-Einstellungen bleiben erhalten). Den

/// Schutz gegen laufende Sessions setzt der delete_pool-Command davor.


  for pool_entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
    let pool_entry = pool_entry.map_err(|e| e.to_string())?;
    let pool = pool_entry.file_name().to_string_lossy().into_owned();
    let projects_root = pool_entry.path().join("projects");

  for pool_entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
    let pool_entry = pool_entry.map_err(|e| e.to_string())?;
    let id = pool_entry.file_name().to_string_lossy().into_owned();
    // Anzeigename aus pool.json; Ordner ohne pool.json unter der ID ausweisen.
    let pool = if pool_entry.path().join(POOL_FILE).is_file() {
      read_pool(paths, &id)?.name
    } else {
      id
    };
    let projects_root = pool_entry.path().join("projects");


  for pool_entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
    let pool_entry = pool_entry.map_err(|e| e.to_string())?;
    let pool = pool_entry.file_name().to_string_lossy().into_owned();
    let projects_root = pool_entry.path().join("projects");

  for pool_entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
    let pool_entry = pool_entry.map_err(|e| e.to_string())?;
    let id = pool_entry.file_name().to_string_lossy().into_owned();
    // Anzeigename aus pool.json; Ordner ohne pool.json unter der ID ausweisen.
    let pool = if pool_entry.path().join(POOL_FILE).is_file() {
      read_pool(paths, &id)?.name
    } else {
      id
    };
    let projects_root = pool_entry.path().join("projects");

-    let pool = pool_entry.file_name().to_string_lossy().into_owned();

+    let id = pool_entry.file_name().to_string_lossy().into_owned();

+    // Anzeigename aus pool.json; Ordner ohne pool.json unter der ID ausweisen.

+    let pool = if pool_entry.path().join(POOL_FILE).is_file() {


1080
1081	#[tauri::command]
1082	fn create_oauth_pool(name: String) -> Result<(), String> {
1083	  create_oauth_pool_in(&Paths::real(), &name)
1084	}
1085
1086	#[tauri::command]
1087	fn create_apikey_pool(name: String, key: String) -> Result<(), String> {
1088	  create_apikey_pool_in(&Paths::real(), &name, &key)
1089	}
1090
1091	#[tauri::command]
1092	fn delete_pool(name: String) -> Result<(), String> {
1093	  let paths = Paths::real();
1094	  let running = running_projects_using_pool(&paths, &name)?;
1095	  if !running.is_empty() {
1096	    return Err(format!(
1097	      "Pool wird noch benutzt — läuft: {}",
1098	      running.join(", ")
1099	    ));
1100	  }
1101	  delete_pool_in(&paths, &name)
1102	}
1103
1104	#[tauri::command]
1105	fn assign_pool(project: String, pool: String) -> Result<(), String> {
1106	  assign_pool_in(&Paths::real(), &project, &pool)
1107	}
1108
1109	#[tauri::command]
1110	fn unassign_pool(project: String) -> Result<(), String> {
1111	  unassign_pool_in(&Paths::real(), &project)
1112	}
1113
1114	#[tauri::command]
1115	fn usage_stats(days: u32) -> Result<Vec<UsageRow>, String> {
1116	  usage_stats_in(&Paths::real(), days)
1117	}
1118
1119	#[tauri::command]
1120	fn set_terminal_config(
1121	  project: String,
1122	  theme: Option<String>,
1123	  icon: Option<String>,
1124	  title: Option<String>,
1125	) -> Result<(), String> {
1126	  set_terminal_config_in(&Paths::real(), &project, TerminalConfig { theme, icon, title })
1127	}
1128
1129	#[tauri::command]
1130	fn set_apikey(pool: String, key: String) -> Result<(), String> {
1131	  set_apikey_in(&Paths::real(), &pool, &key)
1132	}
1133
1134	/// Keychain-Service-Name des Pools: claude legt pro CLAUDE_CONFIG_DIR einen
1135	/// suffixierten Eintrag an, Suffix = erste 8 Hex-Zeichen von SHA-256 über
1136	/// den Pool-Pfad.
1137	fn keychain_service(paths: &Paths, pool: &str) -> String {
1138	  let hash = Sha256::digest(paths.pool_dir(pool).to_string_lossy().as_bytes());
1139	  let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect();
1140	  format!("Claude Code-credentials-{}", &hex[..8])
1141	}
1142
1143	fn keychain_entry_exists(service: &str) -> Result<bool, String> {
1144	  let out = Command::new("security")
1145	    .args(["find-generic-password", "-s", service])
1146	    .output()
1147	    .map_err(|e| e.to_string())?;
1148	  Ok(out.status.success())
1149	}

#[tauri::command]
fn create_oauth_pool(name: String) -> Result<(), String> {
  create_oauth_pool_in(&Paths::real(), &name)
}

#[tauri::command]
fn create_apikey_pool(name: String, key: String) -> Result<(), String> {
  create_apikey_pool_in(&Paths::real(), &name, &key)
}

#[tauri::command]
fn delete_pool(name: String) -> Result<(), String> {
  let paths = Paths::real();
  let running = running_projects_using_pool(&paths, &name)?;
  if !running.is_empty() {
    return Err(format!(
      "Pool wird noch benutzt — läuft: {}",
      running.join(", ")
    ));
  }
  delete_pool_in(&paths, &name)
}

#[tauri::command]
fn assign_pool(project: String, pool: String) -> Result<(), String> {
  assign_pool_in(&Paths::real(), &project, &pool)
}

#[tauri::command]
fn unassign_pool(project: String) -> Result<(), String> {
  unassign_pool_in(&Paths::real(), &project)
}

#[tauri::command]
fn usage_stats(days: u32) -> Result<Vec<UsageRow>, String> {
  usage_stats_in(&Paths::real(), days)
}

#[tauri::command]
fn set_terminal_config(
  project: String,
  theme: Option<String>,
  icon: Option<String>,
  title: Option<String>,
) -> Result<(), String> {
  set_terminal_config_in(&Paths::real(), &project, TerminalConfig { theme, icon, title })
}

#[tauri::command]
fn set_apikey(pool: String, key: String) -> Result<(), String> {
  set_apikey_in(&Paths::real(), &pool, &key)
}

/// Keychain-Service-Name des Pools: claude legt pro CLAUDE_CONFIG_DIR einen
/// suffixierten Eintrag an, Suffix = erste 8 Hex-Zeichen von SHA-256 über
/// den Pool-Pfad.
fn keychain_service(paths: &Paths, pool: &str) -> String {
  let hash = Sha256::digest(paths.pool_dir(pool).to_string_lossy().as_bytes());
  let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect();
  format!("Claude Code-credentials-{}", &hex[..8])
}

fn keychain_entry_exists(service: &str) -> Result<bool, String> {
  let out = Command::new("security")
    .args(["find-generic-password", "-s", service])
    .output()
    .map_err(|e| e.to_string())?;
  Ok(out.status.success())
}


#[tauri::command]
fn create_oauth_pool(name: String) -> Result<(), String> {
  create_oauth_pool_in(&Paths::real(), &name)
}

#[tauri::command]
fn create_apikey_pool(name: String, key: String) -> Result<(), String> {
  create_apikey_pool_in(&Paths::real(), &name, &key)
}

#[tauri::command]
fn delete_pool(name: String) -> Result<(), String> {
  let paths = Paths::real();
  let running = running_projects_using_pool(&paths, &name)?;
  if !running.is_empty() {
    return Err(format!(
      "Pool wird noch benutzt — läuft: {}",
      running.join(", ")
    ));
  }
  delete_pool_in(&paths, &name)
}

#[tauri::command]
fn create_oauth_pool(name: String) -> Result<String, String> {
  create_oauth_pool_in(&Paths::real(), &name)
}

#[tauri::command]
fn create_apikey_pool(name: String, key: String) -> Result<String, String> {
  create_apikey_pool_in(&Paths::real(), &name, &key)
}

#[tauri::command]
fn rename_pool(pool: String, name: String) -> Result<(), String> {
  rename_pool_in(&Paths::real(), &pool, &name)
}

#[tauri::command]
fn delete_pool(pool: String) -> Result<(), String> {
  let paths = Paths::real();
  let running = running_projects_using_pool(&paths, &pool)?;
  if !running.is_empty() {
    return Err(format!(
      "Pool wird noch benutzt — läuft: {}",
      running.join(", ")
    ));
  }
  delete_pool_in(&paths, &pool)
}


#[tauri::command]
fn create_oauth_pool(name: String) -> Result<(), String> {
  create_oauth_pool_in(&Paths::real(), &name)
}

#[tauri::command]
fn create_apikey_pool(name: String, key: String) -> Result<(), String> {
  create_apikey_pool_in(&Paths::real(), &name, &key)
}

#[tauri::command]
fn delete_pool(name: String) -> Result<(), String> {
  let paths = Paths::real();
  let running = running_projects_using_pool(&paths, &name)?;
  if !running.is_empty() {
    return Err(format!(
      "Pool wird noch benutzt — läuft: {}",
      running.join(", ")
    ));
  }
  delete_pool_in(&paths, &name)
}

#[tauri::command]
fn create_oauth_pool(name: String) -> Result<String, String> {
  create_oauth_pool_in(&Paths::real(), &name)
}

#[tauri::command]
fn create_apikey_pool(name: String, key: String) -> Result<String, String> {
  create_apikey_pool_in(&Paths::real(), &name, &key)
}

#[tauri::command]
fn rename_pool(pool: String, name: String) -> Result<(), String> {
  rename_pool_in(&Paths::real(), &pool, &name)
}

#[tauri::command]
fn delete_pool(pool: String) -> Result<(), String> {
  let paths = Paths::real();
  let running = running_projects_using_pool(&paths, &pool)?;
  if !running.is_empty() {
    return Err(format!(
      "Pool wird noch benutzt — läuft: {}",
      running.join(", ")
    ));
  }
  delete_pool_in(&paths, &pool)
}

-fn create_oauth_pool(name: String) -> Result<(), String> {

+fn create_oauth_pool(name: String) -> Result<String, String> {

-fn create_apikey_pool(name: String, key: String) -> Result<(), String> {

+fn create_apikey_pool(name: String, key: String) -> Result<String, String> {

-fn delete_pool(name: String) -> Result<(), String> {

+fn rename_pool(pool: String, name: String) -> Result<(), String> {

+  rename_pool_in(&Paths::real(), &pool, &name)

+fn delete_pool(pool: String) -> Result<(), String> {

-  let running = running_projects_using_pool(&paths, &name)?;

+  let running = running_projects_using_pool(&paths, &pool)?;


Bash

grep -n "delete_pool,\|create_apikey_pool,\|rename_pool" /Users/marcus.hinz/projects/ai-control/src-tauri/src/lib.rs

Command-Registrierung finden


789:fn rename_pool_in(paths: &Paths, pool: &str, name: &str) -> Result<(), String> {
1092:fn rename_pool(pool: String, name: String) -> Result<(), String> {
1093:  rename_pool_in(&Paths::real(), &pool, &name)
1360:      create_apikey_pool,
1361:      delete_pool,


Edit

      create_apikey_pool,
      delete_pool,

      create_apikey_pool,
      rename_pool,
      delete_pool,


      create_apikey_pool,
      delete_pool,

      create_apikey_pool,
      rename_pool,
      delete_pool,


Jetzt die Tests anpassen (Ordner ist jetzt UUID, Helfer geben die ID zurück):


1414
1415	  static N: AtomicU32 = AtomicU32::new(0);
1416
1417	  /// Frisches Pseudo-Home pro Test; claude-projects existiert wie auf dem
1418	  /// echten System.
1419	  fn tmp_paths() -> Paths {
1420	    let dir = std::env::temp_dir().join(format!(
1421	      "ai-control-test-{}-{}",
1422	      std::process::id(),
1423	      N.fetch_add(1, Ordering::SeqCst)
1424	    ));
1425	    fs::create_dir_all(dir.join("claude-projects")).unwrap();
1426	    Paths { home: dir }
1427	  }
1428
1429	  fn make_oauth_pool(p: &Paths, name: &str) {
1430	    create_oauth_pool_in(p, name).unwrap();
1431	  }
1432
1433	  fn mode(path: &PathBuf) -> u32 {
1434	    fs::metadata(path).unwrap().permissions().mode() & 0o777
1435	  }
1436
1437	  // -- Pool anlegen --
1438
1439	  #[test]
1440	  fn apikey_pool_anlegen() {
1441	    let p = tmp_paths();
1442	    create_apikey_pool_in(&p, "kunde", "sk-test-123").unwrap();
1443	    let dir = p.pool_dir("kunde");
1444
1445	    let pool: Pool =
1446	      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
1447	    assert_eq!(pool.name, "kunde");
1448	    assert_eq!(pool.credential_type, "apikey");
1449
1450	    let key_path = dir.join(APIKEY_FILE);
1451	    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-test-123\n");
1452	    assert_eq!(mode(&key_path), 0o600);
1453
1454	    let settings: serde_json::Value =
1455	      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
1456	        .unwrap();
1457	    assert_eq!(
1458	      settings["apiKeyHelper"].as_str().unwrap(),
1459	      format!("cat '{}'", key_path.display())
1460	    );
1461	  }
1462
1463	  #[test]
1464	  fn oauth_pool_anlegen() {
1465	    let p = tmp_paths();
1466	    make_oauth_pool(&p, "privat");
1467	    let dir = p.pool_dir("privat");
1468
1469	    let pool: Pool =
1470	      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
1471	    assert_eq!(pool.credential_type, "oauth");
1472
1473	    // Kein Credentials-File — den Login macht claude selbst beim ersten Start.
1474	    assert!(!dir.join(".credentials.json").exists());
1475	    // Grundausstattung: settings.json mit aufgeräumten UI-Defaults + CLAUDE.md,
1476	    // damit claude nicht ins Onboarding fällt und keine Vorschlagstabelle zeigt.
1477	    let settings: serde_json::Value =
1478	      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
1479	        .unwrap();
1480	    assert_eq!(settings["promptSuggestionEnabled"], serde_json::json!(false));
1481	    assert_eq!(settings["awaySummaryEnabled"], serde_json::json!(false));
1482	    assert!(dir.join("CLAUDE.md").is_file());
1483	  }
1484
1485	  #[test]
1486	  fn pool_init_erhaelt_bestehende_claude_md() {
1487	    let p = tmp_paths();
1488	    let dir = p.pool_dir("privat");
1489	    fs::create_dir_all(&dir).unwrap();
1490	    fs::write(dir.join("CLAUDE.md"), "meine Vorgaben\n").unwrap();
1491	    // Anlegen darf die vorhandene CLAUDE.md nicht überschreiben.
1492	    // (check_new_pool erlaubt keinen bestehenden Ordner → direkt init testen)
1493	    init_pool_config(&dir, serde_json::json!({})).unwrap();
1494	    assert_eq!(
1495	      fs::read_to_string(dir.join("CLAUDE.md")).unwrap(),
1496	      "meine Vorgaben\n"
1497	    );
1498	  }
1499
1500	  #[test]
1501	  fn pool_doppelt_anlegen_scheitert() {
1502	    let p = tmp_paths();
1503	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1504	    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
1505	    assert!(err.contains("existiert bereits"));
1506	  }
1507
1508	  #[test]
1509	  fn pool_leerer_key_scheitert() {
1510	    let p = tmp_paths();
1511	    assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());
1512	  }
1513
1514	  #[test]
1515	  fn pool_name_mit_slash_scheitert() {
1516	    let p = tmp_paths();
1517	    assert!(create_apikey_pool_in(&p, "a/b", "sk-1").is_err());
1518	  }
1519
1520	  // -- Pool ändern --
1521
1522	  #[test]
1523	  fn apikey_aendern() {
1524	    let p = tmp_paths();
1525	    create_apikey_pool_in(&p, "kunde", "sk-alt").unwrap();
1526	    set_apikey_in(&p, "kunde", "sk-neu").unwrap();
1527	    let key_path = p.pool_dir("kunde").join(APIKEY_FILE);
1528	    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-neu\n");
1529	    assert_eq!(mode(&key_path), 0o600);
1530	  }
1531
1532	  #[test]
1533	  fn apikey_aendern_auf_oauth_pool_scheitert() {
1534	    let p = tmp_paths();
1535	    make_oauth_pool(&p, "privat");
1536	    assert!(set_apikey_in(&p, "privat", "sk-1").is_err());
1537	  }
1538
1539	  #[test]
1540	  fn ensure_oauth_pool_auf_apikey_scheitert() {
1541	    let p = tmp_paths();
1542	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1543	    assert!(ensure_oauth_pool(&p, "kunde").is_err());
1544	    make_oauth_pool(&p, "privat");
1545	    assert!(ensure_oauth_pool(&p, "privat").is_ok());
1546	  }
1547
1548	  // -- Pool löschen --
1549
1550	  #[test]
1551	  fn pool_loeschen() {
1552	    let p = tmp_paths();
1553	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1554	    delete_pool_in(&p, "kunde").unwrap();
1555	    assert!(!p.pool_dir("kunde").exists());
1556	  }
1557
1558	  // -- Projekt-Wizard --
1559
1560	  #[test]
1561	  fn projekt_wizard_scaffold() {
1562	    let p = tmp_paths();
1563	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1564	    create_project_full_in(
1565	      &p,
1566	      "neu",
1567	      Some("kunde"),
1568	      Some("~/projects/neu"),
1569	      true,
1570	      TerminalConfig {
1571	        theme: Some("dracula".into()),
1572	        icon: None,
1573	        title: Some("Neu".into()),
1574	      },
1575	      true,
1576	    )
1577	    .unwrap();
1578
1579	    let dir = p.projects_dir().join("neu");
1580	    assert!(dir.join("memory").is_dir());
1581	    assert_eq!(
1582	      fs::read_to_string(dir.join(".gitignore")).unwrap(),
1583	      ".ai-control-running\n"
1584	    );
1585	    assert!(p.home.join("projects").join("neu").is_dir());
1586
1587	    let cfg = read_project_config_in(&p, "neu").unwrap();
1588	    assert_eq!(cfg.pool.as_deref(), Some("kunde"));
1589	    assert_eq!(cfg.terminal.title.as_deref(), Some("Neu"));
1590	    assert_eq!(cfg.terminal.theme.as_deref(), Some("dracula"));
1591
1592	    let settings: serde_json::Value = serde_json::from_str(
1593	      &fs::read_to_string(dir.join(".claude").join("settings.json")).unwrap(),
1594	    )
1595	    .unwrap();
1596	    assert_eq!(settings["autoMemoryDirectory"], "~/claude-projects/neu/memory");
1597	    assert_eq!(settings["permissions"]["allow"][0], "Edit(~/projects/neu/**)");
1598	    assert_eq!(
1599	      settings["permissions"]["allow"][1],
1600	      "Edit(~/claude-projects/neu/**)"
1601	    );
1602	    assert_eq!(settings["permissions"]["additionalDirectories"][0], "~/projects/neu");
1603	    // todo=true: einziger SessionStart-Hook (kein pool-guard mehr), Datei da
1604	    assert!(dir.join(TODO_FILE).is_file());
1605	    let groups = settings["hooks"]["SessionStart"].as_array().unwrap();
1606	    assert_eq!(groups.len(), 1);
1607	    assert!(groups[0]["hooks"][0]["command"]
1608	      .as_str()
1609	      .unwrap()
1610	      .contains(TODO_FILE));
1611	  }
1612
1613	  #[test]

  static N: AtomicU32 = AtomicU32::new(0);

  /// Frisches Pseudo-Home pro Test; claude-projects existiert wie auf dem
  /// echten System.
  fn tmp_paths() -> Paths {
    let dir = std::env::temp_dir().join(format!(
      "ai-control-test-{}-{}",
      std::process::id(),
      N.fetch_add(1, Ordering::SeqCst)
    ));
    fs::create_dir_all(dir.join("claude-projects")).unwrap();
    Paths { home: dir }
  }

  fn make_oauth_pool(p: &Paths, name: &str) {
    create_oauth_pool_in(p, name).unwrap();
  }

  fn mode(path: &PathBuf) -> u32 {
    fs::metadata(path).unwrap().permissions().mode() & 0o777
  }

  // -- Pool anlegen --

  #[test]
  fn apikey_pool_anlegen() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-test-123").unwrap();
    let dir = p.pool_dir("kunde");

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

    let key_path = dir.join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-test-123\n");
    assert_eq!(mode(&key_path), 0o600);

    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(
      settings["apiKeyHelper"].as_str().unwrap(),
      format!("cat '{}'", key_path.display())
    );
  }

  #[test]
  fn oauth_pool_anlegen() {
    let p = tmp_paths();
    make_oauth_pool(&p, "privat");
    let dir = p.pool_dir("privat");

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.credential_type, "oauth");

    // Kein Credentials-File — den Login macht claude selbst beim ersten Start.
    assert!(!dir.join(".credentials.json").exists());
    // Grundausstattung: settings.json mit aufgeräumten UI-Defaults + CLAUDE.md,
    // damit claude nicht ins Onboarding fällt und keine Vorschlagstabelle zeigt.
    let settings: serde_json::Value =
      serde_json::from_str(&fs::read_to_string(dir.join("settings.json")).unwrap())
        .unwrap();
    assert_eq!(settings["promptSuggestionEnabled"], serde_json::json!(false));
    assert_eq!(settings["awaySummaryEnabled"], serde_json::json!(false));
    assert!(dir.join("CLAUDE.md").is_file());
  }

  #[test]
  fn pool_init_erhaelt_bestehende_claude_md() {
    let p = tmp_paths();
    let dir = p.pool_dir("privat");
    fs::create_dir_all(&dir).unwrap();
    fs::write(dir.join("CLAUDE.md"), "meine Vorgaben\n").unwrap();
    // Anlegen darf die vorhandene CLAUDE.md nicht überschreiben.
    // (check_new_pool erlaubt keinen bestehenden Ordner → direkt init testen)
    init_pool_config(&dir, serde_json::json!({})).unwrap();
    assert_eq!(
      fs::read_to_string(dir.join("CLAUDE.md")).unwrap(),
      "meine Vorgaben\n"
    );
  }

  #[test]
  fn pool_doppelt_anlegen_scheitert() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));
  }

  #[test]
  fn pool_leerer_key_scheitert() {
    let p = tmp_paths();
    assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());
  }

  #[test]
  fn pool_name_mit_slash_scheitert() {
    let p = tmp_paths();
    assert!(create_apikey_pool_in(&p, "a/b", "sk-1").is_err());
  }

  // -- Pool ändern --

  #[test]
  fn apikey_aendern() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-alt").unwrap();
    set_apikey_in(&p, "kunde", "sk-neu").unwrap();
    let key_path = p.pool_dir("kunde").join(APIKEY_FILE);
    assert_eq!(fs::read_to_string(&key_path).unwrap(), "sk-neu\n");
    assert_eq!(mode(&key_path), 0o600);
  }

  #[test]
  fn apikey_aendern_auf_oauth_pool_scheitert() {
    let p = tmp_paths();
    make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, "privat", "sk-1").is_err());
  }

  #[test]
  fn ensure_oauth_pool_auf_apikey_scheitert() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    assert!(ensure_oauth_pool(&p, "kunde").is_err());
    make_oauth_pool(&p, "privat");
    assert!(ensure_oauth_pool(&p, "privat").is_ok());
  }

  // -- Pool löschen --

  #[test]
  fn pool_loeschen() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    delete_pool_in(&p, "kunde").unwrap();
    assert!(!p.pool_dir("kunde").exists());
  }

  // -- Projekt-Wizard --

  #[test]
  fn projekt_wizard_scaffold() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_full_in(
      &p,
      "neu",
      Some("kunde"),
      Some("~/projects/neu"),
      true,
      TerminalConfig {
        theme: Some("dracula".into()),
        icon: None,
        title: Some("Neu".into()),
      },
      true,
    )
    .unwrap();

    let dir = p.projects_dir().join("neu");
    assert!(dir.join("memory").is_dir());
    assert_eq!(
      fs::read_to_string(dir.join(".gitignore")).unwrap(),
      ".ai-control-running\n"
    );
    assert!(p.home.join("projects").join("neu").is_dir());

    let cfg = read_project_config_in(&p, "neu").unwrap();
    assert_eq!(cfg.pool.as_deref(), Some("kunde"));
    assert_eq!(cfg.terminal.title.as_deref(), Some("Neu"));
    assert_eq!(cfg.terminal.theme.as_deref(), Some("dracula"));

    let settings: serde_json::Value = serde_json::from_str(
      &fs::read_to_string(dir.join(".claude").join("settings.json")).unwrap(),
    )
    .unwrap();
    assert_eq!(settings["autoMemoryDirectory"], "~/claude-projects/neu/memory");
    assert_eq!(settings["permissions"]["allow"][0], "Edit(~/projects/neu/**)");
    assert_eq!(
      settings["permissions"]["allow"][1],
      "Edit(~/claude-projects/neu/**)"
    );
    assert_eq!(settings["permissions"]["additionalDirectories"][0], "~/projects/neu");
    // todo=true: einziger SessionStart-Hook (kein pool-guard mehr), Datei da
    assert!(dir.join(TODO_FILE).is_file());
    let groups = settings["hooks"]["SessionStart"].as_array().unwrap();
    assert_eq!(groups.len(), 1);
    assert!(groups[0]["hooks"][0]["command"]
      .as_str()
      .unwrap()
      .contains(TODO_FILE));
  }

  #[test]


Bash

grep -n '"kunde"\|"privat"\|make_oauth_pool\|pool_dir(' /Users/marcus.hinz/projects/ai-control/src-tauri/src/lib.rs | awk -F: '$1 > 1400'

Alle Pool-Bezüge in Tests auflisten


1429:  fn make_oauth_pool(p: &Paths, name: &str) {
1442:    create_apikey_pool_in(&p, "kunde", "sk-test-123").unwrap();
1443:    let dir = p.pool_dir("kunde");
1447:    assert_eq!(pool.name, "kunde");
1466:    make_oauth_pool(&p, "privat");
1467:    let dir = p.pool_dir("privat");
1488:    let dir = p.pool_dir("privat");
1503:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1504:    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
1511:    assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());
1525:    create_apikey_pool_in(&p, "kunde", "sk-alt").unwrap();
1526:    set_apikey_in(&p, "kunde", "sk-neu").unwrap();
1527:    let key_path = p.pool_dir("kunde").join(APIKEY_FILE);
1535:    make_oauth_pool(&p, "privat");
1536:    assert!(set_apikey_in(&p, "privat", "sk-1").is_err());
1542:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1543:    assert!(ensure_oauth_pool(&p, "kunde").is_err());
1544:    make_oauth_pool(&p, "privat");
1545:    assert!(ensure_oauth_pool(&p, "privat").is_ok());
1553:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1554:    delete_pool_in(&p, "kunde").unwrap();
1555:    assert!(!p.pool_dir("kunde").exists());
1563:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1567:      Some("kunde"),
1588:    assert_eq!(cfg.pool.as_deref(), Some("kunde"));
1718:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1723:    let dir = p.pool_dir("kunde").join("projects").join(&encoded);
1744:    assert_eq!(r.pool, "kunde");
1756:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1781:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1782:    make_oauth_pool(&p, "privat");
1785:    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
1787:    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);
1789:    fs::write(p.pool_dir("kunde").join(APIKEY_FILE), "\n").unwrap();
1791:    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
1793:    fs::remove_file(p.pool_dir("kunde").join(APIKEY_FILE)).unwrap();
1795:    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
1801:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1803:    assign_pool_in(&p, "proj", "kunde").unwrap();
1804:    delete_pool_in(&p, "kunde").unwrap();
1805:    assert!(!p.pool_dir("kunde").exists());
1812:    create_oauth_pool_in(&p, "privat").unwrap();
1813:    let pooldir = p.pool_dir("privat");
1817:      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, "privat").join(name));
1818:      let target = pool_data_dir(&p, "privat").join(name);
1823:    assert!(p.projects_dir().join("pool").join("privat").is_dir());
1839:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1841:    assign_pool_in(&p, "proj", "kunde").unwrap();
1848:    delete_pool_in(&p, "kunde").unwrap();
1865:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1867:    assign_pool_in(&p, "proj", "kunde").unwrap();
1872:    assert_eq!(cfg.pool.as_deref(), Some("kunde"));
1888:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1890:    assign_pool_in(&p, "proj", "kunde").unwrap();
1907:    make_oauth_pool(&p, "privat");
1908:    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1911:    assign_pool_in(&p, "proj", "privat").unwrap();
1912:    assign_pool_in(&p, "proj", "kunde").unwrap();
1916:    assert_eq!(cfg, serde_json::json!({ "pool": "kunde" }));


1700	      "requestId": req,
1701	      "message": {
1702	        "id": id,
1703	        "model": model,
1704	        "usage": {
1705	          "input_tokens": input,
1706	          "output_tokens": output,
1707	          "cache_creation_input_tokens": 1_000_000,
1708	          "cache_read_input_tokens": 2_000_000
1709	        }
1710	      }
1711	    })
1712	    .to_string()
1713	  }
1714
1715	  #[test]
1716	  fn verbrauch_aggregation_dedup_und_zeitfilter() {
1717	    let p = tmp_paths();
1718	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1719	    create_project_in(&p, "proj").unwrap();
1720
1721	    let proj_path = p.projects_dir().join("proj");
1722	    let encoded = encode_project_path(&proj_path.to_string_lossy());
1723	    let dir = p.pool_dir("kunde").join("projects").join(&encoded);
1724	    fs::create_dir_all(&dir).unwrap();
1725
1726	    let lines = [
1727	      // Sonnet: 1M in ($3) + 1M out ($15) + 1M cache-write ($3.75) + 2M cache-read ($0.60)
1728	      usage_line("msg_1", "req_1", "2099-01-01T10:00:00.000Z", "claude-sonnet-5", 1_000_000, 1_000_000),
1729	      // Retry-Duplikat: gleiche message.id + requestId → zählt nicht
1730	      usage_line("msg_1", "req_1", "2099-01-01T10:00:01.000Z", "claude-sonnet-5", 1_000_000, 1_000_000),
1731	      // außerhalb des Zeitfensters → zählt nicht
1732	      usage_line("msg_2", "req_2", "2020-01-01T00:00:00.000Z", "claude-sonnet-5", 5, 5),
1733	      // kein usage-Objekt → ignoriert
1734	      r#"{"type":"user","timestamp":"2099-01-01T10:00:02.000Z"}"#.to_string(),
1735	      // kaputte Zeile (laufende Session) → ignoriert
1736	      r#"{"type":"assist"#.to_string(),
1737	    ];
1738	    fs::write(dir.join("session.jsonl"), lines.join("\n")).unwrap();
1739
1740	    // Cutoff = jetzt − 365 Tage: schließt 2020 aus, 2099 ein — datumsunabhängig
1741	    let rows = usage_stats_in(&p, 365).unwrap();
1742	    assert_eq!(rows.len(), 1);
1743	    let r = &rows[0];
1744	    assert_eq!(r.pool, "kunde");
1745	    assert_eq!(r.project, "proj");
1746	    assert_eq!(r.totals.input_tokens, 1_000_000);
1747	    assert_eq!(r.totals.output_tokens, 1_000_000);
1748	    assert_eq!(r.totals.cache_creation_tokens, 1_000_000);
1749	    assert_eq!(r.totals.cache_read_tokens, 2_000_000);
1750	    assert!((r.totals.cost_usd - 22.35).abs() < 1e-9);
1751	  }
1752
1753	  #[test]
1754	  fn verbrauch_leer_ohne_transcripts() {
1755	    let p = tmp_paths();
1756	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1757	    assert!(usage_stats_in(&p, 30).unwrap().is_empty());
1758	  }
1759
1760	  #[test]
1761	  fn parse_ts_referenz() {
1762	    // 2026-01-01 = 1767225600; +181 Tage bis 01.07. +10 h = 1782900000
1763	    assert_eq!(parse_ts("2026-07-01T10:00:00.000Z"), Some(1_782_900_000));
1764	    assert_eq!(parse_ts("kein datum"), None);
1765	  }
1766
1767	  /// Referenzwert vom echten System: privateDefault → 096c4ef9
1768	  /// (verifiziert 2026-07-03 gegen den von claude angelegten Eintrag).
1769	  #[test]
1770	  fn keychain_service_suffix() {
1771	    let p = Paths { home: PathBuf::from("/Users/marcus.hinz") };
1772	    assert_eq!(
1773	      keychain_service(&p, "privateDefault"),
1774	      "Claude Code-credentials-096c4ef9"
1775	    );
1776	  }
1777
1778	  #[test]
1779	  fn pool_key_status() {
1780	    let p = tmp_paths();
1781	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1782	    make_oauth_pool(&p, "privat");
1783
1784	    let pools = list_pools_in(&p).unwrap();
1785	    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
1786	    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
1787	    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);
1788
1789	    fs::write(p.pool_dir("kunde").join(APIKEY_FILE), "\n").unwrap();
1790	    let pools = list_pools_in(&p).unwrap();
1791	    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
1792
1793	    fs::remove_file(p.pool_dir("kunde").join(APIKEY_FILE)).unwrap();
1794	    let pools = list_pools_in(&p).unwrap();
1795	    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
1796	  }
1797
1798	  #[test]
1799	  fn pool_loeschen_loest_zuordnungen() {
1800	    let p = tmp_paths();
1801	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1802	    create_project_in(&p, "proj").unwrap();
1803	    assign_pool_in(&p, "proj", "kunde").unwrap();
1804	    delete_pool_in(&p, "kunde").unwrap();
1805	    assert!(!p.pool_dir("kunde").exists());
1806	    assert!(!p.project_config("proj").exists());
1807	  }
1808
1809	  #[test]
1810	  fn pool_anlegen_verlinkt_synced_runtime() {
1811	    let p = tmp_paths();
1812	    create_oauth_pool_in(&p, "privat").unwrap();
1813	    let pooldir = p.pool_dir("privat");
1814	    for (name, is_dir) in SYNCED_RUNTIME {
1815	      let link = pooldir.join(name);
1816	      assert!(link.is_symlink(), "{name} sollte Symlink sein");
1817	      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, "privat").join(name));
1818	      let target = pool_data_dir(&p, "privat").join(name);
1819	      assert_eq!(target.is_dir(), is_dir);
1820	      assert!(target.exists());
1821	    }
1822	    // Zieldaten liegen im claude-projects-Repo unter pool/<name>/
1823	    assert!(p.projects_dir().join("pool").join("privat").is_dir());
1824	  }
1825
1826	  #[test]
1827	  fn sync_optin_default_aus_und_umschaltbar() {
1828	    let p = tmp_paths();
1829	    assert!(!sync_on_session_end(&p)); // default: kein Sync ohne Zustimmung
1830	    set_sync_on_session_end_in(&p, true).unwrap();
1831	    assert!(sync_on_session_end(&p));
1832	    set_sync_on_session_end_in(&p, false).unwrap();
1833	    assert!(!sync_on_session_end(&p));
1834	  }
1835
1836	  #[test]
1837	  fn pool_loeschen_erhaelt_terminal_config() {
1838	    let p = tmp_paths();
1839	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1840	    create_project_in(&p, "proj").unwrap();
1841	    assign_pool_in(&p, "proj", "kunde").unwrap();
1842	    set_terminal_config_in(
1843	      &p,
1844	      "proj",
1845	      TerminalConfig { theme: Some("dracula".into()), icon: None, title: None },
1846	    )
1847	    .unwrap();
1848	    delete_pool_in(&p, "kunde").unwrap();
1849	    let cfg = read_project_config_in(&p, "proj").unwrap();
1850	    assert_eq!(cfg.pool, None);
1851	    assert_eq!(cfg.terminal.theme.as_deref(), Some("dracula"));
1852	  }
1853
1854	  #[test]
1855	  fn pool_loeschen_unbekannt_scheitert() {
1856	    let p = tmp_paths();
1857	    assert!(delete_pool_in(&p, "gibtsnicht").is_err());
1858	  }
1859
1860	  // -- Projekt zuordnen / rausnehmen / wechseln --
1861
1862	  #[test]
1863	  fn projekt_zuordnen() {
1864	    let p = tmp_paths();
1865	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1866	    create_project_in(&p, "proj").unwrap();
1867	    assign_pool_in(&p, "proj", "kunde").unwrap();
1868
1869	    let cfg: ProjectConfig =
1870	      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
1871	        .unwrap();
1872	    assert_eq!(cfg.pool.as_deref(), Some("kunde"));
1873
1874	    let pools = list_pools_in(&p).unwrap();
1875	    assert_eq!(pools[0].projects, vec!["proj"]);
1876	  }
1877
1878	  #[test]
1879	  fn projekt_zuordnen_pool_fehlt_scheitert() {
1880	    let p = tmp_paths();
1881	    create_project_in(&p, "proj").unwrap();
1882	    assert!(assign_pool_in(&p, "proj", "gibtsnicht").is_err());
1883	  }
1884
1885	  #[test]
1886	  fn projekt_rausnehmen() {
1887	    let p = tmp_paths();
1888	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1889	    create_project_in(&p, "proj").unwrap();
1890	    assign_pool_in(&p, "proj", "kunde").unwrap();
1891	    unassign_pool_in(&p, "proj").unwrap();
1892	    assert!(!p.project_config("proj").exists());
1893	  }
1894
1895	  #[test]
1896	  fn projekt_rausnehmen_ohne_zuordnung_scheitert() {
1897	    let p = tmp_paths();
1898	    create_project_in(&p, "proj").unwrap();
1899	    assert!(unassign_pool_in(&p, "proj").is_err());
1900	  }
1901
1902	  /// Wechsel oauth → apikey: das Projekt sieht nur den Pool-Namen,
1903	  /// der Credential-Typ ist ihm egal.
1904	  #[test]
1905	  fn pool_wechseln_typ_ist_projekt_egal() {
1906	    let p = tmp_paths();
1907	    make_oauth_pool(&p, "privat");
1908	    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
1909	    create_project_in(&p, "proj").unwrap();
1910
1911	    assign_pool_in(&p, "proj", "privat").unwrap();
1912	    assign_pool_in(&p, "proj", "kunde").unwrap();
1913
1914	    let raw = fs::read_to_string(p.project_config("proj")).unwrap();
1915	    let cfg: serde_json::Value = serde_json::from_str(&raw).unwrap();
1916	    assert_eq!(cfg, serde_json::json!({ "pool": "kunde" }));
1917	  }
1918
1919	  // -- Projekt anlegen / löschen --
1920
1921	  #[test]
1922	  fn projekt_anlegen() {
1923	    let p = tmp_paths();
1924	    create_project_in(&p, "proj").unwrap();
1925	    assert!(p.projects_dir().join("proj").join(".claude").is_dir());
1926
1927	    let projects = list_projects_in(&p).unwrap();
1928	    assert_eq!(projects.len(), 1);
1929	    assert_eq!(projects[0].name, "proj");
1930	    assert_eq!(projects[0].pool, None);
1931	  }
1932
1933	  #[test]
1934	  fn projekt_doppelt_anlegen_scheitert() {
1935	    let p = tmp_paths();
1936	    create_project_in(&p, "proj").unwrap();
1937	    assert!(create_project_in(&p, "proj").is_err());
1938	  }
1939

      "requestId": req,
      "message": {
        "id": id,
        "model": model,
        "usage": {
          "input_tokens": input,
          "output_tokens": output,
          "cache_creation_input_tokens": 1_000_000,
          "cache_read_input_tokens": 2_000_000
        }
      }
    })
    .to_string()
  }

  #[test]
  fn verbrauch_aggregation_dedup_und_zeitfilter() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();

    let proj_path = p.projects_dir().join("proj");
    let encoded = encode_project_path(&proj_path.to_string_lossy());
    let dir = p.pool_dir("kunde").join("projects").join(&encoded);
    fs::create_dir_all(&dir).unwrap();

    let lines = [
      // Sonnet: 1M in ($3) + 1M out ($15) + 1M cache-write ($3.75) + 2M cache-read ($0.60)
      usage_line("msg_1", "req_1", "2099-01-01T10:00:00.000Z", "claude-sonnet-5", 1_000_000, 1_000_000),
      // Retry-Duplikat: gleiche message.id + requestId → zählt nicht
      usage_line("msg_1", "req_1", "2099-01-01T10:00:01.000Z", "claude-sonnet-5", 1_000_000, 1_000_000),
      // außerhalb des Zeitfensters → zählt nicht
      usage_line("msg_2", "req_2", "2020-01-01T00:00:00.000Z", "claude-sonnet-5", 5, 5),
      // kein usage-Objekt → ignoriert
      r#"{"type":"user","timestamp":"2099-01-01T10:00:02.000Z"}"#.to_string(),
      // kaputte Zeile (laufende Session) → ignoriert
      r#"{"type":"assist"#.to_string(),
    ];
    fs::write(dir.join("session.jsonl"), lines.join("\n")).unwrap();

    // Cutoff = jetzt − 365 Tage: schließt 2020 aus, 2099 ein — datumsunabhängig
    let rows = usage_stats_in(&p, 365).unwrap();
    assert_eq!(rows.len(), 1);
    let r = &rows[0];
    assert_eq!(r.pool, "kunde");
    assert_eq!(r.project, "proj");
    assert_eq!(r.totals.input_tokens, 1_000_000);
    assert_eq!(r.totals.output_tokens, 1_000_000);
    assert_eq!(r.totals.cache_creation_tokens, 1_000_000);
    assert_eq!(r.totals.cache_read_tokens, 2_000_000);
    assert!((r.totals.cost_usd - 22.35).abs() < 1e-9);
  }

  #[test]
  fn verbrauch_leer_ohne_transcripts() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    assert!(usage_stats_in(&p, 30).unwrap().is_empty());
  }

  #[test]
  fn parse_ts_referenz() {
    // 2026-01-01 = 1767225600; +181 Tage bis 01.07. +10 h = 1782900000
    assert_eq!(parse_ts("2026-07-01T10:00:00.000Z"), Some(1_782_900_000));
    assert_eq!(parse_ts("kein datum"), None);
  }

  /// Referenzwert vom echten System: privateDefault → 096c4ef9
  /// (verifiziert 2026-07-03 gegen den von claude angelegten Eintrag).
  #[test]
  fn keychain_service_suffix() {
    let p = Paths { home: PathBuf::from("/Users/marcus.hinz") };
    assert_eq!(
      keychain_service(&p, "privateDefault"),
      "Claude Code-credentials-096c4ef9"
    );
  }

  #[test]
  fn pool_key_status() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    make_oauth_pool(&p, "privat");

    let pools = list_pools_in(&p).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);

    fs::write(p.pool_dir("kunde").join(APIKEY_FILE), "\n").unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

    fs::remove_file(p.pool_dir("kunde").join(APIKEY_FILE)).unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

  #[test]
  fn pool_loeschen_loest_zuordnungen() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();
    delete_pool_in(&p, "kunde").unwrap();
    assert!(!p.pool_dir("kunde").exists());
    assert!(!p.project_config("proj").exists());
  }

  #[test]
  fn pool_anlegen_verlinkt_synced_runtime() {
    let p = tmp_paths();
    create_oauth_pool_in(&p, "privat").unwrap();
    let pooldir = p.pool_dir("privat");
    for (name, is_dir) in SYNCED_RUNTIME {
      let link = pooldir.join(name);
      assert!(link.is_symlink(), "{name} sollte Symlink sein");
      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, "privat").join(name));
      let target = pool_data_dir(&p, "privat").join(name);
      assert_eq!(target.is_dir(), is_dir);
      assert!(target.exists());
    }
    // Zieldaten liegen im claude-projects-Repo unter pool/<name>/
    assert!(p.projects_dir().join("pool").join("privat").is_dir());
  }

  #[test]
  fn sync_optin_default_aus_und_umschaltbar() {
    let p = tmp_paths();
    assert!(!sync_on_session_end(&p)); // default: kein Sync ohne Zustimmung
    set_sync_on_session_end_in(&p, true).unwrap();
    assert!(sync_on_session_end(&p));
    set_sync_on_session_end_in(&p, false).unwrap();
    assert!(!sync_on_session_end(&p));
  }

  #[test]
  fn pool_loeschen_erhaelt_terminal_config() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();
    set_terminal_config_in(
      &p,
      "proj",
      TerminalConfig { theme: Some("dracula".into()), icon: None, title: None },
    )
    .unwrap();
    delete_pool_in(&p, "kunde").unwrap();
    let cfg = read_project_config_in(&p, "proj").unwrap();
    assert_eq!(cfg.pool, None);
    assert_eq!(cfg.terminal.theme.as_deref(), Some("dracula"));
  }

  #[test]
  fn pool_loeschen_unbekannt_scheitert() {
    let p = tmp_paths();
    assert!(delete_pool_in(&p, "gibtsnicht").is_err());
  }

  // -- Projekt zuordnen / rausnehmen / wechseln --

  #[test]
  fn projekt_zuordnen() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();

    let cfg: ProjectConfig =
      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
        .unwrap();
    assert_eq!(cfg.pool.as_deref(), Some("kunde"));

    let pools = list_pools_in(&p).unwrap();
    assert_eq!(pools[0].projects, vec!["proj"]);
  }

  #[test]
  fn projekt_zuordnen_pool_fehlt_scheitert() {
    let p = tmp_paths();
    create_project_in(&p, "proj").unwrap();
    assert!(assign_pool_in(&p, "proj", "gibtsnicht").is_err());
  }

  #[test]
  fn projekt_rausnehmen() {
    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();
    unassign_pool_in(&p, "proj").unwrap();
    assert!(!p.project_config("proj").exists());
  }

  #[test]
  fn projekt_rausnehmen_ohne_zuordnung_scheitert() {
    let p = tmp_paths();
    create_project_in(&p, "proj").unwrap();
    assert!(unassign_pool_in(&p, "proj").is_err());
  }

  /// Wechsel oauth → apikey: das Projekt sieht nur den Pool-Namen,
  /// der Credential-Typ ist ihm egal.
  #[test]
  fn pool_wechseln_typ_ist_projekt_egal() {
    let p = tmp_paths();
    make_oauth_pool(&p, "privat");
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();

    assign_pool_in(&p, "proj", "privat").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();

    let raw = fs::read_to_string(p.project_config("proj")).unwrap();
    let cfg: serde_json::Value = serde_json::from_str(&raw).unwrap();
    assert_eq!(cfg, serde_json::json!({ "pool": "kunde" }));
  }

  // -- Projekt anlegen / löschen --

  #[test]
  fn projekt_anlegen() {
    let p = tmp_paths();
    create_project_in(&p, "proj").unwrap();
    assert!(p.projects_dir().join("proj").join(".claude").is_dir());

    let projects = list_projects_in(&p).unwrap();
    assert_eq!(projects.len(), 1);
    assert_eq!(projects[0].name, "proj");
    assert_eq!(projects[0].pool, None);
  }

  #[test]
  fn projekt_doppelt_anlegen_scheitert() {
    let p = tmp_paths();
    create_project_in(&p, "proj").unwrap();
    assert!(create_project_in(&p, "proj").is_err());
  }


Edit

  fn make_oauth_pool(p: &Paths, name: &str) {
    create_oauth_pool_in(p, name).unwrap();
  }

  /// Liefert die Pool-ID (UUID-Ordnername).
  fn make_oauth_pool(p: &Paths, name: &str) -> String {
    create_oauth_pool_in(p, name).unwrap()
  }

  fn make_apikey_pool(p: &Paths, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, name, key).unwrap()
  }


  fn make_oauth_pool(p: &Paths, name: &str) {
    create_oauth_pool_in(p, name).unwrap();
  }

  /// Liefert die Pool-ID (UUID-Ordnername).
  fn make_oauth_pool(p: &Paths, name: &str) -> String {
    create_oauth_pool_in(p, name).unwrap()
  }

  fn make_apikey_pool(p: &Paths, name: &str, key: &str) -> String {
    create_apikey_pool_in(p, name, key).unwrap()
  }

-  fn make_oauth_pool(p: &Paths, name: &str) {

-    create_oauth_pool_in(p, name).unwrap();

+  /// Liefert die Pool-ID (UUID-Ordnername).

+  fn make_oauth_pool(p: &Paths, name: &str) -> String {

+    create_oauth_pool_in(p, name).unwrap()

+  fn make_apikey_pool(p: &Paths, name: &str, key: &str) -> String {

+    create_apikey_pool_in(p, name, key).unwrap()


    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-test-123").unwrap();
    let dir = p.pool_dir("kunde");

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-test-123");
    // Ordnername ist eine UUID, der Anzeigename steht nur in pool.json.
    assert!(uuid::Uuid::parse_str(&id).is_ok());
    let dir = p.pool_dir(&id);

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");


    let p = tmp_paths();
    create_apikey_pool_in(&p, "kunde", "sk-test-123").unwrap();
    let dir = p.pool_dir("kunde");

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-test-123");
    // Ordnername ist eine UUID, der Anzeigename steht nur in pool.json.
    assert!(uuid::Uuid::parse_str(&id).is_ok());
    let dir = p.pool_dir(&id);

    let pool: Pool =
      serde_json::from_str(&fs::read_to_string(dir.join(POOL_FILE)).unwrap()).unwrap();
    assert_eq!(pool.name, "kunde");
    assert_eq!(pool.credential_type, "apikey");

-    create_apikey_pool_in(&p, "kunde", "sk-test-123").unwrap();

+    let id = make_apikey_pool(&p, "kunde", "sk-test-123");

+    // Ordnername ist eine UUID, der Anzeigename steht nur in pool.json.

+    assert!(uuid::Uuid::parse_str(&id).is_ok());


    let p = tmp_paths();
    make_oauth_pool(&p, "privat");
    let dir = p.pool_dir("privat");

    let pool: Pool =

    let p = tmp_paths();
    let id = make_oauth_pool(&p, "privat");
    let dir = p.pool_dir(&id);

    let pool: Pool =


    let p = tmp_paths();
    make_oauth_pool(&p, "privat");
    let dir = p.pool_dir("privat");

    let pool: Pool =

    let p = tmp_paths();
    let id = make_oauth_pool(&p, "privat");
    let dir = p.pool_dir(&id);

    let pool: Pool =

+    let id = make_oauth_pool(&p, "privat");


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));
  }

    make_apikey_pool(&p, "kunde", "sk-1");
    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));
  }

  // -- Pool umbenennen --

  #[test]
  fn pool_umbenennen() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &id).unwrap();

    rename_pool_in(&p, &id, "kunde-neu").unwrap();

    // Nur der Anzeigename ändert sich — Ordner, Typ und Zuordnung bleiben.
    let pool = read_pool(&p, &id).unwrap();
    assert_eq!(pool.name, "kunde-neu");
    assert_eq!(pool.credential_type, "apikey");
    assert!(p.pool_dir(&id).is_dir());
    let cfg = read_project_config_in(&p, "proj").unwrap();
    assert_eq!(cfg.pool.as_deref(), Some(id.as_str()));
  }

  #[test]
  fn pool_umbenennen_auf_vergebenen_namen_scheitert() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");
    let err = rename_pool_in(&p, &id, "privat").unwrap_err();
    assert!(err.contains("existiert bereits"));
    // Umbenennen auf den eigenen Namen ist erlaubt (No-op).
    rename_pool_in(&p, &id, "kunde").unwrap();
  }


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));
  }

    make_apikey_pool(&p, "kunde", "sk-1");
    let err = create_apikey_pool_in(&p, "kunde", "sk-2").unwrap_err();
    assert!(err.contains("existiert bereits"));
  }

  // -- Pool umbenennen --

  #[test]
  fn pool_umbenennen() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &id).unwrap();

    rename_pool_in(&p, &id, "kunde-neu").unwrap();

    // Nur der Anzeigename ändert sich — Ordner, Typ und Zuordnung bleiben.
    let pool = read_pool(&p, &id).unwrap();
    assert_eq!(pool.name, "kunde-neu");
    assert_eq!(pool.credential_type, "apikey");
    assert!(p.pool_dir(&id).is_dir());
    let cfg = read_project_config_in(&p, "proj").unwrap();
    assert_eq!(cfg.pool.as_deref(), Some(id.as_str()));
  }

  #[test]
  fn pool_umbenennen_auf_vergebenen_namen_scheitert() {
    let p = tmp_paths();
    let id = make_apikey_pool(&p, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");
    let err = rename_pool_in(&p, &id, "privat").unwrap_err();
    assert!(err.contains("existiert bereits"));
    // Umbenennen auf den eigenen Namen ist erlaubt (No-op).
    rename_pool_in(&p, &id, "kunde").unwrap();
  }

-    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();

+    make_apikey_pool(&p, "kunde", "sk-1");

+    let id = make_apikey_pool(&p, "kunde", "sk-1");

+    create_project_in(&p, "proj").unwrap();

+    assign_pool_in(&p, "proj", &id).unwrap();

+    rename_pool_in(&p, &id, "kunde-neu").unwrap();

+    // Nur der Anzeigename ändert sich — Ordner, Typ und Zuordnung bleiben.

+    let pool = read_pool(&p, &id).unwrap();

+    assert_eq!(pool.name, "kunde-neu");

+    assert_eq!(pool.credential_type, "apikey");

+    let cfg = read_project_config_in(&p, "proj").unwrap();

+    assert_eq!(cfg.pool.as_deref(), Some(id.as_str()));

+  fn pool_umbenennen_auf_vergebenen_namen_scheitert() {

+    let err = rename_pool_in(&p, &id, "privat").unwrap_err();

+    assert!(err.contains("existiert bereits"));

+    // Umbenennen auf den eigenen Namen ist erlaubt (No-op).

+    rename_pool_in(&p, &id, "kunde").unwrap();

assert!(create_apikey_pool_in(&p, "kunde", "  ").is_err());


    create_apikey_pool_in(&p, "kunde", "sk-alt").unwrap();
    set_apikey_in(&p, "kunde", "sk-neu").unwrap();
    let key_path = p.pool_dir("kunde").join(APIKEY_FILE);

    let id = make_apikey_pool(&p, "kunde", "sk-alt");
    set_apikey_in(&p, &id, "sk-neu").unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);


    create_apikey_pool_in(&p, "kunde", "sk-alt").unwrap();
    set_apikey_in(&p, "kunde", "sk-neu").unwrap();
    let key_path = p.pool_dir("kunde").join(APIKEY_FILE);

    let id = make_apikey_pool(&p, "kunde", "sk-alt");
    set_apikey_in(&p, &id, "sk-neu").unwrap();
    let key_path = p.pool_dir(&id).join(APIKEY_FILE);

-    create_apikey_pool_in(&p, "kunde", "sk-alt").unwrap();

-    set_apikey_in(&p, "kunde", "sk-neu").unwrap();

-    let key_path = p.pool_dir("kunde").join(APIKEY_FILE);

+    let id = make_apikey_pool(&p, "kunde", "sk-alt");

+    set_apikey_in(&p, &id, "sk-neu").unwrap();

+    let key_path = p.pool_dir(&id).join(APIKEY_FILE);


    make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, "privat", "sk-1").is_err());
  }

    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &id, "sk-1").is_err());
  }


    make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, "privat", "sk-1").is_err());
  }

    let id = make_oauth_pool(&p, "privat");
    assert!(set_apikey_in(&p, &id, "sk-1").is_err());
  }

-    assert!(set_apikey_in(&p, "privat", "sk-1").is_err());

+    let id = make_oauth_pool(&p, "privat");

+    assert!(set_apikey_in(&p, &id, "sk-1").is_err());


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    assert!(ensure_oauth_pool(&p, "kunde").is_err());
    make_oauth_pool(&p, "privat");
    assert!(ensure_oauth_pool(&p, "privat").is_ok());
  }

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    assert!(ensure_oauth_pool(&p, &kunde).is_err());
    let privat = make_oauth_pool(&p, "privat");
    assert!(ensure_oauth_pool(&p, &privat).is_ok());
  }


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    assert!(ensure_oauth_pool(&p, "kunde").is_err());
    make_oauth_pool(&p, "privat");
    assert!(ensure_oauth_pool(&p, "privat").is_ok());
  }

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    assert!(ensure_oauth_pool(&p, &kunde).is_err());
    let privat = make_oauth_pool(&p, "privat");
    assert!(ensure_oauth_pool(&p, &privat).is_ok());
  }

-    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();

-    assert!(ensure_oauth_pool(&p, "kunde").is_err());

-    assert!(ensure_oauth_pool(&p, "privat").is_ok());

+    let kunde = make_apikey_pool(&p, "kunde", "sk-1");

+    assert!(ensure_oauth_pool(&p, &kunde).is_err());

+    let privat = make_oauth_pool(&p, "privat");

+    assert!(ensure_oauth_pool(&p, &privat).is_ok());


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    delete_pool_in(&p, "kunde").unwrap();
    assert!(!p.pool_dir("kunde").exists());
  }

    let id = make_apikey_pool(&p, "kunde", "sk-1");
    delete_pool_in(&p, &id).unwrap();
    assert!(!p.pool_dir(&id).exists());
  }


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    delete_pool_in(&p, "kunde").unwrap();
    assert!(!p.pool_dir("kunde").exists());
  }

    let id = make_apikey_pool(&p, "kunde", "sk-1");
    delete_pool_in(&p, &id).unwrap();
    assert!(!p.pool_dir(&id).exists());
  }

-    delete_pool_in(&p, "kunde").unwrap();

-    assert!(!p.pool_dir("kunde").exists());

+    let id = make_apikey_pool(&p, "kunde", "sk-1");

+    assert!(!p.pool_dir(&id).exists());


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_full_in(
      &p,
      "neu",
      Some("kunde"),

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_full_in(
      &p,
      "neu",
      Some(&kunde),


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_full_in(
      &p,
      "neu",
      Some("kunde"),

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_full_in(
      &p,
      "neu",
      Some(&kunde),


    let cfg = read_project_config_in(&p, "neu").unwrap();
    assert_eq!(cfg.pool.as_deref(), Some("kunde"));

    let cfg = read_project_config_in(&p, "neu").unwrap();
    assert_eq!(cfg.pool.as_deref(), Some(kunde.as_str()));


    let cfg = read_project_config_in(&p, "neu").unwrap();
    assert_eq!(cfg.pool.as_deref(), Some("kunde"));

    let cfg = read_project_config_in(&p, "neu").unwrap();
    assert_eq!(cfg.pool.as_deref(), Some(kunde.as_str()));

-    assert_eq!(cfg.pool.as_deref(), Some("kunde"));

+    assert_eq!(cfg.pool.as_deref(), Some(kunde.as_str()));


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();

    let proj_path = p.projects_dir().join("proj");
    let encoded = encode_project_path(&proj_path.to_string_lossy());
    let dir = p.pool_dir("kunde").join("projects").join(&encoded);

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    let proj_path = p.projects_dir().join("proj");
    let encoded = encode_project_path(&proj_path.to_string_lossy());
    let dir = p.pool_dir(&kunde).join("projects").join(&encoded);


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();

    let proj_path = p.projects_dir().join("proj");
    let encoded = encode_project_path(&proj_path.to_string_lossy());
    let dir = p.pool_dir("kunde").join("projects").join(&encoded);

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    let proj_path = p.projects_dir().join("proj");
    let encoded = encode_project_path(&proj_path.to_string_lossy());
    let dir = p.pool_dir(&kunde).join("projects").join(&encoded);

-    let dir = p.pool_dir("kunde").join("projects").join(&encoded);

+    let dir = p.pool_dir(&kunde).join("projects").join(&encoded);


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    assert!(usage_stats_in(&p, 30).unwrap().is_empty());

    make_apikey_pool(&p, "kunde", "sk-1");
    assert!(usage_stats_in(&p, 30).unwrap().is_empty());


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    assert!(usage_stats_in(&p, 30).unwrap().is_empty());

    make_apikey_pool(&p, "kunde", "sk-1");
    assert!(usage_stats_in(&p, 30).unwrap().is_empty());

-    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();

+    make_apikey_pool(&p, "kunde", "sk-1");


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    make_oauth_pool(&p, "privat");

    let pools = list_pools_in(&p).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);

    fs::write(p.pool_dir("kunde").join(APIKEY_FILE), "\n").unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

    fs::remove_file(p.pool_dir("kunde").join(APIKEY_FILE)).unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");

    let pools = list_pools_in(&p).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);

    fs::write(p.pool_dir(&kunde).join(APIKEY_FILE), "\n").unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

    fs::remove_file(p.pool_dir(&kunde).join(APIKEY_FILE)).unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    make_oauth_pool(&p, "privat");

    let pools = list_pools_in(&p).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);

    fs::write(p.pool_dir("kunde").join(APIKEY_FILE), "\n").unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

    fs::remove_file(p.pool_dir("kunde").join(APIKEY_FILE)).unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    make_oauth_pool(&p, "privat");

    let pools = list_pools_in(&p).unwrap();
    assert!(pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
    // oauth: Keychain wird im Listing bewusst nicht geprüft → immer true.
    assert!(pools.iter().find(|x| x.name == "privat").unwrap().has_credentials);

    fs::write(p.pool_dir(&kunde).join(APIKEY_FILE), "\n").unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);

    fs::remove_file(p.pool_dir(&kunde).join(APIKEY_FILE)).unwrap();
    let pools = list_pools_in(&p).unwrap();
    assert!(!pools.iter().find(|x| x.name == "kunde").unwrap().has_credentials);
  }

+    let kunde = make_apikey_pool(&p, "kunde", "sk-1");

-    fs::write(p.pool_dir("kunde").join(APIKEY_FILE), "\n").unwrap();

+    fs::write(p.pool_dir(&kunde).join(APIKEY_FILE), "\n").unwrap();

-    fs::remove_file(p.pool_dir("kunde").join(APIKEY_FILE)).unwrap();

+    fs::remove_file(p.pool_dir(&kunde).join(APIKEY_FILE)).unwrap();


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();
    delete_pool_in(&p, "kunde").unwrap();
    assert!(!p.pool_dir("kunde").exists());
    assert!(!p.project_config("proj").exists());

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    delete_pool_in(&p, &kunde).unwrap();
    assert!(!p.pool_dir(&kunde).exists());
    assert!(!p.project_config("proj").exists());


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();
    delete_pool_in(&p, "kunde").unwrap();
    assert!(!p.pool_dir("kunde").exists());
    assert!(!p.project_config("proj").exists());

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    delete_pool_in(&p, &kunde).unwrap();
    assert!(!p.pool_dir(&kunde).exists());
    assert!(!p.project_config("proj").exists());

-    assign_pool_in(&p, "proj", "kunde").unwrap();

-    delete_pool_in(&p, "kunde").unwrap();

-    assert!(!p.pool_dir("kunde").exists());

+    assign_pool_in(&p, "proj", &kunde).unwrap();

+    delete_pool_in(&p, &kunde).unwrap();

+    assert!(!p.pool_dir(&kunde).exists());


    create_oauth_pool_in(&p, "privat").unwrap();
    let pooldir = p.pool_dir("privat");
    for (name, is_dir) in SYNCED_RUNTIME {
      let link = pooldir.join(name);
      assert!(link.is_symlink(), "{name} sollte Symlink sein");
      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, "privat").join(name));
      let target = pool_data_dir(&p, "privat").join(name);
      assert_eq!(target.is_dir(), is_dir);
      assert!(target.exists());
    }
    // Zieldaten liegen im claude-projects-Repo unter pool/<name>/
    assert!(p.projects_dir().join("pool").join("privat").is_dir());

    let id = make_oauth_pool(&p, "privat");
    let pooldir = p.pool_dir(&id);
    for (name, is_dir) in SYNCED_RUNTIME {
      let link = pooldir.join(name);
      assert!(link.is_symlink(), "{name} sollte Symlink sein");
      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, &id).join(name));
      let target = pool_data_dir(&p, &id).join(name);
      assert_eq!(target.is_dir(), is_dir);
      assert!(target.exists());
    }
    // Zieldaten liegen im claude-projects-Repo unter pool/<id>/
    assert!(p.projects_dir().join("pool").join(&id).is_dir());


    create_oauth_pool_in(&p, "privat").unwrap();
    let pooldir = p.pool_dir("privat");
    for (name, is_dir) in SYNCED_RUNTIME {
      let link = pooldir.join(name);
      assert!(link.is_symlink(), "{name} sollte Symlink sein");
      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, "privat").join(name));
      let target = pool_data_dir(&p, "privat").join(name);
      assert_eq!(target.is_dir(), is_dir);
      assert!(target.exists());
    }
    // Zieldaten liegen im claude-projects-Repo unter pool/<name>/
    assert!(p.projects_dir().join("pool").join("privat").is_dir());

    let id = make_oauth_pool(&p, "privat");
    let pooldir = p.pool_dir(&id);
    for (name, is_dir) in SYNCED_RUNTIME {
      let link = pooldir.join(name);
      assert!(link.is_symlink(), "{name} sollte Symlink sein");
      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, &id).join(name));
      let target = pool_data_dir(&p, &id).join(name);
      assert_eq!(target.is_dir(), is_dir);
      assert!(target.exists());
    }
    // Zieldaten liegen im claude-projects-Repo unter pool/<id>/
    assert!(p.projects_dir().join("pool").join(&id).is_dir());

-    create_oauth_pool_in(&p, "privat").unwrap();

-    let pooldir = p.pool_dir("privat");

+    let id = make_oauth_pool(&p, "privat");

-      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, "privat").join(name));

-      let target = pool_data_dir(&p, "privat").join(name);

+      assert_eq!(fs::read_link(&link).unwrap(), pool_data_dir(&p, &id).join(name));

+      let target = pool_data_dir(&p, &id).join(name);

-    // Zieldaten liegen im claude-projects-Repo unter pool/<name>/

-    assert!(p.projects_dir().join("pool").join("privat").is_dir());

+    // Zieldaten liegen im claude-projects-Repo unter pool/<id>/

+    assert!(p.projects_dir().join("pool").join(&id).is_dir());


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();
    set_terminal_config_in(

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    set_terminal_config_in(


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();
    set_terminal_config_in(

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    set_terminal_config_in(

-    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();

+    let kunde = make_apikey_pool(&p, "kunde", "sk-1");

-    assign_pool_in(&p, "proj", "kunde").unwrap();

+    assign_pool_in(&p, "proj", &kunde).unwrap();


    )
    .unwrap();
    delete_pool_in(&p, "kunde").unwrap();
    let cfg = read_project_config_in(&p, "proj").unwrap();

    )
    .unwrap();
    delete_pool_in(&p, &kunde).unwrap();
    let cfg = read_project_config_in(&p, "proj").unwrap();


    )
    .unwrap();
    delete_pool_in(&p, "kunde").unwrap();
    let cfg = read_project_config_in(&p, "proj").unwrap();

    )
    .unwrap();
    delete_pool_in(&p, &kunde).unwrap();
    let cfg = read_project_config_in(&p, "proj").unwrap();

TerminalConfig { theme: Some("dracula".into()), icon: None, title: None },

-    delete_pool_in(&p, "kunde").unwrap();

+    delete_pool_in(&p, &kunde).unwrap();


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();

    let cfg: ProjectConfig =
      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
        .unwrap();
    assert_eq!(cfg.pool.as_deref(), Some("kunde"));

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();

    let cfg: ProjectConfig =
      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
        .unwrap();
    assert_eq!(cfg.pool.as_deref(), Some(kunde.as_str()));


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();

    let cfg: ProjectConfig =
      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
        .unwrap();
    assert_eq!(cfg.pool.as_deref(), Some("kunde"));

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();

    let cfg: ProjectConfig =
      serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
        .unwrap();
    assert_eq!(cfg.pool.as_deref(), Some(kunde.as_str()));

-    assert_eq!(cfg.pool.as_deref(), Some("kunde"));

+    assert_eq!(cfg.pool.as_deref(), Some(kunde.as_str()));


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();
    unassign_pool_in(&p, "proj").unwrap();

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    unassign_pool_in(&p, "proj").unwrap();


    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();
    unassign_pool_in(&p, "proj").unwrap();

    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();
    unassign_pool_in(&p, "proj").unwrap();


    make_oauth_pool(&p, "privat");
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();

    assign_pool_in(&p, "proj", "privat").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();

    let raw = fs::read_to_string(p.project_config("proj")).unwrap();
    let cfg: serde_json::Value = serde_json::from_str(&raw).unwrap();
    assert_eq!(cfg, serde_json::json!({ "pool": "kunde" }));

    let privat = make_oauth_pool(&p, "privat");
    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    assign_pool_in(&p, "proj", &privat).unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();

    let raw = fs::read_to_string(p.project_config("proj")).unwrap();
    let cfg: serde_json::Value = serde_json::from_str(&raw).unwrap();
    assert_eq!(cfg, serde_json::json!({ "pool": kunde }));


    make_oauth_pool(&p, "privat");
    create_apikey_pool_in(&p, "kunde", "sk-1").unwrap();
    create_project_in(&p, "proj").unwrap();

    assign_pool_in(&p, "proj", "privat").unwrap();
    assign_pool_in(&p, "proj", "kunde").unwrap();

    let raw = fs::read_to_string(p.project_config("proj")).unwrap();
    let cfg: serde_json::Value = serde_json::from_str(&raw).unwrap();
    assert_eq!(cfg, serde_json::json!({ "pool": "kunde" }));

    let privat = make_oauth_pool(&p, "privat");
    let kunde = make_apikey_pool(&p, "kunde", "sk-1");
    create_project_in(&p, "proj").unwrap();

    assign_pool_in(&p, "proj", &privat).unwrap();
    assign_pool_in(&p, "proj", &kunde).unwrap();

    let raw = fs::read_to_string(p.project_config("proj")).unwrap();
    let cfg: serde_json::Value = serde_json::from_str(&raw).unwrap();
    assert_eq!(cfg, serde_json::json!({ "pool": kunde }));

+    let privat = make_oauth_pool(&p, "privat");

-    assign_pool_in(&p, "proj", "privat").unwrap();

+    assign_pool_in(&p, "proj", &privat).unwrap();

-    assert_eq!(cfg, serde_json::json!({ "pool": "kunde" }));

+    assert_eq!(cfg, serde_json::json!({ "pool": kunde }));


Rust-Seite fertig; jetzt kompilieren und testen:


Bash

cd /Users/marcus.hinz/projects/ai-control/src-tauri && CARGO_HOME=~/tools/.cargo ~/tools/.cargo/bin/cargo test 2>&1 | tail -25

Rust-Tests laufen lassen


error: rustup could not choose a version of cargo to run, because one wasn't specified explicitly, and no default is configured.
help: run 'rustup default stable' to download the latest stable release of Rust and set it as your default toolchain.


cat /Users/marcus.hinz/projects/ai-control/build.sh 2>/dev/null || ls /Users/marcus.hinz/projects/ai-control

build.sh ansehen


#!/usr/bin/env bash
set -euo pipefail

DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

export CARGO_HOME="$HOME/tools/.cargo"
export RUSTUP_HOME="$HOME/tools/.rustup"
export PATH="$CARGO_HOME/bin:$PATH"

cd "$DIR"
npm run tauri build -- --bundles app "$@"

# Dev-Bundle in ein .noindex-Verzeichnis schieben und aus Launch Services
# austragen — Spotlight indiziert .noindex-Ordner nicht, damit erscheint im
# Starter/Spotlight nur die installierte Kopie unter ~/Applications.
BUNDLE="$DIR/src-tauri/target/release/bundle/macos"
NOINDEX="$DIR/src-tauri/target/release/bundle/macos.noindex"
LSREGISTER=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister
# lsregister -u meldet -10814, wenn das Bundle nicht registriert ist —
# erwartetes Ergebnis, kein Fehler.
"$LSREGISTER" -u "$BUNDLE/ai-control.app" || true
rm -rf "$NOINDEX/ai-control.app"
mkdir -p "$NOINDEX"
mv "$BUNDLE/ai-control.app" "$NOINDEX/"
"$LSREGISTER" -u "$NOINDEX/ai-control.app" || true


CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test 2>&1 | tail -25

Rust-Tests mit RUSTUP_HOME laufen lassen


failures:
    tests::apikey_aendern
    tests::apikey_aendern_auf_oauth_pool_scheitert
    tests::apikey_pool_anlegen
    tests::ensure_oauth_pool_auf_apikey_scheitert
    tests::oauth_pool_anlegen
    tests::pool_anlegen_verlinkt_synced_runtime
    tests::pool_doppelt_anlegen_scheitert
    tests::pool_key_status
    tests::pool_loeschen
    tests::pool_loeschen_erhaelt_terminal_config
    tests::pool_loeschen_loest_zuordnungen
    tests::pool_umbenennen
    tests::pool_umbenennen_auf_vergebenen_namen_scheitert
    tests::pool_wechseln_typ_ist_projekt_egal
    tests::projekt_rausnehmen
    tests::projekt_wizard_scaffold
    tests::projekt_zuordnen
    tests::verbrauch_aggregation_dedup_und_zeitfilter
    tests::verbrauch_leer_ohne_transcripts

test result: FAILED. 22 passed; 19 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.80s

error: test failed, to rerun pass `--lib`


CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test --lib tests::apikey_pool_anlegen 2>&1 | grep -B2 -A10 "panicked"

Fehlerursache eines Tests ansehen


---- tests::apikey_pool_anlegen stdout ----

thread 'tests::apikey_pool_anlegen' (296035) panicked at src/lib.rs:1435:41:
called `Result::unwrap()` on an `Err` value: "No such file or directory (os error 2)"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    tests::apikey_pool_anlegen

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 40 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`


`check_new_pool` listet jetzt die Pools, aber beim allerersten Pool existiert `pools/` noch nicht. Fix:


Edit

fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  for pool in list_pools_in(paths)? {
    if pool.name == name {
      return Err(format!("Pool existiert bereits: {name}"));
    }
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}

fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  // Beim ersten Pool existiert pools/ noch nicht — dann ist kein Name vergeben.
  if paths.pools_dir().is_dir() {
    for pool in list_pools_in(paths)? {
      if pool.name == name {
        return Err(format!("Pool existiert bereits: {name}"));
      }
    }
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}


fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  for pool in list_pools_in(paths)? {
    if pool.name == name {
      return Err(format!("Pool existiert bereits: {name}"));
    }
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}

fn check_new_pool(paths: &Paths, name: &str) -> Result<PathBuf, String> {
  check_name(name)?;
  // Beim ersten Pool existiert pools/ noch nicht — dann ist kein Name vergeben.
  if paths.pools_dir().is_dir() {
    for pool in list_pools_in(paths)? {
      if pool.name == name {
        return Err(format!("Pool existiert bereits: {name}"));
      }
    }
  }
  Ok(paths.pool_dir(&uuid::Uuid::new_v4().to_string()))
}

/// Ordner für einen neuen Pool: pools/<UUID v4>.

-      return Err(format!("Pool existiert bereits: {name}"));

+  // Beim ersten Pool existiert pools/ noch nicht — dann ist kein Name vergeben.

+    for pool in list_pools_in(paths)? {

+        return Err(format!("Pool existiert bereits: {name}"));


Bash

CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test 2>&1 | tail -5

Rust-Tests erneut laufen lassen


running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


CARGO_HOME=~/tools/.cargo RUSTUP_HOME=~/tools/.rustup PATH=~/tools/.cargo/bin:$PATH cargo test 2>&1 | grep "test result"

Testergebnis komplett anzeigen


test result: ok. 41 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s