API & browser testing · agent-native

Tests an agent can write, and CI can trust.

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.

$ curl -fsSL https://robot.detesting.ai/cli/install.sh | bash
dt suite run --all --env ci -o junit
environment ci (project: payments · resolved from ./.detesting.json) auth 8 tests 8 passed 2.4s orders 14 tests 14 passed 6.1s billing 9 tests 1 failed 8 passed 4.8s refund exceeds balance → step 2 “POST /refunds” status expected 409 actual 500 first seen run 741 · failed 3 of last 20 · flakiness 0.11 31 tests · 30 passed · 1 failed · 13.3s · run #749 junit written to ./dt-results.xml exit 1 — a test failed; the API is broken, not the harness
68MCP tools over HTTP
52assertion types
2interfaces, one engine
0·1·2exit codes CI can read

Two ways in

The agent and the shell run the same engine.

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.

MCP · robot.detesting.ai/mcp

68 tools for Claude and other agents

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.

CLI · dt 2.0

One grammar: 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
Local execution

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.

Where it executes

--local runs here and stores the result (the default). --remote hands execution to the server. --offline runs here and records nothing.

Guard rails

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

A test is a chain, not a request.

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.

Project — the boundary everything is scoped to ├── Collection one per project · Postman v2.0/2.1 · OpenAPI 3.0.x · Swagger 2.0 │ └── Endpoints[] the API surface under test ├── Environments[] baseUrl, credentials, flags — cloneable, diffable ├── Test suites[] api or browser · sequential or parallel · setup + teardown │ └── Test cases[] │ └── Steps[] request → assertions[] → variableExtractions[] └── Test runs[] numbered, replayable, diffable, cleanable
01

Import the spec

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.

02

Describe the environments

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".

03

Generate, then edit

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.

04

Preview before you spend a run

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.

05

Run, then find out what changed

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

Five layers, and the run tells you which one won.

Values resolve in a fixed precedence. What a response gives back outranks everything you configured — which is exactly what makes chained steps work.

PriorityLayerTypically holds
1Environment

baseUrl, credentials, feature flags

2Suite overrides

Config that belongs to one group of tests

3Test case

Defaults for a single scenario

4Runtime

Values passed in when the run starts

5Extracted

Whatever the last response returned — highest priority

Chaining

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}}"
Generated data

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})}}
Conditions

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

52 ways to be specific about what "passed" means.

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.

Response · 21

statusstatus.rangeexistsnotExistsequalscontainsmatchespatternfieldgreaterThangreaterThanOrEquallessThanlessThanOrEqualinRangeinisArrayisObjectnotEmptyarrayLengthresponseTimeschemashape.matches

Browser · 31

element.existselement.not.existselement.visibleelement.hiddenelement.enabledelement.disabledelement.text.equalselement.text.containselement.text.matcheselement.value.equalselement.attribute.equalselement.countelement.hasClasspage.title.equalspage.title.containspage.url.equalspage.url.containspage.url.not.containspage.url.matchespage.content.containspage.content.not.containspage.content.matchesconsole.contains.errorconsole.not.contains.errorconsole.contains.warningnetwork.request.madenetwork.response.statusperformance.load.timeperformance.first.contentful.paintscreenshot.matchesvisual.matches_baseline
Shape drift

One assertion that outlives your field list

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.

A step, in full
{
  "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

Record in Chrome, replay under Playwright.

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.

Two engines

Playwright by default, Puppeteer-stealth when you're being blocked

Switch per execution. Stealth mode exists for sites behind bot detection; everything else is faster under Playwright.

Selectors

Fallback chains, not one brittle string

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']"]
]
Evidence

Screenshots, console, network, baselines

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

A red build should say what is new.

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.

History on every failure

Is this new, or is it Tuesday?

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 })
Behaviour coverage

Outcomes covered, not endpoints touched

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
Test quality

Scored when written, re-scored when run

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.

Fixtures

Setup and teardown with rules that hold

  • A setup failure runs no tests — there is no point asserting against a system that could not be prepared.
  • Teardown still runs after a failed setup, because setup may have created something first.
  • Teardown never turns a passing suite red; its result is reported separately.
  • Parallel mode refuses a suite whose tests feed each other, naming the coupled tests — move the shared work into setup.
Cleanup

Sweep what a crashed run created

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}}"
}
Unattended

Schedules and notifications

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

Three exit codes, because two is a lie.

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.

0
Everything passed

Every test in the selection asserted successfully.

1
A test failed

The system under test is broken. This is the signal that should block a merge.

2
The harness failed

A missing variable, an SSRF block, an unreachable host. Fix the test setup, not the API.

Reports

Output CI already knows how to read

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 ...
Config resolution

dt whoami says where each value came from

Most 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

Connect an agent, or install the CLI.

The dashboard signs in through BitBot. Everything programmatic authenticates with a dtst_ API key you generate there.

1 · Agent over MCP

Point Claude at the HTTP transport

{ "mcpServers": { "detesting": { "url": "https://robot.detesting.ai/mcp", "headers": { "Authorization": "Bearer dtst_…" } } } }
2 · Shell and CI

Install dt, bind a directory

$ curl -fsSL https://robot.detesting.ai/cli/install.sh | bash $ dt login # BitBot device flow $ dt project join my-project # writes .detesting.json $ dt suite run --all --env ci # 0 · 1 · 2

Node 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.

Access

Where things live

  • Dashboard and API — robot.detesting.ai
  • MCP endpoint — robot.detesting.ai/mcp
  • CLI installer — robot.detesting.ai/cli/install.sh
  • Sign-in — BitBot SSO, verified through JWKS
  • Programmatic — dtst_ API keys, scoped to one project
Under the hood

Stack

Node.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

Pruebas que escribe tu agente y en las que CI puede confiar.

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.

$ curl -fsSL https://robot.detesting.ai/cli/install.sh | bash
dt suite run --all --env ci -o junit
entorno ci (proyecto: payments · resuelto desde ./.detesting.json) auth 8 pruebas 8 aprobadas 2.4s orders 14 pruebas 14 aprobadas 6.1s billing 9 pruebas 1 fallida 8 aprobadas 4.8s reembolso mayor al saldo → paso 2 “POST /refunds” status esperado 409 obtenido 500 vista por primera vez en #741 · falló 3 de las últimas 20 · inestabilidad 0.11 31 pruebas · 30 aprobadas · 1 fallida · 13.3s · ejecución #749 junit escrito en ./dt-results.xml salida 1 — falló una prueba: lo roto es la API, no el arnés
68herramientas MCP por HTTP
52tipos de aserción
2interfaces, un solo motor
0·1·2códigos de salida que CI entiende

Dos formas de entrar

El agente y la terminal ejecutan el mismo motor.

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.

MCP · robot.detesting.ai/mcp

68 herramientas para Claude y otros agentes

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.

CLI · dt 2.0

Una gramática: 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
Ejecución local

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.

Dónde se ejecuta

--local ejecuta aquí y guarda el resultado (predeterminado). --remote deja la ejecución al servidor. --offline ejecuta aquí y no registra nada.

Barandales

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

Una prueba es una cadena, no una petición.

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.

Proyecto — el límite al que todo pertenece ├── Colección una por proyecto · Postman v2.0/2.1 · OpenAPI 3.0.x · Swagger 2.0 │ └── Endpoints[] la superficie de API bajo prueba ├── Entornos[] baseUrl, credenciales, banderas — clonables y comparables ├── Suites[] api o browser · secuencial o paralela · setup y teardown │ └── Casos[] │ └── Pasos[] request → assertions[] → variableExtractions[] └── Ejecuciones[] numeradas, repetibles, comparables, con limpieza
01

Importa la especificación

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.

02

Describe los entornos

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á?”.

03

Genera y luego edita

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.

04

Previsualiza antes de gastar una ejecución

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.

05

Ejecuta y averigua qué cambió

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

Cinco capas, y la ejecución te dice cuál ganó.

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.

PrioridadCapaSuele contener
1Entorno

baseUrl, credenciales, banderas

2Suite

Configuración que pertenece a un grupo de pruebas

3Caso de prueba

Valores por omisión de un escenario

4Tiempo de ejecución

Valores que pasas al arrancar la ejecución

5Extraídas

Lo que devolvió la última respuesta — máxima prioridad

Encadenado

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}}"
Datos generados

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})}}
Condiciones

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

52 maneras de decir con precisión qué significa “pasó”.

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.

Respuesta · 21

statusstatus.rangeexistsnotExistsequalscontainsmatchespatternfieldgreaterThangreaterThanOrEquallessThanlessThanOrEqualinRangeinisArrayisObjectnotEmptyarrayLengthresponseTimeschemashape.matches

Navegador · 31

element.existselement.not.existselement.visibleelement.hiddenelement.enabledelement.disabledelement.text.equalselement.text.containselement.text.matcheselement.value.equalselement.attribute.equalselement.countelement.hasClasspage.title.equalspage.title.containspage.url.equalspage.url.containspage.url.not.containspage.url.matchespage.content.containspage.content.not.containspage.content.matchesconsole.contains.errorconsole.not.contains.errorconsole.contains.warningnetwork.request.madenetwork.response.statusperformance.load.timeperformance.first.contentful.paintscreenshot.matchesvisual.matches_baseline
Deriva de forma

Una aserción que sobrevive a tu lista de campos

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.

Un paso, completo
{
  "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

Graba en Chrome, reprodúcelo con Playwright.

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.

Dos motores

Playwright por omisión, Puppeteer-stealth cuando te bloquean

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.

Selectores

Cadenas de alternativas, no una sola cuerda frágil

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']"]
]
Evidencia

Capturas, consola, red y líneas base

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

Un build en rojo debería decir qué es nuevo.

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.

Historial en cada falla

¿Esto es nuevo o es de todos los días?

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 })
Cobertura de comportamiento

Resultados cubiertos, no endpoints tocados

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
Calidad de las pruebas

Calificadas al escribirlas, recalificadas al correrlas

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.

Fixtures

Setup y teardown con reglas que se sostienen

  • Si falla el setup no corre ninguna prueba: no tiene sentido verificar contra un sistema que no se pudo preparar.
  • El teardown corre igual tras un setup fallido, porque el setup pudo haber creado algo antes de caerse.
  • El teardown nunca pone en rojo una suite que aprobó; su resultado se reporta aparte.
  • El modo paralelo rechaza una suite cuyas pruebas se alimentan entre sí, y nombra a las acopladas — mueve la preparación compartida al setup.
Limpieza

Barre lo que dejó una ejecución caída

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}}"
}
Sin supervisión

Programaciones y notificaciones

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

Tres códigos de salida, porque dos mienten.

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.

0
Todo aprobó

Cada prueba de la selección verificó correctamente.

1
Falló una prueba

El sistema bajo prueba está roto. Esta es la señal que debe bloquear un merge.

2
Falló el arnés

Una variable faltante, un bloqueo por SSRF, un host inalcanzable. Arregla la preparación de la prueba, no la API.

Reportes

Salidas que CI ya sabe leer

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 ...
De dónde sale la configuración

dt whoami dice de dónde vino cada valor

Gana 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

Conecta un agente o instala la CLI.

El panel entra con BitBot. Todo lo programático se autentica con una llave de API dtst_ que generas ahí mismo.

1 · Agente por MCP

Apunta Claude al transporte HTTP

{ "mcpServers": { "detesting": { "url": "https://robot.detesting.ai/mcp", "headers": { "Authorization": "Bearer dtst_…" } } } }
2 · Terminal y CI

Instala dt y vincula un directorio

$ curl -fsSL https://robot.detesting.ai/cli/install.sh | bash $ dt login # flujo de dispositivo BitBot $ dt project join my-project # escribe .detesting.json $ dt suite run --all --env ci # 0 · 1 · 2

Node 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.

Acceso

Dónde vive cada cosa

  • Panel y API — robot.detesting.ai
  • Endpoint MCP — robot.detesting.ai/mcp
  • Instalador de la CLI — robot.detesting.ai/cli/install.sh
  • Inicio de sesión — SSO de BitBot, verificado con JWKS
  • Programático — llaves dtst_, acotadas a un proyecto
Por dentro

Stack

Node.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.