Skip to content

Commit 5340afb

Browse files
committed
feat: Add QA Subagent Profile leveraging Local Gemma4 Swarm (#9749)
1 parent 8eb2eb3 commit 5340afb

4 files changed

Lines changed: 160 additions & 3 deletions

File tree

ai/Agent.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class Agent extends Base {
5252
*/
5353
subAgents: {
5454
browser : async () => (await import('./agent/profile/Browser.mjs')).default,
55-
librarian: async () => (await import('./agent/profile/Librarian.mjs')).default
55+
librarian: async () => (await import('./agent/profile/Librarian.mjs')).default,
56+
qa : async () => (await import('./agent/profile/QA.mjs')).default
5657
}
5758
}
5859

ai/agent/profile/QA.mjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Agent from '../../Agent.mjs';
2+
3+
/**
4+
* A specialized sub-agent for generating and validating automated test coverage.
5+
* Configured by default to execute locally ensuring zero-cost test suite scaling.
6+
*
7+
* @class Neo.ai.agent.profile.QA
8+
* @extends Neo.ai.Agent
9+
*/
10+
class QA extends Agent {
11+
static config = {
12+
/**
13+
* @member {String} className='Neo.ai.agent.profile.QA'
14+
* @protected
15+
*/
16+
className: 'Neo.ai.agent.profile.QA',
17+
/**
18+
* The QA Bot executes natively on the local swarm to prevent token explosion.
19+
* Default Provider is Ollama, targeting the workspace's default local inference daemon.
20+
* @member {String|Neo.ai.provider.Base} modelProvider='ollama'
21+
*/
22+
modelProvider: 'ollama',
23+
/**
24+
* @member {String} model='gemma4'
25+
*/
26+
model: 'gemma4',
27+
/**
28+
* The QA agent needs context matching capabilities to assert components,
29+
* so we attach the knowledge base.
30+
* @member {String[]} servers=['knowledge-base']
31+
*/
32+
systemPrompt: `You are the QA Subagent, a strict Quality Assurance automation engineer specializing in Neo.mjs and Playwright.
33+
Your responsibility is to analyze given class designs or code implementations, and output highly robust, isolated unit tests using Playwright syntax inside the Neo context.
34+
35+
CRITICAL NEO.MJS UNIT TESTING RULES:
36+
1. NEVER use generic Playwright page navigation (e.g. page.goto). Tests run in Node.js using "Single-Thread Simulation".
37+
2. You MUST import the setup block:
38+
import {setup} from '../../setup.mjs';
39+
setup({ neoConfig: { allowVdomUpdatesInTests: true, unitTestMode: true, useDomApiRenderer: true }, appConfig: { name: 'TestApp', isMounted: () => true, vnodeInitialising: false }});
40+
3. You MUST import the Neo framework and augment the core environment:
41+
import Neo from '../../../../src/Neo.mjs';
42+
import * as core from '../../../../src/core/_export.mjs';
43+
4. To test a Component VDOM rendering, import the renderer and engine:
44+
import DomApiVnodeCreator from '../../../../src/vdom/util/DomApiVnodeCreator.mjs';
45+
import VdomHelper from '../../../../src/vdom/Helper.mjs';
46+
5. Always create components using Neo.create:
47+
componentInstance = Neo.create(Button, { iconCls: 'fa fa-user', text: 'Hello' });
48+
6. Always manually trigger virtual mounting:
49+
const { vnode } = await componentInstance.initVnode();
50+
componentInstance.mounted = true;
51+
7. Always perform state mutation tests using the generic set() API to trigger automated VDOM loops (do NOT hallucinate internal updateVnode methods):
52+
await componentInstance.set({ text: 'Update' });
53+
await VdomHelper.render(componentInstance.vnode, container);
54+
8. Always destroy instances in test.afterEach().
55+
56+
Never write boilerplate placeholders. ALWAYS use exhaustive assertions. If you cannot test something, explicitly fail and state why.`
57+
}
58+
}
59+
60+
export default Neo.setupClass(QA);

ai/provider/Ollama.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ class OllamaProvider extends Base {
2020
*/
2121
host: 'http://127.0.0.1:11434',
2222
/**
23-
* @member {String} modelName='gemma-4'
23+
* @member {String} modelName='gemma4'
2424
*/
25-
modelName: 'gemma-4',
25+
modelName: 'gemma4',
2626
/**
2727
* @member {String[]} requiredEnv=[]
2828
*/
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {setup} from '../../../setup.mjs';
2+
3+
const appName = 'QAAgentTest';
4+
5+
setup({
6+
neoConfig: {
7+
allowVdomUpdatesInTests: false,
8+
unitTestMode : true,
9+
useDomApiRenderer : false
10+
},
11+
appConfig: {
12+
name : appName,
13+
isMounted : () => true,
14+
vnodeInitialising: false
15+
}
16+
});
17+
18+
import {test, expect} from '@playwright/test';
19+
import Neo from '../../../../../src/Neo.mjs';
20+
import * as core from '../../../../../src/core/_export.mjs';
21+
import Agent from '../../../../../ai/Agent.mjs';
22+
import Assembler from '../../../../../ai/context/Assembler.mjs';
23+
import path from 'path';
24+
import {fileURLToPath} from 'url';
25+
import dotenv from 'dotenv';
26+
27+
const __filename = fileURLToPath(import.meta.url);
28+
const __dirname = path.dirname(__filename);
29+
dotenv.config({path: path.resolve(__dirname, '../../../../../.env'), quiet: true});
30+
31+
test.describe('QA Sub-Agent Swarm Node', () => {
32+
let primaryAgent;
33+
let originalAssemble;
34+
35+
test.beforeAll(() => {
36+
originalAssemble = Assembler.prototype.assemble;
37+
// Mock the ContextAssembler across the entire Swarm to prevent
38+
// offline Test nodes from trying to query the live MCP Vector Databases.
39+
Assembler.prototype.assemble = async function({systemPrompt, userQuery}) {
40+
return {
41+
system: systemPrompt,
42+
messages: [{ role: 'user', content: userQuery }]
43+
};
44+
};
45+
});
46+
47+
test.afterAll(async () => {
48+
Assembler.prototype.assemble = originalAssemble;
49+
if (primaryAgent) {
50+
await primaryAgent.disconnect();
51+
}
52+
});
53+
54+
test('Primary Agent delegates unit testing task to QA via Gemma4/Ollama', async () => {
55+
// Skip test in CI environments or if we explicitly don't have a local daemon
56+
// Since this relies on a local Ollama daemon hosting gemma-4, we just let it run locally
57+
// to evaluate the Swarm's intelligence.
58+
test.setTimeout(120000); // Give Gemma4 up to 2 minutes to generate the test code
59+
60+
// The primary orchestrator boots up
61+
primaryAgent = Neo.create(Agent, {
62+
servers: []
63+
});
64+
65+
await primaryAgent.initAsync();
66+
67+
const request = `
68+
Analyze the component 'Neo.button.Base'.
69+
Write a 100% complete Playwright test suite for it.
70+
Focus exclusively on DOM mounting and testing the 'text' configuration property mutability.
71+
Return ONLY the code block and nothing else.
72+
`;
73+
74+
// We DO NOT mock the delegate method here. We want to actually hit the local Gemma-4 node
75+
// through the Ollama provider to evaluate its ability to follow the System Prompt rules.
76+
console.log('Sending request to local Gemma4 Swarm node...');
77+
const generatedCode = await primaryAgent.delegate('qa', request);
78+
79+
// Basic sanity checks on the raw AI output string to ensure it adhered to the Neo.mjs Testing Primitives
80+
expect(typeof generatedCode).toBe('string');
81+
expect(generatedCode.length).toBeGreaterThan(100);
82+
83+
// Core assertions to ensure Gemma4 learned the rules from its System Prompt
84+
expect(generatedCode).toContain('import {setup} from');
85+
expect(generatedCode).toContain("import Neo from '../../../../src/Neo.mjs'");
86+
expect(generatedCode).toContain('DomApiVnodeCreator');
87+
expect(generatedCode).toContain('Neo.create(');
88+
expect(generatedCode).toContain('initVnode()');
89+
expect(generatedCode).toContain('.mounted = true');
90+
expect(generatedCode).toContain('.set({');
91+
92+
console.log('\n--- Gemma4 QA Output Start ---');
93+
console.log(generatedCode);
94+
console.log('--- Gemma4 QA Output End ---\n');
95+
});
96+
});

0 commit comments

Comments
 (0)