These tests will run any time a request in this collection is sent.
-
+
{
initialScroll={testsScroll}
onScroll={setTestsScroll}
/>
+
);
};
diff --git a/packages/bruno-app/src/utils/ai/index.js b/packages/bruno-app/src/utils/ai/index.js
index a2552d207..9e4ea8d71 100644
--- a/packages/bruno-app/src/utils/ai/index.js
+++ b/packages/bruno-app/src/utils/ai/index.js
@@ -24,6 +24,23 @@ export const testAiProvider = ({ providerId }) =>
export const aiGenerateText = (params) =>
callIpc('renderer:ai-generate-text', params);
+export const aiGenerateScript = (params) =>
+ callIpc('renderer:ai-generate-script', params);
+
+export const buildRequestContextFromItem = (item) => {
+ if (!item) return null;
+ const req = item.draft ? item.draft.request : item.request;
+ if (!req) return null;
+
+ return {
+ url: req.url || '',
+ method: req.method || 'GET',
+ headers: Array.isArray(req.headers) ? req.headers : [],
+ params: Array.isArray(req.params) ? req.params : [],
+ body: req.body || null
+ };
+};
+
/**
* Start a streaming generation. Subscribes to the corresponding `main:ai-stream-*`
* channels filtered by streamId. Returns a handle with `.stop()` and a promise
diff --git a/packages/bruno-electron/src/ipc/ai/index.js b/packages/bruno-electron/src/ipc/ai/index.js
index 7f3636c2d..f418674e3 100644
--- a/packages/bruno-electron/src/ipc/ai/index.js
+++ b/packages/bruno-electron/src/ipc/ai/index.js
@@ -11,6 +11,7 @@ const {
getAvailableModels,
clearSdkCache
} = require('./providers');
+const { SCRIPT_PROMPTS, SCRIPT_TYPES, buildScriptUserPrompt, stripCodeFences } = require('./script-prompts');
const activeStreams = new Map();
@@ -49,6 +50,16 @@ const resolveModel = (modelId) => {
});
};
+const pickDefaultModelId = () => {
+ const aiPreferences = getAiPrefs();
+ const hasApiKey = (providerId) => aiKeyStore.hasKey(providerId);
+ const available = getAvailableModels({ aiPreferences, hasApiKey });
+ if (available.length === 0) return null;
+ const preferred = aiPreferences.defaultModel;
+ if (preferred && available.some((m) => m.id === preferred)) return preferred;
+ return available[0].id;
+};
+
const registerAiIpc = (mainWindow) => {
ipcMain.handle('renderer:get-ai-status', async () => buildStatus());
@@ -133,6 +144,42 @@ const registerAiIpc = (mainWindow) => {
}
});
+ ipcMain.handle('renderer:ai-generate-script', async (_event, params) => {
+ const { scriptType, prompt, currentScript, requestContext, model: requestedModel } = params || {};
+
+ if (!SCRIPT_TYPES.includes(scriptType)) {
+ return { error: `Unknown scriptType: ${scriptType}` };
+ }
+ if (!prompt || !prompt.trim()) {
+ return { error: 'Prompt is required' };
+ }
+
+ const modelId = requestedModel || pickDefaultModelId();
+ if (!modelId) {
+ return { error: 'No AI model available. Configure a provider in Preferences > AI.' };
+ }
+
+ let model;
+ try {
+ model = resolveModel(modelId);
+ } catch (err) {
+ return { error: err.message };
+ }
+
+ try {
+ const { text } = await generateText({
+ model,
+ system: SCRIPT_PROMPTS[scriptType],
+ prompt: buildScriptUserPrompt({ userPrompt: prompt, currentScript, requestContext }),
+ maxOutputTokens: 2048
+ });
+ return { content: stripCodeFences(text), modelId };
+ } catch (err) {
+ console.error('AI generate-script error:', err);
+ return { error: err.message || 'Failed to generate script' };
+ }
+ });
+
ipcMain.on('renderer:ai-stream-text', async (_event, params) => {
const { streamId, model: modelId, system, messages, prompt, maxTokens, temperature } = params || {};
if (!streamId) return;
diff --git a/packages/bruno-electron/src/ipc/ai/script-prompts.js b/packages/bruno-electron/src/ipc/ai/script-prompts.js
new file mode 100644
index 000000000..a5f2c3b06
--- /dev/null
+++ b/packages/bruno-electron/src/ipc/ai/script-prompts.js
@@ -0,0 +1,207 @@
+const BRUNO_API_REFERENCE = `## Bruno API Reference
+
+### bru – environment & variables
+\`\`\`js
+bru.getEnvVar(key)
+bru.setEnvVar(key, value)
+bru.setEnvVar(key, value, { persist: true })
+bru.hasEnvVar(key)
+bru.deleteEnvVar(key)
+bru.getEnvName()
+
+bru.getGlobalEnvVar(key)
+bru.setGlobalEnvVar(key, value)
+
+bru.getVar(key) // runtime var
+bru.setVar(key, value)
+bru.hasVar(key)
+bru.deleteVar(key)
+
+bru.getCollectionVar(key)
+bru.getFolderVar(key)
+bru.getRequestVar(key)
+bru.getSecretVar(key)
+bru.getProcessEnv(key)
+\`\`\`
+
+### bru – utilities
+\`\`\`js
+bru.cwd()
+bru.getCollectionName()
+bru.interpolate(strOrObj)
+await bru.sleep(ms)
+bru.visualize(htmlString)
+bru.setNextRequest(requestName)
+await bru.sendRequest({ url, method, headers, body })
+\`\`\`
+
+### req – request object (available in pre-request, post-response, tests)
+\`\`\`js
+req.url, req.method, req.headers, req.body
+req.getUrl() / req.setUrl(url)
+req.getMethod() / req.setMethod(method)
+req.getHeaders() / req.setHeaders(headers)
+req.getHeader(name) / req.setHeader(name, value)
+req.getBody() / req.setBody(data)
+req.getTimeout() / req.setTimeout(ms)
+\`\`\`
+
+### res – response object (available in post-response and tests only)
+\`\`\`js
+res.status, res.statusText, res.headers, res.body, res.responseTime
+res.getStatus()
+res.getStatusText()
+res.getHeaders()
+res.getHeader(name)
+res.getBody()
+res.setBody(data)
+res.getResponseTime()
+res('data.user.name') // jsonpath-style query
+\`\`\`
+
+### Chai assertions (tests only)
+\`\`\`js
+expect(x).to.equal(y)
+expect(x).to.eql(y)
+expect(x).to.be.a('string')
+expect(x).to.have.property('p')
+expect(x).to.include(y)
+expect(x).to.have.lengthOf(n)
+expect(x).to.be.true / .false / .null
+expect(x).to.be.above(n) / .below(n)
+expect(x).to.match(/regex/)
+expect(x).to.exist
+\`\`\`
+`;
+
+const COMMON_OUTPUT_RULES = `## Output Rules
+
+Return ONLY raw JavaScript code that can be executed directly. No markdown fences, no backticks, no commentary, no preamble. Begin with the first line of code.
+
+If existing code was provided, return the COMPLETE updated script (your output replaces the entire file). Preserve any existing logic the user did not ask you to remove.`;
+
+const SCRIPT_PROMPTS = {
+ 'tests': `You are an AI assistant that writes test scripts for the Bruno API client.
+
+${BRUNO_API_REFERENCE}
+
+## Tests Context
+
+Tests run AFTER the response is received. Available globals: \`bru\`, \`req\`, \`res\`, \`test\`, \`expect\`.
+
+Wrap each assertion in a \`test()\` block:
+\`\`\`js
+test("status code is 200", function() {
+ expect(res.getStatus()).to.equal(200);
+});
+\`\`\`
+
+Common patterns:
+- Status: \`expect(res.getStatus()).to.equal(200)\`
+- Header: \`expect(res.getHeader('content-type')).to.include('application/json')\`
+- Body field: \`expect(res.getBody()).to.have.property('id')\`
+- JSON path: \`expect(res('data.user.id')).to.exist\`
+- Response time: \`expect(res.getResponseTime()).to.be.below(1000)\`
+
+Do NOT use \`bru.setEnvVar\` or other side-effecting calls inside tests — keep tests pure assertions.
+
+${COMMON_OUTPUT_RULES}`,
+
+ 'pre-request': `You are an AI assistant that writes pre-request scripts for the Bruno API client.
+
+${BRUNO_API_REFERENCE}
+
+## Pre-Request Context
+
+Pre-request scripts run BEFORE the HTTP request is sent. Available globals: \`bru\`, \`req\`.
+
+Common use cases:
+- Set headers: \`req.setHeader('Authorization', 'Bearer ' + bru.getEnvVar('token'))\`
+- Compute variables: \`bru.setVar('timestamp', Date.now())\`
+- Modify the URL, body, or method
+- Conditional logic before sending
+
+The \`res\` object is NOT available here — the response does not yet exist. Do NOT use \`test()\` or \`expect()\` — those belong in the Tests tab.
+
+${COMMON_OUTPUT_RULES}`,
+
+ 'post-response': `You are an AI assistant that writes post-response scripts for the Bruno API client.
+
+${BRUNO_API_REFERENCE}
+
+## Post-Response Context
+
+Post-response scripts run AFTER the HTTP response is received, before tests. Available globals: \`bru\`, \`req\`, \`res\`.
+
+Common use cases:
+- Extract data: \`bru.setEnvVar('token', res('data.token'))\`
+- Log: \`console.log('Status:', res.getStatus())\`
+- Conditional follow-up logic based on the response
+
+Do NOT use \`test()\` or \`expect()\` — those belong in the Tests tab.
+
+${COMMON_OUTPUT_RULES}`
+};
+
+const SCRIPT_TYPES = Object.keys(SCRIPT_PROMPTS);
+
+const formatRequestContext = (ctx) => {
+ if (!ctx) return '';
+ const parts = [];
+
+ if (ctx.url || ctx.method) {
+ parts.push(`Request: ${ctx.method || 'GET'} ${ctx.url || ''}`);
+ }
+
+ const headers = (ctx.headers || []).filter((h) => h?.enabled && h?.name);
+ if (headers.length) {
+ parts.push(`Headers:\n${headers.map((h) => ` ${h.name}: ${h.value ?? ''}`).join('\n')}`);
+ }
+
+ const params = (ctx.params || []).filter((p) => p?.enabled && p?.name);
+ if (params.length) {
+ parts.push(`Params:\n${params.map((p) => ` ${p.name}: ${p.value ?? ''}`).join('\n')}`);
+ }
+
+ const body = ctx.body;
+ if (body && body.mode && body.mode !== 'none') {
+ let bodyText = '';
+ if (body.mode === 'json') bodyText = body.json || '';
+ else if (body.mode === 'text') bodyText = body.text || '';
+ else if (body.mode === 'xml') bodyText = body.xml || '';
+ else if (body.mode === 'graphql') bodyText = body.graphql?.query || '';
+ if (bodyText) parts.push(`Body (${body.mode}):\n${bodyText.slice(0, 2000)}`);
+ }
+
+ return parts.join('\n\n');
+};
+
+const buildScriptUserPrompt = ({ userPrompt, currentScript, requestContext }) => {
+ const sections = [];
+ const contextStr = formatRequestContext(requestContext);
+ if (contextStr) sections.push(`HTTP Request Context\n${contextStr}`);
+ if (currentScript && currentScript.trim()) {
+ sections.push(`Existing Code\n\`\`\`js\n${currentScript}\n\`\`\``);
+ }
+ sections.push(`User Request\n${userPrompt}`);
+ return sections.join('\n\n');
+};
+
+const stripCodeFences = (text) => {
+ if (!text) return '';
+ let out = text.trim();
+ // strip leading code fence with optional language
+ out = out.replace(/^```[\w-]*\n?/, '');
+ // strip trailing code fence
+ out = out.replace(/\n?```\s*$/, '');
+ // drop "Here is..." style preambles
+ out = out.replace(/^(?:Here(?:'s| is| are)[^\n]*\n)+/i, '');
+ return out.replace(/^\n+/, '');
+};
+
+module.exports = {
+ SCRIPT_PROMPTS,
+ SCRIPT_TYPES,
+ buildScriptUserPrompt,
+ stripCodeFences
+};