@diyor28/qa-core 0.1.0
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.
- package/dist/agent/context/codecs/browser-state.codec.d.ts +33 -0
- package/dist/agent/context/codecs/browser-state.codec.d.ts.map +1 -0
- package/dist/agent/context/codecs/browser-state.codec.js +46 -0
- package/dist/agent/context/codecs/index.d.ts +3 -0
- package/dist/agent/context/codecs/index.d.ts.map +1 -0
- package/dist/agent/context/codecs/index.js +2 -0
- package/dist/agent/context/codecs/qa-scenario.codec.d.ts +21 -0
- package/dist/agent/context/codecs/qa-scenario.codec.d.ts.map +1 -0
- package/dist/agent/context/codecs/qa-scenario.codec.js +44 -0
- package/dist/agent/context/index.d.ts +4 -0
- package/dist/agent/context/index.d.ts.map +1 -0
- package/dist/agent/context/index.js +3 -0
- package/dist/agent/context/qa-context.service.d.ts +24 -0
- package/dist/agent/context/qa-context.service.d.ts.map +1 -0
- package/dist/agent/context/qa-context.service.js +53 -0
- package/dist/agent/context/qa-policy.d.ts +24 -0
- package/dist/agent/context/qa-policy.d.ts.map +1 -0
- package/dist/agent/context/qa-policy.js +47 -0
- package/dist/agent/loop.d.ts +28 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +111 -0
- package/dist/agent/system-prompt.d.ts +2 -0
- package/dist/agent/system-prompt.d.ts.map +1 -0
- package/dist/agent/system-prompt.js +39 -0
- package/dist/agent/tools/index.d.ts +5 -0
- package/dist/agent/tools/index.d.ts.map +1 -0
- package/dist/agent/tools/index.js +152 -0
- package/dist/browser/controller.d.ts +82 -0
- package/dist/browser/controller.d.ts.map +1 -0
- package/dist/browser/controller.js +289 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/report/builder.d.ts +23 -0
- package/dist/report/builder.d.ts.map +1 -0
- package/dist/report/builder.js +69 -0
- package/dist/runner.d.ts +10 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +143 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/vision/analyzer.d.ts +8 -0
- package/dist/vision/analyzer.d.ts.map +1 -0
- package/dist/vision/analyzer.js +49 -0
- package/dist/vision/prompts.d.ts +2 -0
- package/dist/vision/prompts.d.ts.map +1 -0
- package/dist/vision/prompts.js +28 -0
- package/package.json +49 -0
- package/src/agent/context/codecs/browser-state.codec.ts +57 -0
- package/src/agent/context/codecs/index.ts +2 -0
- package/src/agent/context/codecs/qa-scenario.codec.ts +55 -0
- package/src/agent/context/index.ts +3 -0
- package/src/agent/context/qa-context.service.ts +90 -0
- package/src/agent/context/qa-policy.ts +54 -0
- package/src/agent/loop.ts +147 -0
- package/src/agent/system-prompt.ts +39 -0
- package/src/agent/tools/index.ts +162 -0
- package/src/browser/controller.ts +321 -0
- package/src/index.ts +19 -0
- package/src/report/builder.ts +94 -0
- package/src/runner.ts +166 -0
- package/src/types.ts +68 -0
- package/src/vision/analyzer.ts +61 -0
- package/src/vision/prompts.ts +28 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
package/dist/runner.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { BrowserController } from './browser/controller.js';
|
|
2
|
+
import { VisionAnalyzer } from './vision/analyzer.js';
|
|
3
|
+
import { AgentLoop } from './agent/loop.js';
|
|
4
|
+
import { ReportBuilder } from './report/builder.js';
|
|
5
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
export class QaRunner {
|
|
8
|
+
config;
|
|
9
|
+
browserController = null;
|
|
10
|
+
visionAnalyzer = null;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
async run() {
|
|
15
|
+
const startTime = Date.now();
|
|
16
|
+
try {
|
|
17
|
+
// Initialize components
|
|
18
|
+
this.emitProgress('step_started', { step: 'initialization', message: 'Initializing QA runner' });
|
|
19
|
+
this.browserController = new BrowserController({
|
|
20
|
+
headless: this.config.browser.headless,
|
|
21
|
+
viewport: this.config.browser.viewport,
|
|
22
|
+
enableTrace: this.config.browser.enableTrace,
|
|
23
|
+
traceDir: this.config.browser.traceDir,
|
|
24
|
+
});
|
|
25
|
+
this.visionAnalyzer = new VisionAnalyzer(this.config.models.geminiApiKey);
|
|
26
|
+
await this.browserController.launch();
|
|
27
|
+
this.emitProgress('step_completed', { step: 'initialization', message: 'Browser launched successfully' });
|
|
28
|
+
// Run agent loop
|
|
29
|
+
this.emitProgress('step_started', { step: 'execution', message: 'Starting test execution' });
|
|
30
|
+
const agentLoop = new AgentLoop({
|
|
31
|
+
cerebrasApiKey: this.config.models.cerebrasApiKey,
|
|
32
|
+
browserController: this.browserController,
|
|
33
|
+
visionAnalyzer: this.visionAnalyzer,
|
|
34
|
+
scenarioName: this.config.scenario.name,
|
|
35
|
+
scenarioContent: this.config.scenario.content,
|
|
36
|
+
baseUrl: this.config.baseUrl,
|
|
37
|
+
defectsToCheck: this.config.defectsToCheck || ['Visual overflow', 'Bad UX', 'Poor contrast'],
|
|
38
|
+
maxIterations: this.config.maxIterations,
|
|
39
|
+
onProgress: this.config.onProgress,
|
|
40
|
+
});
|
|
41
|
+
const agentResult = await agentLoop.run();
|
|
42
|
+
this.emitProgress('step_completed', {
|
|
43
|
+
step: 'execution',
|
|
44
|
+
message: 'Test execution completed',
|
|
45
|
+
findings: agentResult.findings.length,
|
|
46
|
+
});
|
|
47
|
+
// Collect artifacts
|
|
48
|
+
this.emitProgress('step_started', { step: 'artifacts', message: 'Collecting artifacts' });
|
|
49
|
+
const artifacts = [];
|
|
50
|
+
// Add all screenshots from browser controller
|
|
51
|
+
const screenshots = this.browserController.getAllScreenshots();
|
|
52
|
+
for (const screenshot of screenshots) {
|
|
53
|
+
artifacts.push({
|
|
54
|
+
type: 'screenshot',
|
|
55
|
+
name: screenshot.name,
|
|
56
|
+
data: screenshot.buffer,
|
|
57
|
+
contentType: 'image/png',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Add trace file if enabled
|
|
61
|
+
if (this.config.browser.enableTrace && this.config.browser.traceDir) {
|
|
62
|
+
try {
|
|
63
|
+
const traceFiles = readdirSync(this.config.browser.traceDir).filter((f) => f.startsWith('trace-'));
|
|
64
|
+
if (traceFiles.length > 0) {
|
|
65
|
+
// Get the most recent trace file
|
|
66
|
+
const latestTrace = traceFiles.sort().pop();
|
|
67
|
+
const tracePath = join(this.config.browser.traceDir, latestTrace);
|
|
68
|
+
const traceBuffer = readFileSync(tracePath);
|
|
69
|
+
artifacts.push({
|
|
70
|
+
type: 'trace',
|
|
71
|
+
name: latestTrace,
|
|
72
|
+
data: traceBuffer,
|
|
73
|
+
contentType: 'application/zip',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.warn('[QaRunner] Failed to collect trace file:', error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this.emitProgress('step_completed', {
|
|
82
|
+
step: 'artifacts',
|
|
83
|
+
message: 'Artifacts collected',
|
|
84
|
+
count: artifacts.length,
|
|
85
|
+
});
|
|
86
|
+
// Build report
|
|
87
|
+
const duration = Date.now() - startTime;
|
|
88
|
+
const report = ReportBuilder.buildReport({
|
|
89
|
+
findings: agentResult.findings,
|
|
90
|
+
artifacts,
|
|
91
|
+
metadata: {
|
|
92
|
+
scenarioName: this.config.scenario.name,
|
|
93
|
+
baseUrl: this.config.baseUrl,
|
|
94
|
+
viewport: this.config.browser.viewport,
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
},
|
|
97
|
+
duration,
|
|
98
|
+
summary: agentResult.summary,
|
|
99
|
+
});
|
|
100
|
+
return report;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error('[QaRunner] Error during test execution:', error);
|
|
104
|
+
// Build error report
|
|
105
|
+
const duration = Date.now() - startTime;
|
|
106
|
+
return ReportBuilder.buildReport({
|
|
107
|
+
findings: [
|
|
108
|
+
{
|
|
109
|
+
severity: 'critical',
|
|
110
|
+
title: 'Test execution failed',
|
|
111
|
+
description: `Error during test execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
+
reproSteps: ['Test runner encountered an error'],
|
|
113
|
+
category: 'functional',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
artifacts: [],
|
|
117
|
+
metadata: {
|
|
118
|
+
scenarioName: this.config.scenario.name,
|
|
119
|
+
baseUrl: this.config.baseUrl,
|
|
120
|
+
viewport: this.config.browser.viewport,
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
},
|
|
123
|
+
duration,
|
|
124
|
+
summary: `Test execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
// Always close browser
|
|
129
|
+
if (this.browserController) {
|
|
130
|
+
await this.browserController.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
emitProgress(type, data) {
|
|
135
|
+
if (this.config.onProgress) {
|
|
136
|
+
this.config.onProgress({
|
|
137
|
+
type,
|
|
138
|
+
data,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface QaScenario {
|
|
2
|
+
name: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface QaFinding {
|
|
6
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
reproSteps: string[];
|
|
10
|
+
expected?: string;
|
|
11
|
+
actual?: string;
|
|
12
|
+
screenshotName?: string;
|
|
13
|
+
category: 'functional' | 'ux' | 'ui' | 'accessibility';
|
|
14
|
+
}
|
|
15
|
+
export interface QaArtifact {
|
|
16
|
+
type: 'screenshot' | 'trace';
|
|
17
|
+
name: string;
|
|
18
|
+
data: Buffer;
|
|
19
|
+
contentType: string;
|
|
20
|
+
}
|
|
21
|
+
export interface QaReport {
|
|
22
|
+
summary: string;
|
|
23
|
+
status: 'passed' | 'failed' | 'issues_found';
|
|
24
|
+
duration: number;
|
|
25
|
+
findings: QaFinding[];
|
|
26
|
+
artifacts: QaArtifact[];
|
|
27
|
+
metadata: {
|
|
28
|
+
scenarioName: string;
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
viewport: {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
};
|
|
34
|
+
timestamp: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface QaProgressEvent {
|
|
38
|
+
type: 'step_started' | 'step_completed' | 'screenshot_captured' | 'finding_discovered' | 'context_usage';
|
|
39
|
+
data: Record<string, unknown>;
|
|
40
|
+
timestamp: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ScreenshotEntry {
|
|
43
|
+
timestamp: number;
|
|
44
|
+
name: string;
|
|
45
|
+
buffer: Buffer;
|
|
46
|
+
context: string;
|
|
47
|
+
url: string;
|
|
48
|
+
}
|
|
49
|
+
export interface QaRunnerConfig {
|
|
50
|
+
browser: {
|
|
51
|
+
headless?: boolean;
|
|
52
|
+
viewport: {
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
};
|
|
56
|
+
enableTrace?: boolean;
|
|
57
|
+
traceDir?: string;
|
|
58
|
+
};
|
|
59
|
+
models: {
|
|
60
|
+
cerebrasApiKey: string;
|
|
61
|
+
geminiApiKey: string;
|
|
62
|
+
};
|
|
63
|
+
baseUrl: string;
|
|
64
|
+
scenario: QaScenario;
|
|
65
|
+
defectsToCheck?: string[];
|
|
66
|
+
maxIterations?: number;
|
|
67
|
+
onProgress?: (event: QaProgressEvent) => void;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,eAAe,CAAC;CACxD;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,cAAc,CAAC;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,QAAQ,EAAE;QACR,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5C,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,cAAc,GAAG,gBAAgB,GAAG,qBAAqB,GAAG,oBAAoB,GAAG,eAAe,CAAC;IACzG,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE;QACP,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,MAAM,EAAE;QACN,cAAc,EAAE,MAAM,CAAC;QACvB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,UAAU,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;CAC/C"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { QaFinding } from '../types.js';
|
|
2
|
+
export declare class VisionAnalyzer {
|
|
3
|
+
private genAI;
|
|
4
|
+
private model;
|
|
5
|
+
constructor(apiKey: string);
|
|
6
|
+
analyzeScreenshot(screenshot: Buffer, context: string, url: string): Promise<QaFinding[]>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/vision/analyzer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,KAAK,CAAM;gBAEP,MAAM,EAAE,MAAM;IAOpB,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,SAAS,EAAE,CAAC;CAyCxB"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
import { buildVisionPrompt } from './prompts.js';
|
|
3
|
+
export class VisionAnalyzer {
|
|
4
|
+
genAI;
|
|
5
|
+
model;
|
|
6
|
+
constructor(apiKey) {
|
|
7
|
+
this.genAI = new GoogleGenerativeAI(apiKey);
|
|
8
|
+
this.model = this.genAI.getGenerativeModel({
|
|
9
|
+
model: 'gemini-3-flash-preview',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
async analyzeScreenshot(screenshot, context, url) {
|
|
13
|
+
try {
|
|
14
|
+
const base64Image = screenshot.toString('base64');
|
|
15
|
+
const prompt = buildVisionPrompt(context, url);
|
|
16
|
+
const result = await this.model.generateContent([
|
|
17
|
+
{
|
|
18
|
+
inlineData: {
|
|
19
|
+
mimeType: 'image/png',
|
|
20
|
+
data: base64Image,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{ text: prompt },
|
|
24
|
+
]);
|
|
25
|
+
const response = await result.response;
|
|
26
|
+
const text = response.text();
|
|
27
|
+
// Parse JSON response
|
|
28
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
29
|
+
if (!jsonMatch) {
|
|
30
|
+
console.warn('[VisionAnalyzer] No JSON found in response:', text);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
34
|
+
const findings = parsed.findings || [];
|
|
35
|
+
// Convert vision findings to QaFinding format
|
|
36
|
+
return findings.map((finding) => ({
|
|
37
|
+
severity: finding.severity || 'medium',
|
|
38
|
+
title: finding.title,
|
|
39
|
+
description: `${finding.description}\n\nSuggestion: ${finding.suggestion || 'N/A'}`,
|
|
40
|
+
reproSteps: [`Screenshot captured at: ${url}`, `Context: ${context}`],
|
|
41
|
+
category: finding.category,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error('[VisionAnalyzer] Error analyzing screenshot:', error);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.d.ts","sourceRoot":"","sources":["../../src/vision/prompts.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CA2BtE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function buildVisionPrompt(context, url) {
|
|
2
|
+
return `You are a ruthless UX/UI critic analyzing a screenshot of a web application.
|
|
3
|
+
|
|
4
|
+
Context: ${context}
|
|
5
|
+
URL: ${url}
|
|
6
|
+
|
|
7
|
+
Analyze this screenshot for:
|
|
8
|
+
- Visual overflow (content cut off, horizontal scroll)
|
|
9
|
+
- Poor color contrast (WCAG AA violations)
|
|
10
|
+
- Bad UX (confusing navigation, hidden CTAs, poor hierarchy)
|
|
11
|
+
- Accessibility issues (missing labels, poor focus indicators)
|
|
12
|
+
|
|
13
|
+
Return findings as JSON array with structure:
|
|
14
|
+
{
|
|
15
|
+
"findings": [
|
|
16
|
+
{
|
|
17
|
+
"category": "ux" | "ui" | "accessibility",
|
|
18
|
+
"severity": "low" | "medium" | "high",
|
|
19
|
+
"title": "Short title",
|
|
20
|
+
"description": "Detailed description",
|
|
21
|
+
"suggestion": "How to fix"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Be concise and specific. Only report actual issues you can clearly see in the screenshot.
|
|
27
|
+
If everything looks good, return an empty findings array.`;
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diyor28/qa-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Core library for AI-powered browser testing with Playwright, GLM-4.7, and Gemini vision analysis",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"qa",
|
|
8
|
+
"testing",
|
|
9
|
+
"playwright",
|
|
10
|
+
"ai",
|
|
11
|
+
"browser-automation",
|
|
12
|
+
"e2e"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/iota-uz/foundry.git",
|
|
17
|
+
"directory": "packages/qa-core"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@ai-sdk/openai": "^1.0.10",
|
|
34
|
+
"@google/generative-ai": "^0.21.0",
|
|
35
|
+
"ai": "^4.0.61",
|
|
36
|
+
"playwright": "^1.49.0",
|
|
37
|
+
"zod": "^3.24.1",
|
|
38
|
+
"@diyor28/context": "1.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.11.5",
|
|
42
|
+
"typescript": "^5.3.3"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc",
|
|
46
|
+
"dev": "tsc --watch",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { BlockCodec, RenderedContent, ContextBlock } from '@diyor28/context';
|
|
3
|
+
import { defaultHash, sortObjectKeys } from '@diyor28/context';
|
|
4
|
+
|
|
5
|
+
export const BrowserStatePayloadSchema = z.object({
|
|
6
|
+
currentUrl: z.string(),
|
|
7
|
+
viewport: z.object({
|
|
8
|
+
width: z.number(),
|
|
9
|
+
height: z.number(),
|
|
10
|
+
}),
|
|
11
|
+
screenshotCount: z.number(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type BrowserStatePayload = z.infer<typeof BrowserStatePayloadSchema>;
|
|
15
|
+
|
|
16
|
+
export const BrowserStateCodec: BlockCodec<BrowserStatePayload> = {
|
|
17
|
+
codecId: 'qa:browser-state',
|
|
18
|
+
version: '1.0.0',
|
|
19
|
+
payloadSchema: BrowserStatePayloadSchema,
|
|
20
|
+
|
|
21
|
+
canonicalize(payload: BrowserStatePayload): unknown {
|
|
22
|
+
return sortObjectKeys(payload);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
hash(canonicalized: unknown): string {
|
|
26
|
+
return defaultHash(canonicalized);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
render(block: ContextBlock<BrowserStatePayload>): RenderedContent {
|
|
30
|
+
const { currentUrl, viewport, screenshotCount } = block.payload;
|
|
31
|
+
|
|
32
|
+
const content = `## Browser State
|
|
33
|
+
|
|
34
|
+
Current URL: ${currentUrl}
|
|
35
|
+
Viewport: ${viewport.width}x${viewport.height}
|
|
36
|
+
Screenshots captured: ${screenshotCount}`;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
anthropic: {
|
|
40
|
+
role: 'user',
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: content,
|
|
45
|
+
cache_control: { type: 'ephemeral' }, // State blocks use ephemeral cache
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
openai: { role: 'user', content },
|
|
50
|
+
gemini: { role: 'user', parts: [{ text: content }] },
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
validate(payload: unknown): BrowserStatePayload {
|
|
55
|
+
return BrowserStatePayloadSchema.parse(payload);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { BlockCodec, RenderedContent, ContextBlock } from '@diyor28/context';
|
|
3
|
+
import { defaultHash, sortObjectKeys } from '@diyor28/context';
|
|
4
|
+
|
|
5
|
+
export const QaScenarioPayloadSchema = z.object({
|
|
6
|
+
scenarioName: z.string(),
|
|
7
|
+
instructions: z.string(),
|
|
8
|
+
baseUrl: z.string().url(),
|
|
9
|
+
defectsToCheck: z.array(z.string()),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type QaScenarioPayload = z.infer<typeof QaScenarioPayloadSchema>;
|
|
13
|
+
|
|
14
|
+
export const QaScenarioCodec: BlockCodec<QaScenarioPayload> = {
|
|
15
|
+
codecId: 'qa:scenario',
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
payloadSchema: QaScenarioPayloadSchema,
|
|
18
|
+
|
|
19
|
+
canonicalize(payload: QaScenarioPayload): unknown {
|
|
20
|
+
return sortObjectKeys({
|
|
21
|
+
scenarioName: payload.scenarioName.trim(),
|
|
22
|
+
instructions: payload.instructions.trim(),
|
|
23
|
+
baseUrl: payload.baseUrl,
|
|
24
|
+
defectsToCheck: [...payload.defectsToCheck].sort(),
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
hash(canonicalized: unknown): string {
|
|
29
|
+
return defaultHash(canonicalized);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
render(block: ContextBlock<QaScenarioPayload>): RenderedContent {
|
|
33
|
+
const { scenarioName, instructions, baseUrl, defectsToCheck } = block.payload;
|
|
34
|
+
|
|
35
|
+
const content = `# Test Scenario: ${scenarioName}
|
|
36
|
+
|
|
37
|
+
**Target URL:** ${baseUrl}
|
|
38
|
+
|
|
39
|
+
## Instructions
|
|
40
|
+
${instructions}
|
|
41
|
+
|
|
42
|
+
## Defects to Check
|
|
43
|
+
${defectsToCheck.map((d) => `- ${d}`).join('\n')}`;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
anthropic: { role: 'user', content },
|
|
47
|
+
openai: { role: 'user', content },
|
|
48
|
+
gemini: { role: 'user', parts: [{ text: content }] },
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
validate(payload: unknown): QaScenarioPayload {
|
|
53
|
+
return QaScenarioPayloadSchema.parse(payload);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContextBuilder,
|
|
3
|
+
type ContextPolicy,
|
|
4
|
+
type OpenAICompiledContext,
|
|
5
|
+
type BlockCodec,
|
|
6
|
+
compileOpenAIContext,
|
|
7
|
+
OpenAITokenEstimator,
|
|
8
|
+
ConversationHistoryCodec,
|
|
9
|
+
type ConversationMessage,
|
|
10
|
+
SystemRulesCodec,
|
|
11
|
+
} from '@diyor28/context';
|
|
12
|
+
import { QaScenarioCodec, BrowserStateCodec } from './codecs/index.js';
|
|
13
|
+
import { QA_POLICY } from './qa-policy.js';
|
|
14
|
+
|
|
15
|
+
export interface BuildQaContextOptions {
|
|
16
|
+
scenarioName: string;
|
|
17
|
+
scenarioInstructions: string;
|
|
18
|
+
baseUrl: string;
|
|
19
|
+
defectsToCheck: string[];
|
|
20
|
+
browserState: {
|
|
21
|
+
currentUrl: string;
|
|
22
|
+
viewport: { width: number; height: number };
|
|
23
|
+
screenshotCount: number;
|
|
24
|
+
};
|
|
25
|
+
conversationHistory: ConversationMessage[];
|
|
26
|
+
systemPrompt: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class QaContextService {
|
|
30
|
+
private readonly tokenEstimator: OpenAITokenEstimator;
|
|
31
|
+
private readonly codecRegistry: Map<string, BlockCodec<unknown>>;
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
// OpenAI token estimator (compatible with Cerebras/GLM-4.7)
|
|
35
|
+
this.tokenEstimator = new OpenAITokenEstimator('gpt-4');
|
|
36
|
+
|
|
37
|
+
// Build codec registry
|
|
38
|
+
this.codecRegistry = new Map<string, BlockCodec<unknown>>([
|
|
39
|
+
[SystemRulesCodec.codecId, SystemRulesCodec as BlockCodec<unknown>],
|
|
40
|
+
[QaScenarioCodec.codecId, QaScenarioCodec as BlockCodec<unknown>],
|
|
41
|
+
[BrowserStateCodec.codecId, BrowserStateCodec as BlockCodec<unknown>],
|
|
42
|
+
[ConversationHistoryCodec.codecId, ConversationHistoryCodec as BlockCodec<unknown>],
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async buildContext(options: BuildQaContextOptions): Promise<OpenAICompiledContext> {
|
|
47
|
+
const builder = new ContextBuilder();
|
|
48
|
+
|
|
49
|
+
// System prompt (pinned - always at top)
|
|
50
|
+
builder.system({
|
|
51
|
+
text: options.systemPrompt,
|
|
52
|
+
cacheable: false, // OpenAI doesn't have prompt caching yet
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// QA scenario (reference - test instructions)
|
|
56
|
+
builder.reference(QaScenarioCodec, {
|
|
57
|
+
scenarioName: options.scenarioName,
|
|
58
|
+
instructions: options.scenarioInstructions,
|
|
59
|
+
baseUrl: options.baseUrl,
|
|
60
|
+
defectsToCheck: options.defectsToCheck,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Browser state (state - current browser info)
|
|
64
|
+
builder.state(BrowserStateCodec, options.browserState);
|
|
65
|
+
|
|
66
|
+
// Conversation history (if any)
|
|
67
|
+
if (options.conversationHistory.length > 0) {
|
|
68
|
+
builder.history(options.conversationHistory);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get graph and create view
|
|
72
|
+
const graph = builder.getGraph();
|
|
73
|
+
const maxTokens = QA_POLICY.contextWindow - QA_POLICY.completionReserve;
|
|
74
|
+
|
|
75
|
+
const view = await graph.createView({
|
|
76
|
+
tokenEstimator: this.tokenEstimator,
|
|
77
|
+
maxTokens,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Compile to OpenAI format
|
|
81
|
+
const compiled = compileOpenAIContext([...view.blocks], QA_POLICY, {
|
|
82
|
+
codecRegistry: this.codecRegistry,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Log context usage (for observability)
|
|
86
|
+
console.log(`[QA Context] Tokens: ${compiled.estimatedTokens}, Blocks: ${view.blocks.length}`);
|
|
87
|
+
|
|
88
|
+
return compiled;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ContextPolicy } from '@diyor28/context';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QA Context Policy
|
|
5
|
+
*
|
|
6
|
+
* Defines context management strategy for QA agent execution.
|
|
7
|
+
* Optimized for browser automation with tool outputs and screenshot management.
|
|
8
|
+
*
|
|
9
|
+
* ## Context Structure
|
|
10
|
+
*
|
|
11
|
+
* 1. System prompt (pinned)
|
|
12
|
+
* 2. QA scenario (reference - test instructions)
|
|
13
|
+
* 3. Browser state (state - current URL, viewport)
|
|
14
|
+
* 4. Tool outputs (tool_output - navigation, clicks, screenshots)
|
|
15
|
+
* 5. Conversation history (history - agent reasoning)
|
|
16
|
+
*
|
|
17
|
+
* ## Compaction Triggers
|
|
18
|
+
*
|
|
19
|
+
* When context exceeds available tokens:
|
|
20
|
+
* - Old tool outputs are pruned (keep last 5)
|
|
21
|
+
* - History is summarized
|
|
22
|
+
* - State blocks are never truncated
|
|
23
|
+
*/
|
|
24
|
+
export const QA_POLICY: ContextPolicy = {
|
|
25
|
+
provider: 'openai', // Cerebras is OpenAI-compatible
|
|
26
|
+
modelId: 'glm-4.7', // GLM-4.7 via Cerebras
|
|
27
|
+
|
|
28
|
+
contextWindow: 200_000, // GLM-4.7 context window
|
|
29
|
+
completionReserve: 8_000, // Reserve for agent response
|
|
30
|
+
|
|
31
|
+
overflowStrategy: 'compact', // Auto-compact old tool outputs
|
|
32
|
+
|
|
33
|
+
kindPriorities: [
|
|
34
|
+
{ kind: 'pinned', minTokens: 500, maxTokens: 2000, truncatable: false },
|
|
35
|
+
{ kind: 'reference', minTokens: 500, maxTokens: 3000, truncatable: false },
|
|
36
|
+
{ kind: 'state', minTokens: 200, maxTokens: 1000, truncatable: false },
|
|
37
|
+
{ kind: 'tool_output', minTokens: 1000, maxTokens: 50000, truncatable: true },
|
|
38
|
+
{ kind: 'history', minTokens: 2000, maxTokens: 100000, truncatable: true },
|
|
39
|
+
{ kind: 'turn', minTokens: 500, maxTokens: 5000, truncatable: false },
|
|
40
|
+
],
|
|
41
|
+
|
|
42
|
+
compaction: {
|
|
43
|
+
pruneToolOutputs: true,
|
|
44
|
+
maxToolOutputAge: 3600, // 1 hour
|
|
45
|
+
maxToolOutputsPerKind: 5, // Keep last 5 tool outputs
|
|
46
|
+
summarizeHistory: true,
|
|
47
|
+
maxHistoryMessages: 20, // Keep last 20 turns
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
sensitivity: {
|
|
51
|
+
maxSensitivity: 'public',
|
|
52
|
+
redactRestricted: true,
|
|
53
|
+
},
|
|
54
|
+
};
|