@diegosouzacdv/jev-browser-mcp 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ versão em produção, use o número explícito no argumento do pacote:
17
17
  "mcpServers": {
18
18
  "jev-browser": {
19
19
  "command": "npx",
20
- "args": ["--yes", "@diegosouzacdv/jev-browser-mcp@0.7.1"],
20
+ "args": ["--yes", "@diegosouzacdv/jev-browser-mcp@0.7.2"],
21
21
  "env": {
22
22
  "OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}",
23
23
  "JEV_BROWSER_MODE": "computer"
@@ -32,7 +32,7 @@ um secret manager ou pelo ambiente do processo; não grave a chave no arquivo
32
32
  de configuração. Para instalar no projeto Node do próprio harness:
33
33
 
34
34
  ```sh
35
- npm install @diegosouzacdv/jev-browser-mcp@0.7.1
35
+ npm install @diegosouzacdv/jev-browser-mcp@0.7.2
36
36
  ```
37
37
 
38
38
  Se o harness executa o MCP repetidamente nesta máquina, instale uma versão
@@ -40,7 +40,7 @@ fixa globalmente para evitar a resolução e o download feitos pelo `npx` em cad
40
40
  inicialização:
41
41
 
42
42
  ```sh
43
- npm install --global @diegosouzacdv/jev-browser-mcp@0.7.1
43
+ npm install --global @diegosouzacdv/jev-browser-mcp@0.7.2
44
44
  ```
45
45
 
46
46
  Depois configure o servidor MCP com `command: "jev-browser-mcp"` e `args: []`.
@@ -48,8 +48,8 @@ Para atualizar, rode `npm install --global
48
48
  @diegosouzacdv/jev-browser-mcp@<versão>` e reinicie o processo do harness.
49
49
 
50
50
  O padrão `computer` exige Chrome ou Edge instalado. Se optar por `harness`,
51
- configure `JEV_BROWSER_MODE=harness` antes de instalar o navegador gerenciado
52
- pelo Playwright com `npx --yes @diegosouzacdv/jev-browser-mcp@0.7.1 --install-browser`.
51
+ configure `JEV_BROWSER_MODE=harness` antes de instalar o navegador gerenciado
52
+ pelo Playwright com `npx --yes @diegosouzacdv/jev-browser-mcp@0.7.2 --install-browser`.
53
53
 
54
54
  O padrão é `computer`: o pacote abre o Chrome/Edge instalado e usa um perfil
55
55
  persistente exclusivo por processo MCP em `browser.computer_user_data_dir`. O
@@ -85,24 +85,49 @@ publicada.
85
85
 
86
86
  ### Contrato do pacote
87
87
 
88
- O executável oferece `browser_health`, `describe_actions`, `choose_next_action`
89
- e `run_browser_flow`, mantém uma sessão do browser por processo e reutiliza
90
- essa sessão entre chamadas. `browser_health` informa versão, sessão e
88
+ O executável oferece `browser_health`, `describe_actions`, `choose_next_action`,
89
+ `run_browser_flow`, `run_test_suite` e `get_browser_flow_result`, mantém uma sessão do browser
90
+ por processo e reutiliza essa sessão entre chamadas. `browser_health` informa versão, sessão e
91
91
  capacidades; `describe_actions` publica o schema atual das ações, opções,
92
92
  aliases, placeholders, campos obrigatórios de `flow`/plano e requisitos
93
93
  condicionais por ação. `run_browser_flow` agrega campos obrigatórios ausentes
94
94
  em todos os planos e etapas antes de abrir o navegador. Consulte essas ferramentas
95
95
  antes de construir um plano para evitar nomes de campos desatualizados. O plano
96
- passado ao Jev continua declarativo: clique, preenchimento, seleção nativa,
97
- hover, espera, teclas aprovadas, asserções por elemento, upload restrito a uma
98
- raiz local configurada e reações idempotentes a um comentário único. Também
99
- aceita um nome de iframe para as ações que ocorrem dentro dele. Não aceita
100
- JavaScript enviado pelo harness nem coordenadas. Além de papel/nome acessível,
96
+ passado ao Jev continua declarativo: clique, preenchimento, seleção nativa,
97
+ hover, espera, teclas aprovadas, asserções por elemento, upload restrito ao
98
+ staging da sessão ou a uma raiz local configurada e reações idempotentes a um
99
+ comentário único. Também
100
+ aceita um nome de iframe para as ações que ocorrem dentro dele. Não aceita
101
+ JavaScript enviado pelo harness nem coordenadas. Além de papel/nome acessível,
101
102
  aceita `label`, `placeholder`, `title`, `text`, `test_id` e `selector`; CSS/XPath
102
103
  são o último recurso e geram um aviso no resultado. Ele usa a
103
- biblioteca Playwright diretamente, sem iniciar um segundo servidor MCP do
104
- Playwright. O transporte MCP usa `stdio`; toda saída de diagnóstico vai para
105
- `stderr` para não misturar com JSON-RPC.
104
+ biblioteca Playwright diretamente, sem iniciar um segundo servidor MCP do
105
+ Playwright. O transporte MCP usa `stdio`; toda saída de diagnóstico vai para
106
+ `stderr` para não misturar com JSON-RPC.
107
+
108
+ ### Validar o MCP com Chromium e uma aplicação sintética
109
+
110
+ O teste ponta a ponta do pacote inicia o servidor MCP real por `stdio`, abre o
111
+ Chromium controlado pelo Playwright e exercita uma aplicação HTTP local
112
+ descartável. Ele cobre descoberta das ferramentas, busca com resposta 200,
113
+ erro 503 com corpo, upload de fixture Unicode seguido de download e comparação
114
+ de conteúdo, além de execução em segundo plano e relatório de suíte. Não acessa
115
+ SAFI, redes externas, bancos ou serviços remotos.
116
+
117
+ No checkout do repositório, instale as dependências e o navegador uma vez e
118
+ execute somente esse cenário:
119
+
120
+ ```sh
121
+ npm ci
122
+ node mcp_servers/jev-browser-npm/bin/jev-browser-mcp.cjs --install-browser
123
+ node --test mcp_servers/jev-browser-npm/tests/stdio.test.mjs
124
+ ```
125
+
126
+ O teste cria e remove sua aplicação e seus arquivos temporários. Para validar
127
+ Oracle, PostgreSQL ou servidores FTP/FTPS/SFTP, configure serviços de teste
128
+ isolados e credenciais pelo ambiente e use os perfis explicitamente descritos
129
+ em “Perfis e integrações de teste”; o cenário sintético não representa essas
130
+ integrações reais.
106
131
 
107
132
  O modo `computer` grava cookies no perfil exclusivo configurado, e conteúdo da
108
133
  página pode conter instruções maliciosas. O Jev recebe a captura acessível com
@@ -185,11 +210,15 @@ Cada plano pode usar:
185
210
  função, armazenamento local ou propriedades de credenciais. Strings e números
186
211
  voltam diretamente; objetos e arrays retornam somente tipo, chaves e tamanho,
187
212
  sem despejar seus valores;
188
- - `http_request` executa somente GET com a sessão da página; `inspect_cookies`
213
+ - `http_request` executa GET/HEAD e, com perfil DEV/HML e autorização explícita,
214
+ também POST/PUT/PATCH/DELETE com a sessão da página; aceita JSON ou multipart,
215
+ limita a resposta e pode extrair campos escalares para variáveis do fluxo.
216
+ `inspect_cookies`
189
217
  retorna apenas metadados e valores redigidos; `read_angular_state` devolve o
190
- nome do estado e nomes dos parâmetros, sem valores. As três ações exigem
191
- `local_only: true` e página em loopback. `http_request` fica restrito à mesma
192
- origem, não segue redirecionamentos e limita/redige o corpo opcional;
218
+ nome do estado e nomes dos parâmetros, sem valores. Inspeções continuam
219
+ exigindo `local_only: true` e página em loopback. `http_request` exige
220
+ `local_only` ou um perfil, fica restrito à mesma origem, não segue
221
+ redirecionamentos e não inclui o corpo sem `include_body: true`;
193
222
  - `frame: "first"` seleciona o primeiro iframe; `frame: {"url_contains":"..."}`
194
223
  localiza um iframe pela URL. Uma string diferente continua buscando o nome ou
195
224
  título exato do frame;
@@ -202,6 +231,9 @@ Cada plano pode usar:
202
231
  - `if_visible` em um clique torna a ação condicional e
203
232
  `when: {"visible":"..."}` em cada plano limita as opções do Jev às que
204
233
  correspondem à tela atual;
234
+ - `near: {"text":"..."}` com `role` e `name` procura o controle com esse nome
235
+ entre os elementos posteriores à âncora; o nome pedido não é descartado nem
236
+ substituído pelo primeiro botão/link seguinte;
205
237
  - `fill` é alias de `type`, `press_key` de `press`, `duration_ms` de `wait.ms` e
206
238
  `value` de campos de texto. `timeout_ms` funciona por etapa. Placeholders usam
207
239
  `{nome}`; referências desconhecidas e `{{nome}}` são recusadas antes da
@@ -215,11 +247,18 @@ Cada plano pode usar:
215
247
  - `clear_storage` limpa cookies, local/session storage, IndexedDB, Cache Storage
216
248
  e service workers do contexto atual. Como opção de fluxo,
217
249
  `options.clear_storage: true` também recarrega a página antes do snapshot inicial;
218
- - `assert_text`, `assert_value`, `assert_visible`, `assert_hidden` e
219
- `assert_enabled`; `assert_text`
220
- pode receber apenas `role` quando a região, como `alert`, não tem nome acessível;
250
+ - `assert_text`, `assert_value`, `assert_attribute`, `assert_visible`,
251
+ `assert_hidden` e `assert_enabled`; `assert_attribute` compara um atributo
252
+ por igualdade ou substring e não inclui o valor observado no resultado;
253
+ `assert_text` pode receber apenas `role` quando a região, como `alert`, não tem
254
+ nome acessível;
221
255
  - `upload_file` para input rotulado, botão que abre o seletor de arquivo ou
222
256
  dropzone;
257
+ - `generate_fixture`, `assert_download`, `assert_db`, `assert_log`,
258
+ `start_email_capture`, `assert_email`, `assert_remote_file`,
259
+ `delete_remote_file` e `run_hook` cobrem dados e serviços externos de teste;
260
+ os limites, pré-requisitos e regras por ambiente estão na seção
261
+ [Perfis e integrações de teste](#perfis-e-integrações-de-teste);
223
262
  - `target_page: "popup"` para continuar no popup aberto com `expect_popup: true`;
224
263
  locators semânticos também atravessam open Shadow DOM. Shadow DOM fechado não
225
264
  é acessível ao Playwright;
@@ -238,7 +277,10 @@ As proteções e evidências por etapa usam estes campos:
238
277
  - `confirm_modal` combina o clique no gatilho, a conferência do texto e o clique
239
278
  afirmativo em uma única etapa protegida. Também aceita os aliases
240
279
  `trigger`/`confirm`, por exemplo
241
- `{"action":"confirm_modal","trigger":"Salvar","expected_text":"Confirma a alteração?","confirm":"OK"}`;
280
+ `{"action":"confirm_modal","trigger":"Salvar","trigger_match":"contains","expected_text":"Confirma a alteração?","confirm":"OK"}`.
281
+ O nome do gatilho é exato por padrão; `trigger_match` também aceita `exact` e
282
+ `regex`. O texto esperado é conferido antes de clicar no botão afirmativo. O
283
+ clique no gatilho ainda respeita a autorização de mutação;
242
284
  - `mutating: true` marca etapas que gravam dados ou disparam efeitos externos.
243
285
  O MCP também classifica botões conhecidos como `Salvar`, `Emitir`, `Cancelar`
244
286
  ou `Sim` pelo nome e pelo controle resolvido, inclusive quando o plano usa um
@@ -266,10 +308,19 @@ booleanos. Use `{pedido}` em `flow`, `expected_outcome` e nos campos textuais do
266
308
  plano para reutilizar um valor sem editar o roteiro em vários lugares. A ação
267
309
  `extract` lê `text` (padrão), `value` ou `attribute` de um elemento e salva o
268
310
  resultado na variável indicada por `as`; etapas seguintes podem usar
269
- `{documento}`. O valor extraído é tratado como sensível e fica oculto no
270
- resultado por padrão; use `sensitive: false` só quando for apropriado exibi-lo.
271
- `resolved_target.match_strategy` informa quando um rótulo foi
272
- associado por proximidade (`label-proximity`) em vez de um `label[for]` direto.
311
+ `{documento}`. O valor extraído é tratado como sensível e fica oculto no
312
+ resultado por padrão; use `sensitive: false` só quando for apropriado exibi-lo.
313
+ Valores que correspondem a segredos já detectados continuam redigidos. Para
314
+ provar que um atributo corresponde a um valor sem devolvê-lo ao harness, use
315
+ `assert_attribute` com `match: "exact"` (padrão) ou `match: "contains"`.
316
+ Por exemplo, para validar o caminho de um documento em um iframe:
317
+
318
+ ```json
319
+ {"action":"assert_attribute","selector":"iframe#documento","attribute":"src","expected":"/documento.pdf","match":"contains"}
320
+ ```
321
+
322
+ `resolved_target.match_strategy` informa quando um rótulo foi
323
+ associado por proximidade (`label-proximity`) em vez de um `label[for]` direto.
273
324
 
274
325
  A ação `assert_network` verifica respostas observadas depois da etapa anterior,
275
326
  incluindo respostas 2xx ou erros esperados como 404. Exemplo:
@@ -290,9 +341,13 @@ da mesma origem e respeita o limite configurado para captura de corpos.
290
341
  `assert_network` consulta respostas desde o marcador da etapa e usa a resposta
291
342
  da navegação inicial como fallback quando nenhuma resposta posterior corresponde.
292
343
  `wait_for_request` e `expected_outcome.request` consultam desde o início do fluxo.
293
- URLs cobertas por uma asserção são retidas em um buffer separado limitado por
294
- `jev_browser_mcp.browser.max_network_history_events` (4096 eventos nesta
295
- configuração). A captura de página também observa requisições de iframes.
344
+ O MCP arma observadores de `wait_for_request` antes dos passos quando o padrão
345
+ pode ser resolvido; se depender de variável extraída, arma assim que a variável
346
+ estiver disponível. A observação retém a resposta mesmo que ela termine durante
347
+ um clique ou que tráfego posterior ultrapasse o buffer circular. `assert_network`
348
+ mantém respostas correspondentes no histórico limitado por
349
+ `jev_browser_mcp.browser.max_network_history_events`. A captura de página também
350
+ observa requisições de iframes.
296
351
  Um único plano `fast_path` composto apenas por `assert_network` e
297
352
  `wait_for_request` não espera um snapshot visual, então endpoints que respondem
298
353
  sem HTML também podem ser verificados. `navigation_http_status` registra um
@@ -478,21 +533,173 @@ asserções são aceitos. `comment` também é aceito em planos e passos, mas é
478
533
  ignorado e não é enviado ao Jev. Campos fora do contrato continuam sendo
479
534
  recusados.
480
535
 
481
- ### Upload de arquivos
482
-
483
- Defina `JEV_BROWSER_UPLOAD_ROOT` como uma pasta absoluta que contenha os arquivos
484
- de fixture. Cada caminho passado ao fluxo também precisa ser absoluto; o MCP
485
- resolve links simbólicos e recusa qualquer arquivo fora dessa raiz. Só aceita
486
- arquivos regulares, até 5 arquivos por passo, 10 MiB por arquivo e 25 MiB no
487
- total do passo, conforme `jev_browser_mcp.browser` em `config/ui-testing.json`.
488
- Os bytes são lidos e validados no processo local antes de serem entregues ao
489
- Playwright. O MCP não envia esses bytes ao Jev nem os inclui diretamente na
490
- resposta; texto que o próprio site exibir na interface ainda pode aparecer no
491
- snapshot devolvido ao harness.
492
-
493
- ```json
494
- {"action":"upload_file","target":"input","label":"Nota fiscal","file_path":"/fixtures/nota.pdf"}
495
- ```
536
+ ### Upload de arquivos
537
+
538
+ Por padrão, `browser_health` cria e devolve `upload_staging_directory` dentro do
539
+ projeto local: `<JEV_BROWSER_PROJECT_ROOT ou diretório de trabalho do MCP>/.jev-browser/tmp/uploads/mcp-<pid>-<sessão>`.
540
+ O harness deve copiar para essa pasta somente os arquivos de fixture que o
541
+ fluxo precisa, e usar o caminho absoluto retornado por `browser_health`. Se o
542
+ processo MCP iniciar com outro diretório de trabalho, defina
543
+ `JEV_BROWSER_PROJECT_ROOT` como o caminho absoluto do projeto. Em um MCP remoto,
544
+ esse caminho pertence ao host do servidor; o harness precisa conseguir gravar
545
+ no mesmo host ou volume.
546
+
547
+ Cada caminho passado ao fluxo precisa ser absoluto; o MCP resolve links
548
+ simbólicos e recusa qualquer arquivo fora do staging privado daquela sessão e
549
+ da raiz explícita configurada, se houver.
550
+ Só aceita arquivos regulares, até 5 arquivos por passo, 10 MiB por arquivo e
551
+ 25 MiB no total do passo, conforme `jev_browser_mcp.browser` em
552
+ `config/ui-testing.json`. O operador pode ajustar os limites no arquivo ou com
553
+ `JEV_BROWSER_MAX_UPLOAD_FILE_BYTES` e
554
+ `JEV_BROWSER_MAX_UPLOAD_TOTAL_BYTES`; o teto total precisa ser pelo menos o
555
+ limite por arquivo. `browser_health` e `describe_actions` mostram os valores
556
+ ativos. Esse teto pertence ao MCP e ao navegador local; alterá-lo não aumenta
557
+ o limite de arquivo aceito pelo provedor Jev. Os bytes são lidos e validados no
558
+ processo local antes de serem entregues ao Playwright. O MCP não envia esses
559
+ bytes ao Jev nem os inclui diretamente na resposta; texto que o próprio site
560
+ exibir na interface ainda pode aparecer no snapshot devolvido ao harness.
561
+
562
+ Depois de um fluxo que usa `upload_file`, `generate_fixture` ou multipart com
563
+ arquivos, o MCP remove todos os arquivos e subpastas da pasta exclusiva da
564
+ sessão, inclusive quando o fluxo falha. Arquivos fornecidos pela raiz
565
+ `JEV_BROWSER_UPLOAD_ROOT` não são apagados. Um
566
+ `dry_run` ou `confirmation_required` preserva os arquivos para que a próxima
567
+ chamada confirmada possa usá-los; o encerramento normal do servidor também
568
+ limpa essa pasta. O diretório e os diretórios pais permanecem, prontos para uma
569
+ próxima chamada. O original fora da pasta de staging não é tocado.
570
+
571
+ `JEV_BROWSER_UPLOAD_ROOT` continua disponível como uma raiz explícita adicional;
572
+ o MCP aceita arquivos tanto dela quanto do staging privado do projeto e nunca
573
+ apaga arquivos da raiz explícita. A pasta privada gerenciada no projeto continua
574
+ disponível para fixtures geradas pelo MCP e multipart; só o staging gerenciado
575
+ é limpo automaticamente. Não aponte para o disco inteiro ou para a pasta home.
576
+
577
+ Fixtures `generate_fixture` aceitam `kind: "pdf"`, `"text"` ou `"bytes"`,
578
+ `as`, `file_name` (inclusive Unicode), `content` para PDF/texto e `size_bytes`
579
+ para bytes determinísticos. São criadas em
580
+ `.jev-browser/tmp/uploads/` dentro do projeto e expostas como variável para
581
+ passos seguintes. O tamanho máximo é o menor entre
582
+ `max_upload_file_bytes` configurado e **47 MiB**, limite atual do provedor Jev
583
+ para um arquivo. `assert_download` compara SHA-256, tamanho e tipo MIME do
584
+ último download com o fixture informado. O original do fixture continua sendo
585
+ temporário e é limpo no teardown do fluxo ou da suíte.
586
+
587
+ ### Perfis e integrações de teste
588
+
589
+ O MCP lê perfis opcionais de `.jev-browser/profiles.json` no projeto. Para outro
590
+ arquivo dentro do projeto, defina `JEV_BROWSER_PROFILES_FILE` com caminho
591
+ absoluto. O arquivo é JSON versão 1 e nunca deve conter senhas ou tokens:
592
+ campos como `connection_env`, `password_env` e `username_env` guardam somente o
593
+ nome da variável de ambiente, e os valores são lidos pelo processo MCP.
594
+
595
+ Exemplo mínimo de perfil DEV:
596
+
597
+ ```json
598
+ {
599
+ "version": 1,
600
+ "profiles": {
601
+ "dev": {
602
+ "environment": "dev",
603
+ "application_origins": ["http://localhost:8000"],
604
+ "database": {
605
+ "engine": "postgresql",
606
+ "connection_env": "JEV_TEST_DATABASE_URL",
607
+ "timeout_ms": 5000,
608
+ "max_rows": 100
609
+ },
610
+ "log_sources": {"api": "logs/test-api.log"},
611
+ "smtp_capture": {"port": 0, "bind_address": "127.0.0.1", "max_messages": 20},
612
+ "remotes": {},
613
+ "hooks": {}
614
+ }
615
+ }
616
+ }
617
+ ```
618
+
619
+ `database.engine` suporta `postgresql` e `oracle`. PostgreSQL usa
620
+ `connection_env`; Oracle usa `connect_env`, `user_env` e `password_env`. Ambos
621
+ exigem `timeout_ms` e `max_rows`. `assert_db` aceita apenas um `SELECT` sem
622
+ comandos adicionais, usa binds, transação somente leitura, timeout e teto de
623
+ linhas do perfil. Configure também as credenciais para uma identidade de banco
624
+ com privilégios somente de leitura; a transação read-only do MCP complementa
625
+ essa separação. Essa ação é somente leitura em DEV, HML e PRD.
626
+
627
+ `application_origins` é a allowlist de origens HTTP. `http_request` usa a sessão
628
+ da página, aceita `GET`, `HEAD`, `POST`, `PUT`, `PATCH` e `DELETE`, JSON ou
629
+ multipart. POST/PUT/PATCH/DELETE exigem `options.allow_mutations: true` e um
630
+ perfil DEV/HML cuja allowlist inclua a origem. PRD recusa mutações. HML só
631
+ permite escrita na aplicação com essa autorização explícita. Para uma página
632
+ local sem perfil, `local_only: true` só libera GET/HEAD em loopback.
633
+ `extract` usa caminhos JSON e só expõe valores escalares; campos com nomes
634
+ sensíveis entram na redação automática.
635
+
636
+ `log_sources` define nomes e caminhos de arquivos que o MCP pode consultar.
637
+ `assert_log` lê apenas essas origens e pode restringir o trecho ao que foi
638
+ escrito desde o início do fluxo ou desde `step:N`; logs são sempre somente
639
+ leitura. Configure caminhos absolutos apenas quando a política local exigir;
640
+ prefira caminhos relativos ao projeto.
641
+
642
+ `smtp_capture` sobe um servidor de captura limitado a `127.0.0.1` ou `::1`;
643
+ `port: 0` escolhe uma porta livre. Use `start_email_capture` antes de acionar o
644
+ envio na aplicação e depois `assert_email` com destinatário, assunto ou corpo.
645
+ O servidor guarda as mensagens somente em memória durante a chamada/suíte e as
646
+ remove no teardown. A resposta e o relatório registram a asserção e horário,
647
+ sem incluir assunto ou corpo.
648
+
649
+ `remotes` permite verificar arquivos em FTP, FTPS ou SFTP, limitado a um
650
+ `root` remoto configurado. Credenciais vêm de `username_env` e `password_env`;
651
+ SFTP também exige `host_fingerprint_env` para validar a chave do host.
652
+ Configure cada usuário remoto em uma pasta isolada/chroot compatível com o
653
+ `root` allowlisted.
654
+ `assert_remote_file` verifica existência e, opcionalmente, tamanho e SHA-256.
655
+ `delete_remote_file` é destrutivo: só funciona em perfil DEV com
656
+ `allow_mutations: true`. Banco e logs permanecem somente leitura em todos os
657
+ ambientes.
658
+
659
+ `hooks` são scripts Node nomeados e allowlisted, com caminho dentro do projeto,
660
+ argumentos declarados (`string`, `integer` ou `boolean`), comprimento máximo e
661
+ `timeout_ms` obrigatório. Não há shell nem comando arbitrário; hooks só podem
662
+ ser definidos/executados em DEV e precisam de `allow_mutations: true`. PRD
663
+ recusa hooks e qualquer etapa marcada `mutating: true`.
664
+
665
+ ### Suítes JSON e relatórios
666
+
667
+ Guarde uma definição por PBI em `.jev-browser/suites/<id>.json`. Ela possui
668
+ `version: 1`, `id`, `criteria` e opcionalmente `description`, `params`,
669
+ `environment_profile`, `flows`, `setup` e `teardown`. Cada fluxo reutilizável
670
+ tem `flow`, `candidate_plans` e opcionalmente `initial_url`, `expected_outcome`,
671
+ `params` e `options`; cada critério tem `id`, `description` e `flow`, que pode
672
+ referenciar um fluxo pelo nome ou declará-lo inline. Placeholders usam
673
+ `{nome}` e recebem prioridade dos parâmetros do pedido sobre os parâmetros do
674
+ arquivo.
675
+
676
+ Antes de abrir o navegador, `run_test_suite` valida todo o JSON, caminhos de
677
+ relatório, placeholders, limites, perfil e todas as etapas de setup, critérios
678
+ e teardown. Executa sequencialmente por padrão e tenta teardown mesmo se setup
679
+ ou critério falhar. A suíte mantém a sessão do browser e a captura de integrações
680
+ durante suas etapas, depois limpa os arquivos temporários. Um relatório
681
+ Markdown é salvo por padrão em `.jev-browser/reports/`; `report_path` aceita um
682
+ caminho relativo dentro do projeto. O relatório inclui status por critério,
683
+ passos, duração, localizadores, evidências de rede, banco, logs, e-mail, arquivos,
684
+ screenshots e limpeza. URLs de evidência perdem query, fragmento e segmentos que
685
+ parecem IDs; valores de campos e corpos de mensagens não são incluídos. Valores
686
+ secretos conhecidos por variáveis de ambiente e parâmetros sensíveis são
687
+ redigidos. O tamanho do arquivo é limitado por
688
+ `max_test_suite_report_bytes`; se o relatório atingir o teto, o resultado
689
+ informa `report_truncated: true` e o próprio Markdown marca o truncamento.
690
+
691
+ Para permitir polling quando a chamada for longa, use `background: true` e
692
+ consulte o `run_id` com `get_browser_flow_result`. A definição da suíte precisa
693
+ ser criada no projeto do processo MCP antes da chamada. `describe_actions`
694
+ publica o formato da suíte, ferramentas, opções, ações, campos condicionais e
695
+ limites atualmente configurados; `browser_health` informa versões e capacidades.
696
+
697
+ Consulte `browser_health` para descobrir a pasta da sessão e, então, passe o
698
+ caminho absoluto do arquivo preparado pelo harness:
699
+
700
+ ```json
701
+ {"action":"upload_file","target":"input","label":"Nota fiscal","file_path":"/projeto/.jev-browser/tmp/uploads/mcp-1234-pid-1234/nota.pdf"}
702
+ ```
496
703
 
497
704
  Para uma área de arrastar, use o papel e o nome acessível da dropzone. Para um
498
705
  botão que abre a janela nativa, use `target: "button"`; sem `target`, a presença
@@ -502,9 +709,9 @@ de `label` seleciona o input e `role`/`name` seleciona esse botão.
502
709
  {"action":"upload_file","target":"dropzone","role":"region","name":"Anexos","file_paths":["/fixtures/a.pdf","/fixtures/b.png"]}
503
710
  ```
504
711
 
505
- Se `JEV_BROWSER_UPLOAD_ROOT` não estiver definido, a ação recusa a execução. O
506
- limite evita que um plano transforme o MCP em leitor arbitrário de arquivos do
507
- computador.
712
+ O limite evita que um plano transforme o MCP em leitor arbitrário de arquivos
713
+ do computador; sem uma raiz explícita, só a pasta temporária isolada da sessão
714
+ é autorizada.
508
715
 
509
716
  Exemplo de chamada:
510
717
 
@@ -583,7 +790,9 @@ compartilhe esse arquivo. `storage_state` não pode ser combinado com
583
790
  `fresh_context` ou `clear_storage`.
584
791
 
585
792
  `browser.max_flow_steps` limita os passos de cada plano candidato; os planos
586
- não são somados. `browser.max_text_entry_chars` limita cada valor digitado. O
793
+ não são somados. O padrão é 24 e pode ser aumentado em `config/ui-testing.json`
794
+ ou por `JEV_BROWSER_MAX_FLOW_STEPS` (por exemplo, `30`).
795
+ `options.max_flow_steps` pode reduzir esse teto por chamada. `browser.max_text_entry_chars` limita cada valor digitado. O
587
796
  resultado contém `status`, o plano escolhido, as ações executadas, a última
588
797
  captura acessível e se o texto descritivo esperado apareceu. `expected_outcome`
589
798
  como string é descrição, não uma asserção: `expected_outcome_visible` é
@@ -628,9 +837,14 @@ origem são capturadas mesmo quando usam transferência chunked e não enviam
628
837
  `Content-Length`. Corpos acima do limite, com formato inválido ou que não
629
838
  podem ser lidos com segurança são omitidos e explicados em `warnings`.
630
839
 
631
- `jev_browser_mcp.browser.max_action_timeout_seconds` limita esperas por ações,
632
- seletores e condições. `JEV_BROWSER_UPLOAD_ROOT` libera uploads somente dentro
633
- de uma pasta absoluta escolhida pelo operador.
840
+ `jev_browser_mcp.browser.max_action_timeout_seconds` limita esperas por ações,
841
+ seletores e condições. `JEV_BROWSER_PROJECT_ROOT` troca o diretório de projeto
842
+ usado para staging quando o diretório de trabalho do MCP não é o projeto. A
843
+ pasta `.jev-browser/tmp/uploads` deve ser tratada como temporária e ignorada
844
+ pelo Git. `JEV_BROWSER_UPLOAD_ROOT` adiciona uma raiz absoluta escolhida pelo
845
+ operador para arquivos fornecidos pelo harness; o MCP não apaga arquivos dessa
846
+ raiz. O staging privado do projeto continua ativo para fixtures e multipart
847
+ gerados pelo MCP.
634
848
  `jev_browser_mcp.browser.max_upload_files`, `max_upload_path_chars`,
635
849
  `max_upload_file_bytes` e `max_upload_total_bytes` limitam quantidade e tamanho.
636
850
  Não configure a raiz como o disco inteiro ou a pasta home: use uma pasta de
@@ -703,6 +917,16 @@ uma lista combina condições, e todas precisam passar. `has_value` aceita
703
917
  `ready_network_idle: true` solicita uma espera best-effort por `networkidle`.
704
918
  `ready_timeout_seconds` limita a espera de inicialização e `ready_stable_ms`
705
919
  define a janela usada para considerar o snapshot estável.
920
+
921
+ Se a duração prevista ultrapassar o timeout de chamada do harness, passe
922
+ `background: true` em `run_browser_flow`. A chamada retorna um `run_id`; consulte
923
+ `get_browser_flow_result` com esse ID e, opcionalmente, `wait_ms` de até 30000
924
+ para aguardar a conclusão, mantendo esse valor abaixo do timeout do harness.
925
+ No resultado da consulta, `status` indica se a execução terminou; o status do
926
+ fluxo em si fica em `result.status`. Os resultados ficam na memória do processo
927
+ MCP por 30 minutos e saem antes se o limite de execuções retidas for atingido.
928
+ Reiniciar o servidor apaga execuções e resultados pendentes. O modo síncrono
929
+ permanece disponível para fluxos curtos.
706
930
 
707
931
  `console_levels` aceita `log`, `info`, `debug`, `warn` e `error`. Com
708
932
  `console_levels: ["log","warn","error"]` e `capture_network_errors`, o retorno
@@ -752,8 +976,9 @@ Para testar microfone, conceda permissão no escopo da chamada:
752
976
  ```
753
977
 
754
978
  `fake_media` aceita tanto o caminho WAV direto quanto o objeto
755
- `{"audio":"caminho.wav"}`. Exige um WAV RIFF válido dentro de
756
- `JEV_BROWSER_UPLOAD_ROOT` e respeita os mesmos limites de upload. Ao informar
979
+ `{"audio":"caminho.wav"}`. Exige um WAV RIFF válido dentro do staging privado
980
+ do projeto ou de `JEV_BROWSER_UPLOAD_ROOT` e respeita os mesmos limites de
981
+ upload. Ao informar
757
982
  `fake_media`, o MCP concede automaticamente a permissão `microphone`, adiciona
758
983
  as opções de dispositivo falso do Chromium e usa o arquivo como entrada de
759
984
  áudio. Sem esse arquivo, a
@@ -11,7 +11,10 @@ Usage:
11
11
 
12
12
  Configuration is read from config/ui-testing.json. Set OPENROUTER_API_KEY in
13
13
  the environment. The default mode is computer; use JEV_BROWSER_MODE=harness to select isolated headless mode.
14
- For upload_file, set JEV_BROWSER_UPLOAD_ROOT to a dedicated fixture directory.
14
+ browser_health reports the project-local upload staging directory by default.
15
+ Set JEV_BROWSER_UPLOAD_ROOT for a dedicated persistent fixture directory.
16
+ JEV_BROWSER_MAX_FLOW_STEPS, JEV_BROWSER_MAX_UPLOAD_FILE_BYTES and
17
+ JEV_BROWSER_MAX_UPLOAD_TOTAL_BYTES override the corresponding configured caps.
15
18
  `;
16
19
 
17
20
  async function main() {
@@ -0,0 +1,57 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export function createBrowserFlowRunStore({ maxEntries, retentionMs, now = Date.now, createId = randomUUID }) {
4
+ const runs = new Map();
5
+
6
+ const pruneExpired = () => {
7
+ for (const [runId, run] of runs) {
8
+ if (run.status !== "running" && run.finishedAt + retentionMs <= now()) runs.delete(runId);
9
+ }
10
+ };
11
+
12
+ const makeRoom = () => {
13
+ pruneExpired();
14
+ while (runs.size >= maxEntries) {
15
+ const completed = [...runs].find(([, run]) => run.status !== "running");
16
+ if (!completed) throw new Error("all background flow slots are in use");
17
+ runs.delete(completed[0]);
18
+ }
19
+ };
20
+
21
+ const snapshot = (run) => ({
22
+ run_id: run.id,
23
+ status: run.status,
24
+ elapsed_ms: Math.max(0, now() - run.startedAt),
25
+ ...(run.finishedAt === undefined ? {} : { finished_at: new Date(run.finishedAt).toISOString() }),
26
+ ...(run.result === undefined ? {} : { result: run.result }),
27
+ ...(run.error === undefined ? {} : { error: run.error }),
28
+ });
29
+
30
+ return Object.freeze({
31
+ start(execute) {
32
+ if (typeof execute !== "function") throw new TypeError("background flow must be a function");
33
+ makeRoom();
34
+ const run = { id: createId(), status: "running", startedAt: now() };
35
+ runs.set(run.id, run);
36
+ run.finished = Promise.resolve().then(execute).then((result) => {
37
+ run.result = result;
38
+ run.status = "completed";
39
+ }, (error) => {
40
+ run.error = String(error?.message || error).slice(0, 1000);
41
+ run.status = "failed";
42
+ }).finally(() => {
43
+ run.finishedAt = now();
44
+ });
45
+ return { run_id: run.id, status: run.status };
46
+ },
47
+ async get(runId, waitMs = 0) {
48
+ pruneExpired();
49
+ const run = runs.get(runId);
50
+ if (!run) return undefined;
51
+ if (run.status === "running" && waitMs > 0) {
52
+ await Promise.race([run.finished, new Promise((resolve) => setTimeout(resolve, waitMs))]);
53
+ }
54
+ return snapshot(run);
55
+ },
56
+ });
57
+ }