Chapter 3 . Agents and workflows . Jira test-plan agent
Jira to test plan: the model writes, the code formats
Type a Jira key and get a formal 13-section test plan. A small Express proxy fetches the ticket, asks Groq for the plan as JSON, and renders the Markdown with plain code, so the layout never depends on the model. The chapter also teaches B.L.A.S.T., the build protocol an AI coding agent followed to make the app.
Objective to Approvals, defined in architecture/test-plan-template.md.
3
Layer 3 tools
jiraClient.js, groqClient.js and testPlan.js; server.js only routes.
5
B.L.A.S.T. phases
Blueprint, Link, Architect, Stylize, Trigger, after a Protocol 0 that creates the memory files.
200
Recorded live run
VWO-48 in progress.md: HTTP 200, a 13-section plan, 2293 bytes of Markdown.
01Why a tester cares
Most test plans start from a Jira ticket, and most of the writing repeats: objective, scope, environments, entry and exit criteria, risks, sign-off. This chapter builds a small app that drafts that plan from the ticket itself. You type a key such as VWO-48, the app fetches the issue, asks a model for the plan as JSON, and shows a formal 13-section plan that you can review and download as Markdown.
Three design choices make it usable at work, and they are the real lessons of the chapter:
The browser never talks to Jira. Jira Cloud's REST API does not allow browser calls (CORS), and an API token must not sit in front-end code, so a small Express proxy makes both outside calls.
The model writes content, code writes the document. Groq returns one JSON object with fixed keys. Plain JavaScript turns it into Markdown, so the headings, tables and file name are the same on every run.
Missing facts become TBD. The prompt forbids invented names, dates and versions, and the code fills any missing key with [] or "TBD", so a partial reply never breaks the page.
sequenceDiagram
participant UI as React UI (port 5173)
participant S as Express proxy (port 8787)
participant J as Jira Cloud REST v3
participant G as Groq openai/gpt-oss-120b
UI->>S: POST /api/generate with jiraId and config
S->>S: mergeConfig, non-empty UI values win over .env
S->>J: GET /rest/api/3/issue/KEY?fields=...
J-->>S: issue JSON, description in ADF
S->>S: normalizeIssue, then buildMessages
S->>G: chat/completions, JSON mode, temperature 0.3
G-->>S: test plan content as JSON
S->>S: generateTestPlan defaults, renderMarkdown
S-->>UI: issue, plan and markdown
One Generate click. Only the proxy holds credentials; the model only ever sees the prompt and returns JSON.
02The B.L.A.S.T. framework
B.L.A.S.T.md is a master system prompt for an AI coding agent. You hand it to the agent together with your objective (Objective.md), and it makes the agent work in a fixed order instead of jumping straight to code: set up memory files, ask questions, prove every connection, then build in layers. The name is the five phases: Blueprint, Link, Architect, Stylize, Trigger.
Phase
What B.L.A.S.T.md asks for
What it produced in this chapter
Protocol 0 Initialization
Create task_plan.md, findings.md, progress.md and a project constitution (schemas, rules, invariants). Write nothing in tools/ until the discovery questions are answered and the schema is approved.
The four memory files. The constitution is LLM.md (the template calls it gemini.md).
B: Blueprint
Ask five discovery questions: North Star, Integrations, Source of Truth, Delivery Payload, Behavioral Rules. Define the JSON input and output shapes before any code. Research.
Four confirmed schemas in LLM.md: config, generate request, normalized issue, test-plan payload.
L: Link
Test every API connection and .env credential with minimal handshake scripts. Do not build logic on a broken link.
tools/handshake.js: fetch one Jira issue, ask Groq for {"ok":true}, exit 0 or 1.
A: Architect
Build in three layers: SOPs in architecture/, a navigation layer that only routes, atomic tools in tools/.
Format the payload for delivery, build a clean UI, show the result to the user for feedback before deploying.
React Generate and Settings tabs, a formatted view and a raw Markdown view, Download .md.
T: Trigger
Move the logic to the cloud, set up triggers, finish the maintenance log.
Vercel serverless functions in api/; a dated maintenance log at the end of LLM.md.
flowchart LR
P0["Protocol 0"] --> B[Blueprint] --> L[Link] --> A[Architect] --> S[Stylize] --> T[Trigger]
A -. a tool fails .-> R[Self-annealing] -.-> A
Protocol 0 first, then the five phases in order. When a tool fails, the self-annealing loop patches it and writes the lesson into its SOP.
The A.N.T. three-layer architecture
The Architect phase splits the build into three layers, because, in the protocol's words, LLMs are probabilistic and business logic must be deterministic. Layer 1 is Markdown that says how each tool behaves (the golden rule: change the SOP before the code). Layer 2 only routes. Layer 3 holds small tools you can test on their own.
The three layers as folders in the chapter. Only Layer 3 calls Jira and Groq.
Three operating rules
Data first. Write the input and output JSON shapes into the constitution before building any tool. Here that is LLM.md, section 3, with four confirmed schemas.
Self-annealing. When a tool fails: read the error, patch the tool, test it, then record the lesson in the matching architecture/*.md. The real example from progress.md: the .env said JIRA_API_TOKEN but the code read JIRA_TOKEN, so every reader now accepts both.
Deliverables versus intermediates. Scratch files go in .tmp/; the project is only done when the payload reaches its destination.
Read the template with this build in mind.B.L.A.S.T.md is generic: it says Layer 3 tools are Python scripts and the constitution is gemini.md. This build used JavaScript and renamed the file LLM.md. The phases and rules are the same.
Objective.md
# Fetch the JIRA ID and Create a Test Plan Generator
# VWO-48 -> Fetch Test Plan
You please read the file of B.L.A.S.T.md again and my objective again, and create a lightweight React application which will take:
- the Jira configuration
- Jira email ID
- Jira token
- my Jira base URL
- GROQ connection API details in the settings and take the JIRA ID and create the TestPlan automatically.
You will be able to create a test plan based on the by fetching the vwo48 automatically. @chapter_03_BLAST_FW/B.L.A.S.T.md
GROQ - openai/gpt-oss-120b (FREE)
fETCH JIRA -> emAIL, token, JIRA - VWO-48
The build brief, exactly as it was typed. prompt.md records the eight prompts that followed it.
03Live demo: one Generate click, step by step
Each press of Step runs one function of the real request path. Pick a ticket, choose where the credentials come from, and choose what the model sends back. VWO-48 and VWO-49 are the Jira responses stored as fixtures in chapter 13; VWO-50 is a bare ticket written for this page. The prompt and all the formatting code are ported from the repo; the model replies are canned, so every run is the same.
One Generate click, step by stepNo API key needed
POST /api/generate→mergeConfig→fetchIssue→normalizeIssue→buildMessages→groqChat→generateTestPlan→renderMarkdown
This stepMessages sent to Groq (ticket values highlighted)
No plan yet
Try these. Set Credentials to Nothing set: the proxy answers with the same 500 that progress.md recorded before the keys were added. Pick VWO-50 to see an empty description turn into (none provided) and the plan fill with TBD. Choose the reply JSON with 3 keys missing (no schedule, risks or approvals) and watch sections 7, 12 and 13. .env without GROQ key fails one step later, after Jira has already answered.
04The code, file by file
Everything that runs, and where it lives in chapter_03_BLAST_FW_JIRA_AI_AGENT.
Piece
What it does
Run or open
npm run dev
Starts node server.js (SERVER, port 8787) and vite (CLIENT, port 5173) together; Vite proxies /api to 8787.
npm run dev, then http://localhost:5173
npm run handshake
The Link phase check: fetches one Jira issue (default VWO-48) and asks Groq for {"ok":true}. Exit code 0 when both pass.
npm run handshake VWO-48
npm run build + npm start
Vite builds dist/; Express serves it and the API on one port with NODE_ENV=production.
http://localhost:8787
server.js
Layer 2. GET /api/config (only whether secrets exist), POST /api/generate, POST /api/save (writes output/test-plan-<id>.md).
started by npm run dev
tools/jiraClient.js
fetchIssue (Basic auth, field filter), flattenAdf, normalizeIssue.
Project memory: checklists, research, a dated progress log, the constitution, and the 8 prompts that drove the build.
read in that order
Fetch and flatten the ticket
Jira REST v3 returns the description as Atlassian Document Format (ADF), a tree of nodes rather than text. flattenAdf walks the tree, keeps the text and adds a line break after every block, then normalizeIssue turns the response into the flat object the prompt needs, with a default for every missing field.
tools/jiraClient.js
// Recursively flatten Atlassian Document Format (ADF) into plain text.functionflattenAdf(node) {
if (!node) return'';
if (typeof node === 'string') return node;
if (Array.isArray(node)) return node.map(flattenAdf).join('');
let text = '';
if (node.type === 'text' && typeof node.text === 'string') text += node.text;
if (node.content) text += flattenAdf(node.content);
// Add line breaks around block-level nodes so the text stays readable.if (['paragraph', 'heading', 'listItem', 'blockquote', 'bulletList', 'orderedList'].includes(node.type)) {
text += '\n';
}
if (node.type === 'hardBreak' || node.type === 'rule') text += '\n';
return text;
}
exportfunctionnormalizeIssue(raw) {
const f = raw.fields || {};
const description = typeof f.description === 'string'
? f.description
: flattenAdf(f.description).replace(/\n{3,}/g, '\n\n').trim();
return {
key: raw.key,
summary: f.summary || '',
description: description || '',
issueType: f.issuetype?.name || 'Unknown',
status: f.status?.name || 'Unknown',
priority: f.priority?.name || 'Unspecified',
components: (f.components || []).map((c) => c.name),
labels: f.labels || [],
fixVersions: (f.fixVersions || []).map((v) => v.name),
reporter: f.reporter?.displayName || 'Unknown',
assignee: f.assignee?.displayName || null,
};
}
Ask Groq for JSON, and refuse anything else
tools/groqClient.js
const GROQ_URL = 'https://api.groq.com/openai/v1/chat/completions';
exportconst GROQ_MODEL = 'openai/gpt-oss-120b';
exportasyncfunctiongroqChat(config, messages, { json = true, temperature = 0.3 } = {}) {
if (!config.groqKey) thrownewError('Missing GROQ API key');
const body = { model: GROQ_MODEL, messages, temperature };
if (json) body.response_format = { type: 'json_object' };
const res = awaitfetch(GROQ_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${config.groqKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!res.ok) {
const errText = await res.text();
thrownewError(`GROQ ${res.status}: ${errText.slice(0, 300)}`);
}
const data = await res.json();
const content = data.choices?.[0]?.message?.content || '';
if (!json) return content;
try {
return JSON.parse(content);
} catch {
thrownewError('GROQ did not return valid JSON');
}
}
The proxy route
mergeConfig lets non-empty Settings values override .env; any error thrown by a tool becomes a 500 with { error }, which the UI shows as the message.
Node.js 18 or newer, a Jira Cloud account with an API token, and a free Groq key. Put the values in .env or type them into the Settings tab (blank Settings fields fall back to .env).
terminal
cd chapter_03_BLAST_FW_JIRA_AI_AGENT
cp .env.sample .env # then fill GROQ_KEY, JIRA_EMAIL, JIRA_API_TOKEN, JIRA_URLnpm install
npm run handshake # Link check, defaults to VWO-48npm run dev # SERVER on 8787 + CLIENT on 5173, open http://localhost:5173
The variables, by name: GROQ_KEY, JIRA_EMAIL, JIRA_API_TOKEN (JIRA_TOKEN also works), JIRA_URL (for example https://your-site.atlassian.net), and optionally PORT (default 8787). The README links to the Atlassian API-token page and console.groq.com/keys.
The handshake. It prints a Jira section with PASS: plus the ticket key and summary, a GROQ section with PASS: {"ok":true}, then LINK OK and exits 0. If either side fails it prints FAIL: with the error and ends with LINK BROKEN and exit code 1. In the recorded run, VWO-48 came back as "Shopping cart total shows $0.00 after applying discount code".
terminal
npm run build # vite build -> dist/npm start # NODE_ENV=production node server.js -> http://localhost:8787
Deploy to your own Vercel project.api/config.js, api/generate.js and api/save.js replace the Express routes; set the four variables in the Vercel dashboard.
terminal
npx vercel link --yes --project <your-project-name>
npx vercel deploy --prod
Only type real credentials into a deployment you control. The Settings tab keeps your Jira token and Groq key in the browser (localStorage, key blast.jira.config) and sends them in the body of every POST /api/generate to whichever server hosts the app.
06What to watch
CORS is the reason for the proxy. A browser-only React app cannot call Jira Cloud REST. The proxy also keeps the token off the client, which matters more.
Descriptions are ADF, not text. REST v3 returns nested JSON. Without flattenAdf the prompt would contain [object Object]. Exotic nodes such as tables degrade to plain text (findings.md).
Ask only for the fields you render. The ?fields= filter keeps comments, history and attachments out of the prompt, which also keeps people's names and long threads away from the model.
The model can still be wrong. JSON mode guarantees valid JSON, not correct content. The rendered footer says it: review before use.
Express is pinned to v4. The production catch-all uses the regex /^(?!\/api).*/; Express 5 breaks app.get('*') (LLM.md, maintenance log).
Save to server is local only. On Vercel, /api/save returns 501 "Saving to server is disabled on serverless. Use Download .md." because the file system is read-only.
npm start uses POSIX syntax.NODE_ENV=production node server.js does not work in Windows cmd; use Git Bash or WSL, or set the variable separately.
Free tiers rate-limit. A Groq 429 comes back as GROQ 429: ... in the UI, which is why the SOP says to surface API errors instead of hiding them.
DDrills for the chapter
Code drills use chapter_03_BLAST_FW_JIRA_AI_AGENT (most run without a key: the tools are plain functions you can import). Playwright drills target the live demo on the Page tab; turn on Show locator badges to see every data-testid.
Flatten a real ticket
Import normalizeIssue from tools/jiraClient.js and pass it chapter_13_CREW_AI_QA_Pipeline/fixtures/VWO-49.json. How does the description start, and what are reporter and assignee?
Hint
node --input-type=module -e "import {normalizeIssue} from './tools/jiraClient.js'; ..." from the chapter 3 folder, with the fixture read by fs.readFileSync.
Expected result
It starts As a platform owner, I want the Orders API to reject any checkout ..., then Acceptance Criteria on its own line. The fixture has no reporter or assignee, so you get "Unknown" and null (the prompt prints Unassigned).
Settings or .env?
The Settings tab has " " (three spaces) as the Jira URL and .env has JIRA_URL. Which URL does mergeConfig use?
Expected result
The .env one. Each field is (c.jiraUrl || '').trim() || env.jiraUrl, so a blank or whitespace-only UI value falls back to .env.
A reply with holes
Groq returns valid JSON without schedule, risks and approvals. What do sections 7, 12 and 13 show, in the Markdown and in the formatted view?
Expected result
Each prints TBD instead of a table. generateTestPlan turns the missing arrays into [], and both renderMarkdown and TestPlanView print TBD for an empty table.
Break the link on purpose
Put a wrong GROQ_KEY in .env and run npm run handshake. What does it print, and what is the exit code?
Expected result
The Jira section passes, the GROQ section prints FAIL: GROQ 401: ... with the start of Groq's error body, and the run ends with LINK BROKEN and exit code 1. That is the Link phase doing its job before any feature work.
Why not call Jira from React?
Give two reasons the app needs server.js between the browser and Jira.
Expected result
Jira Cloud REST does not send CORS headers that allow a browser origin, so the call fails in the browser; and the API token would be visible to anyone who opens the page. The proxy fixes both.
Map the protocol to the folder
For each B.L.A.S.T. phase, name one file in the chapter it produced.
Expected result
Protocol 0: task_plan.md, findings.md, progress.md, LLM.md. Blueprint: the schemas in LLM.md section 3. Link: tools/handshake.js. Architect: architecture/*.md, server.js, tools/*.js. Stylize: src/. Trigger: api/*.js, vercel.json and the maintenance log.
Assert the happy pathPlaywright
Write a Playwright test for the demo: run VWO-48 end to end, then assert the status, the prompt and the number of plan sections.
Hint
Locators: jt-run, jt-status, jt-prompt, and the level-4 headings inside jt-plan.
Expected result
jt-status contains HTTP 200, jt-prompt contains Key: VWO-48, and jt-plan has 13 level-4 headings.
Assert the recorded failurePlaywright
Select Nothing set in jt-creds, run, and assert where the pipeline stopped.
Expected result
jt-status contains {"error":"Missing Jira base URL"}, jt-node-jira has the class is-error, and every node after it has is-skip.
Download the MarkdownPlaywright
Run VWO-49, click jt-download and check the file name and the first line of the file.
Hint
Wrap the click with page.waitForEvent('download'), then read await download.path() with fs.
Expected result
test-plan-VWO-49.md, starting with # Test Plan: Add server-side validation so an order total of $0.00 cannot be submitted (the title comes from the canned reply).
SSolutions: the demo spec and the key files
The Playwright spec passes against this page as written. The other tabs are the chapter's core files, straight from the course repo.
tests/jira-test-plan-agent-demo.spec.ts
import { test, expect } from'@playwright/test';
const URL = 'https://app.thetestingacademy.com/ai/blueprint/learn/jira-test-plan-agent.html';
test('VWO-48 goes through all 8 steps and renders a 13-section plan', async ({ page }) => {
await page.goto(URL);
await page.getByTestId('jt-run').click();
awaitexpect(page.getByTestId('jt-status')).toContainText('HTTP 200');
awaitexpect(page.getByTestId('jt-node-render')).toHaveClass(/is-done/);
awaitexpect(page.getByTestId('jt-prompt')).toContainText('Key: VWO-48');
awaitexpect(page.getByTestId('jt-prompt')).toContainText('Return ONLY a JSON object with EXACTLY these keys');
awaitexpect(page.getByTestId('jt-plan').getByRole('heading', { level: 4 })).toHaveCount(13);
});
test('missing keys become TBD, and the plan downloads as Markdown', async ({ page }) => {
await page.goto(URL);
await page.getByTestId('jt-reply').selectOption('partial');
await page.getByTestId('jt-run').click();
const plan = page.getByTestId('jt-plan');
awaitexpect(plan.getByRole('region', { name: '7. Schedule' })).toContainText('TBD');
awaitexpect(plan.getByRole('region', { name: '13. Approvals' })).toContainText('TBD');
awaitexpect(page.getByTestId('jt-status')).toContainText('3 of them TBD');
await page.getByTestId('jt-view').click();
awaitexpect(page.getByTestId('jt-markdown')).toContainText('## 12. Risks & Mitigations');
const download = page.waitForEvent('download');
await page.getByTestId('jt-download').click();
expect((await download).suggestedFilename()).toBe('test-plan-VWO-48.md');
});
test('with no credentials the proxy answers 500 before calling Jira', async ({ page }) => {
await page.goto(URL);
await page.getByTestId('jt-creds').selectOption('none');
await page.getByTestId('jt-run').click();
awaitexpect(page.getByTestId('jt-status')).toContainText('{"error":"Missing Jira base URL"}');
awaitexpect(page.getByTestId('jt-node-jira')).toHaveClass(/is-error/);
awaitexpect(page.getByTestId('jt-node-groq')).toHaveClass(/is-skip/);
});
Verbatim from the course repo, except that long dashes are printed as hyphens.
B.L.A.S.T.md
# 🚀 B.L.A.S.T. Master System Prompt
**Identity:** You are the **System Pilot**. Your mission is to build deterministic, self-healing automation in Antigravity using the **B.L.A.S.T.** (Blueprint, Link, Architect, Stylize, Trigger) protocol and the **A.N.T.** 3-layer architecture. You prioritize reliability over speed and never guess at business logic.
---
## 🟢 Protocol 0: Initialization (Mandatory)
Before any code is written or tools are built:
1. **Initialize Project Memory**
- Create:
- `task_plan.md` → Phases, goals, and checklists
- `findings.md` → Research, discoveries, constraints
- `progress.md` → What was done, errors, tests, results
- Initialize `LLM.md` as the **Project Constitution**:
- Data schemas
- Behavioral rules
- Architectural invariants
2. **Halt Execution**
You are strictly forbidden from writing scripts in `tools/` until:
- Discovery Questions are answered
- The Data Schema is defined in `gemini.md`
- `task_plan.md` has an approved Blueprint
---
## 🏗️ Phase 1: B - Blueprint (Vision & Logic)
**1. Discovery:** Ask the user the following 5 questions:
- **North Star:** What is the singular desired outcome?
- **Integrations:** Which external services (Slack, Shopify, etc.) do we need? Are keys ready?
- **Source of Truth:** Where does the primary data live?
- **Delivery Payload:** How and where should the final result be delivered?
- **Behavioral Rules:** How should the system "act"? (e.g., Tone, specific logic constraints, or "Do Not" rules).
**2. Data-First Rule:** You must define the **JSON Data Schema** (Input/Output shapes) in `gemini.md`. Coding only begins once the "Payload" shape is confirmed.
**3. Research:** Search github repos and other databases for any helpful resources for this project
---
## ⚡ Phase 2: L - Link (Connectivity)
**1. Verification:** Test all API connections and `.env` credentials.
**2. Handshake:** Build minimal scripts in `tools/` to verify that external services are responding correctly. Do not proceed to full logic if the "Link" is broken.
---
## ⚙️ Phase 3: A - Architect (The 3-Layer Build)
You operate within a 3-layer architecture that separates concerns to maximize reliability. LLMs are probabilistic; business logic must be deterministic.
**Layer 1: Architecture (`architecture/`)**
- Technical SOPs written in Markdown.
- Define goals, inputs, tool logic, and edge cases.
- **The Golden Rule:** If logic changes, update the SOP before updating the code.
**Layer 2: Navigation (Decision Making)**
- This is your reasoning layer. You route data between SOPs and Tools.
- You do not try to perform complex tasks yourself; you call execution tools in the right order.
**Layer 3: Tools (`tools/`)**
- Deterministic Python scripts. Atomic and testable.
- Environment variables/tokens are stored in `.env`.
- Use `.tmp/` for all intermediate file operations.
---
## ✨ Phase 4: S - Stylize (Refinement & UI)
**1. Payload Refinement:** Format all outputs (Slack blocks, Notion layouts, Email HTML) for professional delivery.
**2. UI/UX:** If the project includes a dashboard or frontend, apply clean CSS/HTML and intuitive layouts.
**3. Feedback:** Present the stylized results to the user for feedback before final deployment.
---
## 🛰️ Phase 5: T - Trigger (Deployment)
**1. Cloud Transfer:** Move finalized logic from local testing to the production cloud environment.
**2. Automation:** Set up execution triggers (Cron jobs, Webhooks, or Listeners).
**3. Documentation:** Finalize the **Maintenance Log** in `gemini.md` for long-term stability.
---
## 🛠️ Operating Principles
### 1. The "Data-First" Rule
Before building any Tool, you must define the **Data Schema** in `gemini.md`.
- What does the raw input look like?
- What does the processed output look like?
- Coding only begins once the "Payload" shape is confirmed.
- After any meaningful task:
- Update `progress.md` with what happened and any errors.
- Store discoveries in `findings.md`.
- Only update `gemini.md` when:
- A schema changes
- A rule is added
- Architecture is modified
`gemini.md` is *law*.
The planning files are *memory*.
### 2. Self-Annealing (The Repair Loop)
When a Tool fails or an error occurs:
1. **Analyze**: Read the stack trace and error message. Do not guess.
2. **Patch**: Fix the Python script in `tools/`.
3. **Test**: Verify the fix works.
4. **Update Architecture**: Update the corresponding `.md` file in `architecture/` with the new learning (e.g., "API requires a specific header" or "Rate limit is 5 calls/sec") so the error never repeats.
### 3. Deliverables vs. Intermediates
- **Local (`.tmp/`):** All scraped data, logs, and temporary files. These are ephemeral and can be deleted.
- **Global (Cloud):** The "Payload." Google Sheets, Databases, or UI updates. **A project is only "Complete" when the payload is in its final cloud destination.**
## 📂 File Structure Reference
Plaintext
`├── gemini.md # Project Map & State Tracking
├── .env # API Keys/Secrets (Verified in 'Link' phase)
├── architecture/ # Layer 1: SOPs (The "How-To")
├── tools/ # Layer 3: Python Scripts (The "Engines")
└── .tmp/ # Temporary Workbench (Intermediates)`