<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Katja</title>
    <description>The latest articles on DEV Community by Katja (@quality_minder).</description>
    <link>https://dev.to/quality_minder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4143124%2Fff12ad70-c7e8-4c33-ada7-696ec770d030.jpg</url>
      <title>DEV Community: Katja</title>
      <link>https://dev.to/quality_minder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quality_minder"/>
    <language>en</language>
    <item>
      <title>My testing agent remembers what it learned. Mostly.</title>
      <dc:creator>Katja</dc:creator>
      <pubDate>Thu, 01 Oct 2026 14:53:40 +0000</pubDate>
      <link>https://dev.to/quality_minder/my-testing-agent-remembers-what-it-learned-mostly-3dl2</link>
      <guid>https://dev.to/quality_minder/my-testing-agent-remembers-what-it-learned-mostly-3dl2</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; An open-source agent that reads a Jira ticket, writes test steps, runs them in a real Chrome window, judges the result from screenshots, and reports back to Jira. The new version has a memory: it reuses browser automation code that worked and drops code that broke. Setup is at the end.&lt;/p&gt;




&lt;p&gt;A little while ago I wrote about &lt;a href="https://dev.to/quality_minder/building-an-ai-qa-agent-that-tests-your-jira-tickets-in-a-real-browser-3pc7"&gt;an AI QA agent that tests your Jira tickets in a real browser&lt;/a&gt;. You give it a ticket ID and a URL. It reads the bug, writes test steps, drives Chrome through them, judges the result from screenshots, and reports back to Jira. If everything passes, it moves the ticket to Done.&lt;/p&gt;

&lt;p&gt;I've updated it since. The big change: it has a memory now.&lt;/p&gt;

&lt;p&gt;The old version started from zero on every ticket. Every step meant an LLM writing fresh browser code, even for a click it had already done a hundred times. That works, but it's slow, it costs tokens, and it's a bit like a colleague who forgets everything over the weekend.&lt;/p&gt;

&lt;p&gt;The new version remembers automation code that worked and reuses it when a similar step comes up again, even on a different ticket and phrased differently. It also forgets code that broke, so it doesn't keep trusting something broken.&lt;/p&gt;

&lt;p&gt;Here's how the updated version works and why I built it this way. If you want to run it yourself, skip to How to try it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two agents, one phone call between them
&lt;/h2&gt;

&lt;p&gt;There are two agents.&lt;/p&gt;

&lt;p&gt;The main one, the orchestrator, turns a Jira ticket into a list of test steps and coordinates running them. The second one, &lt;code&gt;step_execution&lt;/code&gt;, does exactly one thing: it executes a single test step against a live browser and tells you how it went.&lt;/p&gt;

&lt;p&gt;The orchestrator calls the sub-agent as a tool, the same way it would call any function. Give it a step, get back a verdict. It never sees a selector, a screenshot comparison or a line of browser code. That separation turned out to matter more than I expected: I can rework the sub-agent without touching the planner.&lt;/p&gt;

&lt;p&gt;Here's the rough shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jira ticket ID ──▶ [Main agent]
                     ├─ Start browser session (BaaS)
                     ├─ Log in to the app under test
                     ├─ Fetch Jira issue + comments
                     ├─ LLM: generate ordered test steps
                     └─ Orchestrator LLM
                          │  one tool call per step
                          ▼
                   [Sub-agent: step_execution]
                     ├─ Screenshot + HTML snapshot
                     ├─ LLM: which page is this? check memory
                     ├─ Reuse a stored program OR write a new one
                     ├─ Run the program in the browser
                     ├─ LLM: compare screenshots with expected result
                     └─ Memory: keep good programs, drop bad ones
                          │  verdict per step
                          ▼
                   back to main agent
                     ├─ Broken step? Retry it
                     ├─ Test finished -&amp;gt; Post results as a Jira comment
                     ├─ All passed? Move ticket to Done
                     └─ Stop browser session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  The main agent: from ticket to test plan
&lt;/h2&gt;

&lt;p&gt;The input is small: a URL, a Jira ID and a Jira user email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It starts a real browser.&lt;/strong&gt; The agent calls our Browser-as-a-Service (BaaS) API to get an isolated browser session, then logs in. BaaS accepts a program string in a tiny automation language, with commands like &lt;code&gt;navigate(...)&lt;/code&gt;, &lt;code&gt;clickN(...)&lt;/code&gt;, &lt;code&gt;sendKeysToElement(...)&lt;/code&gt; and &lt;code&gt;takeScreenshot(...)&lt;/code&gt;, sent as JSON to the BaaS runner. What comes back is the result: a &lt;code&gt;sessionID&lt;/code&gt;, which travels through the whole run, and a screenshot of the freshly logged-in app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It reads the whole ticket, comments included.&lt;/strong&gt; Two calls to the Jira REST API, one for the issue and one for its comments. Small JavaScript nodes clean both up. The comments matter. That's often where the real reproduction steps are hiding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It plans like a QA engineer.&lt;/strong&gt; The LLM gets the bug summary, the description, the comments, the current screenshot, and a library of reusable procedures for the app we test, which is a platform for running EOS/L10 meetings. For example, "start an L10 meeting" has two variants, depending on whether a Resume button is already on screen.&lt;/p&gt;

&lt;p&gt;It has to return strict JSON, an ordered list of steps:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stepDescription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stepExpectedResult"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sessionID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;sessionID&lt;/code&gt; is filled in by a template, and the prompt forbids the model from inventing or changing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An LLM runs the loop.&lt;/strong&gt; The orchestrator gets the whole list and one tool: the sub-agent. Its instructions are short:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Execute each step by calling the tool. Wait for the result before proceeding. If status is &lt;code&gt;test_broken&lt;/code&gt;, retry once. If still broken, or if &lt;code&gt;failed&lt;/code&gt;, stop execution.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;It reports back.&lt;/strong&gt; The orchestrator's transcript of tool results goes to Jira as a comment. A JavaScript node checks the status of the last step. If it passed, the agent finds the ticket's "Done" transition and fires it. If not, the ticket stays open and waits for me. At the end the browser session is shut down.&lt;/p&gt;
&lt;h2&gt;
  
  
  The sub-agent: run one step, remember it
&lt;/h2&gt;

&lt;p&gt;This is the part I'm happy about. &lt;code&gt;step_execution&lt;/code&gt; gets a session ID, a step description and an expected result. It does five things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Look before touching anything.&lt;/strong&gt; One BaaS call returns a screenshot and the page's HTML. A second call asks BaaS which automation commands it currently supports. That list goes straight into the code-generation prompt, so the language isn't frozen into my prompt text. If we add a command to BaaS tomorrow, the agent can use it tomorrow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Work out where it is, then check its memory.&lt;/strong&gt; An LLM classifies the screenshot into one of a fixed set of pages, the old Page Object idea: Active Meeting, Issues, Scorecard and so on. Then it has to search a memory collection called &lt;code&gt;test_steps&lt;/code&gt;, filtered by that page. It's looking for a step that means the same thing, the same action with the same expected outcome, even if it's worded differently. It returns the step plus one flag: &lt;code&gt;from_memory&lt;/code&gt;, true or false.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Reuse or write.&lt;/strong&gt; A branch splits on that flag.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Found:&lt;/strong&gt; pull the stored program from memory. No code generation at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not found:&lt;/strong&gt; a second LLM writes a new program from scratch. It can only use the commands discovered in step 1, and it has house rules. Use &lt;code&gt;clickN&lt;/code&gt; for repeated selectors. Fall back to &lt;code&gt;llmClickElement&lt;/code&gt; when there's no reliable selector. Output one line, no template literals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the bet. The first time the agent sees something like "click Raise Issue on a to-do," an LLM writes the code. The next time a similar step shows up, on a different ticket, phrased differently, it should come straight from memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Run it and look again.&lt;/strong&gt; The program, stored or new, goes to the browser, followed by another screenshot. A third LLM compares before and after against the expected result. It returns one of four verdicts: &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;broken&lt;/code&gt; or &lt;code&gt;test_broken&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Remember or forget.&lt;/strong&gt; A final branch decides what happens to memory.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;success&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;: store the program that was used, tagged by page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;broken&lt;/code&gt; or &lt;code&gt;test_broken&lt;/code&gt;: delete the stored program that just produced a bad result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So code that works gets reused, and code that misbehaves gets thrown out. The agent shouldn't keep trusting automation that just let it down. That's the theory, anyway.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to try it
&lt;/h2&gt;

&lt;p&gt;The whole thing is open source and runs on Docker. It's built from two pieces. &lt;strong&gt;Agent Factory&lt;/strong&gt; is the studio where you design, edit and chat with agents. &lt;strong&gt;BaaS&lt;/strong&gt; (Browser as a Service) is what actually drives Chrome: it opens pages, clicks, waits and takes screenshots for the agent.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Ursa-Minor-Beta" rel="noopener noreferrer"&gt;
        Ursa-Minor-Beta
      &lt;/a&gt; / &lt;a href="https://github.com/Ursa-Minor-Beta/agent-factory-docker-api-ui" rel="noopener noreferrer"&gt;
        agent-factory-docker-api-ui
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Docker deployment for Agent Factory (agent-factory + agent-factory-ui).
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Agent Factory Deploy&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Docker deployment for Agent Factory (&lt;a href="https://github.com/Ursa-Minor-Beta/agent-factory" rel="noopener noreferrer"&gt;agent-factory&lt;/a&gt; + &lt;a href="https://github.com/Ursa-Minor-Beta/agent-factory-ui" rel="noopener noreferrer"&gt;agent-factory-ui&lt;/a&gt;).&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick Start&lt;/h2&gt;
&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Copy environment file:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;cp .env.example .env&lt;/pre&gt;

&lt;/div&gt;

&lt;ol start="2"&gt;
&lt;li&gt;
&lt;p&gt;Edit &lt;code&gt;.env&lt;/code&gt; with your configuration (at minimum set &lt;code&gt;ADMIN_EMAIL&lt;/code&gt; and &lt;code&gt;ADMIN_PASSWORD&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run with local MongoDB:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;docker-compose --profile with-db up --build&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Or with external MongoDB (set &lt;code&gt;MONGODB_URI&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;):&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;docker-compose up --build&lt;/pre&gt;

&lt;/div&gt;

&lt;ol start="4"&gt;
&lt;li&gt;Access
&lt;ul&gt;
&lt;li&gt;UI: &lt;a href="http://localhost:8080" rel="nofollow noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API: &lt;a href="http://localhost:3000" rel="nofollow noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Deploy Specific Version&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;API_VERSION=&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;branch/tag&lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; UI_VERSION=&lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;branch/tag&lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; docker-compose up --build&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Update After Branch Changes&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;docker-compose --profile with-db build --no-cache &lt;span class="pl-k"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker-compose --profile with-db up&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Configuration&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;&lt;/p&gt;&lt;div class="table-wrapper-paragraph"&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;table&gt;

&lt;thead&gt;

&lt;tr&gt;

&lt;th&gt;Variable&lt;/th&gt;

&lt;th&gt;Description&lt;/th&gt;

&lt;th&gt;Default&lt;/th&gt;

&lt;/tr&gt;

&lt;/thead&gt;

&lt;tbody&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;API_VERSION&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;API branch/tag to deploy&lt;/td&gt;

&lt;td&gt;&lt;code&gt;master&lt;/code&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;UI_VERSION&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;UI branch/tag to deploy&lt;/td&gt;

&lt;td&gt;&lt;code&gt;master&lt;/code&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;API_PORT&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;API exposed port&lt;/td&gt;

&lt;td&gt;&lt;code&gt;3000&lt;/code&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;UI_PORT&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;UI exposed port&lt;/td&gt;

&lt;td&gt;&lt;code&gt;8080&lt;/code&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;MONGO_PORT&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;MongoDB exposed port&lt;/td&gt;

&lt;td&gt;&lt;code&gt;27017&lt;/code&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;MONGO_USER&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;MongoDB root username&lt;/td&gt;

&lt;td&gt;-&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;MONGO_PASSWORD&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;MongoDB root password&lt;/td&gt;

&lt;td&gt;-&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;JWT_SECRET&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;JWT signing secret&lt;/td&gt;

&lt;td&gt;-&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;ENCRYPTION_KEY&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;Encryption key (32 bytes)&lt;/td&gt;

&lt;td&gt;-&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;MONGODB_URI&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;MongoDB connection string&lt;/td&gt;

&lt;td&gt;&lt;code&gt;mongodb://mongo:27017/agent-factory&lt;/code&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;tr&gt;

&lt;td&gt;&lt;code&gt;VITE_API_URL&lt;/code&gt;&lt;/td&gt;

&lt;td&gt;API URL for UI&lt;/td&gt;

&lt;td&gt;&lt;code&gt;&lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;&lt;/code&gt;&lt;/td&gt;

&lt;/tr&gt;

&lt;/tbody&gt;

&lt;/table&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;…&lt;/div&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Ursa-Minor-Beta/agent-factory-docker-api-ui" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Ursa-Minor-Beta" rel="noopener noreferrer"&gt;
        Ursa-Minor-Beta
      &lt;/a&gt; / &lt;a href="https://github.com/Ursa-Minor-Beta/baas" rel="noopener noreferrer"&gt;
        baas
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Browser as a Service — browser automation and document extraction over HTTP
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;BaaS — Browser as a Service&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;BaaS drives a real Chrome instance behind an HTTP API. You POST a small
JavaScript-like program describing what to do in the browser; BaaS runs it and
returns the page, a screenshot, cookies, a downloaded file, or Markdown
extracted from whatever the page served you.&lt;/p&gt;
&lt;p&gt;It also parses documents without a browser: PDF, DOCX, XLSX, PPTX, CSV and
plain text all convert to Markdown through the same API.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser automation&lt;/strong&gt; — navigate, click, type, wait, upload, download
screenshot, run arbitrary JS, all through ChromeDP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM-assisted actions&lt;/strong&gt; — &lt;code&gt;llmClick('the login button')&lt;/code&gt;
&lt;code&gt;llmLogin(user, pass)&lt;/code&gt;, &lt;code&gt;llmText('one-time code')&lt;/code&gt;, for pages whose selectors
you do not know in advance. Backed by OpenAI, Azure OpenAI or Ollama.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync and async sessions&lt;/strong&gt; — one-shot programs, or a long-lived session you
send commands to over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readability extraction&lt;/strong&gt; — article text, tables and metadata from a URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document conversion&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Ursa-Minor-Beta/baas" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The short version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start Agent Factory.&lt;/strong&gt; Clone the repo, set an admin email and password in &lt;code&gt;.env&lt;/code&gt;, and run:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose &lt;span class="nt"&gt;--profile&lt;/span&gt; with-db up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run BaaS.&lt;/strong&gt; Clone the repo and set an API key, &lt;code&gt;BROWSER_HEADFUL=true&lt;/code&gt; and your LLM credentials in its &lt;code&gt;.env&lt;/code&gt;. If you run BaaS locally, also set your Chrome path. Locally it operates your own browser, and you get to watch the agent click around in a real window. Map &lt;code&gt;host.docker.internal&lt;/code&gt; to &lt;code&gt;127.0.0.1&lt;/code&gt; so the two can find each other, then start BaaS. It will complain about missing Pandoc and friends. Ignore it, that's what success looks like.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connect and smoke test.&lt;/strong&gt; In the studio at &lt;code&gt;localhost:8080&lt;/code&gt;, add &lt;code&gt;BAAS_API_KEY&lt;/code&gt; under Secrets and your LLM key under Provider. Then run the Browser Screenshot agent on google.com. If you get a screenshot back, you're wired up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hook up Jira.&lt;/strong&gt; Create an Atlassian API token, base64-encode &lt;code&gt;email:token&lt;/code&gt;, and save it as the &lt;code&gt;Jira_auth&lt;/code&gt; secret:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"you@example.com:your-api-token"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;In the Test Orchestrator agent, set your test environment URL and Jira subdomain, and hardcode your login in the &lt;code&gt;http-1&lt;/code&gt; node. Login rarely changes, so there's no point paying tokens for it every run.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run it and tune it.&lt;/strong&gt; Chat with Test Orchestrator, give it a ticket ID like &lt;code&gt;SCRUM-165&lt;/code&gt; and your URL, and watch. Then teach it your app: describe it in the &lt;code&gt;llm-generate-steps&lt;/code&gt; prompt (your user documentation works well here), and adjust the three Test Step Executor prompts. &lt;code&gt;llm-0&lt;/code&gt; adapts steps to the screen, &lt;code&gt;llm-1&lt;/code&gt; writes the BaaS commands, and &lt;code&gt;llm-2&lt;/code&gt; decides pass, fail or broken.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every command and config value is in my previous post: &lt;a href="https://quality-minded.medium.com/building-an-ai-qa-agent-that-tests-your-jira-tickets-in-a-real-browser-65d1cf7201b0" rel="noopener noreferrer"&gt;Building an AI QA Agent That Tests Your Jira Tickets in a Real Browser&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a side project, and I'd honestly like to know whether to keep building it. If it's useful, star the repo, send me a DM, or leave a comment. If it breaks on your app, tell me that too. That's often the more useful message.&lt;/p&gt;

&lt;p&gt;Keep humans on the loop, and don't ship on Fridays.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>qa</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building an AI QA Agent That Tests Your Jira Tickets in a Real Browser</title>
      <dc:creator>Katja</dc:creator>
      <pubDate>Fri, 25 Sep 2026 17:05:32 +0000</pubDate>
      <link>https://dev.to/quality_minder/building-an-ai-qa-agent-that-tests-your-jira-tickets-in-a-real-browser-3pc7</link>
      <guid>https://dev.to/quality_minder/building-an-ai-qa-agent-that-tests-your-jira-tickets-in-a-real-browser-3pc7</guid>
      <description>&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;Give it a Jira ticket ID and a test environment URL, and this agent reads the ticket, writes test steps, and runs them in a real Chrome window while you watch.&lt;/p&gt;

&lt;p&gt;It's built from two open-source pieces. &lt;strong&gt;Agent Factory&lt;/strong&gt; is the studio where agents are designed, edited, and chatted with. &lt;strong&gt;BaaS&lt;/strong&gt; (Browser as a Service) is the part that actually drives the browser: it opens pages, clicks, waits, and takes screenshots on the agent's behalf.&lt;/p&gt;

&lt;p&gt;This post walks through setting both up locally and running the bug check agent against your own app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;p&gt;Required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub account&lt;/li&gt;
&lt;li&gt;An OpenAI API key (Anthropic works too, but I recommend starting with OpenAI)&lt;/li&gt;
&lt;li&gt;Google Chrome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommended:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker.&lt;/strong&gt; The instructions below assume it, but a bare-metal setup works as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt;, so you can run BaaS locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Start Agent Factory
&lt;/h2&gt;

&lt;p&gt;Clone the repo: &lt;a href="https://github.com/Ursa-Minor-Beta/agent-factory-docker-api-ui" rel="noopener noreferrer"&gt;Ursa-Minor-Beta/agent-factory-docker-api-ui&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then create your env file and bring everything up with the bundled database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# edit .env – at minimum set the admin email and password&lt;/span&gt;
docker-compose &lt;span class="nt"&gt;--profile&lt;/span&gt; with-db up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The admin email and password in &lt;code&gt;.env&lt;/code&gt; are what you'll use to log in to the factory later, so pick something you'll remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Run BaaS locally
&lt;/h2&gt;

&lt;p&gt;I recommend running BaaS locally while you debug and in Docker while you test, because locally you can watch exactly what the agent does in the browser. If the local setup fights you (OS permissions and the like), fall back to Docker. It's stable.&lt;/p&gt;

&lt;p&gt;Start MongoDB and create the env file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;baas
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; mongodb
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
vim .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set these values in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;pick-a-key&amp;gt;

&lt;span class="nv"&gt;BROWSER_EXECUTABLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'&lt;/span&gt;
&lt;span class="nv"&gt;BROWSER_HEADFUL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true

&lt;/span&gt;&lt;span class="nv"&gt;LLM_CLIENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openai
&lt;span class="nv"&gt;OPENAI_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your token here&amp;gt;"&lt;/span&gt;
&lt;span class="nv"&gt;OPENAI_ORGANIZATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your organization here&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BROWSER_HEADFUL=true&lt;/code&gt; is what makes the Chrome window visible. The path above is for macOS; point it at your own Chrome binary on other systems. Remember your &lt;code&gt;API_KEY&lt;/code&gt;, because the studio needs it in Step 3.&lt;/p&gt;

&lt;p&gt;Next, map &lt;code&gt;host.docker.internal&lt;/code&gt; to localhost so the Dockerized factory and the local BaaS can find each other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vim /etc/hosts
&lt;span class="c"&gt;# add this line:&lt;/span&gt;
127.0.0.1       host.docker.internal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, load the env and start BaaS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; ./.env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
go run ./cmd/baas
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see warnings about missing Pandoc, PDF-to-image, and wmctrl. Ignore them: you don't need those tools, and this is what a successful start looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Connect the studio and run a smoke test
&lt;/h2&gt;

&lt;p&gt;Open the studio at &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt; and log in with the admin credentials from Step 1. Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Secrets&lt;/strong&gt; and add &lt;code&gt;BAAS_API_KEY&lt;/code&gt;, set to the same value as &lt;code&gt;API_KEY&lt;/code&gt; in the BaaS &lt;code&gt;.env&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Provider&lt;/strong&gt; and add your OpenAI (or Anthropic) API key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole setup. To check the browser actually works, go to &lt;strong&gt;Agents → Browser Screenshot&lt;/strong&gt; and enter &lt;code&gt;https://google.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A new Chrome window will open. It may ask for permissions, so allow them. If everything is wired up, the window loads Google and the chat returns a browser session ID plus a screenshot of the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Wire up Jira and the Test Orchestrator
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create a Jira auth secret
&lt;/h3&gt;

&lt;p&gt;Log in to &lt;a href="https://id.atlassian.com/manage-profile/security/api-tokens" rel="noopener noreferrer"&gt;Atlassian API tokens&lt;/a&gt; and choose &lt;strong&gt;Create API token with scopes&lt;/strong&gt;. Then encode your email and token together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"you@example.com:your-api-token"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the output to the studio's &lt;strong&gt;Secrets&lt;/strong&gt; under the name &lt;code&gt;Jira_auth&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure the orchestrator
&lt;/h3&gt;

&lt;p&gt;In the factory, go to &lt;strong&gt;Agents&lt;/strong&gt;, find &lt;strong&gt;Test Orchestrator&lt;/strong&gt;, and click &lt;strong&gt;Edit&lt;/strong&gt;. Two things to change:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the &lt;code&gt;input-1&lt;/code&gt; node, replace the default URL (&lt;code&gt;https://google.com&lt;/code&gt;) with your test environment URL, and set your &lt;code&gt;jiraSubdomain&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Decide how to handle login in the &lt;code&gt;http-1&lt;/code&gt; node.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For login, I recommend hardcoding it in &lt;code&gt;http-1&lt;/code&gt;. Login flows rarely change, so scripting them once saves tokens on every run.&lt;/p&gt;

&lt;p&gt;If you'd rather skip a separate login step, set the body of &lt;code&gt;http-1&lt;/code&gt; to the following. It starts a browser session, opens the URL, and takes a screenshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;program&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;navigate('{{node:input-1.url}}'); sleep('3s'); waitReady(body,'timeout:20s'); takeScreenshot('screenshot', 'timeout:10s');&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;sessionID&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;{{node:http-0.response.result.sessionID}}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;stopSession&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: false}"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Run your first test, then tune it
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;Test Orchestrator&lt;/strong&gt; and click &lt;strong&gt;Chat&lt;/strong&gt;. Send it your Jira ticket ID (something like &lt;code&gt;SCRUM-165&lt;/code&gt;) and your test environment URL. The agent takes it from there and runs the first test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Teach it about your app
&lt;/h3&gt;

&lt;p&gt;The prompt in &lt;code&gt;llm-generate-steps&lt;/code&gt; is where test steps get written. Replace the example details there with what you know about your application. Pasting in your user documentation, if you have any, works well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tune the Test Step Executor
&lt;/h3&gt;

&lt;p&gt;The orchestrator uses &lt;strong&gt;Test Step Executor&lt;/strong&gt; as a skill, and its prompts are worth adapting to your app and its quirks. Each LLM node has one job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;llm-0&lt;/code&gt;&lt;/strong&gt; adapts each test step to what's actually on screen, using the screenshot. Tell it about any layout or navigation it should expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;llm-1&lt;/code&gt;&lt;/strong&gt; writes the BaaS commands. Tell it which selectors to prefer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;llm-2&lt;/code&gt;&lt;/strong&gt; decides whether a step passed, failed, or is broken. Tell it anything to check closely, like whether the logo is visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Over to you
&lt;/h2&gt;

&lt;p&gt;I hope this agent saves you some manual testing. If you run into problems or have ideas, get in touch.&lt;/p&gt;

&lt;p&gt;One honest ask: this is a side project, and I'd like to know whether to keep building it. If it's useful to you, &lt;a href="https://github.com/Ursa-Minor-Beta/agent-factory-docker-api-ui" rel="noopener noreferrer"&gt;star the repo on GitHub&lt;/a&gt;, send me a DM, or leave a comment below. Any of those tells me the time is well spent.&lt;/p&gt;

&lt;p&gt;Keep humans on the loop and don’t ship on Fridays!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>automation</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
