API & browser testing · agent-native
detesting.ai turns an OpenAPI or Postman spec into multi-step test suites that chain variables, assert on outcomes rather than status codes, and run either on our servers or on your machine — from Claude through 68 MCP tools, or from a shell with dt.
Two ways in
Both interfaces write to the same database and execute through the same @detesting.ai/engine package, so a suite reaches the same verdict wherever it runs. A CLI run shows up in the dashboard like any other, tagged source: CLI.
Typed, project-scoped, no shell. Collections, environments, suites, cases, runs, browser tests, schedules, notifications, visual baselines, coverage and strategies. Context resolves itself — one collection per project means you almost never pass an ID.
Ten project- and API-key-management tools exist only on the stdio transport. Over HTTP the project comes from your API key, so a project-scoped key must not be able to mint credentials or act elsewhere.
dt <noun> <verb> [target]Verbs are the same on every noun — ls · show · create · edit · rm, plus run on suites and tests. Because execution hangs off the noun, run stays free as the noun for history: dt run show 214.
dt suite run auth --env local dt test run --failed --env local dt suite run --all --env ci -o junit dt run show 214 --failures
dt bundles the same engine the server runs, so tests execute on the machine you are sitting at — against localhost or anything else the internet cannot reach — while results still record centrally.
--local runs here and stores the result (the default). --remote hands execution to the server. --offline runs here and records nothing.
The SSRF policy applies to local runs too. Reaching localhost works only because the environment sets allowInternalTargets — sitting on a developer machine is not an exemption.
The model
Step 1 extracts a token, step 2 spends it. Extractions propagate across test cases inside a sequential suite, so a create → read → update → delete lifecycle is one honest scenario instead of four disconnected requests.
Upload a Postman collection or OpenAPI document and the endpoints, request schemas, declared status codes and response shapes are stored — which is what makes generation and coverage measurement possible later.
URLs and credentials live in environments, never in steps, so the same suite runs against local, staging and CI. Clone one to make the next; dt env diff staging prod answers "why does it pass here and fail there".
create_test_from_endpoint writes a step from what the spec already declares — happy, auth, validation or not-found. The validation scenario omits one required field rather than sending nonsense, so validation is under test and not the parser.
The most common failure here is a variable that never resolved. A dry run resolves the whole precedence chain and reports where every value came from, checks each assertion type has a handler and each JSONPath parses, applies the SSRF policy — and sends nothing. Every problem it reports is a harness problem.
Runs are numbered and keep step-level request, response, assertion and variable-snapshot detail. Failures carry their own history, and any two runs can be diffed.
Variables
Values resolve in a fixed precedence. What a response gives back outranks everything you configured — which is exactly what makes chained steps work.
| Priority | Layer | Typically holds |
|---|---|---|
| 1 | Environment | baseUrl, credentials, feature flags |
| 2 | Suite overrides | Config that belongs to one group of tests |
| 3 | Test case | Defaults for a single scenario |
| 4 | Runtime | Values passed in when the run starts |
| 5 | Extracted | Whatever the last response returned — highest priority |
Extract with a JSONPath, spend it by name in the next step or the next test.
variableExtractions: [
{ variableName: "orderId",
jsonPath: "$.id" }
]
url: "{{baseUrl}}/orders/{{orderId}}"
Faker is built in, so tests stop colliding over the same fixture email.
{{faker.internet.email}}
{{faker.person.fullName}}
{{faker.number.int({min:1,max:99})}}
Steps can be skipped on the state of a variable, the page or a previous result — variable_equals, variable_gt, element_visible, url_contains, combined with and / or.
Assertions
Status 200 is not a passing test. Assert on the body, the shape, the DOM, the console, the network and the clock — with field paths in JSONPath and selectors that fall back.
shape.matches records the response shape the first time it sees it and passes. After that it fails when a field disappears or changes type. Added fields are reported but do not fail — an API that grows a field has usually not broken its consumers.
{
"name": "Create order",
"order": 0,
"request": {
"method": "POST",
"url": "{{baseUrl}}/orders",
"headers": [{ "key": "Authorization",
"value": "Bearer {{authToken}}" }],
"body": { "sku": "AX-9", "qty": 2 }
},
"assertions": [
{ "type": "status", "value": 201 },
{ "type": "equals", "field": "$.status",
"value": "pending" },
{ "type": "shape.matches" }
],
"variableExtractions": [
{ "variableName": "orderId", "jsonPath": "$.id" }
]
}
Browser testing
Browser suites use the Chrome Recorder step format, so a recorded flow imports directly — then gets the same variables, conditions and assertions as an API test.
Switch per execution. Stealth mode exists for sites behind bot detection; everything else is faster under Playwright.
Each step carries an ordered list of selectors and the engine tries them until one matches. forceJsClick dispatches the pointer events React, Vue and Svelte handlers actually listen for.
"selectors": [ ["[data-testid='submit']"], ["#submit-btn"], ["button[type='submit']"] ]
Steps capture full-page screenshots; runs keep console output and network activity. Visual baselines live in the database, so baseline assertions run on the server — a local run reports them as unavailable rather than passing blind.
Reading a run
Forty long-standing failures are very good at hiding the one regression that matters. Failures are matched by signature — test, step, assertion type and path — so a different actual value is recognised as the same failure, not a new one.
Each failure can be annotated with how often it has failed in the recent window, when it was first seen, its flakiness score, and whether the previous run failed the same way.
get_run({ runId, detail: 'failures',
history: true })
diff_test_runs({ runA: 748, runB: 749 })
Endpoint coverage calls GET /users covered the moment one test asserts a 200. This measures against every status code the spec declares. Endpoints with no documented responses are reported as unmeasurable rather than 0% — that would be a claim about the endpoint, not about your tests.
POST /orders 201 400 401 409 2 of 4 GET /orders/:id 200 404 403 1 of 3
Twelve static checks grade every test case 0–100 at creation: no assertions, no status check, tautologies like exists $ or matches .*, unparseable JSONPath, use-before-define variables, fragile positional selectors. Execution then adds dynamic signals — tests that always pass, tests that flake.
Mark an extraction as a resource and the run records it the moment it exists, so even a cancelled run can be swept afterwards with dt run cleanup <#>. Teardown covers the suite that finished; this covers the one that did not.
resource: {
type: "order",
deleteVia: "DELETE {{baseUrl}}/orders/{{id}}"
}
Cron schedules run suites against an environment on a timezone you set, and can be paused and resumed. Results reach a webhook signed with an HMAC secret, or Telegram, on the events you pick: passed, failed, error, partial, completed.
CI
A missing variable is not a bug in your API, and a pipeline should not report it as one. Telling a broken system apart from a broken harness without parsing text is the whole reason this distinction exists.
Every test in the selection asserted successfully.
The system under test is broken. This is the signal that should block a merge.
A missing variable, an SSRF block, an unreachable host. Fix the test setup, not the API.
Coloured tables when stdout is a terminal; json, junit, tap or markdown when it is not. Colour disappears on redirect, so piping gives you a file rather than escape codes.
dt suite run --all --env ci -o junit dt suite ls -o json | jq dt suite ls -q | xargs -n1 ...
dt whoami says where each value came fromMost specific wins: --project for one invocation, then ./.detesting.json searched upward, then the active context, then global config. When a command touches the wrong project, that ordering is the question you actually have.
Getting started
The dashboard signs in through BitBot. Everything programmatic authenticates with a dtst_ API key you generate there.
dt, bind a directoryNode 20 or newer. The tarball is about 108 KB and is served next to the installer rather than published to npm, so there is no registry account involved. Chromium for browser tests is roughly 300 MB and stays opt-in: dt browser install.
robot.detesting.ai/mcprobot.detesting.ai/cli/install.shdtst_ API keys, scoped to one projectNode.js and Express with MongoDB behind the API; a SvelteKit dashboard with live run updates over WebSocket; Playwright and Puppeteer for browser work; @detesting.ai/engine shared by the server and the CLI so both reach the same verdict.
Pruebas de API y navegador · nativo para agentes
detesting.ai convierte una especificación OpenAPI o Postman en suites de varios pasos que encadenan variables, verifican comportamientos en vez de códigos de estado, y se ejecutan en nuestros servidores o en tu máquina — desde Claude con 68 herramientas MCP, o desde una terminal con dt.
Dos formas de entrar
Las dos interfaces escriben en la misma base de datos y ejecutan con el mismo paquete @detesting.ai/engine, así que una suite llega al mismo veredicto sin importar dónde corra. Una ejecución desde la CLI aparece en el panel como cualquier otra, marcada source: CLI.
Tipadas, acotadas al proyecto, sin terminal. Colecciones, entornos, suites, casos, ejecuciones, pruebas de navegador, programaciones, notificaciones, líneas base visuales, cobertura y estrategias. El contexto se resuelve solo: una colección por proyecto significa que casi nunca pasas un ID.
Diez herramientas de gestión de proyectos y llaves de API existen únicamente en el transporte stdio. Por HTTP el proyecto sale de tu llave de API, así que una llave acotada a un proyecto no debe poder emitir credenciales ni actuar fuera de él.
dt <sustantivo> <verbo> [objetivo]Los verbos son los mismos en cada sustantivo — ls · show · create · edit · rm, más run en suites y pruebas. Como la ejecución cuelga del sustantivo, la palabra run queda libre como sustantivo para el historial: dt run show 214.
dt suite run auth --env local dt test run --failed --env local dt suite run --all --env ci -o junit dt run show 214 --failures
dt incluye el mismo motor que corre el servidor, así que las pruebas se ejecutan en la máquina donde estás — contra localhost o cualquier cosa que internet no alcance — y el resultado se registra igual en el servidor.
--local ejecuta aquí y guarda el resultado (predeterminado). --remote deja la ejecución al servidor. --offline ejecuta aquí y no registra nada.
La política contra SSRF también aplica en local. Llegar a localhost funciona solo porque el entorno declara allowInternalTargets: estar en una máquina de desarrollo no es una excepción.
El modelo
El paso 1 extrae un token y el paso 2 lo gasta. Las extracciones se propagan entre casos dentro de una suite secuencial, así que un ciclo crear → leer → actualizar → borrar es un solo escenario honesto en lugar de cuatro peticiones sueltas.
Sube una colección de Postman o un documento OpenAPI y quedan guardados los endpoints, los esquemas de petición, los códigos de estado declarados y las formas de respuesta — que es lo que después hace posible generar pruebas y medir cobertura.
Las URLs y las credenciales viven en entornos, nunca en los pasos, así la misma suite corre contra local, staging y CI. Clona uno para crear el siguiente; dt env diff staging prod responde “¿por qué pasa aquí y falla allá?”.
create_test_from_endpoint escribe un paso a partir de lo que la especificación ya declara — happy, auth, validation o not-found. El escenario de validación omite un campo obligatorio en vez de mandar basura, así lo que se prueba es la validación y no el parser.
Aquí la falla más común es una variable que nunca se resolvió. Una corrida en seco resuelve toda la cadena de precedencia e informa de dónde salió cada valor, comprueba que cada tipo de aserción tenga implementación y que cada JSONPath sea válido, aplica la política contra SSRF — y no envía nada. Todo lo que reporta es un problema del arnés.
Las ejecuciones se numeran y guardan el detalle de cada paso: petición, respuesta, aserciones y foto de las variables. Cada falla trae su historial y dos ejecuciones cualesquiera se pueden comparar.
Variables
Los valores se resuelven en una precedencia fija. Lo que devuelve una respuesta le gana a todo lo que configuraste — que es justo lo que hace funcionar a los pasos encadenados.
| Prioridad | Capa | Suele contener |
|---|---|---|
| 1 | Entorno | baseUrl, credenciales, banderas |
| 2 | Suite | Configuración que pertenece a un grupo de pruebas |
| 3 | Caso de prueba | Valores por omisión de un escenario |
| 4 | Tiempo de ejecución | Valores que pasas al arrancar la ejecución |
| 5 | Extraídas | Lo que devolvió la última respuesta — máxima prioridad |
Extrae con un JSONPath y gástalo por nombre en el siguiente paso o en la siguiente prueba.
variableExtractions: [
{ variableName: "orderId",
jsonPath: "$.id" }
]
url: "{{baseUrl}}/orders/{{orderId}}"
Faker viene integrado, así las pruebas dejan de pelearse por el mismo correo de prueba.
{{faker.internet.email}}
{{faker.person.fullName}}
{{faker.number.int({min:1,max:99})}}
Un paso se puede saltar según el estado de una variable, de la página o de un resultado anterior — variable_equals, variable_gt, element_visible, url_contains, combinables con and / or.
Aserciones
Un 200 no es una prueba aprobada. Verifica el cuerpo, la forma, el DOM, la consola, la red y el reloj — con rutas JSONPath y selectores que tienen alternativas.
shape.matches registra la forma de la respuesta la primera vez que la ve y aprueba. A partir de ahí falla cuando un campo desaparece o cambia de tipo. Los campos nuevos se reportan pero no fallan: una API que gana un campo normalmente no rompió a sus consumidores.
{
"name": "Crear orden",
"order": 0,
"request": {
"method": "POST",
"url": "{{baseUrl}}/orders",
"headers": [{ "key": "Authorization",
"value": "Bearer {{authToken}}" }],
"body": { "sku": "AX-9", "qty": 2 }
},
"assertions": [
{ "type": "status", "value": 201 },
{ "type": "equals", "field": "$.status",
"value": "pending" },
{ "type": "shape.matches" }
],
"variableExtractions": [
{ "variableName": "orderId", "jsonPath": "$.id" }
]
}
Pruebas de navegador
Las suites de navegador usan el formato de pasos de Chrome Recorder, así que un flujo grabado se importa directo — y después recibe las mismas variables, condiciones y aserciones que una prueba de API.
Se elige por ejecución. El modo stealth existe para sitios con detección de bots; para todo lo demás Playwright es más rápido.
Cada paso lleva una lista ordenada de selectores y el motor los prueba hasta que uno coincide. forceJsClick dispara los eventos de puntero que los manejadores de React, Vue y Svelte sí escuchan.
"selectors": [ ["[data-testid='submit']"], ["#submit-btn"], ["button[type='submit']"] ]
Los pasos capturan pantallazos de página completa y la ejecución guarda consola y actividad de red. Las líneas base visuales viven en la base de datos, así que esas aserciones corren en el servidor — una ejecución local las reporta como no disponibles en lugar de aprobar a ciegas.
Leer una ejecución
Cuarenta fallas de siempre son buenísimas escondiendo la única regresión que importa. Las fallas se emparejan por firma — prueba, paso, tipo de aserción y ruta — así que un valor distinto se reconoce como la misma falla y no como una nueva.
Cada falla puede venir anotada con cuántas veces falló en la ventana reciente, cuándo se vio por primera vez, su puntaje de inestabilidad y si la ejecución anterior falló igual.
get_run({ runId, detail: 'failures',
history: true })
diff_test_runs({ runA: 748, runB: 749 })
La cobertura por endpoint da por cubierto GET /users en cuanto una prueba verifica un 200. Esto mide contra cada código de estado que declara la especificación. Los endpoints sin respuestas documentadas se reportan como no medibles en vez de 0%: eso sería una afirmación sobre el endpoint, no sobre tus pruebas.
POST /orders 201 400 401 409 2 de 4 GET /orders/:id 200 404 403 1 de 3
Doce comprobaciones estáticas califican cada caso de 0 a 100 al crearlo: sin aserciones, sin verificación de estado, tautologías como exists $ o matches .*, JSONPath inválido, variables usadas antes de existir, selectores posicionales frágiles. La ejecución agrega señales dinámicas — pruebas que siempre pasan, pruebas inestables.
Marca una extracción como recurso y la ejecución lo registra en el momento en que existe, así incluso una ejecución cancelada se puede barrer después con dt run cleanup <#>. El teardown cubre la suite que terminó; esto cubre la que no.
resource: {
type: "order",
deleteVia: "DELETE {{baseUrl}}/orders/{{id}}"
}
Las programaciones cron corren suites contra un entorno en la zona horaria que definas, y se pueden pausar y reanudar. El resultado llega a un webhook firmado con un secreto HMAC, o a Telegram, en los eventos que elijas: aprobada, fallida, error, parcial, completada.
CI
Una variable faltante no es un bug de tu API y el pipeline no debería reportarla como tal. Distinguir un sistema roto de un arnés roto sin parsear texto es toda la razón de esta diferencia.
Cada prueba de la selección verificó correctamente.
El sistema bajo prueba está roto. Esta es la señal que debe bloquear un merge.
Una variable faltante, un bloqueo por SSRF, un host inalcanzable. Arregla la preparación de la prueba, no la API.
Tablas con color cuando stdout es una terminal; json, junit, tap o markdown cuando no lo es. El color desaparece al redirigir, así que un pipe te da un archivo y no códigos de escape.
dt suite run --all --env ci -o junit dt suite ls -o json | jq dt suite ls -q | xargs -n1 ...
dt whoami dice de dónde vino cada valorGana lo más específico: --project para una sola invocación, luego ./.detesting.json buscado hacia arriba, luego el contexto activo, luego la configuración global. Cuando un comando toca el proyecto equivocado, ese orden es justo lo que necesitas ver.
Empezar
El panel entra con BitBot. Todo lo programático se autentica con una llave de API dtst_ que generas ahí mismo.
dt y vincula un directorioNode 20 o superior. El tarball pesa unos 108 KB y se sirve junto al instalador en lugar de publicarse en npm, así que no hay cuenta de registro de por medio. Chromium para pruebas de navegador pesa unos 300 MB y queda opcional: dt browser install.
robot.detesting.ai/mcprobot.detesting.ai/cli/install.shdtst_, acotadas a un proyectoNode.js y Express con MongoDB detrás de la API; un panel en SvelteKit con actualizaciones de ejecución en vivo por WebSocket; Playwright y Puppeteer para el navegador; @detesting.ai/engine compartido por el servidor y la CLI para que ambos lleguen al mismo veredicto.