Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ These sit alongside the generated API commands in the same group, which own the
primitives — `elevenlabs agents create | get | list | update | delete`, and the
`elevenlabs agents branches ...` subgroup for branch management.

> The `tests` command group, `residency`, and `components add` are being ported from v0 and will land in follow-up changes.
> `residency` and `components add` are being ported from v0 and will land in follow-up changes.

### Tools

Expand All @@ -133,6 +133,26 @@ elevenlabs tools delete <tool_id>
elevenlabs tools delete --all
```

### Tests

Manage agent tests, tracked in `tests.json` with configs under `test_configs/`. Attach them to an agent's `platform_settings.testing.attached_tests` and run them with `elevenlabs agents test <agent_id>`.

```bash
# Create a test from a template, upload it, and register it in tests.json
elevenlabs tests add <name> [--template basic-llm|tool|conversation-flow|customer-service]
elevenlabs tests templates list

# Sync test configs with ElevenLabs
elevenlabs tests push [--test <test_id>] [--config-dir test_configs] [--dry-run]
elevenlabs tests pull [--test <test_id>] [--output-dir test_configs] [--update] [--all] [--dry-run]

# Delete a test locally and in ElevenLabs
elevenlabs tests delete <test_id>
elevenlabs tests delete --all
```

`tests push` also **auto-discovers** untracked configs: it scans `--config-dir` recursively for `.json` files that look like tests (a `chat_history` array or a `success_condition` string) and registers them in `tests.json` before pushing, so you can drop a config in and push without editing the index by hand.

### Templates

Pre-built starting configurations for `agents add`, listed by `elevenlabs agents templates list` (inspect one with `agents templates show <template>`):
Expand Down
80 changes: 80 additions & 0 deletions cli/elevenlabs/workflow/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,86 @@ pub fn delete_tool(ctx: &AppContext, tool_id: &str) -> Result<(), CliError> {
Ok(())
}

// ── Test API ────────────────────────────────────────────────────────

/// Create a test (raw JSON). Returns the full response, containing the new `id`.
pub fn create_test(ctx: &AppContext, config: &Value) -> Result<Value, CliError> {
raw_request(
ctx,
Method::POST,
"v1/convai/agent-testing/create",
Some(config.clone()),
None,
)
}

/// Update a test (raw JSON).
pub fn update_test(ctx: &AppContext, test_id: &str, config: &Value) -> Result<Value, CliError> {
raw_request(
ctx,
Method::PUT,
&format!("v1/convai/agent-testing/{test_id}"),
Some(config.clone()),
None,
)
}

/// Fetch a test's full config (raw JSON).
pub fn get_test(ctx: &AppContext, test_id: &str) -> Result<Value, CliError> {
raw_request(
ctx,
Method::GET,
&format!("v1/convai/agent-testing/{test_id}"),
None,
None,
)
}

/// List every test, paginating to completion.
pub fn list_tests(ctx: &AppContext) -> Result<Vec<Value>, CliError> {
let mut all = Vec::new();
let mut cursor: Option<String> = None;
loop {
let mut query = vec![("page_size".to_string(), "100".to_string())];
if let Some(c) = &cursor {
query.push(("cursor".to_string(), c.clone()));
}
let resp = raw_request(
ctx,
Method::GET,
"v1/convai/agent-testing",
None,
Some(query),
)?;
if let Some(tests) = resp.get("tests").and_then(Value::as_array) {
all.extend(tests.iter().cloned());
}
if !resp.get("has_more").and_then(Value::as_bool).unwrap_or(false) {
break;
}
cursor = resp
.get("next_cursor")
.and_then(Value::as_str)
.map(String::from);
if cursor.is_none() {
break;
}
}
Ok(all)
}

/// Delete a test.
pub fn delete_test(ctx: &AppContext, test_id: &str) -> Result<(), CliError> {
raw_request(
ctx,
Method::DELETE,
&format!("v1/convai/agent-testing/{test_id}"),
None,
None,
)?;
Ok(())
}

// ── Test-running API (for `agents test`) ────────────────────────────

/// Run the given tests on an agent. Returns the invocation (raw JSON).
Expand Down
37 changes: 37 additions & 0 deletions cli/elevenlabs/workflow/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,43 @@ pub fn save_tests(config: &TestsConfig) -> Result<(), CliError> {
write_json(&tests_path(), config)
}

// ── Config discovery ────────────────────────────────────────────────

/// Recursively collect every `.json` file under `dir`, sorted, with paths
/// normalized to forward slashes so index files stay portable across
/// platforms. Returns empty when the directory doesn't exist. Ports v0's
/// `discoverJsonFiles`.
pub fn discover_json_files(dir: &str) -> Vec<String> {
let root = Path::new(dir);
if !root.exists() {
return Vec::new();
}
let mut found = Vec::new();
walk_json(root, &mut found);
found.sort();
found
}

fn walk_json(dir: &Path, found: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk_json(&path, found);
} else if path.extension().and_then(|e| e.to_str()) == Some("json") {
// Normalize to posix separators for portable index entries.
let as_posix = path
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/");
found.push(as_posix);
}
}
}

// ── Interactive prompt ──────────────────────────────────────────────

/// Ask a `y/N` question on stdin. Ports v0's `promptForConfirmation`:
Expand Down
Loading
Loading